ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

UART、SPI、I²C总线选型指南:从物理层到协议层的实战对比

UART、SPI、I²C总线选型指南:从物理层到协议层的实战对比

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

很多刚接触嵌入式开发的朋友,第一次面对UART、SPI、I²C这三个名词时,脑子里往往是一团浆糊。数据手册上写着“支持UART、SPI、I²C”,但到底该用哪个,为什么这个传感器用I²C而那个Flash用SPI,心里完全没底。我刚开始做项目时也这样,后来踩了几次坑才明白,选型这件事,本质上不是看哪个“更高级”,而是看哪个“更合适”。

要理解它们的区别,得先从物理层入手。这三种总线最直观的差异,就体现在引脚数量和信号线上。

UART,全称通用异步收发器,它是最“古老”也最简单的串行通信方式。说它简单,是因为它连时钟线都不需要。两个设备通信,只需要三根线:TX(发送)、RX(接收)、GND(地)。发送方和接收方各自约定好一个波特率,比如9600、115200,然后按照这个节奏收发数据。因为没有时钟线,所以叫“异步”。这就好比两个人约好了每隔一秒说一个字,谁也不用给谁打拍子,全凭各自的节奏感。但问题也来了,如果两个人的节奏感有偏差,说着说着就乱了,所以UART对波特率的精度有一定要求,一般误差不能超过2%到3%。

SPI,全称串行外设接口,它是这三种总线里速度最快的。SPI采用主从架构,通常需要四根线:SCLK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。时钟线由主机提供,数据在时钟的边沿上被采样,所以它是“同步”通信。SPI的引脚名字很直白,MOSI就是主机往外发数据,MISO就是主机往里收数据。片选线用来选中某个从设备,因为SPI总线上可以挂多个从机,但每个从机都需要一根独立的CS线。这就意味着,如果你挂5个SPI从设备,主机就需要5根片选线,加上时钟和两根数据线,总共要占用7个IO口。SPI的速度可以做到几十兆赫兹,适合需要高速数据传输的场景,比如LCD屏幕、Flash存储、高速ADC等。

I²C,全称集成电路总线,它的特点是“两根线挂一堆设备”。I²C只需要两根线:SCL(时钟)和SDA(数据),而且这两根线是开漏输出,需要外接上拉电阻。所有设备都并联在这两根线上,每个设备有一个唯一的7位地址。主机发起通信时,先发送地址,匹配到地址的设备才会响应。这就好比一个教室,老师点名,只有被点到名字的学生才站起来回答问题。I²C的速度相对较慢,标准模式100kHz,快速模式400kHz,高速模式3.4MHz,但它的优势在于引脚少、支持多设备、有应答机制,适合连接低速传感器、EEPROM、RTC等。

把这三种总线的物理层特性放在一起对比,差异一目了然:

特性UARTSPII²C
信号线数量2(TX/RX)+ GND4(SCLK/MOSI/MISO/CS)+ GND2(SCL/SDA)+ GND
通信方式异步同步同步
拓扑结构点对点一主多从(独立片选)一主多从(地址寻址)
最高速率通常≤1Mbps可达数十Mbps标准100k/快速400k/高速3.4M
多设备支持仅点对点需要额外片选线地址寻址,两根线搞定
硬件开销极低中等低(需上拉电阻)

这张表建议刚入门的朋友存下来,选型时先过一遍,心里就有个大概方向了。

2. 协议层的博弈:为什么SPI快而I²C稳

理解了物理层的差异,只能算入门。真正决定选型的,是协议层的设计哲学。这三种总线在协议层的取舍,直接影响了它们的速度、可靠性和适用场景。

2.1 UART的“裸奔”哲学:简单但脆弱

UART的协议简单到令人发指。一帧数据由起始位、数据位、校验位、停止位组成。起始位是一个低电平,告诉对方“我要开始发了”;然后是5到9个数据位,通常是8位;接着是可选的校验位,用于简单的错误检测;最后是停止位,高电平,表示这一帧结束。没有地址,没有应答,没有时钟同步,发出去就不管了。

这种设计的优点是硬件成本极低,几乎任何单片机都有UART外设,连8位机都不例外。但缺点也很明显:第一,只能点对点,两个设备之间通信,没法挂多个设备;第二,没有应答机制,发送方不知道接收方有没有收到;第三,没有时钟线,对波特率精度有要求;第四,速度上不去,通常115200bps是常见配置,再高就容易出错。

