☰
SPI、I2C、UART串行总线选型指南:从物理层到实战调试
2026/10/18 17:16:56 网站建设 项目流程

1. 三种串行总线的本质差异:从物理层说起

很多刚接触嵌入式开发的朋友,第一次看到SPI、UART、I2C这三个词的时候,脑子里基本是一团浆糊。它们都是串行通信,都能传数据,那到底什么时候用哪个?我见过不少人做项目时随手选一个,结果调试到怀疑人生——要么速度上不去,要么设备挂多了地址冲突,要么通信距离稍微拉长就丢包。其实这三个协议各有各的脾气,选对了事半功倍,选错了就是给自己挖坑。

先把最核心的结论摆出来:SPI拼的是速度,I2C拼的是引脚效率,UART拼的是简单和通用。这三句话你记住了,后面所有的细节都是围绕这个核心展开的。

从物理层来看,三者的信号线结构完全不同。UART最少只需要两根线:TX和RX,一发一收,全双工。SPI至少需要四根线:SCLK、MOSI、MISO、CS,也是全双工,但每多挂一个从设备就要多一根CS片选线。I2C只需要两根线:SCL和SDA,半双工,但理论上可以挂一百多个设备,靠地址区分。

这里有个容易被忽略的点:UART和SPI的全双工是物理上独立的收发通道,而I2C的半双工意味着同一时刻只能收或者发。这个差异直接影响了它们的应用场景。比如你要做一个音频数据采集,麦克风数据要实时传给主控,同时主控还要给麦克风发配置命令,那SPI或者UART就更合适。但如果只是偶尔读一下温度传感器的值,I2C两根线就够了,省下来的引脚可以干别的事。

再来说电气特性。UART通常是异步通信,没有时钟线,收发双方靠预先约定的波特率来同步。这就意味着波特率的精度直接影响通信可靠性。你用内部RC振荡器做时钟源,波特率误差可能到3%以上,通信就会间歇性出错。SPI和I2C都是同步通信,有时钟线,接收方跟着时钟采样,对时钟精度的容忍度高得多。这也是为什么SPI能轻松跑到几十兆赫兹,而UART一般到几兆就顶天了。

还有一个关键差异是电平标准。UART最常见的是TTL电平(3.3V或5V),也有RS-232、RS-485这种差分或高压标准,适合长距离传输。I2C和SPI基本都是板级总线,TTL电平,传输距离通常不超过几十厘米。你要是想用I2C拉一根两米长的线去接传感器,大概率会失败,除非加缓冲器或者降低速率。

我刚开始做项目的时候,觉得I2C最省线,什么都想往上挂。后来发现I2C的总线电容有严格限制,标准模式最大400pF,快速模式也差不多。每挂一个设备,引脚电容和走线电容都会累加,挂到五六个设备以后,波形就明显变形了,上升沿变缓,通信开始随机出错。这时候要么加I2C缓冲器,要么就得换方案。SPI就没这个问题,每个设备独立片选,总线负载轻得多。

所以选型的第一步,不是看哪个协议“高级”,而是看你的引脚预算、速度需求、设备数量、通信距离这四个硬约束。把这四个想清楚了,答案基本就出来了。

2. SPI的实战逻辑:为什么它快,以及快的代价

SPI能跑得快,根本原因在于它的协议极其简单。没有地址帧,没有起始位停止位,没有应答位,主设备拉低片选,时钟一打,数据就从MOSI出去、从MISO进来。整个过程就是移位寄存器的操作,硬件实现非常直接。我实测过,在STM32F4上SPI时钟跑到42MHz,配合DMA传输,连续传几兆字节的数据几乎不占CPU。这种效率是I2C和UART很难做到的。

但SPI的简单是有代价的。第一个代价是引脚数量。每增加一个从设备,就要多一根CS线。你要挂八个SPI从设备,加上SCLK、MOSI、MISO,总共就是十一根线。如果主控引脚紧张,这就很要命。有些方案用译码器来扩展片选,比如3-8译码器,用三根GPIO控制八根CS,能省一些引脚,但增加了硬件复杂度。

