news 2026/10/28 20:55:03

IIC通信协议详解:软件模拟时序、六个函数与上板调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
IIC通信协议详解:软件模拟时序、六个函数与上板调试

简介:《IIC通信协议详解》面向嵌入式初学者、单片机开发者及需要复习总线时序的工程师,聚焦两线式串行总线的原理理解与代码落地。文档围绕SDA、SCL两条信号线展开,说明半双工通信中主机与从机的数据收发关系,区分软件IIC与硬件IIC在实现方式、执行效率、管脚占用和接口灵活性上的差异,并依次讲解空闲状态、开始信号、停止信号、应答信号及数据发送的时序约束,对总线空闲时由上拉电阻拉高电平的电路状态也作出说明。文中配有启动、停止、应答与字节发送等C语言示例,便于对照波形理解SCL高电平期间SDA保持稳定、低电平期间允许变化以及ACK/NACK判定,也可作为调试传感器、EEPROM等外设通信问题的参考。压缩包内为1个docx文件,大小约81KB,轻量易读,已有1074人学习。适合作为IIC入门笔记或项目开发时快速查阅时序要点的资料。

1. 一块新焊的传感器读回全是 0xFF,问题往往不在传感器

新打样的板子回来,传感器焊上去,程序烧进去,串口一打印,ID 全是0xFF,换一片芯片还是0xFF。这种时候多数人第一反应是芯片坏了,实际上十有八九是 IIC 通信协议的时序没对齐:起始位少拉了一个电平,或者应答位没等到就往下走了。这份《IIC 通信协议详解》文档解决的正是这一类问题,它把两线式串行总线的时序规则和一套可直接落地的软件模拟代码放在一起讲,从空闲状态、起始停止信号,一路写到发送字节、等待应答、读取字节六个函数。适合手上只有普通 I/O 口、又必须把传感器或 EEPROM 驱动起来的嵌入式开发者,也适合刚接触 IIC、被“SCL 高电平期间 SDA 不许变”这句话卡住的新手。下面按文档本身的脉络拆一遍,顺带把上板调试里那些容易翻车的地方补上。

2. 两线制总线拆解:SDA、SCL 的分工与软件/硬件 IIC 的选型

2.1 两条线怎么把 8 位数据串起来

IIC 的物理层极其克制:只有数据线 SDA 和时钟线 SCL 两根,加上各自的上拉电阻。所有器件都挂在同一对线上,靠地址区分谁在说话,所以它天生是半双工——同一时刻只能有一个方向的数据流动,主机发的时候从机只能听,主机收的时候从机才能发。这一点很多人一开始不适应,总觉得“双向传送”就该像串口那样同时收发,实际上 IIC 的“双向”指的是两个方向都能传,不是同时传。

时钟线 SCL 的归属决定了谁掌握节奏。整个通信过程里 SCL 一律由主机产生,从机没有权利主动拉时钟(时钟拉伸是另一回事,属于从机请求主机等待),所以从机哪怕再慢,也只能靠拉低 SCL 让主机等着,而不能自己起一个时钟周期。数据线 SDA 则是分时复用:主机写的时候它归主机驱动,应答位和读数据的时候它归从机驱动,切换驱动方的那个瞬间最容易出毛刺,后面讲 SDA_IN/SDA_OUT 的时候会具体说。

速率上,标准模式 100kbps,快速模式 400kbps,文档里写的“高速 IIC 总线一般可达 400kbs 以上”指的就是快速模式这一档。真跑到 400k 的时候,一个位的宽度只有 2.5 微秒,软件模拟的延时函数精度就开始吃紧了,所以常见做法是软件模拟先老老实实跑 100k,等逻辑跑通了再往上压。

2.2 软件 IIC 和硬件 IIC,到底该选哪个

文档里对这两者的区分说得很清楚:软件 IIC 是用单片机的两个普通 I/O 口模拟出 IIC 波形,靠软件控制管脚的高低电平;硬件 IIC 是芯片内部有专门的 I2C 外设和驱动电路,引脚是固定的复用管脚,配置方式是直接写内部寄存器。这两条路的差别不只是“效率高低”,落到项目里是几个很现实的取舍。

维度软件 IIC硬件 IIC
引脚选择任意两个 I/O,可随时改固定复用脚,受封装限制
CPU 占用全程占用,逐位翻转低,可配合中断/DMA
速率上限受延时函数精度限制,通常 ≤400k可跑满外设规格
移植成本改几个宏定义即可需按芯片外设库重写
调试难度波形全在代码里,可单步看异常时状态寄存器晦涩

