☰
SPI、UART、I2C总线选型指南:从原理到实战的嵌入式通信全解析
2026/10/18 1:48:23 网站建设 项目流程

1. 三种总线到底该怎么选:先搞清楚它们各自在解决什么问题

嵌入式开发干久了,你会发现一个很有意思的现象:新手最怕的是写代码,老手最怕的是选通信方式。SPI、UART、I2C这三兄弟,几乎每个做硬件驱动的人都会碰到,但真正能把它们用对、用好的,其实没想象中那么多。我见过太多项目,前期随便选了一个总线,后期发现设备数量不够、速率上不去、或者引脚资源紧张,最后不得不推翻重来。

这篇文章想做的事情很直接:把SPI、UART、I2C这三种总线从底层原理到实际选型,再到代码实现和调试排错,完整地梳理一遍。不管你是刚接触单片机的新手,还是已经做过几个项目但总觉得理解不够透彻的开发者,都能从中找到对自己有用的东西。我会尽量用生活化的类比来解释那些看起来枯燥的时序图和寄存器配置,同时也会分享一些我在实际项目中踩过的坑和总结出来的经验。

先说一个核心观点:这三种总线没有谁绝对优于谁,它们各自有明确的适用场景。选错了不是总线的问题,是你没搞清楚需求。就像你不会开卡车去买菜,也不会骑自行车去搬家一样,工具要匹配场景。

在展开之前,先快速建立一个整体认知。UART是最简单的点对点异步通信,两根线就能搞定,适合两个设备之间的低速数据传输。I2C用两根线挂载多个设备,靠地址区分,适合传感器这类低速外设。SPI用四根线(有时三根),速度最快,适合需要高速数据吞吐的场景,比如显示屏、Flash存储。这三个各有各的生态位,下面逐个拆开讲。

2. UART:最容易被低估的“老黄牛”

2.1 UART的通信原理与帧结构

UART全称是通用异步收发器,关键词在“异步”两个字上。什么叫异步?就是通信双方没有共享的时钟线,全靠事先约定好的波特率来同步。这就像两个人约好每隔一秒说一个字,虽然没有节拍器,但只要双方手表走得一样准,就能正常交流。

一个完整的UART数据帧包含以下几个部分:起始位、数据位、校验位、停止位。起始位是一个逻辑低电平,告诉对方“我要开始发数据了”。数据位通常是5到9位,最常见的是8位。校验位用于简单的错误检测,可以选奇校验、偶校验或者不要校验。停止位是逻辑高电平,表示这一帧结束了。

这里有个容易搞混的地方:波特率和比特率不是一回事。波特率指的是每秒传输的码元数,比特率是每秒传输的比特数。在UART中,每个码元携带1比特信息,所以两者数值相等,但概念不同。实际配置的时候,你只需要关心波特率就行了,常见的有9600、115200、921600等。

注意:通信双方的波特率必须一致,误差一般不能超过2%到3%。如果一方用115200,另一方用9600,收到的全是乱码。这就像一个人说中文一个人说英文,频率完全对不上。

2.2 硬件连接与电平标准

UART的硬件连接非常简单,发送端TX接接收端RX,接收端RX接发送端TX,再加上共地就可以了。注意这里是交叉连接,不是TX接TX。我见过不少新手第一次接线的时候直接对着接,结果怎么都收不到数据,排查半天才发现是线接反了。

电平标准方面,常见的有TTL电平、RS-232电平和RS-485电平。TTL电平就是单片机引脚直接输出的0V和3.3V(或5V),适合板内短距离通信。RS-232使用负逻辑,-3V到-15V表示逻辑1,+3V到+15V表示逻辑0,抗干扰能力更强,传输距离也更远。RS-485使用差分信号,抗干扰能力最强,适合工业环境下的长距离通信。

实际项目中,如果你只是两块板子之间短距离通信,TTL电平就够了。但如果要穿过嘈杂的工业环境,或者传输距离超过几米,就得考虑加电平转换芯片了。我之前做过一个项目,两块板子之间大概5米的距离,用TTL电平直接连,数据丢包率很高,后来加了RS-485转换芯片,问题立刻解决。

2.3 代码实现:从寄存器配置到中断接收

