在电子产品硬件设计当中,I2C 是一种很常见的同步、串行、低速、近距离通信接口 ,用于连接各种IC、传感器等器件,它们都会提供I2C接口与SoC主控相连,比如陀螺仪、加速度计、触摸屏等,其最大优势在于可以在总线上扩展多个外围设备的支持。
Linux内核开发者为了让驱动开发工程师在内核中方便的添加自己的I2C设备驱动程序,更容易的在linux下驱动自己的I2C接口硬件,进而引入了I2C总线框架 。与Linux下的platform虚拟总线不同的是,I2C是实际的物理总线 ,所以I2C总线框架也是Linux下总线、设备、驱动模型的产物。
本章来学习一下如何在Linux下的I2C总线框架,以及如何使用I2C总线框架编写一个I2C接口的外设驱动程序;本章重点是学习Linux下的I2C总线框架。
I2C&AP3216C简介
I2C简介
这里跟stm32裸机开发就是一样的了,不再赘述。
I2C主要的就是总线的读写时序,下图是写时序:
下图是I2C读时序:
STM32MP1 I2C简介
STM32MP157D有6个I2C接口 ,其中I2C4和I2C6可以在A7安全模式或者A7非安全模式下使用,M4无法使用,STM32MP157 的I2C部分特性如下:
- 兼容I2C总线规范第03版。
- 支持从模式和主模式,支持多主模式功能。
- 支持标准模式(Sm)、快速模式(Fm)和超快速模式(Fm+),其中,标准模式100kHz,快速模式400 kHz,超快速模式可以到1MHz。
- 7 位和10位寻址模式。
- 多个7位从地址,所有7位地址应答模式。
- 软件复位。
- 带DMA功能的1字节缓冲。
- 广播呼叫。
AP3216C简介
STM32MP1开发板上通过I2C5连接了一个三合一环境传感器:AP3216C 。AP3216C是由敦南科技推出的一款传感器,其支持环境光强度(ALS)、接近距离(PS)和红外线强度(IR)这三个环境参数检测。该芯片可以通过IIC接口与主控制相连,并且支持中断,AP3216C的特点如下:
- I2C接口,快速模式下波特率可以到400Kbit/S。
- 多种工作模式选择:ALS、PS+IR、ALS+PS+IR、PD等等。
- 内建温度补偿电路。
- 宽工作温度范围(-30 C - +80 C)。
- 超小封装,4.1mm x 2.4mm x 1.35mm。
- 环境光传感器具有16位分辨率。
- 接近传感器和红外传感器具有10位分辨率。
AP3216C常被用于手机、平板、导航设备等,其内置的接近传感器可以用于检测是否有物体接近,比如手机上用来检测耳朵是否接触听筒,如果检测到的话就表示正在打电话,手机就会关闭手机屏幕以省电。也可以使用环境光传感器检测光照强度,可以实现自动背光亮度调节。AP3216C结构如下图所示:
AP3216的设备地址为0X1E,同几乎所有的 I2C从器件一样,AP3216C内部也有一些寄存器,通过这些寄存器可以配置AP3216C的工作模式,并且读取相应的数据。 AP3216C用的寄存器如下图所示:
在上图中,0X00这个寄存器是模式控制寄存器,用来设置AP3216C的工作模式,一般开始先将其设置为0X04,也就是先软件复位一次AP3216C。接下来根据实际使用情况选择合适的工作模式,比如设置为0X03,也就是开启ALS+PS+IR。0X0A-0X0F这6个寄存器就是数据寄存器,保存着ALS、PS和IR这三个传感器获取到的数据值。如果同时打开ALS、PS和IR的读取间隔最少要112.5ms,因为AP3216C完成一次转换需要112.5ms。
Linux I2C总线框架简介
使用裸机的方式编写一个I2C器件的驱动程序,一般需要实现两部分:
- I2C 主机驱动。
- I2C 设备驱动。
I2C主机驱动也就是SoC的I2C控制器对应的驱动程序 ,I2C设备驱动其实就是挂在I2C总线下的具体设备对应的驱动程序,例如eeprom、触摸屏IC、传感器IC等;对于主机驱动来说,一旦编写完成就不需要再做修改 ,其他的I2C设备直接调用主机驱动提供的API函数完成读写操作即可。这个正好符合Linux的驱动分离与分层的思想,因此Linux内核也将I2C驱动分为两部分。
Linux内核开发者为了让驱动开发工程师在内核中方便的添加自己的I2C设备驱动程序,方便更容易的在linux下驱动自己的I2C接口硬件,进而引入了I2C总线框架,一般也叫作I2C子系统,Linux下I2C子系统总体框架如下所示:
从上图可以看出,I2C子系统分为三大组成部分。
I2C核心(I2C-core)
I2C核心提供了I2C总线驱动(适配器)和设备驱动的注册、注销方法,I2C通信方法(algorithm)与具体硬件无关的代码,以及探测设备地址的上层代码等。
I2C总线驱动(I2C adapter)
I2C总线驱动是I2C适配器的软件实现,提供I2C适配器与从设备间完成数据通信的能力 。I2C总线驱动由i2c_adapter和i2c_algorithm来描述。I2C适配器是SoC中内置i2c控制器的软件抽象,可以理解为他所代表的是一个I2C主机。
I2C设备驱动(I2C client driver)
包括两部分:设备的注册和驱动的注册。
I2C子系统帮助内核统一管理I2C设备,让驱动开发工程师在内核中可以更加容易地添加自己的I2C设备驱动程序。
I2C总线驱动
首先来看一下I2C总线,在讲platform的时候就说过,platform是虚拟出来的一条总线,目的是为了实现总线、设备、驱动框架。对于I2C而言,不需要虚拟出一条总线,直接使用I2C总线即可 。I2C总线驱动重点是I2C适配器(也就是SoC的I2C接口控制器)驱动 ,这里要用到两个重要的数据结构:i2c_adapter和i2c_algorithm 。I2C子系统将SoC的I2C适配器(控制器)抽象成一个i2c_adapter结构体,i2c_adapter结构体定义在include/linux/i2c.h文件中,结构体内容如下:
第688行,i2c_algorithm类型的指针变量algo,对于一个I2C适配器,肯定要对外提供读写API函数,设备驱动程序可以使用这些API函数来完成读写操作。i2c_algorithm就是I2C适配器与IIC设备进行通信的方法。
i2c_algorithm结构体定义在include/linux/i2c.h文件中,内容如下:
第536行,master_xfer就是I2C适配器的传输函数,可以通过此函数来完成与IIC设备之间的通信。
第540行,smbus_xfer就是SMBUS总线的传输函数。smbus协议是从I2C协议的基础上发展而来的,他们之间有很大的相似度,SMBus与I2C总线之间在时序特性上存在一些差别,应用于移动PC和桌面PC系统中的低速率通讯。
综上所述,I2C总线驱动,或者说I2C适配器驱动的主要工作就是初始化i2c_adapter结构体变量,然后设置i2c_algorithm中的master_xfer函数 。完成以后通过i2c_add_numbered_adapter或i2c_add_adapter这两个函数向I2C子系统注册设置好的i2c_adapter,这两个 函数的原型如下:
c
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
这两个函数的区别在于i2c_add_adapter会动态分配一个总线编号,而i2c_add_numbered_adapter函数则指定一个静态的总线编号。函数参数和返回值含义如下:
- adapter或adap:要添加到Linux内核中的i2c_adapter,也就是I2C适配器。
- 返回值:0,成功;负值,失败。
如果要删除I2C适配器的话使用i2c_del_adapter函数即可,函数原型如下:
c
void i2c_del_adapter(struct i2c_adapter * adap)
函数参数和返回值含义如下:
- adap:要删除的I2C适配器。
- 返回值:无。
关于I2C的总线(控制器或适配器)驱动就讲解到这里,一般SoC的I2C总线驱动都是由半导体厂商编写的,比如STM32MP1的I2C适配器驱动ST官方已经编写好了 ,这个不需要用户去编写。因此I2C总线驱动对SoC使用者来说是被屏蔽掉的,只要专注I2C设备驱动即可。
I2C总线设备
I2C设备驱动重点关注两个数据结构:i2c_client和i2c_driver ,根据总线、设备和驱动模型,I2C总线上一小节已经讲了。还剩下设备和驱动,i2c_client用于描述I2C总线下的设备,i2c_driver则用于描述I2C总线下的设备驱动,类似于platform总线下的platform_device和platform_driver。
i2c_client结构体
i2c_client结构体定义在include/linux/i2c.h文件中,内容如下:
一个I2C设备对应一个i2c_client结构体变量,系统每检测到一个I2C从设备就会给这个设备分配一个i2c_client。
i2c_driver结构体
i2c_driver类似platform_driver,是编写I2C设备驱动重点要处理的内容,i2c_driver结构体定义在include/linux/i2c.h文件中,内容如下:
c
示例代码41.2.2.2 i2c_driver结构体
253 struct i2c_driver {
254 unsigned int class;
255
256 /* Standard driver model interfaces */
257 int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);
258 int (*remove)(struct i2c_client *client);
259
260 /* New driver model interface to aid the seamless removal of
261 * the current probe()'s, more commonly unused than used
262 second parameter.*/
263 int (*probe_new)(struct i2c_client *client);
264
265 /* driver model interfaces that don't relate to enumeration */
266 void (*shutdown)(struct i2c_client *client);
267
268 /* Alert callback, for example for the SMBus alert protocol.
269 * The format and meaning of the data value depends on the
270 * protocol. For the SMBus alert protocol, there is a single
271 * bit of data passed as the alert response's low bit ("event
272 * flag"). For the SMBus Host Notify protocol, the data
273 * corresponds to the 16-bit payload data reported by the
274 slave device acting as master.*/
275 void (*alert)(struct i2c_client *client, enum i2c_alert_protocol protocol,
276 unsigned int data);
277
278 /* a ioctl like command that can be used to perform specific
279 * functions with the device.
280 */
281 int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
282
283 struct device_driver driver;
284 const struct i2c_device_id *id_table;
285
286 /* Device detection callback for automatic device creation */
287 int (*detect)(struct i2c_client *client, struct i2c_board_info *info);
288 const unsigned short *address_list;
289 struct list_head clients;
290
291 bool disable_i2c_core_irq_mapping;
292 };
第257行,当I2C设备和驱动匹配成功以后probe函数就会执行,和platform驱动一样。
第283行,device_driver驱动结构体,如果使用设备树的话,需要设置device_driver的of_match_table成员变量,也就是驱动的兼容(compatible)属性。
第284行,id_table是传统的、未使用设备树的设备匹配ID表。
对于I2C设备驱动编写人来说,重点工作就是构建i2c_driver,构建完成以后需要向I2C子系统注册这个i2c_driver。i2c_driver注册函数为int i2c_register_driver,此函数原型如下:
c
int i2c_register_driver(struct module *owner,
struct i2c_driver *driver)
函数参数和返回值含义如下:
- owner:一般为THIS_MODULE。
- driver:要注册的i2c_driver。
- 返回值:0,成功;负值,失败。
另外i2c_add_driver也常常用于注册i2c_driver,i2c_add_driver是一个宏,定义如下:
i2c_add_driver就是对i2c_register_driver做了一个简单的封装,只有一个参数,就是要注册
的i2c_driver。
注销I2C设备驱动的时候需要将前面注册的i2c_driver从I2C子系统中注销掉,需要用到
i2c_del_driver函数,此函数原型如下:
c
void i2c_del_driver(struct i2c_driver *driver)
函数参数和返回值含义如下:
- driver:要注销的i2c_driver。
- 返回值:无。
i2c_driver的注册示例代码如下:
c
示例代码41.2.2.4 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 ap3216c_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);
45 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);
第16-19行,i2c_device_id,无设备树的时候匹配ID表。
第22-25行,of_device_id,设备树所使用的匹配表。
第28-37行,i2c_driver,当I2C设备和I2C驱动匹配成功以后probe函数就会执行,这些和platform驱动一样,probe函数里面基本就是标准的字符设备驱动那一套了。
I2C设备和驱动匹配过程
I2C设备和驱动的匹配过程是由I2C子系统核心层来完成的,drivers/i2c/i2c-core-base.c就
是I2C的核心部分,I2C核心提供了一些与具体硬件无关的API函数,比如前面讲过的:
i2c_adapter注册/注销函数
c
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
void i2c_del_adapter(struct i2c_adapter * adap)
i2c_driver注册/注销函数
c
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
int i2c_add_driver (struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)
设备和驱动的匹配过程也是由核心层完成的,I2C总线的数据结构为i2c_bus_type,定义在drivers/i2c/i2c-core-base.c文件,i2c_bus_type内容如下:
.match就是I2C总线的设备和驱动匹配函数,在这里就是i2c_device_match函数,此函数内容如下:
第100行,i2c_of_match_device函数用于完成设备树中定义的设备与驱动匹配过程。比较I2C设备节点的compatible属性和of_device_id中的compatible属性是否相等,如果相当的话就表示I2C设备和驱动匹配。
第104行,acpi_driver_match_device函数用于ACPI形式的匹配。
第110行,i2c_match_id函数用于传统的、无设备树的I2C设备和驱动匹配过程。比较I2C设备名字和i2c_device_id的 name字段是否相等,相等的话就说明I2C设备和驱动匹配成功。
STM32MP1 I2C适配器驱动分析
上一小节讲解了Linux下的I2C子系统,重点分为I2C适配器驱动和I2C设备驱动,其中I2C适配器驱动就是SoC的I2C控制器驱动。I2C设备驱动是需要用户根据不同的I2C从设备去编写,而I2C适配器驱动一般都是SoC厂商去编写的,比如ST就已经提供了STM3MP21的I2C适配器驱动程序。在内核源码arch/arm/boot/dts/stm32mp151.dtsi设备树文件中找到STM32MP1的I2C控制器节点,节点内容如下所示:
重点关注i2c1节点的compatible属性值,因为通过compatible属性值可以在Linux源码里
面找到对应的驱动文件。这里i2c1节点的compatible属性值"st,stm32mp15-i2c",在Linux源码中搜索这个字符串即可找到对应的驱动文件。STM32MP1的I2C适配器驱动驱动文件为drivers/i2c/busses/i2c-stm32f7.c,在此文件中有如下内容:
从示例代码41.3.2可以看出,STM32MP1的I2C适配器驱动是个标准的platform驱动,由此可以看出,虽然I2C总线为别的设备提供了一种总线驱动框架,但是I2C适配器却是platform驱动。
第2529行,"st,stm32mp15-i2c"属性值,设备树中i2c1节点的compatible属性值就是与此匹配上的。因此i2c-stm32f7.c文件就是STM32MP1的I2C适配器驱动文件。
第2533行,当设备和驱动匹配成功以后stm32f7_i2c_probe函数就会执行 ,stm32f7_i2c_probe函数就会完成I2C适配器初始化工作。stm32f7_i2c_probe函数内容如下所示(有省略):
c
示例代码41.3.3 stm32f7_i2c_probe函数代码段
2106 static int stm32f7_i2c_probe(struct platform_device *pdev)
2107 {
2108 struct stm32f7_i2c_dev *i2c_dev;
2109 const struct stm32f7_i2c_setup *setup;
2110 struct resource *res;
2111 u32 rise_time, fall_time;
2112 struct i2c_adapter *adap;
2113 struct reset_control *rst;
2114 dma_addr_t phy_addr;
2115 int irq_error, ret;
2116
2117 i2c_dev = devm_kzalloc(&pdev->dev, sizeof(*i2c_dev), GFP_KERNEL);
2118 if (!i2c_dev)
2119 return -ENOMEM;
2120
2121 res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
2122 i2c_dev->base = devm_ioremap_resource(&pdev->dev, res);
2123 if (IS_ERR(i2c_dev->base))
2124 return PTR_ERR(i2c_dev->base);
2125 phy_addr = (dma_addr_t)res->start;
2126
2127 i2c_dev->irq_event = platform_get_irq(pdev, 0);
2128 if (i2c_dev->irq_event <= 0) {
2129 if (i2c_dev->irq_event != -EPROBE_DEFER)
2130 dev_err(&pdev->dev, "Failed to get IRQ event: %d\n",
2131 i2c_dev->irq_event);
2132 return i2c_dev->irq_event ? : -ENOENT;
2133 }
2134
2135 irq_error = platform_get_irq(pdev, 1);
2136 if (irq_error <= 0) {
2137 if (irq_error != -EPROBE_DEFER)
2138 dev_err(&pdev->dev, "Failed to get IRQ error: %d\n",
2139 irq_error);
2140 return irq_error ? : -ENOENT;
2141 }
......
2159 ret = device_property_read_u32(&pdev->dev, "clock-frequency",
2160 &i2c_dev->bus_rate);
2161 if (ret)
2162 i2c_dev->bus_rate = I2C_STD_RATE;
2163
2164 if (i2c_dev->bus_rate > I2C_FASTPLUS_RATE) {
2165 dev_err(&pdev->dev, "Invalid bus speed (%i>%i)\n",
2166 i2c_dev->bus_rate, I2C_FASTPLUS_RATE);
2167 return -EINVAL;
2168 }
......
2183
2184 ret = devm_request_threaded_irq(&pdev->dev, i2c_dev->irq_event,
2185 stm32f7_i2c_isr_event,
2186 stm32f7_i2c_isr_event_thread,
2187 IRQF_ONESHOT,
2188 pdev->name, i2c_dev);
2189 if (ret) {
2190 dev_err(&pdev->dev, "Failed to request irq event %i\n",
2191 i2c_dev->irq_event);
2192 goto clk_free;
2193 }
2194
2195 ret = devm_request_irq(&pdev->dev, irq_error, stm32f7_i2c_isr_error, 0,
2196 pdev->name, i2c_dev);
2197 if (ret) {
2198 dev_err(&pdev->dev, "Failed to request irq error %i\n",
2199 irq_error);
2200 goto clk_free;
2201 }
2226 if (i2c_dev->bus_rate > I2C_FAST_RATE) {
2227 ret = stm32f7_i2c_setup_fm_plus_bits(pdev, i2c_dev);
2228 if (ret)
2229 goto clk_free;
2230 }
2231
2232 adap = &i2c_dev->adap;
2233 i2c_set_adapdata(adap, i2c_dev);
2234 snprintf(adap->name, sizeof(adap->name), "STM32F7 I2C(%pa)",
2235 &res->start);
2236 adap->owner = THIS_MODULE;
2237 adap->timeout = 2 * HZ;
2238 adap->retries = 3;
2239 adap->algo = &stm32f7_i2c_algo;
2240 adap->dev.parent = &pdev->dev;
2241 adap->dev.of_node = pdev->dev.of_node;
2242
2243 init_completion(&i2c_dev->complete);
2244
2245 /* Init DMA config if supported */
2246 i2c_dev->dma = stm32_i2c_dma_request(i2c_dev->dev, phy_addr,
2247 STM32F7_I2C_TXDR,
2248 STM32F7_I2C_RXDR);
2249 if (PTR_ERR(i2c_dev->dma) == -ENODEV)
2250 i2c_dev->dma = NULL;
2251 else if (IS_ERR(i2c_dev->dma)) {
2252 ret = PTR_ERR(i2c_dev->dma);
2253 goto fmp_clear;
2254 }
......
2276 stm32f7_i2c_hw_config(i2c_dev);
2277
2278 ret = i2c_add_adapter(adap);
2279 if (ret)
2280 goto pm_disable;
......
2307 return 0;
......
2340 }
第2117行,ST使用stm32f7_i2c_dev结构体来表示STM32MP1系列SOC的I2C控制器,这里使用devm_kzalloc函数来申请内存。
第2121-2122行,调用platform_get_resource函数从设备树中获取I2C1控制器寄存器物理基地址 ,也就是0x40012000。获取到寄存器基地址以后使用devm_ioremap_resource函数对其进行内存映射,得到可以在Linux中使用的虚拟地址。
第2127行和第2135行,调用platform_get_irq函数获取中断号。
第2159-2160行,设置I2C频率默认为I2C_STD_RATE=100KHz,如果设备树节点设置了"clock-frequency"属性的话I2C频率就使用clock-frequency属性值。
第2184-2196行,注册I2C控制器的两个中断。
第2232-2241行,stm32f7_i2c_dev结构体有个adap的成员变量,adap就是i2c_adapter,这里初始化i2c_adapter 。第2239行设置i2c_adapter的algo成员变量为stm32f7_i2c_algo,也就是设置i2c_algorithm。
第2246行,申请DMA,看来STM32MP1的I2C适配器驱动是可以采用DMA方式。
第2276行,调用stm32f7_i2c_hw_config函数初始化I2C1控制器的相关硬件寄存器。
第2278行,调用i2c_add_adapter函数向Linux内核注册i2c_adapter。
stm32f7_i2c_probe函数主要的工作就是一下两点:
- 初始化i2c_adapter,设置i2c_algorithm为 stm32f7_i2c_algo,最后向Linux内核注册i2c_adapter。
- 初始化I2C1控制器的相关寄存器。
stm32f7_i2c_algo包含I2C1适配器与I2C设备的通信函数master_xfer,stm32f7_i2c_algo 结构体定义如下:
先来看一下.functionality,functionality用于返回此I2C适配器支持什么样的通信协议,在这里functionality就是stm32f7_i2c_func函数,stm32f7_i2c_func函数内容如下:
重点来看一下stm32f7_i2c_xfer函数 ,因为最终就是通过此函数来完成与I2C设备通信的,此函数内容如下:
c
示例代码41.3.6stm32f7_i2c_xfer函数
1657 static int stm32f7_i2c_xfer(struct i2c_adapter *i2c_adap,
1658 struct i2c_msg msgs[], int num)
1659 {
1660 struct stm32f7_i2c_dev *i2c_dev = i2c_get_adapdata(i2c_adap);
1661 struct stm32f7_i2c_msg *f7_msg = &i2c_dev->f7_msg;
1662 struct stm32_i2c_dma *dma = i2c_dev->dma;
1663 unsigned long time_left;
1664 int ret;
1665
1666 i2c_dev->msg = msgs;
1667 i2c_dev->msg_num = num;
1668 i2c_dev->msg_id = 0;
1669 f7_msg->smbus = false;
1670
1671 ret = pm_runtime_get_sync(i2c_dev->dev);
1672 if (ret < 0)
1673 return ret;
1674
1675 ret = stm32f7_i2c_wait_free_bus(i2c_dev);
1676 if (ret)
1677 goto pm_free;
1678
1679 stm32f7_i2c_xfer_msg(i2c_dev, msgs);
1680
1681 time_left = wait_for_completion_timeout(&i2c_dev->complete,
1682 i2c_dev->adap.timeout);
1683 ret = f7_msg->result;
1684
1685 if (!time_left) {
1686 dev_dbg(i2c_dev->dev, "Access to slave 0x%x timed out\n",
1687 i2c_dev->msg->addr);
1688 if (i2c_dev->use_dma)
1689 dmaengine_terminate_all(dma->chan_using);
1690 ret = -ETIMEDOUT;
1691 }
1692
1693 pm_free:
1694 pm_runtime_mark_last_busy(i2c_dev->dev);
1695 pm_runtime_put_autosuspend(i2c_dev->dev);
1696
1697 return (ret < 0) ? ret : num;
1698 }
第1675行,调用stm32f7_i2c_wait_free_bus函数等待I2C总线空闲,也就是读取I2C控制的ISR寄存器的bit15(BUSY)位,此位用来标记I2C控制器是否忙。
第1679行,调用stm32f7_i2c_xfer_msg函数发送数据,此函数也是操作I2C控制器硬件寄存器的。
I2C设备驱动编写流程
I2C适配器驱动SOC厂商已经编写好了,需要做的就是编写具体的设备驱动,本小节就来学习一下I2C设备驱动的详细编写流程。
I2C设备信息描述
未使用设备树
首先肯定要描述I2C设备节点信息,先来看一下没有使用设备树的时候是如何在BSP里面描述I2C设备信息的,在未使用设备树的时候需要在BSP里面使用i2c_board_info结构体来描述一个具体的I2C设备。i2c_board_info结构体如下:
type和addr这两个成员变量是必须要设置的,一个是I2C设备的名字,一个是I2C设备的器件地址。举个例子,打开arch/arm/mach-imx/mach-armadillo5x0.c文件,此文件中有关于s35390a这个I2C器件对应的设备描述信息:
示例代码41.4.1.2中使用I2C_BOARD_INFO来完成armadillo5x0_i2c_rtc的初始化工作,I2C_BOARD_INFO是一个宏,定义如下:
可以看出I2C_BOARD_INFO宏其实就是设置i2c_board_info的type和addr这两个成员变量,因此示例代码41.4.1.2的主要工作就是设置I2C设备名字为s35390a,器件地址为0X30。
可以在Linux源码里面全局搜索i2c_board_info,会找到大量以i2c_board_info定义的I2C设备信息,这些就是未使用设备树的时候I2C设备的描述方式,当采用了设备树以后就不会再使用i2c_board_info来描述I2C设备了。
使用设备树
使用设备树的时候I2C设备信息通过创建相应的节点就行了,比如在STM32MP1的开发板上有一个I2C器件AP3216C,这是三合一的环境传感器,并且该器件挂在STM32MP1的I2C5总线接口上,因此必须在i2c5节点下创建一个子节点来描述AP3216C设备,节点示例如下所示:
c
示例代码41.4.1.4 i2c从设备节点示例
1 &i2c5 {
2 pinctrl-names = "default", "sleep";
3 pinctrl-0 = <&i2c5_pins_a>;
4 pinctrl-1 = <&i2c5_pins_sleep_a>;
5 status = "okay";
6
7 ap3216c@1e {
8 compatible = " alientek,ap3216c";
9 reg = <0x1e>;
10 };
11 };
第2-4行,设置了i2c5的pinmux的配置。
第7-10行,向i2c5添加ap3216c子节点,第7行"ap3216c@1e"是子节点名字 ,"@"后面的"1e"就是ap3216c的I2C器件地址。第8行设置compatible属性值为"alientek,ap3216c"。
第9行的reg属性也是设置ap3216c的器件地址的,因此值为0x1e。
I2C设备节点的创建重点是compatible属性和reg属性的设置,一个用于匹配驱动,一个用于设置器件地址。
I2C设备数据收发处理流程
I2C设备驱动首先要做的就是初始化i2c_driver并向Linux内核注册 。当设备和驱动匹配以后i2c_driver里面的probe函数就会执行,probe函数里面所做的就是字符设备驱动那一套了。一般需要在probe函数里面初始化I2C设备 ,要初始化I2C设备就必须能够对I2C设备寄存器进行读写操作,这里就要用到i2c_transfer函数 了。i2c_transfer函数最终会调用I2C适配器中i2c_algorithm里面的master_xfer函数,对于STM32MP1而言就是stm32f7_i2c_xfer这个函数。i2c_transfer函数原型如下:
c
int i2c_transfer(struct i2c_adapter *adap,
struct i2c_msg *msgs,
int num)
函数参数和返回值含义如下:
- adap:所使用的I2C适配器,i2c_client会保存其对应的i2c_adapter。
- msgs:I2C要发送的一个或多个消息。
- num:消息数量,也就是msgs的数量。
- 返回值:负值,失败,其他非负值,发送的msgs数量。
重点来看一下msgs这个参数,这是一个i2c_msg类型的指针参数 ,I2C进行数据收发就是消息的传递,Linux内核使用i2c_msg结构体来描述一个消息。i2c_msg结构体定义在include/uapi/linux/i2c.h文件中,结构体内容如下:
使用i2c_transfer函数发送数据之前要先构建好i2c_msg,使用i2c_transfer进行I2C数据收
发的示例代码如下:
c
示例代码41.4.2.2 I2C设备多寄存器数据读写
1 /* 设备结构体 */
2 struct xxx_dev {
3 ......
4 void *private_data; /* 私有数据,一般会设置为i2c_client */
5 };
6
7 /*
8 * @description : 读取I2C设备多个寄存器数据
9 * @param -- dev : I2C设备
10 * @param -- reg : 要读取的寄存器首地址
11 * @param -- val : 读取到的数据
12 * @param -- len : 要读取的数据长度
13 * @return : 操作结果
14 */
15 static int xxx_read_regs(struct xxx_dev *dev, u8 reg, void *val, int len)
16 {
17 int ret;
18 struct i2c_msg msg[2];
19 struct i2c_client *client = (struct i2c_client *) dev->private_data;
20
21 /* msg[0],第一条写消息,发送要读取的寄存器首地址 */
22 msg[0].addr = client->addr; /* I2C器件地址 */
23 msg[0].flags = 0; /* 标记为发送数据 */
24 msg[0].buf = ® /* 读取的首地址 */
25 msg[0].len = 1; /* reg长度 */
26
27 /* msg[1],第二条读消息,读取寄存器数据 */
28 msg[1].addr = client->addr; /* I2C器件地址 */
29 msg[1].flags = I2C_M_RD; /* 标记为读取数据 */
30 msg[1].buf = val; /* 读取数据缓冲区 */
31 msg[1].len = len; /* 要读取的数据长度 */
32
33 ret = i2c_transfer(client->adapter, msg, 2);
34 if(ret == 2) {
35 ret = 0;
36 } else {
37 ret = -EREMOTEIO;
38 }
39 return ret;
40 }
41
42 /*
43 * @description : 向I2C设备多个寄存器写入数据
44 * @param -- dev : 要写入的设备结构体
45 * @param -- reg : 要写入的寄存器首地址
46 * @param -- val : 要写入的数据缓冲区
47 * @param -- len : 要写入的数据长度
48 * @return : 操作结果
49 */
50 static s32 xxx_write_regs(struct xxx_dev *dev, u8 reg, u8 *buf, u8 len)
51 {
52 u8 b[256];
53 struct i2c_msg msg;
54 struct i2c_client *client = (struct i2c_client *) dev->private_data;
55
56 b[0] = reg; /* 寄存器首地址 */
57 memcpy(&b[1],buf,len); /* 将要发送的数据拷贝到数组b里面 */
58
59 msg.addr = client->addr; /* I2C器件地址 */
60 msg.flags = 0; /* 标记为写数据 */
61
62 msg.buf = b; /* 要发送的数据缓冲区 */
63 msg.len = len + 1; /* 要发送的数据长度 */
64
65 return i2c_transfer(client->adapter, &msg, 1);
66 }
第2-5行,设备结构体,在设备结构体里面添加一个执行void的指针成员变量private_data,此成员变量用于保存设备的私有数据。在I2C设备驱动中一般将其指向I2C设备对应的i2c_client。
第15-40行,xxx_read_regs函数用于读取I2C设备多个寄存器数据。第18行定义了一个i2c_msg数组,2个数组元素,因为I2C读取数据的时候要先发送要读取的寄存器地址,然后再读取数据,所以需要准备两个i2c_msg。一个用于发送寄存器地址,一个用于读取寄存器值。对于msg[0],将flags设置为0,表示写数据。msg[0]的addr是I2C设备的器件地址,msg[0]的buf成员变量就是要读取的寄存器地址。对于msg[1],将flags设置为I2C_M_RD,表示读取数据。msg[1]的buf成员变量用于保存读取到的数据,len成员变量就是要读取的数据长度。调用i2c_transfer函数完成I2C数据读操作。
第50-66行,xxx_write_regs函数用于向I2C设备多个寄存器写数据,I2C写操作要比读操作简单一点,因此一个i2c_msg即可。数组b用于存放寄存器首地址和要发送的数据,第59行设置msg的addr为I2C器件地址。第60行设置msg的flags为0,也就是写数据。第62行设
置要发送的数据,也就是数组b。第63行设置msg的len为len+1,因为要加上一个字节的寄存器地址。最后通过i2c_transfer函数完成向I2C设备的写操作。
另外还有两个API函数分别用于I2C数据的收发操作,这两个函数最终都会调用i2c_transfer。
首先来看一下I2C数据发送函数i2c_master_send,函数原型如下:
c
int i2c_master_send(const struct i2c_client *client,
const char *buf,
int count)
函数参数和返回值含义如下:
- client:I2C设备对应的i2c_client。
- buf:要发送的数据。
- count:要发送的数据字节数,要小于64KB,因为i2c_msg的len成员变量是一个u16(无符号16位)类型的数据。
- 返回值:负值,失败;其他非负值,发送的字节数。
I2C数据接收函数为i2c_master_recv,函数原型如下:
c
int i2c_master_recv(const struct i2c_client *client,
char *buf,
int count)
函数参数和返回值含义如下:
- client:I2C设备对应的i2c_client。
- buf:要接收的数据。
- count:要接收的数据字节数,要小于64KB,因为i2c_msg的len成员变量是一个u16(无符号16位)类型的数据。
- 返回值:负值,失败;其他非负值,发送的字节数。
关于Linux下I2C设备驱动的编写流程就讲解到这里,重点就是i2c_msg的构建和i2c_transfer函数的调用,接下来就编写AP3216C这个I2C设备的Linux驱动。
硬件原理图分析
AP3216C的原理图如下图所示:
从上图可以看出AP3216C使用的是I2C5,其中I2C5_SCL使用的是PA11这个IO,I2C_SDA使用的是PA12这个IO。AP3216C还有个中断引脚,这里没有用到中断功能。
实验程序编写
IO修改或添加
AP3216C用到了I2C5接口。因为I2C5所使用的IO分别为PA11和PA12,所以要根据数据手册设置I2C5的pinmux的配置 。如果要用到AP3216C的中断功能的话还需要初始化AP_INT对应的PE4这个 IO,本章实验不使用中断功能。因此只需要设置PA11和PA12这两个IO复用为AF4功能,ST其实已经将这个两个IO设置好了 ,打开stm32mp15-pinctrl.dtsi
然后找到如下内容:
c
示例代码41.6.1.1 I2C5的pinmux配置
1 i2c5_pins_a: i2c5-0 {
2 pins {
3 pinmux = <STM32_PINMUX('A', 11, AF4)>, /* I2C5_SCL */
4 <STM32_PINMUX('A', 12, AF4)>; /* I2C5_SDA */
5 bias-disable;
6 drive-open-drain;
7 slew-rate = <0>;
8 };
9 };
10
11 i2c5_pins_sleep_a: i2c5-1 {
12 pins {
13 pinmux = <STM32_PINMUX('A', 11, ANALOG)>, /* I2C5_SCL */
14 <STM32_PINMUX('A', 12, ANALOG)>; /* I2C5_SDA */
15
16 };
17 };
示例代码41.6.1.1中,定义了I2C5接口的两个pinmux配置分别为:i2c5_pins_a和i2c5_pins_sleep_a。第一个默认的状态下使用,第二个是在sleep状态下使用。
在i2c5节点追加ap3216c子节点
接着打开stm32mp157d-atk.dts文件,通过节点内容追加的方式,向i2c5节点中添加"ap3216c@1e"子节点,节点如下所示:
c
示例代码41.6.1.2 向i2c5追加ap3216c子节点
1 &i2c5 {
2 pinctrl-names = "default", "sleep";
3 pinctrl-0 = <&i2c5_pins_a>;
4 pinctrl-1 = <&i2c5_pins_sleep_a>;
5 status = "okay";
6
7 ap3216c@1e {
8 compatible = "alientek,ap3216c";
9 reg = <0x1e>;
10 };
11 };
第2-4行,给I2C5节点设置了pinmux配置。
第7行,ap3216c子节点,@后面的"1e"是ap3216c的器件地址。
第8行,设置compatible值为"alientek,ap3216c"。
第9行,reg属性也是设置ap3216c器件地址的,因此reg设置为0x1e。
设备树修改完成以后使用"make dtbs"重新编译一下,然后使用新的设备树启动Linux内核。/sys/bus/i2c/devices目录下存放着所有I2C设备,如果设备树修改正确的话,会在/sys/bus/i2c/devices目录下看到一个名为"0-001e"的子目录,如下图所示:
上图中的"0-001e"就是ap3216c的设备目录,"1e"就是ap3216c器件地址。进入0-001e目录,可以看到"name"文件,name文件保存着此设备名字,在这里就是"ap3216c"。
AP3216C驱动编写
需要先创建一个ap3216creg.h文件,是一个寄存器头文件,要在其中保存寄存器的地址。
ap3216c.c就是正式的驱动程序。
首先创建设备结构体,与之前的字符设备区别就是,需要添加一个i2c_client结构体指针*client表示i2c设备,以及最后要添加光传感器的unsigner short变量ir,als,ps。
接着编写ap3216c_read_regs来读取寄存器数据,这个与之前源码解读中的内容是很类似的,创建一个i2c_msg结构体类型的数组msg[2],msg[0]用来保存发送的首地址,msg[1]保存接收的首地址;然后调用i2c_transfer进行传输。
编写ap3216c_write_regs来写入寄存器,同样创建一个i2c_msg结构体类型的msg,并定义一个u8的b[256]数组,其保存寄存器首地址,并通过memcpy把数据拷贝到b中;msg则是设子ap3216c的地址,标记,buf就是刚才的b,写入长度是len+1(还有寄存器地址需要写入),最后直接return出来i2c_transfer。
然后进行封装,ap3216c_read_reg里面调用刚写好的ap3216c_read_regs;ap3216c_write_reg调用ap3216c_write_regs。
之后编写ap3216c_readdata来读取AP3216C的数据,这里就是要注意数据读取需要有大于112.5ms的时间间隔,这边应该就是根据这个传感器的手册来编写的读取的函数。
接着编写ap3216c_open这个打开设备的函数,首先要通过filp获取cdev指针,再通过cdev获取ap3216c_dev的首地址;然后就是通过ap3216c_write_reg进行传感器的初始化。
接着编写ap3216c_read从设备读取数据,同样的方法获取ap3216c_dev的首地址之后,通过ap3216c_readdata读取,然后传入自定义的short类型的data[3]数组,之后copy_to_user读取。
关闭设备就是release函数,里面直接return 0就可以。
操作函数集file_operations就是open、read和release函数。
接着就是probe函数ap3216c_probe,里面需要通过devm_kzalloc申请ap3216cdev的空间,然后就是注册字符设备驱动的常规操作,alloc_chrdev_region创建设备号,然后ap3216cdev->cdev.owner就是THIS_MODULE,cdev_init初始化cdev,cdev_add添加cdev,然后class_create创建类,device_create创建设备;区别是,最后要i2c_set_clientdata保存一下ap3216cdev结构体。
驱动的remove函数,需要先i2c_get_clientdata获取cdev,然后就是老样子cdev_del删除cdev,unregister_chrdev_region注销设备号,device_destroy注销设备然后class_destroy注销类。
建立一个ID的匹配列表,i2c_device_id结构体类型的ap3216c_id[]的数组,里面就是匹配的"alientek,ap3216c" ;还有一个of_device_id结构体类型的ap3216c_of_match[]数组保存.compatible属性。
建立i2c驱动结构体i2c_driver结构体类型的ap3216c_driver,保存.probe和.remove函数,.driver就是.owner属性,.name以及.of_match_table属性,最后要加一个.id_table。(这里如果使用设备树,这个id_table是可以不用的,那个是传统的没有设备数的时候才需要的)
最后就是驱动的入口和出口函数,这里分别是ap3216c_init调用i2c_add_driver;以及ap3216c_exit调用i2c_del_driver。
最最后面就是module_init和module_exit,以及MODULE_LICENSE、MODULE_AUTHER以及MODULE_INFO。
编写测试APP
传入的argc是2个。
filename=argv[1]之后,open打开字符设备,然后通过while(1)死循环,read得到数据并分别把3个传感器数据保存,死循环外面再加一个close即可。
运行测试
编译驱动程序
这里还是一样,就把Makefile的obj-m改成ap3216c.o,然后"make"就可以了。
编译测试APP
可以通过如下命令:
|---------------------------------------------------------|
| arm-none-linux-gnueabihf-gcc ap3216cApp.c -o ap3216cApp |
运行测试
将上一小节编译出来ap3216c.ko和ap3216cApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中。输入如下命令加载ap3216c.ko这个驱动模块:
|------------------------------------------------------|
| depmod //第一次加载驱动的时候需要运行此命令 modprobe ap3216c //加载驱动模块 |
加载成功以后,可以通过如下命令测试:
|---------------------------|
| ./ap3216cApp /dev/ap3216c |
测试APP会不断从AP3216C中读取数据,并打印到终端,如下图所示:
总结
与裸机开发还是有所区别,在裸机开发的时候,当时的说法是IIC因为专利等问题,一般都是不会直接用硬件IIC的,当时是直接手动GPIO来软件实现IIC的。
在STM32MP1这边,I2C就是直接采用硬件I2C,需要关注的就是怎么在设备树中添加i2c节点,然后驱动程序的写法就可以了。
pinctrl一般都是会有写好的,就是要在自己的设备树里面添加对应的i2c节点。
驱动程序中,具体的传感器读取,需要参考传感器的使用文档,I2C的读写在本篇笔记中是有一个模板的,只要把他搬过去就可以了;至于驱动程序的其他部分,基本就是字符设备的基本驱动代码,区别就是获取设备首地址的时候,要先通过cdev=filp->f_path.dentry->d_inode->i_cdev; 获取cdev首地址之后,再通过container_of获取传感器设备的首地址。