I2C原理

I2c基础知识

Inter Integrated Circuit 串行总线的缩写,是 PHILIPS 公司推出的芯片间串行传输总线。它以 1 根串行数据线( SDA )和 1 根串行时钟线( SCL )实现了半双工的同步数据传输。具有接口线少,控制方式简化,器件封装形式小,通信速率较高等优点。在主从通信中,可以有多个 I2C 总线器件同时接到 I2C 总线 上,通过地址来识别通信对象。IIC 接口的协议里面包括设备地址信息,可以同一总线上连接多个从设备,通过应答来互通数据及命令。但是传输速率有限,标准模式下可达到 100Kbps快速模式下可达到400Kbps (我们开发板一般在 130Kbps ),高速模式下达到 4Mbps ,不能实现全双工,不适合传输很多的数据。

  • **串行:**数据位(0和1)是一位接着一位,排成一队,在单一通道(一条线)上依次传输的。

  • 同步:数据传输的节奏( timing )由一个统一的、共享的时钟信号来控制和协调。发送方和接收方都步调一致地根据这个时钟信号的跳变来发送和读取数据。

  • 半双工:I2C只有一条数据线(SDA)。这条线要被主机和所有从机共享。在任一时刻,这条线上只能有一个“说话者”来驱动它为高电平或低电平,否则会发生冲突

I2C信号

总线传输的数据不收限制,但是每次发到 SDA 上的必须是 8 位,并且主机发送 8位后释放总线,从机收到数据后必须拉低 SDA 一个时钟,回应 ACK 表示数据接收成功,我们如果示波器上看到的波形就是每次 9 位数据, 8bit+1bit ack 。

I2C协议中数据传输的单位是字节,也就是8位。但是要用到9个时钟:前面8个时钟用来传输8数据,第9个时钟用来传输回应信号。传输时,先传输最高位(MSB)。

  • 开始信号(S):SCL为高电平时,SDA由====电平==向低==电平跳变,==开始==传送数据。
  • 结束信号(P):SCL为高电平时,SDA由====电平==向高==电平跳变,==结束==传送数据。
  • 响应信号(ACK):接收器在接收到8位数据后,在第9个时钟周期拉低SDA
  • SDA上传输的==数据==必须在SCL为==高电平期间保持稳定==,SDA上的数据只能在SCL为==低电平期间变化==

I2C协议信号如下:

image-20210220151524099

从机收到一字节数据后,如果需要一些时间处理,则会拉低 SCL ,让传输进入等待状态,处理完成,释放 SCL ,继续传输,如上图。

读写时序

写时序

img

  1. 空闲状态:SCL 和SDA 都为高电平。

  2. 发送起始信号(Start):SCL 高电平时 SDA 线从高电平向低电平切换。

  3. 发送设备地址:7bit设备地址位(从设备的地址),加上1bit读/写位(0表示写,1表示读)。

  4. 等待应答(ACK):主机等待从设备的应答时,SDA 会被释放到高电平状态,从机通过拉低SDA 来表示ACK,否则则表示 NACK。

  5. 发送数据: 如果收到应答信号,主机就会开始发送数据帧。数据帧通常包括要写入的寄存器地址和要写入的数据。

  6. 等待应答(ACK): 主机等待从设备发送应答信号,以确保从设备已成功接收到数据。重复步骤5和步骤6:略。

  7. 发送停止信号(Stop): SCL 高电平时 SDA 线从低电平向高电平切换。

读时序

img

  1. 发送起始信号(Start)和设备地址: 主设备首先发送起始信号来开始通信,然后发送目标设备的地址。

  2. 等待应答(ACK): 主设备发送完地址后,会释放数据线(SDA)并等待从设备的应答。

  3. 寄存器地址: 然后发送需要读取的目标设备的寄存器地址。

  4. 等待应答(ACK): 主设备发送完地址后,会释放数据线(SDA)并等待从设备的应答。

  5. 设备地址:重新发送设备地址,此时的读写位为1(读)。

  6. 等待应答(ACK): 主设备发送完地址后,会释放数据线(SDA)并等待从设备的应答。

  7. 接收数据:主设备向从设备发送数据请求后,从设备会开始发送数据帧,主设备接收从设备发送的数据。

  8. 发送应答(ACK)或非应答(NACK): 主设备在接收每个数据字节后,会向从设备发送一个应答信号(ACK)或非应答信号(NACK),以指示是否要继续接收数据。如果主设备准备好继续接收数据,则发送应答信号(ACK);如果主设备不想继续接收数据(例如,数据传输完成),则发送非应答信号(NACK)。

  9. 重复步骤7和步骤8: 主设备会重复步骤7和步骤8,直到接收到所有需要的数据为止。

  10. 发送停止信号(Stop): 当所有数据都被接收完毕后,主设备发送停止信号来结束通信。

