一、Linux的I2C子系统框架介绍
- I2C总线和I2C协议、SMBus协议
- 标准模式:100kbps、快速模式:400kbps、高速模式:3.4Mbps
- I2C接上拉电阻的作用:保证总线空闲处于高电平(开漏/开极输出输出0时总线处于高阻态)、实现线与功能(一个设备拉低总线的时候其他设备也能感知到)
- I2C上拉电阻的常规值:100kbps选10k电阻、400kbps选4.7k电阻、3.4Mbps选2.2k电阻
- I2C协议:起始信号(SCL为高电平期间SDA由高变低)、结束信号(SCL为高电平期间SDA由低变高)、传输以字节为单位、每个字节后必须加一个应答位、位序为先MSB,目标设备可拉低SCL设置控制器等待
- 目标地址是7bit,后跟读写方向位(0为写、1位读)组成一个字节
二、I2C子系统的分层介绍
- I2C设备驱动,包括一般的I2C设备驱动和i2c-dev.c,形成两种节点,/dev/xxx和/dev/i2c-N
- I2C核心层,如i2c-core-smbus.c、i2c-core-base.c,给上层提供各种API
- I2C控制器驱动,包含一般的I2C控制器驱动和i2c-gpio.c,后者使用GPIO模拟I2C的传输过程
- i2c-dev.c是通用的I2C设备驱动,i2c-gpio.c为GPIO模拟I2C的实现
三、典型的设备树文件
// 普通的I2C设备,挂在I2C0总线下,地址是0x50
&i2c0{
i2c_dev@50 {
compatible = "i2c_dev_compatible";
reg = <0x50>;
};
};
// 一般的I2C控制器
i2c_adap_virt: virtual_i2c_adapter {
compatible = "user,virtual_i2c_adapter";
#address-cells = <1>;
#size-cells = <0>;
... // 其他信息
};
// 使用GPIO模拟的I2C控制器
i2c-gpio-0 {
compatible = "i2c-gpio";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c_bitbang>;
gpios = <&GPIOA 7 GPIO_ACTIVE_HIGH>, //SCL
<&GPIOA 8 GPIO_ACTIVE_HIGH>; //SDA
i2c-gpio,sda-open-drain;
i2c-gpio,scl-open-drain;
i2c-gpio,delay-us=<5>; //100K
#address-cells = <1>;
#size-cells = <0>;
... // 其他信息
};
四、I2C的重要结构体
struct i2c_adapter { // 描述I2C控制器
const struct i2c_algorithm *algo; // I2C具体的传输实现
int nr; // 第几个I2C控制器
...
};
struct i2c_algorithm { // 描述I2C的传输
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, int num); // 传输函数
...
u32 (*functionality)(struct i2c_adapter *adap); // 控制器支持哪些功能
};
struct i2c_msg { // 描述I2C传递的消息
__u16 addr; // 从机地址
__u16 flag; // 标志位
__u16 len; // 长度
__u8 *buf; // 指向msg数据的指针
};
struct i2c_driver { // 描述I2C驱动,类似platform_driver
int (*probe_new)(struct i2c_client *client);
int (*remove)(struct i2c_client *client);
struct device_driver driver; // compatible成员在这里
const struct i2c_device_id *id_table;
...
};
struct i2c_client { // 描述I2C设备,类似platform_device
unsigned short flags; // 标志
unsigned short addr; // I2C设备地址
struct i2c_adapter *adapter; // 控制器
struct device dev; // 必备的device结构体
};
五、驱动程序的流程
// I2C设备驱动
1. 入口函数中,使用i2c_add_driver注册一个i2c_driver,出口函数中,使用i2c_del_driver注销一个i2c_driver
2. I2C-BUS和Platform-BUS类似,入口函数注册相应driver,出口函数注销driver,compatible属性匹配时,调用probe函数
3. probe函数中,类似其他字符设备。使用alloc_chrdev_region、cdev_init、cdev_add、class_create、device_create
4. 关键的file_operations结构体中的读写函数,可以借助i2c_smbus_xx或者i2c_transfer函数实现
// I2C控制器驱动
1. 使用平台设备驱动程序的框架,注册I2C控制器驱动
2. probe函数中,分配、设置、注册一个i2c_adapter结构体,使用i2c_add_adapter注册一个控制器的结构体
3. 关键的成员i2c_algorithm需要最少实现master_xfer和functionality函数
六、i2c-dev.c介绍
1. 入口函数
register_chrdev_region // 主设备号89,名称为i2c
class_create // 创建类,辅助创建节点
i2c_for_each_dev(NULL, i2cdev_attach_adapter); // 绑定存在的I2C控制器,形成的/dev/i2c-N表示每个控制器
cdev_init
cdev_device_add
2. 字符设备对应的fops
open
adap = i2c_get_adapter(minor); // 得到控制器
kzalloc // 分配一个i2c_client表示I2C设备
设置I2C设备所属的控制器
release
i2c_put_adapter(client->adapter); // 释放控制器
kfree // 释放i2c_client的空间
read
i2c_master_recv
write
i2c_master_send
ioctl
I2C_SLAVE/I2C_SLAVE_FORCE // 设置client的addr
I2C_TENBIT // 设置client的flag
I2C_PEC // 设置client的flag
I2C_FUNCS // 调用i2c_algorithm的functionality函数获取能力
I2C_RDWR // 构建msg,执行i2c_transfer
I2C_SMBUS // 构建msg,执行i2c_smbus_xfer
...
3. 应用程序如何使用/dev/i2c-x,构建i2c_rdwr_ioctl_data结构体,用i2c_msg进行填充,并使用I2C_RDWR的ioctl实现I2C读写
七、i2c-gpio.c介绍
1. 使用平台设备驱动程序的框架,注册该platform_driver
2. probe函数
解析设备树,获得时钟频率、SCL、SDA是否上拉等信息
构建SCL和SDA对应的gpio_desc结构体
构建adapter结构体对象
i2c_bit_add_numbered_bus(adap)
注册i2c_bit_algo,实现transfer函数和functionality函数
调用add_adapter,注册一个adapter
八、总结
- 可以看到一般的I2C设备驱动和i2c-dev.c的实现类似,只不过两者生成的节点不同,后者对控制器生成了一个单独的节点,同样可以调用i2c协议或smbus协议的xfer函数
- i2c-gpio.c和一般的I2C控制器驱动类似,都使用add_adapter实现控制器的注册,关键之处在于xfer函数,前者使用GPIO做模拟实现I2C协议。
九、关键函数
i2c_add_driver实际为i2c_register_driver,函数中将i2c_driver结构体中的driver成员,调用driver_register进行注册,最后调用i2c_for_each_dev(driver, __process_new_driver);
i2c_del_driver先执行i2c_for_each_dev(driver, __process_removed_driver);,再执行driver_unregister
i2c_get_adapter通过索引值得到注册i2c_adapter,之后调用get_device引用计数值加1
i2c_put_adapter调用put_deivce引用计数值减1,之后调用module_put减少i2c_adapter模块计数
i2c_new_client_device传入i2c_adapter的指针和i2c_board_info指针,得到一个i2c_client,并调用driver_register进行注册
- i2c_board_info的关键成员有type表示类型名称、flags表示标志位、addr表示I2C设备地址、platform_data表示平台数据、resources表示资源、num_resources表示资源的数量,of_node表示设备节点指针
i2c_transfer是I2C核心层提供的I2C传输函数,传输的单位是i2c_msg,传输的个数是参数num,核心实现是调用对应的adapter的algo成员的master_xfer函数实现I2C数据传输,当i2c_msg的flag为0表示写、为1表示读,还有一些其他的宏控制I2C的操作
- i2c_client是什么时候创建的?因此i2c_client需要绑定一个i2c_adapter,因此必定是在i2c_adapter注册完之后才会创建i2c_client,在i2c控制器的驱动程序的probe函数的末尾,调用i2c_add_adapter注册一个i2c_adapter,其中调用i2c_register_adapter注册一个i2c_adapter,其中通过of_i2c_register_devices解析设备树并从设备树节点中提取出i2c_board_info,最后调用i2c_new_client_device注册了一个i2c_client
十、工具
- i2c-tools工具
- i2cdetect -l列出所有的I2C总线
- i2cdetect -F 1 列出I2C1上的功能集
- i2cdetect -a -y 1 扫描I2C1上的所有I2C地址
- i2cdump -f -a 1 0x38 读I2C1上的0x38的所有寄存器的值
- i2cdump -f -r 0x80-0xff 1 0x38 读I2C1上的0x38的指定范围寄存器的值
- i2cset -f -r 1 0x38 0x80 0x11 强制向I2C1的0x38的寄存器0x80写入值0x11
- i2cget -f 1 0x38 0x80 强制从I2C1的0x38地址的寄存器0x80读数
- i2ctransfer 1 w2@0x38 0x80 0x22 向I2C1的0x38地址的寄存器0x80写两个字节0x22
- i2ctransfer 1 w1@0x38 0x80 r1 向I2C1的0x38地址的寄存器0x80读一个字节