☰
嵌入式必懂:UART、I2C、SPI、I2S串行协议对比与实战避坑指南
2026/9/28 14:16:35 网站建设 项目流程

嵌入式工程师对这四个词应该都不陌生:I2C、I2S、SPI、UART。原理图上到处是它们的名字,芯片手册里躺着它们的时序图,调试日志更是靠UART一路输出的。但说实话,很多兄弟只停留在"会用"状态——对着初始化代码抄一遍,波形不对就换模式,时序错了就调参数,至于为什么是这套机制,很少停下来细想。我见过把I2S当I2C来配的,也见过SPI片选用软件模拟导致ADC数据偶尔跳变、折腾了两天查不出原因的。这些问题的根源,往往不是代码写错了,而是对协议本质的理解有偏差。不管你是刚入门的嵌入式新人,还是已经在产品里调通了好几颗外设的老手,这篇对比都能给你一些平时文档里翻不到的细节。

1. 四种协议到底是什么,别急着背参数

开门见山,先说它们为什么能共存。串行通信的共同点是拿一根或几根线,把数据一位一位传出去,省引脚、好布线。但不同的应用场景对"传多快、能连多少个、能不能双向、能不能确认收到"都有完全不同的要求,于是芯片行业演化出了这四种形态,各有专攻。

1.1 同步、异步与"约定"这件事

UART是异步通信,收发双方之间没有时钟线,全靠事先约定的波特率来采样。SPI、I2C、I2S都有时钟线,属于同步通信,谁发时钟谁做主,时序由硬件保证。这条差异决定了它们在抗干扰、传输距离和速率上限上的基本性格。

异步看似省了布线,但代价是双方必须接受"波特率时钟源有偏差"的现实。晶振差几个ppm,在115200波特率下一帧误差不到半个位宽,问题不大;要是把波特率顶到921600,再让板子跑在高温环境,误差就可能吃掉半个位周期,导致乱码。所以UART高速率场景我从来不用内部RC振荡器,老老实实外接晶振,这是第一个容易忽略的坑。

1.2 UART:点对点、老而弥坚

UART全称通用异步收发器,基本原理就是两根线:TX和RX交叉连接,全双工。帧格式是大家熟悉的那套:空闲时总线为高电平,检测到下降沿就是起始位,然后依次传数据位,通常是8位、LSB在前,加上可选的奇偶校验位,最后是停止位,拉高至少一个位周期。接收端从检测到下降沿那一刻起,就要按波特率一位一位地采样,所以双方时钟偏差一多,采样点就会慢慢滑向位边界。

热词里那个"16550行业标准UART"值得多说一句。16550是当年PC上的经典串口控制器,因为引入了16字节FIFO,一下子解决了一个字节一中断、CPU被中断风暴打垮的问题。后来的芯片几乎都在寄存器布局上兼容它,直到今天Linux下的串口驱动里还有大量ns16550的血统。UART的短板是只能点对点,一条串口线上只有收发两端;想挂多个设备就得转RS485,变成半双工总线,还要自己定义地址协议。好处的老练在于简单、可靠、距离远,MCU基本都有,调试口首选基本上就是它。

USB转串口芯片干的活,本质是把UART信号桥接成USB,让电脑把它识别成一个COM口。FT231X、FT232R这两颗我用了不少,驱动出问题的概率其实不大,真出问题多数是驱动版本和Win10/Win11的兼容性,后文会专门讲。

1.3 I2C:两根线、开漏、一挂一大串

I2C是NXP前身飞利浦搞的总线,两条线:SCL时钟、SDA数据,都靠开漏输出加外部上拉电阻工作。开漏意味着任何设备都可以把线拉低,也可以释放掉让上拉电阻把线拉回高。这个结构是I2C一切机制的基石:多设备共享不需要三态门争抢,从机还能把SCL主动拉低来让主机"等等我",这就是时钟拉伸。

理解了开漏,你自然就明白I2C的速率为什么被卡在几百KHz到几MHz。上拉电阻要对总线电容充电,阻值越小上升沿越快,但功耗越大、灌电流也越大,不能一味减小。所以I2C走线不能太长,超过十厘米就开始有信号完整性问题。

I2C的数据帧格式说穿了很简单:起始条件,SCL高电平时SDA由高到低;然后是8位地址加读写位;最后是应答位。从机在地址匹配后拉低SDA回ACK,主机收到ACK才继续发数据。读操作的最后,主机回一个NACK再给停止条件,意思是"够了,别发了"。有热词搜"i2c自由数据模式",很多芯片手册里叫无格式或连续读写模式,本质是地址自动递增,主机不用每字节重发地址,非常适合读写EEPROM这类连续存储块。7位地址在不扩展的情况下最多挂127个设备,如果总线上必须挂多颗同地址芯片,就得分组供电或者用I2C多路复用器,这也是"i2c控制的多路复用"解决的问题。

1.4 SPI:全双工、高速、片选为王

SPI全称串行外设接口,最少四根线:SCLK时钟、MOSI主机出从机入、MISO从机出主机入、CS片选,通常低有效。跟I2C最大的不同在于,SPI是全双工,主机时钟每打一拍,同时移出一位、移入一位,效率比I2C高出一截。速率上限通常也不受电容充电约束,几十MHz很常见,配DMA可以持续搬数据。

SPI的四种工作模式由CPOL和CPHA组合决定。CPOL是时钟空闲电平,CPHA决定采样沿,新手最常犯的错就是把主从双方的模式配反,波形看着似乎有传输,但数据全是乱的。多从机场景有两种典型拓扑:独立片选,每个从机一根CS;菊花链,数据从第一个从机串到下一个。独立片选占用引脚多但灵活,菊花链省引脚但延迟和软件寻址都变复杂。

SPI没有标准地址,也没有ACK机制。发一个字节,从机到底收没收到,协议层根本不管。所以很多器件会扩展出状态寄存器、忙引脚、中断脚,本质上都是在弥补"无应答"的缺失。这颗雷新手踩得特别多,选型时就要想清楚:如果对端不支持状态检查,上层就得自己加校验。

1.5 I2S:名字像I2C,其实是音频专用道

I2S是飞利浦为数字音频定义的接口,名字和I2C就差一个字母,常被新手混为一谈,其实它和I2C除了都是串行,没有任何亲缘关系。I2S有三根核心线:BCLK位时钟,LRCK/WS左右声道选择也叫帧同步,SD串行数据,可以有多根。标准I2S格式里,数据要比WS的变化晚一个BCLK,WS低电平通常代表左声道,高电平代表右声道。

很多人配I2S只盯着BCLK和LRCK,漏了MCLK主时钟。很多音频Codec要求MCU提供MCLK,比如12.288MHz或24.576MHz,是采样率整数倍。对新手来说这是最隐蔽的坑:逻辑分析仪一看,I2S数据流波形正常,喇叭就是不出声,或者只出沙沙声,先查MCLK有没有给。

I2S的数据格式也不只有一种,常见的有I2S标准、左对齐、右对齐,位数有16/24/32bit,DMA配置时数据宽度和声道数搞错,声音就会变调或左右颠倒。热词里那个"esp32-c3 i2s输出"经常翻车,原因大多不是硬件,而是slot、位数和时钟分频没对齐。波形能看到但声音不对,去对照Codec手册的音频格式映射表,基本都能找到答案。

2. 参数横评和选型逻辑:别再凭感觉挑协议

2.1 一张表看穿速度、引脚和拓扑

先把四种协议的硬指标摆在一起,后面选型思路都围绕这张表展开。

对比项UARTI2CSPII2S
信号线2根(TX/RX)2根(SDA/SCL)4根以上(SCLK/MOSI/MISO/CS)3根以上(BCLK/LRCK/SD)
同步方式异步同步同步同步
拓扑点对点(可转RS485)多设备总线(带地址)多从机(独立片选/菊花链)点对点/少设备
常见速率0.3~12Mbps(看电平)100k/400k/1M/3.4M几MHz~几十MHz最高几十MHz
双工方式全双工半双工全双工全双工(数据流方向固定)
应答机制无有ACK/NACK、时钟拉伸无无
传输距离较长短(板级)短(板级)短(板级)
典型场景调试日志、GPS、MODBUS传感器、EEPROM、PMBus、SMBusFlash、LCD、ADC、FPGA配置音频Codec、DAC/ADC

2.2 选型要看这五件事,别只看速率

第一件是引脚预算。一个项目能用多少IO,能不能给每颗外设分到独立的时钟线和片选,往往比速率瓶颈更先卡脖子。引脚紧张又要挂多个从机,I2C两根线就能解决;引脚充足且希望以最快速度搬数据,直接SPI。

第二件是从机数量和寻址方式。总线上挂一堆同型号传感器,又没有地址跳线区分,用I2C会非常痛苦,要么靠多路复用器,要么改软件枚举;SPI就简单粗暴许多,每个从机一根CS,但每多一个从机就多占一个GPIO。

第三件是需不需要"确认收到"。I2C自带ACK,读写EEPROM时每字节都能感知从机是否响应,调试阶段极其友好;SPI无应答,一次突发传完才发现错误,就得靠CRC这类上层协议兜底。如果设备可靠度要求高、希望能即时发现通信异常,I2C的确认机制是实打实的优势。

第四件是板内还是板间。I2C和SPI都是板级总线,走线长了信号完整性就开始恶化,尤其是I2C的上拉电阻和长线电容组合,波形上升沿会明显变缓。UART走线随意得多,配上RS485或CAN解决板间通信。很多路由器、服务器主板保留UART做带外管理,靠的就是这个长距离优势。

第五件,如果数据是单向音频流、又需要和采样率同步的帧时钟,直接选I2S,别拿SPI硬扛。SPI也能传音频比特流,但你要自己在软件里对齐帧、管理左右声道和位钟,纯粹是自我折磨。

2.3 影响范围:从传感器到存储再到音频,一线定全局

选协议影响的往往不只是某颗器件的接法,而是整个系统架构。传感器领域,温湿度、光照、触摸屏、陀螺仪大量用I2C,因为低功耗、少引脚、多地址。像GT911这类触摸屏控制器通常挂I2C,通信失败的原因往往不是时序不对,而是地址或复位时序没处理对,后面章节展开。

存储和显示领域,Flash、LCD、SD卡几乎都是SPI,既要速率又要QSPI扩展。主控端RK3588这类SoC上,SPI接口经常接NOR Flash做启动引导,PCIe/NVMe存系统盘,一套混合存储方案里既有SPI又有高速接口,各管一段。音频领域则是很典型的组合拳:Codec的控制寄存器走I2C,音频数据流走I2S,两条总线并行,各司其职。

电源管理领域有个容易搞混的点:PMBus和SMBus底层都是I2C。PMBus是电源管理协议跑在I2C物理层之上,给电源模块读写寄存器、调压、读状态用的,它和I2C不是并列关系,而是上下层关系。有人问"pmbus和i2c区别",一句话:I2C是传输层协议,PMBus是应用层命令集。还有"linux phy不使用mdio使用i2c"的情况,部分PHY芯片确实没有MDIO管理口而是I2C管理口,这说明选型永远要跟着芯片能力走,不能只看习惯。

3. 实操:从波形到代码,亲手验证四种差异

3.1 逻辑分析仪抓波形:四种协议一眼区分

总有人问我怎么快速判断一条总线上跑的是什么协议。我的答案始终是:抓波形。逻辑分析仪挂好参考地,所有通道全部接上,先看有几根活跃线:

  • 一根线,空闲是高电平,偶尔来个起始位加一串等间隔脉冲,多半是UART;
  • 两根线,一个明显是时钟、另一个是数据,而且数据在时钟高电平期间保持稳定,是I2C;
  • 四根以上线,时钟速率快,每传输一个字节片选都会拉低再拉高,是SPI;
  • 三根线,其中一根按固定频率连续翻转,另一根是低得多的低频方波,第三根是数据,基本就是I2S。

I2C解码非常直观:起始条件后面跟着地址字节,低7位是地址,第8位是读写位,之后每个字节后面都有ACK位。用逻辑分析仪软件解码时,注意设对电压阈值,1.8V的总线别拿3.3V阈值去解,不然结果乱七八糟。热词里有人搜"i2s逻辑分析仪波形",标准做法是把BCLK设为采样参考,在LRCK边沿附近看数据的起始位置,确定是I2S标准还是左对齐。I2S数据是MSB先出,按这个顺序拼位就对了。

3.2 STM32上的初始化配置示例:同等配置逻辑全覆盖

以STM32 HAL库为例,演示四种外设的最小初始化。代码不是让你照抄,而是体会每种协议的配置逻辑差异。

UART,USART1,波特率115200:

UART_HandleTypeDef huart1; huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; HAL_UART_Init(&huart1);

UART的初始化逻辑最少,因为协议本身简单,波特率、数据位、停止位、校验设置完就结束。

I2C,I2C1,400kHz,7位地址主机模式:

I2C_HandleTypeDef hi2c1; hi2c1.Instance = I2C1; hi2c1.Init.ClockSpeed = 400000; hi2c1.Init.OwnAddress = 0; hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; HAL_I2C_Init(&hi2c1);

注意I2C即使做主机也要设置OwnAddress,还要挑地址模式。之后调用HAL_I2C_Master_Transmit/Receive时,传入的参数就是目标从机地址。

SPI,SPI1,Mode 0,8位,主模式:

SPI_HandleTypeDef hspi1; hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_64; hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1);

SPI的配置项在四者里最多,CPOL/CPHA、NSS软硬件、分频系数、MSB/LSB每一项都可能决定通信成败。

I2S,SPI2复用为I2S,飞利浦标准,16bit,48kHz:

I2S_HandleTypeDef hi2s2; hi2s2.Instance = SPI2; hi2s2.Init.Mode = I2S_MODE_MASTER_TX; hi2s2.Init.Standard = I2S_STANDARD_PHILIPS; hi2s2.Init.DataFormat = I2S_DATAFORMAT_16B; hi2s2.Init.MCLKOutput = I2S_MCLKOUTPUT_ENABLE; hi2s2.Init.AudioFreq = I2S_AUDIOFREQ_48K; hi2s2.Init.CPOL = I2S_CPOL_LOW; HAL_I2S_Init(&hi2s2);

I2S在STM32上是挂在SPI外设上的,所以Instance还是SPI2,但配置项换成了Standard、DataFormat、AudioFreq、CPOL这些I2S专属选项,MCLK输出也在这里控制,音频分频由AudioFreq自动算。

实际调试顺序我建议固定下来:先用UART输出日志确认系统活着,再用I2C读一颗设备的ID验证总线,接着用SPI读写Flash,最后才轮到I2S出声音。每一步都有大量可观测输出,出错了也容易定位。

3.3 硬件片选与软件片选的取舍

"SPI硬件片选与软件片选"这个问题几乎每次都会被问。硬件片选是SPI外设自己控制的NSS引脚,软件片选是代码手动拉一个GPIO。我的结论是:

硬件片选适合高速传输和需要极低延时的场景,比如ADC连续采样、Flash大数据块读写,因为外设能在字节之间精确控制片选脉冲,省去GPIO操作的额外开销。缺点是硬件NSS引脚固定,一旦被其他功能复用,就变成被迫切到软件片选。

软件片选灵活,任何GPIO都能做CS,移植性好。但在高速率下,两次CS之间的GPIO翻转延迟会拉长字节间隙,极端情况下会被从机误判成一帧中的空闲段。软件片选还有一个特别容易忽略的操作顺序:必须先拉低CS,再启动SPI外设传输,传完最后一个字节后马上拉高。顺序反了,从机把CS边沿当帧边界,收到的数据必然错位。

热词里那个"cs最小能做到多少"其实是在问片选脉冲最小宽度。芯片手册通常会给CS Setup和CS Hold时间,比如ADC要求CS低电平至少保持几百纳秒。如果软件模拟时GPIO翻转太慢,或者SPI时钟太快导致CS脉冲太窄,采到的ADC数据就会偶尔跳变。用示波器或逻辑分析仪看CS和SCLK的相对时序,很容易确认问题。

4. 踩坑实录:这四兄弟的常见问题排查

4.1 I2C:地址、死锁、从机不响应

I2C的坑基本集中在三个地方。

地址不对是很常见的一种。GT911触摸屏有多个7位地址,手册有时写7位、有时写含读写位的8位形式,对不齐就会一直NACK。注意I2C地址有两种表示法:纯7位地址和包含读写位的字节,两者数值差一位。比如0x28是7位地址,换算成字节形式可能是0x50,背错一位就全错。

SDA死锁是最让人抓狂的。主机收到NACK或者程序中途停住,总线停在SDA为低,后面所有通信全部失败。这是因为某个设备把SDA拉低了却没人释放。排查手法很简单:连续给出9个SCL时钟脉冲,让从机把未完成的字节发完;实在不行,把SCL和SDA临时配成GPIO输出高,人为复位总线。

