资讯详情

I2C总线规范精读:位传输、仲裁、寻址与故障排查

📅 2026/10/29 8:46:46 | 华诺云谱 👁 阅读
I2C总线规范精读:位传输、仲裁、寻址与故障排查
简介《I2C总线协议详解中文》是一份面向嵌入式软硬件工程师、单片机学习者及电子类学生的总线协议参考文档重点梳理I2C从版本演进到通信机制的完整知识框架适合用于接口调试、外设驱动开发与协议复习。压缩包内含1个PDF文件大小约872KB篇幅集中便于在电脑或移动设备上随查随用。目前已有842人学习下载具备一定参考热度。内容覆盖I2C的设计与厂商收益、SDA/SCL主从总线概念、标准/快速/高速模式、数据有效性、起始停止条件、字节格式与应答、7位及10位寻址并延伸至仲裁和时钟同步等要点目录按规范章节组织可帮助读者快速定位关键概念理解时序约束与通信流程为实际总线的配置、排错和系统优化提供清晰依据。1. 一块板子卡在起始条件上让我把这份 I2C 中文规范翻烂了新板第一次上电逻辑分析仪上只看到 SCL 拉高、SDA 抖了一下然后什么都没有——从机不响应读数全 0xFF。示波器一量SDA 上升沿爬到 2.1V 用了 1.4 µs压根没进高电平判定区。问题不在代码在上拉电阻选大了。后来我把这份 I2C 总线规范的中文译稿打印出来夹在工位挡板上它把位传输、仲裁、7 位/10 位寻址、Rp 与 RS 的取值边界、混合速度总线这些事一次讲全了不是教程式的“带你入门”而是原厂那份定义的直译条款编号都能对上。适合已经会用 HAL 库调 I2C、但一遇到“偶发不 ACK”“总线锁死”“多主机丢包”就抓瞎的人也适合刚接手驱动、需要一份能查参数的案头资料的新手。它解决的不是“I2C 怎么写”而是“I2C 为什么这么写”。2. 位传输层起始停止条件、数据有效窗口和响应位怎么在代码里对齐这一层是所有 I2C 故障的第一现场。HAL 库把细节包成了黑匣子一旦时序出问题你连从哪一微妙开始查都不知道。把位级动作拆开看其实只有三件事什么时候允许改 SDA、什么样的边沿算起始或停止、第九个时钟由谁拉低。2.1 开漏与上拉两条线为什么必须“只能拉低”SDA 和 SCL 都是双向线输出级必须是漏极开路或集电极开路靠上拉电阻或电流源回到高电平。这个约束不是为了省电是为了实现“线与”只要总线上有一个器件拉低整条线就是低全部释放才变高。多主机仲裁、从机时钟拉伸、应答位全都建立在这个物理特性上。换成推挽输出两个器件同时驱动一高一低就是短路芯片当场发烫。选型上要注意两点。第一上拉不能随便取 10 kΩ总线电容 400 pF 是规范给的硬上限阻值越大上升沿越慢边沿爬到阈值之前数据就被采样了通信时好时坏。第二器件数量不是按个数算的是按总电容算的每挂一个器件都要加几 pF 到十几 pF 的引脚电容和走线电容超过 400 pF 就得加缓冲器或者分段。提示规范里写的是“连接到总线的接口数量只由总线电容 400 pF 限制决定”不是“最多挂 112 个器件”这类经验值。2.2 起始、重复起始、停止三个边沿和它们的建立时间起始条件 S 的定义是SCL 为高电平时SDA 由高变低。停止条件 P 是SCL 为高电平时SDA 由低变高。注意判定依据是 SDA 在 SCL 高电平期间的跳变不是 SCL 的边沿——这一点决定了硬件 I2C 可以在不逐位采样的情况下捕获起始。产生起始条件之前总线必须处于空闲态也就是 SDA 和 SCL 都被释放为高并且维持一个最小总线空闲时间。起始条件之后总线进入忙态直到停止条件出现才释放。如果只发重复起始 Sr 而不发停止总线一直保持忙——这正是读操作“写地址→重复起始→读数据”能成立的原因中间不允许别的器件插进来。建立时间这块是软件模拟 I2C 最容易偷工减料的地方。标准模式下起始条件的保持时间、停止条件的建立时间都有最小值要求代码里靠 delay 硬撑把主频超到 200 MHz 后 delay 循环被编译器优化掉示波器上起始条件的台阶就没了。我一般会把这几段延时写成__NOP()拼的固定循环或者用定时器打底不让编译器随便动。2.3 字节与响应位MSB 先行第九个时钟谁来拉低每个字节固定 8 位高位先发。发完 8 位第 9 个时钟脉冲是响应位发送器释放 SDA接收器把它拉低表示 ACK保持高表示 NAK。这里有个方向问题必须分清——响应时钟永远由主机产生但拉低 SDA 的是接收方。主机写数据时从机是接收方从机拉低主机读数据时主机是接收方主机拉低。规范里给了三种典型场景。从机不认识自己的地址就不响应SDA 保持高主机随后发停止或重复起始。从机认了地址但传了一半收不动了也在某个字节后不响应主机同样终止。主机作为接收器读最后一个字节时必须主动回 NAK告诉从机“别再发了”否则从机会一直往总线上灌数据程序就卡在读循环里出不来。这条在驱动里对应的是“读 N 个字节前 N-1 个 ACK最后一个 NAK”很多现成库默认全 ACK用之前得翻一眼。2.4 一份可以直接抄的位操作骨架下面这段是软件模拟的时序骨架gpio换成你手上的 HAL 或寄存器操作即可重点是每个动作和延时参数的位置。# i2c_bitbang.py —— 标准模式骨架Fast 模式改 t_low/t_high 即可 class I2CBitBang: def __init__(self, gpio, t_low_ns5000, t_high_ns5000, t_buf_ns4700): self.g gpio self.t_low t_low_ns / 1e9 # SCL 低电平时间 self.t_high t_high_ns / 1e9 # SCL 高电平时间 self.t_buf t_buf_ns / 1e9 # 停止到下次起始的总线空闲时间 def _sda_low(self): self.g.write_sda(0) def _sda_release(self): self.g.mode_sda_input() # 开漏脚转输入 释放 def _scl_low(self): self.g.write_scl(0) def _scl_release(self): self.g.mode_scl_input() def start(self): self._sda_release(); self._scl_release(); self._need(self.t_buf) self._sda_low() # SCL 高时 SDA 下降 起始 self._need(self.t_low) self._scl_low(); self._need(self.t_low) def stop(self): self._sda_low(); self._need(self.t_low) self._scl_release(); self._need(self.t_buf) self._sda_release() # SCL 高时 SDA 上升 停止 self._need(self.t_buf) def write_byte(self, value): for i in range(7, -1, -1): # MSB 先行 self._scl_low() self._sda_release() if (value i) 1 else self._sda_low() self._need(self.t_low) self._scl_release(); self._need(self.t_high) self._scl_low(); self._sda_release() # 第 9 个时钟主机释放 SDA self._need(self.t_low) self._scl_release() while self.g.read_scl() 0: # 从机在拉伸时钟必须等 pass self._need(self.t_high) ack (self.g.read_sda() 0) # 读到低 ACK self._scl_low() return ack def read_byte(self, ackTrue): value 0 self._sda_release() # 读之前主机必须释放 SDA for _ in range(8): self._scl_low(); self._need(self.t_low) self._scl_release(); self._need(self.t_high) value (value 1) | (1 if self.g.read_sda() else 0) self._scl_low() self._sda_low() if ack else self._sda_release() self._need(self.t_low) self._scl_release(); self._need(self.t_high) self._scl_low() return value def _need(self, seconds): self.g.busy_wait(seconds)参数上t_low5 µs、t_high5 µs对应 100 kHz 标准模式快速模式把两者改成约 1.3 µs 和 0.6 µst_buf从 4.7 µs 降到 1.3 µs 左右。read_byte里最后那个ack参数就是“主机接收器最后一个字节回 NAK”的落点别看它只有一行读 EEPROM 死机的坑八成都出在这里。while self.g.read_scl() 0是时钟拉伸的兜底没有它从机忙的时候主机会当作已经推进后面整帧错位。3. 仲裁与时钟同步两个主机同时开口凭什么不丢数据“多主机总线”这四个字在单 MCU 系统里很难有实感直到你把两个板子挂到同一条 I2C 上看波形。仲裁机制不是软件协商出来的是线与逻辑的副产品理解它之后很多“偶发错帧”就有了解释。3.1 线与仲裁为什么不会破坏报文仲裁发生在 SDA 上条件是 SCL 为高电平。每个主机在发出每一位后都会回读 SDA如果自己发的是 1读回来却是 0说明有别的主机在发 0自己输了立刻关掉 SDA 输出级退出转为从机状态或等待下一次起始。关键在于赢的那一方发出的内容和总线上的实际电平始终一致所以报文一个位都没被破坏。仲裁可以持续很多位。第一阶段比的是地址位如果两个主机恰好寻址同一个从机就继续比数据位如果都是主机接收器还会比响应位。这也是为什么规范强调“地址不能重复”——地址撞了不会立刻出错会在数据段才开始分胜负那时候从机已经进入接收状态问题看起来就莫名其妙了。3.2 时钟同步低电平取最长高电平取最短SCL 也是线与。某个主机把 SCL 拉低整条线就是低所有主机都释放线才变高。这个过程自然产生了同步低电平周期由低电平最长的那个器件决定高电平周期由高电平最短的那个器件决定。换成工程语言就是总线上跑得最快的主机会被最慢的那个拖住而且拖得毫无怨言。这解释了为什么从机拉低 SCL 就能直接让主机停下来等——不需要任何握手协议物理层就完成了。也解释了混合速度总线为什么必须分段400 kHz 的主机和 100 kHz 的从机挂一起实际速率会被拉到 100 kHz 附近除非用 Hs 模式那套主代码切换机制。3.3 时钟拉伸软件模拟 I2C 最容易翻车的地方从机在接收或发送一个字节后如果内部还没准备好可以把 SCL 保持低电平主机必须检测到并等待。规范里明确主机发出的时钟只有在“慢速从机延长时钟”或“仲裁被另一主机改变”这两种情况下才会被改变。硬件 I2C 外设会自动处理拉伸但软件模拟不会。常见的翻车写法是拉低 SCL、改 SDA、延时、释放 SCL、延时然后直接进下一位——中间从来没读过 SCL 的实际电平。从机在第一字节后拉低 SCL 拖了 200 µs主机这边 5 µs 就推进了后续所有位全部错位读回来的数据看着像随机数。注意释放 SCL 之后、采样 SDA 之前必须回读 SCL 直到它为高。这个检查在正常通信里开销很小但能挡掉绝大部分“偶发丢字节”。3.4 用二十行代码把仲裁过程跑一遍拿真实硬件验证仲裁代价不小用一段模型代码先把逻辑跑通再去看波形会快很多。def sda_arbitrate(frames): frames: 每个主机准备发出的字节MSB 先。 返回仍留在总线上的主机下标以及总线上实际出现的位序列。 alive list(range(len(frames))) bus_log [] for bit in range(8): wired 1 for i in alive: if ((frames[i] (7 - bit)) 1) 0: wired 0 # 任一主机拉低总线即为低 bus_log.append(wired) # 发 1 却读到 0 的主机输掉仲裁立即退出 alive [i for i in alive if not (((frames[i] (7 - bit)) 1) 1 and wired 0)] if len(alive) 1: break return alive, bus_log def sync_one_cycle(low_ticks, high_ticks): 简化模型低电平取最长高电平取最短 return max(low_ticks), min(high_ticks)sda_arbitrate的输出可以直接对照逻辑分析仪bus_log就是总线上真实出现的位剩下那个alive下标对应的就是从机最终收到的地址。如果两个主机的frames前七位完全一样只有第八位不同你会看到仲裁一直持续到最后一个地址位才分出胜负和规范描述的“仲裁可持续多位”完全对得上。sync_one_cycle是个粗模型真实机制是逐周期地重置计数器但它足够解释一件事总线上如果同时存在 100 kHz 和 400 kHz 的主机实际 SCL 周期不是取平均而是被拉长到接近慢的那一方。写混合速度系统的时序预算时别按快的算。4. 寻址与速率边界7 位、10 位地址怎么算100k/400k/3.4M 差在哪地址算错是 I2C 调试里最冤的一类问题——代码逻辑全对波形也漂亮就是从机不理你。原因往往是左移了一位、读写位放反了或者撞进了保留地址区间。4.1 7 位地址一个字节里的地址位和读写位第一个字节的高 7 位是地址最低位是方向位0 表示主机要写1 表示主机要读。也就是说从机手册上写的0x50是 7 位地址总线上实际出现的写字节是0xA0读字节是0xA1。很多库函数要求传 8 位地址已经左移过有些要求传 7 位原值混用一次就够查半天。规范把这 7 位地址分成几段0000 000是广播呼叫0000 001是 CBUS 地址1111 0XX留给 10 位寻址的头字节剩下的才是普通器件地址。实际可自由分配的范围大致是0x08到0x77中间还要剔除厂商预留给特殊功能的几个。4.2 10 位寻址两个字节头读操作要两次起始10 位地址是为了解决 7 位地址不够用的问题能多出 1024 个地址。它的帧结构是两字节头字节固定1111 0XX加上方向位XX是 10 位地址的高 2 位第二字节是地址的低 8 位。写操作很直接起始 → 头字节写→ 地址低 8 位 → 数据……。读操作麻烦一点起始 → 头字节写→ 地址低 8 位 → 重复起始 → 头字节读→ 读数据。中间那个重复起始不能省省了从机会把方向位当成数据。第一次上手写 10 位器件时我在这上面卡了整整一个下午。10 位器件和 7 位器件可以挂在同一条总线上因为1111 0XX这段地址在 7 位空间里是保留的不会冲突。但要注意某些老器件的地址译码做得不严看到1111 0XX也会响应这时候就只能分段了。4.3 保留地址广播呼叫、起始字节和 CBUS 三处雷区广播呼叫地址0000 0000是写给总线上所有器件听的。规范里定义了第二字节的几个特殊含义比如某个值表示软复位某些值表示写可编程地址部分。如果总线上挂着不认这套约定的器件一次广播写就可能把它们全带进异常状态。我自己踩过一次本想给一颗 IO 扩展器发配置地址少算一位变成了广播地址结果整条总线上三颗器件同时复位。起始字节0000 0001是为那些采样率低、来不及在正常起始条件窗口内响应的 MCU 准备的。主机先发这个字节产生 7 个时钟加一个响应位让从机有足够时间准备。它不改变总线状态纯粹是“打个招呼”。如果从机不支持回 NAK主机照常继续。CBUS 兼容地址0000 001X属于历史遗留跟 I2C 报文格式不完全一样规范里写了这种情况下不产生响应靠停止条件终止。碰到标着 CBUS 兼容的老器件别按标准流程去等 ACK。4.4 三种速度模式Hs 模式为什么要先发主代码标准模式 100 kbit/s、快速模式 400 kbit/s、高速模式 3.4 Mbit/s这三级不是把时钟调快就行。快速模式引入斜率控制和输入滤波是为了在更快的边沿下压住 EMC高速模式则改了一套完全不同的电气规范上拉电阻、驱动电流、负载电容全都要重算。Hs 模式最特别的是切换流程主机先按 F/S 模式发一个起始接着发0000 1XXX主代码后 3 位是主代码编号然后重复起始之后才能用高速时序通信。这个主代码的作用是让总线上只支持 F/S 的从机忽略后面所有流量——它们看到0000 1XXX不属于自己能识别的地址就不响应安静等着。混合速度总线之所以能共存靠的就是这一段。提示规范 2.1 版里对 Hs 模式重复起始后的 SCLH 延长做了修正手上如果是最早那版译稿时序参数以新版为准。4.5 地址计算脚本与自查表地址位运算错一次就够难受写个几行脚本固化下来更省事。def first_byte(addr7, readFalse): if not 0 addr7 0x7F: raise ValueError(7 位地址越界) return (addr7 1) | (1 if read else 0) def ten_bit_header(addr10, readFalse): if not 0 addr10 0x3FF: raise ValueError(10 位地址越界) hi2 (addr10 7) 0x03 # 高 2 位放到 bit2:bit1 return 0xF0 | (hi2 1) | (1 if read else 0) def ten_bit_frame(addr10, readFalse): return ten_bit_header(addr10, read), addr10 0xFF print(hex(first_byte(0x50, readTrue))) # 0xa1 print([hex(x) for x in ten_bit_frame(0x2A5)]) # [0xf5, 0xa5]first_byte就是最常用的 7 位左移加方向位ten_bit_header里(addr10 7) 0x03取出地址的最高两位左移一位后填进头字节的 bit2 和 bit1其余位由0xF0固定。ten_bit_frame返回两个字节读操作时头字节要先发写方向、重复起始后再发读方向这两次调用参数不同别复用同一个返回值。地址段含义可否用于普通器件0000 000广播呼叫否0000 001起始字节 / CBUS否0000 01X保留否0000 1XX保留否0000 0001 段至 0111 0111普通 7 位器件是1111 0XX10 位寻址头否1111 1XX保留否这张表对照规范第 10 章和第 14 章画的实际分配时我习惯先在表上把已占用地址标掉再给新器件挑空位比事后扫描快。5. 避坑排查五个示波器上抓不到的现场故障波形干净、代码编译通过、地址表也核过问题依然出现——这类故障基本都在时序边角或者电气边界上。下面五条是这几年见得最多的每条都按现象、原因、解法记录。5.1 现象地址扫描每个地址都回 ACK扫描脚本跑完从0x08到0x77全部返回成功读数据清一色0xFF。第一反应通常是“总线上挂了十几个器件”但实际只焊了两颗。原因一般是 SDA 没上拉或者上拉虚焊主机释放 SDA 后读到的是悬空高电平程序把“读到高”误判成了某种 ACK 逻辑或者在没发时钟的情况下就返回了成功。另一种可能是主机读 SDA 的引脚模式配错配成了推挽输出永远读回自己写的值。解决分两步先断电用万用表量 SDA、SCL 到 VDD 的电阻确认上拉存在且阻值合理再单独跑一次“发地址、发一个字节、采样第 9 个时钟”的最小序列看 ACK 位是不是真的被拉低。如果 ACK 位波形上是高那不管扫描脚本返回什么从机都没认。5.2 现象连续读 EEPROM跑到最后一个字节就卡死读 16 字节前 15 个正常第 16 个读回来是0xFF然后程序停在读循环里不出来或者下一次通信直接失败。原因是主机接收器在最后一个字节没有回 NAK。从机收不到“结束”信号继续往总线上送下一个字节主机这边已经准备发停止了两边状态对不上。规范里写得很清楚主机接收器必须通过在最后一个字节不产生响应来通知从机发送器数据结束。解决是把读循环拆成“前 N-1 个 ACK、最后一个 NAK”NAK 之后立刻发停止条件。如果用的是封装好的库去翻它的读函数签名很多默认参数是acktrue一直到底得手动关掉最后一个。5.3 现象总线锁死SDA 一直被某颗从机拉低设备运行中复位 MCU重启后 I2C 完全不通示波器上 SDA 恒为低。断电再上电有时能恢复有时不能。原因是 MCU 复位发生在一次传输中间从机正处在输出 0 的位里。它还在等下一个 SCL 时钟把自己的这一位送完而主机已经不知道现场了两边互相等总线就挂住了。标准解法是总线恢复序列把 SCL 配置成开漏输出手动打 9 个时钟脉冲让从机把剩下的位包括 ACK 位全部吐完然后手动产生一个停止条件最后复位 I2C 外设。9 这个数字是因为一个字节加一个响应位最多 9 个时钟打完从机一定处于空闲等待起始的状态。我一般把这段写成上电初始化的一部分每次启动先跑一次比事后救火省心。5.4 现象双主机系统偶发地址错乱概率很低但复现得到两块 MCU 挂同一条总线各自都有机会发起传输。压测几万次会出现一两次从机收到错误地址或者数据段错位。原因多半是仲裁丢失后没做处理。硬件 I2C本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