4. 从机发送模式实现:从机响应主机读请求的数据发送流程

好,咱们接着聊。上一章我们把从机接收模式讲透了,说白了就是主机写、从机收。那反过来呢?主机要从从机读数据,这时候从机就得切换到发送模式。这个场景在实际项目里太常见了——比如主机去读传感器的温度值、读EEPROM里存的数据,都是这个流程。

我记得刚做I2C从机那会儿,总觉得发送比接收简单。结果呢?第一次调通接收,换到发送就翻车了。问题出在哪?时钟同步、数据对齐、应答处理,一个没注意就卡死。今天咱们就把这个流程彻底拆开,讲清楚。

4.1 从机发送模式的核心流程

先看整体流程。主机发起读操作时,总线上的信号序列是这样的:

  1. 主机发送起始条件(S)
  2. 主机发送从机地址 + 读位(7位地址 + R/W=1)
  3. 从机应答(ACK)
  4. 从机发送数据字节(每字节8位,MSB先发)
  5. 主机应答(ACK表示还要继续读,NACK表示够了)
  6. 重复步骤4-5,直到主机发NACK
  7. 主机发送停止条件(P)

嗯,这里有个关键点:应答方向反了。接收模式是从机发ACK给主机,发送模式是主机发ACK给从机。很多新手在这里搞混,我当年也踩过这个坑。

核心原则:谁发数据,谁就不管应答;谁收数据,谁就负责应答。

发送模式下:从机发数据,主机收数据,所以主机发ACK/NACK。

4.2 从机发送模式的时序细节

咱们把时序拆开看。下面这张图是我画的标准从机发送时序,你注意看SCL和SDA的配合关系。

从机发送模式时序流程 SCL SDA S 从机地址 + 读位 ACK 数据字节(从机发) ACK P 关键时序要点: 1. 从机在SCL低电平时改变SDA数据,SCL高电平时SDA必须稳定 2. 主机在第9个SCL周期发送ACK/NACK,从机在第9个周期释放SDA 3. 主机发NACK后,从机必须释放总线,等待主机发停止条件

你看这个时序,从机发数据的时候,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 调试实战:用逻辑分析仪抓波形

说一千道一万,不如看波形。下面是我调试从机发送模式时常用的检查步骤:

  1. 检查起始条件:SDA在SCL高电平时从高变低,这个边沿必须干净
  2. 检查地址+读位:7位地址 + 1位读(1),确认地址没错
  3. 检查从机ACK:第9个时钟,SDA被从机拉低
  4. 检查数据字节:8位数据,MSB先发,SCL高电平时SDA稳定
  5. 检查主机ACK/NACK:第9个时钟,SDA被主机拉低(ACK)或拉高(NACK)
  6. 检查停止条件:SDA在SCL高电平时从低变高

我的调试口诀:

起始看边沿,地址看对齐,应答看第九,数据看稳定,停止看上升。

每次调不通,按这个顺序查一遍,90%的问题都能定位。

4.7 从机发送模式的状态机总图

最后,我把整个从机发送模式的状态机画成一张图,方便你对照代码理解。

从机发送模式状态机 IDLE ADDR_MATCH DATA_SEND WAIT_ACK STOP 起始+地址匹配+读位 ACK完成 8位发完 NACK或数据发完 停止条件 ACK且还有数据 注意:每个状态转换都对应一个SCL时钟边沿事件,状态机必须在SCL上升沿或下降沿触发

嗯,这张图基本把从机发送的整个生命周期都画出来了。你写代码的时候,对着这个状态机一步步实现就行。记住,每个状态转换都要对应一个明确的SCL边沿事件,别在中间状态里做耗时操作。