注意:Repeated Start

  • Repeated Start是一个特殊的起始信号,其时序与普通的起始信号(Start)相同:

  • SCL 线为高电平。

  • SDA 线从高电平拉低到低电平。

  • 但它发生在当前通信尚未完全结束之前(即没有发送停止信号 STOP

I2C设备地址

I2C从设备地址有两种不同长度格式,用于在总线上唯一标识一个设备。

  • 7位地址
    • 这是最常用、最广泛支持的地址模式。
    • 理论上,7位地址可以表示 2^7 = 128 个设备地址
    • 但其中一些地址是保留地址(例如,广播地址 0x00、CBUS地址 0x01 等),所以实际可用的地址范围是 0x080x77 (十六进制)112个
    • 我们通常说的设备地址(例如 0x50)指的是这个7位地址。
    • 7位地址[Addr6:Addr0] + [R/W#位]。例如,与地址为 0x50 (二进制 1010000) 的设备写入,发出的第一个字节是 0xA0 (1010000 + 0 = 10100000)。
  • 10位地址
    • 为了解决7位地址可能不够用的问题,协议扩展了10位地址。
    • 理论上,10位地址可以表示 2^10 = 1024 个设备地址,极大地扩展了地址空间。
    • 它的兼容性不如7位地址好,并非所有I2C主控制器都完美支持。
    • 10位地址:需要两个字节来发送。
      • 第一个字节:特殊前缀 11110 + Addr9:Addr8 + W#位 (通常为0,表示写)。
      • 第二个字节:剩下的 Addr7:Addr0

设备地址:

大多数I2C从设备芯片(如EEPROM、传感器)都有1到3个地址引脚(A0, A1, A2)。可以通过将这些引脚连接到高电平(VCC)、低电平(GND)或者留空(内部可能有上拉/下拉)来组合出不同的地址值。这在芯片的数据手册(Datasheet)中有明确说明。

image-20250913214025569

例如:一个EEPROM芯片,其7位基础地址是 0b1010(A3-A0),加上A2, A1, A0引脚的状态,就组成了完整的7位地址。如果将A2引脚接GND,A1接VCC,A0接GND,那么最终的地址可能就是 0b1010010(即 0x52)。

I2C硬件连接图

image-20250913211800659

特点:

  • 在一个芯片(SoC)内部,有一个或多个I2C控制器
  • 在一个I2C控制器上,可以连接一个或多个I2C设备
  • I2C总线只需要2条线:时钟线SCL、数据线SDA
  • 在I2C总线的SCL、SDA线上,都有上拉电阻

I.MX6U I2C 简介

I.MX6U 提供了4 个I2C 外设,I.MX6U 的I2C 支持两种模式:标准模式和快速模式,标准模式下I2C 数据传输速率最高是100Kbits/s,在快速模式下数据传输速率最高为400Kbits/s。

AP3216C 简介

AP3216C是由敦南科技推出的一款传感器,其支持环境光强度(ALS)、接近距离(PS)和红外线强度(IR)这三个环境参数检测。

特点:

  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 位分辨率。

image-20250522094454109

AP3216C结构图

image-20250522094545307

image-20250522094553661

寄存器设置

  • 首先,0X00 这个寄存器是模式控制寄存器,用来设置AP3216C 的工作模式,一般开始先将其设置为0X04,也就是先软件复位一次AP3216C。
  • 然后,根据实际使用情况选择合适的工作模式,比如设置为0X03,也就是开启ALS+PS+IR。
  • 接着,从0X0A~0X0F 这6 个寄存器就是数据寄存器,保存着ALS、PS 和IR 这三个传感器获取到的数据值。如果同时打开ALS、
    PS 和IR 则读取间隔最少要112.5ms,因为AP3216C 完成一次转换需要112.5ms。

配置步骤:

  1. 初始化相应的IO:初始化I2C1 相应的IO,设置其复用功能,如果要使用AP3216C 中断功能的话,还需要设置AP3216C 的中断IO。
  2. 初始化I2C1:初始化I2C1 接口,设置波特率。
  3. 初始化AP3216C:初始化AP3216C,读取AP3216C 的数据。

硬件原理分析

可以看出AP3216C 使用的是I2C1,其中I2C1_SCL 使用的UART4_TXD 这个IO、I2C1_SDA 使用的是UART4_R XD 这个IO。

image-20250522095926407

I2C-tool

I2C tools包含一套用于Linux应用层测试各种各样I2C功能的工具。它的主要功能包括:总线探测工具、SMBus访问帮助程序、EEPROM解码脚本、EEPROM编程工具和用于SMBus访问的python模块。只要你所使用的内核中包含I2C设备驱动,那么就可以在你的板子中正常使用这个测试工具。

tool命令

i2cdetect

i2cdetect的主要功能就是I2C设备查询,它用于扫描I2C总线上的设备。它输出一个表,其中包含指定总线上检测到的设备的列表。该命令的常用格式为:

1
i2cdetect [-y] [-a] [-q|-r] i2cbus [first last]

具体参数的含义如下:

-y 取消交互模式。默认情况下,i2cdetect将等待用户的确认,当使用此标志时,它将直接执行操作。
-a 强制扫描非规则地址。一般不推荐。
-q 使用SMBus“快速写入”命令进行探测。一般不推荐。
-r 使用SMBus“接收字节”命令进行探测。一般不推荐。
-F 显示适配器实现的功能列表并退出。
-V 显示I2C工具的版本并推出。
-l 显示已经在系统中使用的I2C总线。
i2cbus 表示要扫描的I2C总线的编号或名称。
first last 表示要扫描的从设备地址范围。

第一,先通过查看当前系统中的I2C的总线情况:

1
i2cdetect -l

img

第二,若总线上挂载I2C从设备,可通过i2cdetect扫描某个I2C总线上的所有设备。可通过控制台输入

1
i2cdetect -y 1

(其中”–”表示地址被探测到了,但没有芯片应答; “UU”因为这个地址目前正在被一个驱动程序使用,探测被省略;而16进制的地址号60,1e和50则表示发现了一个外部片选从地址为0x60,0x1e(AP3216)和0x50(eeprom)的外设芯片。

img

img

第三,查询I2C总线1 (I2C -1)的功能,命令为

1
i2cdetect -F 1

img

i2cget

i2cget的主要功能是获取I2C外设某一寄存器的内容。该命令的常用格式为:

1
i2cget [-f] [-y] [-a] i2cbus chip-address [data-address [mode]]
-f 强制访问设备,即使它已经很忙。 默认情况下,i2cget将拒绝访问已经在内核驱动程序控制下的设备。
-y 取消交互模式。默认情况下,i2cdetect将等待用户的确认,当使用此标志时,它将直接执行操作。
-a 允许在0x00 - 0x07和0x78 - 0x7f之间使用地址。一般不推荐。
i2cbus 表示要扫描的I2C总线的编号或名称。这个数字应该与i2cdetect -l列出的总线之一相对应。
chip-address 要操作的外设从地址。
data-address 被查看外设的寄存器地址。
mode 显示数据的方式: b (read byte data, default) w (read word data) c (write byte/read byte)

下面是完成读取0总线上从地址为0x50的外设的0x10寄存器的数据,命令为:

1
i2cget -y -f 0 0x50 0x10

img

i2cdump

i2cdump的主要功能查看I2C从设备器件所有寄存器的值。 该命令的常用格式为:

1
i2cdump [-f] [-r first-last] [-y] [-a] i2cbus address [mode [bank [bankreg]]]
-f 强制访问设备,即使它已经很忙。 默认情况下,i2cget将拒绝访问已经在内核驱动程序控制下的设备。
-r 限制正在访问的寄存器范围。 此选项仅在模式b,w,c和W中可用。对于模式W,first必须是偶数,last必须是奇数。
-y 取消交互模式。默认情况下,i2cdetect将等待用户的确认,当使用此标志时,它将直接执行操作。即使从机没有发送 ACK,也继续读取。
-V 显示I2C工具的版本并推出。
i2cbus 表示要扫描的I2C总线的编号或名称。这个数字应该对应于i2cdetect -l列出的总线之一。
first last 表示要扫描的从设备地址范围。
mode b: 单个字节 w:16位字 s:SMBus模块 i:I2C模块的读取大小 c: 连续读取所有字节,对于具有地址自动递增功能的芯片(如EEPROM)非常有用。 W与 w类似,只是读命令只能在偶数寄存器地址上发出;这也是主要用于EEPROM的。

下面是完成读取0总线上从地址为0x50的eeprom的数据,命令为:

1
i2cdump -f -y 0 0x50

img

分析:

左侧列(十六进制地址)

  • 每行的第一个数字表示当前读取的寄存器地址(以十六进制表示),步长为 16(即每行显示 16 字节)。
  • 例如,0: 表示从地址 0x00 开始,10: 表示从地址 0x10 开始,依此类推。

中间列(十六进制数据)

  • 每行显示 16 个字节的数据,每个字节用两个十六进制字符表示。
  • 例如,ff ff ff ff ff ff ff ff 表示连续 8 个字节,每个字节的值都是 0xFF

侧列(ASCII 表示)

  • 将每个字节的值转换为 ASCII 字符表示。如果字节值无法转换为可打印的 ASCII 字符,则显示为点号 .
  • 例如,ff 对应的 ASCII 值是不可打印字符,因此显示为 .

i2cset

i2cset的主要功能是通过I2C总线设置设备中某寄存器的值。该命令的常用格式为:

1
i2cset [-f] [-y] [-m mask] [-r] i2cbus chip-address data-address [value] ...[mode]
-f 强制访问设备,即使它已经很忙。 默认情况下,i2cget将拒绝访问已经在内核驱动程序控制下的设备。
-r 在写入值之后立即读取它,并将结果与写入的值进行比较。
-y 取消交互模式。默认情况下,i2cdetect将等待用户的确认,当使用此标志时,它将直接执行操作。
-V 显示I2C工具的版本并推出。
i2cbus 表示要扫描的I2C总线的编号或名称。这个数字应该对应于i2cdetect -l列出的总线之一。
-m mask 如果指定mask参数,那么描述哪些value位将是实际写入data-addres的。掩码中设置为1的位将从值中取出,而设置为0的位将从数据地址中读取,从而由操作保存。
mode b: 单个字节 w:16位字 s:SMBus模块 i:I2C模块的读取大小 c: 连续读取所有字节,对于具有地址自动递增功能的芯片(如EEPROM)非常有用。 W与 w类似,只是读命令只能在偶数寄存器地址上发出;这也是主要用于EEPROM的。

下面是完成向0总线上从地址为0x50的eeprom的0x10寄存器写入0x55,命令为:

1
i2cset -y -f 0 0x50 0x10 0x55

然后用i2cget读取0总线上从地址为0x50的eeprom的0x10寄存器的数据,命令为:

1
i2cget -y -f 0 0x50 0x10

img

i2ctransfer

i2ctransfer的主要功能是在一次传输中发送用户定义的I2C消息。i2ctransfer是一个创建I2C消息并将其合并为一个传输发送的程序。对于读消息,接收缓冲区的内容被打印到stdout,每个读消息一行。

该命令的常用格式为:

1
i2ctransfer [-f] [-y] [-v] [-a] i2cbus desc [data] [desc [data]] 
-f 强制访问设备,即使它已经很忙。 默认情况下,i2cget将拒绝访问已经在内核驱动程序控制下的设备。
-y 取消交互模式。默认情况下,i2cdetect将等待用户的确认,当使用此标志时,它将直接执行操作。
-v 启用详细输出。它将打印所有信息发送,即不仅为读消息,也为写消息。
-V 显示I2C工具的版本并推出。
-a 允许在0x00 - 0x02和0x78 - 0x7f之间使用地址。一般不推荐。
i2cbus 表示要扫描的I2C总线的编号或名称。这个数字应该对应于i2cdetect -l列出的总线之一。

下面是完成向0总线上从地址为0x50的eeprom的0x20开始的4个寄存器写入0x01,0x02,0x03,0x04命令为:i2ctransfer -f -y 0 w5@0x50 0x20 0x01 0x02 0x03 0x04然后再通过命令

1
i2ctransfer -f -y 0 w1@0x50 0x20 r4

将0x20地址的4个寄存器数据读出来,见下图:

image-20241022220511702

I2C驱动开发

image-20250913221841441

Linux内核将I2C驱动分为两部分:

  • I2C 总线驱动,I2C 总线驱动就是SOC的I2C控制器驱动,也叫做I2C 适配器驱动。
  • I2C 设备驱动,I2C 设备驱动就是针对具体的I2C设备而编写的驱动。

I2C 适配器

i2c_adapter I2C总线适配器相当于一个I2C设备挂载点,处理器上的I2C控制器就是一个典型的I2C总线适配器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct i2c_adapter {
struct module *owner;
unsigned int class; /* classes to allow probing for */
const struct i2c_algorithm *algo; /* 总线访问算法 */
void *algo_data;

/* data fields that are valid for all devices */
struct rt_mutex bus_lock;

int timeout; /*超时时间,单位同 jiffies*/
int retries;//重试次数
struct device dev; /*适配器设备*/

int nr;//总线号
char name[48];
struct completion dev_released;

struct mutex userspace_clients_lock;
struct list_head userspace_clients;

struct i2c_bus_recovery_info *bus_recovery_info;
const struct i2c_adapter_quirks *quirks;
};
1
2
int i2c_add_adapter(struct i2c_adapter* adap):∥使用动态I2C总线号
int i2c_add_numbered_adapter(struct i2c_adapter* adap)/∥使用指定I2C总线号
  • adapter 或adap:要添加到Linux 内核中的i2c_adapter,也就是I2C 适配器。

  • **返回值:**0,成功;负值,失败。

上面两个函数会调用i2c_register_adapter 注册I2C 适配器,并在/dev 目录产生一个主设备号为I2C_MAJOR的I2C 设备节点。i2c_del_adapter函数从内核删除一个i2c_adapter:

1
void i2c_del_adapter(struct i2c_adapter * adap)
  • **adap:**要删除的I2C 适配器。

  • **返回值:**无。

I2C算法

I2C 算法(i2c_algorithm)表示一套通信方法。一个I2C 适配器需要一个通信规则(i2c_algorithm)来控制适配器产生特定的时序。对于一个I2C 适配器,肯定要对外提供读写API 函数,设备驱动程序可以使用这些API 函数来完成读写操作。i2c_algorithm 就是I2C 适配器与IIC 设备进行通信的方法。

1
2
3
4
5
6
7
8
9
10
11
12
struct i2c_algorithm {
//I2C 总线传输函数
int (*master_xfer)(struct i2c_adapter *adapstruct i2c_msg *msgsint num);
//SMBUS 总线传输函数
int (*smbus_xfer) (struct i2c_adapter *adapu16 addrunsigned short flagschar read_write
u8 commandint sizeunion i2c_smbus_data *data);
u32 (*functionality) (struct i2c_adapter *);/*检测adapter 支持的功能*/
#if IS_ENABLEDCONFIG_I2C_SLAVE
int (*reg_slave)(struct i2c_client *client);
int (*unreg_slave)(struct i2c_client *client);
#endif
};
  • master_xfer就是I2C适配器的传输函数,可以通过此函数来完成与IIC 设备之间的通信。

  • smbus_xfer就是SMBUS总线的传输函数

I2C从设备

挂载在I2C 总线上的不能控制总线的设备称为I2C 从设备,通常是一个外围芯片。i2c_client 结构表示连接到I2C 总线上的从设备。每个从设备具有一个或者多个I2C 地址。处理器根据I2C 地址访问I2C 从设备,而I2C 从设备则根据地址决定是否对I2C命令进行响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct i2c_client {
unsigned short flags; /* 标志 */
unsigned short addr; /* 芯片地址,7 位,存在低7 位 */
/* addresses are stored in the */
/* _LOWER_ 7 bits */
char name[I2C_NAME_SIZE]; /* 名字 */
struct i2c_adapter *adapter; /* 对应的I2C 适配器 */
struct device dev; //* 设备结构体 */
int irq; /* 中断 */
struct list_head detected;
#if IS_ENABLED(CONFIG_I2C_SLAVE)
i2c_slave_cb_t slave_cb; /* callback for slave mode */
#endif
};

一个设备对应一个i2c_client,每检测到一个I2C 设备就会给这个I2C 设备分配一个i2c_client。

这段代码定义了 struct i2c_client,它是Linux内核中用于表示I2C总线上连接的从设备(slave device)的数据结构。每个 i2c_client 实例代表一个通过I2C协议与系统通信的硬件芯片或设备。以下是该结构体各成员变量的解释:

  • flags: 这是一个标志位字段,用来标识设备的一些特性。例如,I2C_CLIENT_TEN 表示该设备使用10位地址,I2C_CLIENT_PEC 表示它支持SMBus包错误校验。

  • addr: 设备在I2C总线上的地址。值得注意的是,7位地址存储在最低的7个位中。

  • name: 一个字符串,用来标识设备类型,通常是通用的芯片名称,可以隐藏第二来源和兼容修订版本。

  • adapter: 指向管理该I2C设备所在总线段的 i2c_adapter 结构的指针。每一个适配器代表一个独立的I2C总线控制器。

  • dev: 一个 device 结构,是Linux驱动模型中的节点,代表这个从设备。

  • irq: 如果设备产生中断请求(IRQ),则此值表示该IRQ的编号。

  • detected: 一个链表项,当设备被检测到时,它会被添加到 i2c_driverclients 列表或者 i2c-coreuserspace_devices 列表中。

  • slave_cb: 如果配置启用了I2C从模式(CONFIG_I2C_SLAVE),那么这是一个回调函数,当适配器处于从模式并且有事件发生时调用,以便将这些事件传递给从设备驱动程序。

这个结构体是构建Linux I2C子系统的基石之一,允许驱动程序与特定的I2C设备进行交互,并且为上层软件提供了必要的信息来正确地配置和操作这些设备。如果你正在编写一个I2C设备驱动程序,你通常需要填充或处理这个结构体以实现对特定硬件的支持。

I2C从设备驱动

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

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
struct i2c_driver {
unsigned int class;

/* Notifies the driver that a new bus has appeared. You should avoid
* using this, it will be removed in a near future.
*/
int (*attach_adapter)(struct i2c_adapter *) __deprecated;

/* Standard driver model interfaces */
int (*probe)(struct i2c_client *, const struct i2c_device_id *);
int (*remove)(struct i2c_client *);

/* driver model interfaces that don't relate to enumeration */
void (*shutdown)(struct i2c_client *);

/* Alert callback, for example for the SMBus alert protocol.
* The format and meaning of the data value depends on the protocol.
* For the SMBus alert protocol, there is a single bit of data passed
* as the alert response's low bit ("event flag").
*/
void (*alert)(struct i2c_client *, unsigned int data);

/* a ioctl like command that can be used to perform specific functions
* with the device.
*/
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);

struct device_driver driver;
const struct i2c_device_id *id_table;

/* Device detection callback for automatic device creation */
int (*detect)(struct i2c_client *, struct i2c_board_info *);
const unsigned short *address_list;
struct list_head clients;
};

这段注释详细描述了 struct i2c_driver 的各个成员及其用途,以及如何正确地实现和使用这个结构体来创建一个I2C设备驱动程序。以下是关键点的总结:

  • 成员变量
    • @class: 指定实例化的I2C设备类型,用于检测时识别设备。
    • @attach_adapter (已废弃): 曾经是适配器添加时的回调函数,现已不再推荐使用。
    • @probe: 设备绑定时的回调函数,当发现新设备并与驱动匹配时调用。
    • @remove: 设备解绑时的回调函数,用于清理工作。
    • @shutdown: 系统关机时调用的回调函数。
    • @alert: SMBus警报协议的回调函数。
    • @command: 总线范围信令的可选回调函数。
    • @driver: 内核设备驱动模型中的驱动程序结构。
    • @id_table: 驱动程序支持的I2C设备列表。
    • @detect: 设备检测的回调函数。
    • @address_list: 探测的I2C地址列表。
    • @clients: 已检测到的客户端列表(仅限i2c-core内部使用)。

i2c_driver 注册函数为

  • int i2c_register_driver,此函数原型如下:

    1
    2
    3
    4
    5
    6
    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 是一个宏,定义如下:

    1
    #define i2c_add_driver(driver) i2c_register_driver(THIS_MODULE, driver)

i2c_driver 注销函数为i2c_del_driver 函数

1
2
3
4
5
void i2c_del_driver(struct i2c_driver *driver)
/*
driver:要注销的i2c_driver。
返回值:无。
*/*

I2C从设备与驱动匹配

设备和驱动的匹配过程也是由I2C 总线完成的,I2C 总线的数据结构为i2c_bus_type

1
2
3
4
5
6
7
struct bus_type i2c_bus_type = {
.name = "i2c",
.match = i2c_device_match,
.probe = i2c_device_probe,
.remove = i2c_device_remove,
.shutdown = i2c_device_shutdown,
};

.match就是I2C 总线的设备和驱动匹配函数,在这里就是i2c_device_match 这个函数。这个函数中of_driver_match_device 函数用于完成设备树设备和驱动匹配。比较I2C 设备节点的compatible 属性和of_device_id 中的compatible 属性是否相等,如果相当的话就表示I2C设备和驱动匹配。

I2C适配器驱动

I.MX6U的I2C 适配器驱动驱动文件为:drivers/i2c/busses/

I2C控制器节点:

1
2
3
4
5
6
7
8
9
i2c1: i2c@021a0000 {
#address-cells = <1>;
#size-cells = <0>;
compatible = "fsl,imx6ul-i2c", "fsl,imx21-i2c";
reg = <0x021a0000 0x4000>;
interrupts = <GIC_SPI 36 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_I2C1>;
status = "disabled";
};

i2c-imx.c

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
static int i2c_imx_probe(struct platform_device *pdev)
{
const struct of_device_id *of_id = of_match_device(i2c_imx_dt_ids,
&pdev->dev);
struct imx_i2c_struct *i2c_imx;
struct resource *res;
struct imxi2c_platform_data *pdata = dev_get_platdata(&pdev->dev);
void __iomem *base;
int irq, ret;
dma_addr_t phy_addr;

dev_dbg(&pdev->dev, "<%s>\n", __func__);
/*调用platform_get_irq 函数获取中断号。*/
irq = platform_get_irq(pdev, 0);
if (irq < 0) {
dev_err(&pdev->dev, "can't get irq number\n");
return irq;
}
/*调用platform_get_resource 函数从设备树中获取I2C1 控制器寄存器物理基
地址,也就是0X021A0000。获取到寄存器基地址以后使用devm_ioremap_resource 函数对其进
行内存映射,得到可以在Linux 内核中使用的虚拟地址。*/
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);

/*,NXP 使用imx_i2c_struct 结构体来表示I.MX 系列SOC 的I2C 控制器,这里使
用devm_kzalloc 函数来申请内存。*/
i2c_imx = devm_kzalloc(&pdev->dev, sizeof(*i2c_imx), GFP_KERNEL);
if (!i2c_imx)
return -ENOMEM;

/* mx_i2c_struct 结构体要有个叫做adapter 的成员变量,adapter 就是
i2c_adapter,这里初始化i2c_adapter */
strlcpy(i2c_imx->adapter.name, pdev->name, sizeof(i2c_imx->adapter.name));
i2c_imx->adapter.owner = THIS_MODULE;
i2c_imx->adapter.algo = &i2c_imx_algo;
i2c_imx->adapter.dev.parent = &pdev->dev;
i2c_imx->adapter.nr = pdev->id;
i2c_imx->adapter.dev.of_node = pdev->dev.of_node;
i2c_imx->base = base;

/*注册I2C 控制器中断,中断服务函数为i2c_imx_isr。*/
ret = devm_request_irq(&pdev->dev, irq, i2c_imx_isr,
IRQF_NO_SUSPEND, pdev->name, i2c_imx);
if (ret) {
dev_err(&pdev->dev, "can't claim irq %d\n", irq);
goto clk_disable;
}
/*设置I2C 频率默认为IMX_I2C_BIT_RATE=100KHz,如果设备树节点设
置了“clock-frequency”属性的话I2C 频率就使用clock-frequency 属性值。*/




/*调用i2c_add_numbered_adapter 函数向Linux 内核注册i2c_adapter。*/

/*,申请DMA,I.MX 的I2C 适配器驱动采用了DMA 方式。*/

/**/

在porbe函数主要工作:

①、初始化i2c_adapter,设置i2c_algorithm 为i2c_imx_algo,最后向Linux 内核注册i2c_adapter。

②、初始化I2C1 控制器的相关寄存器。i2c_imx_algo 包含I2C1 适配器与I2C 设备的通信函数master_xfer,i2c_imx_algo 结构体定义如下:

1
2
3
4
static struct i2c_algorithm i2c_imx_algo = {
.master_xfer = i2c_imx_xfer,/*i2c_imx_xfer 函数,因为最终就是通过此函数来完成与I2C 设备通信*/
.functionality = i2c_imx_func,
};

i2c_imx_xfer函数:

  • 调用i2c_imx_start 函数开启I2C 通信。

  • 如果是从I2C 设备读数据的话就调用i2c_imx_read 函数。

  • 向I2C 设备写数据,如果要用DMA 的话就使用i2c_imx_dma_write 函数来完成写数据。如果不使用DMA 的话就使用i2c_imx_write 函数完成写数据。

  • I2C 通信完成以后调用i2c_imx_stop 函数停止I2C 通信。

I2C数据收发

I2C设备驱动首先要做的就是初始化i2c_driver并向Linux 内核注册。

当设备和驱动匹配以后i2c_driver里面的probe函数就会执行,probe函数里面所做的就是字符设备驱动那一套了。

一般需要在probe函数里面初始化I2C 设备要初始化I2C设备就必须能够对I2C设备寄存器进行读写操作,这里就要用到i2c_transfer函数了。

i2c_transfer函数最终会调用I2C适配器中i2c_algorithm里面的master_xfer函数,对于I.MX6U而言就是i2c_imx_xfer这个函数

  • i2c_transfer

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

    **adap:**所使用的I2C 适配器,i2c_client 会保存其对应的i2c_adapter。

    **msgs:**I2C 要发送的一个或多个消息。

    **num:**消息数量,也就是msgs 的数量。

    **返回值:**负值,失败,其他非负值,发送的msgs 数量。

  • i2c_msg结构体i2c.h

    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
    /**
    * struct i2c_msg - I2C消息结构体,用于描述一次I2C传输的信息
    * @addr: 从设备地址(I2C从机地址)
    * @flags: 传输标志(控制I2C传输的行为)
    * @len: 消息长度(数据的字节数)
    * @buf: 消息数据指针(指向存储传输数据的缓冲区)
    */
    struct i2c_msg {
    __u16 addr; /* 从设备地址 */
    __u16 flags; /* 传输标志 */
    #define I2C_M_RD 0x0001 /* 读数据(从从机到主机) */
    /* 保证I2C_M_RD的值为0x0001 */
    #define I2C_M_TEN 0x0010 /* 这是一个10位芯片地址 */
    #define I2C_M_DMA_SAFE 0x0200 /* 此消息的缓冲区是DMA安全的 */
    /* 仅在内核空间有意义 */
    /* 用户空间缓冲区无论如何都会被复制 */
    #define I2C_M_RECV_LEN 0x0400 /* 长度将是第一个接收到的字节 */
    #define I2C_M_NO_RD_ACK 0x0800 /* 如果支持I2C_FUNC_PROTOCOL_MANGLING功能 */
    #define I2C_M_IGNORE_NAK 0x1000 /* 如果支持I2C_FUNC_PROTOCOL_MANGLING功能 */
    #define I2C_M_REV_DIR_ADDR 0x2000 /* 如果支持I2C_FUNC_PROTOCOL_MANGLING功能 */
    #define I2C_M_NOSTART 0x4000 /* 如果支持I2C_FUNC_NOSTART功能 */
    #define I2C_M_STOP 0x8000 /* 如果支持I2C_FUNC_PROTOCOL_MANGLING功能 */
    __u16 len; /* 消息长度 */
    __u8 *buf; /* 消息数据指针 */
    };
  • i2c_transfer函数收发的示例代码:

    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
    /*读取数据*/
    /*
    * @description : 从ap3216c读取多个寄存器数据
    * @param - dev: ap3216c设备
    * @param - reg: 要读取的寄存器首地址
    * @param - val: 读取到的数据
    * @param - len: 要读取的数据长度
    * @return : 操作结果
    */
    static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
    {
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = (struct i2c_client *)dev->private_data;

    /* msg[0]为发送要读取的首地址 */
    msg[0].addr = client->addr; /* ap3216c地址 */
    msg[0].flags = 0; /* 标记为发送数据 */
    msg[0].buf = &reg; /* 读取的首地址 */
    msg[0].len = 1; /* reg长度*/

    /* msg[1]读取数据 */
    msg[1].addr = client->addr; /* ap3216c地址 */
    msg[1].flags = I2C_M_RD; /* 标记为读取数据*/
    msg[1].buf = val; /* 读取数据缓冲区 */
    msg[1].len = len; /* 要读取的数据长度*/

    ret = i2c_transfer(client->adapter, msg, 2);
    if(ret == 2) {
    ret = 0;
    } else {
    printk("i2c rd failed=%d reg=%06x len=%d\n",ret, reg, len);
    ret = -EREMOTEIO;
    }
    return ret;
    }

    /*写数据*/
    /*
    * @description : 向ap3216c多个寄存器写入数据
    * @param - dev: ap3216c设备
    * @param - reg: 要写入的寄存器首地址
    * @param - val: 要写入的数据缓冲区
    * @param - len: 要写入的数据长度
    * @return : 操作结果
    */
    static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
    {
    u8 b[256];
    struct i2c_msg msg;
    struct i2c_client *client = (struct i2c_client *)dev->private_data;

    b[0] = reg; /* 寄存器首地址 */
    memcpy(&b[1],buf,len); /* 将要写入的数据拷贝到数组b里面 */

    msg.addr = client->addr; /* ap3216c地址 */
    msg.flags = 0; /* 标记为写数据 */

    msg.buf = b; /* 要写入的数据缓冲区 */
    msg.len = len + 1; /* 要写入的数据长度 */

    return i2c_transfer(client->adapter, &msg, 1);
    }
  • 封装好的SMBUS函数

    函数名 函数原型 参数说明 功能描述
    i2c_smbus_write_quick s32 i2c_smbus_write_quick(struct i2c_client *client, u8 value) client: I2C客户端设备指针 value: 要发送的值 发送快速写入命令,SMBus最简短的写入操作
    i2c_smbus_read_byte s32 i2c_smbus_read_byte(struct i2c_client *client) client: I2C客户端设备指针 从设备读取一个字节(无指定命令字节)
    i2c_smbus_write_byte s32 i2c_smbus_write_byte(struct i2c_client *client, u8 value) client: I2C客户端设备指针 value: 要写入的字节值 向设备写入一个字节(无指定命令字节)
    i2c_smbus_read_byte_data s32 i2c_smbus_read_byte_data(struct i2c_client *client, u8 command) client: I2C客户端设备指针 command: 命令/寄存器地址 从指定命令/寄存器读取一个字节
    i2c_smbus_write_byte_data s32 i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value) client: I2C客户端设备指针 command: 命令/寄存器地址 value: 要写入的字节值 向指定命令/寄存器写入一个字节
    i2c_smbus_read_word_data s32 i2c_smbus_read_word_data(struct i2c_client *client, u8 command) client: I2C客户端设备指针 command: 命令/寄存器地址 从指定命令/寄存器读取一个字(16位)
    i2c_smbus_write_word_data s32 i2c_smbus_write_word_data(struct i2c_client *client, u8 command, u16 value) client: I2C客户端设备指针 command: 命令/寄存器地址 value: 要写入的字值 向指定命令/寄存器写入一个字(16位)
    i2c_smbus_process_call s32 i2c_smbus_process_call(struct i2c_client *client, u8 command, u16 value) client: I2C客户端设备指针 command: 命令/寄存器地址 value: 要写入的字值 写入一个字,然后读取一个字(命令-响应操作)
    i2c_smbus_read_block_data s32 i2c_smbus_read_block_data(struct i2c_client *client, u8 command, u8 *values) client: I2C客户端设备指针 command: 命令/寄存器地址 values: 存储读取数据的缓冲区 从指定命令/寄存器读取一个数据块
    i2c_smbus_write_block_data s32 i2c_smbus_write_block_data(struct i2c_client *client, u8 command, u8 length, const u8 *values) client: I2C客户端设备指针 command: 命令/寄存器地址 length: 数据长度 values: 要写入的数据缓冲区 向指定命令/寄存器写入一个数据块
    i2c_smbus_read_i2c_block_data s32 i2c_smbus_read_i2c_block_data(struct i2c_client *client, u8 command, u8 length, u8 *values) client: I2C客户端设备指针 command: 命令/寄存器地址 length: 要读取的数据长度 values: 存储读取数据的缓冲区 从指定命令/寄存器读取一个数据块(I2C块读取)
    i2c_smbus_write_i2c_block_data s32 i2c_smbus_write_i2c_block_data(struct i2c_client *client, u8 command, u8 length, const u8 *values) client: I2C客户端设备指针 command: 命令/寄存器地址 length: 要写入的数据长度 values: 要写入的数据缓冲区 向指定命令/寄存器写入一个数据块(I2C块写入)
    i2c_smbus_block_process_call s32 i2c_smbus_block_process_call(struct i2c_client *client, u8 command, u8 length, u8 *values) client: I2C客户端设备指针 command: 命令/寄存器地址 length: 数据长度 values: 写入时包含数据,读取时存储数据 写入一个数据块,然后读取一个数据块(块命令-响应操作)

AP3216C实验

设备树节点编写:

1
2
3
4
5
6
7
8
9
10
11
12
/*AP3216C 是连接到I2C1 上的,因此需要在i2c1 节点下添加ap3216c 的设备子节点,在i2c1节点下面添加设备节点*/
&i2c1 {
clock-frequency = <100000>;//clock-frequency 属性为I2C 频率,这里设置为100KHz。
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_i2c1>;
status = "okay";

ap3216c@1e {
compatible = "kevin,ap3216c";//设置compatible 值
reg = < 0x1e >;//reg 属性也是设置ap3216c 器件地址的,因此reg 设置为0x1e。
};
};

基于master_xfer编写代码:

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/i2c.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

// --------------------------- 1. 硬件基础信息定义 -------------------------
#define AP3216C_ADDR 0X1E /* AP3216C I2C 从地址 */
// AP3216C 关键寄存器地址
#define AP3216C_SYSTEMCONG 0x00 /* 系统配置寄存器(复位/功能使能) */
#define AP3216C_IRDATALOW 0x0A /* IR 数据低字节 */
#define AP3216C_IRDATAHIGH 0x0B /* IR 数据高字节 */
#define AP3216C_ALSDATALOW 0x0C /* ALS 数据低字节 */
#define AP3216C_ALSDATAHIGH 0X0D /* ALS 数据高字节 */
#define AP3216C_PSDATALOW 0X0E /* PS 数据低字节 */
#define AP3216C_PSDATAHIGH 0X0F /* PS 数据高字节 */

// --------------------------- 2. 字符设备基础信息 ---------------------------
#define AP3216C_CNT 1 /* 设备数量(单设备) */
#define AP3216C_NAME "ap3216c"/* 设备名(/dev/ap3216c) */

// --------------------------- 3. 设备核心结构体 ---------------------------
struct ap3216c_dev {
dev_t devid; /* 字符设备ID(主+次设备号) */
struct cdev cdev; /* 字符设备对象 */
struct class *class; /* 设备类(用于udev创建设备节点) */
struct device *device; /* 设备实例 */
struct device_node *nd; /* 设备树节点(预留) */
int major; /* 主设备号 */
void *private_data; /* 私有数据(指向I2C客户端) */
unsigned short ir; /* IR传感器数据 */
unsigned short als; /* ALS传感器数据 */
unsigned short ps; /* PS传感器数据 */
};
static struct ap3216c_dev ap3216cdev; /* 全局设备实例 */

// --------------------------- 4. I2C 读写函数 ---------------------------
/*
* 通用I2C读写函数:支持单寄存器/多寄存器读写
* @param dev: 设备结构体指针
* @param reg: 寄存器首地址
* @param val: 数据缓冲区(读:存储结果;写:传入数据)
* @param len: 读写长度(1=单寄存器,>1=多寄存器)
* @param is_read: 读写标志(1=读,0=写)
* @return: 0=成功,-EREMOTEIO=I2C通信失败
*/
static int ap3216c_i2c_rw(struct ap3216c_dev *dev, u8 reg, void *val, int len, bool is_read)
{
int ret;
struct i2c_msg msg[2];
struct i2c_client *client = (struct i2c_client *)dev->private_data;

/* 第一步:发送寄存器首地址(读写操作都需要先传地址) */
msg[0].addr = client->addr; /* I2C从地址 */
msg[0].flags = 0; /* 写操作(发送地址) */
msg[0].buf = &reg; /* 寄存器地址 */
msg[0].len = 1; /* 地址长度1字节 */

if (is_read) {
/* 读操作:第二步读取数据 */
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; /* 读操作标志 */
msg[1].buf = val; /* 存储读取数据的缓冲区 */
msg[1].len = len; /* 读取长度 */
ret = i2c_transfer(client->adapter, msg, 2); /* 2条消息(地址+读数据) */
if (ret != 2) ret = -EREMOTEIO;
} else {
/* 写操作:第二步发送数据(地址+数据合并为1条消息) */
u8 *write_buf = kmalloc(len + 1, GFP_KERNEL); /* 分配地址+数据缓冲区 */
if (!write_buf) return -ENOMEM;

write_buf[0] = reg; /* 首字节=寄存器地址 */
memcpy(&write_buf[1], val, len);/* 后续字节=要写入的数据 */

msg[0].buf = write_buf; /* 地址+数据缓冲区 */
msg[0].len = len + 1; /* 总长度=地址1字节+数据len字节 */
ret = i2c_transfer(client->adapter, msg, 1); /* 1条消息(地址+写数据) */
kfree(write_buf); /* 释放临时缓冲区 */

if (ret != 1) ret = -EREMOTEIO;
}

if (ret < 0) {
printk("I2C %s failed: reg=0x%02x, len=%d, ret=%d\n",
is_read ? "read" : "write", reg, len, ret);
}
return ret < 0 ? ret : 0;
}

// --------------------------- 5. 传感器数据读取与解析 ---------------------------
/*
* 读取并解析IR/ALS/PS数据(直接调用合并后的I2C函数)
* @param dev: 设备结构体指针
*/
static void ap3216c_readdata(struct ap3216c_dev *dev)
{
unsigned char buf[6]; /* 存储6个寄存器数据(IR/ALS/PS各2字节) */

/* 一次读取6个连续寄存器(0x0A~0x0F),替代原for循环单寄存器读取 */
ap3216c_i2c_rw(dev, AP3216C_IRDATALOW, buf, 6, 1);

/* 解析IR数据(10位有效,IR_OF=buf[0]第7位) */
if (buf[0] & 0X80) dev->ir = 0; /* 数据溢出,无效 */
else dev->ir = ((unsigned short)buf[1] << 2) | (buf[0] & 0X03);

/* 解析ALS数据(16位有效) */
dev->als = ((unsigned short)buf[3] << 8) | buf[2];

/* 解析PS数据(10位有效,PS_OF=buf[4]第6位) */
if (buf[4] & 0X40) dev->ps = 0; /* 数据溢出,无效 */
else dev->ps = ((unsigned short)(buf[5] & 0X3F) << 4) | (buf[4] & 0X0F);
}

// --------------------------- 6. 字符设备操作函数 ---------------------------
/* 设备打开:初始化传感器(复位+使能功能) */
static int ap3216c_open(struct inode *inode, struct file *filp)
{
u8 reset_val = 0x04; /* 复位命令 */
u8 en_val = 0X03; /* 使能IR+ALS+PS */
struct ap3216c_dev *dev = &ap3216cdev;

filp->private_data = dev; /* 关联设备结构体 */

/* 复位传感器(调用合并后的写函数,单寄存器写入) */
ap3216c_i2c_rw(dev, AP3216C_SYSTEMCONG, &reset_val, 1, 0);
mdelay(50); /* 复位等待(手册要求≥10ms) */

/* 使能传感器功能(单寄存器写入) */
ap3216c_i2c_rw(dev, AP3216C_SYSTEMCONG, &en_val, 1, 0);
return 0;
}

/* 设备读:向用户空间返回传感器数据 */
static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
short data[3]; /* 存储IR/ALS/PS数据(各2字节) */
long err;
struct ap3216c_dev *dev = filp->private_data;

/* 仅支持读取6字节(3个short),否则返回参数错误 */
if (cnt != sizeof(data)) return -EINVAL;

ap3216c_readdata(dev); /* 读取并解析传感器数据 */

/* 整理数据格式(IR→ALS→PS) */
data[0] = dev->ir;
data[1] = dev->als;
data[2] = dev->ps;

/* 拷贝数据到用户空间 */
err = copy_to_user(buf, data, sizeof(data));
return err ? -EFAULT : sizeof(data);
}