我早期做项目时,用UART连接一个GPS模块,波特率设了115200,数据偶尔会丢几个字节。排查了半天,发现是晶振精度不够,波特率误差偏大。后来换了更高精度的晶振,问题才解决。所以UART适合什么场景?适合两个设备之间低速、短距离、数据量不大的通信,比如调试串口、GPS模块、蓝牙模块、某些传感器。

2.2 SPI的“全双工”暴力美学:快就一个字

SPI的协议设计核心就一个字:快。它没有复杂的帧格式,没有地址,没有应答,主机拉低片选,然后时钟一打,数据就在MOSI和MISO上同时进出。全双工,意味着发送和接收可以同时进行,效率极高。

SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)组合而成。CPOL决定时钟空闲时是高电平还是低电平,CPHA决定数据在时钟的哪个边沿被采样。这四种模式分别是Mode 0、Mode 1、Mode 2、Mode 3。主从设备必须配置成相同的模式,否则数据会错位。我见过不少新手在这里翻车,明明代码写得没问题,就是读不到数据,最后发现是模式没对上。

SPI的另一个特点是“无应答”。主机发了数据,从机收没收到,主机不知道。这在某些场景下是问题,比如向Flash写数据,你得等Flash内部编程完成,但SPI没有机制告诉你“我忙完了”。所以很多Flash芯片会额外提供一根“忙”信号线,或者需要主机主动发送读状态寄存器命令来查询。这就是SPI的代价:速度快,但协议层的可靠性保障少,需要软件或额外引脚来弥补。

2.3 I²C的“点名”机制:稳字当头

I²C的协议设计核心是“可控”。它有一套完整的帧格式:起始条件、地址帧、读写位、应答位、数据帧、停止条件。每次通信,主机先发起始条件,然后发7位地址加1位读写位,匹配到地址的从机拉低SDA线作为应答,然后才开始传数据。每传一个字节,接收方都要回一个应答位,告诉发送方“我收到了”。

这套机制的好处是:第一,多设备支持,两根线可以挂几十个设备,只要地址不冲突;第二,有应答机制,发送方知道数据有没有被正确接收;第三,有起始和停止条件,通信过程清晰可控;第四,支持时钟拉伸,从机如果处理不过来,可以拉低SCL线让主机等待。

但代价是速度慢。每次传数据都要地址帧、应答位,协议开销大。而且开漏输出加上拉电阻,上升沿比较慢,限制了速率。另外,I²C总线上如果某个设备死机拉低SDA线,整个总线就挂了,排查起来很头疼。我遇到过好几次I²C总线被拉死的情况,最后都是靠断电重启或者用I²C复位命令解决的。

所以I²C适合什么场景?适合连接多个低速设备,比如温度传感器、加速度计、EEPROM、RTC、IO扩展芯片等。这些设备数据量不大,但对可靠性有要求,I²C的应答机制正好满足。

3. 项目选型的决策树:从需求倒推方案

理论讲完了,回到实际项目。面对一个具体的需求,到底该怎么选?我总结了一套决策流程,基本上按这个思路走,不会出大错。

3.1 第一步:明确通信速率需求

速率是选型的第一个分水岭。先问自己:我需要传多少数据,多久传一次?

如果数据量很小,比如每秒几十个字节,那UART和I²C都能胜任,SPI就有点大材小用。如果数据量很大,比如要传图像数据、音频数据,或者要读写高速ADC的采样结果,那SPI是唯一选择。UART和I²C的速率上限摆在那里,硬上只会出错。

举个例子,我之前做一个项目,需要从外部Flash读取音频文件并播放。Flash容量16MB,音频码率128kbps。一开始想用I²C,因为引脚少,接线简单。但算了一下,I²C快速模式400kHz,实际有效数据速率大概也就几十kB/s,读一首3分钟的歌曲要好几秒,体验太差。后来换成SPI,时钟跑到20MHz,读取速度瞬间上去了,播放流畅。这就是速率的硬约束。

3.2 第二步:数一数要挂几个设备

设备数量是第二个关键因素。UART只能点对点,如果你要连两个设备,就得用两个UART外设,或者加一个多路复用器。SPI可以挂多个设备,但每个设备需要一根独立的片选线,IO口消耗大。I²C可以挂多个设备,两根线搞定,地址寻址,非常省引脚。

这里有个经验:如果你的MCU引脚紧张,比如用的是QFN封装的小芯片,那I²C的优势就非常明显。我曾经用过一个20引脚的MCU,要连接温度传感器、EEPROM、IO扩展芯片三个外设。如果用SPI,光片选线就要3根,加上时钟和数据线,总共5根;用I²C,两根线全搞定,省下来的引脚可以干别的。所以引脚资源紧张时,优先考虑I²C。

