☰
UART、SPI、I²C总线选型实战:从物理层到项目踩坑经验
2026/10/14 16:50:21 网站建设 项目流程

1. 三种总线的本质差异:从物理层到协议层

刚入行那会儿,我在一个传感器采集项目上栽过跟头。当时板子上留了一个I²C接口的温湿度传感器,我图省事直接把它挂到了SPI总线上,结果调了两天数据死活读不出来。后来翻数据手册才发现,那个传感器的SDA引脚在SPI模式下根本不具备三态输出能力,硬件上就不兼容。这件事让我明白一个道理:选总线不是看哪个快就用哪个,而是要看协议本身的设计哲学是否匹配你的场景。

UART、SPI、I²C这三种串行通信总线,几乎每个嵌入式开发者都会接触到。但很多人对它们的理解停留在“UART是异步的、SPI是四根线、I²C是两根线”这种表面层次。真正到了项目选型的时候,光知道这些远远不够。你需要理解它们各自的物理层电气特性、协议层的帧结构设计、以及这些设计带来的系统级影响。

先说说UART。它的全称是通用异步收发器,注意“异步”这两个字——这意味着通信双方没有共享的时钟线。发送方和接收方各自按照约定的波特率来采样数据。这就好比两个人约定好每隔一秒说一个字,但彼此的手表可能有微小误差。短时间内没问题,时间长了误差累积就会导致采样错位。所以UART对晶振精度的要求其实比很多人想象的要高,一般要求双方波特率误差控制在2%以内,高速率下甚至要控制在1%以内。

UART的帧结构通常是:1个起始位(低电平)、5到9个数据位、可选的校验位、1到2个停止位(高电平)。起始位的作用是告诉接收方“注意,我要开始发数据了”,接收方检测到下降沿后启动内部采样时钟。这种设计的好处是简单、成本极低,两根线(TX和RX)就能全双工通信。但缺点也很明显:没有地址概念,只能点对点;没有时钟线,高速时可靠性下降;每个字节都有起始位和停止位的开销,有效数据率打折扣。

SPI则是另一套思路。它是同步总线,有一根SCK时钟线由主机驱动。数据在时钟边沿被采样,所以不存在波特率误差累积的问题。SPI通常有四根线:SCK、MOSI(主机输出从机输入)、MISO(主机输入从机输出)、CS(片选)。CS线是SPI实现多设备的关键——每个从设备独占一根CS线,主机拉低哪个CS就选中哪个设备。这就带来了一个直接后果:SPI的引脚数随从设备数量线性增长。挂8个从设备,主机就需要8根CS线,加上SCK、MOSI、MISO,总共11根线。在引脚资源紧张的MCU上,这是很现实的约束。

SPI的协议层极其简单,本质上就是一个移位寄存器环。主机和从机的移位寄存器在SCK驱动下同时移位,8个时钟周期后完成一个字节的交换。没有起始位、没有停止位、没有地址帧、没有ACK/NACK。这种极简设计带来的好处是速率可以做得非常高,几十MHz甚至上百MHz都很常见。但代价是没有任何协议层的错误检测机制,也没有流控,全靠开发者自己保证时序正确。

I²C的设计目标则完全不同。它用两根线(SCL时钟线和SDA数据线)实现了多设备通信,而且支持多主机。每个从设备有唯一的7位(或10位)地址。通信开始时,主机发送起始条件(SCL高时SDA由高变低),然后发送地址帧,从设备匹配地址后拉低SDA作为ACK应答。I²C的SDA和SCL都是开漏输出,需要外部上拉电阻。这意味着总线上任何设备都可以拉低线路,但释放后由电阻拉高——这就是I²C实现仲裁和时钟同步的物理基础。

I²C的速率相对较低,标准模式100kHz,快速模式400kHz,高速模式3.4MHz。但它的引脚效率极高,两根线理论上可以挂128个设备(7位地址去掉保留地址)。而且它支持热插拔和多主机仲裁,这在一些复杂系统中非常有用。代价是协议开销大——每个字节传输后都需要一个ACK位,起始和停止条件也有额外时序,有效数据率远低于时钟频率。

