IIC通信协议详解:软件IIC时序、C函数与避坑排查

发布时间:2026/10/28 20:55:05

IIC通信协议详解:软件IIC时序、C函数与避坑排查 简介《IIC通信协议详解》是一份面向嵌入式入门者与单片机开发学习者的技术文档围绕两线式串行总线的原理理解与代码实现展开可帮助读者厘清SDA、SCL时序关系并理清软件IIC与硬件IIC的差异、适用场景及通信流程。压缩包内含1个docx文件大小约81KB内容相对集中便于按主题查阅与随用随看。文档覆盖空闲状态、开始信号、停止信号、应答信号和字节发送等关键环节配有起始、停止、ACK/NACK及发送单字节的程序片段并强调SCL高电平期间SDA不得变化、低电平才可改变等约束同时提及半双工通信特点与软硬件实现取舍。已有1074人学习适合用作IIC协议的基础梳理材料也可为传感器、EEPROM等外设调试提供时序判断与代码参考。1. 从一次读不到数据的调试说起IIC 到底该怎么学焊好一块小板子上电读传感器 ID 寄存器串口回来永远是0xFF。示波器一挂SCL 那条线像被谁按住了一样怎么都拉不到高电平——上拉电阻漏贴了。这种亏写过 IIC 驱动的人基本都吃过一次。IIC 只有两根线SDA 传数据、SCL 打节拍看上去比 SPI、UART 都好懂真要自己从零写出稳定能跑的时序坑一点都不少起始信号差一个微秒、应答位方向没切回来、延时常量算错现象全是读不到。这份《IIC 通信协议详解》走的是另一条路它不满足于讲协议把软件 IIC 从空闲状态、起始、停止、应答一直到字节收发逐段落到可以直接抄进工程的 C 函数里。适合两类人——刚学单片机、想搞明白两根线凭什么能通信的新手以及手上有现成硬件 IIC 外设、但碰上时序异常需要退回软件模拟来定位问题的老手。2. 协议分层与软硬件选型先用软件 IIC 把时序握在手里拿到一份讲协议的资料最怕它一口气把电气特性、仲裁、时钟同步全倒出来看完还是不知道怎么下手。这份文档的组织顺序比较务实先给两条线的电平规则再区分软件 IIC 与硬件 IIC最后才一段一段写代码。这个顺序其实就是实际开发的顺序——先把什么时候能改电平这条铁律刻进脑子后面所有函数都是它的推论。2.1 两条线上的电平规则所有时序的总根子文档里反复强调的两句话是整个 IIC 的地基当 SCL 为高电平时SDA 不允许变化当 SCL 为低电平时SDA 可以任意变化0 或 1。理解了这两句起始信号、停止信号、数据位、应答位就全都成了一句话的推论。起始信号是SCL 高电平期间 SDA 由高变低这不是违规吗是违规但协议规定这个违规组合专门用来表示开始停止信号同理是SCL 高电平期间 SDA 由低变高。数据位则老老实实遵守规则SCL 低电平时把数据摆上 SDASCL 拉高后接收方采样高电平期间数据必须稳住不动。文档还补了一条容易被忽略的只有在 SCL 为高电平时IIC 电路才记录 SDA 上的电平SCL 为低时无论 SDA 是什么都不采样。这句解释了为什么拉低 SCL 再改 SDA是安全的也解释了为什么很多新手代码在 SCL 高电平期间改数据从机却能偶尔读到正确值——不是对了是碰巧采样时刻躲开了跳变。总线状态SCLSDA说明空闲11两线均被上拉电阻拉高器件输出级截止起始1→1→0SCL 保持高SDA 下降沿数据位高电平采样稳定高电平期间 SDA 不可跳变应答 ACK第 9 个时钟高0接收方拉低 SDA非应答 NACK第 9 个时钟高1接收方释放 SDA停止1→0→1SCL 保持高SDA 上升沿这张表建议直接贴在工位边上。后面写任何一行驱动代码都能在表里找到它对应的位置。2.2 软件 IIC 与硬件 IIC 的取舍管脚灵活性和吞吐量的对赌文档把两者讲得很清楚软件 IIC 是用单片机两个普通 I/O 口模拟出来的靠软件翻转管脚状态去凑出 IIC 波形硬件 IIC 对应芯片内部的 I2C 外设有专门的驱动电路和专用管脚配置的是内部寄存器。实际选型时我一般这么判断维度软件 IIC硬件 IIC管脚占用任意 GPIO可随时改固定复用管脚速率受延时精度和中断影响一般 100~200kbps 稳可到 400kbps 甚至更高CPU 开销全程占用每个位都要进函数配置完可由中断/DMA 搬运多设备冲突处理自己写重试和总线恢复部分芯片硬件层处理部分要靠 errata 绕可移植性换芯片只改几个宏换芯片基本要重写初始化调试可见性每一步都能打断点、点灯黑匣子出错只能看寄存器状态一句话总结我自己的用法板子还在调、传感器还没认全的时候用软件 IIC等协议跑通、数据量上来了再切硬件 IIC。文档里也点明了硬件效率远高于软件而软件不受管脚限制、接口灵活——这句话反过来读就是软件 IIC 的价值在灵活和可控不在快。还有一个词值得单独拎出来IIC 是半双工通信。同一时刻数据只能朝一个方向走主机发完一个字节必须等从机应答从机才能继续。这和 UART 的全双工不一样写代码时不能想当然地边发边收很多读寄存器的时序错误根子就在没接受半双工这个前提。2.3 从文档到代码的映射五个状态与七个函数文档把一次完整通信拆成开始、结束、发送、响应、接收五个部分。落到 C 代码上我习惯分成三层管脚层SDA_OUT()/SDA_IN()切方向IIC_SDA/IIC_SCL读写电平时序层IIC_Start/IIC_Stop/IIC_Ack/IIC_NAck字节层IIC_Send_Byte/IIC_Wait_Ack/IIC_Read_Byte。再往上就是设备层比如写寄存器地址 读 N 个字节这种组合时序文档没有展开但那属于具体器件手册的事协议层不该管。这七个函数加起来不到两百行却覆盖了 90% 的常见外设EEPROM、温湿度传感器、OLED 屏、加速度计、RTC 芯片用的都是这一套。把它们的时序抠准了剩下的就是查器件手册填寄存器地址。3. 空闲、起始与停止把时序图翻译成 GPIO 动作真正开始写代码第一件事不是写起始信号是确认总线现在处于什么状态。文档专门用一段讲空闲状态SDA 和 SCL 同时为高电平各器件的输出级场效应管截止总线被释放由两条线上各自的上拉电阻把电平拉高。这句话里藏着两个信息——一是空闲不是没人管而是被电阻管着二是如果上拉电阻缺失或阻值太大总线根本回不到空闲态后面所有时序都会错位。3.1 上拉电阻为什么总线上不能没有它IIC 的器件输出级是开漏结构只能主动把线拉低拉高全靠外部上拉电阻。所以你会看到很多模块上已经焊了 4.7kΩ 或 10kΩ 的电阻也见过有人把它拆了接更小的阻值。阻值怎么选本质是上升沿时间和功耗的折中阻值大上升沿变缓高速率下采样点可能还没到高电平阻值小边沿陡峭但器件灌电流变大。总线电容越大挂的设备越多、走线越长越要往小里选。这段文档没有给具体阻值但它的表述很准确空闲状态由两条信号线各自的上拉电阻把电平拉高。我调试时看总线是否真的空闲用的就是最土的办法——上电不初始化先用万用表量 SDA、SCL 对 VCC 的电压接近电源电压才算合格。低一半、低更多先查上拉和短路别急着怀疑代码。3.2 起始信号SCL 高电平期间 SDA 的下降沿起始信号的定义是当 SCL 为高期间SDA 由高到低的跳变文档特意加了一句它是一种电平跳变时序信号而不是一个电平。这句话是防坑的关键因为很多初学者会写成把 SDA 拉低 起始漏了 SCL 必须为高的前提。文档给的实现我按注释的顺序读一遍// 产生 IIC 起始信号 // 1. 设置 SDA 为输出方向 // 2. 先拉高 SDA再拉高 SCL回到空闲状态 // 3. 在 SCL 为高期间拉低 SDA形成下降沿 // 4. 拉低 SCL钳住总线准备发送或接收数据 void IIC_Start(void) { SDA_OUT(); // SDA 线切为输出方向 IIC_SDA 1; // 先给数据线高电平 IIC_SCL 1; // 时钟线拉高总线进入空闲态 delay_us(4); // 保持空闲电平等待从机识别 IIC_SDA 0; // START: SCL 为高时SDA 由高变低 delay_us(4); // 保持该跳变满足从机的建立时间 IIC_SCL 0; // 钳住 I2C 总线准备发送或接收数据 }逻辑说明这四步的顺序不能乱。先SDA_OUT()是因为上电后 GPIO 默认可能是输入方向没切对写IIC_SDA 1只是写了个寄存器管脚未必有输出先拉高 SDA 再拉高 SCL是为了避免在 SCL 已经为高的时候把 SDA 从低拉高——那会被从机误判成停止信号。最后把 SCL 拉低是为了离开SCL 高电平期间 SDA 不许变的约束区接下来的数据位才有地方改。参数说明delay_us(4)是这套代码里最敏感的常量。4μs 对应约 125kbps 的理论上限实际因为函数调用开销会慢一些。如果你的主频很低或者用了不带优化的编译选项4μs 可能远远不够表现为起始信号发出去了但从机没反应。3.3 停止信号为什么必须先拉低 SCL停止信号是当 SCL 为高期间SDA 由低到高的跳变同样是一个跳变而不是一个电平。文档的实现是这样的// 产生 IIC 停止信号 // 1. 设置 SDA 为输出方向 // 2. 先拉低 SCL再拉低 SDA // 3. 拉高 SCL此时 SDA 仍为低 // 4. 在 SCL 为高期间拉高 SDA形成上升沿 void IIC_Stop(void) { SDA_OUT(); // SDA 线切为输出方向 IIC_SCL 0; // 先拉低时钟允许 SDA 变化 IIC_SDA 0; // STOP: 先把数据线拉到低 delay_us(4); IIC_SCL 1; // 时钟拉高此时 SDA 保持低电平 IIC_SDA 1; // 在 SCL 高期间 SDA 由低变高发出结束信号 delay_us(4); }逻辑说明第一步必须先IIC_SCL 0。假设上一步是接收完字节SCL 可能还停在高电平这时候直接把 SDA 从高拉到低会被从机当成起始信号总线状态立刻错乱。先拉低时钟等于给自己争取了一个可以随便改 SDA的窗口把 SDA 先摆到低电平再把 SCL 拉高最后才抬 SDA形成合法的上升沿。参数说明这里的delay_us(4)同样是给从机的识别时间。文档在发送字节的注释里提过对某些模块这三个延时都是必须的说的就是这类延时不是保险丝是时序本身的一部分。一个常见的翻车点是初始化 GPIO 后没发停止信号就直接开始读数据。如果上一次通信因为异常中断总线可能卡在某个从机正拉着 SDA 的状态此时主机贸然发起始波形就对不上了。我一般在上电初始化末尾补一句IIC_Stop()当作后悔药把总线拉回确定的空闲态。3.4 延时常量与 400kbps一个位周期怎么算文档提到高速 IIC 总线一般可达 400kbps 以上。这个数字不是拿来感受的是要换算成延时参数的。400kbps 意味着每秒 40 万个位每个位的周期是 2.5μs半边高电平半边低电平各 1.25μs。标准模式 100kbps 则是位周期 10μs高低各 5μs。所以你会看到不同代码里的delay_us常量大不一样写delay_us(4)的实际上跑在 100kbps 附近甚至更低写delay_us(1)或干脆用空指令的冲着快速模式去的。换算完还得留余量。软件 IIC 的延时函数本身有调用开销GPIO 翻转也要时间实际位周期永远比算出来的长。我的经验是想要 100kbps延时按 80kbps 算想要 400kbps先确认主频够高、延时函数是精准的忙等而不是走系统节拍否则老老实实降到 200kbps 用。稳定比标称速率重要得多。4. 字节收发与应答一个字节的 8 个时钟和等待应答的超时起始信号把总线钳住之后接下来就是一位一位地搬数据。IIC 的数据位是边沿触发的SCL 低电平期间主机把数据摆上 SDASCL 上升沿之后接收方采样SCL 高电平期间数据保持稳定SCL 下降沿之后才能改下一位。八个节拍送完一个字节第九个节拍交给接收方回应答。4.1 发送一个字节MSB 优先与高位判断文档的IIC_Send_Byte用的是最常见的写法——每次取最高位送完之后把整个字节左移一位// IIC 发送一个字节 // 返回值未使用实际应答由 IIC_Wait_Ack 单独判断 void IIC_Send_Byte(u8 txd) { u8 t; SDA_OUT(); // 发送前确保 SDA 是输出方向 IIC_SCL 0; // 拉低时钟开始数据传输 for (t 0; t 8; t) { // 取当前字节的最高位为 1 则 SDA 拉高为 0 则拉低 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); } }逻辑说明txd 0x80判断的是 bit7 7把它变成 0 或 1。也可以直接写IIC_SDA (txd 0x80) 7文档注释里两种都给了。关键是txd 1这一句在送完当前位之后立刻左移下一轮循环用同一个判断逻辑就能取到次高位。整个循环跑完 8 次正好把 txd 的 8 位从高到低送出去。参数说明txd是要发的一个字节注意函数内部对它做了左移是按值传递外面传进来的变量不会被改但如果传的是某个寄存器变量的引用或者后面还要用它得留个心眼。三次delay_us(2)分别对应数据建立时间高电平保持时间低电平准备时间少了任何一个波形都会变窄从机在高速下就可能采错。还有一点文档专门写了只有当 SCL 被拉低后SDA 才能被改变。代码里每次改IIC_SDA之前都确认IIC_SCL 0这个顺序是硬约束不是风格问题。4.2 等待应答方向切换与超时保护发完 8 位第 9 个时钟周期交给从机。主机要做两件事把 SDA 从输出切成输入不然主机会一直驱动这根线从机拉不低然后看从机有没有把它拉低。文档的写法是// 等待应答信号到来 // 返回值1 表示接收应答失败0 表示接收应答成功 u8 IIC_Wait_Ack(void) { u8 ucErrTime 0; SDA_IN(); // SDA 切换为输入交给从机驱动 IIC_SDA 1; // 释放数据线拉高以便检测下降 delay_us(1); IIC_SCL 1; // 时钟拉高从机在此周期内应答 delay_us(1); while (READ_SDA) // 读取 SDA若一直为高表示没有应答 { ucErrTime; if (ucErrTime 250) { IIC_Stop(); // 超时后发停止信号释放总线 return 1; } } IIC_SCL 0; // 时钟拉低应答周期结束 return 0; }逻辑说明SDA_IN()是整个函数的核心方向不切后面READ_SDA读到的永远是主机自己写进去的 1从机应答到此为止。IIC_SDA 1这一句在切为输入后写实际作用因芯片而异——有的 MCU 输入模式下写 ODR 仍会影响上拉/下拉有的完全无效但保留它没有坏处能兼容一部分先写后切的老代码。参数说明ucErrTime 250是超时门限文档没说这个 250 是怎么来的本质上是个经验值——循环 250 次的总时间大致在几百微秒到一两毫秒之间足够从机响应又不至于让主程序卡太久。如果从机没接、地址错了、或者器件正在复位这个循环不设上限就是死循环整个系统卡在这里。这个超时是必须的不是可选项。超时后调用IIC_Stop()也有讲究总线已经乱了发一个停止信号能让还在等待的从机复位状态机下次通信才有可能成功。很多只写返回错误不写释放总线的实现第二次调用还是失败原因就在这。4.3 接收一个字节与 ACK/NACK 的收尾接收是发送的镜像主机先把 SDA 切成输入每来一个 SCL 高电平读一位左移拼装八位读完由主机决定给应答还是非应答。// 读 1 个字节ack1 发送 ACKack0 发送 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; // 拉低时钟允许从机改变 SDA delay_us(2); IIC_SCL 1; // 拉高时钟此时 SDA 上是从机送出的数据位 receive 1; // 左移一位给新数据腾位置 if (READ_SDA) receive; // 读到 1 则最低位置 1 delay_us(1); } if (!ack) IIC_NAck(); // 主机不想再读了回 NACK else IIC_Ack(); // 主机还要继续读回 ACK return receive; }逻辑说明receive 1必须在读 SDA之前执行否则第一次左移会把刚读到的最高位挤掉。这是新手最常写反的一行现象是读回来的字节整体右移一位、最低位恒为 0。READ_SDA在 SCL 高电平期间读取正好对应协议规定的采样窗口。参数说明ack控制第 9 个时钟周期主机给什么。读最后一个字节时必须传 0发 NACK通知从机我不要了你可以释放 SDA 了这样主机才能顺利发出停止信号如果最后一个字节还回 ACK从机会继续往 SDA 上送数据主机这时发停止信号两者就在总线上打架波形会很难看。4.4 组装一次完整的寄存器读五个原子函数的组合把上面几个函数串起来就是实际外设驱动里最常见的读寄存器操作。以读某传感器的一个 8 位寄存器为例常见做法是// 读指定器件某个 8 位寄存器的值 // dev_addr: 7 位从机地址reg: 寄存器地址 // 返回读到的字节通信失败返回 0xFF u8 IIC_Read_Reg(u8 dev_addr, u8 reg) { u8 data; IIC_Start(); // 起始信号 IIC_Send_Byte((dev_addr 1) | 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_addr 1) | 1); // 地址 读方向 if (IIC_Wait_Ack()) { IIC_Stop(); return 0xFF; } data IIC_Read_Byte(0); // 只读一个字节回 NACK IIC_Stop(); // 停止信号释放总线 return data; }逻辑说明注意中间那个IIC_Start()是重复起始不是先 Stop 再 Start。区别在于重复起始期间总线控制权没有释放其他主机不会插进来如果先 Stop 再 Start理论上别的设备可以抢占总线读到的数据就可能来自错误的对象。大部分单主系统里两者现象一样但协议上只有重复起始是标准写法。参数说明dev_addr 1是因为 IIC 的地址帧是 7 位地址加 1 位读写方向位左移之后最低位空出来写操作填 0读操作填 1。很多人第一次看到(addr 1) | 0会愣一下其实就是把读写位拼进去。每步都判断应答失败立刻 Stop 并返回错误值这套写法能保证任何一步出错都不会把总线留在占用状态。5. 避坑与排查示波器上看不出来的五个坑协议本身不复杂出事的地方往往在协议之外。下面五条是我和周围人踩过的每条都按现象是什么 → 真正的原因 → 怎么解决整理能省掉不少盯代码的时间。5.1 五条高频出错的记录第一坑读回来的数据恒为 0xFF现象起始信号、地址、寄存器地址全都发了IIC_Wait_Ack也返回 0有应答但读出来的字节永远是0xFF。原因IIC_Read_Byte里没有正确切换到输入方向或者SDA_IN()和SDA_OUT()的宏定义写反了。主机还在驱动 SDA读到的是自己写进去的高电平。解决在IIC_Read_Byte入口处打断点读之前先量一下 SDA 的电平如果主机还在拉高说明方向切换失败。检查宏定义里引脚配置寄存器的位有没有写对输入模式是否配成了上拉输入而不是浮空。第二坑从机地址对了但偶尔应答失败现象单步调试永远成功全速跑十次失败一两次。原因延时常量在单步时被拉长了几十倍全速下不够用或者主循环里有中断打断把某个高电平周期撑得太长从机判为超时。解决先把delay_us整体放大一倍验证是否是时序余量问题确认问题时在关键时序函数里临时关中断测完再开。如果是从机超时判定导致需要把整个字节收发做成原子的不能中途被高优先级中断切走。第三坑上电第一次通信必失败复位一次就好现象冷启动后第一次读总是错误按一下复位键之后就正常了。原因从机上电复位比主机慢主机初始化完立刻发时序从机还没准备好或者总线上残留了上一次异常掉电时的电平状态。解决初始化末尾加一段上电延时并在第一次通信前先发一个IIC_Stop()清理总线。如果总线真的被某个从机拉死常见做法是手动给 SCL 打 9 个时钟脉冲让从机的移位寄存器把剩余的位数吐完再发停止信号。第四坑程序卡死在等待应答里看门狗都救不回来现象拔掉传感器或者换一块没焊芯片的板子程序直接停住串口不再输出。原因IIC_Wait_Ack没有超时判断while (READ_SDA)死循环。解决必须加计数超时超时后发停止信号并返回错误。这个错误值要往上传让上层决定是重试还是报故障不能悄悄吃掉。带看门狗的系统里等待应答的循环里最好也喂一口狗否则复位之后总线状态还是乱的。第五坑多设备挂在同一总线上读 A 设备却影响了 B现象总线挂了两三个 IIC 器件单独调都正常一起用就随机出错。原因地址冲突或者其中一个器件的上拉电阻被重复焊接导致等效阻值过小、边沿过冲还有一种情况是某个器件在掉电时把 SDA 拉低整条总线被拖死。解决先断开其他器件逐个验证地址用扫描程序把总线上所有应答的地址打印出来对比手册上拉电阻只保留一组其余拆掉掉电拉低的问题只能从硬件上解决加隔离或者改上电顺序。5.2 一张上板自检清单代码调不通的时候我按这个顺序走一遍基本能定位到问题在哪一层万用表量 SDA、SCL 对 VCC 电压两条线空闲时都应接近电源电压上电不发任何信号用逻辑分析仪或示波器看两条线是否被意外拉低只发IIC_Start()看波形上 SCL 高电平期间 SDA 是否有干净的下降沿发地址字节确认第 9 个时钟周期 SDA 是否被从机拉低全部失败时换一个已知能用的器件交叉验证先排除是代码还是硬件。这套流程的价值在于把读不到数据这个笼统现象切成五段每一段都有明确的观察点。比一遍遍改代码试运气靠谱得多。6. 二次开发把它改成能跑在任意引脚、任意速率的总线层这份文档给的函数是直接操作IIC_SDA、IIC_SCL两个宏的换个芯片或者换组引脚就得改函数体。真正拿到工程里用第一件事是把管脚抽出去第二件事是给总线加上故障恢复能力。6.1 用宏和函数指针把管脚、延时抽象出来不改函数体、只改配置的做法是把引脚操作全部收进宏里函数只依赖这几个宏名// 移植时只改这一段下面所有时序函数不用动 #define IIC_PORT GPIOB #define IIC_SCL_PIN GPIO_PIN_6 #define IIC_SDA_PIN GPIO_PIN_7 #define IIC_SCL_H() HAL_GPIO_WritePin(IIC_PORT, IIC_SCL_PIN, GPIO_PIN_SET) #define IIC_SCL_L() HAL_GPIO_WritePin(IIC_PORT, IIC_SCL_PIN, GPIO_PIN_RESET) #define IIC_SDA_H() HAL_GPIO_WritePin(IIC_PORT, IIC_SDA_PIN, GPIO_PIN_SET) #define IIC_SDA_L() HAL_GPIO_WritePin(IIC_PORT, IIC_SDA_PIN, GPIO_PIN_RESET) #define READ_SDA HAL_GPIO_ReadPin(IIC_PORT, IIC_SDA_PIN) // 速率相关单位微秒改这一处就能整体调快调慢 #define IIC_DELAY_US(x) delay_us(x)这样换芯片只需要改端口和引脚号时序函数一行不动。更进一步可以把IIC_SCL_H()这类宏换成函数指针运行时选择走软件模拟还是走硬件外设同一套上层设备代码两种底层都能用。我一般不会一上来就做到函数指针这层除非确实有产品线要在两种方案间切换否则先把宏抽象做干净就够用了。6.2 加超时、重试和总线恢复原始代码里的超时只出现在IIC_Wait_Ack起始、停止、收发字节本身都没有保护。工程化改造我通常补三样给每个while等待加计数上限超时统一返回错误码在设备层加重试比如读寄存器失败重试 3 次每次之间补一个停止信号加一个IIC_Bus_Recover()函数检测到 SDA 长时间为低时手动打时钟脉冲把从机状态机推完。恢复函数的关键在于先把 SCL 配成输出、SDA 配成输入然后最多打 9 个 SCL 脉冲每打一个读一次 SDA直到 SDA 变高最后补一个停止信号。这一段代码文档里没有但凡是产品化的 IIC 驱动几乎都有掉了电、热插拔、从机死机全靠它把总线救回来。6.3 改完怎么验证还是对的改完之后别急着上业务代码先跑三个验证一是用逻辑分析仪抓一次完整读寄存器逐位核对起始、地址、应答、数据、停止是否符合时序图二是把延时常量减半再翻倍确认在 ±50% 范围内通信仍然稳定说明时序有余量三是人为把从机断电再上电看恢复函数能不能把总线拉回可用状态。这三个验证跑通这套驱动才敢往产品里放。改这类底层代码我吃过最大的亏是看着能跑就不管了——软件 IIC 在实验台上永远稳一进现场碰上长线、多设备、电源波动就开始玄学。从那以后我每次写完或改完 IIC 驱动都强制走一遍逻辑分析仪逐位核对加断电恢复测试不管工期多紧。希望帮到你手上这份文档里的时序和函数就是走这套流程最好的起点。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/28 20:55:05

