前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >I2C总线架构 之 设备驱动

I2C总线架构 之 设备驱动

作者头像
开源519
发布2020-08-17 00:16:27
1.8K0
发布2020-08-17 00:16:27
举报
文章被收录于专栏:开源519开源519

引言

I2C设备驱动是I2C框架中最接近应用层的,其上接应用层,下接I2C核心。也是驱动开发人员需要实现的代码,在此驱动中我们只需负责以下步骤(以ap3216c为例):

a. 添加硬件信息(设备树)

b. 搭建驱动框架

c. 构建i2c_driver,并注册到linux i2c中

d. 注册字符设备

e. 向应用层提供i2c设备操作接口

f. 注销i2c设备

本篇文章会按照以上六个阶段展开解析。

流程解析

a. 添加硬件信息设备树(设备树)

首先观察硬件i2c设备挂载到哪个i2c总线上,然后在设备树文件找到该总线的设备节点,在节点下创建子节点描述i2c设备硬件信息即可。

&i2c1 {
  clock-frequency = <100000>;
  pinctrl-names = "default";
  pinctrl-0 = <&pinctrl_i2c1>;
  status = "okay";

  ap3216c@1e {
    compatible = "100ask,ap3216c";
    reg = <0x1e>;
  };
};

b. 搭建驱动框架

所谓搭建驱动框架,无非就是字符驱动将驱动入口、出口、以及对应用层的接口实现。与其他字符驱动的搭建是一样的。

c. 构建i2c_driver,并注册到linux

首先先看需要构建的i2c_driver结构体原型:

struct i2c_driver {
 unsigned int class;

 int (*attach_adapter)(struct i2c_adapter *) __deprecated;
 int (*probe)(struct i2c_client *, const struct i2c_device_id *);
 int (*remove)(struct i2c_client *);
 void (*shutdown)(struct i2c_client *);
 void (*alert)(struct i2c_client *, enum i2c_alert_protocol protocol,
        unsigned int data);

 int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);

 struct device_driver driver;
 const struct i2c_device_id *id_table;

 int (*detect)(struct i2c_client *, struct i2c_board_info *);
 const unsigned short *address_list;
 struct list_head clients;
};

注: i2c_driver类似于platform_driver,在i2c_driver注册到内核且名称与设备树匹配一致就会进入probe中,在要卸载该驱动时会进入remove中。因此要填充i2c_driver的入口函数probe、出口函数(remove)和用于匹配的硬件信息driver。

static struct i2c_driver ap3216c_device_driver = {
 .probe  = ap3216c_probe,
 .remove = ap3216c_remove,
 .driver = {
  .name = PLATFORM_NAME,
  .owner = THIS_MODULE,
  .of_match_table = ap3216c_table,
   },
 .id_table = ap3216c_id,
};

d. 注册i2c设备

static int __init ap3216c_init(void)
{
    int ret = 0;
    printk("%s:%d: Entry %s \r\n", __FILE__, __LINE__, __func__);

 ret = i2c_add_driver(&ap3216c_device_driver);
 
 return ret;
}

注册i2c设备很简单,只需要在初始化中调用Linux提供的宏i2c_add_driver 即可。但是i2c_add_driver具体如何实现,有必要了解一下:

首先,这个宏调用了i2c_register_driver:

#define i2c_add_driver(driver) \
 i2c_register_driver(THIS_MODULE, driver)

再大致了解i2c_register_driver函数流程:

--- drivers ---  i2c_core.c --- i2c_register_driver(         --- driver->driver.bus = &i2c_bus_type
                                    |    struct module *owner,       |- INIT_LIST_HEAD(&driver->clients)
                                    |    struct i2c_driver *driver)  |- driver_register(&driver->driver)
                                    |                                |- i2c_for_each_dev(driver, __process_new_driver)
                                    |- i2c_for_each_dev(                    --- bus_for_each_dev(&i2c_bus_type, NULL, data, fn)
                                    |    void *data,
                                    |    int (*fn)(struct device *, void *))
                                    |- __process_new_driver( --- if (dev->type != &i2c_adapter_type)
                                         struct device *dev,  |      return 0;
                                         void *data)          |- i2c_do_add_adapter(data, to_i2c_adapter(dev));

小结:

从上图流程来看,在将i2c结构体注册进内核时,调用了属于i2c核心的i2c_register_driver。进入i2c核心中,会将i2c结构体添加到i2c链表中,并实现i2c_client与i2c_driver的匹配,匹配成功会进入i2c_driver 结构体的probe函数中。(具体实现放在I2C核心文章分析)

e. 向应用层提供i2c设备操作接口

成功进入probe函数后,就说明i2c驱动配置基本成功。接下来在probe中需要实现字符驱动的注册,以及实现对外的读写接口。字符驱动的注册代码,与其他字符驱动是一致的,浏览代码实现即可。主要分析对外接口的读写i2c设备操作:

在单片机的程序中,实现对i2c设备的读写,需要手动实现读写i2c寄存器,或者通过GPIO模拟i2c时序与i2c设备通信。而在Linux中,如何与i2c设备的具体通信已经被封装成固定的API,在程序中填充这些API的数据参数调用即可,列举读写单个字节的实现:

static int ap3216c_read_regs(struct sap3216c_dev *dev, unsigned char reg, 
                                              void *val, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = (struct i2c_client *)dev->private_data;
    
    msg[0].addr = client->addr;
    msg[0].flags = 0;
    msg[0].buf = &reg;
    msg[0].len = 1;

    msg[1].addr = client->addr;
    msg[1].flags = I2C_M_RD;
    msg[1].buf = val;
    msg[1].len = len;

    ret = i2c_transfer(client->adapter, msg, 2);
    if (ret ==2 ) {
        ret = 0;    
    } else {
        printk("%s %d: i2c transfer error!\n ", __func__, __LINE__);
        ret = -EREMOTEIO;    
    }    

    return ret;
}

static int ap3216c_write_regs(struct sap3216c_dev *dev, unsigned char reg, 
                                   unsigned char *buf, unsigned char len) 
{
    unsigned char temp_buf[256];
    struct i2c_msg msg;
    struct i2c_client *client = (struct i2c_client *)dev->private_data;

    temp_buf[0] = reg;
    memcpy(&temp_buf[1], buf, len);
    
    msg.addr = client->addr;
    msg.flags = 0;
    msg.buf = temp_buf;
    msg.len = len+1;

    return i2c_transfer(client->adapter, &msg, 1);
}

小结:

由以上代码发现,在与i2c设备的读写通信中,都是通过调用i2c_transfer实现。

i2c_transfer三个参数意义 :

(1) client->adapter: 该i2c设备连接的i2c总线适配器;

(2) msg:需要发送的数据;

(3) 1:需要发送的msg个数。

通过以上读写的实现,与上一篇文章 《I2C总线架构 之 I2C协议》 读写时序是对应的:

(1) 写操作只需要一个msg结构体:

  • 起始位 + 写操作(msg[0]) + 停止位。

(2) 读操作需要两个msg结构体 :

  • 起始位+ 写操作(写入地址 msg[0])+ 起始位 + 读操作(存入msg[1]) + 停止位。

f. 注销i2c设备

注销操作:在字符驱动出口函数中,卸载掉注册的i2c设备。这里调用i2c_del_driver即可实现,与i2c_add_driver是对应的。

static void __exit ap3216c_exit(void)
{
 printk("%s:%d: Entry %s \r\n", __FILE__, __LINE__, __func__);

 i2c_del_driver(&ap3216c_device_driver);
}

总结

到这里本篇文章对i2c设备驱动的具体分析基本完成。本篇以ap3216c光敏传感器代码为例,从入口到出口代码走向展开分析。通读文章大致了解,会发现本篇i2c设备驱动与虚拟总线platform架构类似。不同的是platform是软件实现的虚拟总线,在soc上并不存在;而i2c总线,在soc上是实际存在的。相同的是两者实现将驱动分层为硬件参数和驱动抽象,在注册时遍历匹配,然后进入正文probe中!

由于Linux内部的实现较为复杂,本篇主要以设备驱动的角度来分析整个驱动的代码走向,涉及到内部API的实现,本篇只大概介绍其功能,剩余部分会放在i2c核心继续分析。

参考:

《Linux设备驱动开发详解》

《【正点原子】I.MX6U嵌入式Linux驱动开发指南V1.4》

https://blog.csdn.net/Egean/article/details/81085077

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 开源519 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 流程解析
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档