I2C 子系统
文章目录
- I2C 子系统
- I2C时序
- 三种状态
- **字节传输**
- 从机地址
- 读写过程
- Linux I2C 设备动态添加机制
- 方法
- 源码逻辑
- 设备支持条件
- 核心代码路径
- 诊断命令
- I2C 卡死
- 其它卡死场景
- SMBUS
I2C时序
I2C总线是由飞利浦(Philips)公司开发的一种双向二线制同步串行总线,实现有效的IC间的控制,它只需要两根线(SDA和SCL)即可在连接于总线上的器件之间传送信息。
I2C总线在传输数据都是按照bit来传送。SCL为时钟线,SDA为数据线;在SCL时钟线为高电平时,SDA数据线上的电平不允许被修改,SCL时钟线为低电平时,SDA数据线上的电平可为高/低
三种状态
- 起始条件:SCL为高电平时,SDA由高电平向低电平切换;表示开始传送数据。
- 停止条件:SCL为高电平时,SDA由低电平向高电平跳变;表示结束传送数据。
- 空闲条件:I2C总线的SDA和SCL两条信号线同时处于高电平时;表示空闲状态
字节传输
发送数据时,由主机先发送一个起始信号,再将SDA信号切换为输出模式,然后将8位数据依次由高到低发送出去;
发送完成后,主机将SDA信号切换为输入模式,等待丛机回应ACK或NAK;再发下一笔数据
从机地址
在I2C总线系统中,每个设备都有它的固定地址,一般由芯片的A0,A1和A2决定。丛机地址字节由**七位地址位(D7-D1位)和一位方向位(**为D0位)组成。
器件地址的D7-D4一般都是被厂家固定了为1111,余下的D3,D2和D1连接到芯片的A2,A1和A0决定;D0为0x00表示写,D0为0x01表示读。大家看例程都是些0xA0和0xA1就是这个原因。
读写过程
1.写数据过程
主机发送I2C总线停止信号,防止总线忙写数据失败
主机发送I2C总线复位信号,确保写数据之前总线处于空闲状态
主机发送I2C总线开始信号,启动一次数据的写入
主机发送I2C丛机地址和写模式(W/R=0)信号,并且等待一个丛机的应答信号
主机接收到ACK的应答信号后,开始多个字节的写入,每写完一个字节需要等待一个丛机的应答信号
主机接收到ACK的应答信号后,发送2IC总线停止信号,确保总线处于空闲状态
2.读数据过程
主机发送I2C总线停止信号,防止总线忙写数据失败
主机发送I2C总线复位信号,确保读数据之前总线处于空闲状态
主机发送I2C总线开始信号,启动一次数据读取
主机发送I2C丛机地址和读模式(W/R=1)信号,并且等待一个丛机的应答信号
主机接收到ACK的应答信号后,开始多个字节的读取,每读完一个字节需要给丛机发送一个ACK应答信号
主机接收到ACK的应答信号后,发送I2C总线停止信号,确保总线处于空闲状态
Linux I2C 设备动态添加机制
方法
方法1:Linux 内核提供了 sysfs 接口来动态添加/删除 I2C 设备
添加 I2C 设备:
echoicm42688 0x68>/sys/bus/i2c/devices/i2c-5/new_device删除 I2C 设备:
echo0x68>/sys/bus/i2c/devices/i2c-5/delete_device等同于设备树:
&i2c5 { icm42600@68 { compatible = "invensense,icm42600"; reg = <0x68>; }; };方法2:overlay 的方式
简单来说就是
源码逻辑
load_module do_init_module do_one_initcall inv_icm42600_driver_init i2c_register_driver driver_register bus_add_driver driver_attach bus_for_each_dev __driver_attach driver_probe_device __driver_probe_device really_probe i2c_device_probe inv_icm42600_probe设备支持条件
- ✅支持动态注册:驱动在
i2c_device_id表中声明设备名称(如{"icm42688", 0}) - ❌不支持场景:
- 设备已通过设备树/ACPI 静态定义
- 驱动未实现对应
i2c_device_id表项 - 内核配置禁用了
CONFIG_I2C_NEW_DEVICES
核心代码路径
| 阶段 | 关键函数 | 源码位置 |
|---|---|---|
| 系统调用 | sysfs_kf_write | fs/sysfs/file.c |
| 地址验证 | i2c_check_addr_validity | drivers/i2c/i2c-core-base.c |
| 设备创建 | i2c_new_client_device | drivers/i2c/i2c-core.c |
| 驱动匹配 | i2c_match_id | drivers/i2c/i2c-core.c |
诊断命令
# 检查I2C总线设备i2cdetect-y5# 跟踪内核I2C操作sudocat/sys/kernel/debug/tracing/trace_pipe&echoicm42688 0x68>/sys/bus/i2c/devices/i2c-5/new_device## I2C 初始化匹配流程:1. 内核解析设备树 → 创建 i2c_client 结构体2. i2c_client 包含:I2C地址(0x68)、设备树节点指针3. 内核比较 compatible="invensense,icm42600"与驱动的 of_match_table4. 匹配成功 → 调用 inv_icm42600_probe(&i2c_client, i2c_device_id)probe函数被调用的时机 ```c // 内核调用链 start_kernel → arch_initcall(i2c_init)// 初始化I2C子系统 → bus_register(&i2c_bus_type)// 注册I2C总线 → i2c_add_driver(&inv_icm42600_driver)// 驱动注册 → driver_register(&i2c_driver.driver)// 注册到内核驱动框架 → bus_for_each_dev(i2c_bus_type)// 遍历所有I2C设备 → driver_match_device(drv, dev)// 匹配设备 → of_driver_match_device // 检查设备树匹配 → acpi_driver_match_device // 检查ACPI匹配 → i2c_match_id // 检查ID表匹配 → driver_probe_device // 匹配成功,调用probe → really_probe → drv->probe(dev)// 调用 inv_icm42600_probeIRQ 识别问题
设备树节点到 IRQ 的映射过程: 设备树节点: interrupts=<GIC_SPI86IRQ_TYPE_LEVEL_HIGH>;// 或 GPIO中断 ↓ of_irq_parse_one()解析出: - interrupt-parent(指向中断控制器:GIC或GPIO控制器)- interrupt specifier(86, IRQ_TYPE_LEVEL_HIGH)↓ irq_create_of_mapping(): - 通过 interrupt-parent 找到 irq_domain - 使用 irq_domain->ops->xlate()转换specifier - 分配 Linux IRQ 号 ↓ 返回 Linux IRQ 号(如123)设备树编译(.dts →.dtb) early_init_dt_scan_root 解析设备树 of_i2c_register_devices// 注册I2C设备of_i2c_register_device i2c_new_client_device i2c_device_probe of_irq_get_byname of_irq_get of_irq_parse_one// 解析设备树中的 interrupts 属性of_property_read_u32_index(device,"interrupts",)irq_create_of_mapping// 将设备树中的中断描述符映射为Linux IRQ号I2C 卡死
现象:
核心问题分三个层次:为什么从机会拉低 SDA、为什么主机复位会触发卡死、如何恢复。
第一层:从机为什么会持续拉低 SDA?
I2C 协议规定:SDA 只能在 SCL 为低时改变。当从机正在输出逻辑 0(拉低 SDA)时,它必须等到"下一个应该输出高电平的下降沿"才会释放 SDA。如果这个下降沿迟迟不来——从机就会一直拉着 SDA 不放。
第二层:主机复位为什么会造成这个局面?
主机在通讯中途复位,外设状态机立刻回到初始状态,SCL 时钟停止输出。从机正好处于拉低 SDA 的状态,等不到下一个下降沿,就永远拿着 SDA 不松手。主机复位完成后回来一看:SCL 高、SDA 被拉低,无法发 START 条件,总线永久僵死。第三层:怎么恢复?
既然缺的是那个"下降沿",就用 GPIO 手动模拟 SCL 上的下降沿,逐个时钟探测,直到从机松开 SDA,然后发一个 STOP 条件正式结束这次通讯。
从机的状态机非常"死心眼":它在 SCL 下降沿驱动时,才会更新 SDA。一旦主机复位打断了 SCL 序列,从机就停在了"SDA=0"这个状态,不知道该什么时候松手。
恢复的本质是补发那个缺失的 SCL 下降沿。由于不知道复位发生在哪一个 bit 期间,只能逐个时钟试探,每发一个下降沿就检查一次 SDA 是否已经被释放(变高)。一旦 SDA 变高,立刻补发 STOP,告诉从机"这次通讯结束了",总线就干净了。
实际代码里通常做法是:在 I2C 初始化函数最开头加一段"总线恢复"逻辑,循环最多 9 次(I2C 最长一帧是 8 bit + 1 ACK = 9 个时钟)模拟 SCL,直到 SDA 被释放。
代码逻辑:
i2c_recover_bus i2c_generic_scl_recoveryinti2c_generic_scl_recovery(structi2c_adapter*adap){structi2c_bus_recovery_info*bri=adap->bus_recovery_info;// 1. 准备恢复if(bri->prepare_recovery)bri->prepare_recovery(adap);// 2. 切换到 GPIO 模式if(bri->pinctrl)pinctrl_select_state(bri->pinctrl,bri->pins_gpio);// 3. 发送 9 个 SCL 时钟脉冲while(i++<RECOVERY_CLK_CNT*2){bri->set_scl(adap,scl);ndelay(5000);// ~100KHz...}// 4. 切回 I2C 模式if(bri->pinctrl)pinctrl_select_state(bri->pinctrl,bri->pins_default);if(bri->unprepare_recovery)bri->unprepare_recovery(adap);}其它卡死场景
本质上都是同一个物理机制——SCL 序列在从机输出低电平期间被截断——但触发路径各不相同,危险程度也差很多
场景1:看门狗复位是最高频的,因为它的触发逻辑几乎形成了正反馈——I2C 传输卡住 → 代码阻塞 → 喂狗超时 → WDT 复位 → 复位后 I2C 再次卡死 → 再次阻塞 → 再次复位。系统就在这个循环里打转,日志里看到的是无限重启,但真正的根因是 I2C 卡死。很多人排查方向错了,以为是代码死锁,其实是总线问题。
场景2:DMA 中途被打断在做错误处理时特别容易踩。典型写法是传输出错了,代码里直接HAL_I2C_DeInit()然后HAL_I2C_Init()重新初始化,以为这样就干净了。但 DeInit 只是重置了主机侧的寄存器,从机根本不知道发生了什么,它还停在中途被截断的状态里。重新 Init 之后主机认为总线是干净的,但 SDA 仍然被从机拉着,于是立刻又卡死。
场景3:欠压掉电是最难复现也最难排查的。关键问题在于主机和从机的欠压复位门限不一样,假设主机的 UVLO 是 2.7V,从机是 2.2V,电池慢慢放电下来,主机先复位了,从机还活着且 SDA 被拉低,等电压回升主机恢复后就卡死了。这种问题只在低电量场合出现,实验室里用稳压电源根本看不到。
场景4:RTOS 任务强杀是多线程系统特有的陷阱。vTaskDelete()不会执行任何清理,如果一个任务正在做 blocking 的 I2C 读取时被删掉,SCL 就停在当前 bit。更隐蔽的是 mutex 超时场景:任务 A 拿着 I2C mutex 在传输,任务 B 等 mutex 超时后放弃并继续跑别的逻辑,但任务 A 的 I2C 传输可能还没结束,两个任务同时认为自己能操作总线,状态完全乱了。
一个实用结论:只要系统里有看门狗,并且 I2C 驱动是阻塞式的(传输等待没有超时保护),就必然会在某个时机触发这个问题。正确的做法是:I2C 传输设超时 → 超时后主动执行总线恢复(9个时钟 + STOP)→ 恢复成功后再喂狗或重试,而不是直接复位或 DeInit
SMBUS
有关系,而且关系很直接。SMBus 本身就是在 I2C 基础上专门为了解决"总线卡死无法自愈"这类问题而设计的,WDT 是它的核心特性之一
核心区别一句话:系统 WDT 是导致 I2C 卡死的"凶手",SMBus Timeout 是从机侧内置的"自愈机制",两者方向相反。
SMBus Timeout 的工作原理是从机内部有一个硬件计时器,一旦 SCL 持续低超过 25ms(这个门限叫TIMEOUT),从机状态机就强制复位,SDA 自动释放。所以如果你的从机是兼容 SMBus 的器件(很多 PMU、电池管理 IC、温度传感器都支持),即使主机 WDT 复位导致 SCL 停摆,最多等 25ms 从机就自己放开 SDA 了,总线能自愈,不需要主机做 9 个时钟恢复。
但有几个常见误解要注意:
纯 I2C 从机不支持 SMBus Timeout。你的芯片数据手册里如果没有明确写 “SMBus compatible” 或者SMBUS_TIMEOUT,那就没有这个自愈能力,WDT 复位之后总线照样卡死。
SMBus Timeout 只保护 SCL 低的情况。它解决的是 clock stretching 超时,也就是 SCL 被拉低太久。但上一讲说的"SCL 冻结在高电平 + SDA 被拉低"这种状态,从机根本不会触发 timeout(因为 SCL 不是低的),所以 SMBus Timeout 对 WDT 复位导致的典型卡死场景实际上帮不上忙。这是一个容易被忽视的盲区。
实际工程上的选择:如果你用的是消费类 I2C 传感器,基本没有 SMBus Timeout,老老实实在主机侧做好总线恢复逻辑。如果用的是 PMBus 电源管理器件或者工业级 SMBus 设备,可以依赖它的 Timeout,但也要搞清楚它能覆盖哪种卡死场景,不要无脑依赖。