在裸机当中,我们需要编写4个文件bsp_i2c.c,bsp_i2c.h,bsp_ap216c.c,bsp_ap326c.h,前两个也就是IIC的驱动接口驱动,后两个就是ap3216c的设备驱动文件,相当于有两部分驱动。
- I2C主机驱动
- I2C设备驱动
对于I2C主机驱动,一旦编写完成就不需要再做修改,其他的I2C设备直接调用主机驱动提供的API函数完成读写操作即可,这也就正好符合了linux的分离和分层的思想,因此linux内核也将I2C分成两个部分。
- I2C总线驱动,也就是SOC的I2C控制器驱动,也叫作I2C适配器驱动。
- I2C设备驱动,I2C设备驱动是专门针对I2C设备而写的驱动。
I2C总线驱动
I2C总线驱动的重点是I2C适配器驱动,这里需要用到两个重要的数据结构:i2c_adapter和i2c_algorithm,linux内核将SOC的I2C适配器抽象成i2c_adapter,结构体定义如下:
第501行,i2c_algorithm类型的指针algo,对于一个I2C适配器,肯定要对外提供读写API函数,设备驱动程序可以使用这些API函数来完后读写操作。i2c_algorithm就是I2C适配器与IIc设备进行通讯的方法。i2c_algorithm结构体如下: 第398行,master_xfer就是I2C适配器的传输函数,可以通过此函数来完成与IIC设备之间的通讯。
第400行,smbus_xfer就是SMBUS总线的传输函数。
综上可知,I2C总线驱动,或者说I2C适配器驱动的主要工作就是初始化i2c-adapter结构体变量,然后设置i2c_algorithm 中的 master_xfer 函数,完成后就可以通过i2c_add_numbered_adapter或者i2c_add_adapter这两个函数向系统注册设置好的i2c_adapter,这两个函数的原型如下:
这两个函数的区别在于 i2c_add_adapter 使用动态的总线号,而 i2c_add_numbered_adapter使用静态总线号。函数参数和返回值含义如下:
adapter和adap:要添加到linux内核的i2c_adapter,也就是I2C适配器。
返回值:0成功,负值失败
删除I2C适配器的话使用i2c_del_adapter函数即可,函数原型如下:
I2C 适配器驱动 NXP 已经编写好了,这个不需要用户去编写。
I2C设备驱动
I2C设备驱动重点关注两个数据结构,i2c_client和i2c_driver,i2c_client就是描述设备信息的(将设备树节点转化为i2c_client),i2c_driver描述驱动内容,类似于platform。
1,i2c_client结构体
i2c_client结构体定义如下:
一个设备对应一个 i2c_client,每检测到一个 I2C 设备就会分配一个i2c_client。
2,i2c_driver结构体
i2c_driver类似于platform_driver,是我们编写I2C设备驱动重点要处理的内容,结构体定义如下:
第170行,当 I2C 设备和驱动匹配成功以后 probe 函数就会执行,和 platform 驱动一样。
第 188 行,device_driver 驱动结构体,如果使用设备树的话,需要设置 device_driver 的of_match_table 成员变量,也就是驱动的兼容(compatible)属性。
第 189 行,id_table 是传统的、未使用设备树的设备匹配 ID 表。
我们的重点工作就是设备驱动的编写,也就是说重点工作是构建i2c_driver,构建完成以后需要向linux内核注册这个i2c_driver。i2c_driver注册函数为i2c_register_driver原型如下 owner:一般为THIS_MODULE
driver:要注册的i2c_driver
返回值:0成功,负值失败
另外也可以用i2c_add_driver注册,与上面的区别就是少了一个owner参数。
注销I2C设备驱动的时候,我们需要将前面注册的i2c_driver从linux内核注销掉,需要用到i2c_del_driver函数。
i2c_driver注册过程如下,真的很类似于platform驱动
设备和驱动的匹配过程是由I2C总线完成的,I2C总线的数据结构为i2c_bus_type,定义如下
.match 就是 I2C 总线的设备和驱动匹配函数,在这里就是 i2c_device_match 这个函数,此函数内容如下:
很容易看出是2c_client与i2c_driver相匹配。上面的匹配方法也是与platform的相似。
I2C设备驱动编写流程
1,未使用设备树
首先肯定是要描述I2C设备节点信息,在未使用设备树的时候需要在 BSP 里面使用 i2c_board_info 结构体来描述一个具体的 I2C 设备。i2c_board_info 结构体如下:
type 和 addr 这两个成员变量是必须要设置的,一个是 I2C 设备的名字,一个是 I2C 设备的 器件地址。打开 arch/arm/mach-imx/mach-mx27_3ds.c 文件,此文件中关于 OV2640 的 I2C 设备信息描述如下:
2,使用设备树
使用设备树的时候 I2C 设备信息通过创建相应的节点就行了,比如 NXP 官方的 EVK 开发板在 I2C1 上接了 mag3110 这个磁力计芯片,因此必须在 i2c1 节点下创建 mag3110 子节点,然后在这个子节点内描述 mag3110 这个芯片的相关信息。打开 imx6ull-14x14-evk.dts 这个设备树 文件,然后找到如下内容:
第7~11行,向i2c1添加mag3110子节点,第7行“mag3110@0e”是子节点名字,“@”后面的0e就是mag3110的器件地址,第8行设置compatible属性值为“fsl,mag3110”,第9行的reg属性就是器件地址,I2C设备节点的创建重点是compatible属性和reg属性来设置的。一个用于匹配驱动,一个用于设置器件地址。
I2C设备数据的收发流程
I2C设备驱动首要做的就是初始化i2c_driver并向linux内核注册,当设备与驱动匹配之后,probe函数就会执行,probe函数里面所做的就是字符设备注册的那一套了。一般需要在probe函数里面初始化I2C设备,要初始化I2C设备就必须对I2C设备的寄存器进行读写操作。这里就要用到 i2c_transfer 函数了。i2c_transfer 函数最终会调用 I2C 适配器中 i2c_algorithm 里面的 master_xfer 函数,对于 I.MX6U 而言就是i2c_imx_xfer 这个函数。i2c_transfer 函数原型如下:
adap:所使用的I2C适配器,i2c_client会保存对应的i2c_adapter。
msgs:I2C要发送一个或多个信息。
num:消息数量,也就是msgs的数量
返回值:负值失败,非负值,发送信息的数量
![](https://img-blog.csdnimg.cn/6969569760144c249ac14bbcd1092ddf.png)
使用i2c_transfer函数发送数据之前要先构建好i2c_msg,事例代码如下:
static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{
int ret;
struct i2c_msg msg[2];
struct i2c_client *client = (struct i2c_client *)dev->private_data;
/* msg[0]为发送要读取的首地址 */
msg[0].addr = client->addr; /* ap3216c地址 */
msg[0].flags = 0; /* 标记为发送数据 */
msg[0].buf = ® /* 读取的首地址 */
msg[0].len = 1; /* reg长度*/
/* msg[1]读取数据 */
msg[1].addr = client->addr; /* ap3216c地址 */
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("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
ret = -EREMOTEIO;
}
return ret;
}
static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
u8 b[256];
struct i2c_msg msg;
struct i2c_client *client = (struct i2c_client *)dev->private_data;
b[0] = reg; /* 寄存器首地址 */
memcpy(&b[1],buf,len); /* 将要写入的数据拷贝到数组b里面 */
msg.addr = client->addr; /* ap3216c地址 */
msg.flags = 0; /* 标记为写数据 */
msg.buf = b; /* 要写入的数据缓冲区 */
msg.len = len + 1; /* 要写入的数据长度 */
return i2c_transfer(client->adapter, &msg, 1);
}
在设备结构体里面添加一个执行void的指针成员变量private_data,这个变量的作用就是先在probe函数里面接收client结构体的地址,然后在读写函数里面将设备信息(设备地址)转换出来使用。
因为I2C读取数据的时候要先发送要读取的寄存器地址,然后在读取数据,所以需要准备两个i2c_msg,一个用于发送寄存器地址,一个用于读取寄存器的值。对于 msg[0],将 flags 设置为 0,表示写数据。msg[0]的 addr 是 I2C 设备的器件地址,msg[0]的 buf成员变量就是要读取的寄存器地址。对于 msg[1],将 flags 设置为 I2C_M_RD,表示读取数据。 msg[1]的 buf 成员变量用于保存读取到的数据,len 成员变量就是要读取的数据长度。调用 i2c_transfer 函数完成 I2C 数据读操作。
I2C设备收发数据所使用的API函数是i2c_transfer,这是下面两个函数的集合,能同时实现收发的功能,首先看一下数据发送函数i2c_master_send,函数原型如下:
client:I2C设备对应的i2c_client
buf:要发送的数据
count:要发送的数据字节数,要小于64kb,因为i2c_msg的len成员变量是一个u16(无符号16位)数据类型
返回值:负值代表失败,非负值代表字节数
I2C数据接收函数为i2c_master_recv,函数原型如下: