2026/10/12 2:58:33

SPI与I2C片间通信实战:电气层、协议层与驱动层全解析

SPI与I2C片间通信实战:电气层、协议层与驱动层全解析 1. 从片内总线到片间总线的思维跃迁1.1 为什么片内通信的经验不能直接照搬到片间搞嵌入式的人大多有个惯性思维在芯片内部我们习惯了AHB、APB这类并行总线地址线和数据线一字排开想读哪个寄存器直接给地址就行时序由硬件保证开发者几乎不用操心对方在不在对方忙不忙这类问题。可一旦信号要跨出芯片边界走到两颗芯片之间的PCB走线上这套思路就彻底行不通了。原因很直接引脚数量就是成本。一颗MCU封装上多引出一组8位并行数据线意味着多8个焊盘、多8根走线、多8个可能的干扰源。对于传感器、EEPROM、小容量Flash这类外设它们传输的数据量本来就不大用并行总线纯属浪费。于是串行总线成了片间通信的主流选择而SPI和I2C就是其中应用最广的两条路线。我在早期做项目时踩过一个典型的坑把片内访问寄存器的思路直接套到I2C上写完地址就立刻去读数据结果读回来全是0xFF。后来才明白片间通信多了一层协议握手——你得先发起始条件、再发设备地址、等应答、再发寄存器地址、再等应答、再重启、再发读命令……每一步都要等对方回应。这个思维转变是从片内走向片间的第一道门槛。1.2 SPI和I2C到底解决了什么问题把这两条总线放在一起看它们解决的核心问题其实是同一个用最少的引脚实现两颗芯片之间可靠的数据交换。但两者的取舍方向完全不同。SPI的思路是用线换速度。它用四根线SCLK、MOSI、MISO、CS换来全双工、无寻址开销的高速传输时钟可以跑到几十MHz甚至上百MHz。它不管总线上挂几个设备每个设备独占一根片选线通信时直接点对点协议简单到几乎没有协议——本质上就是一个移位寄存器在时钟驱动下循环移位。I2C的思路是用协议换引脚。它只用两根线SCL、SDA靠7位或10位地址区分总线上最多上百个设备还支持多主多从。代价是协议复杂、速度受限标准模式100kHz快速模式400kHz高速模式3.4MHz而且每次通信都要经历起始、寻址、应答、数据传输、停止这一整套流程。选择哪条路线取决于你的场景。读一个陀螺仪的6字节数据I2C足够刷一块240x320的TFT屏SPI才是正解。这个判断逻辑后面我会展开讲。1.3 这篇内容适合谁看如果你正在做嵌入式开发手上有传感器、存储器、显示屏这类外设要接或者你被I2C的总线锁死、SPI的时序对不上折腾过那这篇内容就是写给你的。我会从电气层、协议层、驱动层三个维度把这两条总线拆开讲配上可以直接抄的参数计算和排查方法。不需要你有很深的数字电路背景但至少要能看懂上拉电阻时钟极性这类基础概念。如果你连GPIO都没点过灯建议先补一下最基础的IO操作再来。2. 电气层那些决定通信成败的硬件细节2.1 SPI的四线制与推挽输出SPI的物理层非常直男。四根线里SCLK、MOSI、CS都是主机单向输出MISO是从机单向输出每一根都由推挽驱动器直接驱动高电平就是高电平低电平就是低电平没有中间态。这种推挽结构带来的好处是边沿陡峭、驱动能力强能跑到很高的频率。但代价是同一时刻只能有一个设备驱动一根线。这就是为什么SPI必须用片选主机拉低哪个CS哪个从机才被允许驱动MISO线其他从机的MISO必须处于高阻态否则两个从机同时驱动MISO就会打架轻则数据错乱重则烧毁引脚。我见过一个新手把两个SPI从机的MISO直接并联CS都拉低结果读出来的数据是两路信号的线与结果怎么调都不对。记住SPI的MISO线在同一时刻只能有一个主人。关于上拉电阻SPI通常不需要外部上拉。因为推挽输出本身就能拉高拉低加不加外部上拉对信号质量影响不大。但在长走线或高干扰环境下可以在SCLK上串一个22到33欧姆的电阻做阻抗匹配抑制过冲和振铃。这个电阻值不是随便定的它要和走线的特征阻抗匹配一般PCB走线特征阻抗在50欧姆左右串阻加上驱动器内阻接近这个值效果最好。2.2 I2C的开漏结构与上拉电阻计算I2C的物理层是另一套哲学。SCL和SDA都是开漏Open-Drain输出器件只能把线拉低不能主动拉高。线要变高全靠外部上拉电阻把电平拽上去。为什么这么设计因为开漏结构天然支持线与逻辑只要有一个器件拉低整条线就是低只有所有器件都释放线才被上拉电阻拉高。这正好实现了多主多从的仲裁机制——两个主机同时发数据谁先发出低电平谁就赢另一个自动退出。这是I2C能支持多主机的物理基础。代价是上升沿变缓。开漏输出拉低很快晶体管导通但拉高要靠上拉电阻给总线电容充电这是一个RC充电过程。上升时间t_r约等于0.8473乘以R乘以C这里的C是总线总电容包括引脚电容、走线电容、器件电容。I2C规范对上升时间有明确要求标准模式1000ns快速模式300ns高速模式120ns。上拉电阻怎么算给你一个实际例子。假设总线电容C200pF要满足快速模式400kHz的上升时间要求300ns以内那么R_max t_r / (0.8473 × C) 300ns / (0.8473 × 200pF) ≈ 1770欧姆所以上拉电阻不能超过约1.77kΩ。但电阻也不能太小否则器件拉低时灌电流过大。I2C规范规定标准模式和快速模式下器件拉低时灌电流最大3mAVOL最大0.4V。假设VDD3.3V那么R_min (VDD - VOL) / IOL (3.3 - 0.4) / 3mA ≈ 967欧姆综合下来这个场景下上拉电阻取1kΩ到1.7kΩ之间比较合适。实际工程中400kHz的I2C总线常用2.2kΩ到4.7kΩ100kHz的常用4.7kΩ到10kΩ。如果总线上挂了很多器件、走线又长电容可能到400pF那上拉电阻就得降到1kΩ以下这时候要检查灌电流是否超标。注意很多人调试I2C时遇到通信不稳定第一反应是换更小的上拉电阻觉得拉得更快。但如果电阻太小导致灌电流超过器件的IOL上限长期工作会损伤引脚。正确做法是先测总线电容再按公式算而不是盲目试。2.3 电平匹配与总线电容的隐形陷阱片间通信还有一个绕不开的问题电平不匹配。主控是3.3V外设是5V直接连会怎样如果外设是开漏的I2C器件5V器件的上拉电阻拉到5V3.3V主控的引脚可能承受不住5V电压。这时候需要一个电平转换器或者用最简单的方案把上拉电阻接到3.3V让总线高电平就是3.3V。因为I2C的高电平阈值通常是0.7×VDD5V器件的阈值是3.5V3.3V可能不够所以这个方案要看具体器件的VIH参数。SPI的电平匹配更麻烦因为它是推挽输出3.3V主控输出高电平就是3.3V5V从机可能识别不了。这时候要么用专门的level shifter要么在MOSI线上做分压要么干脆选同电压的器件。我个人的经验是能选同电压就选同电压电平转换芯片会增加成本、占用板面、引入额外延迟还多一个故障点。总线电容是另一个隐形杀手。I2C规范规定总线电容不能超过400pF。这个电容来自哪里每个器件的引脚电容典型10pF、PCB走线电容约1pF每厘米、连接器电容。挂10个器件就是100pF走线20厘米又是20pF再加上杂散电容很容易逼近400pF。电容一大上升沿就慢通信就出错。所以I2C总线上不是想挂多少就挂多少长走线场景要特别小心。3. 协议层时序、模式与那些容易搞错的参数3.1 SPI的四种模式CPOL和CPHA的组合SPI协议里最容易让人迷糊的就是**时钟极性CPOL和时钟相位CPHA**这两个参数。它们组合出四种模式从机 datasheet 里会明确写支持Mode 0或Mode 3如果你配错了读出来的数据就是错位的。先说CPOL它定义时钟空闲时的电平。CPOL0表示空闲时SCLK为低CPOL1表示空闲时为高。再说CPHA它定义数据在哪个边沿采样。CPHA0表示在第一个边沿即从空闲电平跳变的那个边沿采样CPHA1表示在第二个边沿采样。组合起来模式CPOLCPHA空闲电平采样边沿移位边沿Mode 000低上升沿下降沿Mode 101低下降沿上升沿Mode 210高下降沿上升沿Mode 311高上升沿下降沿怎么记我的口诀是CPOL决定空闲在哪CPHA决定第几个边沿采样。Mode 0和Mode 3最常用很多Flash和传感器默认Mode 0。如果你发现读回来的数据整体偏移了一位八成是CPHA配反了如果数据完全乱套可能是CPOL错了。实操心得调试SPI时先用示波器或逻辑分析仪抓一下SCLK和MOSI的波形确认空闲电平和采样边沿再对照从机datasheet配置。不要凭感觉猜猜错的概率是四分之三。3.2 I2C的起始、应答与时钟拉伸I2C的协议流程比SPI复杂得多核心在于它有一套完整的对话规则。一次典型的I2C写操作是这样的主机发出起始条件SCL高时SDA由高变低然后发7位从机地址加1位读写位0表示写从机如果在线就拉低SDA一个时钟周期作为应答ACK主机收到ACK后继续发寄存器地址从机再ACK然后发数据从机再ACK最后主机发停止条件SCL高时SDA由低变高。读操作稍微绕一点主机先发起始、发地址加写位、等ACK、发寄存器地址、等ACK然后发重复起始条件Repeated Start再发地址加读位、等ACK然后从机开始发数据主机每收一个字节要发ACK表示还要继续读或NACK表示读完了最后发停止条件。这里有个新手常犯的错误读操作时忘了发重复起始直接发停止再重新起始。虽然有些器件能容忍但标准做法是用重复起始因为停止再起始会让总线短暂空闲可能被其他主机抢走控制权。**时钟拉伸Clock Stretching**是I2C的一个特色机制。从机如果处理不过来可以在ACK阶段把SCL线拉低强制主机等待。主机必须检测SCL是否真的变高了如果没变高就继续等。这个机制对从机很友好但对主机驱动有要求——如果你的GPIO模拟I2C没有检测SCL实际电平遇到会拉伸时钟的从机就会出错。3.3 速率选择不是越快越好SPI的速率选择相对自由从机datasheet会给出最大时钟频率你只要不超过就行。但实际选速率时要考虑走线长度和信号完整性。10MHz以上建议走线尽量短必要时加串阻50MHz以上基本要考虑传输线效应了。I2C的速率是分档的标准模式100kHz、快速模式400kHz、快速模式 1MHz、高速模式3.4MHz。选哪一档要看总线上最慢的器件支持到哪。如果总线上有个只支持100kHz的老器件整条总线就只能跑100kHz不能单独给某个器件提速。我个人的经验是I2C能用400kHz就别用100kHz能省不少时间。比如读一个9轴传感器14个字节的数据100kHz下光传输就要约1.4ms400kHz下只要350us。对于需要高频采样的场景这个差距很关键。但提速的前提是上拉电阻和总线电容都满足要求否则提速反而导致误码。4. 驱动层从裸机时序到中断与DMA4.1 GPIO模拟SPI最灵活也最考验功力很多MCU的硬件SPI外设数量有限或者引脚被其他功能占用了这时候就得用GPIO模拟SPI俗称软件SPI。它的好处是引脚随便选、时序随便调坏处是速度上不去而且占用CPU。软件SPI的核心就是一个循环按Mode 0的时序写大概是这样的uint8_t soft_spi_transfer(uint8_t data) { uint8_t recv 0; for (int i 7; i 0; i--) { // CPOL0空闲低先准备数据 if (data (1 i)) MOSI_HIGH(); else MOSI_LOW(); // 上升沿采样 SCLK_HIGH(); delay_short(); if (MISO_READ()) recv | (1 i); // 下降沿移位 SCLK_LOW(); delay_short(); } return recv; }这段代码看起来简单但有几个坑。第一delay_short()的长度决定了最高速率太短了从机反应不过来太长了速度慢。第二编译器的优化可能把延时循环优化掉要用volatile或者内联汇编保证时序。第三中断会打断时序如果系统里有高优先级中断软件SPI可能出错必要时要在传输期间关中断。实操心得软件SPI的速率一般能到1到5MHz再高就不稳定了。如果只是读个传感器够用如果要刷屏还是老老实实用硬件SPI。4.2 硬件SPI的配置要点与DMA加持硬件SPI外设帮你处理了时序你只需要配置好参数、往数据寄存器写数据就行。配置时要注意几个点第一时钟分频。SPI时钟来自总线时钟通过分频器得到SCLK。比如总线时钟72MHz分频8就是9MHz。要确保不超过从机最大频率同时考虑信号完整性。第二数据帧格式。大多数器件是8位帧但有些ADC或显示屏是16位帧。还有MSB先行还是LSB先行绝大多数是MSB先行但个别器件是LSB。第三片选管理。硬件SPI的CS可以由硬件自动控制也可以软件控制GPIO。硬件自动CS在传输间隙会释放有些从机要求CS在整个传输期间保持低电平这时候就得用软件CS。当传输大量数据时DMA是救星。比如刷一块TFT屏一帧数据几百KB如果用CPU轮询发送CPU就被占死了。用DMA把数据从内存搬到SPI数据寄存器CPU可以去做别的事传输完成再中断通知。配置DMA时要注意数据宽度匹配8位还是16位、传输方向内存到外设、以及传输完成中断的处理。4.3 I2C的中断与状态机设计I2C的驱动比SPI复杂因为它的协议流程有多个阶段每个阶段都要等应答。用轮询方式写I2C代码会写成一长串发起始、等标志、发地址、等标志、发数据、等标志可读性差还容易卡死。更好的方式是用状态机加中断。把一次I2C传输拆成若干状态起始、发地址、等地址ACK、发数据、等数据ACK、停止。每个状态在中断里推进主循环只管发起传输和检查完成标志。这样即使某个从机没应答也不会死等可以超时退出。I2C最容易出的问题是总线锁死。什么情况会锁死如果主机在发数据时被复位而SCL恰好是低电平从机可能认为传输还在进行一直拉着SDA等时钟。这时候总线就卡住了主机再发起始也没用。解锁的方法是在SCL上手动发9个时钟脉冲让从机把剩余的数据位发完然后发一个停止条件。具体操作是把SCL配成GPIO输出手动翻转9次再把SDA配成输出发停止条件最后重新初始化I2C外设。这个技巧我救过好几次现场值得记下来。5. 实战选型与常见问题排查5.1 SPI还是I2C一张表帮你决策维度SPII2C引脚数4根含CS每多一个从机多一根CS2根从机数量不影响引脚速率高可达几十MHz低标准100k/快速400k双工全双工半双工寻址靠CS硬件寻址靠地址软件寻址多主不支持支持协议复杂度极简较复杂适用场景高速外设Flash、TFT、ADC低速多设备传感器、EEPROM、RTC选型逻辑很简单要速度选SPI要省引脚选I2C。如果总线上设备多、每个设备数据量小I2C的两根线优势明显如果只有一个高速设备SPI的四根线换来的是几倍甚至几十倍的速率。5.2 通信失败排查速查表现象可能原因排查方法完全无响应接线错误、供电缺失、CS/地址错万用表测通断示波器看波形数据全0xFFMISO未接、从机未选中、上拉缺失检查MISO连接和CS电平数据全0x00从机未供电、时钟未输出测从机VDD和SCLK数据错位CPOL/CPHA配错、MSB/LSB反对照datasheet改模式偶发误码上拉电阻不当、总线电容大、干扰算上拉、缩短走线、加屏蔽I2C总线锁死主机复位时SCL为低手动发9个时钟解锁时钟拉伸超时主机未检测SCL实际电平驱动里加SCL电平检测5.3 几个我踩过的坑和独家技巧坑一SPI的CS在传输间隙被拉高。有些硬件SPI在两次传输之间会自动释放CS但某些Flash要求CS在整个命令序列期间保持低电平。解决办法是用软件GPIO控制CS在整段传输前后手动拉低拉高。坑二I2C地址的7位和8位混淆。datasheet上写的地址有时是7位如0x68有时是8位含读写位如0xD0。写驱动时如果搞混怎么都通信不上。记住7位地址左移一位加读写位才是总线上实际发送的字节。坑三SPI的MISO在高阻态时浮空。如果从机没被选中MISO是高阻主机读到的就是随机值。有些驱动会误判为有效数据。解决办法是给MISO加一个弱上拉如100kΩ让空闲时读到确定的高电平。技巧一用逻辑分析仪抓一次完整通信。不管是SPI还是I2C抓一次波形胜过看十遍datasheet。逻辑分析仪能直接解码出地址、数据、ACK哪里出错一目了然。这是我最推荐的调试手段。技巧二I2C总线上每个器件地址列个表。项目里挂多个I2C器件时把每个器件的7位地址、支持的速率、是否需要时钟拉伸列成表格贴在工位上调试时对照能省很多翻datasheet的时间。技巧三SPI速率逐步往上试。不要一上来就设最高速率从1MHz开始通了再往上加找到稳定工作的上限。这样能快速定位是速率问题还是其他问题。6. 从两条总线看片间通信的设计哲学6.1 简单与复杂的权衡SPI和I2C代表了两种截然不同的设计哲学。SPI选择了硬件简单、协议极简把复杂度留给了系统设计者——你得自己管理片选、自己处理多从机的MISO冲突。I2C选择了引脚极简、协议完善把复杂度封装在协议里——寻址、仲裁、应答、时钟拉伸都是协议的一部分。这两种哲学没有优劣只有适配。当你需要极致速度时SPI的简单就是优势当你需要挂一堆低速设备时I2C的协议完善就是优势。理解这一点比记住具体参数更重要。6.2 片间通信的通用原则不管是SPI、I2C还是其他片间总线有几条原则是通用的。第一先确认电气层再调协议层。很多人一上来就改驱动代码其实问题可能出在上拉电阻或电平匹配上。用示波器看波形先确认电平对不对、边沿干不干净再去看协议。第二时序参数要留余量。datasheet给的是极限值实际设计要留20%到30%的余量。比如从机最大支持10MHz你就跑8MHz上升时间要求300ns你设计到200ns。第三总线上设备越少越可靠。每多一个设备就多一份电容、多一个潜在故障点。能用点对点就不用总线能少挂一个就少挂一个。6.3 后续可以延伸的方向把SPI和I2C吃透之后往深了走有几个方向。一是高速差分总线当速率到几百MHz、走线到几十厘米时单端信号就不行了得用差分传输。二是带CRC校验的可靠通信在工业环境下总线上有干扰光靠协议本身的ACK不够需要加CRC和重传机制。三是多主仲裁的深入理解I2C的多主仲裁机制设计得很巧妙理解它对设计其他多节点系统很有启发。我个人在实际操作中的体会是片间通信的调试时间往往比写驱动代码的时间还长而大部分问题都出在电气层和时序配置上真正协议逻辑的bug反而少。所以每次新接一个外设我都会先用逻辑分析仪抓一次通信确认物理层没问题再开始写驱动。这个习惯帮我省下了大量返工时间。