第二个代价是没有硬件应答机制。SPI主设备根本不知道从设备有没有收到数据,也不知道从设备是不是还活着。你发出去的数据就像扔进黑洞,有没有响应全靠从设备自己的行为来判断。所以SPI协议里经常需要软件层做校验,比如读一个设备ID寄存器,或者用CRC校验数据。我见过一个项目,SPI Flash偶尔写入失败,查了半天发现是片选时序不对,从设备还没准备好主设备就开始打时钟了。后来在CS拉低和第一个时钟沿之间加了几个微秒的延时,问题就解决了。

第三个代价是缺乏标准。SPI有四种模式,由CPOL和CPHA组合决定。CPOL决定时钟空闲时是高还是低,CPHA决定数据在第一个还是第二个时钟沿采样。这四个模式没有统一规定,每个从设备的数据手册都会告诉你它支持哪种模式。我踩过的最大的坑就是:主设备配了模式0,从设备要求模式3,结果数据完全错位,读出来的全是0xFF。所以拿到一个SPI设备,第一件事就是翻数据手册确认模式,然后用逻辑分析仪抓波形验证。

SPI的典型应用场景很明确:需要高速数据传输、从设备数量不多、板内通信。比如SPI Flash、SPI屏幕、SD卡(SPI模式)、高速ADC、无线模块。这些场景要么数据量大,要么实时性要求高,I2C和UART都扛不住。

在实际接线时,有几个细节值得注意。MISO线在主设备端通常需要上拉或下拉,因为当片选无效时,从设备的MISO引脚是高阻态,如果不处理,主设备的MISO输入会浮空,可能导致漏电流或者误触发。很多MCU内部有可配置的上拉,但外部再加一个10kΩ的上拉更稳妥。另外,SPI的走线尽量等长,尤其是时钟频率高的时候,SCLK和MOSI/MISO的长度差异会导致时序偏移。我在一个四层板上做过测试,SCLK和MISO长度差超过两厘米,在30MHz以上就开始出现偶发误码。

还有一个经验:SPI的片选信号最好用硬件控制,不要用软件GPIO模拟。软件控制片选的话,在片选切换的间隙,时钟线可能会有毛刺,某些敏感的从设备会误识别为有效时钟。用硬件SPI外设的NSS引脚,或者至少用GPIO但保证切换时SCLK稳定。

提示:调试SPI通信时,逻辑分析仪比示波器更好用。示波器只能看一两路信号,逻辑分析仪可以同时抓SCLK、MOSI、MISO、CS四路,直接解码出数据字节,效率高很多。

3. I2C的地址机制与总线仲裁:两根线背后的复杂世界

I2C用两根线挂一堆设备,听起来很优雅,但背后的机制比SPI复杂得多。核心在于地址机制和总线仲裁,这两点决定了I2C的灵活性和局限性。

每个I2C从设备都有一个7位地址,理论上可以挂128个设备。但实际可用的地址空间要扣除保留地址,比如0x00是通用呼叫地址,0x01到0x07是CBUS兼容地址,0x78到0x7F是10位地址的前缀。所以真正能用的7位地址大概有112个。这看起来很多,但问题是很多传感器的地址是固定的,或者只有两三个可选值。比如某个温度传感器地址是0x48,另一个ADC地址也是0x48,你就没法把它们挂在同一条I2C总线上。这时候要么用I2C多路复用器,要么换一个地址不同的器件。

地址冲突是I2C项目中最常见的问题之一。我做过一个环境监测板,上面挂了温度、湿度、气压三个传感器,结果发现温度和气压的地址都是0x76。解决办法是查数据手册,看有没有地址配置引脚。有些器件提供一个ADDR引脚,接GND是一个地址,接VCC是另一个地址。如果两个器件都没有可配置地址,那就只能分两条I2C总线,或者用多路复用器。

总线仲裁是I2C的另一个核心机制。当多个主设备同时想占用总线时,I2C通过线与逻辑来实现仲裁。SDA和SCL都是开漏输出,任何设备拉低都会把线拉低。主设备在发送数据的同时会回读SDA线,如果发现自己发的是1但读回来是0,说明有另一个主设备在拉低,自己就退出仲裁。这个机制保证了多主设备场景下的数据完整性,但也意味着I2C的速率受限于最慢的设备。如果总线上有一个只支持100kHz的器件,那整条总线就只能跑100kHz。

I2C的速率等级分为标准模式100kHz、快速模式400kHz、快速模式+ 1MHz、高速模式3.4MHz。但实际能跑多快,取决于总线电容和上拉电阻。上拉电阻的选择是个技术活:阻值太小,功耗大,低电平灌电流可能超过器件的驱动能力;阻值太大,上升沿变缓,高速通信时波形还没恢复到高电平就被拉低了。标准做法是根据总线电容来计算,公式是R(max) = tr / (0.8473 × C),其中tr是允许的最大上升时间。比如总线电容200pF,快速模式要求上升时间不超过300ns,那上拉电阻最大约1.7kΩ。实际选1.5kΩ到2.2kΩ比较常见。

I2C还有一个让人头疼的问题是时钟拉伸。从设备如果处理不过来,可以把SCL线拉低,强制主设备等待。这个机制本身是好的,但有些主设备的硬件I2C外设不支持时钟拉伸,或者支持得不好,就会导致通信失败。我在一个项目里用某款MCU的硬件I2C读EEPROM,偶尔会卡死,后来发现是EEPROM在写入周期内会拉伸时钟,而那个MCU的I2C外设没有正确处理。换成软件模拟I2C,问题就消失了。所以如果你的I2C总线上有会拉伸时钟的器件,一定要确认主设备是否支持。

I2C的典型应用场景是低速传感器、EEPROM、RTC、IO扩展芯片。这些设备数据量不大,但对引脚数量敏感。比如一个温湿度传感器,每分钟读一次,I2C两根线完全够用,换成SPI就要四根线,浪费。

注意:I2C总线上所有设备的地必须共地,否则电平参考不一致,通信必然失败。另外,SDA和SCL的走线尽量短,如果必须走长线,考虑用I2C缓冲器或者降低速率。

4. UART的异步世界:没有时钟线怎么保证数据正确

UART是三种协议里最古老、最简单、也最通用的。它没有时钟线,收发双方靠约定的波特率来同步。这听起来很不靠谱,但实际上只要双方时钟精度足够,通信非常稳定。UART的核心在于起始位和停止位的帧结构:空闲时线是高电平,发送方拉低一个位时间作为起始位,接收方检测到这个下降沿就开始按波特率采样数据位,最后用停止位恢复高电平。

波特率的误差容忍度是UART设计的关键参数。一般来说,总误差不能超过±2%到±3%,否则接收方会在错误的位置采样,导致误码。误差来源有两部分:发送方和接收方的时钟偏差。如果双方都用晶振,精度可以做到±20ppm,几乎可以忽略。但如果一方用内部RC振荡器,精度可能只有±1%到±2%,再加上波特率分频的取整误差,总误差就可能超标。我见过一个案例,某款低成本MCU用内部RC跑115200波特率,常温下通信正常,一到冬天就频繁出错。后来测了一下,低温下RC频率漂移了3%,超过了容忍范围。换成外部晶振就解决了。

UART的帧结构可以配置:数据位5到9位,停止位1或2位,校验位无、奇、偶、标记、空格。最常用的配置是8N1:8位数据、无校验、1位停止位。一个字节的传输需要10个位时间(1起始+8数据+1停止),所以115200波特率下,每秒最多传11520字节。这个速率对于大多数控制命令和调试信息足够了,但传大量数据就吃力了。

UART的硬件流控是很多人忽略的功能。RTS和CTS两根线用来控制数据流,防止接收方缓冲区溢出。当你用UART传大量数据时,如果没有流控,接收方处理不过来就会丢数据。我在一个项目里用UART传图像数据,开始没接流控线,结果每隔几百字节就丢一包。后来把RTS和CTS接上,让接收方在缓冲区快满时拉高RTS,发送方暂停,问题就解决了。当然,流控需要双方都支持,而且多两根线。

UART的最大优势是通用性。几乎所有的MCU都有UART,PC有串口(或者USB转串口),很多模块默认就是UART接口。调试信息输出、AT命令控制、GPS模块、蓝牙模块,这些都是UART的天下。而且UART的协议简单,用GPIO模拟也容易,不像SPI和I2C对时序要求那么严格。

但UART的局限性也很明显。第一是点对点,一条UART只能连两个设备,不能像I2C那样挂一堆。第二是速率上限,虽然理论上可以跑到几兆,但实际受限于线缆质量和时钟精度,一般115200到921600是常见范围。第三是没有地址机制,多设备场景需要额外的硬件切换或者多路UART。

UART的接线有个基本原则:TX接RX,RX接TX,地接地。这个看起来简单,但我见过太多人把TX接TX,然后纳闷为什么收不到数据。另外,如果两个设备的电平标准不同,比如一个是3.3V一个是5V,需要电平转换,否则可能损坏3.3V器件。最简单的电平转换用一个分压电阻或者专用电平转换芯片。

提示:调试UART时,先确认波特率、数据位、停止位、校验位是否匹配。如果收到乱码,大概率是波特率不对。如果完全没数据,检查TX/RX是否接反,地是否共地。

5. 选型决策树:拿到需求后怎么快速锁定协议

前面把三个协议的脾气都摸了一遍,现在回到最实际的问题:拿到一个具体需求,怎么快速决定用哪个。我总结了一个决策流程,基本能覆盖大多数场景。

第一步,看设备数量和引脚预算。如果总线上要挂三个以上的设备,而且引脚紧张,优先考虑I2C。如果只挂一两个设备,SPI和UART都可以。如果设备数量多但速度要求高,可以考虑SPI加译码器扩展片选,或者用多个SPI总线。

第二步,看速度需求。需要传图像、音频、大量传感器数据,选SPI。只需要传控制命令、偶尔读传感器值,I2C或UART都行。这里有个经验值:超过1Mbps的持续传输,基本只能选SPI。UART在921600以上对时钟精度要求很高,I2C快速模式才400kHz,快速模式+也才1MHz,而且实际很难跑满。

第三步,看通信距离。板内通信,三个都可以。超过30厘米,UART加RS-485或者RS-232更合适。I2C和SPI都不适合长距离,除非加缓冲器或者降低速率。

第四步,看设备是否已有接口。很多传感器和模块出厂就固定了接口,比如温湿度传感器大多是I2C,SPI Flash只有SPI,GPS模块只有UART。这种情况下没得选,只能适配。

第五步,看主设备资源。有些MCU的SPI外设多,有些I2C外设多,有些UART多。如果硬件资源紧张,可能需要用GPIO模拟。软件模拟SPI和I2C都比较容易,UART稍微麻烦一点,因为对时序要求更严格。

下面这个表格可以帮你快速对照:

维度SPII2CUART
线数4+CS22
全双工是否是
最大速率几十MHz3.4MHz几MHz
设备数量每设备一根CS理论上128点对点
通信距离板内板内可达数十米
硬件复杂度低中低
协议开销极低中低
典型场景Flash、屏幕、高速ADC传感器、EEPROM调试、模块、GPS

在实际项目中,经常是三种协议混用。比如一个数据采集板,用SPI读高速ADC,用I2C读温度传感器,用UART输出调试信息和连接无线模块。这种组合很常见,关键是每种协议用在它最擅长的地方。

我个人的经验是:不要为了省引脚而牺牲性能,也不要为了性能而浪费引脚。先满足功能需求,再优化成本和复杂度。有时候多花几毛钱用一个引脚更多的MCU,比花几天时间调试I2C地址冲突要划算得多。

6. 调试实战:逻辑分析仪抓波形与常见故障定位

理论说再多,不如实际抓一次波形。调试这三种协议,逻辑分析仪是必备工具。它比示波器便宜,通道多,而且自带协议解码功能,能直接把波形翻译成字节。我用的是一个八通道的逻辑分析仪,配合开源软件,抓SPI、I2C、UART都没问题。

SPI调试的典型流程:先把逻辑分析仪的通道接到SCLK、MOSI、MISO、CS上,设置SPI解码器,配置正确的CPOL和CPHA。然后触发一次传输,看解码出来的数据对不对。如果数据错位,检查CPOL/CPHA设置。如果完全没有数据,检查CS是否拉低,SCLK是否有波形。如果MISO一直是高阻态,检查从设备是否供电正常、片选是否接对。

