一、修改设备树
1、IO修改
首先要修改IO,我们使用的是I2C1接口,而I2C1接口使用到了UART4_TXD 和 UART4_RXD
,所以我们需要在设备树里面设置UART4_TXD 和 UART4_RXD 这两个 IO,我们需要修改的内容为:
pinctrl_i2c1: i2c1grp {
fsl,pins = <
MX6UL_PAD_UART4_TX_DATA__I2C1_SCL 0x4001b8b0
MX6UL_PAD_UART4_RX_DATA__I2C1_SDA 0x4001b8b0
>;
};
pinctrl_i2c1就是I2C1的IO节点,将UART4_TXD和UART4_RXD这两个IO复用为I2C1_SCL和I2C1_SDA,电气属性都设置为0x4001b8b0。
2、在I2C1节点下加入ap3216c子节点
我们要在i2c1节点中添加ap3216c子节点,修改以后如下所示:
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
ap3216c@1e {
compatible = "alientek,ap3216c";
reg = <0x1e>;
};
};
在ap3216c子节点,@后面的“1e”是ap3216c的器件地址。设置compatible值为“alientek,ap3216c”。reg属性也是设置ap3216c器件地址的,所以reg设置为0x1e。修改完成后,我们怎么检测我们的设备树有没有修改好呢?
使用新的设备树启动Linux内核。进入sys/bus/i2c/devices目录,然后输入ls查看会有一个0-001e的子目录,然后使用cat 0-001e/name命令,我们会看到设备的名字。
二、驱动的编写
I2C总线-设备-驱动模型
1、i2c_driver表明能支持哪些设备:
- 使用of_match_table来判断
- 设备树中,某个I2C控制器节点下可以创建I2C设备节点
- 如果I2C设备节点的compatible属性跟of_match_table的某项兼容,则匹配成功
- i2c_client.name跟某个of_match_table[i].compatible值相同,则匹配成功
- 设备树中,某个I2C控制器节点下可以创建I2C设备节点
- 使用id_table来判断
- i2c_client.name跟某个id_table[i].name值相同,则匹配成功
i2c_driver跟i2c_client匹配成功后,就调用i2c_probe函数。
2、i2c_client设备
i2c_client表示一个I2C设备,创建i2c_client的方法有四种:
- 通过I2C bus number来创建
int i2c_register_board_info(int busnum, struct i2c_board_info const *info, unsigned len);
- 通过设备树来创建
该方法的具体过程上面一节已经详细讲解(我们的代码也是基于这个方法来构建的)
有时候不知道该设备挂载在那个I2C bus下,无法知道它对应的I2C bus number,但是可以通过其他方法知道对应的i2c_adapter结构体。
可以使用下面两个函数来创建i2c_client:
- i2c_new_device
static struct i2c_board_info sfe4001_hwmon_info = {
I2C_BOARD_INFO("max6647", 0x4e),
};
int sfe4001_init(struct efx_nic *efx)
{
(...)
efx->board_info.hwmon_client =
i2c_new_device(&efx->i2c_adap, &sfe4001_hwmon_info);
(...)
}
- i2c_new_probed_device
static const unsigned short normal_i2c[] = { 0x2c, 0x2d, I2C_CLIENT_END };
static int usb_hcd_nxp_probe(struct platform_device *pdev)
{
(...)
struct i2c_adapter *i2c_adap;
struct i2c_board_info i2c_info;
(...)
i2c_adap = i2c_get_adapter(2);
memset(&i2c_info, 0, sizeof(struct i2c_board_info));
strscpy(i2c_info.type, "isp1301_nxp", sizeof(i2c_info.type));
isp1301_i2c_client = i2c_new_probed_device(i2c_adap, &i2c_info,
normal_i2c, NULL);
i2c_put_adapter(i2c_adap);
(...)
}
差别:
- i2c_new_device:会创建i2c_client,即使该设备并不存在
- i2c_new_probed_device:
- 他成功的话,会创建i2c_client,并且表示这个设备肯定存在
- I2C设备的地址可能发生变化,比如该设备有引脚电平不同,设备地址就不一样
- 可以罗列出可能的地址
- i2c_new_probed_device使用这些地址判断设备是否存在
方法3:由i2c_driver.detect函数来判断是否有对应的I2C设备并生成i2c_client
方法4:通过用户空间(user-space)生成,调试时、或者不方便通过代码明确生成i2c_client
时,可以通过用户空间来生成。
// 创建一个i2c_client, .name = "eeprom", .addr=0x50, .adapter是i2c-3
# echo eeprom 0x50 > /sys/bus/i2c/devices/i2c-3/new_device
// 删除一个i2c_client
# echo 0x50 > /sys/bus/i2c/devices/i2c-3/delete_device
以上是i2c_client的四种创建方法,
接下来我们来看看i2c_driver程序的套路,都是I2C总线-设备-驱动模型,分配、设置、注册一个I2C_driver结构体:
在probe函数中,分配、设置、注册file_operations结构体。
在file_operations的函数中,使用i2c_transfer等函数来发起I2C传输
那我们来看下具体是怎么实现的呢?
//驱动入口函数
static int __init ap3216c_init(void)
{
int ret = 0;
ret = i2c_add_driver(&i2c_ap3216c_driver);
return ret;
}
首先驱动函数中的i2c_add_driver是向内核中注册i2c_driver,该结构体如下:
/*设备树匹配列表*/
static const struct of_device_id ap3216c_of_match[] = {
{.compatible = "alientek, ap3216c"},
{/*Sentinel*/}
};
//传统匹配方式ID列表
static const struct i2c_device_id ap3216c_id[] = {
{"alientek, ap3216c", 0},
{}
};
/*i2c驱动结构体*/
static struct i2c_driver i2c_ap3216c_driver = {
.driver = {
.name = "ap3216c",
.of_match_table = ap3216c_of_match,
.owner = THIS_MODULE,
},
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.id_table = ap3216c_id,
};
ap3216c_of_match 匹配表, of_device_id 类型,用于设备树设备和驱动匹配。这里只写了一个 compatible 属性,值为“alientek,ap3216c”。而ap3216c_id 匹配表, i2c_device_id 类型。用于传统的设备和驱动匹配,也就是没有使用设备树的时候。ap3216c_probe 函数,当 I2C 设备和驱动匹配成功以后此函数就会执行,和platform 驱动框架一样。此函数前面都是标准的字符设备注册代码,最后面会将此函数的第一个参数 client 传递给 ap3216cdev 的 private_data 成员变量。该函数具体内容如下:
/**
* i2c驱动的probe函数,当驱动与设备匹配以后此函数就会执行
* client:i2c设备
* id:i2c设备ID
* 返回值:0成功;其他,失败
*
*/
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
//1、构建设备号
if(ap3216cdev.major){
ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
}else{
alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME);
ap3216cdev.major = MAJOR(ap3216cdev.devid);
}
//2、注册设备
cdev_init(&ap3216cdev.cdev, &ap3216c_ops);
cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);
//3、创建类
ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
//4、创建设备
ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);
ap3216cdev.private_data = client;
return 0;
}
注册设备是会需要两个参数一个是cdev,另一个是file_operations结构体,而在这个结构体中包含了open,read等和上层APP交互的函数。接下来就是对具体的IIC设备ap3216c的寄存器进行读写操作了,我就不贴代码了,需要翻阅数据手册来具体编写,我们主要是通过调用i2c_transfer函数来传递数据。到此我们已经可以使用系统的IIC总线来读写我们的设备了。
这只是一种I2C的简单实现,后面有时间我们再详细看看i2c_transfer是怎么实现的,一个比较重要的一个叫I2C_Adapter的东西。