我一般的判断标准是:板子上 I2C 外设脚刚好空着、要跑 400k 以上、或者一帧数据量大到影响主循环节奏,就用硬件 IIC;引脚已经被占满、只需要偶尔读一个传感器寄存器、或者要在多组总线上挂不同器件,就用软件 IIC。这份文档给的是软件 IIC 那一套,恰好是移植成本最低、最容易在不同芯片之间搬来搬去的写法,也是新手理解时序最直观的入口。

提示:同一块板子上不要一半器件走硬件 IIC 一半走软件 IIC 挂到同一对总线,两边时序参数不一致时会出现“单独都能通、一起就失败”的玄学现象。

2.3 文档的结构与建议阅读顺序

这份文档的组织方式是按“先协议、后代码”来的:前半部分讲概述、空闲状态、起始停止信号、应答信号,后半部分直接给对应函数。真正上手时不必从头顺着读,我建议的顺序是——先看空闲状态和“SCL 高电平期间 SDA 不许变化”这条铁律,把电气规则建立起来;再看起始和停止两个函数,因为这两个是所有通信的边界;然后看发送字节和等待应答这一对,它们是配对使用的;最后看读字节,因为读操作的采样点比写更绕,放在有写操作基础之后理解会顺很多。

文档里六个函数是有依赖关系的:IIC_Start和IIC_Stop是骨架,IIC_Send_Byte依赖SDA_OUT宏,IIC_Wait_Ack依赖SDA_IN宏和READ_SDA宏,IIC_Read_Byte依赖IIC_Ack/IIC_NAck。这意味着移植的时候,宏定义层没搞定,六个函数一个都跑不起来,下一章会先把这一层拆开。

3. 时序地基:空闲态、起始位与停止位的代码拆解

3.1 空闲态和两条不能破的时序铁律

空闲状态是没通信时的初始状态,规定为 SDA 和 SCL 同时为高。这时候所有器件的输出级场效应管都截止,等于把总线释放掉,由两根线上各自的上拉电阻把电平拉回高。理解了“释放”这个词很关键——IIC 总线上任何器件想输出高电平,实际动作是把线放开让上拉拉上去,而不是主动推高,这就是它与线(wired-AND)特性的来源。也正因为如此,两个器件同时驱动总线不会烧片子,但一个拉高一个拉低时,总线呈现低电平。

文档里那两条规则是所有时序判断的依据,值得反复念两遍:发送和接收数据时,SCL 为高电平期间 SDA 不允许变化;SCL 为低电平期间 SDA 可以任意变化。第二条是任意时候都成立——IIC 电路只在 SCL 高电平时对 SDA 采样,SCL 低电平时 SDA 上是什么都不记录。

把这两条翻译成代码习惯就是:改 SDA 之前先确认 SCL 是低的,改完 SDA 再去拉高 SCL。文档后面每个函数基本都是这个节奏的排列组合,看懂了这一句,六个函数的顺序就不用背了。

3.2 IIC_Start:为什么顺序是“先拉高 SDA 再拉高 SCL”

// 产生 IIC 起始信号 // 1.设置 SDA 为输出 // 2.先拉高 SDA,再拉高 SCL,进入空闲状态 // 3.在 SCL 高电平期间拉低 SDA,形成下降沿 // 4.拉低 SCL,钳住总线,准备发送或接收 void IIC_Start(void) { SDA_OUT(); // 把 SDA 切成输出方向 IIC_SDA = 1; // 先释放 SDA,让它被上拉拉高 IIC_SCL = 1; // 再拉高 SCL,此时总线处于空闲态 delay_us(4); // 保持一个建立时间,等电平稳定 IIC_SDA = 0; // SCL 为高时 SDA 由高变低 —— 起始信号 delay_us(4); // 保持时间,让从机能采到这个跳变 IIC_SCL = 0; // 拉低时钟,钳住总线 }

这段代码里最容易写反的是前两行的顺序。如果先拉高 SCL 再拉高 SDA,从机会把“SCL 高电平期间 SDA 上升”识别成停止信号,等于一上电就发了个结束帧,后面的数据自然没人理。所以必须先让 SDA 回到高、总线进入空闲,再动 SCL,最后制造那一个高电平期间的下降沿。

