当前位置:   article > 正文

正点原子嵌入式linux驱动开发——Linux I2C驱动_linux硬件i2c

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写时序

下图是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结构如下图所示:

AP3216C结构图

AP3216的设备地址为0X1E,同几乎所有的 I2C从器件一样,AP3216C内部也有一些寄存器,通过这些寄存器可以配置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核心(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文件中,结构体内容如下:

i2c_adapter结构体

第688行,i2c_algorithm类型的指针变量algo,对于一个I2C适配器,肯定要对外提供读写API函数,设备驱动程序可以使用这些API函数来完成读写操作。i2c_algorithm就是I2C适配器与IIC设备进行通信的方法

i2c_algorithm结构体定义在include/linux/i2c.h文件中,内容如下:

i2c_algorithm结构体

第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,这两个 函数的原型如下:

int i2c_add_adapter(struct i2c_adapter *adapter) 
int i2c_add_numbered_adapter(struct i2c_adapter *adap)
  • 1
  • 2

这两个函数的区别在于i2c_add_adapter会动态分配一个总线编号,而i2c_add_numbered_adapter函数则指定一个静态的总线编号。函数参数和返回值含义如下:

  • adapter或adap:要添加到Linux内核中的i2c_adapter,也就是I2C适配器。
  • 返回值:0,成功;负值,失败。

如果要删除I2C适配器的话使用i2c_del_adapter函数即可,函数原型如下:

void i2c_del_adapter(struct i2c_adapter * adap)
  • 1

函数参数和返回值含义如下:

  • 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_client结构体

一个I2C设备对应一个i2c_client结构体变量,系统每检测到一个I2C从设备就会给这个设备分配一个i2c_client。

i2c_driver结构体

i2c_driver类似platform_driver,是编写I2C设备驱动重点要处理的内容,i2c_driver结构体定义在include/linux/i2c.h文件中,内容如下:

示例代码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 };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

第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,此函数原型如下:

int i2c_register_driver(struct module *owner, 
						struct i2c_driver *driver)
  • 1
  • 2

函数参数和返回值含义如下:

  • owner:一般为THIS_MODULE。
  • driver:要注册的i2c_driver。
  • 返回值:0,成功;负值,失败。

另外i2c_add_driver也常常用于注册i2c_driver,i2c_add_driver是一个宏,定义如下:

i2c_add_driver宏

i2c_add_driver就是对i2c_register_driver做了一个简单的封装,只有一个参数,就是要注册
的i2c_driver。

注销I2C设备驱动的时候需要将前面注册的i2c_driver从I2C子系统中注销掉,需要用到
i2c_del_driver函数
,此函数原型如下:

void i2c_del_driver(struct i2c_driver *driver) 
  • 1

函数参数和返回值含义如下:

  • driver:要注销的i2c_driver。
  • 返回值:无。

i2c_driver的注册示例代码如下:

示例代码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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

第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注册/注销函数

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)
  • 1
  • 2
  • 3

i2c_driver注册/注销函数

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)
  • 1
  • 2
  • 3

设备和驱动的匹配过程也是由核心层完成的,I2C总线的数据结构为i2c_bus_type,定义在drivers/i2c/i2c-core-base.c文件,i2c_bus_type内容如下:

i2c_bus_type结构体

.match就是I2C总线的设备和驱动匹配函数,在这里就是i2c_device_match函数,此函数内容如下:

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控制器节点

重点关注i2c1节点的compatible属性值,因为通过compatible属性值可以在Linux源码里
面找到对应的驱动文件
。这里i2c1节点的compatible属性值“st,stm32mp15-i2c”,在Linux源码中搜索这个字符串即可找到对应的驱动文件。STM32MP1的I2C适配器驱动驱动文件为drivers/i2c/busses/i2c-stm32f7.c,在此文件中有如下内容:

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函数内容如下所示(有省略):

示例代码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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108

第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 结构体定义如下:

stm32f7_i2c_algo结构体

先来看一下.functionality,functionality用于返回此I2C适配器支持什么样的通信协议,在这里functionality就是stm32f7_i2c_func函数,stm32f7_i2c_func函数内容如下:

stm32f7_i2c_func函数

重点来看一下stm32f7_i2c_xfer函数 ,因为最终就是通过此函数来完成与I2C设备通信的,此函数内容如下:

示例代码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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

第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结构体如下:

i2c_board_info结构体

type和addr这两个成员变量是必须要设置的,一个是I2C设备的名字,一个是I2C设备的器件地址。举个例子,打开arch/arm/mach-imx/mach-armadillo5x0.c文件,此文件中有关于s35390a这个I2C器件对应的设备描述信息:

s35390a的I2C设备信息

示例代码41.4.1.2中使用I2C_BOARD_INFO来完成armadillo5x0_i2c_rtc的初始化工作,I2C_BOARD_INFO是一个宏,定义如下:

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设备,节点示例如下所示:

示例代码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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

第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函数原型如下:

int i2c_transfer(struct i2c_adapter *adap, 
				 struct i2c_msg *msgs, 
				 int num)
  • 1
  • 2
  • 3

函数参数和返回值含义如下:

  • 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_msg结构体

使用i2c_transfer函数发送数据之前要先构建好i2c_msg,使用i2c_transfer进行I2C数据收
发的示例代码如下:

示例代码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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67

第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,函数原型如下:

int i2c_master_send(const struct i2c_client *client, 
					const char *buf, 
					int count)
  • 1
  • 2
  • 3

函数参数和返回值含义如下:

  • client:I2C设备对应的i2c_client。
  • buf:要发送的数据。
  • count:要发送的数据字节数,要小于64KB,因为i2c_msg的len成员变量是一个u16(无符号16位)类型的数据。
  • 返回值:负值,失败;其他非负值,发送的字节数。

I2C数据接收函数为i2c_master_recv,函数原型如下:

int i2c_master_recv(const struct i2c_client *client, 
					char *buf, 
					int count)
  • 1
  • 2
  • 3

函数参数和返回值含义如下:

  • client:I2C设备对应的i2c_client。
  • buf:要接收的数据。
  • count:要接收的数据字节数,要小于64KB,因为i2c_msg的len成员变量是一个u16(无符号16位)类型的数据。
  • 返回值:负值,失败;其他非负值,发送的字节数。

关于Linux下I2C设备驱动的编写流程就讲解到这里,重点就是i2c_msg的构建和i2c_transfer函数的调用,接下来就编写AP3216C这个I2C设备的Linux驱动。

硬件原理图分析

AP3216C的原理图如下图所示:

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
然后找到如下内容:

示例代码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 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

示例代码41.6.1.1中,定义了I2C5接口的两个pinmux配置分别为:i2c5_pins_a和i2c5_pins_sleep_a。第一个默认的状态下使用,第二个是在sleep状态下使用。

在i2c5节点追加ap3216c子节点

接着打开stm32mp157d-atk.dts文件,通过节点内容追加的方式,向i2c5节点中添加“ap3216c@1e”子节点,节点如下所示:

示例代码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 };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

第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”的子目录,如下图所示:

当前系统I2C设备

上图中的“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中读取数据,并打印到终端,如下图所示:

获取到的AP3216C数据

总结

与裸机开发还是有所区别,在裸机开发的时候,当时的说法是IIC因为专利等问题,一般都是不会直接用硬件IIC的,当时是直接手动GPIO来软件实现IIC的。

在STM32MP1这边,I2C就是直接采用硬件I2C,需要关注的就是怎么在设备树中添加i2c节点,然后驱动程序的写法就可以了。

pinctrl一般都是会有写好的,就是要在自己的设备树里面添加对应的i2c节点。

驱动程序中,具体的传感器读取,需要参考传感器的使用文档,I2C的读写在本篇笔记中是有一个模板的,只要把他搬过去就可以了;至于驱动程序的其他部分,基本就是字符设备的基本驱动代码,区别就是获取设备首地址的时候,要先通过cdev=filp->f_path.dentry->d_inode->i_cdev; 获取cdev首地址之后,再通过container_of获取传感器设备的首地址。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/205825
推荐阅读
相关标签
  

闽ICP备14008679号