/* 设备关闭:无特殊操作 */
static int ap3216c_release(struct inode *inode, struct file *filp)
{
return 0;
}

/* 字符设备操作集 */
static const struct file_operations ap3216c_ops = {
.owner = THIS_MODULE,
.open = ap3216c_open,
.read = ap3216c_read,
.release = ap3216c_release,
};

// --------------------------- 7. I2C 驱动匹配与探针 ---------------------------
/* 传统I2C设备ID匹配表 */
static const struct i2c_device_id ap3216c_id[] = {
{"kevin,ap3216c", 0},
{} /* 哨兵 */
};

/* 设备树匹配表 */
static const struct of_device_id ap3216c_of_match[] = {
{.compatible = "kevin,ap3216c"},
{} /* 哨兵 */
};

/* I2C驱动探针:设备匹配成功后初始化 */
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
/* 1. 分配设备号 */
if (ap3216cdev.major) {
ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
} else {
alloc_chrdev_region(&ap3216cdev.devid, 0, AP3216C_CNT, AP3216C_NAME);
ap3216cdev.major = MAJOR(ap3216cdev.devid);
}

/* 2. 注册字符设备 */
cdev_init(&ap3216cdev.cdev, &ap3216c_ops);
cdev_add(&ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);

/* 3. 创建设备类和设备节点 */
ap3216cdev.class = class_create(THIS_MODULE, AP3216C_NAME);
if (IS_ERR(ap3216cdev.class)) return PTR_ERR(ap3216cdev.class);

