正点原子嵌入式linux驱动开发——Linux I2C驱动

在电子产品硬件设计当中,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部分特性如下:

  1. 兼容I2C总线规范第03版。
  2. 支持从模式和主模式,支持多主模式功能。
  3. 支持标准模式(Sm)、快速模式(Fm)和超快速模式(Fm+),其中,标准模式100kHz,快速模式400 kHz,超快速模式可以到1MHz。
  4. 7 位和10位寻址模式。
  5. 多个7位从地址,所有7位地址应答模式。
  6. 软件复位。
  7. 带DMA功能的1字节缓冲。
  8. 广播呼叫。

AP3216C简介

STM32MP1开发板上通过I2C5连接了一个三合一环境传感器:AP3216C 。AP3216C是由敦南科技推出的一款传感器,其支持环境光强度(ALS)、接近距离(PS)和红外线强度(IR)这三个环境参数检测。该芯片可以通过IIC接口与主控制相连,并且支持中断,AP3216C的特点如下:

  1. I2C接口,快速模式下波特率可以到400Kbit/S。
  2. 多种工作模式选择:ALS、PS+IR、ALS+PS+IR、PD等等。
  3. 内建温度补偿电路。
  4. 宽工作温度范围(-30 C - +80 C)。
  5. 超小封装,4.1mm x 2.4mm x 1.35mm。
  6. 环境光传感器具有16位分辨率。
  7. 接近传感器和红外传感器具有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器件的驱动程序,一般需要实现两部分:

  1. I2C 主机驱动。
  2. 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函数主要的工作就是一下两点:

  1. 初始化i2c_adapter,设置i2c_algorithm为 stm32f7_i2c_algo,最后向Linux内核注册i2c_adapter。
  2. 初始化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 = &reg; /* 读取的首地址 */ 
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获取传感器设备的首地址。

相关推荐
路溪非溪15 分钟前
关于Linux内核中头文件问题相关总结
linux
Lovyk3 小时前
Linux 正则表达式
linux·运维
好望角雾眠3 小时前
第一阶段C#基础-10:集合(Arraylist,list,Dictionary等)
笔记·学习·c#
艾伦~耶格尔3 小时前
【集合框架LinkedList底层添加元素机制】
java·开发语言·学习·面试
星仔编程3 小时前
python学习DAY46打卡
学习
Fireworkitte4 小时前
Ubuntu、CentOS、AlmaLinux 9.5的 rc.local实现 开机启动
linux·ubuntu·centos
大霞上仙4 小时前
实现自学习系统,输入excel文件,能学习后进行相应回答
python·学习·excel
sword devil9004 小时前
ubuntu常见问题汇总
linux·ubuntu
ac.char4 小时前
在CentOS系统中查询已删除但仍占用磁盘空间的文件
linux·运维·centos
曙曙学编程5 小时前
stm32——GPIO
c语言·c++·stm32·单片机·嵌入式硬件