3.3 第三步:评估可靠性和实时性要求

可靠性和实时性也是重要考量。UART没有应答,数据丢了就丢了,适合对可靠性要求不高的场景,比如调试打印。SPI没有应答,但速度快,适合对实时性要求高、数据量大的场景,比如LCD刷新。I²C有应答,可靠性高,但速度慢,适合对可靠性要求高、数据量不大的场景,比如传感器读取。

还有一个容易被忽略的点:通信距离。UART和I²C都是单端信号,抗干扰能力一般,适合板内通信,距离一般不超过几十厘米。SPI也是单端,但速度快,走线要注意阻抗匹配。如果通信距离超过一米,这三种总线都不太合适,需要考虑差分信号或者加收发器。

3.4 第四步:看看外设支持什么接口

有时候选型不是你想选什么就选什么,而是外设支持什么。比如你选了一个温度传感器,它只支持I²C接口,那你就只能用I²C。所以选型时,先看外设的数据手册,确认它支持哪些接口,然后再结合前面的因素做决定。

我一般会做一个简单的表格,把候选外设的接口类型、速率、引脚数、地址选项列出来,然后对照MCU的资源,看哪个组合最合适。这个习惯帮我省了很多来回改板的时间。

4. 实战中的坑:那些数据手册不会告诉你的事

理论归理论,实际调试时,这三种总线各有各的坑。我挑几个印象深刻的说说,希望能帮你少走弯路。

4.1 UART的波特率误差:不是设了就能通

UART最容易被忽视的就是波特率误差。很多人以为在代码里写个115200就万事大吉了,实际上,如果MCU的时钟源精度不够,或者分频系数算得不对,实际波特率可能偏差很大。一般UART接收端能容忍的误差在2%到3%左右,超过这个范围,数据就会出错。

怎么算波特率误差?假设MCU主频是8MHz,你要设115200波特率。分频系数 = 8000000 / (16 × 115200) ≈ 4.34。取整数4,实际波特率 = 8000000 / (16 × 4) = 125000,误差 = (125000 - 115200) / 115200 ≈ 8.5%。这个误差太大了,通信肯定不稳定。解决办法是换一个主频,比如用7.3728MHz,这个频率是115200的整数倍,分频系数正好是4,误差为0。所以很多老工程师会推荐用7.3728MHz或11.0592MHz的晶振,就是为了UART通信方便。

提示:如果你的项目对UART通信稳定性要求高,优先选择能整除常用波特率的晶振频率,比如7.3728MHz、11.0592MHz、14.7456MHz等。

4.2 SPI的片选时序:拉早拉晚都是错

SPI的片选信号看似简单,拉低选中,拉高释放。但实际调试时,片选的时序很关键。有些SPI从设备要求片选在时钟稳定之前就拉低,有些则要求片选和第一个时钟边沿之间有一定延时。如果片选拉得太早,从设备可能还没准备好;拉得太晚,第一个时钟边沿可能被漏掉。

我遇到过一个问题:用SPI驱动一块LCD屏幕,初始化时死活不显示。查了半天,发现是片选信号在发送命令之前就拉低了,但LCD要求片选拉低后至少等待10微秒才能接收命令。后来在片选拉低后加了一个短延时,问题解决。所以看数据手册时,一定要仔细看时序图,特别是片选、时钟、数据之间的时间关系。

4.3 I²C的上拉电阻:阻值选不对,通信就报废

I²C的上拉电阻是个老生常谈的问题,但新手还是经常踩坑。I²C的SCL和SDA是开漏输出,必须外接上拉电阻才能输出高电平。上拉电阻的阻值直接影响通信质量:阻值太大,上升沿太慢,高速通信时波形还没到高电平就被拉低了;阻值太小,功耗大,而且可能超过器件的灌电流能力。

一般来说,标准模式100kHz可以用10kΩ,快速模式400kHz用4.7kΩ,高速模式用2.2kΩ甚至更小。但这不是绝对的,还要看总线电容。总线电容越大,上升沿越慢,需要更小的上拉电阻。经验公式是:上升时间 ≈ 0.847 × R × C,其中R是上拉电阻,C是总线电容。如果上升时间超过协议规定的时间,通信就会出错。

