SPI、I2C、UART通信协议选型指南:从原理到实战 1. 三种通信协议到底在解决什么问题嵌入式开发干久了你会发现一个很有意思的现象很多人能背出SPI有四根线、I2C有两根线、UART是异步的但一到实际项目选型就犯难。我见过不少工作两三年的工程师画板子的时候随手选一个调试不通了再回头翻手册浪费大量时间。问题的根源不在于记不住定义而在于没有从“数据怎么在芯片之间流动”这个角度去理解它们。SPI、UART、I2C本质上解决的是同一类问题让两颗或多颗芯片通过有限的引脚可靠地交换数据。但它们的解题思路完全不同。UART像是两个人约好时间聊天没有时钟线全靠事先约定好的语速I2C像是一条公交线路多个站点共享两条线每个站点有唯一地址SPI则像是点对点的专用高速公路速度快、线多、但只能一对一或一对多通过片选切换。这篇文章适合谁看如果你正在做单片机项目、画原理图、写驱动或者面试时被问到通信协议选型那这篇内容就是给你准备的。我会从实际项目出发把三种协议的底层逻辑、参数计算、代码实现、踩坑经验全部拆开讲清楚。不堆砌手册上的定义只讲你在实际工作中真正用得上的东西。2. 三种通信协议的核心原理拆解2.1 UART没有时钟线靠什么保证数据不错乱UART的全称是通用异步收发器关键词在“异步”两个字。它没有时钟线发送方和接收方各自用自己的时钟源靠预先约定的波特率来同步。你可以把它想象成两个人用摩尔斯电码通信事先说好每个点持续0.1秒那双方就都按这个节奏来。那问题来了如果发送方的时钟和接收方的时钟有偏差怎么办这就是UART最核心的容错机制——起始位和停止位。每一帧数据以起始位低电平开头以停止位高电平结尾。接收方在检测到起始位的下降沿时会重新对齐自己的采样时钟这样每次传输都能重新同步累积误差不会超过一帧。一帧UART数据的典型结构是这样的部分位数作用起始位1位标志一帧数据的开始低电平数据位5-9位实际传输的数据通常8位校验位0或1位奇偶校验可选停止位1或2位标志一帧结束高电平波特率的选择有个经验公式波特率误差要控制在2%以内否则接收方采样点会偏移到数据位的边缘导致误码。比如你用8MHz晶振产生9600波特率分频系数是8000000/(16×9600)52.08取整52的话实际波特率是8000000/(16×52)9615.38误差0.16%完全没问题。但如果用7.3728MHz晶振分频系数是48刚好整除误差为0这也是为什么很多老式单片机推荐用7.3728MHz或11.0592MHz晶振——就是为了让常用波特率能整除。注意UART的TTL电平和RS-232电平是两回事。单片机的TX/RX引脚输出的是TTL电平0V/3.3V或5V而传统串口用的是RS-232电平正负12V左右。直接对接会烧芯片中间必须加电平转换芯片。2.2 I2C两根线挂多个设备地址冲突怎么破I2C最吸引人的地方就是两根线可以挂几十个设备。SDA是数据线SCL是时钟线所有设备都并联在这两条线上。每个设备有唯一的7位地址主机发起通信时先发地址匹配的设备才会响应。但这里有个实际问题I2C的SDA和SCL都是开漏输出也就是说设备只能把线拉低不能主动拉高。线要变高必须靠上拉电阻。这就引出了I2C最关键的硬件设计点——上拉电阻的选型。上拉电阻太大上升沿太慢高速通信时波形还没恢复到高电平就被拉低了上拉电阻太小功耗大而且灌电流可能超过器件的承受能力。计算公式是这样的上升时间 t_r ≈ 0.847 × R_p × C_bus其中C_bus是总线电容包括PCB走线电容和器件引脚电容通常按每根线100pF估算。I2C标准模式100kHz要求上升时间小于1000ns快速模式400kHz要求小于300ns。假设C_bus200pF快速模式下R_p 300ns / (0.847 × 200pF) ≈ 1.77kΩ。所以4.7kΩ在快速模式下可能偏大2.2kΩ或1.5kΩ更合适。但还要检查灌电流3.3V / 1.5kΩ 2.2mA大多数I2C器件能承受3mA以上没问题。I2C还有一个容易忽略的细节时钟拉伸。从设备如果处理不过来可以把SCL线拉低强制主机等待。这在读传感器数据时很常见比如某些温湿度传感器转换一次需要几十毫秒期间就会拉伸时钟。如果你的I2C驱动不支持时钟拉伸通信就会失败。2.3 SPI四根线换来的高速与灵活SPI是三种协议里最快的没有地址帧开销没有起始停止位数据可以连续流式传输。它的四根线分别是SCLK时钟由主机产生MOSI主机输出从机输入MISO主机输入从机输出CS片选每个从机一根低电平有效SPI的灵活性体现在模式配置上。时钟极性CPOL和时钟相位CPHA组合出四种模式模式CPOLCPHA采样沿移位沿Mode 000上升沿下降沿Mode 101下降沿上升沿Mode 210下降沿上升沿Mode 311上升沿下降沿选错模式是SPI调试中最常见的问题。症状是读出来的数据整体偏移一位或者全是0xFF/0x00。我的经验是先查从机手册确认模式再用逻辑分析仪抓波形对照不要凭感觉试。SPI没有应答机制主机发了数据不知道从机收没收到。所以高速通信时要注意信号完整性SCLK频率太高、走线太长、没有阻抗匹配都会导致误码。一般PCB上SPI走线控制在10cm以内超过的话降低时钟频率或者加串联电阻。3. 实际项目中的选型逻辑与参数计算3.1 一张表看懂三种协议的适用场景维度UARTI2CSPI线数2TX/RX2SDA/SCL4SCLK/MOSI/MISO/CS拓扑点对点多主多从一主多从寻址方式无7位/10位地址片选线典型速率9.6k-115.2kbps100k-3.4Mbps1M-100Mbps时钟异步同步同步应答机制无有无引脚开销低低高每个从机一根CS适用距离板内/短距离板内板内选型的核心逻辑是看数据量和设备数量。数据量小、设备少、引脚紧张优先I2C数据量大、实时性要求高选SPI两个设备之间简单通信UART最省事。3.2 速率与距离的取舍什么时候该降速很多人调通信不通就怀疑代码其实信号完整性才是高频出问题的重灾区。我整理了一个经验对照表协议速率安全走线长度普通FR4超过后的措施UART115.2kbps1-2米加驱动芯片I2C100kHz30-50cm减小上拉电阻I2C400kHz10-20cm减小上拉电阻缩短走线SPI1MHz20-30cm串联22Ω电阻SPI10MHz5-10cm阻抗匹配缩短走线SPI50MHz5cm需要专业SI设计这个表不是绝对的但能帮你快速判断问题方向。我曾经遇到一个项目SPI时钟设到20MHz走线15cm数据随机出错。降到5MHz就稳定了后来在时钟线上串了33Ω电阻又试到10MHz也没问题。串联电阻的作用是抑制反射成本几分钱效果立竿见影。3.3 多设备共存时的地址与片选管理I2C的地址冲突是个经典问题。比如你用了两个同型号的温湿度传感器出厂地址一样怎么办大多数这类传感器提供地址引脚可以配置成不同的地址。如果只有两个可选地址那就只能挂两个。再多的话需要I2C多路复用器比如TCA9548A一路变八路每路可以挂相同地址的设备。SPI的片选管理相对简单每个从机一根CS线主机拉低哪个就选哪个。但要注意CS的默认状态必须配置为高电平否则上电瞬间所有从机都被选中MISO线会打架。我在STM32上就踩过这个坑初始化时忘了把CS引脚设为高结果两个SPI从机同时输出读出来的数据全是乱的。4. 代码层面的实现要点与调试方法4.1 UART中断接收的缓冲区设计UART接收最怕数据溢出。主循环处理慢串口数据一直来硬件缓冲区只有一两个字节很快就丢了。解决方案是环形缓冲区中断。#define UART_BUF_SIZE 256 typedef struct { uint8_t buffer[UART_BUF_SIZE]; volatile uint16_t head; volatile uint16_t tail; } ring_buf_t; ring_buf_t uart_rx_buf; void UART_IRQHandler(void) { if (UART_GetITStatus(UART_IT_RXNE)) { uint8_t data UART_ReceiveData(); uint16_t next (uart_rx_buf.head 1) % UART_BUF_SIZE; if (next ! uart_rx_buf.tail) { uart_rx_buf.buffer[uart_rx_buf.head] data; uart_rx_buf.head next; } // 如果next tail说明缓冲区满了丢弃数据 } } uint8_t uart_read_byte(uint8_t *data) { if (uart_rx_buf.head uart_rx_buf.tail) { return 0; // 空 } *data uart_rx_buf.buffer[uart_rx_buf.tail]; uart_rx_buf.tail (uart_rx_buf.tail 1) % UART_BUF_SIZE; return 1; }这个设计的精髓在于head和tail的追赶逻辑。head由中断更新tail由主循环更新两者相等表示空head1等于tail表示满。这样不需要加锁中断和主循环各管各的指针天然线程安全。注意head和tail必须用volatile修饰否则编译器优化后可能把变量缓存到寄存器里导致主循环读不到中断的更新。4.2 I2C读写时序的软件模拟与硬件外设选择I2C有两种实现方式硬件外设和软件模拟。硬件外设省CPU但不同芯片的I2C外设有各种奇怪的bug比如某些型号在从机拉伸时钟时会卡死。软件模拟灵活任何GPIO都能用但占用CPU时间。软件模拟I2C的核心是精确控制SDA和SCL的时序。以写一个字节为例void i2c_start(void) { SDA_HIGH(); SCL_HIGH(); delay_us(4); SDA_LOW(); delay_us(4); SCL_LOW(); delay_us(4); } void i2c_write_byte(uint8_t byte) { for (int i 0; i 8; i) { if (byte 0x80) SDA_HIGH(); else SDA_LOW(); byte 1; delay_us(2); SCL_HIGH(); delay_us(4); SCL_LOW(); delay_us(2); } // 第9个时钟读ACK SDA_INPUT(); SCL_HIGH(); delay_us(4); // 读SDA电平判断ACK SCL_LOW(); delay_us(2); SDA_OUTPUT(); }delay_us的具体值取决于你的I2C速率。100kHz的话一个时钟周期10us高电平5us低电平5us。但实际调试时上升沿的时间由外部上拉电阻决定软件延时只是保证低电平持续时间足够。如果上拉电阻太大SCL还没升到高电平你就开始下一步了从机根本认不到时钟。4.3 SPI的DMA传输与片选控制SPI高速传输必须用DMA否则CPU一直在搬数据什么都干不了。但DMA和片选配合有个坑DMA传输完成中断里才能拉高CS如果提前拉高最后一个字节还没发完就被截断了。void spi_dma_transfer(uint8_t *tx, uint8_t *rx, uint16_t len) { CS_LOW(); DMA_SetAddr(DMA_CH_TX, tx); DMA_SetAddr(DMA_CH_RX, rx); DMA_SetLen(DMA_CH_TX, len); DMA_SetLen(DMA_CH_RX, len); DMA_Enable(DMA_CH_TX); DMA_Enable(DMA_CH_RX); SPI_EnableDMA(SPI1, SPI_DMA_TX | SPI_DMA_RX); SPI_Enable(SPI1); } void DMA_TX_Complete_IRQHandler(void) { // 等待RX也完成 while (!DMA_GetFlag(DMA_CH_RX, DMA_FLAG_TC)); SPI_Disable(SPI1); CS_HIGH(); DMA_ClearFlag(DMA_CH_TX, DMA_FLAG_TC); DMA_ClearFlag(DMA_CH_RX, DMA_FLAG_TC); }这里的关键是等RX也完成再拉高CS。因为SPI是全双工TX发完的时候RX可能还在收最后几个字节。只等TX完成就拉高CS从机会认为通信结束后面的数据就丢了。5. 常见问题排查与避坑经验5.1 通信完全无反应的排查顺序遇到通信不通按这个顺序查能解决80%的问题电源和地用万用表量从机供电是否正常地是否共地。我遇到过好几次是忘了接从机的地信号参考点都没有怎么可能通。引脚配置TX/RX有没有配反I2C的SDA/SCL有没有配成开漏SPI的MISO有没有配成输入这些在STM32的CubeMX里点错了生成代码就是错的。时钟使能外设时钟开了没GPIO时钟开了没DMA时钟开了没漏一个就全盘不动。上拉电阻I2C的SDA/SCL必须有上拉很多新手以为芯片内部有其实大多数没有。片选信号SPI的CS有没有拉低有些从机要求CS在每次传输前有下降沿不能一直拉低。5.2 数据偶尔出错但大部分正常这种“时好时坏”的问题最难查通常是时序裕量不够。可能的原因波特率误差接近2%的临界值换晶振或调整分频系数I2C上拉电阻偏大上升沿太慢减小电阻试试SPI时钟太快降低频率看是否改善电源纹波大通信瞬间电流变化导致电平抖动加去耦电容我的经验是先用逻辑分析仪抓波形看时钟和数据的关系是否满足从机手册的建立/保持时间要求。没有逻辑分析仪的话用示波器看SCL和SDA的边沿如果上升沿明显变缓就是上拉电阻的问题。5.3 多设备通信时的相互干扰I2C总线上挂多个设备一个设备出问题可能把整条总线拉死。比如某个设备死机后一直拉低SDA主机发什么它都不理其他设备也没法通信。解决方案是加I2C缓冲器或复用器把设备分组隔离。或者用总线恢复机制主机发送9个时钟脉冲让从机把剩余的数据位移完释放SDA线。SPI多从机的话注意MISO线的竞争。同一时刻只能有一个从机输出MISO如果两个从机的CS同时有效一个输出高一个输出低就会短路。所以CS的控制逻辑必须严格任何时候只能拉低一个。5.4 常见问题速查表现象可能原因排查方法完全无数据电源/地/时钟/引脚配置万用表示波器逐项检查数据固定为0xFFMISO未接或从机未响应检查CS和从机供电数据固定为0x00SDA/SCL接反或上拉缺失交换线序测试数据整体偏移一位SPI模式选错切换CPOL/CPHA四种组合偶尔误码时序裕量不足降速或减小上拉电阻多设备时好时坏地址冲突或CS竞争逐个设备单独测试高速时出错信号反射串联电阻或缩短走线6. 从项目实战中积累的选型与调试心得6.1 什么时候优先选I2C而不是SPI很多人觉得SPI快就无脑选SPI其实不对。引脚资源紧张的时候I2C的优势非常明显。比如你要挂一个EEPROM、一个温湿度传感器、一个OLED屏用SPI需要3根共用线3根CS6根线用I2C只需要2根线。PCB走线少、布线简单、成本低。而且I2C有应答机制主机发了数据从机必须回ACK没回就说明通信失败可以重试。SPI没有这个机制发了就发了从机收没收到主机不知道。对于可靠性要求高但速度要求不高的场景I2C更合适。6.2 UART在调试中的不可替代性不管项目里用不用UART做正式通信调试串口几乎是必备的。打印日志、查看变量、输出错误码没有比UART更简单的了。我习惯在项目初期就把UART调试口配好用printf重定向后面调什么外设都方便。但要注意调试串口不要用太高的波特率。115200足够用了再高的话如果USB转串口芯片质量一般容易丢数据。而且调试信息本身不需要那么快省下来的CPU时间干正事。6.3 逻辑分析仪是调通信协议的最佳投资我买过最值的调试工具就是逻辑分析仪几百块钱的八通道版本支持I2C、SPI、UART协议解码。抓一段波形软件直接告诉你地址、数据、ACK位比对着示波器数时钟脉冲效率高十倍。比如I2C通信失败逻辑分析仪一看主机发了地址0x48从机没回ACK。那就说明从机没响应要么地址错了要么从机没上电要么从机死机了。方向立刻明确不用瞎猜。6.4 通信协议的未来高速与低功耗的平衡现在的新芯片越来越多地采用组合方案比如用SPI做高速数据采集用I2C做配置和状态读取用UART做调试输出。没有一种协议能通吃所有场景理解每种协议的优缺点根据实际需求灵活组合才是正确的做法。另外I3C作为I2C的升级版在保持两根线的同时把速率提到了12.5Mbps还支持带内中断。虽然目前普及度还不高但以后可能会逐渐替代I2C在传感器领域的地位。如果你的项目周期长选型时可以关注一下芯片是否支持I3C。我在实际项目中总结出一条原则先确定数据量和实时性要求再确定设备数量和引脚预算最后才选协议。反过来先选协议再凑需求往往会在后期发现各种不匹配。通信协议没有好坏之分只有合不合适。把三种协议的底层逻辑吃透遇到问题能快速定位比背一百遍定义都有用。