I2C总线架构 之 设备驱动
引言
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中
首先先看需要构建的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 = ®
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
如需技术交流,可关注公众号“开源519”。
![](https://img.haomeiwen.com/i18557296/b521a9ad8cd860ae.jpg)