1)实验平台:正点原子STM32MP157开发板
2)购买链接:https://item.taobao.com/item.htm?&id=629270721801
3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-318813-1-1.html
4)正点原子官方B站:https://space.bilibili.com/394620890
5)正点原子STM32MP157技术交流群:691905614

第四十章 Linux I2C驱动实验

对于 I2C 我相信大家都很熟悉,基本上做过单片机开发的朋友都接触过,在电子产品硬件设计当中,I2C 是一种很常见的同步、串行、低速、近距离通信接口,用于连接各种 IC、传感器等器件,它们都会提供 I2C 接口与 SoC 主控相连,比如陀螺仪、加速度计、触摸屏等,其最大优势在于可以在总线上扩展多个外围设备的支持。
Linux 内核开发者为了让驱动开发工程师在内核中方便的添加自己的 I2C 设备驱动程序,更容易的在 linux 下驱动自己的 I2C 接口硬件,进而引入了 I2C 总线框架。与 Linux 下的 platform 虚拟总线不同的是,I2C 是实际的物理总线,所以 I2C 总线框架也是Linux 下总线、设备、驱动模型的产物。
本章我们来学习一下如何在 Linux 下的 I2C 总线框架,以及如何使用 I2C 总线框架编写一个 I2C 接口的外设驱动程序;本章重点是学习 Linux 下的 I2C 总线框架。

40.1 I2C & AP3216C简介
40.1.1 I2C简介
I2C是很常见的一种总线协议,I2C是NXP公司设计的,I2C使用两条线在主控制器和从机之间进行数据通信。一条是SCL(串行时钟线),另外一条是SDA(串行数据线),这两条数据线需要接上拉电阻,总线空闲的时候SCL和SDA处于高电平。I2C总线标准模式下速度可以达到100Kb/S,快速模式下可以达到400Kb/S。I2C总线工作是按照一定的协议来运行的,接下来就看一下I2C协议。
I2C是支持多从机的,也就是一个I2C控制器下可以挂多个I2C从设备,这些不同的I2C从设备有不同的器件地址,这样I2C主控制器就可以通过I2C设备的器件地址访问指定的I2C设备了,一个I2C总线连接多个I2C设备如图40.1.1.1所示:

图40.1.1.1 I2C多个设备连接结构图
图40.1.1.1中SDA和SCL这两根线必须要接一个上拉电阻,一般是4.7K。其余的I2C从器件都挂接到SDA和SCL这两根线上,这样就可以通过SDA和SCL这两根线来访问多个I2C设备。
接下来看一下I2C协议有关的术语:
1、起始位
顾名思义,也就是I2C通信起始标志,通过这个起始位就可以告诉I2C从机,“我”要开始进行I2C通信了。在SCL为高电平的时候,SDA出现下降沿就表示为起始位,如图40.1.1.2所示:

图40.1.1.2 I2C通信起始位
2、停止位
停止位就是停止I2C通信的标志位,和起始位的功能相反。在SCL位高电平的时候,SDA出现上升沿就表示为停止位,如图40.1.1.3所示:

图40.1.1.3 I2C通信停止位
3、数据传输
I2C总线在数据传输的时候要保证在SCL高电平期间,SDA上的数据稳定,因此SDA上的数据变化只能在SCL低电平期间发生,如图40.1.1.4所示:

图40.1.1.4 I2C数据传输
4、应答信号
当I2C主机发送完8位数据以后会将SDA设置为输入状态,等待I2C从机应答,也就是等待I2C从机告诉主机它接收到数据了。应答信号是由从机发出的,主机需要提供应答信号所需的时钟,主机发送完8位数据以后紧跟着的一个时钟信号就是给应答信号使用的。从机通过将SDA拉低来表示发出应答信号,表示通信成功,否则表示通信失败。
5、I2C写时序
主机通过I2C总线与从机之间进行通信不外乎两个操作:写和读,I2C总线单字节写时序如图40.1.1.5所示:

图40.1.1.5 I2C写时序
图40.1.1.5就是I2C写时序,我们来看一下写时序的具体步骤:
1)、开始信号。
2)、发送I2C设备地址,每个I2C器件都有一个设备地址,通过发送具体的设备地址来决定访问哪个I2C器件。这是一个8位的数据,其中高7位是设备地址,最后1位是读写位,为1的话表示这是一个读操作,为0的话表示这是一个写操作。
3)、 I2C器件地址后面跟着一个读写位,为0表示写操作,为1表示读操作。
4)、从机发送的ACK应答信号。
5)、重新发送开始信号。
6)、发送要写写入数据的寄存器地址。
7)、从机发送的ACK应答信号。
8)、发送要写入寄存器的数据。
9)、从机发送的ACK应答信号。
10)、停止信号。
6、I2C读时序
I2C总线单字节读时序如图40.1.1.6所示:

图40.1.1.6 I2C单字节读时序
I2C单字节读时序比写时序要复杂一点,读时序分为4大步,第一步是发送设备地址,第二步是发送要读取的寄存器地址,第三步重新发送设备地址,最后一步就是I2C从器件输出要读取的寄存器值,我们具体来看一下这步。
1)、主机发送起始信号。
2)、主机发送要读取的I2C从设备地址。
3)、读写控制位,因为是向I2C从设备发送数据,因此是写信号。
4)、从机发送的ACK应答信号。
5)、重新发送START信号。
6)、主机发送要读取的寄存器地址。
7)、从机发送的ACK应答信号。
8)、重新发送START信号。
9)、重新发送要读取的I2C从设备地址。
10)、读写控制位,这里是读信号,表示接下来是从I2C从设备里面读取数据。
11)、从机发送的ACK应答信号。
12)、从I2C器件里面读取到的数据。
13)、主机发出NO ACK信号,表示读取完成,不需要从机再发送ACK信号了。
14)、主机发出STOP信号,停止I2C通信。
7、I2C多字节读写时序
有时候我们需要读写多个字节,多字节读写时序和单字节的基本一致,只是在读写数据的时候可以连续发送多个自己的数据,其他的控制时序都是和单字节一样的。
40.1.2 STM32MP1 I2C 简介
STM32MP157D 有 6 个 I2C 接口,其中 I2C4 和 I2C6 可以在 A7 安全模式或者 A7 非安全模式下使用,M4 无法使用,STM32MP157 的 I2C 部分特性如下:
①、兼容I2C总线规范第03版。
②、支持从模式和主模式,支持多主模式功能。
③、支持标准模式 (Sm)、快速模式 (Fm) 和超快速模式 (Fm+),其中,标准模式100kHz,快速模式400 kHz,超快速模式可以到1 MHz。
④、7 位和10位寻址模式。
⑤、多个7位从地址,所有7位地址应答模式。
⑥、软件复位。
⑦、带DMA功能的1字节缓冲。
⑧、广播呼叫。
关于STM32M157 IIC更多详细的介绍,请参考《STM32MP157参考手册》相关章节。
40.1.3 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结构如图40.1.3.1所示:

图40.1.3.1 AP3216C结构图
AP3216的设备地址为0X1E,同几乎所有的I2C从器件一样,AP3216C内部也有一些寄存器,通过这些寄存器我们可以配置AP3216C的工作模式,并且读取相应的数据。AP3216C我们用的寄存器如表40.1.3.1所示:

表40.1.3.1 本章使用的AP3216C寄存器表
在表40.1.3.1中,0X00这个寄存器是模式控制寄存器,用来设置AP3216C的工作模式,一般开始先将其设置为0X04,也就是先软件复位一次AP3216C。接下来根据实际使用情况选择合适的工作模式,比如设置为0X03,也就是开启ALS+PS+IR。0X0A~0X0F这6个寄存器就是数据寄存器,保存着ALS、PS和IR这三个传感器获取到的数据值。如果同时打开ALS、PS和IR的读取间隔最少要112.5ms,因为AP3216C完成一次转换需要112.5ms。关于AP3216C的介绍就到这里,如果要想详细的研究此芯片的话,请大家自行查阅其数据手册。
40.2 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子系统总体框架如下所示:

图40.2.3.1 I2C子系统框架图
从图40.2.3.1可以知道,I2C子系统分为三大组成部分:
1、I2C核心(I2C-core)
I2C核心提供了I2C总线驱动(适配器)和设备驱动的注册、注销方法,I2C通信方法(algorithm)与具体硬件无关的代码,以及探测设备地址的上层代码等;
2、I2C总线驱动(I2C adapter)
I2C总线驱动是I2C适配器的软件实现,提供I2C适配器与从设备间完成数据通信的能力。I2C总线驱动由i2c_adapter和i2c_algorithm来描述。I2C适配器是SoC中内置i2c控制器的软件抽象,可以理解为他所代表的是一个I2C主机;
3、I2C设备驱动(I2C client driver)
包括两部分:设备的注册和驱动的注册。
I2C子系统帮助内核统一管理I2C设备,让驱动开发工程师在内核中可以更加容易地添加自己的I2C设备驱动程序。
40.2.1 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文件中,结构体内容如下:

示例代码40.2.1 i2c_adapter结构体
685 struct i2c_adapter {686     struct module *owner;
687     unsigned int class;
688     const struct i2c_algorithm *algo;
689     void *algo_data;
690
691     /* data fields that are valid for all devices   */
692     const struct i2c_lock_operations *lock_ops;
693     struct rt_mutex bus_lock;
694     struct rt_mutex mux_lock;
695
696     int timeout;                    /* in jiffies */
697     int retries;
698     struct device dev;              /* the adapter device */
699     unsigned long locked_flags; /* owned by the I2C core */
700 #define I2C_ALF_IS_SUSPENDED            0
701 #define I2C_ALF_SUSPEND_REPORTED        1
702
703     int nr;
704     char name[48];
705     struct completion dev_released;
706
707     struct mutex userspace_clients_lock;
708     struct list_head userspace_clients;
709
710     struct i2c_bus_recovery_info *bus_recovery_info;
711     const struct i2c_adapter_quirks *quirks;
712
713     struct irq_domain *host_notify_domain;
714 };

第 688 行,i2c_algorithm 类型的指针变量 algo,对于一个 I2C 适配器,肯定要对外提供读写 API 函数,设备驱动程序可以使用这些 API 函数来完成读写操作。i2c_algorithm 就是 I2C 适配器与 IIC 设备进行通信的方法。
i2c_algorithm 结构体定义在 include/linux/i2c.h 文件中,内容如下:

示例代码40.2.1.2 i2c_algorithm结构体
526 struct i2c_algorithm {527     /*
528      * If an adapter algorithm can't do I2C-level access, set
529      * master_xfer to NULL. If an adapter algorithm can do SMBus
530      * access, set smbus_xfer. If set to NULL, the SMBus protocol is
531      * simulated using common I2C messages.
532      *
533      * master_xfer should return the number of messages successfully
534      * processed, or a negative value on error
535      */
536     int (*master_xfer)(struct i2c_adapter *adap,
struct i2c_msg *msgs,
537                int num);
538     int (*master_xfer_atomic)(struct i2c_adapter *adap,
539                    struct i2c_msg *msgs, int num);
540     int (*smbus_xfer)(struct i2c_adapter *adap, u16 addr,
541               unsigned short flags, char read_write,
542               u8 command, int size, union i2c_smbus_data *data);
543     int (*smbus_xfer_atomic)(struct i2c_adapter *adap, u16 addr,
544                  unsigned short flags, char read_write,
545                  u8 command, int size, union i2c_smbus_data *data);
546
547     /* To determine what the adapter supports */
548     u32 (*functionality)(struct i2c_adapter *adap);
549
550 #if IS_ENABLED(CONFIG_I2C_SLAVE)
551     int (*reg_slave)(struct i2c_client *client);
552     int (*unreg_slave)(struct i2c_client *client);
553 #endif
554 };

第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)
这两个函数的区别在于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)
函数参数和返回值含义如下:
adap:要删除的I2C适配器。
返回值:无。
关于I2C的总线(控制器或适配器)驱动就讲解到这里,一般SoC的I2C总线驱动都是由半导体厂商编写的,比如STM32MP1的I2C适配器驱动ST官方已经编写好了,这个不需要用户去编写。因此I2C总线驱动对我们这些SoC使用者来说是被屏蔽掉的,我们只要专注于I2C设备驱动即可,除非你是在半导体公司上班,工作内容就是写I2C适配器驱动。
40.2.2 I2C总线设备
I2C设备驱动重点关注两个数据结构:i2c_client和i2c_driver,根据总线、设备和驱动模型,I2C总线上一小节已经讲了。还剩下设备和驱动,i2c_client用于描述I2C总线下的设备,i2c_driver则用于描述I2C总线下的设备驱动,类似于platform总线下的platform_device和platform_driver。
1、i2c_client结构体
i2c_client结构体定义在include/linux/i2c.h文件中,内容如下:

示例代码40.2.2.1 i2c_client结构体
313     struct i2c_client {314         unsigned short flags;               /* div., see below          */
......
328         struct i2c_adapter *adapter;    /* the adapter we sit on    */
329         struct device dev;                  /* the device structure     */
330         int init_irq;           /* irq set at initialization    */
331         int irq;                    /* irq issued by device             */
332         struct list_head detected;
333     #if IS_ENABLED(CONFIG_I2C_SLAVE)
334         i2c_slave_cb_t slave_cb;    /* callback for slave mode      */
335     #endif
336     };

一个I2C设备对应一个i2c_client结构体变量,系统每检测到一个I2C从设备就会给这个设备分配一个i2c_client。
2、i2c_driver结构体
i2c_driver类似platform_driver,是我们编写I2C设备驱动重点要处理的内容,i2c_driver结构体定义在include/linux/i2c.h文件中,内容如下:

示例代码40.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,此函数原型如下:
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是一个宏,定义如下:

示例代码40.2.2.3 i2c_add_driver宏
844     #define i2c_add_driver(driver) \
845         i2c_register_driver(THIS_MODULE, driver)

i2c_add_driver就是对i2c_register_driver做了一个简单的封装,只有一个参数,就是要注册的i2c_driver。
注销I2C设备驱动的时候需要将前面注册的i2c_driver从I2C子系统中注销掉,需要用到i2c_del_driver函数,此函数原型如下:
void i2c_del_driver(struct i2c_driver *driver)
函数参数和返回值含义如下:
driver:要注销的i2c_driver。
返回值:无。
i2c_driver的注册示例代码如下:

示例代码40.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函数里面基本就是标准的字符设备驱动那一套了。

40.2.3 I2C 设备和驱动匹配过程
I2C设备和驱动的匹配过程是由I2C子系统核心层来完成的,drivers/i2c/i2c-core-base.c就是I2C的核心部分,I2C核心提供了一些与具体硬件无关的API函数,比如前面讲过的:
1、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)
2、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)
设备和驱动的匹配过程也是由核心层完成的,I2C总线的数据结构为i2c_bus_type,定义在drivers/i2c/i2c-core-base.c文件,i2c_bus_type内容如下:

示例代码40.2.3.1 i2c_bus_type结构体
492 struct bus_type i2c_bus_type = {493     .name       = "i2c",
494     .match      = i2c_device_match,
495     .probe      = i2c_device_probe,
496     .remove     = i2c_device_remove,
497     .shutdown   = i2c_device_shutdown,
498 };

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

示例代码40.2.3.2 i2c_device_match函数
93  static int i2c_device_match(struct device *dev,
struct device_driver *drv)
94  {95      struct i2c_client   *client = i2c_verify_client(dev);
96      struct i2c_driver   *driver;
97
98
99      /* Attempt an OF style match */
100     if (i2c_of_match_device(drv->of_match_table, client))
101         return 1;
102
103     /* Then ACPI style match */
104     if (acpi_driver_match_device(dev, drv))
105         return 1;
106
107     driver = to_i2c_driver(drv);
108
109     /* Finally an I2C match */
110     if (i2c_match_id(driver->id_table, client))
111         return 1;
112
113     return 0;
114 }

第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设备和驱动匹配成功。
40.3 STM32MP1 I2C 适配器驱动分析
上一小节我们讲解了Linux下的I2C子系统,重点分为I2C适配器驱动和I2C设备驱动,其中I2C适配器驱动就是SoC的I2C控制器驱动。I2C设备驱动是需要用户根据不同的I2C从设备去编写,而I2C适配器驱动一般都是SoC厂商去编写的,比如ST就已经提供了STM3MP21的I2C适配器驱动程序。在内核源码arch/arm/boot/dts/stm32mp151.dtsi设备树文件中找到STM32MP1的I2C控制器节点,节点内容如下所示:

示例代码40.3.1 I2C1控制器节点
590     i2c1: i2c@40012000 {591         compatible = "st,stm32mp15-i2c";
592         reg = <0x40012000 0x400>;
593         interrupt-names = "event", "error";
594         interrupts-extended = <&exti 21 IRQ_TYPE_LEVEL_HIGH>,
595                       <&intc GIC_SPI 32 IRQ_TYPE_LEVEL_HIGH>;
596         clocks = <&rcc I2C1_K>;
597         resets = <&rcc I2C1_R>;
598         #address-cells = <1>;
599         #size-cells = <0>;
600         dmas = <&dmamux1 33 0x400 0x80000001>,
601                <&dmamux1 34 0x400 0x80000001>;
602         dma-names = "rx", "tx";
603         power-domains = <&pd_core>;
604         st,syscfg-fmp = <&syscfg 0x4 0x1>;
605         wakeup-source;
606         status = "disabled";
607     };
重点关注i2c1节点的compatible属性值,因为通过compatible属性值可以在Linux源码里面找到对应的驱动文件。这里i2c1节点的compatible属性值“st,stm32mp15-i2c”,在Linux源码中搜索这个字符串即可找到对应的驱动文件。STM32MP1的I2C适配器驱动驱动文件为drivers/i2c/busses/i2c-stm32f7.c,在此文件中有如下内容:
示例代码40.3.2 i2c-stm32f7.c文件代码段
2520    static const struct of_device_id stm32f7_i2c_match[] = {2521     { .compatible = "st,stm32f7-i2c", .data = &stm32f7_setup},
2522     { .compatible = "st,stm32mp15-i2c", .data = &stm32mp15_setup},
2523     {},
2524    };
2525    MODULE_DEVICE_TABLE(of, stm32f7_i2c_match);
2526
2527    static struct platform_driver stm32f7_i2c_driver = {2528        .driver = {2529            .name = "stm32f7-i2c",
2530            .of_match_table = stm32f7_i2c_match,
2531            .pm = &stm32f7_i2c_pm_ops,
2532        },
2533        .probe = stm32f7_i2c_probe,
2534        .remove = stm32f7_i2c_remove,
2535    };
2536
2537    module_platform_driver(stm32f7_i2c_driver);
从示例代码40.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函数内容如下所示(有省略):
示例代码40.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 结构体定义如下:

示例代码40.3.4 stm32f7_i2c_algo结构体
2098    static const struct i2c_algorithm stm32f7_i2c_algo = {2099        .master_xfer = stm32f7_i2c_xfer,
2100        .smbus_xfer = stm32f7_i2c_smbus_xfer,
2101        .functionality = stm32f7_i2c_func,
2102        .reg_slave = stm32f7_i2c_reg_slave,
2103        .unreg_slave = stm32f7_i2c_unreg_slave,
2104    };

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

示例代码40.3.5stm32f7_i2c_func函数
2088    static u32 stm32f7_i2c_func(struct i2c_adapter *adap)
2089    {2090        return I2C_FUNC_I2C | I2C_FUNC_10BIT_ADDR | I2C_FUNC_SLAVE |
2091            I2C_FUNC_SMBUS_QUICK | I2C_FUNC_SMBUS_BYTE |
......
2094            I2C_FUNC_SMBUS_PROC_CALL | I2C_FUNC_SMBUS_PEC |
2095            I2C_FUNC_SMBUS_I2C_BLOCK | I2C_FUNC_SMBUS_HOST_NOTIFY;
2096    }
重点来看一下stm32f7_i2c_xfer函数,因为最终就是通过此函数来完成与I2C设备通信的,此函数内容如下:
示例代码40.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控制器硬件寄存器的,具体内容这里就不分析了,大家感兴趣可以自己阅读代码。

40.4 I2C设备驱动编写流程
I2C适配器驱动SOC厂商已经替我们编写好了,我们需要做的就是编写具体的设备驱动,本小节我们就来学习一下I2C设备驱动的详细编写流程。
40.4.1 I2C设备信息描述
1、未使用设备树的时候
首先肯定要描述I2C设备节点信息,先来看一下没有使用设备树的时候是如何在BSP里面描述I2C设备信息的,在未使用设备树的时候需要在BSP里面使用i2c_board_info结构体来描述一个具体的I2C设备。i2c_board_info结构体如下:

示例代码40.4.1.1 i2c_board_info结构体
406 struct i2c_board_info {407     char        type[I2C_NAME_SIZE];
408     unsigned short  flags;
409     unsigned short  addr;
410     const char  *dev_name;
411     void        *platform_data;
412     struct device_node *of_node;
413     struct fwnode_handle *fwnode;
414     const struct property_entry *properties;
415     const struct resource *resources;
416     unsigned int    num_resources;
417     int     irq;
418 };

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

示例代码40.4.1.2 s35390a的I2C设备信息
246 static struct i2c_board_info armadillo5x0_i2c_rtc = {247     I2C_BOARD_INFO("s35390a", 0x30),
248 };
示例代码40.4.1.2中使用I2C_BOARD_INFO来完成armadillo5x0_i2c_rtc的初始化工作,I2C_BOARD_INFO是一个宏,定义如下:
示例代码40.4.1.3 I2C_BOARD_INFO宏
433 #define I2C_BOARD_INFO(dev_type, dev_addr) \434     .type = dev_type, .addr = (dev_addr)

可以看出,I2C_BOARD_INFO宏其实就是设置i2c_board_info的type和addr这两个成员变量,因此示例代码40.4.1.2的主要工作就是设置I2C设备名字为s35390a,器件地址为0X30。
大家可以在Linux源码里面全局搜索i2c_board_info,会找到大量以i2c_board_info定义的I2C设备信息,这些就是未使用设备树的时候I2C设备的描述方式,当采用了设备树以后就不会再使用i2c_board_info来描述I2C设备了。
2、使用设备树的时候
使用设备树的时候I2C设备信息通过创建相应的节点就行了,比如在我们的STM32MP1的开发板上有一个I2C器件AP3216C,这是三合一的环境传感器,并且该器件挂在STM32MP1 I2C5总线接口上,因此必须在i2c5节点下创建一个子节点来描述AP3216C设备,节点示例如下所示:

示例代码40.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属性的设置,一个用于匹配驱动,一个用于设置器件地址。
40.4.2 I2C设备数据收发处理流程
在40.2.2小节已经说过了,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)
函数参数和返回值含义如下:
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文件中,结构体内容如下:

示例代码40.4.2.1 i2c_msg结构体
69 struct i2c_msg {70      __u16 addr;                     /* 从机地址             */
71      __u16 flags;                    /* 标志           */
72      #define I2C_M_TEN               0x0010
73      #define I2C_M_RD                0x0001
74      #define I2C_M_STOP              0x8000
75      #define I2C_M_NOSTART           0x4000
76      #define I2C_M_REV_DIR_ADDR  0x2000
77      #define I2C_M_IGNORE_NAK    0x1000
78      #define I2C_M_NO_RD_ACK     0x0800
79      #define I2C_M_RECV_LEN      0x0400
80      __u16 len;                      /* 消息(本msg)长度   */
81      __u8 *buf;                      /* 消息数据             */
82 };
使用i2c_transfer函数发送数据之前要先构建好i2c_msg,使用i2c_transfer进行I2C数据收发的示例代码如下:
示例代码40.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,函数原型如下:

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,函数原型如下:
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驱动。
40.5 硬件原理图分析
AP3612C原理图如图40.5.1所示:

图40.5.1 AP3216C原理图
从图40.5.1可以看出AP3216C使用的是I2C5,其中I2C5_SCL使用的是PA11这个IO,I2C_SDA使用的是PA12这个IO。AP3216C还有个中断引脚,这里我们没有用到中断功能。
40.6 实验程序编写
本实验对应的例程路径为:开发板光盘1、程序源码2、Linux驱动例程21_iic。
40.6.1 修改设备树
1、IO修改或添加
AP3216C用到了I2C5接口。因为I2C5所使用的IO分别为PA11和PA12,所以我们要根据数据手册设置I2C5的pinmux的配置。如果要用到AP3216C的中断功能的话还需要初始化AP_INT对应的PE4这个IO,本章实验我们不使用中断功能。因此只需要设置PA11和PA12这个两个IO复用为AF4功能,ST其实已经将这个两个IO设置好了,打开stm32mp15-pinctrl.dtsi,然后找到如下内容:

示例代码40.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  };
示例代码40.6.1.1中,定义了I2C5接口的两个pinmux配置分别为:i2c5_pins_a和i2c5_pins_sleep_a。第一个默认的状态下使用,第二个是在sleep状态下使用。

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

示例代码40.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”的子目录,如图40.6.1.1所示:

图40.6.1.1 当前系统I2C设备
图40.6.1.1中的“0-001e”就是ap3216c的设备目录,“1e”就是ap3216c器件地址。进入0-001e目录,可以看到“name”文件,name文件保存着此设备名字,在这里就是“ap3216c”,如图40.6.1.2所示:

图40.6.1.2 ap3216c器件名字
40.6.2 AP3216C驱动编写
新建名为“21_iic”的文件夹,然后在21_iic文件夹里面创建vscode工程,工作区命名为“iic”。工程创建好以后新建ap3216c.c和ap3216creg.h这两个文件,ap3216c.c为AP3216C的驱动代码,ap3216creg.h是AP3216C寄存器头文件。先在ap3216creg.h中定义好AP3216C的寄存器,输入如下内容,

示例代码40.6.2.1 ap3216creg.h文件代码段
1   #ifndef AP3216C_H
2   #define AP3216C_H
3   /***************************************************************
4   Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
5   文件名    : ap3216creg.h
6   作者      : 正点原子Linux团队
7   版本      : V1.0
8   描述      : AP3216C寄存器地址描述头文件
9   其他      : 无
10  论坛      : www.openedv.com
11  日志      : 初版V1.0 2021/03/19 正点原子Linux团队创建
12  ***************************************************************/
13
14  #define AP3216C_ADDR            0X1E        /* AP3216C器件地址      */
15
16  /* AP3316C寄存器 */
17  #define AP3216C_SYSTEMCONG  0x00    /* 配置寄存器            */
18  #define AP3216C_INTSTATUS       0X01    /* 中断状态寄存器          */
19  #define AP3216C_INTCLEAR        0X02    /* 中断清除寄存器          */
20  #define AP3216C_IRDATALOW       0x0A    /* IR数据低字节      */
21  #define AP3216C_IRDATAHIGH      0x0B    /* IR数据高字节      */
22  #define AP3216C_ALSDATALOW      0x0C    /* ALS数据低字节     */
23  #define AP3216C_ALSDATAHIGH 0X0D    /* ALS数据高字节     */
24  #define AP3216C_PSDATALOW       0X0E    /* PS数据低字节      */
25  #define AP3216C_PSDATAHIGH      0X0F    /* PS数据高字节      */
26
27  #endif

ap3216creg.h没什么好讲的,就是一些寄存器宏定义。然后在ap3216c.c输入如下内容:

示例代码40.6.2.2 ap3216.c文件代码段
1  /***************************************************************
2  Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3  文件名      : ap3216c.c
4  作者       : 正点原子Linux团队
5  版本       : V1.0
6  描述       : AP3216C驱动程序
7  其他       : 无
8  论坛       : www.openedv.com
9  日志       : 初版V1.0 2021/03/19 正点原子Linux团队创建
10 ***************************************************************/
11  #include <linux/types.h>
12  #include <linux/kernel.h>
13  #include <linux/delay.h>
14  #include <linux/ide.h>
15  #include <linux/init.h>
16  #include <linux/module.h>
17  #include <linux/errno.h>
18  #include <linux/gpio.h>
19  #include <linux/cdev.h>
20  #include <linux/device.h>
21  #include <linux/of_gpio.h>
22  #include <linux/semaphore.h>
23  #include <linux/timer.h>
24  #include <linux/i2c.h>
25  #include <asm/mach/map.h>
26  #include <asm/uaccess.h>
27  #include <asm/io.h>
28  #include "ap3216creg.h"
29
30  #define AP3216C_CNT 1
31  #define AP3216C_NAME    "ap3216c"
32
33  struct ap3216c_dev {34      struct i2c_client *client;      /* i2c 设备       */
35      dev_t devid;                    /* 设备号          */
36      struct cdev cdev;               /* cdev             */
37      struct class *class;            /* 类                */
38      struct device *device;          /* 设备               */
39      struct device_node  *nd;    /* 设备节点             */
40      unsigned short ir, als, ps; /* 三个光传感器数据     */
41  };
42
43  /*
44   * @description    :   从ap3216c读取多个寄存器数据
45   * @param – dev    :   ap3216c设备
46   * @param – reg    :   要读取的寄存器首地址
47   * @param – val    :   读取到的数据
48   * @param – len    :   要读取的数据长度
49   * @return         :   操作结果
50   */
51  static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg,
void *val, int len)
52  {53      int ret;
54      struct i2c_msg msg[2];
55      struct i2c_client *client = (struct i2c_client *)dev->client;
56
57      /* msg[0]为发送要读取的首地址 */
58      msg[0].addr = client->addr;         /* ap3216c地址    */
59      msg[0].flags = 0;                      /* 标记为发送数据  */
60      msg[0].buf = &reg;                     /* 读取的首地址       */
61      msg[0].len = 1;                        /* reg长度            */
62
63      /* msg[1]读取数据 */
64      msg[1].addr = client->addr;             /* ap3216c地址    */
65      msg[1].flags = I2C_M_RD;               /* 标记为读取数据  */
66      msg[1].buf = val;                      /* 读取数据缓冲区  */
67      msg[1].len = len;                      /* 要读取的数据长度 */
68
69      ret = i2c_transfer(client->adapter, msg, 2);
70      if(ret == 2) {71          ret = 0;
72      } else {73          printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
74          ret = -EREMOTEIO;
75      }
76      return ret;
77  }
78
79  /*
80   * @description:   向ap3216c多个寄存器写入数据
81   * @param - dev:   ap3216c设备
82   * @param - reg:   要写入的寄存器首地址
83   * @param - val:   要写入的数据缓冲区
84   * @param - len:   要写入的数据长度
85   * @return    :    操作结果
86   */
87  static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg,
u8 *buf, u8 len)
88  {89      u8 b[256];
90      struct i2c_msg msg;
91      struct i2c_client *client = (struct i2c_client *)dev->client;
92
93      b[0] = reg;                    /* 寄存器首地址                       */
94      memcpy(&b[1],buf,len);          /* 将要写入的数据拷贝到数组b里面  */
95
96      msg.addr = client->addr;        /* ap3216c地址                    */
97      msg.flags = 0;                 /* 标记为写数据                       */
98
99      msg.buf = b;                   /* 要写入的数据缓冲区                */
100     msg.len = len + 1;            /* 要写入的数据长度                     */
101
102     return i2c_transfer(client->adapter, &msg, 1);
103 }
104
105 /*
106  * @description:   读取ap3216c指定寄存器值,读取一个寄存器
107  * @param - dev:   ap3216c设备
108  * @param - reg:   要读取的寄存器
109  * @return    :    读取到的寄存器值
110  */
111 static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev,
u8 reg)
112 {113     u8 data = 0;
114
115     ap3216c_read_regs(dev, reg, &data, 1);
116     return data;
117 }
118
119 /*
120  * @description:   向ap3216c指定寄存器写入指定的值,写一个寄存器
121  * @param - dev:   ap3216c设备
122  * @param - reg:   要写的寄存器
123  * @param - data:  要写入的值
124  * @return   :     无
125  */
126 static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg,
u8 data)
127 {128     u8 buf = 0;
129     buf = data;
130     ap3216c_write_regs(dev, reg, &buf, 1);
131 }
132
133 /*
134  * @description    : 读取AP3216C的数据,包括ALS,PS和IR, 注意!如果同时
135  *                  :打开ALS,IR+PS两次数据读取的时间间隔要大于112.5ms
136  * @param – ir : ir数据
137  * @param - ps     : ps数据
138  * @param - ps     : als数据
139  * @return         : 无。
140  */
141 void ap3216c_readdata(struct ap3216c_dev *dev)
142 {143     unsigned char i =0;
144     unsigned char buf[6];
145
146     /* 循环读取所有传感器数据 */
147     for(i = 0; i < 6; i++) {148         buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);
149     }
150
151     if(buf[0] & 0X80)       /* IR_OF位为1,则数据无效   */
152         dev->ir = 0;
153     else                    /* 读取IR传感器的数据       */
154         dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);
155
156     dev->als = ((unsigned short)buf[3] << 8) | buf[2];
157
158     if(buf[4] & 0x40)       /* IR_OF位为1,则数据无效  */
159         dev->ps = 0;
160     else                    /* 读取PS传感器的数据       */
161         dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
162 }
163
164 /*
165  * @description    : 打开设备
166  * @param – inode  : 传递给驱动的inode
167  * @param - filp   : 设备文件,file结构体有个叫做private_data的成员变量
168  *                     一般在open的时候将private_data指向设备结构体。
169  * @return         : 0 成功;其他 失败
170  */
171 static int ap3216c_open(struct inode *inode, struct file *filp)
172 {173     /* 从file结构体获取cdev指针,再根据cdev获取ap3216c_dev首地址 */
174     struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;
175     struct ap3216c_dev *ap3216cdev = container_of(cdev,
struct ap3216c_dev, cdev);
176
177     /* 初始化AP3216C */
178     ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0x04);
179     mdelay(50);
180     ap3216c_write_reg(ap3216cdev, AP3216C_SYSTEMCONG, 0X03);
181     return 0;
182 }
183
184 /*
185  * @description    : 从设备读取数据
186  * @param - filp   : 要打开的设备文件(文件描述符)
187  * @param - buf    : 返回给用户空间的数据缓冲区
188  * @param - cnt    : 要读取的数据长度
189  * @param - offt   : 相对于文件首地址的偏移
190  * @return         : 读取的字节数,如果为负值,表示读取失败
191  */
192 static ssize_t ap3216c_read(struct file *filp, char __user *buf,
size_t cnt, loff_t *off)
193 {194     short data[3];
195     long err = 0;
196
197     struct cdev *cdev = filp->f_path.dentry->d_inode->i_cdev;
198     struct ap3216c_dev *dev = container_of(cdev, struct ap3216c_dev,
cdev);
199
200     ap3216c_readdata(dev);
201
202     data[0] = dev->ir;
203     data[1] = dev->als;
204     data[2] = dev->ps;
205     err = copy_to_user(buf, data, sizeof(data));
206     return 0;
207 }
208
209 /*
210  * @description    : 关闭/释放设备
211  * @param - filp   : 要关闭的设备文件(文件描述符)
212  * @return         : 0 成功;其他 失败
213  */
214 static int ap3216c_release(struct inode *inode, struct file *filp)
215 {216     return 0;
217 }
218
219 /* AP3216C操作函数 */
220 static const struct file_operations ap3216c_ops = {221     .owner = THIS_MODULE,
222     .open = ap3216c_open,
223     .read = ap3216c_read,
224     .release = ap3216c_release,
225 };
226
227  /*
228   * @description   : i2c驱动的probe函数,当驱动与
229   *                    设备匹配以后此函数就会执行
230   * @param – client    : i2c设备
231   * @param - id        : i2c设备ID
232   * @return            : 0,成功;其他负值,失败
233   */
234 static int ap3216c_probe(struct i2c_client *client,
const struct i2c_device_id *id)
235 {236     int ret;
237     struct ap3216c_dev *ap3216cdev;
238
239
240     ap3216cdev = devm_kzalloc(&client->dev, sizeof(*ap3216cdev),
GFP_KERNEL);
241     if(!ap3216cdev)
242         return -ENOMEM;
243
244     /* 注册字符设备驱动 */
245     /* 1、创建设备号 */
246     ret = alloc_chrdev_region(&ap3216cdev->devid, 0, AP3216C_CNT,
AP3216C_NAME);
247     if(ret < 0) {248         pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n",
AP3216C_NAME, ret);
249         return -ENOMEM;
250     }
251
252     /* 2、初始化cdev */
253     ap3216cdev->cdev.owner = THIS_MODULE;
254     cdev_init(&ap3216cdev->cdev, &ap3216c_ops);
255
256     /* 3、添加一个cdev */
257     ret = cdev_add(&ap3216cdev->cdev, ap3216cdev->devid,
AP3216C_CNT);
258     if(ret < 0) {259         goto del_unregister;
260     }
261
262     /* 4、创建类 */
263     ap3216cdev->class = class_create(THIS_MODULE, AP3216C_NAME);
264     if (IS_ERR(ap3216cdev->class)) {265         goto del_cdev;
266     }
267
268     /* 5、创建设备 */
269     ap3216cdev->device = device_create(ap3216cdev->class, NULL,
ap3216cdev->devid, NULL, AP3216C_NAME);
270     if (IS_ERR(ap3216cdev->device)) {271         goto destroy_class;
272     }
273     ap3216cdev->client = client;
274     /* 保存ap3216cdev结构体 */
275     i2c_set_clientdata(client,ap3216cdev);
276
277     return 0;
278 destroy_class:
279     device_destroy(ap3216cdev->class, ap3216cdev->devid);
280 del_cdev:
281     cdev_del(&ap3216cdev->cdev);
282 del_unregister:
283     unregister_chrdev_region(ap3216cdev->devid, AP3216C_CNT);
284     return -EIO;
285 }
286
287 /*
288  * @description  : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
289  * @param - client     : i2c设备
290  * @return             : 0,成功;其他负值,失败
291  */
292 static int ap3216c_remove(struct i2c_client *client)
293 {294     struct ap3216c_dev *ap3216cdev = i2c_get_clientdata(client);
295     /* 注销字符设备驱动 */
296     /* 1、删除cdev */
297     cdev_del(&ap3216cdev->cdev);
298     /* 2、注销设备号 */
299     unregister_chrdev_region(ap3216cdev->devid, AP3216C_CNT);
300     /* 3、注销设备 */
301     device_destroy(ap3216cdev->class, ap3216cdev->devid);
302     /* 4、注销类 */
303     class_destroy(ap3216cdev->class);
304     return 0;
305 }
306
307 /* 传统匹配方式ID列表 */
308 static const struct i2c_device_id ap3216c_id[] = {309     {"alientek,ap3216c", 0},
310     {}
311 };
312
313 /* 设备树匹配列表 */
314 static const struct of_device_id ap3216c_of_match[] = {315     { .compatible = "alientek,ap3216c" },
316     { /* Sentinel */ }
317 };
318
319 /* i2c驱动结构体 */
320 static struct i2c_driver ap3216c_driver = {321     .probe = ap3216c_probe,
322     .remove = ap3216c_remove,
323     .driver = {324             .owner = THIS_MODULE,
325             .name = "ap3216c",
326             .of_match_table = ap3216c_of_match,
327            },
328     .id_table = ap3216c_id,
329 };
330
331 /*
332  * @description    : 驱动入口函数
333  * @param          : 无
334  * @return         : 无
335  */
336 static int __init ap3216c_init(void)
337 {338     int ret = 0;
339
340     ret = i2c_add_driver(&ap3216c_driver);
341     return ret;
342 }
343
344 /*
345  * @description    : 驱动出口函数
346  * @param          : 无
347  * @return         : 无
348  */
349 static void __exit ap3216c_exit(void)
350 {351     i2c_del_driver(&ap3216c_driver);
352 }
353
354 /* module_i2c_driver(ap3216c_driver) */
355
356 module_init(ap3216c_init);
357 module_exit(ap3216c_exit);
358 MODULE_LICENSE("GPL");
359 MODULE_AUTHOR("ALIENTEK");
360 MODULE_INFO(intree, "Y");