把这三种总线放在一起对比,核心差异可以归纳成一张表:

特性UARTSPII²C
线数2(TX/RX)4+(每从机加CS)2(SCL/SDA)
时钟异步同步同步
拓扑点对点一主多从多主多从
寻址无片选线地址帧
全双工是是半双工
典型速率9600bps~1Mbps1MHz~100MHz100kHz~3.4MHz
错误检测校验位无ACK/NACK
引脚开销固定2根随从机增长固定2根
上拉电阻不需要不需要需要

这张表是选型的起点,但不是终点。因为实际项目中,你还要考虑软件复杂度、DMA支持、中断开销、总线电容限制、电平匹配等一系列工程因素。下面我会逐一拆开讲。

2. 项目选型时真正该问自己的几个问题

很多教程讲选型,上来就给结论:“高速用SPI,多设备用I²C,简单通信用UART”。这种口诀在简单场景下没错,但真实项目往往更复杂。我总结了一套自己的决策流程,核心是问自己五个问题。

2.1 数据量和实时性要求到底有多高

先算一笔账。假设你要接一个加速度计,输出三轴16位数据,采样率1kHz。每轴2字节,三轴6字节,加上可能的帧头帧尾,算8字节一帧。1kHz就是8000字节/秒,即64kbps。这个速率UART在115200bps下就能轻松搞定,I²C在400kHz下也绰绰有余,SPI更是杀鸡用牛刀。

但如果你要接的是一个摄像头模块,输出QVGA(320x240)RGB565图像,30帧每秒。每帧320x240x2=153600字节,30帧就是4.6MB/s,即36.8Mbps。这个量级UART完全不可能,I²C高速模式3.4Mbps也不够,只有SPI在50MHz以上才能胜任。

所以第一个问题就是:你的峰值数据率是多少?注意是峰值,不是平均值。很多传感器有突发模式,平时数据率低,但触发后短时间内会输出大量数据。如果总线带宽按平均值设计,突发时就会丢数据。

这里有个经验公式:所需总线时钟频率 ≈ 数据率 × 协议开销系数。UART每字节有起始位和停止位,开销约20%,所以波特率至少是数据率的1.25倍。I²C每字节有ACK位,加上起始停止条件,开销约10%~15%。SPI几乎没有协议开销,但软件读写寄存器的开销可能成为瓶颈。

2.2 总线上要挂多少个设备

设备数量直接决定了引脚预算。假设你用一颗48引脚的MCU,可用GPIO大概30个左右。如果挂6个SPI从设备,光CS线就占6根,加上SCK、MOSI、MISO,总共9根。如果这些设备还需要中断引脚、复位引脚,引脚很快就不够用了。

I²C在这方面优势明显,两根线挂多少设备都行(只要地址不冲突)。但要注意地址冲突问题——很多传感器厂商只提供两三个可选地址,如果你要挂多个同型号传感器,很可能地址撞车。这时候就需要I²C多路复用器,比如TCA9548A这类芯片,它可以把一路I²C分成8路,每路可以挂地址相同的设备。但这又增加了成本和板子面积。

UART则是每个设备独占一个UART外设。大多数MCU有2到8个UART,挂多了就不够用。而且UART是点对点的,不能像I²C那样共享总线。

所以第二个问题是:设备数量和引脚预算是否匹配?如果设备多且引脚紧张,I²C是首选;如果设备少但速率要求高,SPI更合适;如果只是两个设备之间通信,UART最简单。

2.3 通信距离和板级环境如何

这三种总线都是板级总线,设计时假设通信距离在几十厘米以内。如果距离拉长,问题就来了。

UART在TTL电平下,几米之内问题不大,但再长就需要转成RS-232或RS-485差分信号。SPI和I²C的设计初衷就是板内通信,通常不超过30厘米。I²C的总线电容限制是400pF,按标准线缆的分布电容估算,大概也就一两米。SPI没有明确的电容限制,但时钟速率越高,信号完整性问题越突出,走线长度要相应缩短。