HC32F460 PC14输出异常:PCR寄存器与数字通道配置解析

简介:这份PDF技术文档面向使用HC32F460做嵌入式开发、在PC14或PC15引脚配置为GPIO输出时遇到电平异常的工程师与学习者。文档从PC14/PC15兼作32K时钟输入、PCRC14与PCRC15复位值为0x8100、数字功能默认关闭这一关键原因切入,结合寄存器地址定义、初始化阶…

2026/10/28 20:55:05

手机摄影原图直出:技术演进与实用技巧

1. 手机摄影的"原图直出"现象解析最近在摄影圈里有个挺有意思的现象:越来越多用户开始抱怨手机拍照"太真实"。就像这个标题说的——"真不怪我原图直出,是这手机太较真,一点颜色都不带差的"。这背后其实反映了手…

2026/10/28 20:55:05

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

简介:《IIC通信协议详解》面向嵌入式初学者、单片机开发者及需要复习总线时序的工程师,聚焦两线式串行总线的原理理解与代码落地。文档围绕SDA、SCL两条信号线展开,说明半双工通信中主机与从机的数据收发关系,区分软件IIC与硬件II…

2026/10/29 1:18:48

Windows任务计划+批处理FTP脚本实现无人值守自动传输

简介:围绕批处理命令与FTP自动传输整理的Word文档,面向Windows及UNIX环境下的系统运维人员和网络管理员,用于解决每天重复登录、手工收发文件既繁琐又容易出错的问题。文档以1.ftp与1.bat两个文本的编写为主线,说明如何用 ftp -n …