delay_us(4)这两个延时不是凑数的。第一个延时保证 SDA 拉高之后总线电平真正稳定下来(上拉电阻不是理想器件,RC 充电需要时间,阻值大的时候更明显),第二个延时保证从机有足够时间捕获 SDA 的下降沿。这两个参数和上拉电阻阻值、总线电容直接相关,4 微秒对应的是 4.7k 上拉、短线缆的典型场景;如果板子上拉用的是 10k,或者排线拉得很长,就需要往上加。

最后IIC_SCL = 0是文档注释里的“钳住总线”,意思是把时钟线攥在主机手里,从机暂时不会动作,等你准备好第一位数据再放开。这个动作在每次发送前都要做一次,它不算协议规定的时序,纯粹是软件模拟时为了方便控制节奏加的习惯。

3.3 IIC_Stop:释放总线的正确姿势

// 产生 IIC 停止信号 // 1.先拉低 SCL,确保可以在 SCL 低电平期间改 SDA // 2.拉低 SDA // 3.拉高 SCL // 4.在 SCL 高电平期间拉高 SDA,形成上升沿,通信结束 void IIC_Stop(void) { SDA_OUT(); // SDA 保持输出 IIC_SCL = 0; // 先拉低时钟,腾出改 SDA 的窗口 IIC_SDA = 0; // 把 SDA 拉到低,为上升沿做准备 delay_us(4); IIC_SCL = 1; // 拉高时钟 IIC_SDA = 1; // SCL 为高时 SDA 由低变高 —— 停止信号 delay_us(4); }

停止信号的判定条件是 SCL 高电平期间 SDA 出现上升沿,所以函数里必须先在 SCL 低电平期间把 SDA 拉到 0,再拉高 SCL,最后放开 SDA 让上拉把它拉成高——这最后一步在很多实现里写成IIC_SDA = 1,也就是把输出寄存器置 1,配合开漏输出等效于释放总线,逻辑上没问题,但要注意如果引脚配置的是推挽输出,这一步就是硬推高,挂在同一总线上的其他器件如果正在拉低,就会出现短暂的电流对灌。

注意:软件 IIC 的 SDA/SCL 必须配置成开漏输出(或“输出时开漏、输入时上拉输入”),配成推挽输出在总线竞争时会出问题,这是新手最常忽略的一条硬件前提。

3.4 延时参数怎么定,不要照抄 4 微秒

文档里所有延时都是delay_us(2)或delay_us(4),注释里还特意提了一句“对某个收音机芯片这三个延时都是必须的”。这句话的潜台词是:这些数值是针对特定从机调出来的,不是普适常数。移植到自己的板子上,延时应该按目标速率反推。

以 100kbps 为例,一个位的周期是 10 微秒,高电平和低电平各占一半是 5 微秒,那么 SCL 拉高之后至少要维持 4 微秒以上才能让从机采稳;跑到 400kbps 时位周期只剩 2.5 微秒,delay_us(4)直接就把速率压死了,反而变成“名义 400k、实际 100k”。所以更稳妥的做法是把延时抽成宏:

// 按目标速率配置半周期延时,单位微秒 // 100kHz -> 5us(半周期),400kHz -> 1.25us,取整后按实测微调 #define IIC_DELAY() delay_us(5) void IIC_Start(void) { SDA_OUT(); IIC_SDA = 1; IIC_SCL = 1; IIC_DELAY(); IIC_SDA = 0; IIC_DELAY(); IIC_SCL = 0; }

这样换芯片、换速率只改一处。实际调试时如果是用示波器或逻辑分析仪看波形,可以直接量 SCL 高电平的宽度,反过来验证延时函数准不准——delay_us本身的精度依赖于系统主频和循环开销,主频一改,实际延时可能差出 30% 以上,这点在低主频配置下尤其明显。

4. 字节级收发链路:应答、发送、等待应答与读取

4.1 第 9 个时钟:ACK 和 NACK 到底谁发

协议规定每传送 8 个数据位,后面必须跟 1 个应答位,所以一帧是 9 个时钟。这第 9 个时钟的归属经常被搞混:时钟仍然由主机发,但数据线由接收方驱动。主机发完一个字节,要在这第 9 个时钟期间把自己对 SDA 的驱动撤掉(释放成高),让从机能把它拉低。

