Linux I2C驱动架构
@
博客说明
| 撰写日期 | 2019.11.20
---|:--😐---
| 完稿日期 | 2019.11.21
| 最近维护 | 暂无
| 本文作者 | multimicro
| 联系方式 | multimicro@qq.com
| 资料链接 | 本文无附件资料
| GitHub| https://github.com/wifialan/drivers
| 原文链接| https://blog.csdn.net/multimicro/article/details/103164746
开发环境
环境说明 | 详细信息 | 备注信息 |
---|---|---|
操作系统 | Ubunut 18.04 | |
开发板 | JZ2440-V3 | |
u-boot | uboot-2012.04.01 | |
busybox | busybox-1.22.1 | |
u-boot和busybox编译器 | arm-linux-gcc (4.4.3) | |
Linux内核 | linux-4.19-rc3 | |
Linux内核编译器 | arm-linux-gnueabi-gcc (4.9.4) |
1. Linux I2C 体系结构
i2c的编程应用流程请参考我的上一篇文章Linux 设备树学习——基于i2c总线分析,这篇文章我是基于i2c总线而写的,里面包含注册和匹配方式的介绍,和相应的模板。
参考宋宝华 《Linux设备驱动开发详解》 第15章内容。
Linux的I2C体系结构分为3个组成部分:
- I2C核心
- I2C总线驱动(适配器驱动)
- I2C设备驱动
1.1 Linux I2C核心
借鉴 宋宝华 《Linux设备驱动开发详解》 第15章第2节内容。
I2C核心(drivers/i2c/i2c-core-base.c)中提供了一组不依赖于硬件平台的接口函数,这个文件一般不需要被工程师修改,但是理解其中的主要函数非常关键,因为I2C总线驱动和设备驱动之间以I2C核心作为纽带。I2C核心中的主要函数如下。
1.1.1 增加 / 删除 i2c_adapter
int i2c_add_adapter(struct i2c_adapter *adap);
void i2c_del_adapter(struct i2c_adapter *adap);
在有设备树版本中,内部都封装了从设备树中获取i2c
设备并注册为i2c client
的方法,如下:
/i2c节点一般表示i2c控制器, 它会被转换为platform_device, 在内核中有对应的platform_driver;
platform_driver的probe函数中会调用i2c_add_numbered_adapter:
i2c_add_numbered_adapter// drivers/i2c/i2c-core-base.c
__i2c_add_numbered_adapter
i2c_register_adapter
of_i2c_register_devices(adap); // drivers/i2c/i2c-core-of.c
for_each_available_child_of_node(bus, node) {
client = of_i2c_register_device(adap, node);
client = i2c_new_device(adap, &info); // 设备树中的i2c子节点被转换为i2c_client
}
上述第一个函数i2c_add_numbered_adapter
内容如下,通过Source Insight软件,可以一步一步定位分析,可以很好的帮助理解设备树汇总的i2c子设备节点是如何转换为i2c client
的。
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
{
if (adap->nr == -1) /* -1 means dynamically assign bus id */
return i2c_add_adapter(adap);
return __i2c_add_numbered_adapter(adap);
}
1.1.2 增加 / 删除 i2c_driver
int i2c_register_driver(struct module *owner, struct i2c_driver *driver);
void i2c_del_driver(struct i2c_driver *driver);
#define i2c_add_driver(driver) \
i2c_register_driver(THIS_MODULE, driver)
这些函数一般用在编写i2c设备driver中,如
drivers/char/at24c256.c
static int __init at24_init(void)
{
return i2c_add_driver(&at24c256_driver);
}
static void __exit at24_exit(void)
{
i2c_del_driver(&at24c256_driver);
printk(DRV_NAME "\tRemove i2c driver success\n");
}
module_init(at24_init);
module_exit(at24_exit);
1.1.3 I2C传输、发送和接收
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
int i2c_master_send(const struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(const struct i2c_client *client, char *buf, int count);
这三个函数的区别在于:
i2c_transfer
一次可以传输多个i2c_msg
i2c_master_send
一次只能发送一个i2c_msg
i2c_master_recv
一次只能接收一个i2c_msg
在开发比较复杂的i2c时序时,采用最多的是i2c_transfer
。参考 Code 1
Code 1
int ret;
struct i2c_msg msg[2];
char buffer_data[100];
memset(buffer_data,0,sizeof(buffer_data));
buffer_data[0] = (char)0x00;
buffer_data[1] = (char)0x00;
buffer_data[2] = (char)0xAA;
buffer_data[3] = (char)0xAB;
buffer_data[4] = (char)0xAC;
/* 1st write data */
msg[0].addr = at24_dev->client->addr | 0x01; //这个是i2c从设备的地址
msg[0].flags = at24_dev->client->flags & 0; //这个是对i2c从设备的读写标志位,和上面的addr组成i2c的第一个8bit数据
msg[0].buf = &buffer_data[0]; //这个是i2c从设备地址后的数据,数组中,每一个数据都是一个8bit数据
msg[0].len = 5; //指定发送除地址位外的多少个数据,上述buffer_data中,前两个是at24c256的从设备地址,后3个是写入的数据
/* 2nd write data */
msg[1].addr = at24_dev->client->addr | 0x01;
msg[1].flags = I2C_M_RD;
msg[1].buf = &buffer_data[5]; //指定接受的数据存放位置,这里从buffer_data中的第6个位置中开始存储读到的数据,
msg[1].len = 3; //指定读的数据个数
//开始i2c传输,发送成功后,会返回发送的次数,由最后一个参数为2可以,需要开启两次传输,那么发送成功后就会返回2
ret = i2c_transfer(at24_dev->client->adapter, &msg[0], 2);
tmp = (ret == 2) ? msg[1].len : ret;
i2c_transfer
发送失败会返回特定的负数,详情参考linux i2c 的通信函数i2c_transfer在什么情况下出现错误
1.2 Linux I2C适配器驱动
1.2.1 I2C适配器驱动的注册与注销
借鉴 宋宝华 《Linux设备驱动开发详解》 第15章第3节内容。
由于I2C总线控制器通常是在内存上的,所以它本身也连接在platform总线上,要通过platform_driver和platform_device的匹配来执行。因此尽管I2C适配器给别人提供了总线,它自己也被认为是接在platform总线上的一个客户。Linux的总线、设备和驱动模型实际上是一个树形结构,每个节点虽然可能成为别的总线控制器,但是自己也被认为是从上一级总线枚举出来的。
也就是说,I2C适配器的初始化,需要在platform_driver
的probe()
函数中完成。这部分大多由芯片厂商提供好了,自己不需要编写。
1.2.2 I2C总线的通信方法
只有I2C适配器驱动是没有灵魂的,需要给它注入一个通信算法i2c_algorithm
,才能然I2C运作起来。
可以说I2C适配器驱动
是来配置I2C硬件
,而i2c_algorithm
是来操作I2C硬件
产生对应的I2C时序波形,完成基于I2C总线的数据传输。
我在开发时,没有自己编写通信方法,但是也可以实现通信,猜测这个也是芯片厂商给写好的,直接调用接口
1.3 Linux I2C设备驱动
这方面主要是完成I2C设备驱动模块的加载与卸载,并实现数据传输,模块的加载与卸载就是经常编写的驱动开发流程,详情参考附录中github代码。
在编写i2c设备driver中的i2c设备读写函数中,如下,我将AT24C256
这个i2c设备注册成了字符设备,调用了file_operations
结构体里面的读写方法。参考 Code 2 和 Code 3
drivers/char/at24c256.c
Code 2
static ssize_t at24_write( struct file *filp, const char __user *buffer, size_t size, loff_t *f_pos )
{
int ret,tmp;
char i;
struct i2c_msg msg;
char buffer_data[100];
copy_from_user(buffer_data, buffer, 10);
memset(buffer_data,0,sizeof(buffer_data));
buffer_data[0] = (char)0x00;
buffer_data[1] = (char)0x00;
for (i = 0; i < 64; ++i)
{
buffer_data[i+2] = i;
}
msg.addr = at24_dev->client->addr | 0x01; //这个是i2c从设备的地址
msg.flags = at24_dev->client->flags & 0;
msg.buf = &buffer_data[0]; //这个是i2c从设备地址后的数据
msg.len = 66;
ret = i2c_transfer(at24_dev->client->adapter,&msg,1);
tmp = (ret == 1) ? msg.len : ret;
printk("i2c code: %d return code: %d addr: 0x%02x%02x ",\
ret,tmp,buffer_data[0],buffer_data[1]);
for (i = 0; i < 64; ++i)
{
printk("Write Data: 0x%02x",buffer_data[i+2]);
}
return 0;
}
Code 3
static ssize_t at24_read( struct file *filp, const char __user *buffer, size_t size, loff_t *f_pos )
{
int ret,tmp;
unsigned long i;
struct i2c_msg msg[3];
char buffer_data[100];
memset(buffer_data,0,sizeof(buffer_data));
msg[0].addr = at24_dev->client->addr | 0x01;
msg[0].flags = I2C_M_RD;
msg[0].buf = &buffer_data[2];
msg[0].len = 64;
ret = i2c_transfer(at24_dev->client->adapter, &msg[0], 1);
tmp = (ret == 1) ? msg[0].len : ret;
printk("i2c code: %d return code: %d addr: 0x%02x%02x ",\
ret,tmp,buffer_data[0],buffer_data[1]);
for (i = 0; i < 64; ++i)
{
printk("Read Data: 0x%02x",buffer_data[i+2]);
}
return 0;
}
附录
- Github代码:at24c256.c