以常见的32位单片机为例,UART的初始化一般包括以下几个步骤:使能时钟、配置引脚复用、设置波特率、配置数据格式、使能收发功能。下面是一个典型的初始化代码框架:

void uart_init(uint32_t baudrate) { // 1. 使能UART和GPIO时钟 RCC->APB2ENR |= RCC_APB2ENR_USART1EN; RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 2. 配置TX为复用推挽输出,RX为浮空输入 GPIOA->CRH &= ~(0xFF << 4); GPIOA->CRH |= (0x0B << 4); // TX GPIOA->CRH |= (0x04 << 8); // RX // 3. 设置波特率 // 假设系统时钟72MHz,波特率115200 // 波特率分频值 = 72M / (16 * 115200) = 39.0625 // 整数部分39,小数部分0.0625*16=1 USART1->BRR = (39 << 4) | 1; // 4. 配置数据格式:8位数据,1位停止位,无校验 USART1->CR1 &= ~USART_CR1_M; // 8位数据 USART1->CR2 &= ~USART_CR2_STOP; // 1位停止位 // 5. 使能收发和UART USART1->CR1 |= USART_CR1_TE | USART_CR1_RE | USART_CR1_UE; }

这段代码里最需要关注的是波特率分频值的计算。很多人直接用库函数,不太关心底层是怎么算的,但一旦遇到通信异常,理解这个计算过程能帮你快速定位问题。公式是:分频值 = 系统时钟 / (16 × 波特率)。为什么是16?因为UART在接收的时候会对每个位进行16倍过采样,取中间几次的值来判断是0还是1,这样能有效抑制噪声。

中断接收的配置也很关键。一般来说,接收中断和空闲中断配合使用效果最好。接收中断每收到一个字节触发一次,空闲中断在总线空闲时触发,可以用来判断一帧数据是否接收完毕。这种组合方式在处理不定长数据帧的时候特别有用。

2.4 UART实操中的常见坑与排查思路

第一个坑:波特率误差累积。如果你的系统时钟不是波特率的整数倍,分频值就会有小数,长时间通信后误差会累积,导致数据错位。解决办法是尽量选择系统时钟能被整除的波特率,或者使用更高精度的外部晶振。

第二个坑:中断优先级配置不当。如果UART中断优先级太低,在高频数据接收时容易丢包。我建议把UART接收中断的优先级设得高一些,尤其是高速通信场景。

第三个坑:忘记使能GPIO时钟。这个听起来很基础,但我见过不止一个新手在这里卡住。配置引脚之前一定要先使能对应的GPIO时钟,否则引脚配置不生效。

排查UART问题的时候,我一般按照这个顺序来:先确认接线是否正确(TX-RX交叉),再确认波特率是否一致,然后检查电平是否匹配,最后用示波器看波形。如果手头没有示波器,可以先用回环测试,把TX和RX短接,自己发自己收,能收到说明配置没问题。

3. I2C:两根线挂一堆设备的艺术

3.1 I2C的寻址机制与通信流程

I2C最吸引人的地方就是只用两根线就能挂载多个设备。这两根线分别是SDA(数据线)和SCL(时钟线),所有设备都并联在上面。那怎么区分不同的设备呢?靠地址。每个I2C设备都有一个7位或10位的地址,主机发起通信时先发送地址,匹配的设备才会响应。

I2C的通信流程可以概括为:起始条件 → 地址帧 → 数据帧 → 停止条件。起始条件是SCL为高时SDA从高变低,停止条件是SCL为高时SDA从低变高。这两个条件都是由主机产生的。

地址帧包含7位地址和1位读写标志。读写标志为0表示写操作,为1表示读操作。发送完地址后,从机如果匹配就会拉低SDA表示应答(ACK),否则SDA保持高电平表示非应答(NACK)。

注意:I2C总线上必须接上拉电阻,通常取4.7kΩ或10kΩ。因为I2C的引脚是开漏输出,没有上拉电阻的话,总线无法拉高,通信根本没法进行。这个坑我踩过,当时调了半天以为是代码问题,最后发现是硬件上忘了加上拉。

3.2 时钟同步与仲裁机制

I2C支持多主机,也就是说总线上可以挂多个主机。那如果两个主机同时想发数据怎么办?这就涉及到时钟同步和仲裁机制。

时钟同步的原理是:所有主机的SCL线是线与关系,只要有一个主机拉低SCL,总线就是低电平。所以SCL的低电平时间由拉低时间最长的主机决定,高电平时间由拉高时间最短的主机决定。这样所有主机的时钟就自动同步了。

仲裁机制发生在SDA线上。两个主机同时发送数据时,每个主机都在监视SDA线的实际电平。如果自己发的是高电平,但SDA线是低电平,说明有另一个主机在拉低SDA,自己就失去了仲裁,主动退出。仲裁过程中数据不会丢失,赢得仲裁的主机继续正常通信。

这套机制设计得很巧妙,实际使用中多主机场景并不多见,但理解它对排查一些偶发的通信异常很有帮助。

3.3 软件模拟I2C vs 硬件I2C

I2C有两种实现方式:硬件I2C和软件模拟I2C。硬件I2C使用芯片内置的I2C外设,配置好寄存器后由硬件自动处理时序。软件模拟I2C就是用普通GPIO口,通过代码控制引脚的高低电平来模拟I2C时序。

硬件I2C的优点是效率高、不占用CPU时间,缺点是不同芯片的I2C外设行为可能有差异,有些芯片的硬件I2C还有已知的缺陷。软件模拟I2C的优点是移植性好、时序可控,缺点是需要占用CPU时间,速率也受限于代码执行速度。

我个人的经验是:如果芯片的硬件I2C稳定可靠,优先用硬件I2C。如果遇到硬件I2C的坑,或者需要移植到多种平台,软件模拟更省心。下面是一个软件模拟I2C的基本框架:

// 软件模拟I2C的基本操作 void i2c_start(void) { SDA_HIGH(); SCL_HIGH(); delay_us(4); SDA_LOW(); // SCL高时SDA拉低 = 起始条件 delay_us(4); SCL_LOW(); delay_us(4); } void i2c_send_byte(uint8_t data) { for (int i = 0; i < 8; i++) { if (data & 0x80) SDA_HIGH(); else SDA_LOW(); data <<= 1; delay_us(2); SCL_HIGH(); delay_us(4); SCL_LOW(); delay_us(2); } // 释放SDA,等待从机应答 SDA_HIGH(); delay_us(2); SCL_HIGH(); delay_us(4); // 读取ACK if (SDA_READ() == 0) { // 收到应答 } SCL_LOW(); delay_us(2); }

这段代码里的延时时间需要根据实际器件的手册来调整。标准模式100kHz、快速模式400kHz、高速模式3.4MHz,不同模式对时序的要求不同。延时太短可能导致从机来不及响应,延时太长会降低通信速率。

3.4 I2C调试实战:逻辑分析仪的使用技巧

调I2C最有效的工具是逻辑分析仪。它能把SDA和SCL上的波形抓下来,自动解析成地址、数据和ACK/NACK,一眼就能看出问题出在哪。

用逻辑分析仪的时候,触发条件设置很关键。我一般把触发条件设为“SDA下降沿且SCL为高”,这样能准确抓到起始条件。抓到的数据里,重点看几个地方:地址是否匹配、从机是否回了ACK、数据字节是否正确、有没有意外的起始或停止条件。

常见的I2C问题包括:从机地址搞错了(7位地址和8位地址混淆)、上拉电阻太大导致上升沿太慢、总线电容太大导致波形畸变、多设备地址冲突等。逻辑分析仪配合示波器一起用,基本能定位90%以上的I2C问题。

4. SPI:速度至上的全双工通信

4.1 SPI的四线制与工作模式

SPI是全双工同步通信,用四根线:SCLK(时钟)、MOSI(主机输出从机输入)、MISO(主机输入从机输出)、CS(片选)。和I2C不同,SPI没有地址机制,每个从机单独一根片选线,主机拉低哪个片选就和哪个从机通信。

SPI有四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)组合决定。CPOL决定时钟空闲时的电平,CPHA决定数据在时钟的哪个边沿采样。具体组合如下:

模式CPOLCPHA空闲电平采样边沿
000低上升沿
101低下降沿
210高下降沿
311高上升沿

选哪个模式取决于从机器件的手册。主机和从机的模式必须一致,否则数据会错位。我遇到过好几次因为模式设错导致读出来的数据全是0xFF或0x00的情况,后来养成习惯,拿到新器件先翻手册确认SPI模式。

4.2 SPI的速率优势与适用场景

SPI最大的优势就是快。I2C标准模式才100kHz,快速模式400kHz,而SPI轻松就能跑到几十MHz。这个差距在需要大量数据传输的场景下非常明显,比如驱动TFT显示屏、读写Flash存储、采集高速ADC数据。

SPI是全双工的,发送和接收同时进行。这意味着你在发送一个字节的同时也在接收一个字节,效率比半双工的总线高不少。不过实际使用中,很多SPI从机并不支持真正的全双工,发送和接收是分时进行的,这一点需要注意。

SPI的缺点也很明显:引脚多。每增加一个从机就要多一根片选线。如果系统里有8个SPI从机,那就需要8根片选线加上4根共用线,一共12根线。相比之下,I2C挂8个设备还是2根线。所以SPI适合设备数量少但速率要求高的场景,I2C适合设备数量多但速率要求不高的场景。