ap3216cdev.device = device_create(ap3216cdev.class, NULL,
ap3216cdev.devid, NULL, AP3216C_NAME);
if (IS_ERR(ap3216cdev.device)) return PTR_ERR(ap3216cdev.device);

/* 4. 保存I2C客户端到私有数据 */
ap3216cdev.private_data = client;
printk("AP3216C probe success\n");
return 0;
}

/* I2C驱动移除:释放资源 */
static int ap3216c_remove(struct i2c_client *client)
{
cdev_del(&ap3216cdev.cdev);
unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT);
device_destroy(ap3216cdev.class, ap3216cdev.devid);
class_destroy(ap3216cdev.class);
printk("AP3216C remove success\n");
return 0;
}

/* I2C驱动结构体 */
static struct i2c_driver ap3216c_driver = {
.probe = ap3216c_probe,
.remove = ap3216c_remove,
.driver = {
.owner = THIS_MODULE,
.name = "ap3216c",
.of_match_table = ap3216c_of_match,
},
.id_table = ap3216c_id,
};

// --------------------------- 8. 驱动入口/出口 ---------------------------
static int __init ap3216c_init(void)
{
return i2c_add_driver(&ap3216c_driver);
}

static void __exit ap3216c_exit(void)
{
i2c_del_driver(&ap3216c_driver);
}

