简介:《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_ns=5000, t_high_ns=5000, t_buf_ns=4700): 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, ack=True): 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_low=5 µs、t_high=5 µs对应 100 kHz 标准模式;快速模式把两者改成约 1.3 µs 和 0.6 µs,t_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, read=False): if not 0 <= addr7 <= 0x7F: raise ValueError("7 位地址越界") return (addr7 << 1) | (1 if read else 0) def ten_bit_header(addr10, read=False): 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, read=False): return ten_bit_header(addr10, read), addr10 & 0xFF print(hex(first_byte(0x50, read=True))) # 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 0XX | 10 位寻址头 | 否 |
| 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 之后立刻发停止条件。如果用的是封装好的库,去翻它的读函数签名,很多默认参数是ack=true一直到底,得手动关掉最后一个。
5.3 现象:总线锁死,SDA 一直被某颗从机拉低
设备运行中复位 MCU,重启后 I2C 完全不通,示波器上 SDA 恒为低。断电再上电有时能恢复,有时不能。
原因是 MCU 复位发生在一次传输中间,从机正处在输出 0 的位里。它还在等下一个 SCL 时钟把自己的这一位送完,而主机已经不知道现场了,两边互相等,总线就挂住了。
标准解法是总线恢复序列:把 SCL 配置成开漏输出,手动打 9 个时钟脉冲,让从机把剩下的位(包括 ACK 位)全部吐完,然后手动产生一个停止条件,最后复位 I2C 外设。9 这个数字是因为一个字节加一个响应位最多 9 个时钟,打完从机一定处于空闲等待起始的状态。我一般把这段写成上电初始化的一部分,每次启动先跑一次,比事后救火省心。
5.4 现象:双主机系统偶发地址错乱,概率很低但复现得到
两块 MCU 挂同一条总线,各自都有机会发起传输。压测几万次会出现一两次从机收到错误地址,或者数据段错位。
原因多半是仲裁丢失后没做处理。硬件 I2C
本文还有配套的精品资源,点击获取