4. 从机发送模式实现:从机响应主机读请求的数据发送流程
好,咱们接着聊。上一章我们把从机接收模式讲透了,说白了就是主机写、从机收。那反过来呢?主机要从从机读数据,这时候从机就得切换到发送模式。这个场景在实际项目里太常见了——比如主机去读传感器的温度值、读EEPROM里存的数据,都是这个流程。
我记得刚做I2C从机那会儿,总觉得发送比接收简单。结果呢?第一次调通接收,换到发送就翻车了。问题出在哪?时钟同步、数据对齐、应答处理,一个没注意就卡死。今天咱们就把这个流程彻底拆开,讲清楚。
4.1 从机发送模式的核心流程
先看整体流程。主机发起读操作时,总线上的信号序列是这样的:
- 主机发送起始条件(S)
- 主机发送从机地址 + 读位(7位地址 + R/W=1)
- 从机应答(ACK)
- 从机发送数据字节(每字节8位,MSB先发)
- 主机应答(ACK表示还要继续读,NACK表示够了)
- 重复步骤4-5,直到主机发NACK
- 主机发送停止条件(P)
嗯,这里有个关键点:应答方向反了。接收模式是从机发ACK给主机,发送模式是主机发ACK给从机。很多新手在这里搞混,我当年也踩过这个坑。
核心原则:谁发数据,谁就不管应答;谁收数据,谁就负责应答。
发送模式下:从机发数据,主机收数据,所以主机发ACK/NACK。
4.2 从机发送模式的时序细节
咱们把时序拆开看。下面这张图是我画的标准从机发送时序,你注意看SCL和SDA的配合关系。
你看这个时序,从机发数据的时候,SDA的变化必须发生在SCL低电平期间。主机在第9个时钟周期拉低SDA表示ACK,拉高表示NACK。这个节奏一旦乱了,总线就卡死了。
4.3 从机发送模式的代码实现
下面给一个实际的从机发送代码框架。我用的是状态机写法,这也是我项目里最常用的方式。
// 从机发送状态机
typedef enum {
SLAVE_TX_IDLE,
SLAVE_TX_ADDR_MATCH,
SLAVE_TX_DATA_SEND,
SLAVE_TX_WAIT_ACK,
SLAVE_TX_STOP
} slave_tx_state_t;
static slave_tx_state_t tx_state = SLAVE_TX_IDLE;
static uint8_t tx_buffer[256];
static uint8_t tx_index = 0;
static uint8_t tx_len = 0;
static uint8_t bit_count = 0;
static uint8_t shift_reg = 0;
// 主状态机 - 在SCL上升沿触发
void slave_tx_fsm(void) {
switch(tx_state) {
case SLAVE_TX_IDLE:
// 等待起始条件 + 地址匹配
if (start_detected && addr_match && (rw_bit == 1)) {
tx_state = SLAVE_TX_ADDR_MATCH;
// 准备发送数据
tx_index = 0;
bit_count = 0;
shift_reg = tx_buffer[tx_index];
// 拉低SDA表示ACK(在第9个时钟)
sda_direction(OUTPUT);
sda_write(0);
}
break;
case SLAVE_TX_ADDR_MATCH:
// 发送ACK后,准备发数据
if (scl_falling_edge) {
tx_state = SLAVE_TX_DATA_SEND;
bit_count = 0;
shift_reg = tx_buffer[tx_index];
// 准备发送MSB
sda_write((shift_reg & 0x80) ? 1 : 0);
}
break;
case SLAVE_TX_DATA_SEND:
// 在SCL上升沿移出数据位
if (scl_rising_edge) {
bit_count++;
if (bit_count < 8) {
shift_reg <<= 1;
sda_write((shift_reg & 0x80) ? 1 : 0);
} else {
// 8位发完,释放SDA,等待主机ACK
sda_direction(INPUT); // 释放总线
tx_state = SLAVE_TX_WAIT_ACK;
}
}
break;
case SLAVE_TX_WAIT_ACK:
// 在第9个SCL上升沿采样SDA
if (scl_rising_edge) {
if (sda_read() == 0) {
// 主机发ACK,继续发下一字节
tx_index++;
if (tx_index < tx_len) {
tx_state = SLAVE_TX_DATA_SEND;
bit_count = 0;
shift_reg = tx_buffer[tx_index];
sda_direction(OUTPUT);
sda_write((shift_reg & 0x80) ? 1 : 0);
} else {
// 数据发完,等待停止
tx_state = SLAVE_TX_STOP;
}
} else {
// 主机发NACK,停止发送
tx_state = SLAVE_TX_STOP;
}
}
break;
case SLAVE_TX_STOP:
// 检测到停止条件后复位
if (stop_detected) {
tx_state = SLAVE_TX_IDLE;
sda_direction(INPUT);
}
break;
}
}
个人经验:这个状态机里最容易被忽略的是SDA方向切换。发数据时SDA是输出,等主机ACK时得切回输入。我见过有人忘了切方向,结果主机拉不低SDA,总线直接锁死。调试时用逻辑分析仪看SDA波形,一眼就能发现问题。
4.4 多字节发送与地址自增
实际项目中,从机往往要发多个字节。比如读一个温度传感器,主机先写寄存器地址,再重新发起读操作,从机从那个地址开始连续发数据。这里有个常见的设计模式:内部地址自增。
| 场景 | 主机操作 | 从机行为 |
|---|---|---|
| 单字节读 | 发地址+读位,收1字节,发NACK+停止 | 发送当前地址的数据 |
| 多字节读(连续) | 发地址+读位,收N字节,最后发NACK+停止 | 每发一字节,内部地址+1,继续发下一字节 |
| 随机地址读 | 先写寄存器地址,再重新发起读操作 | 从指定地址开始发送 |
我记得有个项目,从机内部地址自增逻辑写错了,地址到255后没回绕到0,结果读出来的数据全是乱的。后来加了个取模运算才搞定。
4.5 常见坑点与调试技巧
坑点1:从机发完数据后不释放SDA
我曾经遇到一个从机,发完最后一个字节后,SDA一直保持输出低电平。主机想发停止条件都发不了,总线直接死锁。解决办法:在NACK检测后,强制将SDA切回输入模式。
坑点2:时钟拉伸导致时序错乱
有些从机处理速度慢,会拉低SCL让主机等待。如果主机不支持时钟拉伸,或者从机拉伸时间太长,就会超时。我建议从机代码里加一个超时保护,SCL低电平超过10ms就复位状态机。
坑点3:起始条件重复检测
多字节读时,主机可能会发重复起始条件(Sr)。从机必须能识别Sr,并且不能把它当成停止条件。我见过一个从机,每次遇到Sr就复位,结果数据全丢了。
4.6 调试实战:用逻辑分析仪抓波形
说一千道一万,不如看波形。下面是我调试从机发送模式时常用的检查步骤:
- 检查起始条件:SDA在SCL高电平时从高变低,这个边沿必须干净
- 检查地址+读位:7位地址 + 1位读(1),确认地址没错
- 检查从机ACK:第9个时钟,SDA被从机拉低
- 检查数据字节:8位数据,MSB先发,SCL高电平时SDA稳定
- 检查主机ACK/NACK:第9个时钟,SDA被主机拉低(ACK)或拉高(NACK)
- 检查停止条件:SDA在SCL高电平时从低变高
我的调试口诀:
起始看边沿,地址看对齐,应答看第九,数据看稳定,停止看上升。
每次调不通,按这个顺序查一遍,90%的问题都能定位。
4.7 从机发送模式的状态机总图
最后,我把整个从机发送模式的状态机画成一张图,方便你对照代码理解。
嗯,这张图基本把从机发送的整个生命周期都画出来了。你写代码的时候,对着这个状态机一步步实现就行。记住,每个状态转换都要对应一个明确的SCL边沿事件,别在中间状态里做耗时操作。