时钟拉伸被卡则多出现在软件模拟I2C的场景。某些低速从机会拉低SCL让主机等待,如果你写代码时没检查SCL是否回到高就直接发下一个时钟,通信就会卡死。软件模拟的基本功就是:每次驱动时钟之前,先等SCL释放回来。

顺带提一个Windows下的HID问题,"HID设备找不到足够资源(代码12)",多半是触摸屏同时挂I2C和HID且驱动资源冲突,先卸载过时驱动、换个USB口或改BIOS的HID选项通常能缓解,跟总线本身关系不大。

4.2 SPI:模式错位、片选毛刺、采样噪声

SPI的坑第一是模式错配。从机手册写明是Mode 0还是Mode 3,主从必须一致。一个隐蔽现象是:主机配成Mode 1,从机是Mode 0,数据偶尔对、偶尔全错,因为双方在相位和极性的组合上始终差半拍。手头没有逻辑分析仪,就先用示波器看SCK边沿和MISO数据变化的相对位置,能看出端倪。

第二是片选毛刺。软件片选的GPIO如果在上电初始化时默认输出低,会导致从机误触发传输。初始化时务必先把CS引脚配成高电平,再去初始化SPI外设。硬件NSS如果被其他复用拉低拉高,也有类似问题。

第三是ADC采样噪声。FPGA或MCU通过SPI接高速ADC时,SCK本身就是采样时钟的一部分,CS的建立保持时间、SCK走线、电源纹波都会直接影响采样精度。我遇到过SPI 10MHz读ADC偶尔出毛刺的情况,用示波器一查,SCK上升沿和CS下降沿只差了2ns,手册要求至少5ns,把速率降到6.6MHz并加了点RC延时就好了。注意,SPI高速并不是只调分频器那么简单,模拟时间预算才是真正的门槛。

4.3 UART:波特率误差和驱动装上识别不了

UART的坑相对温和,但也有隐蔽的。

波特率误差始终存在。STM32的PCLK如果不是波特率的整倍数,实际波特率和理论值就对不上。误差超过2%~3%就有风险。配置时看HAL库算出来的USARTDIV余数,尽量选误差小的组合。921600这类高速率,误差被放大得更明显,调试时乱码先查这一项。

USB转串口驱动识别不了,FT231X和FT232R在Windows下不识别,大概率不是硬件坏了,是驱动版本和系统兼容问题。旧版驱动在Win10/Win11上经常会弹"设备无法启动(代码10)",或者干脆不识别。换装FTDI官方最新驱动,确认设备管理器的VID/PID是0403/6001就行。别一看到"无法识别"就怀疑芯片是假货,先看描述符。

乱码的原因一般是波特率不一致、电平不匹配或者共地问题。共地这个坑太容易被忽视:两个板子各插一个USB转串口,却不共地,数据就偶尔对偶尔错,经常被误判成时序问题。两根走线的地没连上,电平参考点漂移,什么波特率都救不回来。

4.4 I2S:时钟没给全、格式错位、左右声道颠倒

I2S的坑第一是MCLK缺失。很多Codec必须要有MCLK才工作,但新手往往只配BCLK和LRCK。表现形式是I2S波形看起来完全正常,喇叭不出声或只有高频噪声。

第二是声道与slot错位。16bit数据放进32bit slot,是左对齐还是右对齐,Codec手册一定有讲,配错了声音就变调,或者感觉"慢速倒放"。像ESP32-C3这类芯片做I2S输出时,DMA的位宽和I2S配置的slot宽度必须统一,不然数据被错位截断。

第三是音频格式不匹配。I2S和DSP/PCM格式之间通常只差一个位的偏移,声音表现是轻微沙沙加正常说话混合。排查套路很固定:逻辑分析仪抓LRCK下降沿和数据第一位,对照Codec手册的时机图。凡是出现"波形正常但声音不对"的,99%是格式配错了。

最后分享一个我自己一直保持的习惯:任何新板子到手,先抓一遍所有总线的波形,再动手写驱动。抓波形不是为了炫技,而是把"协议到底在干什么"这个问题在落地前就先在心里种下答案。每次改协议配置,也只动一个变量,再抓波形看变化,而不是一把梭全改完再慢慢猜。这个习惯帮我省下的排查时间,比我写这篇对比用的时间多得多。希望你也试试。

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

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

立即咨询