如果你的两个模块之间需要排线连接,比如主板到显示面板,那就要特别小心。排线的分布电感和电容会导致信号振铃和边沿退化。我见过一个项目,SPI时钟跑到20MHz,排线长度15厘米,结果数据误码率很高。后来降到10MHz并加了串联端接电阻才稳定。

所以第三个问题是:通信距离和走线环境是否恶劣?如果距离超过20厘米或者经过排线、连接器,优先考虑降低速率或者改用差分总线。I²C在这种场景下反而比SPI更稳,因为它的速率低、开漏输出抗干扰能力稍好。

2.4 软件开销和CPU占用率能否接受

这一点经常被忽略。SPI虽然硬件简单,但软件上通常需要手动控制CS线。每次传输前拉低CS,传输后拉高CS。如果传输频繁,CS线的翻转会消耗CPU时间。而且SPI没有流控,主机发数据时不知道从机是否准备好,只能靠固定延时或者额外握手线。

I²C的软件开销更大。每次传输都要发送起始条件、地址帧、等待ACK、发送数据、等待ACK、发送停止条件。如果使用中断或DMA,配置也更复杂。而且I²C总线锁死是出了名的常见问题——如果从机在传输过程中复位,可能会一直拉低SDA线,导致总线无法释放。这时候需要主机发送9个时钟脉冲来解锁,软件上要专门处理。

UART的软件开销介于两者之间。它有硬件FIFO的话会轻松很多,但如果没有FIFO,每个字节都要中断处理,高波特率下CPU占用率会很高。

所以第四个问题是:你的CPU有多少余量?如果CPU已经很忙,优先选带DMA的SPI或UART,避免用I²C轮询。如果CPU空闲,I²C的软件复杂度可以接受。

2.5 系统对可靠性和错误检测的要求

SPI没有任何协议层错误检测。如果时钟受到干扰多移了一位,数据就全错了,而且SPI本身不知道。UART有校验位,可以检测单比特错误,但校验能力有限。I²C有ACK/NACK机制,从机收到每个字节都会应答,主机可以知道传输是否成功。

在工业环境或电机控制等干扰较大的场景,I²C的ACK机制和开漏输出的抗干扰特性反而比SPI更有优势。虽然速率低,但数据可靠性更高。如果SPI要用在干扰环境,通常需要外加CRC校验或者使用差分SPI(比如一些隔离器支持的方案)。

所以第五个问题是:你的应用环境有多恶劣?实验室环境SPI随便跑,工业现场可能I²C更稳妥。

3. 典型场景下的选型实战与踩坑记录

理论说完了,来看几个我实际经历过的场景。每个场景我都会说明当时的选择、理由、以及后来踩到的坑。

3.1 多传感器数据采集板:I²C的地址冲突与总线锁死

有一个环境监测项目,板子上要接温度、湿度、气压、光照四种传感器。最初选型时,我毫不犹豫选了I²C,因为四种传感器都是I²C接口,两根线搞定,引脚最省。

画原理图时才发现问题:温度和湿度是同一个厂商的传感器,地址固定为0x40和0x44,不冲突。但气压传感器地址是0x77,光照传感器地址也是0x77。这就撞车了。

解决方案有两个:一是换一款地址可配的光照传感器,二是加I²C多路复用器。我选了前者,因为多路复用器增加成本且占用板子面积。但换料时又发现,可配地址的那款光照传感器精度不如原来那款。最后折中方案是:光照传感器单独挂一路I²C,用MCU的第二个I²C外设。好在MCU有两个I²C,问题解决。

但更大的坑在后面。产品小批量试产时,发现偶尔有板子启动后I²C总线无响应。用示波器抓波形,发现SDA线一直被拉低。排查后确认是光照传感器在上电过程中,如果电源上升沿不够陡,内部逻辑会进入异常状态,持续拉低SDA。主机发送任何数据都无法得到ACK,总线死锁。

解决办法是在软件初始化时,先发送9个SCL脉冲。如果SDA被从机拉低,这9个脉冲会让从机完成当前字节的移位,然后释放SDA。之后再发送正常的起始条件,总线就恢复了。这个9脉冲解锁法后来成了我所有I²C项目的标配初始化代码。

注意:I²C总线锁死是高频问题,尤其是传感器上电时序不一致时。建议在初始化代码中无条件加入9脉冲解锁,成本几乎为零,但能省去大量现场排查时间。

3.2 高速ADC数据采集:SPI的CS时序与DMA配置

另一个项目是振动信号采集,用了一颗16位、1MSPS的ADC。数据率是16Mbps,I²C和UART都不可能,只能上SPI。

SPI时钟设到20MHz,理论上余量足够。但调试时发现数据偶尔错位。用逻辑分析仪抓波形,发现CS线在两次传输之间有一个窄脉冲。原因是软件里先拉高CS,然后立即准备下一次传输的数据,但准备数据耗时不确定,导致CS高电平时间过短,ADC还没完成内部转换就开始了下一次传输。

修正方法是改用DMA传输,并且让CS线由硬件自动控制。很多MCU的SPI外设支持硬件CS,配置好后,SPI使能时CS自动拉低,传输完成自动拉高,时序由硬件保证,软件不用干预。如果MCU不支持硬件CS,那就用定时器或者PWM来控制CS,确保高电平时间满足从机要求。

这个项目还遇到一个SPI特有的问题:MISO线上的数据在SCK边沿采样时,由于走线延迟,建立时间不够。20MHz下周期50ns,走线延迟几纳秒就可能吃掉大部分余量。后来在MISO线上串了一个33欧姆电阻,并缩短走线长度,问题才解决。

提示:SPI速率高时,信号完整性是头号敌人。串联端接电阻、缩短走线、增加地平面是三大法宝。如果还不行,就降速——可靠性比速度重要。

3.3 调试串口与固件升级:UART的波特率误差与流控

UART最常用的场景就是调试打印和固件升级。调试打印一般115200bps,对精度要求不高。但固件升级时,数据量大,如果波特率误差大,升级过程中会校验失败。

我遇到过一批板子,调试串口用内部RC振荡器作为时钟源,标称精度±2%。常温下没问题,但到了冬天低温环境,RC振荡器频率漂移,波特率误差超过3%,导致升级失败率飙升。后来改用外部晶振作为UART时钟源,问题消失。

所以UART选型时,一定要确认时钟源精度。如果产品工作温度范围宽,内部RC振荡器往往不够,必须用晶振。另外,高波特率下建议启用硬件流控(RTS/CTS),防止接收方缓冲区溢出。如果没有流控线,就要在协议层加握手,比如XON/XOFF软件流控,或者自定义ACK机制。

固件升级场景还有一个坑:UART的DMA接收如果配置成循环模式,数据会覆盖。如果配置成普通模式,每包数据都要重新配置DMA。我一般用空闲中断加DMA的方式:DMA接收不定长数据,总线空闲时触发中断,在中断里处理整包数据。这样效率最高,CPU占用最低。

3.4 多设备混合总线系统:如何分配总线资源

实际项目往往不是单一总线。比如一个数据采集网关,可能同时有SPI接的以太网控制器、I²C接的EEPROM和传感器、UART接的调试口和无线模块。

这种混合系统选型时,我的原则是:按数据率和实时性分层。高速设备(以太网、ADC、显示屏)走SPI,独占总线,用DMA传输。中低速设备(EEPROM、传感器)走I²C,共享总线,轮询或中断方式。调试和通信模块走UART,独立外设,互不干扰。

但要注意总线之间的相互影响。比如SPI跑高速时,如果和I²C走线平行且距离近,SPI的时钟谐波可能耦合到I²C线上,导致I²C误码。布局时要把高速总线和低速总线分开,必要时用地线隔离。电源也要做好去耦,每个芯片的电源引脚附近放0.1uF电容,总线驱动器的电源还要加10uF钽电容。