电平含义很简单:第 9 个时钟高电平期间 SDA 为低 = ACK,表示“我收到了”;为高 = NACK,表示“没收到”或者“我不要了”。主机读从机数据时反过来,主机在最后一个字节之后回一个 NACK,意思是“够了,别发了”,然后发停止信号释放总线。

// 产生 ACK 应答:SCL 低电平期间把 SDA 拉低,再拉高 SCL 让从机采样 void IIC_Ack(void) { IIC_SCL = 0; // 确保处于低电平窗口 SDA_OUT(); // SDA 切回输出 IIC_SDA = 0; // 拉低,表示有效应答 delay_us(2); IIC_SCL = 1; // 拉高时钟,第 9 个脉冲 delay_us(2); IIC_SCL = 0; // 拉低,结束应答位 } // 不产生应答:SDA 保持高电平,SCL 完成一次低-高-低 void IIC_NAck(void) { IIC_SCL = 0; SDA_OUT(); IIC_SDA = 1; // 保持高,表示非应答 delay_us(2); IIC_SCL = 1; delay_us(2); IIC_SCL = 0; }

文档注释里那句“SCL 在 SDA 一直为低电平期间完成低高电平转换”是这段代码的精华。它点明了一个因果顺序:先把 SDA 定死,再让 SCL 走完一个脉冲,而不是反过来。如果把顺序写反,在 SCL 已经拉高之后才去改 SDA,就违反了“SCL 高电平期间 SDA 不许变化”的铁律,从机可能把它误判成起始或停止信号。

4.2 IIC_Send_Byte:从最高位开始的移位逻辑

// IIC 发送一个字节,返回从机有无应答(此处先只看发送部分) void IIC_Send_Byte(u8 txd) { u8 t; SDA_OUT(); // SDA 为输出 IIC_SCL = 0; // 拉低时钟,开始数据传输 for (t = 0; t < 8; t++) { if ((txd & 0x80) >> 7) // 取当前最高位 IIC_SDA = 1; else IIC_SDA = 0; txd <<= 1; // 左移,把下一位顶到最高位 delay_us(2); // 数据建立时间 IIC_SCL = 1; // 拉高时钟,从机在此采样 delay_us(2); // 高电平保持 IIC_SCL = 0; // 拉低,允许下一位变化 delay_us(2); } }

IIC 是高位先行(MSB first),所以每次取txd的 bit7,取完左移一位。这一条和串口(低位先行)正好相反,写代码时如果把这个方向搞错了,读回来的寄存器值会是“位序倒过来”的样子,看起来像数据错乱,实际上每一位都对,只是顺序反了。我在第一次调传感器的时候就在这上面浪费了半天,因为器件 ID 恰好是个看起来“像但不完全像”的值。

代码里的注释提到了另一种写法IIC_SDA = (txd & 0x80) >> 7;,效果等价,只是把 if 换成了赋值表达式。两种都能用,用 if 版本在单步调试时更容易在分支上设断点,所以我一般保留 if 形式。

延时分布也值得看一眼:改 SDA 之后等 2 微秒再拉高 SCL,这是数据建立时间;SCL 高电平维持 2 微秒,这是让从机采样;拉低 SCL 之后又等 2 微秒,这是给下一位数据留出变化窗口。三段延时加起来正好一个位周期 6 微秒左右,对应大约 160kbps 的实际速率,比标称 100k 稍快,属于“能用但不保证余量”的范围。

提示:IIC_Send_Byte只负责发 8 位,不发第 9 位的应答。发完必须紧跟一次IIC_Wait_Ack(),这两个函数是配对调用的,漏掉应答位的话从机会认为这一帧没结束,后续数据全部错位。

4.3 IIC_Wait_Ack:用超时把死等变成可恢复

// 等待应答信号到来 // 返回值:1 表示接收应答失败,0 表示成功 u8 IIC_Wait_Ack(void) { u8 ucErrTime = 0; SDA_IN(); // SDA 切成输入,把总线交给从机 IIC_SDA = 1; delay_us(1); // 释放 SDA,让上拉拉高 IIC_SCL = 1; delay_us(1); // 第 9 个时钟,让从机驱动应答位 while (READ_SDA) // 读到高电平说明从机没拉低 { ucErrTime++; if (ucErrTime > 250) // 超时保护,避免死循环 { IIC_Stop(); // 主动发停止信号复位总线 return 1; } } IIC_SCL = 0; // 时钟拉低,结束应答位 return 0; }