4.3 SPI驱动开发:DMA传输与双缓冲

在高吞吐量场景下,用CPU轮询或者中断来搬运SPI数据会占用大量CPU时间。这时候DMA就派上用场了。DMA可以在不占用CPU的情况下自动把数据从内存搬到SPI数据寄存器,或者反过来。

配置SPI DMA传输的基本步骤是:使能DMA时钟、配置DMA通道、设置传输方向和数据宽度、使能SPI的DMA请求、启动传输。下面是一个典型的配置示例:

void spi_dma_init(void) { // 使能DMA时钟 RCC->AHBENR |= RCC_AHBENR_DMA1EN; // 配置DMA通道:SPI1_TX DMA1_Channel3->CPAR = (uint32_t)&SPI1->DR; // 外设地址 DMA1_Channel3->CMAR = (uint32_t)tx_buffer; // 内存地址 DMA1_Channel3->CNDTR = tx_len; // 传输长度 DMA1_Channel3->CCR |= DMA_CCR_DIR; // 内存到外设 DMA1_Channel3->CCR |= DMA_CCR_MINC; // 内存地址递增 DMA1_Channel3->CCR |= DMA_CCR_TCIE; // 传输完成中断 // 使能SPI的DMA发送请求 SPI1->CR2 |= SPI_CR2_TXDMAEN; // 启动DMA DMA1_Channel3->CCR |= DMA_CCR_EN; }

双缓冲是另一个提升SPI吞吐量的技巧。原理是准备两块缓冲区,DMA在发送缓冲区A的时候,CPU往缓冲区B填数据,A发完了立刻切换到B,同时CPU往A填下一批数据。这样DMA和CPU并行工作,数据传输不会断流。

4.4 SPI信号完整性与长距离传输

SPI速率高带来的一个副作用是信号完整性问题。速率越高,信号反射、串扰、地弹效应越明显。在PCB布线的时候,SPI的时钟线要尽量短,最好走等长线,远离其他高速信号。如果走线比较长,可以考虑串联端接电阻来抑制反射。

长距离传输SPI信号的时候,TTL电平就不太够用了。可以考虑用差分信号转换芯片,把单端SPI转成差分信号传输,接收端再转回来。这样传输距离可以从几十厘米延长到几米甚至十几米。

注意:SPI的片选线在空闲时应该保持高电平,确保从机不被误选中。如果片选线悬空或者上电时处于低电平,从机可能会误响应,导致总线冲突。我建议片选线加上拉电阻,确保空闲状态稳定。

5. 三种总线的横向对比与选型决策

5.1 关键参数对比表

特性UARTI2CSPI
线数2(TX/RX)2(SDA/SCL)4(SCLK/MOSI/MISO/CS)
拓扑点对点多主多从一主多从
寻址方式无7位/10位地址片选线
最大速率通常≤1Mbps标准100k/快速400k/高速3.4M可达数十Mbps
双工方式全双工半双工全双工
时钟异步同步同步
引脚开销低低高(每个从机一根CS)
抗干扰一般一般一般(高速时较差)
典型应用调试串口、GPS、蓝牙模块传感器、EEPROM、RTCFlash、显示屏、ADC

这张表基本涵盖了选型时需要考虑的所有维度。实际做决策的时候,我一般按照这个优先级来:先看速率需求,再看设备数量,最后看引脚资源。

5.2 选型决策树:从需求出发

假设你现在要做一个数据采集项目,需要连接温度传感器、EEPROM存储和一块TFT显示屏。温度传感器数据量小、速率要求低,I2C完全够用。EEPROM也是低速器件,可以和温度传感器共用I2C总线。TFT显示屏需要刷屏,数据量大、速率要求高,必须用SPI。这样组合下来,I2C挂两个低速器件,SPI单独驱动显示屏,各取所需。

再假设你要做一个多传感器融合的项目,有加速度计、陀螺仪、磁力计、气压计,这些传感器都是低速器件,数据量不大,全部挂在I2C总线上最省引脚。如果其中某个传感器需要高速采样,比如振动监测,那就单独用SPI。

还有一种情况是设备之间距离比较远,比如主控板和执行机构之间隔了几米。这种情况下UART加RS-485转换是最经济可靠的方案。I2C和SPI都不太适合长距离传输,除非加额外的驱动芯片。

5.3 混合使用策略与引脚规划

实际项目中,三种总线往往是混合使用的。一个典型的单片机系统可能同时有:UART用于调试输出和连接无线模块,I2C连接传感器和EEPROM,SPI驱动显示屏和Flash。这时候引脚规划就很重要了。

我的经验是:先把SPI的引脚定下来,因为SPI对引脚位置有要求(通常需要特定的复用功能引脚),而且SPI的时钟线最好靠近主控芯片。然后规划I2C引脚,I2C的引脚相对灵活,很多GPIO都可以复用。最后分配UART引脚,UART通常也有固定的复用引脚。

如果引脚实在不够用,可以考虑用I2C扩展芯片来增加GPIO,或者用模拟开关来切换SPI片选。但这些方案都会增加成本和复杂度,能在前期规划好的话尽量不要走到这一步。

6. 调试工具与实战排错指南

6.1 逻辑分析仪:总线调试的瑞士军刀

逻辑分析仪是我调试总线问题的首选工具。它能把总线上的波形抓下来,自动解析成协议数据,比盯着示波器看波形效率高得多。选逻辑分析仪的时候,重点看几个参数:采样率、通道数、协议解析支持。

采样率至少要是被测信号频率的4倍以上,最好10倍。比如你要抓10MHz的SPI信号,采样率至少40MHz,推荐100MHz以上。通道数方面,SPI需要4通道,I2C需要2通道,UART需要2通道。如果要多总线同时抓,8通道或16通道的更合适。

协议解析功能是逻辑分析仪的核心价值。好的逻辑分析仪能自动识别起始条件、地址、数据、ACK/NACK,还能把多个数据帧拼成完整的传输过程。用的时候注意设置正确的协议参数,比如I2C的地址位数、SPI的CPOL/CPHA,设错了解析结果就是乱的。

6.2 常见故障速查表

现象可能原因排查方法
完全无数据接线错误、时钟未使能、电源未接检查接线、量电压、回环测试
数据乱码波特率不匹配、时钟误差大核对双方波特率、换晶振
偶发丢包中断优先级低、缓冲区溢出提高中断优先级、增大缓冲区
I2C无应答地址错误、上拉电阻缺失用逻辑分析仪看地址、检查上拉
SPI数据错位模式不匹配、片选时序不对核对CPOL/CPHA、检查CS时序
长距离通信失败电平不匹配、干扰太大加电平转换、用差分传输

这张表是我这些年调试总线的经验总结,基本上覆盖了80%以上的常见问题。遇到问题的时候按表排查,能省不少时间。

6.3 示波器与万用表的配合使用

逻辑分析仪擅长看协议层的问题,但物理层的问题还得靠示波器和万用表。示波器可以看信号的上升沿、下降沿、过冲、振铃、噪声容限。万用表可以量电压、通断、电阻。

我一般先用万用表确认电源和地正常,再用示波器看信号波形是否干净,最后用逻辑分析仪看协议数据是否正确。这个顺序从物理层到协议层,逐层排查,效率最高。

示波器看波形的时候,重点关注几个指标:上升时间是否满足器件要求、高电平和低电平是否在有效范围内、有没有明显的过冲或振铃、时钟占空比是否接近50%。如果波形质量差,先解决硬件问题,再调软件。

7. 进阶话题:从裸机到RTOS的总线管理

7.1 多任务环境下的总线互斥

在RTOS环境下,多个任务可能同时访问同一条总线。如果没有互斥保护,就会出现数据交错、时序混乱的问题。解决办法是给每条总线配一个互斥锁(Mutex),任务访问总线前先获取锁,访问完释放锁。

互斥锁的粒度需要权衡。锁的粒度太粗,会影响并发性能;锁的粒度太细,会增加死锁风险。我的经验是:以一次完整的总线传输为单位加锁,比如一次I2C读写操作、一次SPI数据帧传输。这样既能保证传输的原子性,又不会长时间占用总线。

7.2 总线错误恢复与超时处理

总线通信不可能永远不出错,关键是要有错误恢复机制。常见的错误包括:从机无应答、总线死锁、数据传输超时。对于I2C,如果从机把SDA拉低不放,总线就死锁了,这时候需要发送9个时钟脉冲让从机释放总线。对于SPI,如果片选信号异常,可能导致从机状态机混乱,需要重新初始化从机。

超时处理也很重要。每次总线操作都要设一个超时时间,超时后主动放弃并返回错误码,不能让程序卡死在等待循环里。超时时间根据总线速率和传输长度来定,一般留2到3倍的余量。

7.3 低功耗场景下的总线策略

电池供电的设备对功耗很敏感。总线在空闲时也会消耗一定的功耗,尤其是上拉电阻上的静态电流。降低功耗的策略包括:空闲时关闭总线外设时钟、增大上拉电阻阻值、减少不必要的轮询。

I2C的上拉电阻在总线空闲时会有持续电流流过。如果上拉电阻是4.7kΩ,总线电压3.3V,那么每个上拉电阻上的电流大约是0.7mA。两根线加起来就是1.4mA,对于低功耗设备来说这个数字不小。可以适当增大上拉电阻到10kΩ甚至22kΩ,代价是上升沿变慢,需要降低通信速率来匹配。

SPI在空闲时如果片选线保持高电平,从机通常处于低功耗状态。但如果片选线控制不当,从机可能一直在等待时钟,功耗会比较高。建议在不需要通信的时候把片选线拉高,确保从机进入休眠。

8. 我个人的一些经验体会

说了这么多技术和操作层面的东西,最后聊几句我自己的感受。这三种总线我用了很多年,从最开始照着例程抄代码,到后来能根据需求快速选型、独立设计通信协议,中间踩过的坑不计其数。最大的体会是:不要迷信任何一种总线,也不要轻视任何一种总线。

UART看起来简单,但要做好错误处理、流控、超时恢复,其实有很多细节。I2C看起来优雅,但总线死锁、地址冲突、上拉电阻选型这些问题能让你调一整天。SPI看起来直接,但信号完整性、片选管理、DMA配置这些坑一个都不少。

还有一个很深的体会是:硬件问题往往比软件问题更难查。软件问题至少还能打断点、看变量,硬件问题只能靠仪器量。所以我在做新项目的时候,硬件设计阶段就会把总线的测试点留出来,方便后期调试。PCB上多留几个测试焊盘,成本几乎为零,但调试的时候能省大量时间。

另外,养成看手册的习惯。每个器件的时序要求、电气特性、注意事项都写在手册里,而且往往就在你忽略的那几页。我见过太多人遇到问题先上网搜,搜了半天没结果,最后翻手册发现第一页就写了。手册是最好的老师,没有之一。

最后说一个关于代码复用的建议。把每种总线的驱动封装成独立的模块,接口统一,底层实现和上层应用分离。这样换平台的时候只需要改底层驱动,上层代码不用动。我现在的项目里,UART、I2C、SPI的驱动都是独立模块,移植到新平台最多花半天时间。这个习惯一旦养成,受益无穷。

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

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

立即咨询