I2C总线恢复期间错误位处理与重试 - 第2节
2. I2C 总线状态机与数据传输流程归纳总结
做 I2C 驱动十几年了,我最大的体会是:理解状态机,比死记时序参数更重要。 你想想看,总线上一旦出问题,你连当前处在哪个阶段都说不清,那还谈什么恢复重试?
这一节我们就彻底把 I2C 的“脉搏”摸清楚。从状态机开始,再到数据怎么流动,最后归纳一张我自己的总结表格——嗯,这张表当年救过我不少次。
2.1 I2C 总线状态机
I2C 总线其实没那么多花哨的状态。主器件内部的有限状态机,核心就那么六七种。我习惯把它们分成两类:控制状态和传输状态。
- 空闲(IDLE):SCL 和 SDA 都释放为高电平。这是起点,也是终点。
- 起始(START):SCL 高时,SDA 从高变低。主器件宣告“我要开始说话了”。
- 重复起始(RESTART):传输中途再拉一次起始条件,用于改变方向或切换从机。
- 数据发送/接收(DATA):每字节 8 位,高位在前。主器件负责提供时钟。
- 应答(ACK):第 9 个时钟,接收方拉低 SDA 表示“收到”。
- 非应答(NACK):第 9 个时钟,SDA 保持高,表示“不想要”或“结束”。
- 停止(STOP):SCL 高时,SDA 从低变高。总线回到空闲。
避坑重点: 我曾经在项目中遇到一个坑——从机在应答位之后没有释放 SDA,导致主器件无法发出停止条件。那时候我才意识到:状态机里,每个状态之间的切换条件都必须严格检查,尤其是应答后的总线释放。
下面这张状态图是我自己画的精简版,覆盖了最常见的完整传输路径。你仔细看箭头上的条件,尤其是 ACK/NACK 的分支——很多 I2C 锁死就发生在这里。
我的小技巧: 在调试时,我会在代码里用一个枚举变量记录当前状态机 step。每次时钟跳变都检查 step 是否合法——这样一旦总线卡住,我能立刻定位到是哪个阶段出了问题,而不是盲目测波形。
2.2 数据传输流程——主从握手详解
好,状态机清楚了,我们再看数据怎么流动。一个典型的 I2C 传输,说白了就是主器件和从器件之间的一场“一问一答”。
- 主器件发出起始条件,总线上所有从机都知道要开始了。
- 主器件发送 7 位从机地址 + 读写位(第 8 位)。高 7 位是地址,最低位 0 表示写,1 表示读。
- 被寻址的从机在第 9 个时钟拉低 SDA 应答。其他从机不理睬。
- 主器件开始发送或接收数据字节。每次 8 位,之后等待应答。
- 如果是从机应答了 ACK,可以继续下一个字节;如果是 NACK,主器件必须停止或者发重复起始。
- 全部传输完成,主器件发出停止条件,总线释放。
2.3 关键细节:应答与仲裁
这里我要重点提两个容易被忽略的点。
警告: 我曾经遇到过从机在应答位之后应该释放 SDA 却没有释放的情况(硬件 bug),导致主器件拉 SDA 为低想发停止,结果 SDA 一直被从机拉着不上去——总线直接锁死。后来我们的重试机制里加了一步:发现异常后先发 9 个时钟脉冲“清场”,再复位从机。
仲裁呢?I2C 允许多主器件,但我们做恢复重试时,仲裁会导致数据冲突。我的建议是:恢复期间,软件层面确保只有主控器主动释放总线,不要同时多个主器件抢线。 否则重试时序会乱套。
2.4 归纳总结——一张实战表
下面这张表是我这些年调 I2C 时反复用到的手册。每次写代码前扫一眼,能避免八九成的低级错误。
| 阶段 | SCL 行为 | SDA 行为 | 主器件责任 | 从器件责任 |
|---|---|---|---|---|
| 空闲 | 高 | 高(上拉) | 释放 SCL & SDA | 释放 SDA |
| 起始 | 高→保持高 | 高→低 | 拉低 SDA | 检测起始 |
| 地址发送 | 产生 8 个脉冲 | 按位输出 | 输出地址+读写位 | 匹配地址 |
| 应答位 | 产生第 9 脉冲 | 输入/输出 | 释放 SDA | 拉低则为 ACK |
| 数据写 | 产生 8 个脉冲 | 主输出 | 输出数据 | 采样数据 |
| 数据读 | 产生 8 个脉冲 | 从输出 | 采样数据 | 输出数据 |
| 停止 | 高→保持高 | 低→高 | 释放 SDA | 检测停止 |
避坑指南: 我曾经在写驱动时忽略了“读操作后主器件必须发 NACK 来结束”,结果从机一直输出下一字节,总线乱套。记住:读最后一个字节之前,主器件必须预先把应答位设置为 NACK,然后在第 9 个时钟不拉低 SDA。
2.5 为什么状态机对重试这么关键?
我见过不少工程师,一遇到 I2C 超时就盲目发停止条件。其实更高效的做法是:先检查当前总线状态(空闲、起始中、数据传输中?),然后“对症下药”。
- 如果总线空闲(SCL & SDA 都高),直接重置状态机,重发起始。
- 如果总线卡在某个字节中途(SCL 为低且 SDA 不确定),先发 9 个时钟脉冲让从机释放 SDA,再发停止。
- 如果从机死锁(应答位后 SDA 被拉低不放),需要强制拉高 SDA 时钟,甚至复位硬件。
说白了,没有状态机这把尺子,你测再多波形也找不到根因。而这张状态图 + 上面的表格,就是我每次写重试逻辑时摆在手边的“挂图”。
嗯,这一节的内容就这些。记住:状态机是骨架,数据流是血肉,重试是医生。 下回遇到 I2C 异常,先问问自己——现在总线在哪个状态?
2. I2C 总线状态机与数据传输流程归纳总结
做 I2C 驱动十几年了,我最大的体会是:理解状态机,比死记时序参数更重要。 你想想看,总线上一旦出问题,你连当前处在哪个阶段都说不清,那还谈什么恢复重试?
这一节我们就彻底把 I2C 的“脉搏”摸清楚。从状态机开始,再到数据怎么流动,最后归纳一张我自己的总结表格——嗯,这张表当年救过我不少次。
2.1 I2C 总线状态机
I2C 总线其实没那么多花哨的状态。主器件内部的有限状态机,核心就那么六七种。我习惯把它们分成两类:控制状态和传输状态。
- 空闲(IDLE):SCL 和 SDA 都释放为高电平。这是起点,也是终点。
- 起始(START):SCL 高时,SDA 从高变低。主器件宣告“我要开始说话了”。
- 重复起始(RESTART):传输中途再拉一次起始条件,用于改变方向或切换从机。
- 数据发送/接收(DATA):每字节 8 位,高位在前。主器件负责提供时钟。
- 应答(ACK):第 9 个时钟,接收方拉低 SDA 表示“收到”。
- 非应答(NACK):第 9 个时钟,SDA 保持高,表示“不想要”或“结束”。
- 停止(STOP):SCL 高时,SDA 从低变高。总线回到空闲。
避坑重点: 我曾经在项目中遇到一个坑——从机在应答位之后没有释放 SDA,导致主器件无法发出停止条件。那时候我才意识到:状态机里,每个状态之间的切换条件都必须严格检查,尤其是应答后的总线释放。
下面这张状态图是我自己画的精简版,覆盖了最常见的完整传输路径。你仔细看箭头上的条件,尤其是 ACK/NACK 的分支——很多 I2C 锁死就发生在这里。
我的小技巧: 在调试时,我会在代码里用一个枚举变量记录当前状态机 step。每次时钟跳变都检查 step 是否合法——这样一旦总线卡住,我能立刻定位到是哪个阶段出了问题,而不是盲目测波形。
2.2 数据传输流程——主从握手详解
好,状态机清楚了,我们再看数据怎么流动。一个典型的 I2C 传输,说白了就是主器件和从器件之间的一场“一问一答”。
- 主器件发出起始条件,总线上所有从机都知道要开始了。
- 主器件发送 7 位从机地址 + 读写位(第 8 位)。高 7 位是地址,最低位 0 表示写,1 表示读。
- 被寻址的从机在第 9 个时钟拉低 SDA 应答。其他从机不理睬。
- 主器件开始发送或接收数据字节。每次 8 位,之后等待应答。
- 如果是从机应答了 ACK,可以继续下一个字节;如果是 NACK,主器件必须停止或者发重复起始。
- 全部传输完成,主器件发出停止条件,总线释放。
2.3 关键细节:应答与仲裁
这里我要重点提两个容易被忽略的点。
警告: 我曾经遇到过从机在应答位之后应该释放 SDA 却没有释放的情况(硬件 bug),导致主器件拉 SDA 为低想发停止,结果 SDA 一直被从机拉着不上去——总线直接锁死。后来我们的重试机制里加了一步:发现异常后先发 9 个时钟脉冲“清场”,再复位从机。
仲裁呢?I2C 允许多主器件,但我们做恢复重试时,仲裁会导致数据冲突。我的建议是:恢复期间,软件层面确保只有主控器主动释放总线,不要同时多个主器件抢线。 否则重试时序会乱套。
2.4 归纳总结——一张实战表
下面这张表是我这些年调 I2C 时反复用到的手册。每次写代码前扫一眼,能避免八九成的低级错误。
| 阶段 | SCL 行为 | SDA 行为 | 主器件责任 | 从器件责任 |
|---|---|---|---|---|
| 空闲 | 高 | 高(上拉) | 释放 SCL & SDA | 释放 SDA |
| 起始 | 高→保持高 | 高→低 | 拉低 SDA | 检测起始 |
| 地址发送 | 产生 8 个脉冲 | 按位输出 | 输出地址+读写位 | 匹配地址 |
| 应答位 | 产生第 9 脉冲 | 输入/输出 | 释放 SDA | 拉低则为 ACK |
| 数据写 | 产生 8 个脉冲 | 主输出 | 输出数据 | 采样数据 |
| 数据读 | 产生 8 个脉冲 | 从输出 | 采样数据 | 输出数据 |
| 停止 | 高→保持高 | 低→高 | 释放 SDA | 检测停止 |
避坑指南: 我曾经在写驱动时忽略了“读操作后主器件必须发 NACK 来结束”,结果从机一直输出下一字节,总线乱套。记住:读最后一个字节之前,主器件必须预先把应答位设置为 NACK,然后在第 9 个时钟不拉低 SDA。
2.5 为什么状态机对重试这么关键?
我见过不少工程师,一遇到 I2C 超时就盲目发停止条件。其实更高效的做法是:先检查当前总线状态(空闲、起始中、数据传输中?),然后“对症下药”。
- 如果总线空闲(SCL & SDA 都高),直接重置状态机,重发起始。
- 如果总线卡在某个字节中途(SCL 为低且 SDA 不确定),先发 9 个时钟脉冲让从机释放 SDA,再发停止。
- 如果从机死锁(应答位后 SDA 被拉低不放),需要强制拉高 SDA 时钟,甚至复位硬件。
说白了,没有状态机这把尺子,你测再多波形也找不到根因。而这张状态图 + 上面的表格,就是我每次写重试逻辑时摆在手边的“挂图”。
嗯,这一节的内容就这些。记住:状态机是骨架,数据流是血肉,重试是医生。 下回遇到 I2C 异常,先问问自己——现在总线在哪个状态?