module_init(ap3216c_init);
module_exit(ap3216c_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("KEVIN");
MODULE_DESCRIPTION("AP3216C I2C Driver");

基于SMBUS编写代码:

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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/of_device.h>
#include <linux/slab.h>
#include <linux/delay.h>
#include <linux/mutex.h>
#include <linux/mod_devicetable.h>
#include <linux/bitops.h>
#include <linux/jiffies.h>
#include <linux/property.h>
#include <linux/acpi.h>
#include <linux/i2c.h>
#include <linux/nvmem-provider.h>
#include <linux/regmap.h>
#include <linux/pm_runtime.h>
#include <linux/gpio/consumer.h>
#include <linux/uaccess.h>
#include <linux/fs.h>

// 全局变量定义
static int major = 0; // 主设备号,0表示由内核动态分配
static struct class *ap3216c_class; // 设备类指针,用于创建sysfs节点
static struct i2c_client *ap3216c_client; // I2C客户端指针,指向匹配的I2C设备

/*
* @description: 读取设备数据(用户空间read系统调用触发)
* @param - file: 设备文件结构体
* @param - buf: 用户空间缓冲区,用于存储读取的数据
* @param - size: 要读取的字节数(必须为6字节,对应3个16位传感器数据)
* @param - offset: 文件偏移量(字符设备忽略)
* @return: 成功返回读取的字节数,失败返回错误码
* 数据格式:6字节,依次为IR(2字节)、光强(2字节)、距离(2字节)
*/
static ssize_t ap3216c_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
int err;
char kernel_buf[6]; // 内核缓冲区,暂存传感器数据
int val; // 临时存储16位传感器数据

// 检查读取长度是否为6字节(3个16位数据),不符合则返回参数错误
if (size != 6)
return -EINVAL;

// 读取IR传感器数据(寄存器地址0xA)
val = i2c_smbus_read_word_data(ap3216c_client, 0xA);
kernel_buf[0] = val & 0xff; // 低8位
kernel_buf[1] = (val >> 8) & 0xff; // 高8位

// 读取环境光传感器数据(寄存器地址0xC)
val = i2c_smbus_read_word_data(ap3216c_client, 0xC);
kernel_buf[2] = val & 0xff; // 低8位
kernel_buf[3] = (val >> 8) & 0xff; // 高8位

// 读取距离传感器数据(寄存器地址0xE)
val = i2c_smbus_read_word_data(ap3216c_client, 0xE);
kernel_buf[4] = val & 0xff; // 低8位
kernel_buf[5] = (val >> 8) & 0xff; // 高8位

// 将内核缓冲区数据拷贝到用户空间
err = copy_to_user(buf, kernel_buf, size);
return size; // 返回实际读取的字节数
}

