下如何在 Linux 下开发 I2C 接口器件驱动,重点是学习 Linux 下的 I2C 驱动框架,按照指定的框架去编写 I2C 设备驱动。
以 AP3216C 这个三合一环境光传感器为例。
1、Linux I2C驱动框架简介
裸机驱动AP3216C:
需要写2部分的驱动:
(1)I2C主机驱动:bsp_i2c.c 和 bsp_i2c.h -------> I.MX6U的接口驱动
(2)I2C设备驱动:bsp_ap3216c.c 和 bsp_ap3216c.h-------> AP3216C 这个 I2C 设备驱动文件。
这个正好符合 Linux 的驱动分离与分层的思想,因此 Linux内核也将 I2C 驱动分为两部分:
(1)I2C总线驱动:就是 SOC 的 I2C 控制器驱动,也叫做 I2C 适配器驱动;
(2)I2C设备驱动:就是针对具体的 I2C 设备而编写的驱动;
1.1、I2C总线驱动
I2C 总线驱动重点是 I2C 适配器(也就是 SOC 的 I2C 接口控制器)驱动,用到2个重要的数据结构:i2c_adapter 和 i2c_algorithm。
SOC 的 I2C 适配器(控制器)抽象成 i2c_adapter,结构体定义在include/linux/i2c.h文件中。在这个结构体中有一个结构体变量:
struct i2c_adapter {
...
const struct i2c_algorithm *algo;//总线访问算法
...
};
对于一个 I2C 适配器,肯定要对外提供读写 API 函数,设备驱动程序可以使用这些 API 函数来完成读写操作。i2c_algorithm 就是 I2C 适配器与 IIC 设备进行通信的方法。i2c_algorithm 结构体定义在include/linux/i2c.h文件中。
I2C 总线驱动,或者说 I2C 适配器驱动的主要工作就是初始化 i2c_adapter 结构体变量,然后设置 i2c_algorithm 中的 master_xfer 函数。完成以后通过 i2c_add_numbered_adapter 或 i2c_add_adapter 这两个函数向系统注册设置好的 i2c_adapter,函数原型如下:
int i2c_add_adapter(struct i2c_adapter *adapter)//使用动态的总线号
int i2c_add_numbered_adapter(struct i2c_adapter *adap)//使用静态总线号
一般 SOC 的 I2C 总线驱动都是由半导体厂商编写的,不需要用户去编写。因此 I2C 总线驱动对我们这些 SOC 使用者来说是被屏蔽掉的,我们只要专注于 I2C 设备驱
动即可。
1.2、I2C设备驱动
重点两个数据结构体:
i2c_client:描述设备信息的。一个设备对应一个 i2c_client,每检测到一个 I2C 设备就会给这个 I2C 设备分配一个i2c_client。
i2c_driver:描述驱动内容,类似于 platform_driver。重点工作就是构建 i2c_driver,构建完成以后需要向Linux 内核注册这个 i2c_driver。i2c_driver 注册函数为 int i2c_register_driver,
i2c_driver 注册流程:
1 /* i2c 驱动的 probe 函数 */
2 static int xxx_probe(struct i2c_client *client,const struct i2c_device_id *id)
3 {
4 /* 函数具体程序 */
5 return 0;
6 }
7
8 /* i2c 驱动的 remove 函数 */
9 static int xxx_remove(struct i2c_client *client)
10 {
11 /* 函数具体程序 */
12 return 0;
13 }
14
15 /* 传统匹配方式 ID 列表 */
16 static const struct i2c_device_id xxx_id[] = {
17 {"xxx", 0},
18 {}
19 };
20
21 /* 设备树匹配列表 */
22 static const struct of_device_id xxx_of_match[] = {
23 { .compatible = "xxx" },
24 { /* Sentinel */ }
25 };
26
27 /* i2c 驱动结构体 */
28 static struct i2c_driver xxx_driver = {
29 .probe = xxx_probe,
30 .remove = xxx_remove,
31 .driver = {
32 .owner = THIS_MODULE,
33 .name = "xxx",
34 .of_match_table = xxx_of_match,
35 },
36 .id_table = xxx_id,
37 };
38
39 /* 驱动入口函数 */
40 static int __init xxx_init(void)
41 {
42 int ret = 0;
43
44 ret = i2c_add_driver(&xxx_driver);
return ret;
46 }
47
48 /* 驱动出口函数 */
49 static void __exit xxx_exit(void)
50 {
51 i2c_del_driver(&xxx_driver);
52 }
53
54 module_init(xxx_init);
55 module_exit(xxx_exit);
当 I2C 设备和 I2C 驱动匹配成功以后 probe 函数就会执行,这些和 platform 驱动一样,probe 函数里面基本就是标准的字符设备驱动那一套了。
1.3、I2C设备和驱动匹配过程
I2C 设备和驱动的匹配过程是由 I2C 核心来完成的,drivers/i2c/i2c-core.c 就是 I2C 的核心部分,I2C 核心提供了一些与具体硬件无关的 API 函数。
设备和驱动的匹配过程也是由 I2C 总线完成的,I2C 总线的数据结构为 i2c_bus_type。
2、Linux I2C设备驱动编写流程
我们只需要编写具体的设备驱动。
2.1 I2C 设备信息描述
2.1.1 未使用设备树的时候
在未使用设备树的时候需要在 BSP 里面使用 i2c_board_info 结构体来描述一个具体的 I2C 设备。 i2c_board_info 结构体中的type 和 addr 这两个成员变量是必须要设置的,一个是 I2C 设备的名字,一个是 I2C 设备的器件地址。
在文件 arch/arm/mach-imx/mach-mx27_3ds.c件中关于 OV2640 的 I2C 设备信息描述
392 static struct i2c_board_info mx27_3ds_i2c_camera = {
393 I2C_BOARD_INFO("ov2640", 0x30),
394 };
使用 I2C_BOARD_INFO 来完成 mx27_3ds_i2c_camera 的初始化工作,I2C_BOARD_INFO 是一个宏
#define I2C_BOARD_INFO(dev_type, dev_addr) .type = dev_type, .addr = (dev_addr)
I2C_BOARD_INFO 宏其实就是设置 i2c_board_info 的 type 和 addr 这两个成员变量。I2C 设备名字为 ov2640,ov2640 的器件地址为 0X30。
2.1.2 使用设备树的时候
使用设备树的时候 I2C 设备信息通过创建相应的节点就行了,在 i2c1 节点下创建 mag3110 子节点,然后在这个子节点内描述 mag3110 这个芯片的相关信息。
&i2c1 {
clock-frequency = <100000>;
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";
codec: wm8960@1a {
compatible = "wlf,wm8960";
reg = <0x1a>;
clocks = <&clks IMX6UL_CLK_SAI2>;
clock-names = "mclk";
wlf,shared-lrclk;
};
mag3110@0e {
compatible = "fsl,mag3110";
reg = <0x0e>;
position = <2>;
status = "disabled";
};
......
};
向 i2c1 添加 mag3110 子节点,“mag3110@0e”是子节点名字,“@”后面的“0e”就是 mag3110 的 I2C 器件地址。 compatible 属性值为“fsl,mag3110”。 reg 属性也是设置 mag3110 的器件地址的,因此值为 0x0e。I2C 设备节点的创建重点是 compatible 属性和 reg 属性的设置,一个用于匹配驱动,一个用于设置器件地址。
2.1 I2C 设备数据收发处理流程
I2C 设备驱动首先要做的就是初始化 i2c_driver 并向 Linux 内核注册。当设备和驱动匹配以后 i2c_driver 里面的 probe 函数就会执行,probe 函数里面所做的就是字符设备驱动那一套了。
2.3 程序编写
2.3.1 修改设备树
(1)IO修改或者添加
AP3216C用到了I2C1接口,开发板上的I2C1接口使用到了UART4_TXD和UART4_RXD,如果用到了中断,还需要设置AP_INT 对应的 GIO1_IO01 这个 IO。
打开imx6ull-alientek-emmc.dts
&iomuxc {
...
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 节点.
(2)在 i2c1 节点追加 ap3216c 子节点
AP3216C 是连接到 I2C1 上的,因此需要在 i2c1 节点下添加 ap3216c 的设备子节点。在imx6ull-alientek-emmc.dts 文件中找到 i2c1 节点,此节点默认内容如下:
1 &i2c1 {
2 clock-frequency = <100000>;//I2C 频率,这里设置为 100KHz
3 pinctrl-names = "default";
4 pinctrl-0 = <&pinctrl_i2c1>;// I2C所使用的IO,pinctrl_i2c1 子节
5 status = "okay";
6
7 mag3110@0e {//NXP官方的EVK开发板上接了mag3110,正点原子没有接,需要删除
8 compatible = "fsl,mag3110";
9 reg = <0x0e>;
10 position = <2>;
11 };
12
13 fxls8471@1e {//NXP官方EVK开发板也接了一个fxls8471,正点原子没有接,需要删除
14 compatible = "fsl,fxls8471";
15 reg = <0x1e>;
16 position = <0>;
17 interrupt-parent = <&gpio5>;
18 interrupts = <0 8>;
19 };
ap3216c@1e {
compatible = "alientek,ap3216c";
reg = <0x1e>;
};
20 };
ap3216c 子节点,@后面的“1e”是 ap3216c 的器件地址。
设置 compatible 值为“alientek,ap3216c”。
reg 属性也是设置 ap3216c 器件地址的,因此 reg 设置为 0x1e。
设备树修改完成以后使用“make dtbs”重新编译一下,然后使用新的设备树启动 Linux 内核。/sys/bus/i2c/devices 目录下存放着所有 I2C 设备,如果设备树修改正确的话,会在/sys/bus/i2c/devices 目录下看到一个名为“0-001e”的子目录。
2.3.2 编写驱动
工程创建好以后新建 ap3216c.c 和 ap3216creg.h 这两个文件