资讯动态

SPI、UART、I2C通信协议选型指南:从原理到实战避坑

发布时间:2026/10/18 2:20:37 来源:尧图企业网站定制
1. 三种通信协议到底该怎么选嵌入式开发干久了你会发现一个很有意思的现象新手最怕的是“协议看不懂”老手最怕的是“协议选错了”。SPI、UART、I2C这三个名字几乎贯穿了每一个嵌入式项目的始终从最简单的温湿度传感器到复杂的多传感器融合系统绕来绕去都绕不开它们。但真正能把三者讲清楚、用明白的人其实没想象中那么多。我见过太多项目前期选型时随手挑了一个通信方式结果后期要么引脚不够用要么速度上不去要么多设备组网时地址冲突最后不得不飞线、改板、重写驱动代价相当大。所以这篇文章我想从实际项目出发把SPI、UART、I2C这三种最常用的板级通信协议掰开揉碎讲一遍重点不是背概念而是搞清楚什么场景该用哪个为什么这么选选错了会踩什么坑。不管你是刚接触单片机的新手还是已经做过几个项目但一直“凭感觉选协议”的开发者这篇内容都能帮你建立一套清晰的判断逻辑。我会从电气特性、协议时序、速度上限、引脚开销、多设备支持、实际代码配置这几个维度逐一对比最后给出可以直接对照使用的选型决策表。全文基于常见的MCU平台和典型外设场景展开不绑定任何特定厂商或芯片型号你可以直接套用到自己的项目里。2. 三种协议的核心差异拆解2.1 先搞清楚它们各自解决什么问题很多人学通信协议的时候习惯一上来就看时序图结果被各种起始位、时钟极性、应答位搞得头晕。其实换个思路先想清楚每种协议诞生的初衷后面再看细节就顺了。UART是最古老的之一它的核心思路特别朴素两根线一根发一根收双方提前约定好同样的速度然后按照固定的格式一帧一帧传数据。它不关心总线上挂了几个设备因为它压根就不支持多设备——UART是点对点的一对一通信。你可以把它想象成两个人打电话线路接通了就只能你跟我说、我跟你说第三个人想加入是没办法的。UART的典型应用就是MCU和PC串口调试、GPS模块、蓝牙模块、某些指纹识别模块等这些场景基本都是“一个主控对一个外设”的结构。I2C的思路完全不同。它用两根线时钟线SCL和数据线SDA挂载多个设备每个设备有自己唯一的地址。主设备发起通信时先发地址总线上所有设备都收到但只有地址匹配的那个才会应答。这就像老师点名全班都能听到但只有被点到的人站起来回答问题。I2C的优势在于引脚极少、支持多设备级联特别适合板级低速外设的管理比如EEPROM、温湿度传感器、加速度计、IO扩展芯片等。缺点是速度相对慢标准模式100kHz快速模式400kHz高速模式虽然能到3.4MHz但实际项目中用得少。SPI则是另一种哲学它不搞地址寻址那一套而是用片选线CS来选设备。每个从设备单独一根CS线主设备拉低谁的CS谁就上总线干活。SPI通常需要四根线SCLK、MOSI、MISO、CS。虽然线多但换来了全双工高速传输的能力。SPI没有协议规定的速度上限实际速度取决于芯片支持几十MHz很常见。它适合需要高速数据吞吐的场景比如Flash存储芯片、TFT显示屏、高速ADC、无线通信模块等。2.2 电气特性和引脚开销的硬账选协议的时候引脚数量往往是第一个卡脖子的因素。尤其是用小封装MCU的时候每个IO都金贵。UART最少需要两根线TX和RX如果只发不收一根也行。但它只能连一个设备想连两个外设就得两组UART也就是四根线。I2C不管挂多少个设备永远只占两根线这是它最大的硬件优势。SPI的引脚开销跟从设备数量直接相关一个从设备四根线两个从设备五根线SCLK、MOSI、MISO共用CS各一根三个从设备六根线以此类推。所以从引脚效率来看I2C最优SPI其次设备越多越明显UART最差。但引脚少不代表一切。I2C的两根线是开漏输出必须外接上拉电阻才能正常工作。上拉电阻的阻值选择有讲究阻值太小功耗大低电平灌电流可能超过芯片承受能力阻值太大上升沿变缓高速通信时波形质量差。一般3.3V系统常用4.7kΩ或10kΩ5V系统常用4.7kΩ。总线电容也有上限标准模式不超过400pF快速模式不超过200pF挂太多设备或者走线太长都会导致通信失败。SPI是推挽输出不需要上拉电阻驱动能力强波形干净这也是它能跑高速的原因之一。UART同样是推挽输出点对点连接电气特性最简单。还有一个容易被忽略的点电平匹配。三种协议本身都不规定电平标准具体取决于芯片的IO电压。3.3V的MCU和5V的外设通信时必须做电平转换否则可能烧毁IO口。I2C因为开漏结构电平转换相对好做用两个MOS管就能搞定双向转换。SPI和UART是推挽输出电平转换需要额外的电平转换芯片或者电阻分压仅适用于单向、低速场景。这一点在选型时就要考虑进去不然后期加转换电路很麻烦。2.3 速度、距离和拓扑结构的真实边界速度方面SPI最快I2C最慢UART居中但分档位。具体来说协议典型速度理论上限实际项目常用UART9600bps~115200bps数Mbps短距离115200bpsI2C100kHz~400kHz3.4MHz高速模式400kHzSPI1MHz~50MHz取决于芯片10MHz~30MHzUART的波特率是双方约定的常见的有9600、19200、38400、57600、115200再往上也有921600甚至更高但对时钟精度要求高误差累积容易导致通信出错。I2C的速度由主设备时钟决定从设备必须能跟上挂多个设备时要以最慢的那个为准。SPI的速度最灵活主设备设多少就是多少但要注意从设备的时序要求比如某些Flash芯片在高速读取时需要特定的Dummy Cycle。距离方面这三种协议都是板级通信设计初衷就是短距离。UART在TTL电平下通常不超过几十厘米加RS232或RS485转换芯片可以延长到几十米甚至上千米但那已经是另一个层面的问题了。I2C和SPI更适合芯片间通信走线一般不超过几十厘米I2C因为开漏结构抗干扰稍好一些但也不建议长距离走线。如果项目里传感器离主控较远优先考虑RS485或者CAN不要硬拉I2C。拓扑结构上UART是点对点I2C是多主多从实际项目中大多单主多从SPI是单主多从。I2C支持多主设备仲裁但实际项目中很少用因为仲裁机制复杂且容易出问题。SPI不支持多主主设备永远是主设备。UART也不支持多主点对点就是点对点。3. 实操配置与代码实现对比3.1 UART配置最基础但最容易翻车UART的配置参数有四个波特率、数据位、停止位、校验位。最常见的组合是115200-8-N-1即波特率1152008位数据位无校验1位停止位。双方必须完全一致否则收到的就是乱码。以常见的MCU HAL库为例UART初始化的核心代码大概长这样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; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; HAL_UART_Init(huart1);发送数据用HAL_UART_Transmit()接收用HAL_UART_Receive()或者中断/DMA方式。这里有个坑HAL_UART_Receive()是阻塞式的如果没收到数据会一直等在主循环里用容易卡死。实际项目中建议用中断接收或者DMA接收配合环形缓冲区这样不占用CPU时间。UART还有一个容易忽略的问题波特率误差。MCU的时钟源如果精度不够分频后实际波特率和设定值有偏差偏差超过3%左右就可能通信失败。用外部晶振比内部RC振荡器靠谱得多。另外如果两个设备地线没有共地UART通信也会出问题因为电平参考点不一致。注意UART调试时如果收到乱码先检查波特率是否一致再检查地线是否共地最后检查时钟精度。这三个原因占了UART通信问题的九成以上。3.2 I2C配置上拉电阻和地址冲突是永恒的话题I2C的配置比UART复杂一些因为涉及到上拉电阻、时钟频率、设备地址等多个因素。硬件上SCL和SDA必须接上拉电阻。很多MCU内部有弱上拉但阻值太大通常几十kΩ只能用于极低速场景正常通信还是得外接。我一般用4.7kΩ3.3V系统下上升沿大概几百纳秒400kHz通信没问题。如果总线上设备多、走线长可以适当减小到2.2kΩ但要注意灌电流不要超过芯片规格。软件配置方面以HAL库为例I2C_HandleTypeDef hi2c1; hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(hi2c1);读写操作的核心是设备地址。7位地址模式下地址是7位但HAL库通常要求传入8位地址左移一位最低位表示读/写。比如一个设备的7位地址是0x48写操作传入0x90读操作传入0x91。这个细节很多新手会搞混导致通信失败。I2C最常见的问题就是地址冲突。总线上挂了两个相同地址的设备通信必然失败。解决方法是查每个设备的数据手册确认地址是否可配置。很多传感器支持通过引脚拉高拉低来改变地址的低位比如一个加速度计可以配置成0x68或0x69。如果地址不可改那就只能换设备或者用I2C多路复用器。另一个常见问题是时钟拉伸。从设备处理不过来的时候会把SCL拉低强制主设备等待。如果主设备不支持时钟拉伸通信就会出错。大部分MCU的硬件I2C都支持但软件模拟I2C需要自己处理。我个人的经验是如果项目对I2C速度要求不高用软件模拟反而更省心因为可以完全控制时序避免硬件I2C的各种奇怪问题。3.3 SPI配置模式选择和片选管理是关键SPI的配置参数主要是时钟极性CPOL和时钟相位CPHA组合起来有四种模式模式CPOLCPHA采样时刻Mode 000第一个边沿上升沿Mode 101第二个边沿下降沿Mode 210第一个边沿下降沿Mode 311第二个边沿上升沿主从设备必须使用相同的模式否则数据错位。大部分Flash芯片用Mode 0或Mode 3具体看数据手册。我遇到过不少情况是模式设错了读出来的数据全是0xFF或者0x00查了半天才发现是CPOL/CPHA不匹配。SPI的片选管理也有讲究。如果总线上有多个从设备每个设备一根CS线主设备在通信前拉低对应的CS通信结束后拉高。注意有些设备要求CS在整帧数据传输期间保持低电平中间不能拉高否则会复位通信状态。另外CS拉高后最好延时几个时钟周期再开始下一次通信给从设备留出处理时间。// SPI发送一个字节 uint8_t SPI_Transfer(uint8_t data) { while (!(SPI1-SR SPI_SR_TXE)); SPI1-DR data; while (!(SPI1-SR SPI_SR_RXNE)); return SPI1-DR; } // 读取Flash ID的典型流程 void ReadFlashID(void) { CS_LOW(); SPI_Transfer(0x9F); // 读ID命令 uint8_t manuf SPI_Transfer(0xFF); uint8_t memType SPI_Transfer(0xFF); uint8_t capacity SPI_Transfer(0xFF); CS_HIGH(); }SPI的速度设置要注意从设备的最高时钟频率。比如某个Flash芯片最高支持50MHz你设成60MHz就可能读写出错。另外SPI的走线尽量等长尤其是高速场景下SCLK和MOSI/MISO的走线长度差异会导致时序偏移。如果板子上有多个SPI设备尽量让它们共享SCLK、MOSI、MISOCS单独走线减少引脚占用。4. 选型决策与常见问题排查4.1 一张表搞定选型决策实际项目中怎么选我整理了一个决策流程你可以直接对照场景特征推荐协议理由点对点、低速、调试输出UART简单、通用、PC直接支持多个低速传感器、引脚紧张I2C两根线挂多个设备高速数据传输、Flash、屏幕SPI速度快、全双工需要长距离通信UARTRS485差分信号抗干扰多主设备竞争I2C多主模式支持仲裁但慎用实时性要求高SPI无地址开销延迟低再补充几个实际经验如果项目里既有I2C设备又有SPI设备优先把高速设备放SPI低速设备放I2C这样总线负载均衡。如果MCU的硬件I2C不够用可以用软件模拟但要注意时序精度。UART如果不够用可以用软件模拟或者用GPIO扩展芯片。还有一个隐藏的考量因素驱动复杂度。UART驱动最简单I2C次之SPI最复杂因为要管理多个CS和模式。如果项目周期紧、团队经验不足优先选简单的方案不要为了“性能”过度设计。4.2 常见问题速查表现象可能原因排查方法UART收乱码波特率不一致、地线未共地、时钟误差大示波器测TX波形确认波特率I2C无应答上拉电阻缺失、地址错误、设备未供电测SCL/SDA空闲时是否为高电平I2C通信偶尔失败总线电容过大、上拉电阻偏大、干扰减小上拉电阻缩短走线SPI读全0xFFCS未拉低、模式错误、从设备未就绪示波器看CS和SCLK时序SPI读全0x00MISO未接、从设备未输出、模式错误检查MISO连线确认CPOL/CPHA多设备I2C冲突地址重复查手册确认地址改地址或加多路复用器4.3 几个容易踩的坑和实操心得第一个坑I2C上拉电阻接到3.3V还是5V。如果总线上有5V设备上拉接到5V3.3V的MCU IO可能受不了。正确做法是上拉接到MCU的IO电压或者用I2C电平转换芯片。我见过有人把上拉接到5V结果3.3V的传感器通信不稳定查了半天才发现是电平问题。第二个坑SPI的CS线忘记初始化。有些MCU的SPI外设会自动控制CS但大部分情况下需要手动控制GPIO。如果CS一直悬空或者一直拉低通信肯定失败。初始化的时候记得把CS引脚配成推挽输出默认拉高。第三个坑UART的DMA接收缓冲区溢出。用DMA接收UART数据时如果数据来得太快缓冲区满了但没及时处理后面的数据就丢了。解决办法是用双缓冲区或者环形缓冲区配合空闲中断一帧数据接收完再处理。第四个坑I2C的时钟频率设太高。有些传感器手册写支持400kHz但实际模块上加了滤波电容或者走线太长400kHz下波形已经变形了。遇到这种情况降到100kHz试试往往就稳了。速度不是越高越好稳定才是第一位的。第五个坑SPI模式记混。我自己的习惯是拿到一个新芯片先看手册确认SPI模式然后在代码里注释清楚。Mode 0和Mode 3最常用如果拿不准先试Mode 0不行再试Mode 3。有些芯片手册写的是“数据在时钟上升沿采样”那就是Mode 0或Mode 2具体看空闲电平。5. 高速场景下的SPI优化与I2C总线扩展5.1 SPI高速传输的时序余量计算当SPI时钟跑到20MHz以上时时序余量就变成一个必须认真对待的问题。很多人代码写得没问题但实际跑起来数据就是不对根源往往在时序上。先算一笔账。假设SPI时钟20MHz一个时钟周期50ns。从设备手册给出的数据建立时间tSU是5ns数据保持时间tH是3ns。主设备输出数据后从设备需要在SCLK有效边沿前后各保持一段时间。如果主设备的输出延迟加上走线延迟超过了这个余量采样就会出错。走线延迟怎么估PCB上信号传播速度大约是每厘米50ps到70ps微带线假设走线10厘米延迟大约0.5ns到0.7ns。看起来不大但加上主设备IO翻转延迟可能几纳秒、从设备输入延迟累积起来就可能吃掉余量。所以高速SPI的走线要尽量短且SCLK与数据线尽量等长。还有一个容易被忽略的因素从设备的输出延迟。主设备在SCLK边沿采样MISO时从设备的数据是在上一个边沿之后才输出的这个输出延迟tV可能十几纳秒。如果主设备采样太早读到的就是无效数据。解决办法是降低SPI时钟或者调整采样边沿改CPHA。我一般会在高速SPI调试时用示波器同时抓SCLK和MISO看数据是否在采样点已经稳定。实操建议SPI时钟超过10MHz时建议用示波器确认波形质量。重点关注过冲、振铃和上升沿时间。如果上升沿超过时钟周期的十分之一说明驱动能力不足或者走线电容太大需要加缓冲或者降低速度。5.2 I2C总线扩展的几种实用方案I2C最大的痛点就是地址冲突和总线电容限制。当项目里需要挂载多个相同型号的传感器时这个问题几乎无法避免。我总结了几种实际可用的扩展方案。第一种是I2C多路复用器。典型芯片如TCA9548A一个主设备接口八个从设备通道通过写寄存器切换通道。每个通道上可以挂相同地址的设备因为同一时刻只有一个通道导通。这种方案成熟稳定缺点是增加了成本和一个额外的寄存器操作步骤。代码上就是在读写目标设备前先写多路复用器选择通道。第二种是软件模拟I2C配合GPIO片选。如果MCU的IO够用可以用普通GPIO模拟I2C每个设备单独一组SCL/SDA。这样完全避免了地址冲突但引脚开销大而且软件模拟的速度和稳定性不如硬件I2C。适合设备数量少、速度要求不高的场景。第三种是改用SPI或UART。如果I2C实在搞不定换协议往往比硬扛更省事。比如多个相同型号的温湿度传感器如果支持SPI直接用SPI加片选问题迎刃而解。选型阶段就要考虑多设备扩展的可行性不要等到画完板子才发现地址冲突。5.3 混合总线系统的设计经验实际项目里很少只用一种协议通常是UART、I2C、SPI混着用。这时候总线规划就很重要。我的习惯是把高速设备屏幕、Flash放SPI低速传感器放I2C调试口和通信模块放UART。SPI的时钟线尽量远离模拟信号线避免干扰。I2C的上拉电阻放在总线末端不要每个设备都放一组。UART的TX/RX如果走排线最好加串阻和TVS管防止静电和浪涌。电源方面三种协议的外设最好独立供电或者加磁珠隔离避免一个设备异常拉低总线电压影响其他设备。地线要单点接地减少地环路干扰。这些细节看起来跟协议本身无关但实际调试时很多通信问题最后都追溯到电源和地线上。6. 调试工具与实战排查思路6.1 逻辑分析仪和示波器的分工调试通信协议工具比经验还重要。我最常用的两个工具是逻辑分析仪和示波器它们的分工很明确。逻辑分析仪擅长看协议层。它能把SCL/SDA或者SCLK/MOSI/MISO的波形直接解码成字节告诉你发了什么、收了什么、有没有应答。排查I2C地址错误、SPI模式错误这类问题逻辑分析仪几分钟就能定位。我一般用8通道以上的逻辑分析仪同时抓CS、SCLK、MOSI、MISO一眼就能看出时序关系。示波器擅长看电气层。信号质量、上升沿、过冲、振铃、噪声这些逻辑分析仪看不出来的问题示波器一测便知。比如I2C通信偶尔失败逻辑分析仪可能显示“无应答”但为什么无应答示波器一看SCL上升沿太缓从设备根本没识别到高电平。这时候就要减小上拉电阻或者缩短走线。实操心得先逻辑分析仪定位“哪个环节出错”再示波器分析“为什么出错”。两个工具配合使用排查效率最高。6.2 从现象反推根因的排查流程遇到通信问题不要东改一下西改一下要有系统的排查思路。我一般按这个顺序来第一步确认硬件连接。电源是否正常、地线是否共地、引脚是否接对、上拉电阻是否焊了。这一步能解决一半以上的问题。第二步确认配置参数。UART的波特率、I2C的时钟频率和地址、SPI的模式和速度逐一核对数据手册。参数不匹配是第二大原因。第三步用工具抓波形。逻辑分析仪看协议层示波器看电气层。波形不会骗人有没有起始位、有没有应答、数据对不对一目了然。第四步隔离问题。如果总线上有多个设备先只留一个确认单个设备能通再逐个加回去。这样可以快速定位是哪个设备或者哪段走线的问题。第五步降速测试。把通信速度降到最低如果低速能通高速不通那就是时序余量或者信号完整性问题。如果低速也不通那就是配置或者硬件问题。这套流程我用了很多年基本上能在半小时内定位大部分通信故障。关键是要有耐心一步一步来不要跳步。6.3 几个真实项目中的排查案例案例一某项目用I2C读取一个压力传感器偶尔读到0x00。逻辑分析仪显示通信正常有地址、有应答、有数据。示波器一看SDA的上升沿在400kHz下已经变成圆弧了从设备在采样点识别到的电平不确定。把上拉电阻从10kΩ换成4.7kΩ问题消失。根因是上拉电阻太大总线电容导致上升沿变缓。案例二某项目用SPI驱动一个显示屏初始化偶尔失败。逻辑分析仪显示SPI模式正确、数据正确但CS信号在两次通信之间没有拉高。查代码发现CS控制函数里有个条件判断写错了导致CS一直保持低电平。从设备没有收到“帧结束”信号状态机卡住了。修复CS控制逻辑后正常。案例三某项目UART接收GPS数据每隔几分钟丢一帧。用DMA接收缓冲区设了256字节。GPS模块在冷启动时会连续输出大量数据256字节不够用缓冲区溢出导致丢帧。改成512字节缓冲区并加上空闲中断处理问题解决。这三个案例的根因分别是电气特性、软件逻辑和缓冲区管理都不是协议本身的问题。但如果你不理解协议的工作原理就很难从现象反推到根因。所以基础概念还是要扎实工具只是辅助。7. 协议选型的长期维护视角7.1 可维护性和可扩展性的权衡选协议的时候除了看当前需求还要想一步这个项目后续会不会加功能会不会换传感器会不会提高数据速率我吃过这方面的亏。早期做一个数据采集项目当时只有两个I2C传感器觉得I2C两根线很省事。结果后来要加第三个传感器地址跟已有的冲突只能加多路复用器重新画板。如果一开始就用SPI每个传感器一根CS加设备只是多一根线的事根本不用改板。所以我的经验是如果项目预期会扩展优先选扩展性好的方案。SPI虽然线多但扩展最方便加设备就加CS。I2C扩展受地址和电容限制适合设备数量固定的场景。UART扩展性最差但胜在简单适合点对点固定连接。另一个考量是代码复用。UART和I2C的驱动代码通常比较通用换一个传感器可能只需要改地址和寄存器操作。SPI的驱动跟具体设备的时序和命令集关系更大换设备往往要重写驱动。如果团队人手紧张选驱动简单的协议能省不少时间。7.2 从项目周期看协议选择项目周期紧的时候选型原则是“用最熟悉的”。如果你对I2C最熟那就优先用I2C哪怕SPI理论上更快。因为调试时间也是成本用不熟的协议踩坑可能比省下来的那点性能更亏。项目周期宽松、有探索空间的时候可以尝试更优的方案。比如高速数据采集SPI虽然驱动复杂但性能优势明显值得花时间调通。多传感器融合I2C虽然速度慢但布线简单可以把精力放在算法上。还有一个现实因素团队的技术栈。如果团队里没人做过SPI高速通信贸然上SPI可能会在时序调试上卡很久。这时候用I2C或者UART先把功能跑通后续再优化是更务实的做法。7.3 我个人的选型习惯最后分享一下我自己的选型习惯供你参考。拿到一个新项目先列外设清单标注每个外设的通信接口需求速度、距离、设备数量。然后按这个优先级排序能用UART就用UART因为最简单UART不够用就上I2C因为引脚少I2C速度或扩展性不够就上SPI。反过来如果项目对速度要求极高直接上SPI不要犹豫。调试阶段我习惯先用逻辑分析仪抓一遍所有通信波形确认协议层没问题再用示波器看信号质量。两个工具配合基本能覆盖所有通信问题。代码层面我会把三种协议的驱动封装成统一的接口上层应用不直接调HAL库而是调自己封装的读写函数。这样换协议或者换芯片时只需要改底层驱动上层代码不动。这个习惯让我在多个项目之间切换时省了很多事。通信协议这东西理论不难难的是实际项目中的各种边界情况。多踩几次坑多抓几次波形自然就有感觉了。希望这篇内容能帮你少走一些弯路选对协议调通通信把项目顺利做完。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