首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
SPI与I2C协议详解:片间串行通信的取舍与调试实战
📅 2026/10/23 3:43:49
✍️ 爱科研究院
👁 阅读 3,247
芯片内部的互联和芯片之间的互联是两个世界。片内信号走的是几百微米的金属线片间信号走的是几厘米的 PCB 铜线片内你可以随时拉一组 32 位并行总线片间你往往只剩几个引脚可用。SPI 和 I2C 就是专为“片间”这种场景设计的两个经典串行协议也是这个系列从处理器内部走向真实外设的第一步。如果你跟我一样正在自己折腾 CPU 核、FPGA 原型或者单片机外设这篇把两个协议的脾气、取舍和调试经验讲清楚能省下不少弯路。其实“从片内到片间”这个跨度难点从来不在协议本身的复杂度——SPI 只有四根线I2C 只有两根线它们的吞吐连片内总线的零头都比不上。真正的难点在于思维切换片内你可以假设全局时钟、无限制数据位宽、微米级走线片外你要面对引脚成本、信号完整性、电平域差异、多设备抢占。下面我不打算照着数据手册念尽量把自己调通这两个协议的过程、踩过的坑、以及一些“当年没人告诉我”的细节摆出来。1. 为什么芯片之间不能直接拉一组并行总线1.1 片内总线的“特权”从哪来如果你跟着这个系列看到了这里应该已经熟悉片上总线的玩法32 位甚至 64 位数据线并排铺开配上一堆地址线和控制线一个周期就能搬一个完整的数据字。片内总线敢这么干是因为它在硅片内部有天然红利。一根线在 die 上就是一长条金属拉多远都不涉及“引出引脚”线间距可以做到微米级多布几十根线完全不心疼。时钟更是如此同一片 die 里走线长度被工艺严格约束全局时钟树可以把偏移压在几十皮秒以内所有寄存器都能在一个统一的节奏里锁存数据。这也是为什么 CPU 内部的寄存器堆、ALU、Cache 接口全是宽并行的——带宽是片内设计的第一追求引脚数量和走线面积相对不值钱。你极少会看到有人为了让 CPU 核少用几根线而去把内部数据通路改成串行那纯粹是没事找事。1.2 出了 die 之后世界立刻变糙可一旦信号穿过封装的引脚来到 PCB 上之前那些“不值钱”的东西全变得昂贵起来。引脚是物理资产一颗芯片无论封装多豪华引脚数都是有限且成本敏感的。你不可能为了外接一个外设就占掉 40 个引脚那这颗芯片的其他功能基本就废了。我见过有人用一颗一百多个引脚的主控去接 8 位并口屏结果整版看起来像蜘蛛网其余传感器全挤在剩下几个 GPIO 上回头自己调试都想骂人。比引脚成本更要命的是信号完整性。PCB 走线不再是微米级短金属条而是几厘米以上的铜线线宽线距都在毫米级过孔、插排、连接器处处都是阻抗不连续点。把 8 根并行数据线同时翻转地弹、串扰、偏斜一起上传输距离稍微一长数据窗口就被糟蹋得没法看。我早年用并行方式接过一个外部存储器20MHz 一下稳定一超就随机丢数据后来发现是两根相邻线间的串扰在特定数据组合下把电平整个带翻。这类问题在串行协议里也会遇到但串行数据线数量少、速率可控处理起来便宜得多。1.3 串行化的代价与收益所以片间通信的共识就是牺牲带宽换取引脚数和信号完整性。实现手法也很朴素——并行转串行、串行转并行本质上是两个移位寄存器。数据位宽从 32 降成 1引脚数从几十降到几条代价是传同样一帧数据要多花 N 倍的时间。但对大多数外设来说这个代价完全可接受读个温度传感器的结果是 16 位放着几 Mbps 不用一秒钟照样能刷几百次完全没有瓶颈。SPI 和 I2C 就是在“低速但够用”的甜点区里长出来的两个经典方案。一个走四根线、全双工、协议薄如纸一个走两根线、半双工、协议精致到带仲裁和时钟拉伸。它们的性格差异恰好对应着两类不同的工程偏好。下面我把它们拆开讲。2. SPI四根线一个主人全双工的搬运工2.1 结构简单到了什么程度SPI 的结构骨架就四根线SCK串行时钟、MOSI主出从入、MISO主入从出、CS片选低有效。主机负责产生所有时钟从机不需要任何内部时钟也不需要知道“现在轮到谁说话”一切动作都由 SCK 的边沿驱动。说它是“一个主人”的全双工搬运工是因为同一时刻两个方向都在传数据主机往 MOSI 上推一位同时从 MISO 上采一位。这也是为什么 SPI 经常配合移位缓存器做数据交换——主机的移位寄存器往从机的移位寄存器里转从机的旧数据反向转回来一次时钟边沿完成一个 bit 的双向搬运。CS 是 SPI 里最容易被低估的一根线。它承担了地址选择的工作主机把某个从机的 CS 拉低就相当于宣布“我现在只跟你说话”其他从机全部靠边站。CS 必须在整个传输帧期间保持低电平这也是 SPI 和 I2C 在多设备拓扑上最大的不同——SPI 用引脚换隔离I2C 用地址换走线。2.2 四种模式的判定方法别靠背表SPI 协议薄薄到它甚至不规定“数据到底在哪个边沿有效”。这个锅甩给了 CPOL 和 CPHA 两个位CPOL 决定空闲时 SCK 的电平CPHA 决定数据采样发生在第一个边沿还是第二个边沿。两个位组合出 0 到 3 号四种模式。别去背“模式 0 是上升沿采样、模式 1 是下降沿采样”这类口诀背完必忘。我在调试时直接手推先看 SCK 空闲电平是高是低再找从空闲电平出发遇到的第一个边沿CPHA0 就在第一个边沿采样CPHA1 就推迟到第二个边沿。比如最常见的模式 0SCK 空闲为低第一个边沿是上升沿所以上升沿采样、下降沿跳变。实际项目里大多数 Flash、SD 卡、温湿度传感器默认都是模式 0 或模式 3但千万不要拿“默认”当“一定”。我有一次接一颗 ADC数据手册画得含含糊糊我按模式 0 初始化读回来的数值永远是一坨固定花屏。后来用逻辑分析仪把 MISO 波形拉出来才发现器件实际工作在模式 3——CPOL 和 CPHA 正好和我的配置差半个相位。从那以后我的原则变成先按模式 0 跑通链路再用逻辑分析仪看真实波形反推器件的采样沿凡是手册没写清楚的一律以波形为准。2.3 没有应答机制意味着什么SPI 没有 ACK。主机发完一个字节从协议层面根本不知道从机是否成功接收、是否执行完毕。这个特性对简单传感器无所谓但对接 Flash、SD 卡这类有内部状态机的存储设备就是个隐患。常规处理办法是写入操作完成后拉高再拉低 CS然后发“读状态寄存器”命令轮询忙位直到清空。这个流程完全靠驱动软件约定SPI 协议本身不提供任何强制保证。没有应答带来另一个后果从机想“慢一点”也没有协议层面的手段。它只能悄悄把 MISO 拉低示意忙但这只是约定不是规范主机完全可以无视它继续吐时钟。所以 SPI 器件数据手册里那个“最大 SCK 频率”是你唯一需要尊重的速度边界。一旦超频从机自己都不知道自己在说什么胡话你会得到一帧逻辑完美但数据垃圾的输出。2.4 用 GPIO 手搓一个 SPI 主机在连硬件 SPI 控制器都没有的环境下比如正在用自研 CPU 核接外设用普通 GPIO 模拟 SPI 是最快的验证手段也最能帮助理解协议本质// 模式0CPOL0, CPHA0, MSB first void spi_write_byte(uint8_t byte) { for (int i 7; i 0; i--) { GPIO_MOSI (byte i) 1; // 先放数据 GPIO_SCK 1; // 上升沿从机采样 GPIO_SCK 0; // 下降沿从机移出下一位 } } uint8_t spi_read_byte(void) { uint8_t val 0; for (int i 7; i 0; i--) { GPIO_SCK 1; // 上升沿主机从 MISO 采样 val (val 1) | GPIO_MISO; GPIO_SCK 0; } return val; }软件模拟看起来简单实际写的时候有一个高频错误数据必须在 SCK 拉高之前放到 MOSI 上而不是同时。把“置数据”和“拉时钟”两句的顺序写反波形上会出现一个极窄的毛刺从机有时采得对有时采得错此bug极难排查。另一个常见问题是 CS 的粒度——一次完整的多字节传输比如读 16 位 ADC 结果CS 必须在整帧期间保持低不能每个字节都拉一遍。我早期用现成的 SPI 库时被它“每字节翻转 CS”的默认行为坑过读回来的 16 位数据永远只有低字节是对的查了半天才发现是片选被当成了字节级片选。3. I2C两根线上的谈判桌3.1 开漏结构和上拉电阻为什么没有第三根线I2C 和 SPI 最大的气质差异在于它只有 SCL 和 SDA 两根线而且这两根线都是开漏结构。任何设备都只能把线拉低不能主动拉高线上的高电平全靠外接上拉电阻提供。听起来像缺陷但这正是多主设备共存的基石当两个设备同时驱动一条线时大家都拉低线就是低一个想拉低、另一个想拉高拉高的一方其实只是在“放手”不会像普通推挽输出那样互相怼烧 IO。上拉电阻的取值绝对不是“随便焊一个”。阻值太大总线电容充放电慢上升沿变得圆头圆脑高速模式下直接超时阻值太小灌电流超限设备可能拉不动或发热。工程经验值标准模式 100kbit/s 常用 4.7kΩ快速模式 400kbit/s 常用 2.2kΩ 或更低最终数值还要结合总线上的设备数和走线电容。我实测过一条 20cm 排线挂了 8 个传感器用 10kΩ 上拉400k 下的信号上升沿完全是正弦波通信时好时坏换成 1kΩ 之后波形锐利得像刀切问题立刻消失。3.2 从 START 到 STOP一个有格式的对话I2C 的帧格式比 SPI 丰满得多。所有通信从主设备发出 START 条件开始SCL 为高时SDA 由高变低。结束时发 STOP 条件SCL 为高时SDA 由低变高。这两个边沿条件在波形上非常明显也是逻辑分析仪解码 I2C 的锚点。中间的数据按字节组织每字节 8 位高位在前第 9 个时钟是 ACK 位。接收方在第 9 个时钟正常应答就把 SDA 拉低一个周期不应答NACK就保持高电平通常意味着地址没匹配、设备忙或者主机通知从机“读够了”。地址方面第一字节的高 7 位是从机地址最低位是读写标志。7 位地址能挂的设备有限规范又定义了 10 位扩展用特殊的 11110 前缀标记。实践中绝大多数从机都是 7 位地址而且很多芯片把地址的低几位做成硬件引脚靠外部接高低电平区分同一型号的多颗设备这个设计在工程上非常实用。3.3 时钟拉伸从机说“等等”的最朴素方式I2C 给从机留了一个正式的“慢下来”机制时钟拉伸。从机发现自己忙不过来可以在某个时钟周期把 SCL 拉低不放。主机有义务在发送下一拍之前检测 SCL发现被拉低了就必须暂停直到从机松开 SCL 为止。这个机制在软件模拟 I2C 主机时特别容易写错。很多人把 SCL 当成纯输出发完 9 个时钟就完事从不回读 SCL 的电平。遇到支持时钟拉伸的从机数据就开始错位——从机拖慢了一拍你这边却没等它。正确做法是在发出每个高电平阶段时把 SCL 释放为输入或者至少回读一次引脚电平确认它没被从机拉低再继续下一个时钟。我在接一款触摸控制器时被这个问题折磨过一整天它只在特定操作序列下偶尔拉伸一下时钟不处理就会随机丢字节一旦处理通信立刻稳定。3.4 软件模拟 I2C 的代码骨架// 模拟 I2C 主机7 位地址标准 100k void i2c_start(void) { SDA_H(); SCL_H(); SDA_L(); // SCL 高时 SDA 拉低 START SCL_L(); } void i2c_stop(void) { SDA_L(); SCL_H(); SDA_H(); // SCL 高时 SDA 拉高 STOP } int i2c_write_byte(uint8_t byte) { for (int i 7; i 0; i--) { if (byte (1 i)) SDA_H(); else SDA_L(); SCL_H(); // 从机在高电平期间采样 SCL_L(); } SDA_IN(); // 释放 SDA准备读 ACK SCL_H(); int ack SDA_READ(); // 0 表示从机应答 SCL_L(); SDA_OUT(); return ack ? -1 : 0; }这段代码里最容易忽略的就是SDA_IN()那一步。不释放 SDA你自己的输出会一直占着线读 ACK 时读到的永远是自己的低电平你会误以为设备没应答。我用示波器卡在这个问题上时一度怀疑器件刚上电就烧了最后才发现是方向没切。读数据方向则反过来主机释放 SDA每一个 SCL 高电平期间采样从机吐出来的位读完最后一字节前发 NACK 表示“够了”再发 STOP。4. 片内与片间的本质差异为什么不能直接沿用片上总线4.1 时钟分发模式的改变片内总线可以依赖全局时钟树靠一根到位的时钟把所有寄存器的锁存动作对齐。但到了片间你不可能给所有芯片都发一根绝对同步的全局时钟。SPI 确实这么干了——把 SCK 当作那个全局时钟但代价是这根线必须短、负载必须小、速率必须收敛。I2C 则走了另一条路时钟由主设备按需产生而且允许从机通过时钟拉伸来反向干预整个总线的时序更像一场有协商的异步会议而不是你熟悉的同步状态机。这也是为什么很多从数字逻辑入手的人第一次看 I2C 波形会觉得别扭你不能只盯着时钟沿看信号而要盯住 SDA 和 SCL 之间的边沿关系——什么时候允许变、什么时候必须稳、什么时候算条件满足。你要从“时钟沿驱动一切”切换到“边沿条件驱动状态机”。4.2 电平与驱动能力的重新思考片内信号的电压由工艺决定1.2V、1.8V 随便用反正都在同一颗 die 上。一出片情况立刻变成主控 3.3V、外设 5V、传感器可能 1.8V电平域不统一是常态。SPI 和 I2C 应对这个问题的能力差异非常大。I2C 因为是开漏结构天然可以用上拉电阻接不同电平——总线低压侧上拉到 3.3V、高压侧上拉到 5V只要两边的灌电流都能把线拉低就行。这也是工业现场大量 5V 传感器能直接挂在 3.3V 主控的 I2C 总线上的原因。SPI 是推挽输出电平转换更容易出错要么加转换芯片要么仔细核对双方输入输出的 VIH/VIL 门槛。我的实际操作建议是先拿万用表量清楚两边的逻辑高、低门槛再决定要不要加转换器。很多标称“3.3V 兼容 5V”的芯片只是输入耐压 5V输出摆幅仍按 3.3V 算看起来能跑但噪声容限已经踩线。我在一块板上就吃过亏SPI Flash 的 MISO 回灌到 5V 主控时电平刚好在门槛附近温度一高就随机读错最后加了一级缓冲器才根治。4.3 从“我们”到“你们”多设备拓扑的复杂度转移片内总线面对的从设备是固定的地址空间由硬件设计决定不存在“别人家的设备”。片间则完全不同同一根 I2C 总线上可以挂着不同厂商、不同批次、不同类型的芯片它们各自认领自己的地址。好处是走线极少坏处是软件里要做冲突排查——两个设备地址撞车是 I2C 系统里最常见的上电即死因。我就遇到过两块同型号传感器默认地址相同必须用它们的地址引脚拨到不同地址才能共存。SPI 解决多设备的方式是“片选矩阵”和“菊花链”。每个从机一根独立 CS主机想和谁说话就拉谁的 CS设备一多引脚就多但总线独占、互不干扰。菊花链则是把 MISO/MOSI 首尾相接适合移位寄存器这类天然级联的器件。如果你在给 FPGA 原型或自研 CPU 加外设我的倾向很明确需要持续吞吐的用 SPI只需要低频读写配置寄存器的用 I2C。这是“异构系统设计”最朴素也最实用的准则。5. 选型不是二选一写在硅片之外的经验5.1 一张表看懂两种协议的适用场景维度SPII2C引脚数4 N×CSN 为从机数2速度典型几十 MHz可到百 MHz100k / 400k / 1M / 3.4M bit/s双工模式全双工半双工地址机制无靠 CS 选择7 位或 10 位地址应答机制无每字节 ACK/NACK多主机困难支持有仲裁机制从机主动反馈仅靠约定时钟拉伸、NACK典型器件Flash、SD 卡、ADC、TFT 屏传感器、EEPROM、RTC、PMIC看这张表就能自行推断出各自的忠实用户群需要持续吞吐的刷屏、读固件、存日志选 SPI只需要低频读写配置项的温湿度、RTC、电池管理选 I2C。还有些器件天生就是 I2C 命比如 EEPROM 这种一字节一字节慢慢抠的存储SPI 的带宽优势根本用不上I2C 的 ACK 反而让每个字节的写入校验变得简单可靠。5.2 总线速率不是越高越好关键是时序余量不少人理所当然地认为“能开 400k 就别开 100k”但 I2C 总线不只是主从两个节点还有上拉电阻、走线寄生电容、多个设备的内部电容一起参与时序。速度翻倍SCL 高电平时间窗口减半从机的响应裕量也被压缩。我调过的板子里有一块很典型SPI Flash 跑 40MHz 读 ID 正常读大数据流时偶发 CRC 错。示波器一看MISO 边沿在 40MHz 下出现明显振铃数据建立时间不够把 SCK 降到 33MHz 问题消失。这说明数据手册的“最大频率”是器件能力上限不是链路保证值每一段走线、每一个过孔、每一颗寄生电容都在蚕食时序裕量。如果你自己写控制器我建议从 1MHz 起步跑稳后用逻辑分析仪看建立保持时间再逐步升频一次只改一个变量。5.3 上拉电阻、电气隔离和大电流外设的实际处理几个非常实用的提醒I2C 上拉电阻不是“装上就行”。高速模式建议 1k~2.2kΩ但要复核灌电流总线上所有设备在低电平时吸收的电流总和不能超过每颗芯片的规格。线越长、设备越多需要的上拉越强但强到一定程度低电平就可能压不到规范要求的 0.4V 以下。两块板之间走排线连接 SPI/I2C千万不要忘记接地线。信号的地返回路径就是那根地线缺了它回路面积变大电磁干扰和串扰直接爆表。我见过太多人只接信号线不接地结果所有高速通信都是满屏毛刺。总线负载电容超过 400pF 时规范上建议加总线缓冲器。你测不了电容也关系不大当上拉电阻压到 1k 还是看到上升沿圆头圆脑时就该改拓扑或加缓冲了。6. 调通它们的实战路线从回环到读 ID6.1 第一阶段逻辑分析仪是你的第二双眼睛调 SPI/I2C 的第一课不是写代码而是学看波形。便宜的多通道逻辑分析仪足够把所有信号线接上抓一帧波形对照数据手册的时序图逐边沿核对。我第一次调 I2C 时代码逻辑自查了好几遍都“没问题”波形一抓出来就露馅SDA 在 SCL 为低时就开始变化等 SCL 拉高再拉低 SDA 做 START起始条件根本没有形成自然一个字节都发不出去。SPI 那边也踩过类似的顺序坑——CS 必须在 SCK 之前至少半个周期拉低我却在 SCK 拉高之后才拉 CS从机压根没进选中状态。这类问题只靠读代码很难发现波形却一目了然。6.2 第二阶段先读 ID再读数据面对任何陌生器件我的调试顺序固定不变第一步永远是读它的 ID 或厂商寄存器。因为 ID 是出厂写死的固定值一旦读出来和手册一致说明通信链路、字节序、模式配置全部正确接下来才放心去读业务数据。如果 ID 都读不对就不要在业务数据上浪费时间先回头查时序。还有个细节初版驱动一律固定 8 位字节、MSB first等链路彻底调通再优化成硬件控制器或 DMA。一上来就开满 FIFO 和中断出问题的时候你连是哪一层在骗你都不知道。6.3 第三阶段写一个自检回环程序手里有 FPGA 开发板或自制 CPU 的话可以把 SPI 的 MOSI 和 MISO 在外部直接短接发一串已知数据0xAA、0x55、0xFF、0x00 交替看能否原样收回这是验证控制器本身最直接的手段。我在自己搭的 CPU 核上先跑了这个回环再接入真实器件读 ID基本能做到一次调通。I2C 同样可以在板级回环测试把 SDA 回接到一个 GPIO 模拟从机或者挂一颗支持回环测试模式的编解码芯片验证 SCL/SDA 两路的时序和电平转换。硬件调试最磨人的不是信号问题而是你根本不知道信号有问题。先把链路分层验证每一层用最小用例确认无误再去信任上一层整个过程会清晰很多。回想一下从片内走到片间收获的不只是两个协议本身而是一套更谨慎的硬件思维有限的引脚、有限的速度、有限的时序窗口每个选择都要拿别的东西去换。SPI 是拿引脚换简单I2C 是拿协议复杂度换走线。如果你也在自己动手搭 CPU 或 FPGA 原型我真心建议先把软件模拟版的 SPI/I2C 彻底调通再考虑硬件控制器——这个过程里养成的“看波形、查数据手册、分步验证”的习惯比协议本身值钱得多后面碰到 PCIe、USB 这些更复杂的串行总线时你也不会慌。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/23 3:43:49
抖店飞鸽客服自动回复系统实战:规则引擎与意图识别轻量级落地
2026/10/23 3:43:49
Windows 11 25H2官方镜像获取、启动盘制作与安装升级指南
2026/10/23 3:38:49
macOS窗口置顶工具Topit使用指南:三步上手与避坑技巧
2026/10/23 4:33:56
Plain Craft Launcher 2:为什么这款开源启动器能让你的Minecraft管理效率提升300%?
2026/10/23 4:33:56
重装 Windows 后怎么快速定制?GTweak 一键关闭遥测、更新与臃肿软件
2026/10/23 4:33:56
Plain Craft Launcher 2:5个颠覆性功能如何重塑你的Minecraft游戏体验
2026/10/23 4:33:56
如何快速掌握Plain Craft Launcher 2:新手完整入门指南
2026/10/23 4:33:56
Java集合类size()方法详解与优化实践
2026/10/23 4:28:55
Android城市选择器:省市区三级联动、拼音索引与定位降级实践
2026/10/23 0:02:44
Verilog实现FPGA密码锁:矩阵键盘与状态机设计实战
2026/10/23 0:02:44
数据服务监控运维实战:从指标体系到告警治理与故障排查
2026/10/23 0:02:44
FreeRTOS任务通知实战详解:内核原理、API与避坑指南
2026/10/23 0:52:23
OpenResearch实战:Claude Code、Codex、OpenCode、Cursor 工作流搭建与故障排查
2026/10/22 17:15:21
知识管理核弹:用Obsidian构建第二大脑的工程化方案(TaoToken 统一 Key 接入版)
2026/10/23 0:52:11
金融AI落地指南:从调研报告到技术选型与工程实践
2026/10/22 5:06:15
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/22 2:07:18
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/21 19:01:08
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)