我遇到过一个SPI屏幕不亮的问题,抓波形发现SCLK和MOSI都有信号,但CS一直是高电平。查原理图发现CS接到了一个被复用的引脚上,而那个引脚默认功能不是GPIO。配置成GPIO输出后,屏幕就正常了。这种问题看代码很难发现,抓波形一眼就能定位。

I2C调试的典型流程:逻辑分析仪接SCL和SDA,设置I2C解码器。先看有没有起始条件(SCL高时SDA下降沿),然后看地址字节是否得到应答(第9个时钟SDA被从设备拉低)。如果地址没有应答,说明从设备没响应,检查地址是否正确、供电是否正常、上拉电阻是否接好。如果有应答但数据错误,检查寄存器地址和数据是否符合预期。

I2C最常见的故障是上拉电阻缺失或阻值不当。有些开发板自带上拉,有些没有。如果你直接连一个裸传感器,没有上拉电阻,SDA和SCL永远是低电平,通信完全无法进行。我刚开始就犯过这个错误,以为I2C和UART一样直接连就行,结果折腾了半天才发现要加上拉。标准做法是SDA和SCL各接一个4.7kΩ上拉到VCC,如果总线电容大或者速率高,适当减小到2.2kΩ或1.5kΩ。

UART调试的典型流程:逻辑分析仪接TX和RX,设置UART解码器,配置正确的波特率、数据位、停止位、校验位。然后看解码出来的字节。如果全是乱码,检查波特率。如果收到部分正确部分错误,检查时钟精度和线缆质量。如果完全没数据,检查TX/RX是否接反。

UART有一个隐蔽的坑:某些MCU的UART引脚在复位后默认是GPIO,需要软件配置成UART功能。如果你忘了配置,TX引脚一直是高阻态,发不出数据。这个在代码里很容易漏掉,尤其是用寄存器直接操作的时候。

还有一个通用技巧:在通信线路上串联小电阻。比如在SPI的SCLK和MOSI上串22Ω到100Ω的电阻,可以抑制反射和过冲,改善信号完整性。尤其是走线较长或者频率较高的时候,这个小电阻能解决很多偶发问题。I2C的SDA和SCL也可以串,但要注意不能影响上拉电阻的分压。

注意:逻辑分析仪的采样率要足够高。SPI跑10MHz的话,采样率至少100MHz才能看清波形。I2C和UART速率低,采样率要求不高,但也要保证每个位时间有足够的采样点。

7. 进阶话题:DMA、中断与多设备混合组网

当你把基础通信跑通之后,下一步就是优化CPU占用和系统实时性。DMA是提升串行通信效率的利器。SPI配合DMA,可以在不占用CPU的情况下传输大量数据。比如刷SPI屏幕,一帧图像几万字节,用DMA传输,CPU可以去处理其他任务。I2C的DMA支持因MCU而异,有些支持,有些不支持。UART的DMA收发很常见,尤其是高速率或者大数据量的时候。

中断的使用也有讲究。SPI和I2C通常用中断来处理传输完成事件,但中断频率不能太高。如果每传一个字节就中断一次,CPU会被频繁打断,效率反而下降。更好的做法是用DMA传输一批数据,传完一批再中断一次。UART的接收中断可以配合环形缓冲区,把收到的数据先存起来,主循环慢慢处理。

多设备混合组网是实际项目中的常态。比如一个主控同时用SPI接Flash、用I2C接传感器、用UART接无线模块。这时候要注意引脚复用和中断优先级。有些MCU的SPI和I2C共用引脚,配置的时候要小心。中断优先级方面,高速通信的优先级应该高于低速通信,避免低速中断阻塞高速数据。

还有一个进阶技巧:用GPIO模拟I2C或SPI。当硬件外设不够用,或者需要更灵活的时序控制时,软件模拟是很好的补充。软件I2C的速率通常只能到100kHz到400kHz,但胜在灵活,可以任意切换引脚。软件SPI可以跑到几MHz,但会占用CPU时间。我一般只在硬件资源实在不够的时候才用软件模拟,因为调试起来更麻烦,时序容易受中断影响。