/*
* @description: 打开设备(用户空间open系统调用触发)
* @param - node: 设备inode结构体
* @param - file: 设备文件结构体
* @return: 0表示成功,其他值表示失败
* 功能:初始化传感器,执行复位并配置工作模式
*/
static int ap3216c_open(struct inode *node, struct file *file)
{
// 写入0x04到寄存器0,触发传感器复位
i2c_smbus_write_byte_data(ap3216c_client, 0, 0x4);
mdelay(20); // 等待复位完成(至少10ms,此处留有余量)

// 写入0x03到寄存器0,配置传感器工作模式(同时开启IR、光强、距离检测)
i2c_smbus_write_byte_data(ap3216c_client, 0, 0x3);
mdelay(250); // 等待传感器稳定工作
return 0;
}

// 文件操作结构体,关联用户空间操作与驱动实现
static struct file_operations ap3216c_ops = {
.owner = THIS_MODULE, // 驱动所属模块,防止模块被意外卸载
.open = ap3216c_open, // 关联open操作
.read = ap3216c_read, // 关联read操作
};



/*
* @description: I2C驱动probe函数,当驱动与设备匹配时调用
* @param - client: I2C客户端结构体,代表匹配的I2C设备
* @param - id: 匹配的设备ID(传统匹配方式使用)
* @return: 0表示成功,其他值表示失败
* 功能:初始化字符设备,创建设备节点,完成驱动初始化
*/
static int ap3216c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__); // 调试信息:文件名、函数名、行号

