简介:这份文档面向单片机与嵌入式开发初学者及需要复习总线时序的工程师,系统梳理IIC通信协议的核心知识,帮助读者解决时序概念模糊、软件模拟与硬件外设混淆、应答判断不清等常见问题。文档先区分软件IIC与硬件IIC的实现差异与效率取舍,再依次讲清空闲状态、开始与停止信号、应答与非应答位,并给出开始、停止、应答、发送字节等关键函数代码,便于对照波形理解边沿触发与采样规则。资源为1个docx文件,压缩包约81KB,体量轻便,适合打印或随工程存档查阅。目前已有1074人学习下载,说明其在入门与面试复习场景中具有一定认可度。读者可从中获得半双工通信的完整时序框架、逐条注释的参考代码以及排错所需的电平变化判据,为驱动传感器、EEPROM等IIC器件打下基础。
1. 从一份 .docx 说起:两根线上的 IIC 通信协议到底难在哪
一份只有几十页的 .docx,把 IIC 通信协议从两根线的电平规则一直写到能直接编译的五个函数——起始、停止、应答、发送字节、接收字节。这类资料的价值不在“讲得全”,而在“抄得动”:它把 400kbps 高速总线背后的时序约束,拆成了 GPIO 拉高拉低加几个delay_us,让你手边没有逻辑分析仪的晚上也能先把波形跑出来。我做嵌入式这些年,第一次在 IIC 上出问题不是因为看不懂协议,而是 SCL 拉高期间手抖改了 SDA,从机直接不认账。这份文档的取舍就很对路:先定死“什么时候能改数据线”,再谈软件模拟和硬件外设的区别。它适合刚上手的单片机开发者、要移植传感器驱动的工程师,以及想知道片上 I2C 外设和 GPIO 模拟到底差在哪、值不值得切换的人。
2. 两条线撑起一个总线:SDA/SCL 的采样窗口与起停时序
2.1 空闲状态与上拉电阻:总线为什么默认是高
IIC 只用两根线:时钟线 SCL 负责节拍,数据线 SDA 一位一位搬数据。空闲状态的定义很干脆——SDA 和 SCL 同时为高,此时总线上所有器件的输出级都处于截止状态,也就是“释放总线”,高电平由上拉电阻拉出来。这句话里藏着 IIC 能挂几十个器件的根本原因:每个设备的 SDA/SCL 引脚内部都是开漏(或开集)结构,只能主动拉低,不能主动拉高。谁想发 0 就把线拽下来,谁想发 1 就松手让上拉电阻把线拉回去。多个设备同时拉低不会烧片子,这构成了“线与”逻辑,也是后面应答信号、时钟拉伸、多主机仲裁全都能成立的前提。
上拉电阻不是随手填的。总线电容规定不超过 400pF,上升时间近似按tr ≈ 0.847 × R × C估算;100kHz 标准模式下用 10kΩ 往往还凑合,跑到 400kHz 快速模式就得降到 2.2k~4.7kΩ,否则上升沿被拉成一条斜坡,从机在 SCL 高电平窗口里采到的可能还是上一拍的残留电平。我一般默认贴 4.7kΩ,板子跑 400kHz 时用示波器看上升沿,超过 300ns 就往下换。板子上已经有多个 IIC 器件时别每个器件都并一组上拉,等效阻值会越并越小,灌电流上去以后从机可能直接拉不低。
提示:调试早期不确定上拉是否合适,可以先把速率降到 100kHz 以下,很多“从机不响应”其实是上升沿太慢,而不是地址写错了。
2.2 起始与停止信号:跳变沿而不是电平
文档里把这两句话放在最前面,是整篇最有分量的两条:起始信号是 SCL 为高期间 SDA 由高到低的跳变;停止信号是 SCL 为高期间 SDA 由低到高的跳变。注意用词是“跳变”,不是“电平”。总线空闲时 SDA 和 SCL 都是高,此时 SDA 掉下来,从机看到的是 SCL 高电平窗口内的一个下降沿,于是知道“主机要开始了”;通信结束时 SCL 先被拉高,SDA 再抬起来,从机看到高电平窗口内的上升沿,于是释放总线。
为什么要这样设计?因为数据位在 SCL 高电平期间必须保持稳定,那么 SCL 高电平期间 SDA 的任何跳变就天然成了“非数据”的控制信号。起始和停止就是仅有的两个例外,其余所有 SDA 变化都必须发生在 SCL 低电平期间。这条规则是整个 IIC 的骨架,软件模拟的一多半 bug 都出在这里。
还有一个容易被忽略的变体:重复起始(Repeated Start,常写成 Sr)。主机发完一个字节、想立刻切换到读方向时,可以不发停止信号,直接在 SCL 高电平期间再打一个 SDA 下降沿,等于“不释放总线重新开一局”。很多传感器的寄存器读时序就是这么写的:先写寄存器地址,再 Sr,再写从机地址+读位。少了这个 Sr,中间插一个 Stop 再 Start,某些器件会认为事务结束并复位内部指针,读回来的永远是第一个寄存器的值。
常见做法是给这几个时序留足余量,标准模式下的参数大致是:起始建立时间tSU;STA≥ 4.7us,起始保持tHD;STA≥ 4.0us,停止建立tSU;STO≥ 4.0us,停止到下一次起始的总线空闲tBUF≥ 4.7us。快速模式(400kHz)这些值缩到 0.6us 量级。文档里的delay_us(4)就是按标准模式下限给的,你如果只跑 100kHz,这个值不算保守,反而是贴着边走的。
2.3 把波形翻成代码:IIC_Start / IIC_Stop 的逐行拆解
文档给的两个函数几乎是软件 IIC 的标准骨架,唯一需要替换的是宏定义——原文用的是抽象宏,移植到具体芯片时换成寄存器或库函数即可。下面这套是 STM32F103 的写法,其它平台照着改引脚号就行。
/* 引脚与方向宏:移植时只改这一段 */ #define IIC_SDA_PIN 7 /* PB7 */ #define IIC_SCL_PIN 6 /* PB6 */ #define SDA_OUT() { GPIOB->CRL &= ~(0xF << 28); GPIOB->CRL |= (0x3 << 28); } /* 推挽输出 50MHz */ #define SDA_IN() { GPIOB->CRL &= ~(0xF << 28); GPIOB->CRL |= (0x8 << 28); } /* 上拉输入 */ #define READ_SDA (GPIOB->IDR & (1 << IIC_SDA_PIN)) #define IIC_SDA_H (GPIOB->BSRR = (1 << IIC_SDA_PIN)) #define IIC_SDA_L (GPIOB->BRR = (1 << IIC_SDA_PIN)) #define IIC_SCL_H (GPIOB->BSRR = (1 << IIC_SCL_PIN)) #define IIC_SCL_L (GPIOB->BRR = (1 << IIC_SCL_PIN))这段宏里BSRR置位、BRR复位,比读-改-写ODR少一次读操作,中断里也安全。SDA_IN()那行把 CRL 的 4 位配置成1000,也就是上拉输入,这一步在IIC_Wait_Ack里是必须的,后面会讲。
/* 起始信号:SCL 高电平期间,SDA 由高到低 */ void IIC_Start(void) { SDA_OUT(); /* 主机要驱动 SDA,先切输出 */ IIC_SDA_H; /* 先让 SDA、SCL 都回高,构成空闲 */ IIC_SCL_H; delay_us(4); /* 满足 tSU;STA,标准模式 4.7us 下限 */ IIC_SDA_L; /* 关键跳变:SCL 还是高,SDA 掉下来 */ delay_us(4); IIC_SCL_L; /* 钳住总线,准备送第一位 */ }逐行看:第一行SDA_OUT()不能省,因为上一次读操作可能把 SDA 切成了输入;中间两行把总线拉回空闲态,给从机一个明确的“新事务开始”前的静默;IIC_SDA_L是那个决定性的下降沿;最后的IIC_SCL_L是给第一位数据准备低电平窗口,因为数据只能在 SCL 低电平期间改。
/* 停止信号:SCL 高电平期间,SDA 由低到高 */ void IIC_Stop(void) { SDA_OUT(); IIC_SCL_L; /* 先确保 SCL 低,才能动 SDA */ IIC_SDA_L; delay_us(4); IIC_SCL_H; /* 抬高 SCL */ IIC_SDA_H; /* 关键跳变:SCL 高期间 SDA 抬起来 */ delay_us(4); }停止函数里IIC_SCL_L放在最前面是有讲究的:如果上一个动作停在IIC_Ack()的末尾,SCL 已经是被拉低的;但如果是异常退出、或者中途return了,SCL 状态不确定,先拉低再动 SDA 才符合规则。很多“总线卡死”的现场,就是停止信号的顺序写反了,SDA 在 SCL 高电平期间被乱动,被从机当成了一个畸形起始信号。
2.4 采样窗口:SCL 高电平期间 SDA 必须稳定
文档里有一句话总结得比很多教材都直接:只有当 SCL 为高电平时,IIC 电路才对 SDA 上的电平进行记录;当 SCL 为低电平时,无论 SDA 是高还是低,都不采样。这意味着“写数据”这个动作的实质是——在 SCL 低电平期间把 SDA 摆到想要的电平,然后拉高 SCL,让从机在这一拍的高电平窗口里把它读走,再拉低 SCL 进入下一位。
建立时间和保持时间在这个窗口里各占一半:数据必须在 SCL 上升沿之前稳定tSU;DAT(标准模式 250ns),并且在 SCL 高电平期间保持不变tHD;DAT(标准模式 0ns,快速模式 900ns 的下降沿保持另算)。软件模拟时这两个值基本由delay_us兜住,但如果代码里在拉高 SCL 之后还有别的操作(比如顺手更新一个全局变量、调个printf),SDA 就可能被挤到高电平窗口里去变化,从机采到的位就错了。
我个人的习惯是把“摆数据、拉高、拉低”这三步写成固定节奏,任何额外逻辑都放到循环外面,别夹在中间。原因很实际:IIC 出错时现象往往不是“完全不通”,而是“偶尔读回一个 0xFF 或者某个固定错值”,这种玄学问题追起来最费时间。
3. 软件 IIC 与硬件 IIC 的选型:GPIO 模拟和片上外设的边界
3.1 软件 IIC 的代价:延时就是时钟
文档对软件 IIC 的定义很朴素:用单片机的两个 I/O 端口模拟出 IIC,用软件控制管脚状态去模拟通信波形。这句话的潜台词是——时钟频率不是一个配置项,而是你代码执行速度的函数。IIC_Send_Byte里每个delay_us(2)加上几条 GPIO 操作,共同决定了 SCL 的实际周期;你改一行打印、开一个中断,波形就变了。
这既是缺点也是优点。缺点是 CPU 全程忙等,一个字节 9 个时钟,每个时钟里有 3 次delay_us,跑 100kHz 时 CPU 基本被占满;时序精度受中断影响,长中断里 SCL 被拉低的时间会超标,从机可能判定超时。优点是不挑引脚,任何两个 GPIO 都能当 IIC 用,这对引脚排布紧张的板子(比如主控只剩两个边角料引脚)几乎是唯一解;二是每一位都可控,调试时可以在任意一位后面插日志、插断点,硬件外设做不到这种粒度。
3.2 硬件 IIC 外设:寄存器配置与总线仲裁
硬件 IIC 对应的是芯片内部的 I2C 外设,有独立的驱动电路,引脚是专用复用脚,配置方式是直接写外设寄存器(或者调库)。文档里那句“硬件 IIC 的效率远高于软件”是对的,但只说了一半。效率高的来源是:起始/停止/应答这些时序由硬件状态机产生,字节收发可以挂中断或 DMA,CPU 只在事务完成时被叫一次。跑 400kHz 时软件模拟很难稳定维持,硬件外设却能轻松拉满。
另一半是硬件外设自带的能力,软件模拟基本做不了:多主机仲裁(两个主机同时发,硬件逐位比较,输的一方自动退出且不破坏数据)、时钟拉伸支持(从机忙不过来时把 SCL 拉低,硬件会等)、错误状态码(AF 应答失败、BERR 总线错误、ARLO 仲裁丢失),以及总线忙状态检测。这些在单主机单从机的简单场景里感知不到,一旦系统里有多个主设备或者从机会间歇性拉低时钟,软件模拟就容易出问题。
但硬件 IIC 也有明确的边界。第一,引脚固定,画板子时没留复用脚就用不了;第二,某些早期型号的 I2C 外设存在已知的状态机缺陷,会出现 BUSY 标志卡住、或者从机不响应时无法自动恢复的情况,社区里流传的“硬件 IIC 不好用”多半来自这里;第三,出问题时寄存器状态不直观,得靠调试器一位一位看,比软件模拟的“看代码就知道第几位错了”难定位得多。
3.3 一张表看清选型边界
| 维度 | 软件 IIC(GPIO 模拟) | 硬件 IIC(片上外设) |
|---|---|---|
| 引脚要求 | 任意两个 GPIO | 固定复用引脚 |
| 实际速率 | 靠延时凑,100kHz 稳,400kHz 需实测 | 100k / 400k / 1M 可配 |
| CPU 占用 | 全程忙等,一个字节毫秒级 | 中断/DMA,事务级开销 |
| 时序精度 | 受中断和函数调用影响 | 硬件保证 |
| 多主机仲裁 | 基本不实现 | 硬件支持 |
| 时钟拉伸 | 需自行读 SCL 电平配合 | 硬件等待 |
| 出错定位 | 逐位可控,可打点 | 看状态寄存器与错误码 |
| 移植成本 | 改几个宏即可跨平台 | 换芯片基本要重写驱动 |
这张表不是让你二选一,而是让你知道什么时候该换。我的经验是:单从机、速率 ≤100kHz、引脚紧张——软件模拟足够,代码透明好维护;多从机、要跑 400kHz、系统里有中断密集的任务——上硬件外设,别硬扛。
3.4 引脚与方向切换的移植清单
从文档里那套抽象宏迁到自己板子上,有四件事必须逐个确认,少一件就会得到“完全没波形”或者“只有第一位对”的结果。
第一步,确认 SDA 的方向切换是否真的生效。SDA_OUT()和SDA_IN()改的是 GPIO 的模式寄存器,但很多 HAL 库的GPIO_Init会把整个端口重配一遍,如果你在SDA_IN()里调了一次初始化,可能把 SCL 的配置也带走了。安全的写法是直接操作模式寄存器位,只改 SDA 对应那 4 位。
第二步,确认输出模式选的是开漏还是推挽。IIC 规范要求开漏,但软件模拟在单主机场景下用推挽也能跑通——代价是总线不具备“线与”能力,将来并上第二个主机或者从机做时钟拉伸时会直接短路。我一般统一配成开漏输出,外部上拉,行为最接近标准。
第三步,确认delay_us的实参是真的微秒。基于SysTick的忙等延时在 72MHz 主频下还算准,但如果你的delay_us是靠for循环空转实现的,编译器一开-O2就可能被优化掉,实际延时变成几十纳秒,SCL 跑到几兆赫兹,从机自然不应答。上电第一件事是用示波器量 SCL 周期,反推实际延时,这个动作比读十页手册都管用。
第四步,确认上电初始状态。很多 IIC 从机在上电后需要一段时间内部复位,这期间拉低 SDA 会被误认为起始信号。稳妥的做法是初始化 GPIO 后先发 9 个 SCL 脉冲(不带起始),把可能卡在中间状态的从机“推”出来,再发停止信号复位总线。
4. 字节收发与 ACK/NACK:五个函数怎么拼成一次完整事务
4.1 IIC_Send_Byte:从最高位开始,边沿对齐
文档里的发送函数用的是“取最高位、左移一位、循环八次”的写法,这是 IIC 的规定:数据高位先发(MSB First)。代码里那个(txd & 0x80) >> 7就是取当前字节的最高位,判断为 1 就把 SDA 拉高,为 0 就拉低,然后txd <<= 1把下一位顶上来。
/* 发送一个字节,返回 0 表示从机应答,1 表示无应答 */ u8 IIC_Send_Byte(u8 txd) { u8 t, ack; SDA_OUT(); /* 发送期间 SDA 必须是输出 */ IIC_SCL_L; /* 从低电平开始,才有权改 SDA */ for (t = 0; t < 8; t++) { if (txd & 0x80) IIC_SDA_H; /* 先摆数据 */ else IIC_SDA_L; txd <<= 1; delay_us(2); /* tSU;DAT,数据在 SCL 上升沿前稳定 */ IIC_SCL_H; /* 拉高,从机在这一拍采样 */ delay_us(2); /* 高电平保持时间 */ IIC_SCL_L; /* 拉低,允许改下一位 */ delay_us(2); } return IIC_Wait_Ack(); /* 第 9 个时钟等应答 */ }三个delay_us(2)的位置不是随便放的。第一个在拉高 SCL 之前,保证数据建立时间;第二个在高电平中间,保证从机采得到;第三个在拉低之后,给从机留出反应时间再进下一位。原文注释里提到“对 TEA5767 这三个延时都是必须的”,说的就是某些从机对时序余量比较敏感,省掉任何一个都会出现“发得出、收不到”的情况。
txd & 0x80和原文的(txd & 0x80) >> 7效果一样,前者少一次移位,读起来也更直接。如果你的编译器对u8左移有符号扩展方面的担心,可以先转成unsigned char再移位,避免第 8 次移位后填进来的位影响判断——虽然这里只判断最高位,影响不到结果,但养成习惯不吃亏。
4.2 IIC_Wait_Ack:SDA 切输入,超时要有出口
应答是 IIC 里唯一一个“方向反转”的时刻。主机发完 8 位,要在第 9 个时钟期间把 SDA 释放出来交给从机,由从机决定拉低(ACK,表示收到)还是松手(NACK,表示没收到或者不想再收)。所以IIC_Wait_Ack的第一件事必须是SDA_IN(),把 SDA 从输出切成输入;第二件事是把 SDA 置高(输入模式下写 1 通常是开启上拉),给从机一个干净的起点。
/* 等待应答:0 = 收到 ACK,1 = 超时无应答 */ u8 IIC_Wait_Ack(void) { u8 ucErrTime = 0; SDA_IN(); /* 释放 SDA,交给从机 */ IIC_SDA_H; /* 输入模式下置高,等效于释放 */ delay_us(1); IIC_SCL_H; /* 第 9 个时钟的高电平窗口 */ delay_us(1); while (READ_SDA) { /* 从机拉低 = ACK,一直高 = 等待 */ ucErrTime++; if (ucErrTime > 250) { IIC_Stop(); /* 超时先发停止,把总线拉回空闲 */ return 1; } } IIC_SCL_L(); /* 收到应答,拉低时钟结束这一拍 */ return 0; }这段代码有两个细节值得盯住。第一,超时阈值 250 是循环次数,不是微秒,它对应的实际时间取决于主频和READ_SDA的展开方式,在不同平台上差好几倍;调参时不要照抄,用示波器量一次从机响应延迟,再按比例改。第二,超时分支里必须调IIC_Stop(),否则 SCL 会停在高电平、SDA 悬空,总线从此处于一个谁都不认的状态,后续所有通信全挂。这是文档里做得对的一处,很多网上流传的版本省掉了这一句,导致一次失败拖垮整个系统。
注意:
READ_SDA读的是输入数据寄存器,前提是SDA_IN()真的把引脚切成了输入。如果宏写错、引脚仍是输出模式,读回来的永远是你自己刚写进去的 1,while会一直转到超时,现象就是“每次都返回无应答”。
4.3 IIC_Read_Byte:先读八位,再决定发 ACK 还是 NACK
接收是发送的镜像:主机把 SDA 切成输入,每个时钟高电平期间读一位,左移拼成字节;读完 8 位后,主机要作为“接收方”回一个应答,告诉从机还要不要继续发。这里的方向又反转了一次——发应答时 SDA 得切回输出。
/* 读一个字节;ack=1 读完发 ACK 继续读,ack=0 发 NACK 表示结束 */ u8 IIC_Read_Byte(u8 ack) { u8 i, receive = 0; SDA_IN(); /* 整个读过程 SDA 是输入 */ for (i = 0; i < 8; i++) { IIC_SCL_L; /* 低电平期,从机准备下一位 */ delay_us(2); IIC_SCL_H; /* 高电平期,主机采样 */ receive <<= 1; if (READ_SDA) receive++; /* 先移位再填最低位 */ delay_us(1); } if (ack) IIC_Ack(); /* 还要继续读,回 ACK */ else IIC_NAck(); /* 最后一个字节,回 NACK */ return receive; }顺序上有个常见错误:receive <<= 1和if (READ_SDA) receive++这两句如果写反,拼出来的字节位序就整个颠倒,读回的值看着像随机数。因为 IIC 是高位先到,第一次读到的位最终要落在 bit7,所以必须“先左移腾位置,再填最低位”,循环 8 次之后最先读到的那位正好被移到最高位。
ack参数怎么传是另一个容易糊涂的点。读单个寄存器时,通常只读 1 个字节,读完直接IIC_Read_Byte(0)发 NACK;连续读多个字节时,前 n-1 个传 1 发 ACK,最后一个传 0 发 NACK。发完 NACK 之后主机才能发停止信号,这是规范要求的顺序——从机看到 NACK 才知道“数据到此为止”,然后释放 SDA,主机才有机会打停止沿。
4.4 拼一次完整事务:向传感器写寄存器地址再读数据
五个函数单独看都不难,真正容易错的是把它们串成一次事务。以最常见的“读某传感器某个寄存器”为例,标准流程是:起始 → 写从机地址+写位 → 等 ACK → 写寄存器地址 → 等 ACK → 重复起始 → 写从机地址+读位 → 等 ACK → 读一个字节 → 发 NACK → 停止。
/* 读从机 dev 的 reg 寄存器,返回读到的字节;0xFF 表示失败 */ u8 IIC_Read_Reg(u8 dev, u8 reg) { IIC_Start(); IIC_Send_Byte(dev & 0xFE); /* 7 位地址左移一位,最低位 0 = 写 */ if (IIC_Wait_Ack()) { IIC_Stop(); return 0xFF; } IIC_Send_Byte(reg); /* 告诉她我要读哪个寄存器 */ if (IIC_Wait_Ack()) { IIC_Stop(); return 0xFF; } IIC_Start(); /* 重复起始,不发停止 */ IIC_Send_Byte(dev | 0x01); /* 同一地址,最低位 1 = 读 */ if (IIC_Wait_Ack()) { IIC_Stop(); return 0xFF; } u8 val = IIC_Read_Byte(0); /* 只读一个字节,回 NACK */ IIC_Stop(); return val; }这个函数里有三个点值得记住。第一,7 位地址要左移一位再拼读写位,dev & 0xFE是写,dev | 0x01是读;有些手册直接给 8 位地址(已经把读写位算进去),这时候就不能再移位,否则地址整体偏一位,从机永远不应答。第二,中间的重复起始不能换成停止再起始,前面说过,某些器件的寄存器指针会被停止信号复位。第三,每个IIC_Wait_Ack()失败都要走IIC_Stop()再返回,让总线回到空闲,不然一次失败会把后面的初始化流程全部带偏。
连续读多个字节时把IIC_Read_Byte的参数改成循环里的(i < len - 1),最后一个传 0,其余传 1,其余结构不变。这类代码我一般会再包一层带重试的封装——失败后IIC_Stop()、延时几毫秒、重试两次,能挡掉大部分上电瞬间的偶发失败。
5. 避坑与排查:示波器上看不出来的六种失败
5.1 从机始终不回 ACK,SDA 一直是高
现象:起始信号发得出来,地址字节也发出去了,但IIC_Wait_Ack()每次都超时返回 1,示波器上看 SDA 在第 9 个时钟期间纹丝不动。
原因通常有三个层次。最常见的是地址错了——7 位地址没左移、或者手册给的是 8 位地址又移了一次,整体偏一位;其次是上拉电阻太大或总线电容太大,上升沿爬得太慢,从机在第 9 个时钟高电平窗口里还没把 SDA 拉到判决门限以下;第三是从机根本没上电或者处于复位状态,压根没在听。
解决顺序建议从便宜的开始:先用万用表确认从机供电和地;再把速率降到 10kHz 试一次,慢速能通说明是上拉或延时问题,换 2.2k~4.7k 上拉、加大delay_us;最后核对地址,拿手册里的 7 位地址自己左移,别信例程里抄来的常量。
5.2 第一次通信正常,第二次开始总线死锁
现象:上电后第一次读写成功,第二次调用时IIC_Start()发不出去,或者IIC_Wait_Ack()永远超时,必须断电重启才能恢复。
原因多半是上一次事务异常退出时没发停止信号,SCL 停在高电平、SDA 被从机拉着低,总线处于“忙”状态,从机在等后续时钟。软件模拟没有总线状态检测,代码照样往下跑,于是越错越远。
解决办法是加一个总线复位函数:把 SCL 切成输出,手动打 9 个时钟脉冲,让从机把没发完的那一位吐完,然后发一个停止信号。这个函数在初始化时调一次、在每次超时后调一次,能救回绝大多数假死现场。
/* 总线复位:9 个时钟 + 停止,把卡住的从机推出来 */ void IIC_Bus_Recover(void) { u8 i; SDA_OUT(); IIC_SDA_H; for (i = 0; i < 9; i++) { /* 最多 9 个脉冲足够清完一个字节 */ IIC_SCL_L; delay_us(5); IIC_SCL_H; delay_us(5); } IIC_Stop(); /* 补一个停止信号,恢复空闲态 */ }5.3 上电后第一次读全是 0xFF,复位一次就正常
现象:冷启动时第一次读传感器返回 0xFF,按一下复位键或者断开重连就正常了。
原因是从机内部复位比主控慢。主控初始化完 GPIO 立刻开始发起始信号,从机这时候还没准备好,把起始沿当成了噪声,后面整个字节就错位了。还有一种可能是 SDA 在初始化时被配置成输出且默认高,从机检测到这个电平后进入了某种保护状态。
解决:初始化后先延时 10~50ms 再通信,具体值查从机手册的“上电到可通信”时间;初始化 GPIO 时先把 SCL 和 SDA 都设成输入并等待稳定,再切输出;初始化流程末尾调一次上面的IIC_Bus_Recover(),成本几乎为零。
5.4 加了打印或者开了中断,通信开始随机失败
现象:单独跑通信测试一切正常,一旦把代码合进主程序、开了定时器中断或者加了串口打印,读写就开始偶发失败,失败率跟中断频率正相关。
原因是软件 IIC 的时序完全靠delay_us忙等,中断一来 CPU 就去干别的,SCL 低电平时间被拉长几百微秒,某些从机会判定总线超时并复位内部状态;如果中断服务程序里也操作了同一个 GPIO 端口,还可能把 SDA 的方向改掉。
解决有两个方向:要么在通信的临界区里关中断(一次字节传输关中断的时间在百微秒量级,大部分系统能接受),要么把延时改小、让整个字节传输时间短到中断来不及插进来。前者更可靠,后者只是碰运气。另外,别在 IIC 函数里调printf,串口发送是阻塞的,等于自己给自己加了一个超长中断。
5.5 硬件 IIC 卡在 BUSY 标志,软件复位也清不掉
现象:切到硬件外设后,程序卡在“等待 BUSY 位清零”的循环里,调I2C_DeInit再I2C_Init也没用,只有断电才行。
原因是 BUSY 位反映的是引脚上的实际电平状态,不是寄存器里的一个可写标志。如果从机把 SCL 拉低做着时钟拉伸、而主控已经复位了外设,硬件看到的仍是“总线忙”,任何软件复位都清不掉。这种情况在从机掉电、或者上一次传输中途主机复位时最容易出现。
解决:把 I2C 外设的引脚临时切回普通 GPIO,用软件方式打 9 个时钟加停止信号(就是 5.2 里那个函数),确认 SCL 和 SDA 都回到高电平之后,再切回复用功能重新初始化外设。顺序反了没有用,先初始化外设再清总线,BUSY 位照样卡着。
6. 从能跑到能扛:把延时、上拉和时钟拉伸调到量级正确
代码能跑通只是及格线。真正决定这套软件 IIC 能不能上产品的,是三个物理量:SCL 的实际频率、上升沿的陡峭程度、以及从机会不会拉低时钟。前两个靠示波器和电阻,第三个靠代码里多读一次电平。
先校准频率。文档里每个位周期有 3 个delay_us(2),加上 GPIO 操作和循环开销,名义上是 6us,实际跑出来往往在 8~12us 之间,对应 80~120kHz,勉强落在标准模式。想跑到 400kHz,得把延压缩到delay_us(1)甚至更小,这时候delay_us本身的精度就成了瓶颈——很多实现里delay_us(1)实际执行 2~3us。我的做法是先写一个只翻转 SCL 的死循环,用示波器量出周期,再按比例反推每个delay_us的实参,把这张表记在工程注释里。
| 目标 SCL | 位周期 | 建议单步延时 | 上拉电阻 | 备注 |
|---|---|---|---|---|
| 10 kHz | 100 us | delay_us(30) | 10 kΩ | 调试期用,容错最高 |
| 100 kHz | 10 us | delay_us(2~3) | 4.7 kΩ | 标准模式,绝大多数传感器够用 |
| 400 kHz | 2.5 us | delay_us(1)并实测 | 2.2 kΩ | 快速模式,需确认从机支持 |
| 1 MHz | 1 us | 裸 GPIO 翻转,不用延时 | 1 kΩ 以内 | 快速+模式,软件模拟很难稳定 |
再处理时钟拉伸。规范允许从机在忙不过来时把 SCL 拉低,主机必须等它松手才能继续。软件模拟的默认写法是“拉高 SCL 之后不管三七二十一延时再拉低”,从机拉低时主机会误以为时钟已经到位,实际上双方在抢线,读回来的数据自然错位。常见做法是在每次拉高 SCL 之后加一个等待循环:
/* 拉高 SCL 后等它真的变高,兼容从机的时钟拉伸 */ static void IIC_SCL_WaitHigh(void) { u16 t = 0; IIC_SCL_H; while (!(GPIOB->IDR & (1 << IIC_SCL_PIN))) { /* 读到低说明从机还拉着 */ if (++t > 1000) { IIC_Stop(); return; } /* 超时兜底,别死等 */ } }这个函数在 100kHz、单从机的场景下几乎不会被触发,但一旦系统里挂上温湿度传感器、EEPROM 这类会做拉伸的器件,它就是救命的那一行。超时阈值同样要靠实测确定,别照抄。
最后是上拉和走线。IIC 两根线如果走长了(超过 20cm)、或者和高速信号并排,串扰会把上升沿搅乱。我一般要求 SDA/SCL 走同一层、尽量短、旁边包地,长度实在压不下来就把速率降到 100kHz,不要硬撑 400kHz。板子上有多个 IIC 器件时,上拉只在靠近主控的一端放一组,别在每个器件旁边都放。
血泪经验是:IIC 的问题九成出在“电平”而不是“代码逻辑”,剩下那一成里又有大半是超时分支没写全。从那以后我每次新画一块带 IIC 的板子,都会先焊上 4.7k 上拉、烧一个只发起始和停止的最小程序,用示波器确认两个沿干净了,再去调驱动。希望帮到你。
本文还有配套的精品资源,点击获取