电源管理也是串行通信中容易被忽略的一环。当系统进入低功耗模式时,通信外设的时钟可能会被关闭,导致通信中断。如果需要在低功耗下保持通信,要确保外设时钟源不被关闭,或者配置唤醒中断。另外,I2C的上拉电阻在低功耗模式下会持续耗电,如果总线空闲,可以考虑用GPIO控制上拉电阻的供电,进一步降低功耗。

最后说一个实际项目中的教训:通信协议要有超时和重试机制。无论SPI、I2C还是UART,都可能因为干扰、时序问题或者设备异常而失败。如果代码里没有超时处理,一个失败的传输可能让整个系统卡死。我的做法是每次通信都设一个超时计数器,超时后重试两到三次,如果还失败就报错并复位从设备。这个机制在实验室里可能看不出差别,但在现场环境中能救命。

提示:在通信线上加TVS二极管可以防止静电和浪涌损坏接口。尤其是对外连接的UART或者I2C接口,TVS是低成本高回报的保护措施。

8. 我踩过的那些坑与最终选型心得

做了这么多年嵌入式,SPI、I2C、UART的坑我基本都踩过一遍。有些是粗心大意,有些是理解不到位,还有些是器件本身的特性。挑几个有代表性的说说,希望能帮你少走弯路。

第一个坑:SPI模式搞错。前面提过,但值得再强调。我买了一个SPI传感器,数据手册上写支持模式0和模式3,我随手配了模式0,结果读出来的数据一直不对。后来用逻辑分析仪抓波形,发现从设备在时钟下降沿输出数据,而模式0是在上升沿采样,完全错位。改成模式3就正常了。教训:永远不要假设SPI模式,一定要看数据手册并用波形验证。

第二个坑:I2C上拉电阻用了10kΩ。在一个3.3V系统里,我用10kΩ上拉,跑100kHz没问题。后来想提速到400kHz,发现波形上升沿太慢,数据经常出错。算了一下,总线电容大概150pF,10kΩ的上升时间约1.5微秒,远超过快速模式要求的300纳秒。换成2.2kΩ后,上升时间降到300纳秒左右,400kHz稳定运行。教训:I2C的上拉电阻要根据速率和总线电容计算,不能随便选。

第三个坑:UART波特率用内部RC。一个低成本项目,为了省晶振,用内部RC跑115200。常温调试没问题,小批量生产后有几块板子通信不稳定。测了一下RC频率,偏差最大的到了2.5%,加上分频误差,总误差接近3%,处于临界状态。后来改成9600波特率,误差容忍度大了,问题消失。教训:高速UART一定要用晶振,内部RC只适合低波特率。

第四个坑:SPI片选用了软件延时。在一个SPI Flash项目里,我用GPIO控制CS,拉低后立即开始传输。结果偶尔写入失败。抓波形发现CS拉低到第一个时钟沿之间只有几十纳秒,而Flash要求至少100纳秒的建立时间。加了一个微秒级延时后,问题解决。教训:SPI的片选建立时间和保持时间要看数据手册,不能想当然。

第五个坑:I2C总线上挂了不同速率的设备。一条总线上有一个400kHz的传感器和一个100kHz的EEPROM。我按400kHz配置主设备,结果EEPROM通信失败。后来意识到I2C总线速率由最慢的设备决定,改成100kHz后两个都正常了。教训:混合速率设备共存时,按最低速率跑。

选型心得方面,我现在的原则是:先看器件手册推荐什么,再看系统需求,最后看主控资源。如果器件只支持一种接口,那就没得选。如果器件支持多种接口,比如有些传感器同时支持SPI和I2C,那就看哪个更符合系统需求。速度要求高选SPI,引脚紧张选I2C。UART主要用于调试和模块通信,很少作为传感器的主要接口。

还有一个经验:在项目初期就把通信接口规划好,不要等到PCB画完了才发现引脚不够或者协议冲突。我见过一个项目,硬件工程师把SPI和I2C的引脚画反了,软件工程师调了一周才发现是硬件问题。如果前期多花半小时核对原理图,能省下一周的时间。

最后,保持对波形的敬畏。无论你觉得自己对协议多熟悉,遇到通信问题,第一件事就是抓波形。波形不会骗人,它能告诉你时序对不对、电平对不对、数据对不对。很多问题看代码看不出来,看波形一目了然。逻辑分析仪不贵,但能帮你省下的时间远超它的价格。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询