4. 从机模式配置:从机地址设定、从模式中断响应与数据收发

好,咱们进入第4章。前面几章我们把主机模式讲透了,从主机怎么发起始条件,到怎么收发数据,再到怎么处理仲裁和时钟拉伸。但I2C总线是个多主从系统,光会当主机不行——你的设备很多时候是作为从机挂在总线上的。比如一个温度传感器、一个EEPROM、一个ADC,它们都是从机。

从机模式说白了就是「被动响应」。主机不发请求,从机就不能主动开口。但被动不等于简单,从机要能正确识别自己的地址,要能及时响应中断,还要能准确收发数据。这里面的坑,我踩过不少。

4.1 从机地址设定:7位地址与10位地址

每个I2C从机设备都有一个唯一的地址。主机就是靠这个地址来选中具体跟谁通信的。地址分两种:7位地址和10位地址。

地址类型 地址范围 可用设备数 典型应用
7位地址 0x08 ~ 0x77 112个 绝大多数传感器、存储器
10位地址 0x0000 ~ 0x03FF 1024个 复杂系统、FPGA、多路复用器

关键点:7位地址中,0x00是通用呼叫地址,0x01是起始字节,0x02~0x07保留,0x78~0x7F也是保留地址。真正能用的只有0x08~0x77这112个地址。

我个人习惯在设计阶段就把地址规划好。一个总线上挂多个同型号传感器时,它们的地址通常通过引脚电平来配置。比如一个温度传感器有A0、A1、A2三个地址引脚,就能组合出8个不同地址。

经验之谈:我在项目中遇到过地址冲突导致通信完全乱套的情况。两个设备用了同一个地址,主机发数据时两个从机同时响应,总线数据全被破坏。后来我养成了一个习惯:上电后先让主机扫描一遍总线上的所有地址,确认没有冲突再开始正常通信。

4.1.1 7位地址的寄存器配置

以STM32为例,从机地址配置在I2C的OAR1寄存器中。代码很简单:

// 设置从机地址为0x32(7位模式)
I2C1->OAR1 = (0x32 << 1) | (1 << 14);  // 左移1位是因为地址位在bit7:1
// bit14置1使能从机地址匹配

嗯,这里要注意:I2C协议中地址是7位,但寄存器里通常左移1位存放,最低位是读写标志位。所以0x32左移1位变成0x64,再写入寄存器。

4.1.2 10位地址的配置

10位地址稍微复杂一点。主机发送10位地址时,先发11110xx作为头字节,再发剩下的8位地址。从机这边要配置两个寄存器:

// 设置10位从机地址为0x02A0
I2C1->OAR2 = 0x02A0;  // 高8位地址
I2C1->OAR1 = (0x02A0 >> 8) | (1 << 15);  // 低2位 + 10位模式使能

警告:10位地址和7位地址不能混用在一个总线上?其实可以,但主机必须知道哪个设备是10位地址。我建议除非必要,尽量用7位地址,省心。

4.2 从模式中断响应机制

从机怎么知道主机来找它了?靠中断。I2C从机在检测到起始条件后,会逐位接收地址,然后与自己配置的地址比较。匹配上了,就产生一个中断,通知CPU来处理。

典型的从机中断事件有这几个:

  • 地址匹配中断(ADDR):主机发送的地址与从机地址一致
  • 接收数据中断(RXNE):从机接收到了一个字节数据
  • 发送数据中断(TXE):从机需要发送下一个字节数据
  • 停止条件中断(STOP):主机发送了停止条件

我刚开始做从机驱动时,犯过一个低级错误:中断里没清标志位,结果CPU一直卡在中断里出不来。后来我写了个检查清单:

  1. 进入中断后先读状态寄存器,判断是哪个事件
  2. 处理完事件后,必须清除对应的中断标志
  3. 退出中断前,检查是否还有其他待处理事件

核心原则:从机中断处理要快,不能在里面做延时操作。因为I2C总线有超时机制,主机等太久会认为从机没响应,直接发停止条件退出通信。

4.3 从机数据收发流程

从机收发数据,核心就是两个方向:主机写从机(从机接收),主机读从机(从机发送)。

4.3.1 主机写从机(从机接收)