ap3216c_client = client; // 保存I2C客户端指针

// 注册字符设备:主设备号为0(动态分配),设备名为"ap3216c",关联文件操作结构体
major = register_chrdev(0, "ap3216c", &ap3216c_ops);

// 创建设备类:类名为"ap3216c_class",用于在/sys/class下创建类目录
ap3216c_class = class_create(THIS_MODULE, "ap3216c_class");
// 创建设备节点:在/dev目录下创建"ap3216c"设备文件
device_create(ap3216c_class, NULL, MKDEV(major, 0), NULL, "ap3216c");

return 0;
}

/*
* @description: I2C驱动remove函数,当驱动被卸载或设备移除时调用
* @param - client: I2C客户端结构体
* @return: 0表示成功
* 功能:清理资源,删除设备节点和类,注销字符设备
*/
static int ap3216c_remove(struct i2c_client *client)
{
// 销毁设备节点
device_destroy(ap3216c_class, MKDEV(major, 0));
// 销毁设备类
class_destroy(ap3216c_class);

// 注销字符设备
unregister_chrdev(major, "ap3216c");

return 0;
}

/*
* 设备树匹配列表
* 用于驱动与设备树中的AP3216C节点匹配,compatible属性需与设备树一致
* 最后一个空结构体为哨兵,标记列表结束
*/
static const struct of_device_id of_match_ids_ap3216c[] = {
{ .compatible = "kevin,ap3216c" },
{}, // 哨兵
};
MODULE_DEVICE_TABLE(of, of_match_ids_ap3216c); // 向内核注册设备树匹配表

/*
* 传统I2C设备ID列表
* 用于无设备树系统中,通过设备名称匹配I2C设备
*/
static const struct i2c_device_id ap3216c_ids[] = {
{ "kevin,ap3216c", 0 },
{}, // 哨兵
};