还有一个经验:如果MCU的引脚支持重映射,尽量把高速总线安排在相邻引脚上,这样PCB走线短且等长,信号质量好。低速总线可以灵活安排。

4. 进阶话题:DMA、中断与总线性能优化

选好总线只是第一步,怎么用好才是关键。这一章讲几个提升总线性能的实战技巧。

4.1 DMA配置的常见误区与正确姿势

DMA是解放CPU的利器,但配置不当反而会引入新问题。以SPI为例,发送和接收通常需要两个DMA通道。发送DMA把数据从内存搬到SPI数据寄存器,接收DMA把数据从寄存器搬到内存。如果只配了发送DMA没配接收DMA,接收数据就会丢失。

配置时要注意数据宽度匹配。SPI数据寄存器可能是8位或16位,DMA的内存地址增量要对应设置。如果SPI配置为16位数据帧,DMA内存地址增量应该是2字节,外设地址增量是0(因为始终读写同一个寄存器)。

还有一个坑是DMA传输完成中断和SPI传输完成中断的区别。DMA传输完成只表示数据已经从内存搬到了SPI寄存器,但SPI可能还在发送最后一个字节。如果此时拉高CS,最后一个字节会丢失。正确做法是等待SPI的TXE(发送空)标志和TC(传输完成)标志都置位后再拉高CS。或者用SPI的硬件CS,让硬件自动处理。

I²C的DMA配置更复杂,因为I²C是半双工,发送和接收不能同时进行。通常需要先配置发送DMA,发送完地址帧后再切换为接收DMA。有些MCU的I²C外设支持DMA双缓冲,可以简化这个流程。如果没有,就要在中断里手动切换。

4.2 中断优先级与实时性保障

当系统里有多个总线同时工作时,中断优先级分配很重要。原则是:速率高的总线优先级高,有硬实时要求的优先级高。

比如SPI接的ADC以1kHz采样,每次采样完成触发中断读取数据。如果这个中断被UART的接收中断阻塞,ADC数据就可能被覆盖。所以SPI中断优先级要高于UART。但UART如果用于固件升级,数据不能丢,那UART的DMA接收中断优先级也要适当提高。

I²C的中断通常不频繁,因为速率低,可以给较低优先级。但I²C的错误中断(如仲裁丢失、总线错误)应该给高优先级,以便快速恢复。

还有一个技巧:如果MCU支持中断嵌套,确保高优先级中断可以打断低优先级中断。如果不支持,就要在低优先级中断里尽量缩短执行时间,把耗时操作放到主循环。

4.3 总线速率与功耗的平衡

高速总线意味着高功耗。SPI在50MHz时,IO翻转功耗可能占到系统总功耗的相当比例。对于电池供电的设备,总线速率要按需调整。

我的做法是动态切换速率。比如传感器平时以低速采集,SPI时钟降到1MHz。当需要高速读取FIFO时,临时把SPI时钟提到20MHz,读完再降回来。这样平均功耗大幅降低。

I²C的功耗相对较低,因为开漏输出加上拉电阻,上升沿靠电阻拉高,速率慢,翻转功耗小。但上拉电阻的阻值要权衡:阻值小,上升沿快,但静态功耗大;阻值大,功耗小,但上升沿慢,高速时可能来不及拉高。一般400kHz下用2.2k到4.7k,100kHz下用4.7k到10k。

UART的功耗和波特率成正比,但通常不是系统功耗大头。如果UART外设支持自动波特率检测和唤醒,可以在空闲时关闭UART时钟,收到起始位再唤醒,进一步省电。

4.4 逻辑分析仪与示波器在总线调试中的用法

