1. I2C协议基础回顾:从机视角理解SCL、SDA时序与地址匹配机制

各位同学好,我是老周。做嵌入式这行十几年了,I2C这个协议几乎天天打交道。说实话,很多工程师写从机代码时,遇到总线锁死、地址无应答、数据错位这些问题,根源往往不是代码逻辑复杂,而是对最基础的时序和地址匹配机制理解不够深。

今天这一章,咱们就从从机的视角,把I2C的底裤扒开看看。你想想看,主机发数据你读数据,这谁都会。但作为从机,你怎么知道主机在叫你?什么时候该应答?什么时候该释放总线?这些才是调试时真正要命的地方。

核心观点:从机开发的本质,就是精确响应SCL时钟边沿上的每一个事件。地址匹配是“敲门砖”,时序是“通行证”。

1.1 SCL与SDA:两条线的“握手”规则

I2C总线就两条线:SCL(时钟线)和SDA(数据线)。但这两条线的配合,藏着不少门道。

空闲状态:SCL和SDA都是高电平。这是总线空闲的标志。我见过不少新手,在初始化时忘了把这两条线拉高,结果一上电总线就处于“忙”状态,主机根本没法发起通信。

起始条件(START):SCL为高电平时,SDA从高电平切换到低电平。这个下降沿告诉所有从机:注意,主机要开始说话了。

停止条件(STOP):SCL为高电平时,SDA从低电平切换到高电平。这个上升沿表示:本次传输结束,总线释放。

数据传输:SCL为低电平时,SDA允许变化;SCL为高电平时,SDA必须稳定。从机就是在SCL的每个高电平期间采样SDA上的数据位。

我的经验:调试时,我习惯用逻辑分析仪同时抓SCL和SDA。如果看到起始条件后SDA在SCL高电平期间跳变,那基本可以断定是时序违规——要么是主机代码有bug,要么是从机响应太慢导致主机超时重试。

1.2 地址匹配机制:从机如何知道“主机在叫我”?

主机发送起始条件后,紧接着发送7位(或10位)从机地址,最后跟一个读写位(R/W)。地址匹配是从机参与通信的“入场券”。

7位地址模式:最常见。主机发送的地址字节高7位是地址,最低位是R/W。例如,地址0x50(1010000)加上读位(1)就是0xA1,加上写位(0)就是0xA0。

地址匹配过程:

  • 从机在每个SCL时钟的高电平期间,采样SDA上的地址位。
  • 收到完整的地址字节后,从机将收到的地址与自己的预设地址比较。
  • 如果匹配,从机在下一个SCL时钟周期拉低SDA,发送应答信号(ACK)。
  • 如果不匹配,从机保持SDA为高电平,发送非应答信号(NACK),然后继续监听总线。

避坑指南:我曾经在一个项目中,从机地址设置正确,但主机就是收不到ACK。查了两天,最后发现是从机在地址匹配后,应答位的SCL时钟还没到来之前,误把SDA拉高了。原因是从机的中断响应延迟太长,导致应答时序错位。记住:应答必须在SCL的第9个时钟周期内完成。

1.3 从机视角的完整通信流程

咱们用一张图把整个流程串起来。下面这张SVG图展示了从机从空闲到地址匹配,再到数据收发的完整状态机。

I2C从机通信状态机 I2C从机通信状态机 空闲(IDLE) 检测到START 接收地址位 匹配? 发送ACK 发送NACK 数据收发 返回监听 关键时序点 • START后9个SCL 时钟接收地址 • 第9个SCL发ACK • 数据字节同样9时钟

1.4 从机应答时序详解

应答机制是I2C协议中最容易被忽视,但又最容易出问题的地方。咱们用表格把几种情况说清楚。

场景 从机行为 SDA状态(第9时钟) 主机响应
地址匹配 拉低SDA 低电平(ACK) 继续发送/接收数据
地址不匹配 释放SDA 高电平(NACK) 发送STOP或重复起始
数据接收完成 拉低SDA 低电平(ACK) 继续发送下一字节
从机忙(无法接收) 释放SDA 高电平(NACK) 发送STOP或重试
从机接收最后一字节 释放SDA 高电平(NACK) 发送STOP

我的习惯:在从机代码中,我会用一个状态变量记录当前是“地址匹配阶段”还是“数据收发阶段”。这两个阶段的应答逻辑其实是一样的,但调试时分开看更容易定位问题。另外,我建议在应答位前后各加一个GPIO翻转,用示波器观察,这样能直观看到从机响应是否及时。

1.5 从机地址的硬件实现与软件配置

地址匹配可以纯硬件实现,也可以软件辅助。不同MCU的做法不一样,但原理相通。

硬件地址匹配:很多MCU的I2C外设自带地址比较器。你只需要配置好从机地址寄存器,硬件会自动完成地址匹配和应答。优点是快,不占CPU。缺点是地址固定,灵活性差。

软件地址匹配:有些低端MCU或模拟I2C实现,需要CPU在中断里逐位比较地址。优点是灵活,可以动态改变地址。缺点是占用CPU,且响应时间要严格控制。

下面是一个典型的从机地址配置代码示例(以STM32为例):

// I2C从机地址配置(7位地址模式)
I2C_HandleTypeDef hi2c1;

void I2C1_Slave_Init(void)
{
    hi2c1.Instance = I2C1;
    hi2c1.Init.Timing = 0x00303D5B;  // 100kHz时序配置
    hi2c1.Init.OwnAddress1 = 0x50;   // 从机地址:0x50
    hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
    hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
    hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
    hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
    
    if (HAL_I2C_Init(&hi2c1) != HAL_OK)
    {
        // 初始化失败处理
        Error_Handler();
    }
}

避坑指南:我曾经在一个项目中,从机地址配置为0x50,但主机发0xA0(写操作)时从机就是不应答。查了半天,发现是地址左移了一位。很多MCU的地址寄存器要求写入的是7位地址左移1位后的值(即0xA0),而不是原始地址0x50。一定要仔细看数据手册!

1.6 时钟拉伸(Clock Stretching)与从机时序约束

时钟拉伸是从机的一个“特权”。当从机需要更多时间处理数据时,可以拉低SCL,迫使主机等待。这个机制在低速从机或需要处理大量数据的场景下非常有用。

工作原理:

  • 从机在SCL低电平期间拉低SCL线。
  • 主机检测到SCL被拉低,会等待SCL释放。
  • 从机处理完成后,释放SCL(拉高),主机继续产生时钟。

但要注意,不是所有主机都支持时钟拉伸。有些主机的I2C控制器有超时机制,如果SCL被拉低超过一定时间,主机会认为总线出错,直接复位或产生错误中断。

我的建议:如果你的从机需要时钟拉伸,务必在数据手册中确认主机的时钟拉伸容忍时间。我一般会把拉伸时间控制在1ms以内,超过这个值,很多主机就会超时。另外,时钟拉伸只应在SCL低电平时发起,在高电平时拉低SCL会被识别为起始条件,造成总线混乱。

1.7 本章小结

好了,这一章的内容就这些。咱们从从机的视角,把I2C最基础的SCL/SDA时序、地址匹配机制、应答流程和时钟拉伸都过了一遍。这些东西看着简单,但调试时90%的问题都出在这里。

记住几个关键点:

  • 地址匹配是通信的起点,匹配失败一切免谈。
  • 应答时序必须精确,第9个时钟是死线。
  • 时钟拉伸要谨慎使用,不是所有主机都买账。
  • 逻辑分析仪是你最好的朋友,抓波形比看代码管用。