由NXP发明的I2C总线是双线制,由串行数据(SDA)、串行时钟(SCL)构成的异步串行总线。SCL有主设备生成,以便在总线上同步数据的传输。I2C用在嵌入式系统中,经常用于连接串行的EEPROM、RTC时钟、GPIO扩张器、温度传感器等。时钟频率常在10K~100K、400K~2M HZ不等。
一、I2C客户端驱动程序架构
I2C控制器驱动程序在设备和总线之间提供抽象层。实现了I2C总线框架,I2C客户端驱动有struct i2c_driver{}表示,而I2C客户端设备有struct i2c_client{}表示。
下面是i2c_driver{}的主要成员
struct i2c_driver {
//标准驱动程序模型接口
int (*probe)(struct i2_client *, const struct i2c_device_id);
int (*remove)(struct i2c_client *);
// 与枚举无关的驱动类型接口
void (*shutdown)(struct i2c_client *);
struct device_driver driver;
const struct i2c_device_id *id_table;
};
1. 驱动probe函数和remove流程
在probe函数中主要实现如下几个主要功能:
- 检查设备是否是所期望的设备;使用i2c_check_functionality()检查I2C总线控制器是否支持设备所需要的功能;
- 初始化设备,使用函数i2c_set_client_data()设置特定的数据;
- 注册合适的内核框架;
// probe函数的原型:
static int foo_probe(struct i2c_client *client, struct i2c_device_id *id);
// 其中struct i2c_client{}代表I2C设备本身,其主要成员如下:
struct i2c_client {
unsigned short flags;
unsigned short addr; // chip address, 7bit,地址被存储在低7位
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter;
struct device dev;
int irq; // 有设备发出的irq
struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb;
#endif
};
// 其中struct i2c_device_id 是正在探测的设备所对应的i2c设备id项
// 设置私有数据的函数如下:
void i2c_set_clientdata(struct i2c_client *client, void *data);
void *i2c_get_clientdata(struct i2c_client *client);
// 在这两个函数的内部会调用dev_set_drvdata和dev_get_drvdata,设置和去除struct device结构中的void *driver_data字段
一个probe函数实现的例子:
// 私有数据
struct mc9s08dz60 {
struct i2c_client *client;
struct gpio_chip chip;
};
static int mc9s08dz60_probe(struct i2c_client *client,
const struct i2c_device_id *id) {
struct mc9s08dz60 *mc9s;
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA))
return -EIO;
mc9s = devm_kzalloc(sizeof(*mc9s), GFP_KERNEL);
mc9s->client = client;
i2c_set_clientdata();
// [...]; // 设置gpio chip相关的功能;
return gpiochip_add(&mc9s->chip);
};
驱动程序的remove函数流程:
// i2c驱动remove函数原型
static int foo_remove(struct i2c_client *client);
// remove函数实现示例
static int mc9s08dz60_remove(struct i2c_client *client) {
struct mc9s08dz60 *mc9s = i2c_get_clientdata(client);
return gpiochip_remove(&mc9s->chip);
}
与platform_driver类似,module_*_driver函数的原型是void module_i2c_driver(foo_driver);
2. 驱动程序和设备通过id_table配置的方式
I2C设备对应的id结构是struct i2c_device_id{}
struct i2c_device_id {
char name[I2C_NAME_SIZE];
kernel_ulong_t driver_data;
};
使用struct i2c_device_id{}的示例如下:
static struct i2c_device_id foo_id_table[] = {
{"foo", my_id_for_foo},
{"bar", my_id_for_bar},
};
MODULE_DEVICE_TABLE(i2c, foo_id_table); // 为了让linux Device Tree根据设备自动加载特定内核module
struct i2c_driver foo_driver = {
.driver = { .name = "foo",
},
.id_table = foo_id_table, // 通过SoC平台文件的方式probe设备
.probe = foo_probe,
.remove = foo_remove,
}
// 要自动注册i2c driver,使用宏module_i2c_driver,
// 自动调用i2c_driver_register()
module_i2c_driver(foo_driver);
二、访问客户端方式
i2c内核提供两种API,一种是普通I2C通信,另一种用于兼容SMBUS的设备,它也适用于I2C设备。
普通i2c通信的基本函数:
// i2c通信的基本函数
int i2c_master_send(struct i2c_client *client, const char *buf, int count);
int i2c_master_recv(struct i2c_client *client, char *buf, int count);
// 下面是一种更灵活的i2c发送消息方式,i2c_transfer()函数可以发送一组消息,消息组可以读和写的组合
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msg, int num);
// i2c_msg{}格式如下:
struct i2c_msg {
u16 addr; // 从设备地址
u16 flags; // 0代表写入,I2C_M_RD代表读取;
u16 len;
u16 *buf;
};
下面是microchip I2C 24LC512 EEPROM字符驱动程序的读取函数,
ssize_t eep_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos) {
[...];
int _reg_addr = dev->current_pointer;
u8 reg_addr[2];
reg_addr[0] = (u8)(_reg_addr>>8);
reg_addr[1] = (u8)(_reg_addr);
struct i2c_msg msg[2];
msg[0].addr = dev->client->addr;
msg[0].flags = 0; // 写入
msg[0].len = 2;
msg[0].buf = reg_addr; // 写入地址
msg[1].addr = dev->client->addr;
msg[1].flags = I2C_M_RD; // 读取
msg[1].len = count;
msg[2].buf = dev->data;
if (i2c_transfer(dev->client->adapter, msg, 2))
pr_err("ee241c512: i2c_transfer failed.");
if (copy_to_user(buf, dev->data, count)!=0) return -EIO;
[...] // 省略其他流程
};
SMBus是intel开发的双线总线,与I2C非常相似。I2C设备与SMBUS兼容,反之则不然。因此,在不知道设备是SMBus设备还是I2C设备的情况下,使用SMBus相关函数是最安全的。
// SMBus Api
s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 comand);
s32 i2c_smbus_read_word_data(struct i2c_client *client, u8 comand);
s32 i2c_smbus_read_block_data(struct i2c_client *client, u8 comand, u8 *values);
s32 i2c_smbus_write_byte_data(struct i2c_client *client, u8 comand, u8 value);
s32 i2c_smbus_write_word_data(struct i2c_client *client, u8 comand, u16 value);
s32 i2c_smbus_write_block_data(struct i2c_client *client, u8 comand,
u8 length, u8 *values);
三、I2C设备和DT树
1. 在SoC开发板文件中实例化I2C设备的方式(旧的方式)
结构体struct i2c_board_info{}用来表示开发板上I2C设备所使用的结构,其定义如下:
struct i2c_board_info {
char type[I2C_NAME_SIZE]; // 这个值应该与i2c_driver.driver.name相同,来进行匹配
unsigned short addr;
void *platform_data;
int irq;
};
接着在SoC board文件中,定义struct i2c_board_info{}数组,使用函数i2c_register_board_info()进行注册,其原型:
int i2c_register_board_info(int busnum,
struct i2c_board_info const *info, unsigned len);
2. 在DT中声明I2C设备
I2C设备节点都是他们所在总线节点的子节点,每个设备只分配一个地址,不涉及长度或范围。声明I2C设备需要的标准属性是reg(用于匹配总线上设备的地址)和compatible属性(用于匹配设备与驱动程序)。
&i2c2 {
pcf8523: rtc@68 {
compatbile="nxp,pcf8523";
reg = <0x68>;
};
eeprom: ee241c512@55 {
compatbile="packt,ee241c512";
reg = <0x55>;
};
};
3. 根据DT中的配置定义和注册I2C客户端驱动程序
使用了DT后,需要增加一个struct of_device_id{}数组。
static struct of_device_id foobar_of_match {
{.compatible="packt,foobar-device",},
{}
};
MODULE_DEVICE_TABLE(of, foobar_of_match); // 用于检测到设备时自动加载模块
static struct i2c_driver foo_drier {
.driver = {.name="foo",
.of_match_table = of_match_ptr
},
.probe = foo_probe,
.id_table = foo_id, // 在4.10以前的内核中,必须存在id_table成员。
};
static int my_probe(struct i2c_client *client, const struct i2c_device_id *id) {
struct of_device_match = of_match_device(foobar_of_match, &client->dev);
if (match) {
/*设备树相关代码*/
} else {
...
}
};
module_i2c_driver(foo_driver);