在示例代码40.6.2.2里,没有定义一个全局变量,那是因为linux内核不推荐使用全局变量,要使用内存的就用devm_kzalloc之类的函数去申请空间。
第33~41行,自定义一个ap3216c_dev结构体。第34行的client成员变量用来存储从设备树提供的i2c_client结构体。第40行的 ir、als 和 ps 分别存储 AP3216C 的 IR、ALS 和 PS 数据。
第 51~77 行,ap3216c_read_regs 函数实现多字节读取,但是 AP3216C 好像不支持连续多字节读取,此函数在测试其他 I2C 设备的时候可以实现多给字节连续读取,但是在 AP3216C 上不能连续读取多个字节,不过读取一个字节没有问题的。
第 87~103 行,ap3216c_write_regs 函数实现连续多字节写操作。
第111~117行,ap3216c_read_reg函数用于读取AP3216C的指定寄存器数据,用于一个寄存器的数据读取。
第126~131行,ap3216c_write_reg函数用于向AP3216C的指定寄存器写入数据,用于一个寄存器的数据写操作。
第141~162行,读取AP3216C的PS、ALS和IR等传感器原始数据值。
第171~225行,标准的字符设备驱动框架。ap3216c_dev结构体里有一个cdev的变量成员,第174行就是获取ap3216c_dev里的cdev这个变量的地址,在第175行使用container_of宏获取ap3216c_dev的首地址。
第234~285行,ap3216c_probe函数,当I2C设备和驱动匹配成功以后此函数就会执行,和platform驱动框架一样。此函数前面都是标准的字符设备注册代码,第275行,调用i2c_set_clientdata函数将ap3216cdev变量的地址绑定到client,进行绑定之后,可以通过i2c_get_clientdata来获取ap3216cdev变量指针。
第292~305行,ap3216c_remove函数,当I2C驱动模块卸载时会执行此函数。第294行通过调用i2c_get_clientdata函数来得到ap3216cdev变量的地址,后面执行的一系列卸载、注销操作都是前面讲到过的标准字符设备。
第308~311行,ap3216c_id匹配表,i2c_device_id类型。用于传统的设备和驱动匹配,也就是没有使用设备树的时候。
第314~317行,ap3216c_of_match匹配表,of_device_id类型,用于设备树设备和驱动匹配。这里只写了一个compatible属性,值为“alientek,ap3216c”。
第320~329行,ap3216c_driver结构体变量,i2c_driver类型。
第336~342行,驱动入口函数ap3216c_init,此函数通过调用i2c_add_driver来向Linux内核注册i2c_driver,也就是ap3216c_driver。
第349~352行,驱动出口函数ap3216c_exit,此函数通过调用i2c_del_driver来注销掉前面注册的ap3216c_driver。
40.6.3 编写测试APP
新建ap3216cApp.c文件,然后在里面输入如下所示内容:

示例代码40.6.3.1 测试APP
12  #include "stdio.h"
13  #include "unistd.h"
14  #include "sys/types.h"
15  #include "sys/stat.h"
16  #include "sys/ioctl.h"
17  #include "fcntl.h"
18  #include "stdlib.h"
19  #include "string.h"
20  #include <poll.h>
21  #include <sys/select.h>
22  #include <sys/time.h>
23  #include <signal.h>
24  #include <fcntl.h>
25  /*
26   * @description    : main主程序
27   * @param - argc   : argv数组元素个数
28   * @param - argv   : 具体参数
29   * @return         : 0 成功;其他 失败
30   */
31  int main(int argc, char *argv[])
32  {33      int fd;
34      char *filename;
35      unsigned short databuf[3];
36      unsigned short ir, als, ps;
37      int ret = 0;
38
39      if (argc != 2) {40          printf("Error Usage!\r\n");
41          return -1;
42      }
43
44      filename = argv[1];
45      fd = open(filename, O_RDWR);
46      if(fd < 0) {47          printf("can't open file %s\r\n", filename);
48          return -1;
49      }
50
51      while (1) {52          ret = read(fd, databuf, sizeof(databuf));
53          if(ret == 0) {            /* 数据读取成功 */
54              ir =  databuf[0];      /* ir传感器数据 */
55              als = databuf[1];      /* als传感器数据 */
56              ps =  databuf[2];      /* ps传感器数据 */
57              printf("ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
58          }
59          usleep(200000);     /*100ms */
60      }
61      close(fd);              /* 关闭文件 */
62      return 0;
63  }
ap3216cApp.c文件内容很简单,就是在while循环中不断的读取AP3216C的设备文件,从而得到ir、als和ps这三个数据值,然后将其输出到终端上。

40.7 运行测试
40.7.1 编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第四十章实验基本一样,只是将obj-m变量的值改为“ap3216c.o”,Makefile内容如下所示:

示例代码40.7.1.1 Makefile文件
1  KERNELDIR := /home/zuozhongkai/linux/my_linux/linux-5.4.31
......
4  obj-m := ap3216c.o
......
11 clean:
12  $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
第4行,设置obj-m变量的值为“ap3216c.o”。
输入如下命令编译出驱动模块文件:

make -j32
编译成功以后就会生成一个名为“ap3216c.ko”的驱动模块文件。
2、编译测试APP
输入如下命令编译ap3216cApp.c这个测试程序:
arm-none-linux-gnueabihf-gcc ap3216cApp.c -o ap3216cApp
编译成功以后就会生成ap3216cApp这个应用程序。
40.7.2 运行测试
将上一小节编译出来ap3216c.ko和ap3216cApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中。输入如下命令加载ap3216c.ko这个驱动模块。
depmod //第一次加载驱动的时候需要运行此命令
modprobe ap3216c //加载驱动模块
当驱动模块加载成功以后使用ap3216cApp来测试,输入如下命令:
./ap3216cApp /dev/ap3216c
测试APP会不断的从AP3216C中读取数据,然后输出到终端上,如图40.7.2.1所示:

图40.7.2.1 获取到的AP3216C数据
大家可以用手电筒照一下AP3216C,或者手指靠近AP3216C来观察传感器数据有没有变化。

【正点原子MP157连载】第四十章 Linux I2C驱动实验-摘自【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.7相关推荐

  1. 【正点原子MP157连载】第四十一章 RGB转HDMI实验-摘自【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.7

    第四十一章 RGB转HDMI实验 目前大多数的显示器都提供了HDMI接口,HDMI的应用范围也越来越广,但是STM32MP157这颗芯片原生并不支持HDMI显示.我们可以通过RGB转HDMI芯片将RG ...

  2. 【正点原子MP157连载】第十九章 OLED实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  3. 【正点原子MP157连载】第十七章 通用定时器实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  4. 【正点原子MP157连载】第二十章 字符设备驱动开发-摘自【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.7

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  5. 【正点原子STM32连载】第二十一章 通用定时器实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

    1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频 ...

  6. 【正点原子FPGA连载】第八章LED灯闪烁实验 -摘自【正点原子】领航者ZYNQ之FPGA开发指南_V2.0

    1)实验平台:正点原子领航者ZYNQ开发板 2)平台购买地址:https://item.taobao.com/item.htm?&id=606160108761 3)全套实验源码+手册+视频下 ...

  7. 【正点原子FPGA连载】 第八章 LED灯闪烁实验摘自【正点原子】DFZU2EG/4EV MPSoC 之FPGA开发指南V1.0

    1)实验平台:正点原子MPSoC开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=692450874670 3)全套实验源码+手册+视频下载地址: h ...

  8. 【正点原子MP157连载】 第十章 U-boot使用-摘自【正点原子】【正点原子】STM32MP1嵌入式Linux驱动开发指南V1.7

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  9. 【正点原子MP157连载】第十章 跑马灯实验-摘自【正点原子】STM32MP1 M4裸机CubeIDE开发指南

    1)实验平台:正点原子STM32MP157开发板 2)购买链接:https://item.taobao.com/item.htm?&id=629270721801 3)全套实验源码+手册+视频 ...

  10. 【正点原子STM32连载】 第三十一章 ADC实验 摘自【正点原子】MiniPro STM32H750 开发指南_V1.1

    1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频 ...

最新文章

  1. Win10下MySQL_Workbench连接远程主机MySQL5.7
  2. c++静态成员变量成员函数
  3. ant external lib
  4. android 构造xml,android 中生成xml文件
  5. go get 的不再src目录中_Go 每日一库之 sqlc:根据 sql 生成代码
  6. java类和对象程序_Java类与对象程序设计基础
  7. node.js 回调函数
  8. 程序、线程、进程的区别(python、Java举例)
  9. PWM级联方案。UART接口的单总线控制多个PWM输出。数字舵机,舵机级联方案
  10. (三)图像的放大和缩小
  11. tensorflow 基础: static shape VS Dynamic shape, get_shape VS tf.shape() , reshape VS set_shape
  12. LinuxC语言のUDP简易聊天室 sokcet
  13. Android手柄数据全解析
  14. Qt编写可视化大屏电子看板系统17-柱状堆积图
  15. c4d安装没有出现语言文字,关于C4D以及渲染器插件安装时遇到的问题以及解决方法...
  16. PAT A1002 A+B for Polynomials (25 分)
  17. 阿里滑块 某宝的x82y解决方法、x5sec
  18. 腾讯全民wifi驱动无法安装
  19. 第十三课:树莓派搭建客户端
  20. [编程题]漂流船问题

热门文章

  1. 登录SSH/winSCP一直显示密码错误
  2. 前端如何实现搜索记录展示以及清楚历史搜索记录
  3. 新一代三维GIS技术在交通行业的创新应用
  4. 论文翻译:混合维在庞加莱几何三维骨架的动作识别
  5. 360 冰刃实验室研究员获得微软史上最高漏洞赏金
  6. scala 自带json_在scala中格式化JSON字符串
  7. 移动100m宽带慢的要死_wifi慢到快崩溃明明100m宽带却像2m的网速教你1招快速解决...
  8. [SV]SystemVerilog 断言(SVA)检查器库(OVL)
  9. Java中IO(三、转换流与打印流)
  10. 小程序生成二维码海报