2026/10/29 1:18:48

从零开始:搭建Python调用Design Ware的IP的验证环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/29 1:18:48

Delphi 12.3中XLSReadWriteII控件读写Excel的完整指南

简介:针对 Delphi 12.3 开发环境的 XLSReadWriteII v6.01.17 控件包,面向需要在桌面程序中实现 Excel 文件读写、报表生成与数据导出的中高级 Delphi 开发者。整套资源共 499 个文件,压缩后约 2.15MB,以 pas 源码为核心&#xff0…

2026/10/29 1:18:48

【GATT】规范精讲[12]: GATT 的 BR/EDR 名片——SDP 记录里藏着哪条通道

GATT系列精讲前十一篇讲的 GATT 过程,大多在 LE 上跑,因为消费电子里 BLE 占了绝大多数。但 GATT 不是 LE 的专属,它也能跑在 BR/EDR 上。经典蓝牙世界里有一套沿用了二十多年的服务发现机制叫 SDP——Service Discovery Protocol。当 GATT 想在 BR/EDR 上安家时,就必须按 …

2026/10/29 1:13:47

TPS259483AYWPR与STM32F469II协同实现工业级电源路径保护

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/28 14:42:12

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/28 15:28:56

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/28 3:32:29

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/29 0:03:42

学生选课系统DFD图绘制指南:边界划分与分层校验实战

简介:文档以顶层与第一层DFD图为核心,完整刻画学生选课系统的逻辑模型,面向软件工程课程设计、系统分析学习及毕设前期建模人群。顶层图界定系统边界,外部实体包括管理员、教师和学生三类角色;第一层图进一步细化用户登…

2026/10/29 0:03:42

JIRA敏捷落地:Scrum角色权限、Sprint与燃尽图避坑指南

简介:围绕“基于JIRA的敏捷开发项目管理”这一主题,面向产品负责人、Scrum Master及研发测试成员,梳理Scrum角色分工、迭代流程与JIRA工具配置的对应关系,适合初、中级团队导入敏捷管理时参考。压缩包共1个docx文件,大…

2026/10/29 0:03:42

SEMI S8人机工程验收实战:从条款拆解到现场实测的完整指南

简介:面向半导体设备设计、制造与运维工程师及安全合规人员,这份资料聚焦SEMI标准体系下的设备安全规范,重点收录SEMI S8-0308《半导体制造设备人体工程学工程安全指南》。内容围绕设备设计原则、错误与事故预防、人体工程学适配三大主线展开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