这个函数的思路是文档里那句话的直译:“先让 SDA=1,再判断在一定时间内 SDA 是否变为 0”。之所以要先把 SDA 置 1,是因为在开漏配置下,写 1 等于释放总线,从机才有机会把它拉低;如果这里留着 0,总线上永远是低电平,while (READ_SDA)一次都不会进,函数直接返回成功,看起来“通了”,实际上根本没检查。

超时计数 250 是一个经验值,按循环体单次执行的时间估算出来的,主频变了就得跟着调。它的价值在于把“从机没接、地址写错、从机掉电”这些情况从死循环变成可返回的错误码——没有这个超时,程序会卡在这一行,看门狗都救不回来。检测到超时之后立刻IIC_Stop()也很有必要,它把总线恢复成空闲态,否则下一次通信会从一个半截的状态开始,越错越乱。

SDA_IN()和SDA_OUT()这两个宏是方向切换的关键,也是最容易踩坑的地方,第 5 章会把这里单独拎出来讲。

4.4 IIC_Read_Byte:采样点在 SCL 高电平期间

// 读 1 个字节,ack=1 时发送 ACK,ack=0 时发送 NACK u8 IIC_Read_Byte(unsigned char ack) { unsigned char i, receive = 0; SDA_IN(); // SDA 设为输入,准备接收 for (i = 0; i < 8; i++) { IIC_SCL = 0; // 低电平期间允许从机改数据 delay_us(2); IIC_SCL = 1; // 拉高,此时 SDA 上是从机的第 i 位 receive <<= 1; // 先左移,把高位让出来 if (READ_SDA) receive++; // 读到 1 就补上 delay_us(1); } if (!ack) IIC_NAck(); // 最后一个字节,回 NACK else IIC_Ack(); // 还要继续读,回 ACK return receive; }

读的写法和写正好互为镜像:写是在 SCL 低电平期间改 SDA、高电平期间让从机采样;读是在 SCL 低电平期间让从机改 SDA、高电平期间主机采样。所以采样语句if (READ_SDA) receive++;必须夹在IIC_SCL = 1;之后,放到前面读到的就是上一位的值。

receive <<= 1放在判断之前,是把已经收到的位往左挪,给新的一位腾出最低位。循环走完 8 次,最先收到的最高位被移了 7 次,正好落在 bit7 上,位序与发送端一致。

读完之后必须回一个应答:继续读就发 ACK,读完最后一个字节发 NACK。这个 NACK 是告诉从机“别再往 SDA 上写数据了,我要发停止信号了”。如果漏掉,从机会继续占用数据线,随后主机拉 SDA 产生停止信号时就会和从机打架,表现出来就是“读一个字节正常,连续读多个就挂”。

5. 上板排查:软件 IIC 调试里最容易翻车的五处

5.1 通信完全没反应,起始位就被吃掉

现象:示波器上 SCL 有波形,SDA 全程一条直线,从机完全不响应,IIC_Wait_Ack每次都超时返回 1。

原因:最常见的是SDA_OUT()/SDA_IN()宏没实现或者实现错了,导致 SDA 引脚一直停在输入模式,IIC_SDA = 0写下去根本没输出。其次是引脚复用没配对,或者上拉电阻漏焊——软件 IIC 依赖外部上拉,很多开发板把上拉做在传感器模块上,单独飞线测芯片时就会缺上拉。

解决:先不接从机,只跑IIC_Start,用万用表测 SDA 在函数执行后是不是真的被拉到了 0。拉不下去就查引脚方向宏和上拉电阻。确认硬件没问题再看时序,起始位那个高电平期间下降沿如果上升沿太缓(上拉太大、线太长),也需要换小阻值上拉。

5.2 方向切换瞬间出现毛刺,从机误判起始位

现象:偶尔能通、偶尔失败,同一份代码复位几次结果都不一样,逻辑分析仪上能看到 SDA 在方向切换的瞬间有个窄脉冲。

原因:多数 MCU 的 GPIO 在“输出寄存器值”和“引脚方向”之间是两套寄存器,从输出切输入的过程中,如果输出寄存器的值还是 0,切过去的瞬间引脚会短暂输出一个低电平,形成毛刺。文档里IIC_Wait_Ack先SDA_IN()再IIC_SDA = 1,如果硬件上真的是先切方向再改寄存器,中间那一小段时间就是隐患。

