• 正点原子嵌入式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写时序

    下图是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获取传感器设备的首地址。

  • 相关阅读:
    HackQuest介绍 web3 学习平台
    Mysql数据库 2.SQL语言 数据类型与字段约束
    高项 07 项目成本管理
    10.23 校招 实习 内推 面经
    计算机毕业设计Java优乐帮育儿系统(系统+程序+mysql数据库+Lw文档)
    vue修饰符的用法
    云仓是什么?如何选择优质云仓?
    项目——群英阁(galaxyHub)
    可视化工具Visdom不显示toolbar和窗口(蓝屏),浏览器工具栏找不到解决方法
    八叉树的范围和射线检测
  • 原文地址:https://blog.csdn.net/xhj12138/article/details/134028745