流程是这样的:

  1. 主机发起始条件 + 从机地址 + 写位(0)
  2. 从机地址匹配,产生ADDR中断
  3. 从机在中断中确认地址,准备接收数据
  4. 主机发一个字节数据,从机产生RXNE中断
  5. 从机读DR寄存器,取走数据
  6. 重复步骤4-5,直到主机发停止条件
// 从机接收中断处理示例
void I2C1_EV_IRQHandler(void) {
    if (I2C1->SR1 & I2C_SR1_ADDR) {
        // 地址匹配,清除ADDR标志
        uint32_t tmp = I2C1->SR1;
        tmp = I2C1->SR2;
        (void)tmp;
        rx_index = 0;
    }
    
    if (I2C1->SR1 & I2C_SR1_RXNE) {
        // 接收到一个字节
        rx_buffer[rx_index++] = I2C1->DR;
    }
    
    if (I2C1->SR1 & I2C_SR1_STOPF) {
        // 收到停止条件,通信结束
        I2C1->CR1 |= I2C_CR1_PE;  // 清除STOPF
        rx_complete = 1;
    }
}

4.3.2 主机读从机(从机发送)

这个方向容易出问题。主机读从机时,从机要主动发送数据。流程:

  1. 主机发起始条件 + 从机地址 + 读位(1)
  2. 从机地址匹配,产生ADDR中断
  3. 从机在中断中确认地址,准备发送数据
  4. 从机往DR寄存器写第一个字节
  5. 主机读取数据,从机产生TXE中断
  6. 从机在TXE中断中写下一个字节到DR
  7. 重复步骤5-6,直到主机发NACK + 停止条件

注意:从机发送时,最后一个字节发完后,主机如果发NACK,从机就不会再产生TXE中断了。所以从机要能检测到NACK信号,及时停止发送。

// 从机发送中断处理示例
void I2C1_EV_IRQHandler(void) {
    if (I2C1->SR1 & I2C_SR1_ADDR) {
        // 地址匹配,准备发送
        uint32_t tmp = I2C1->SR1;
        tmp = I2C1->SR2;
        (void)tmp;
        tx_index = 0;
        // 预写第一个字节
        I2C1->DR = tx_buffer[tx_index++];
    }
    
    if (I2C1->SR1 & I2C_SR1_TXE) {
        // 发送缓冲区空,写下一个字节
        if (tx_index < tx_length) {
            I2C1->DR = tx_buffer[tx_index++];
        }
    }
    
    if (I2C1->SR1 & I2C_SR1_AF) {
        // 收到NACK,主机不想再读了
        I2C1->SR1 &= ~I2C_SR1_AF;  // 清除NACK标志
        tx_complete = 1;
    }
}

4.4 从机模式的核心流程图

下面这张图把从机模式的核心流程串起来了。从等待起始条件开始,到地址匹配,再到数据收发,最后到停止条件结束。

从机模式核心流程图 等待起始条件 接收地址 + R/W位 地址匹配? 忽略,继续监听 产生ADDR中断 判断R/W位 R/W=0:从机接收 (RXNE中断) R/W=1:从机发送 (TXE中断) 检测停止条件 通信结束

4.5 常见问题与避坑指南

从机模式调试起来比主机麻烦,因为从机是被动的,你没法主动触发事件。我总结几个常见问题:

技巧:调试从机时,我习惯用逻辑分析仪同时抓SCL、SDA和从机的中断输出引脚。这样能清楚看到:主机发地址后,从机有没有产生中断。如果地址对了但没中断,八成是寄存器配置问题。

  • 地址不匹配:检查寄存器里的地址值对不对,左移位数对不对,地址模式(7位/10位)有没有配错
  • 中断没响应:检查中断使能位有没有开,NVIC里中断优先级有没有配,中断函数名对不对
  • 数据错位:从机接收时第一个字节经常丢?检查ADDR中断里有没有读SR2清标志
  • 总线卡死:从机没响应导致SCL一直被拉低?检查从机是不是在忙别的任务,中断响应够不够快

我曾经踩过的坑:有一次从机发送数据,主机那边收到的全是0xFF。查了半天,发现是从机在TXE中断里写DR寄存器时,写的是局部变量,函数退出后变量就没了。后来改成用全局缓冲区才解决。嗯,这种低级错误,犯过一次就记住了。

从机模式配置说难不难,说简单也不简单。核心就三点:地址配对、中断处理好、数据收发流程走对。把这三点吃透了,从机驱动就稳了。