解决:把顺序改成“先在输出模式下把输出寄存器写成 1,再切换到输入模式”,也就是IIC_SDA = 1; SDA_IN();。这样切换发生时引脚是被上拉着的,不会往下掉。这个改动看起来只有一行,但它是那种“不改也能跑、改了才稳”的血泪经验,尤其是总线上挂了多个器件、电容比较大的时候。

5.3 连续读多个字节,第二个开始全错

现象:读单个寄存器正常,一读连续地址(比如一次性读 6 个字节的加速度数据)就只有第一个对,后面全是 0 或 0xFF。

原因:读循环里最后一个字节之后的应答处理错了。要么是每个字节都回 ACK、包括最后一个,从机以为主机还要读,继续占着数据线;要么是IIC_Read_Byte里循环结束时没有把 SCL 拉低,导致下一个字节的起始条件不干净。

解决:确认最后一个字节传ack = 0,前面几个传ack = 1。同时在读循环结束后、发停止信号之前,检查 SCL 是否已经回到低电平。如果用的是带子地址的器件,还要确认写完寄存器地址后有没有重新发起始信号( repeated start ),有些器件不支持 repeated start,必须 Stop 之后再 Start。

5.4 主频一改就全崩,延时函数名不副实

现象:在开发板上跑得好好的驱动,换到另一块主频更低的板子上,全部超时。

原因:delay_us()通常是用循环计数实现的,计数次数按主频算好写死,主频一变,实际延时就会成倍偏移。主频变低时延时变长、速率变慢,一般还能通;主频变高时延时变短,可能短到从机来不及采样,直接失败。

解决:把延时实现改成基于定时器或者SysTick的精确延时,或者在delay_us里做编译期校验。更省事的做法是把 IIC 的延时单独抽成一个宏,不跟系统延时函数共用,这样即使系统延时被别处改坏,IIC 也不受影响。

5.5 多器件挂在同一总线上,单独都通、一起就挂

现象:每个传感器单独接都能读到 ID,两个一起接上,就只有一个能通,或者两个都时通时不通。

原因:一是地址冲突,两颗同型号器件地址引脚没拉开,真的撞地址了;二是总线电容超了,线拉得太长、上拉又偏大,上升沿变缓,高速下采样出错;三是某个器件在总线忙的时候乱拉 SDA,把整个总线拖死。

解决:先用扫描的方式把每个器件的地址打出来确认不冲突。总线电容的问题靠减小上拉阻值(4.7k 换 2.2k)和缩短走线解决。如果怀疑某个器件拖总线,可以逐个摘掉验证。另外,上电初始化之前先发 9 个时钟脉冲把可能卡在半截状态的从机“敲”出来,这个技巧在多器件系统里很好用。

6. 二次开发:把这套位操作改造成能挂多组总线的驱动层

文档给的六个函数是全局函数,引脚靠宏定义写死,这在只有一个 IIC 总线的板子上够用,但只要项目里出现第二组 IIC(比如一组接传感器、一组接 EEPROM,甚至同一颗 MCU 上软件模拟和硬件外设混用),这套写法就得复制一遍改宏,维护起来很难受。我的做法是把引脚操作抽成函数指针,做成一个总线对象。

// 把引脚操作抽象成回调,一套时序代码可服务多组总线 typedef struct { void (*sda_out)(void); void (*sda_in)(void); void (*sda_set)(unsigned char level); unsigned char (*sda_get)(void); void (*scl_set)(unsigned char level); void (*delay)(void); } iic_bus_t; // 时序函数只依赖 bus,不再依赖具体引脚 void iic_start(iic_bus_t *bus) { bus->sda_out(); bus->sda_set(1); bus->scl_set(1); bus->delay(); bus->sda_set(0); // SCL 高电平期间 SDA 下降沿 bus->delay(); bus->scl_set(0); }

改造的关键在于把“什么时候拉哪根线”和“怎么拉这根线”彻底分开:前者是协议,写一次就够;后者是硬件相关,每个平台实现一遍。这样加一组总线只需要再填一个iic_bus_t实例,时序代码一行都不用改。往上再包一层,就是设备层的读写函数:

// 设备层:地址 + 寄存器读写,业务代码只调这一层 unsigned char iic_read_reg(iic_bus_t *bus, unsigned char dev, unsigned char reg) { unsigned char val; iic_start(bus); iic_send_byte(bus, dev << 1); // 写方向 if (iic_wait_ack(bus)) return 0xFF; // 从机不应答,直接退出 iic_send_byte(bus, reg); // 寄存器地址 iic_wait_ack(bus); iic_start(bus); // repeated start iic_send_byte(bus, (dev << 1) | 1); // 读方向 iic_wait_ack(bus); val = iic_read_byte(bus, 0); // 最后一个字节回 NACK iic_stop(bus); return val; }

验证改造是否成功,不要靠“读到的数据看着像对的”,而是分三步:先用逻辑分析仪抓一次完整波形,对照时序图逐段核对起始、地址、应答、数据、停止;再故意把从机拔掉,确认所有函数都能在超时后返回错误而不是卡死;最后把同一份设备层代码分别绑到软件总线和硬件总线上跑同一个传感器,两次读到的数据完全一致,才算真的抽象干净了。

改造的时候还有一个容易忽略的细节:iic_wait_ack里超时后的iic_stop必须是总线级别的复位动作,不能只拉一下线就返回。我见过有实现里超时直接 return,结果总线停在 SCL 低电平、SDA 被从机拉着,下一次通信从中间开始,错误会一路传下去,越查越偏。

从那以后我每次把这套位操作搬到新平台,都强制先跑一遍“空总线 + 超时返回”的测试,再接真实器件——总线能干净地失败,比总线碰巧能通更让人安心。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 20:54:43

2022小额借贷系统源码解析:沙盒验证与合规部署指南

简介&#xff1a;本资源是一套2022年更新的小额借贷贷款系统完整源码包&#xff0c;面向PHP后端开发者、金融科技学习者及Web全栈初学者&#xff0c;聚焦信贷业务系统开发实践与快速部署能力提升。资源包含2525个文件&#xff0c;主体为455个PHP核心逻辑文件、211个JS交互脚本、…

作者头像 李华
网站建设 2026/10/28 20:54:15

企业网络拓扑配置实战:从VLAN划分到NAT转换的完整指南

简介&#xff1a;这份资源面向正在学习企业网络设计与仿真的网络专业学生及运维初学者&#xff0c;聚焦接入层交换机的完整配置流程&#xff0c;帮助读者理解从设备命名、安全加固到VLAN划分与端口绑定的落地方法。压缩包共1个文件&#xff0c;为docx文档&#xff0c;大小约1.4…

作者头像 李华
网站建设 2026/10/28 20:53:18

Mask R-CNN在肝脏肿瘤精准分割中的实践与优化

1. 肝脏肿瘤精准分割的技术挑战与解决方案肝脏肿瘤的医学影像分割一直是计算机辅助诊断领域的核心难题。传统方法依赖放射科医生手动勾画肿瘤边界&#xff0c;不仅耗时耗力&#xff08;单例CT扫描平均需要30-45分钟标注&#xff09;&#xff0c;而且存在约15-20%的观察者间差异…

作者头像 李华
网站建设 2026/10/28 20:51:57

两个ECC:从P-384证书密码学到SAP LSMW数据迁移

"ECC"这三个字母&#xff0c;在IT圈里能拆出好几种完全不同的含义&#xff1a;做安全的人看到它&#xff0c;第一反应是椭圆曲线密码学&#xff08;Elliptic Curve Cryptography&#xff09;&#xff0c;脑海里浮现的是P-256、P-384这些曲线参数和证书链&#xff1b;…

作者头像 李华
网站建设 2026/10/28 20:51:44

无损音乐曲库整理与FLAC转MP3批量转码实践指南

1. 先把“无损”这件事说清楚&#xff0c;不然转码全是白费功夫做无损音乐资源整理这件事&#xff0c;我前后折腾了七八年&#xff0c;最开始那两年基本都在交学费。很多人一上来就问“哪个网站能下无损”&#xff0c;其实方向就偏了——你真正该先弄明白的是手里那份文件到底是…

作者头像 李华
网站建设 2026/10/28 20:51:20

Agent长期记忆六大方案实战选型指南

1. 为什么“长期记忆”是Agent落地的生死线&#xff0c;而不是锦上添花的功能你有没有试过和一个AI助手聊了十分钟&#xff0c;它还记得你三分钟前说的“我正在准备下周的客户提案”&#xff0c;但到了第五分钟&#xff0c;它突然问&#xff1a;“您刚才提到的客户是哪一家&…

作者头像 李华