我一般会在板子上预留上拉电阻的位置,先用4.7kΩ试,如果波形不好再调整。用示波器看SCL和SDA的上升沿,如果上升沿太缓,就减小电阻;如果上升沿有振铃,就增大电阻。这个调试过程虽然麻烦,但一次调好,后面就稳了。

4.4 多设备地址冲突:I²C的隐形炸弹

I²C总线上挂多个设备时,地址冲突是个大问题。每个I²C设备都有一个7位地址,如果两个设备地址相同,主机就无法区分它们。有些设备的地址是固定的,有些可以通过引脚配置。选型时一定要确认地址是否可配,以及总线上有没有地址冲突。

我曾经在一个项目里挂了两个同型号的传感器,地址都是0x68,结果怎么都读不到数据。后来发现其中一个传感器有一个地址选择引脚,拉高后地址变成0x69,问题才解决。所以选型时,尽量选地址可配的器件,或者提前规划好地址分配,避免冲突。

5. 进阶思路:什么时候需要“混搭”或“转换”

实际项目中,有时候单一总线满足不了需求,需要混搭或者转换。这一块往往被新手忽略,但却是老手经常用的技巧。

5.1 用IO模拟I²C:引脚不够时的救命稻草

有些MCU的硬件I²C外设只有一两个,但你需要挂多个I²C设备,怎么办?可以用普通IO口模拟I²C时序。虽然速度慢一点,但胜在灵活,想用哪个引脚就用哪个引脚。模拟I²C的关键是时序要准,特别是起始条件、停止条件、应答位的时序。我一般会用示波器抓一下波形,确认时序符合协议要求。

模拟I²C还有一个好处:当硬件I²C总线被拉死时,可以通过模拟时序发送复位脉冲来恢复总线。硬件I²C外设一旦死锁,往往需要断电重启,而模拟I²C可以灵活处理。

5.2 SPI转UART:用专用芯片扩展串口

有些项目需要多个UART,但MCU的UART外设不够用。这时候可以用SPI转UART的专用芯片,比如常见的SPI转串口芯片,一颗芯片可以扩展出多个串口。主机通过SPI与芯片通信,芯片再对外提供UART接口。这种方案在工业控制中很常见,因为工业设备往往需要多个串口连接不同的模块。

选这类芯片时,要注意SPI的速率和UART的波特率是否匹配,以及芯片内部FIFO的深度。FIFO太浅的话,高速通信时容易丢数据。

5.3 I²C转SPI:低速总线控制高速设备

有时候MCU只有I²C接口,但外设是SPI接口,怎么办?可以用I²C转SPI的桥接芯片。主机通过I²C发送命令,桥接芯片转换成SPI时序去控制外设。这种方案适合低速控制场景,比如通过I²C配置SPI接口的ADC,然后ADC通过SPI高速采集数据,但数据还是通过I²C回传。不过要注意,I²C的速率会成为瓶颈,如果数据量大,还是直接上SPI更合适。

6. 我的选型清单:一张表搞定决策

说了这么多,最后分享一个我常用的选型清单。每次做新项目,我都会对着这个清单过一遍,基本不会漏掉关键因素。

考量维度优先选UART优先选SPI优先选I²C
通信速率低速(≤115200bps)高速(≥1Mbps)中低速(≤400kbps)
设备数量点对点少量从机(片选线够用)多从机(引脚紧张)
引脚资源充裕充裕紧张
可靠性要求低(调试打印)中(需软件补偿)高(有应答机制)
通信距离板内短距离板内短距离板内短距离
外设接口仅支持UART仅支持SPI仅支持I²C
典型应用调试串口、GPS、蓝牙Flash、LCD、高速ADC传感器、EEPROM、RTC

这张表不是绝对的,实际选型时还要结合具体项目需求。比如有些传感器同时支持SPI和I²C,那就看哪个更省引脚、哪个速率更合适。有些Flash同时支持SPI和UART,那基本没人用UART,因为太慢了。

我在实际项目中的体会是,选型没有“最好”,只有“最合适”。UART简单可靠,SPI快如闪电,I²C稳如老狗,各有各的用武之地。关键是要理解它们的底层原理,知道它们的优缺点,然后根据项目需求做权衡。刚开始可能会纠结,但做多了就有感觉了,看到需求基本就能条件反射地选出合适的方案。

最后再分享一个小技巧:如果你实在拿不准,可以在板子上同时预留UART、SPI、I²C的接口,用0欧姆电阻或者跳线选择。这样即使选错了,改板也方便。我早期做项目时经常这么干,虽然浪费一点板子面积,但省了很多改板的时间和成本。等经验丰富了,再慢慢优化。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进