调试总线问题,逻辑分析仪比示波器更实用。因为它能直接解码协议,把波形翻译成字节。我常用的配置是:采样率至少是总线时钟的5倍,比如SPI 20MHz,采样率至少100MHz。通道分配上,SPI用4个通道(SCK、MOSI、MISO、CS),I²C用2个通道(SCL、SDA),UART用2个通道(TX、RX)。

触发条件设置很关键。比如调试I²C总线锁死,触发条件设为SDA持续低电平超过1ms。调试SPI数据错位,触发条件设为CS下降沿。逻辑分析仪还能测量时序参数,比如CS建立时间、数据保持时间,对照从机数据手册检查是否满足。

示波器则用于看信号质量。比如SPI时钟的过冲、振铃、上升时间。如果上升时间超过周期的十分之一,高速下就可能出问题。这时候需要调整驱动能力、串联电阻或改善走线。

提示:逻辑分析仪和示波器互补,前者看协议,后者看信号。两个都用过之后,你会发现很多“协议问题”其实是信号完整性问题。

5. 从选型到落地:一份可复用的检查清单

说了这么多,最后给一份我在项目中实际使用的检查清单。每次新项目选总线时,我会逐条过一遍,避免遗漏。

第一步:明确需求

  • 峰值数据率是多少?平均数据率是多少?
  • 数据是单向还是双向?需要全双工吗?
  • 实时性要求如何?允许的最大延迟是多少?
  • 通信距离多远?经过连接器或排线吗?
  • 环境干扰程度如何?有电机、继电器等干扰源吗?

第二步:盘点资源

  • MCU有哪些总线外设?各几个?
  • 可用GPIO数量?引脚是否支持重映射?
  • DMA通道数量?哪些总线支持DMA?
  • 时钟源精度如何?能否满足UART波特率要求?
  • 电源域和电平是否匹配?需要电平转换吗?

第三步:评估软件

  • 是否有现成驱动库?裸机还是RTOS?
  • 中断优先级如何分配?
  • 是否需要总线锁死恢复机制?
  • 固件升级通道用哪个总线?
  • 调试打印用哪个总线?

第四步:硬件设计

  • 上拉电阻(I²C)阻值计算了吗?
  • 串联端接电阻(SPI高速)需要吗?
  • 总线走线长度和隔离措施?
  • 电源去耦电容布局?
  • 测试点是否预留?方便接逻辑分析仪吗?

第五步:验证计划

  • 用逻辑分析仪抓一次完整通信波形
  • 测量时序参数是否满足从机手册
  • 高低温下测试通信可靠性
  • 电源波动时测试是否丢数据
  • 长时间运行测试有无总线锁死

这份清单看起来繁琐,但每一条都是踩过坑之后总结出来的。比如“上拉电阻阻值计算”这一条,我见过太多人直接抄参考设计的4.7k,结果总线电容大时上升沿太慢,400kHz下通信失败。正确做法是根据总线电容和上升时间要求算:R = t_r / (0.8473 × C),其中t_r是上升时间,C是总线电容。标准模式t_r最大1000ns,快速模式最大300ns。

再比如“测试点预留”,很多板子画完后才发现没地方接逻辑分析仪,调试时只能飞线,痛苦不堪。建议在SCK、MOSI、MISO、CS、SCL、SDA、TX、RX这些关键信号上都留测试点,成本几乎为零,但调试效率天差地别。

最后说一个我自己的教训。曾经有一个项目,SPI接的Flash芯片,读写都正常,但批量生产时发现千分之三的板子无法识别Flash。排查后发现是SPI的CS线走线太长,且没有端接电阻,导致CS下降沿有振铃,偶尔被Flash误判为时钟脉冲。后来在CS线上串了100欧姆电阻,问题解决。这个案例告诉我,即使是低速控制信号,走线长了也要当高速信号处理。

选总线这件事,没有绝对的对错,只有适合不适合。UART简单可靠但速率有限,SPI高速灵活但引脚多,I²C省引脚但协议开销大。理解它们的设计哲学,结合项目实际约束,才能做出不后悔的选择。希望这些经验能帮你少走一些弯路。

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

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

立即咨询