3. 从机状态机设计:IDLE、ADDR、DATA_ACK、DATA_NACK 状态转移
状态机,说白了就是 I2C 从机的「大脑」。它决定了芯片什么时候该听、什么时候该说、什么时候该闭嘴。我见过不少新手工程师,上来就对着寄存器手册一顿配置,结果从机要么不响应,要么乱发数据——十有八九是状态机没理清楚。
这一章,我们就来拆解从机状态机的四个核心状态:IDLE、ADDR、DATA_ACK、DATA_NACK。搞懂了它们之间的转移关系,你写代码时心里就有底了。
3.1 状态机总览:四个状态,一条主线
先看整体框架。从机状态机其实不复杂,它只处理两件事:谁在跟我说话? 和 他要我干什么?
下面这张图,是我自己画的状态转移图。你把它存下来,写代码时对照着看,能省不少 debug 时间。
3.2 状态详解:每个状态在干什么?
3.2.1 IDLE 状态——等待的起点
IDLE 是默认状态。从机上电复位后,或者收到 STOP 条件后,都会回到这里。在这个状态下,从机什么都不做,只是盯着 SDA 线,等待 START 条件出现。
IDLE 状态下,从机要做的检查很简单:
- SDA 从高变低,同时 SCL 为高 → 检测到 START 条件 → 跳转到 ADDR 状态
- 否则 → 保持 IDLE
3.2.2 ADDR 状态——你是谁?
进入 ADDR 状态后,从机开始接收主机发送的 7 位地址(或 10 位地址的第一字节)和读写位。这是从机唯一一次「自我介绍」的机会。
地址匹配的逻辑是这样的:
| 条件 | 结果 | 下一个状态 |
|---|---|---|
| 接收到的地址 == 本机地址 | 拉低 SDA 发送 ACK | DATA_ACK |
| 接收到的地址 != 本机地址 | 释放 SDA(高阻) | IDLE |
| 接收到广播地址 (0x00) | 取决于是否使能广播响应 | DATA_ACK 或 IDLE |
3.2.3 DATA_ACK 状态——正常收发数据
这是从机最忙的状态。主机发数据,从机收;主机要数据,从机发。每次传输一个字节(8 位),然后从机在第 9 个 SCL 脉冲拉低 SDA 表示 ACK。
在 DATA_ACK 状态下,从机需要区分两种场景:
- 主机写从机(W=0): 从机接收数据字节,存入内部寄存器或 FIFO,然后发送 ACK。
- 主机读从机(R=1): 从机从内部寄存器读取数据,放到 SDA 上,然后等待主机发送 ACK。
这里有个容易搞混的地方:ACK 是谁发的?
- 主机写从机 → 从机发 ACK
- 主机读从机 → 主机发 ACK
你想想看,如果读操作时从机发 ACK,那 SDA 就被从机拉住了,主机根本没法发数据。所以方向一定要分清楚。
3.2.4 DATA_NACK 状态——异常或结束信号
DATA_NACK 状态通常意味着两件事之一:
- 主机不想继续读了: 主机在读操作的第 9 个时钟释放 SDA(不发 ACK),告诉从机「够了,别发了」。
- 从机不想继续收了: 从机在写操作的第 9 个时钟释放 SDA,告诉主机「我满了,别发了」。
进入 DATA_NACK 后,从机应该做什么?
我个人习惯在 DATA_NACK 状态里加一个超时计数器。如果超过一定时间(比如 10 个 SCL 周期)既没有 STOP 也没有 Sr,就强制复位到 IDLE。这是为了防止主机异常掉电导致总线卡死。
3.3 状态转移的完整流程
把四个状态串起来,一个典型的从机通信流程是这样的:
- IDLE → ADDR: 主机发送 START,从机检测到后进入地址匹配阶段。
- ADDR → DATA_ACK: 地址匹配成功,从机发送 ACK,准备收发数据。
- DATA_ACK → DATA_ACK: 正常传输,每发完一个字节,根据方向决定谁发 ACK,然后继续下一个字节。
- DATA_ACK → DATA_NACK: 主机发送 NACK(读操作)或从机发送 NACK(写操作),表示传输结束。
- DATA_NACK → IDLE: 主机发送 STOP 条件,从机回到空闲状态。
当然,还有几个特殊情况:
- 地址不匹配: ADDR → IDLE,从机忽略本次通信。
- 重复 START: 在任何数据状态下收到 Sr,都要回到 ADDR 状态重新匹配地址。
- 总线错误: 如果检测到 SDA 被异常拉低超过 9 个时钟,强制进入 IDLE。
3.4 代码实现要点
下面是一个简化的状态机实现框架。实际项目中,你需要在 SCL 的每个上升沿或下降沿触发状态检查。
typedef enum {
STATE_IDLE,
STATE_ADDR,
STATE_DATA_ACK,
STATE_DATA_NACK
} i2c_slave_state_t;
i2c_slave_state_t current_state = STATE_IDLE;
void i2c_slave_fsm(uint8_t sda_in, uint8_t scl_in) {
switch (current_state) {
case STATE_IDLE:
if (detect_start(sda_in, scl_in)) {
current_state = STATE_ADDR;
reset_bit_counter();
}
break;
case STATE_ADDR:
if (bit_counter == 8) {
if (address_match()) {
send_ack();
current_state = STATE_DATA_ACK;
} else {
send_nack();
current_state = STATE_IDLE;
}
}
break;
case STATE_DATA_ACK:
if (bit_counter == 8) {
if (direction == WRITE) {
// 从机接收数据,然后发 ACK
store_received_byte();
send_ack();
} else {
// 从机发送数据,等待主机 ACK
load_transmit_byte();
if (receive_ack()) {
// 主机想继续读
current_state = STATE_DATA_ACK;
} else {
// 主机不想读了
current_state = STATE_DATA_NACK;
}
}
}
break;
case STATE_DATA_NACK:
if (detect_stop(sda_in, scl_in)) {
current_state = STATE_IDLE;
} else if (detect_repeated_start(sda_in, scl_in)) {
current_state = STATE_ADDR;
}
break;
}
}
3.5 常见问题与避坑
最后,分享几个我在调试从机状态机时遇到过的典型问题:
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 从机不响应地址 | ADDR 状态里处理时间过长,错过了 ACK 窗口 | 把地址比较和 ACK 生成放在硬件里做,或者用更快的时钟 |
| 数据错位 | IDLE 状态没有复位位计数器或数据指针 | 进入 IDLE 时强制复位所有通信相关的指针和计数器 |
| 总线卡死 | 从机在 DATA_NACK 状态没有超时处理,主机异常掉电后一直等待 STOP | 增加超时计数器,超时后强制回到 IDLE |
| 重复 START 处理异常 | 状态机没有在 DATA_ACK 状态下检测 Sr | 在所有非 IDLE 状态下都要检测 Sr 条件 |
嗯,状态机这部分内容就这些。你把它吃透了,I2C 从机开发就成功了一大半。剩下的就是根据具体芯片的寄存器去适配了——但核心逻辑,万变不离其宗。