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图展示了从机从空闲到地址匹配,再到数据收发的完整状态机。
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个时钟是死线。
- 时钟拉伸要谨慎使用,不是所有主机都买账。
- 逻辑分析仪是你最好的朋友,抓波形比看代码管用。