// I2C驱动结构体,描述驱动的核心信息
static struct i2c_driver i2c_ap3216c_driver = {
.driver = {
.name = "ap3216c", // 驱动名称,用于内核管理和调试
.of_match_table = of_match_ids_ap3216c, // 设备树匹配表
},
.probe = ap3216c_probe, // 设备匹配成功时调用的函数
.remove = ap3216c_remove, // 设备移除时调用的函数
.id_table = ap3216c_ids, // 传统设备ID匹配表
};

/*
* @description: 驱动初始化函数,模块加载时调用
* @return: 0表示成功,其他值表示失败
* 功能:注册I2C驱动到内核
*/
static int __init i2c_driver_ap3216c_init(void)
{
return i2c_add_driver(&i2c_ap3216c_driver); // 注册I2C驱动
}
/*
* @description: 驱动退出函数,模块卸载时调用
* 功能:从内核注销I2C驱动
*/
static void __exit i2c_driver_ap3216c_exit(void)
{
i2c_del_driver(&i2c_ap3216c_driver); // 注销I2C驱动
}
module_init(i2c_driver_ap3216c_init); // 声明模块入口函数
module_exit(i2c_driver_ap3216c_exit); // 声明模块出口函数

// 模块元信息
MODULE_LICENSE("GPL"); // 模块许可证(GPL协议)
MODULE_AUTHOR("KEVIN"); // 模块作者
MODULE_DESCRIPTION("AP3216C I2C Driver"); // 模块描述

应用层代码:

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
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "sys/ioctl.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
#include <poll.h>
#include <sys/select.h>
#include <sys/time.h>
#include <signal.h>
#include <fcntl.h>
/*
* @description : main主程序
* @param - argc : argv数组元素个数
* @param - argv : 具体参数
* @return : 0 成功;其他 失败
*/
int main(int argc, char *argv[])
{
int fd;
char *filename;
unsigned short databuf[3];
unsigned short ir, als, ps;
int ret = 0;

if (argc != 2) {
printf("Error Usage!\r\n");
return -1;
}

filename = argv[1];
fd = open(filename, O_RDWR);
if(fd < 0) {
printf("can't open file %s\r\n", filename);
return -1;
}

while (1) {
ret = read(fd, databuf, sizeof(databuf));
if(ret == 0) { /* 数据读取成功 */
ir = databuf[0]; /* ir传感器数据 */
als = databuf[1]; /* als传感器数据 */
ps = databuf[2]; /* ps传感器数据 */
printf("ir = %d, als = %d, ps = %d\r\n", ir, als, ps);
}
usleep(200000); /*100ms */
}
close(fd); /* 关闭文件 */
return 0;
}

遇到的问题

1.参数不对

代码:

1
2
3
4
<1>dev->psd = ((unsigned short)(buf[5] & 0x3F) << 4) | (buf[4] & 0x0F);

<2>dev->psd = ((unsigned short)buf[5] & 0x3F << 4) | (buf[4] & 0x0F);

原因:

运算符优先级括号的作用 在 C 语言中,==<< 的优先级高于 &==,因此 0x3F << 4 会先计算。在表达式 2 中,缺少括号导致了错误的计算顺序。0x3F << 4 被错误地解释为一个常量掩码,而不是对 buf[5] 的结果进行位移。

表达式 1:

1
dev->psd = ((unsigned short)(buf[5] & 0x3F) << 4) | (buf[4] & 0x0F);

分析:

  • (buf[5] & 0x3F)buf[5] 的高两位被屏蔽掉,只保留低 6 位(0x3F 表示二进制 00111111)。
    • 最大值为 0x3F(即十进制 63)。
  • ((unsigned short)(buf[5] & 0x3F) << 4):将结果左移 4 位。
    • 左移 4 位相当于乘以 $2^4 = 16$。
    • 最大值为 63 * 16 = 1008
  • (buf[4] & 0x0F)buf[4] 的高四位被屏蔽掉,只保留低 4 位(0x0F 表示二进制 00001111)。
    • 最大值为 0x0F(即十进制 15)。
  • | 操作:将上述两部分按位或(bitwise OR)组合。
    • 最大值为 1008 + 15 = 1023

结论:

表达式 1 的最大值为 1023


表达式 2:

1
dev->psd = ((unsigned short)buf[5] & 0x3F << 4) | (buf[4] & 0x0F);

分析:

这里的关键问题是 运算符优先级括号的作用

  • 在 C 语言中,<< 的优先级高于 &,因此 0x3F << 4 会先计算。
    • 0x3F << 4:将 0x3F(即二进制 00111111)左移 4 位。
      • 结果为 0x3F0(即二进制 001111110000)。
  • ((unsigned short)buf[5] & 0x3F << 4) 等价于 ((unsigned short)buf[5] & 0x3F0)
    • 这里 buf[5] 只有 8 位,最高有效位为第 7 位。
    • 0x3F0 的低 8 位是 0000,因此 buf[5] & 0x3F0 的结果始终为 0
  • (buf[4] & 0x0F):这部分与表达式 1 相同,最大值为 15
  • | 操作:由于第一部分的结果始终为 0,最终结果完全由第二部分决定。
    • 最大值为 15

II2死锁

SCL一直为低

某个器件在“时钟拉伸”后没有释放 SCL,或控制器/短路把 SCL拉住。例如:从设备一直在内部处理/EEPROM写周期,一直拉低 SCL 却没超时保护。

解决方法:

  • 复位该从机(硬件复位脚或掉电重上电)。
  • 使用具有超时(timeout)功能的从设备。

SDA 一直为低

情况:常见于主机重置或异常中断在半个字节时,从机还在等剩余时钟,持续拉住 SDA。读写调用永远不返回/超时,逻辑分析仪上看不到新的 START/STOP。

原因:

  • 发生异常:
    • 假设在发送第N个比特时,主机将SCL拉低,准备设置下一位数据。与此同时,从机可能需要在这一位做出响应(例如,它正在输出数据位的‘0’,因此正在驱动SDA线为低电平)。
    • 就在SCL为低、SDA也为低的这个时刻,主机CPU突然复位了
  • 总线状态冻结:
    • 主机复位后,其I2C控制器模块(硬件)通常会被重置,停止驱动SCL和SDA线。SCL和SDA线通过外部上拉电阻变为高电平是“默认的释放状态”。
    • 但是,那个正在参与通信的从机没有复位!它仍然“记得”传输正在进行。因为它正在发送一个‘0’,所以它仍在紧紧地驱动SDA线为低电平,试图将这一位数据保持住。
    • 从机的这个行为覆盖了上拉电阻的效果,导致SDA线被强制拉低
  • 死锁形成(关键步骤)
    • 主机复位完成后,软件试图重新初始化I2C控制器并开始新的传输。
    • 在开始任何传输之前,主机的I2C控制器硬件(或软件)会执行一个总线忙检查(Bus Busy Check)。它检查SDASCL线是否都为高电平,以判断总线是否空闲
    • 此时,它检测到SDA线始终为低电平(被那个“固执”的从机拉着)。根据协议,总线被视为“忙”(BUSY)。
    • 因此,主机的I2C控制器拒绝产生起始条件(Start Condition),因为它要求起始条件必须在总线空闲时(SDA和SCL都为高)才能产生。
    • 矛盾就在这里:主机在等待SDA变高以启动传输,而从机在等待SCL线出现时钟脉冲(上升沿或下降沿)来完成它当前比特的传输并最终释放SDA。双方都在等待对方先行动,死锁就此发生。

解决方法:

  1. 将主机的SCL引脚配置为GPIO输出模式
  2. 循环控制该GPIO输出高电平和低电平,人为产生时钟脉冲。
  3. 同时监测SDA线。一旦发现SDA线被释放(变为高电平),立即发送一个停止条件(STOP)(先拉高SDA,再拉高SCL,然后先拉低SDA再拉高SDA)。
  4. 将SCL引脚切换回I2C功能模式。
  5. 重新初始化I2C总线。