ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

嵌入式通信协议选型指南:I2C、SPI、UART、I2S核心差异与实战避坑

嵌入式通信协议选型指南:I2C、SPI、UART、I2S核心差异与实战避坑 1. 四种通信协议的核心定位与选型逻辑1.1 为什么嵌入式工程师绕不开这四种总线做嵌入式开发只要涉及到芯片之间的数据交互i2c、i2s、spi、uart这四个词几乎一定会出现在你的选型清单里。我刚入行那会儿面对一个传感器模块第一反应就是“随便挑一个能通的就行”结果在项目后期被时序问题、速率瓶颈和引脚资源折腾得够呛。后来踩坑踩多了才明白这四种协议各有各的脾气选错了不是不能跑而是会在某个你意想不到的环节给你埋雷。从宏观上看这四种协议解决的是不同层次的问题。uart是最古老的异步串行通信方式它不依赖时钟线靠双方约定的波特率来同步数据位适合点对点、低速率、长距离的场景比如调试串口、GPS模块、工业仪表的通信。i2c是同步串行总线用两根线SCL和SDA挂载多个从设备靠地址寻址速率通常在100kHz到400kHz之间适合板内低速外设比如EEPROM、温湿度传感器、RTC芯片。spi同样是同步串行但它用四根线SCLK、MOSI、MISO、CS全双工、无寻址开销速率可以轻松跑到几十MHz适合高速数据流比如Flash存储、显示屏、ADC采样。i2s则是专门为音频数据设计的同步串行协议它本质上是spi的一个“专业化变体”把数据线和时钟线做了明确分工专门传输PCM音频流速率和时序都围绕音频采样率来设计。理解这四者的定位差异比死记它们的时序图重要得多。你可以把uart想象成两个人打电话约定好语速就行i2c像一条走廊上多个房间共用两个传话筒每个房间有编号spi像一条高速公路四车道并行速度快但占地方i2s则像一条专门运送音频样本的传送带节拍由音频时钟严格把控。1.2 选型时最容易犯的三个错误第一个错误是“速率至上”。很多人一看spi能跑50MHz就恨不得所有外设都挂spi。但实际项目中一个温度传感器每秒钟才更新一次数据你用spi去读除了多占三根引脚之外没有任何收益。i2c两根线就能挂一串传感器PCB布线也简单得多。第二个错误是“忽略引脚资源”。spi每增加一个从设备通常就要多一根片选线CS。如果你板子上有8个spi从设备那就是8根CS加4根总线线一共12根引脚。而i2c挂8个设备还是那两根线。在引脚紧张的MCU上这个差异是致命的。第三个错误是“混淆i2s和spi”。i2s的时序和spi很像但它的时钟极性、帧格式、数据对齐方式都有专门规定。我曾经试过用spi去驱动一个i2s的音频DAC结果出来的声音全是噪声后来才发现i2s的WS字选择信号和spi的CS信号在时序上完全不同。i2s的WS是在每个音频样本开始时翻转而spi的CS是在整个传输周期内保持有效。1.3 一张表看清四者的核心参数差异特性uarti2cspii2s线数2TX/RX2SCL/SDA4SCLK/MOSI/MISO/CS3SCK/WS/SD同步方式异步同步同步同步双工全双工半双工全双工半双工单数据线寻址无7位/10位地址片选线无典型速率9.6k~115.2kbps100k~400kHz1M~50MHz取决于采样率多设备点对点总线挂载片选扩展点对点典型应用调试、GPS、工业传感器、EEPROMFlash、屏幕、ADC音频编解码这张表我建议你打印出来贴在工位上。每次选型的时候扫一眼能省下不少纠结的时间。2. i2c协议深度拆解与实操避坑2.1 i2c的物理层设计为什么必须用开漏加外部上拉i2c最让人困惑的一点就是为什么SCL和SDA必须配置成开漏输出而且一定要加外部上拉电阻这个问题我当年面试被问过后来带新人的时候也反复讲。开漏输出的意思是引脚只能主动拉低不能主动拉高。拉高的时候引脚处于高阻态靠外部上拉电阻把电平拉上去。这样做的好处是总线上任何一个设备都可以拉低总线但没有任何设备能强行拉高总线。这就实现了“线与”逻辑——只要有一个设备拉低总线就是低电平所有设备都释放总线才被上拉电阻拉高。如果不用开漏而用推挽输出两个设备一个想拉高一个想拉低就会形成电源到地的直通路径瞬间大电流可能烧毁引脚。开漏加外部上拉从根本上避免了这个问题。上拉电阻的取值是个技术活。阻值太小功耗大而且当设备拉低总线时灌电流可能超过引脚的承受能力。阻值太大总线电容和上拉电阻形成的RC充电时间常数就大上升沿变缓高速通信时波形还没到高电平就被拉低了导致通信失败。标准模式下100kHz常用4.7kΩ快速模式下400kHz常用2.2kΩ或1.8kΩ。具体取值要根据总线电容来算上升时间tr ≈ 0.847 × R × C其中C是总线总电容包括引脚电容和走线电容一般要求tr小于300ns快速模式。我遇到过一个问题一块板子上i2c上拉电阻用了10kΩ低速跑没问题一上400kHz就随机丢数据。用示波器一看SCL上升沿明显变缓从低到高花了将近1微秒。换成2.2kΩ之后波形立刻变得陡峭通信稳定。所以如果你遇到i2c高速通信不稳定第一件事就是检查上拉电阻和波形上升时间。2.2 i2c时序图的关键节点解读i2c的时序图看起来复杂但核心就几个关键节点起始条件、地址帧、应答位、数据帧、停止条件。起始条件SCL为高电平时SDA从高变低。这个“高电平期间拉低数据线”的动作是i2c独有的用来告诉总线上所有设备“我要开始通信了”。地址帧起始条件之后主机发送7位从机地址加1位读写方向位。地址帧发送时SDA在SCL低电平期间变化在SCL高电平期间保持稳定。这是i2c的基本规则数据线在时钟高电平期间必须稳定变化只能发生在时钟低电平期间。应答位每发送完8位数据地址或数据接收方要在第9个时钟周期把SDA拉低表示“我收到了”。如果接收方没有拉低主机就会认为通信失败通常会产生一个停止条件或重新发起起始条件。停止条件SCL为高电平时SDA从低变高。和起始条件正好相反。用逻辑分析仪抓i2c波形的时候我习惯先看起始和停止条件是否干净再看每个字节后的应答位是否正常。如果某个字节后没有应答说明从机地址错了或者从机没准备好。如果所有字节都有应答但数据不对那就要检查寄存器地址和数据格式了。2.3 i2c读写EEPROM的实操代码与注意事项以常见的24C02 EEPROM为例用Verilog实现i2c读写是很多FPGA项目的必修课。核心状态机通常包括IDLE、START、SEND_ADDR、ACK1、SEND_REG、ACK2、SEND_DATA、ACK3、STOP。// i2c写一个字节的简化状态机片段 localparam IDLE 0, START 1, SEND_ADDR 2, ACK1 3; localparam SEND_REG 4, ACK2 5, SEND_DATA 6, ACK3 7, STOP 8; always (posedge clk) begin case(state) IDLE: if (start_en) state START; START: begin sda 1b0; // SCL高时拉低SDA state SEND_ADDR; end SEND_ADDR: begin // 发送7位设备地址写方向位 if (bit_cnt 7) state ACK1; end ACK1: begin sda 1bz; // 释放SDA等待从机应答 if (scl_high sda 1b0) state SEND_REG; end // ... 后续状态类似 endcase end这段代码里有个关键点在ACK状态必须把SDA置为高阻态1bz让从机有机会拉低。如果你保持SDA为输出高电平从机拉低的时候就会形成冲突。这是新手最容易犯的错误之一。另一个坑是时序参数。24C02在5V供电时SCL频率最高400kHz但很多国产替代型号可能只支持100kHz。如果你按400kHz写时序换一批芯片就可能出问题。我的做法是在代码里把时钟分频参数做成可配置的调试阶段先用100kHz稳定后再往上调。2.4 i2c扩展与PMBus的区别当你的i2c总线上设备太多地址冲突怎么办常见方案有两种一是用i2c多路复用器如TCA9548A它像一个“交换机”把一路i2c分成8路每路可以挂相同地址的设备二是用软件模拟i2c通过不同的GPIO引脚分组来扩展。PMBus是在i2c基础上发展出来的电源管理协议物理层完全兼容i2c但应用层定义了专门的命令格式用于读取电压、电流、温度等电源参数。如果你只是读一个普通传感器用i2c就够了如果你要管理一堆电源模块PMBus的标准化命令会让代码更通用。3. spi协议的高速传输与DMA实战3.1 spi的四种模式与时序配置spi的时序配置由两个参数决定CPOL时钟极性和CPHA时钟相位。CPOL决定空闲时SCLK是高还是低CPHA决定数据在SCLK的哪个边沿采样。组合起来就是四种模式Mode 0CPOL0, CPHA0、Mode 10,1、Mode 21,0、Mode 31,1。大部分Flash芯片用Mode 0或Mode 3传感器用Mode 0的居多。但总有一些“个性”芯片用Mode 1或Mode 2。我遇到过一颗ADC手册上写的是Mode 1但我按Mode 1配置死活读不出数据后来用逻辑分析仪抓波形才发现它的数据在SCLK下降沿变化、上升沿采样实际上对应的是Mode 0。所以手册和实际波形不一致的时候以波形为准。配置spi的时候先确认从设备手册里的CPOL和CPHA要求然后在MCU的spi初始化代码里设置对应的参数。以STM32为例SPI_InitTypeDef SPI_InitStruct; SPI_InitStruct.SPI_Mode SPI_Mode_Master; SPI_InitStruct.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStruct.SPI_DataSize SPI_DataSize_8b; SPI_InitStruct.SPI_CPOL SPI_CPOL_Low; // Mode 0 SPI_InitStruct.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStruct.SPI_NSS SPI_NSS_Soft; // 软件片选 SPI_InitStruct.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; SPI_InitStruct.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStruct);3.2 硬件片选与软件片选的取舍spi的片选信号CS有两种管理方式硬件片选和软件片选。硬件片选由spi外设自动控制传输开始时拉低结束时拉高时序精准。软件片选则是用普通GPIO手动控制灵活但需要你自己保证时序。硬件片选的优势是省CPU干预适合DMA传输场景。但它的缺点是片选引脚固定不能随意映射到任意GPIO。软件片选的优势是引脚灵活而且可以在两次传输之间插入自定义延时满足某些芯片对CS建立时间和保持时间的要求。我的经验是如果spi外设支持硬件片选且引脚够用优先用硬件片选如果从设备对CS时序有特殊要求比如CS拉低后需要延时才能发时钟那就用软件片选在拉低CS之后加一个微秒级延时。3.3 STM32F103通过DMA读取spi数据的完整流程STM32F103的spi配合DMA是经典组合能实现高速数据采集而不占用CPU。以读取一颗spi ADC的数据为例完整流程如下第一步初始化spi为Master模式配置好时钟极性和相位速率先设低一点比如1MHz方便调试。第二步初始化DMA通道。spi1的RX通常对应DMA1 Channel2TX对应Channel3。配置DMA为循环模式或单次模式数据宽度为字节。DMA_InitTypeDef DMA_InitStruct; DMA_DeInit(DMA1_Channel2); DMA_InitStruct.DMA_PeripheralBaseAddr (uint32_t)SPI1-DR; DMA_InitStruct.DMA_MemoryBaseAddr (uint32_t)rx_buffer; DMA_InitStruct.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStruct.DMA_BufferSize 256; DMA_InitStruct.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStruct.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStruct.DMA_Mode DMA_Mode_Normal; DMA_InitStruct.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel2, DMA_InitStruct);第三步使能spi的DMA接收请求SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);第四步拉低CS使能DMA通道然后手动往SPI-DR写一个字节触发时钟。之后DMA会自动把接收到的数据搬到内存。第五步等待DMA传输完成标志拉高CS处理数据。这里有个坑spi是全双工你发一个字节才能收一个字节。如果只配置了RX DMA而没有触发TX时钟不会产生DMA也收不到数据。所以必须手动或通过TX DMA发送一个“哑元”字节来启动时钟。我一般用TX DMA配合把发送缓冲区填0xFF这样收发都走DMACPU完全解放。3.4 spi通信不生效的排查清单spi通信失败是家常便饭我整理了一个排查顺序按这个走基本能定位问题排查项检查方法常见问题片选信号示波器看CS是否拉低CS没接、CS极性反了时钟信号示波器看SCLK有无波形spi外设没使能、引脚复用没配时钟极性/相位对照从设备手册Mode配置错误数据线示波器看MOSI/MISOMOSI和MISO接反速率降低到100kHz试试速率太高、走线太长电源和地万用表测从设备供电从设备没供电、地没共我印象最深的一次是spi Flash读不出ID查了半天发现是CS引脚被复用成了其他功能初始化的时候没配置成GPIO。所以每次调试spi第一步永远是确认引脚配置。4. uart异步通信的可靠性与DMA接收4.1 uart波形分析与波特率误差计算uart没有时钟线靠双方约定的波特率来同步。发送方按波特率逐位输出接收方在起始位下降沿开始计时在每个位的中间点采样。如果双方波特率有偏差采样点会逐渐偏移偏差累积到半个位宽时就会采错。波特率误差的计算公式是误差 (实际波特率 - 理论波特率) / 理论波特率 × 100%。一般要求误差小于2%最好小于1%。以STM32F103为例外部晶振8MHz要得到115200bps分频系数是8000000 / 115200 ≈ 69.44。取整数69实际波特率是8000000 / 69 ≈ 115942误差约0.64%可以接受。如果取70实际波特率是114285误差约0.79%也可以。但如果你的系统时钟不是8MHz而是7.3728MHz那115200就能整除误差为0。用逻辑分析仪看uart波形时重点看起始位的下降沿是否干净每个位的宽度是否一致。如果位宽忽长忽短可能是波特率配置有问题如果波形有毛刺可能是信号完整性问题需要加串阻或缩短走线。4.2 STM32标准库uart DMA中断收发配置用DMA收发uart数据是STM32项目中的标准操作。发送用DMA可以避免CPU轮询等待接收用DMA加空闲中断可以处理不定长数据。发送配置把DMA配置为内存到外设使能uart的TX DMA请求。调用USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE)然后往发送缓冲区填数据使能DMA通道即可。接收配置DMA配置为外设到内存使能uart的RX DMA请求。关键是要开启uart的空闲中断IDLE当总线空闲时触发中断在中断里计算接收到的数据长度。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { USART_ReceiveData(USART1); // 清除IDLE标志 DMA_Cmd(DMA1_Channel5, DISABLE); rx_len RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); // 处理rx_buffer中的数据 DMA_SetCurrDataCounter(DMA1_Channel5, RX_BUFFER_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }这个方案的好处是CPU只在数据收完后处理一次中间不需要干预。但要注意DMA缓冲区要足够大防止数据溢出。我一般用256字节或512字节的环形缓冲区。4.3 uart阻塞与非阻塞的适用场景阻塞发送就是调用发送函数后一直等直到数据发完才返回。优点是代码简单缺点是浪费CPU时间。非阻塞发送是启动DMA后立即返回CPU去干别的事发完后通过中断或标志位通知。我的选择标准是如果发送频率低、数据量小比如每秒发几条日志用阻塞发送无所谓如果发送频率高、数据量大比如持续上传传感器数据必须用DMA非阻塞。接收端则一律建议用DMA加空闲中断因为你永远不知道对方什么时候发数据过来。4.4 FT231X USB转uart驱动的常见问题FT231X是常用的USB转串口芯片在Windows和Linux下都有成熟驱动。常见问题有两个一是驱动装好后设备管理器里出现黄色感叹号通常是驱动版本不匹配去官网下载最新驱动即可二是串口能打开但收发数据乱码检查波特率、数据位、停止位、校验位是否和对方一致。FT231X默认支持的最高波特率是3Mbps但实际能跑多高取决于USB带宽和驱动。在Linux下FT231X通常被识别为/dev/ttyUSB0。如果权限不够把用户加入dialout组即可。用dmesg | grep tty可以查看设备是否被正确识别。5. i2s音频协议与spi的本质区别5.1 i2s的三线制与音频时钟体系i2s用三根线传输音频SCK位时钟、WS字选择也叫LRCK、SD串行数据。SCK的频率等于采样率 × 位宽 × 声道数。比如44.1kHz采样率、16位宽、双声道SCK就是44100 × 16 × 2 1.4112MHz。WS的频率等于采样率每个音频样本翻转一次低电平表示左声道高电平表示右声道或者反过来取决于芯片定义。i2s的数据在SCK的边沿变化在另一个边沿被采样。具体是哪个边沿由芯片的i2s模式决定。标准i2s模式下数据在SCK下降沿变化上升沿采样左对齐模式下数据在WS翻转后立即有效右对齐模式则相反。5.2 i2s与spi的时序对比i2s和spi最像的地方是都有时钟线和数据线但区别在于spi的CS是传输使能信号整个传输期间保持有效i2s的WS是声道选择信号每个样本都翻转。spi的数据宽度通常是8位或16位由配置决定i2s的数据宽度固定为音频位宽16/24/32位由音频格式决定。spi可以多从设备靠CS区分i2s通常点对点一个主机对一个DAC或ADC。spi的时钟频率由主机随意设定i2s的时钟频率必须严格等于采样率 × 位宽 × 声道数否则音频会变调。我曾经试图用spi接口去驱动一个i2s DAC把WS当成CS来控制结果出来的声音断断续续。后来改用MCU的i2s外设配置好音频时钟声音立刻正常。这个教训告诉我专用协议要用专用外设不要试图用通用外设去“模拟”。5.3 i2s在音频编解码中的实际应用一个典型的i2s音频链路是MCU通过i2s发送PCM数据给DAC芯片如WM8978DAC把数字信号转成模拟信号输出到耳机或喇叭。反过来麦克风通过i2s ADC把模拟声音转成PCM数据发给MCU。配置i2s的时候除了SCK、WS、SD三根线通常还需要一根MCLK主时钟频率是采样率的256倍或384倍给DAC内部锁相环用。MCLK不稳定会导致音频有杂音。我一般用MCU的I2S_CKIN引脚或者专门的音频晶振来提供MCLK。6. 常见问题速查与独家避坑经验6.1 四种协议的问题排查速查表协议典型问题排查第一步独家技巧i2c通信不上查上拉电阻和地址用逻辑分析仪看起始条件和ACKi2c高速丢数据看SCL上升沿减小上拉电阻到2.2kspi读不出数据查CS和时钟极性降速到100kHz试spiDMA收不到查TX是否触发手动发一个0xFF启动时钟uart乱码查波特率和时钟用示波器测位宽uart丢数据查DMA缓冲和空闲中断加大缓冲区、开IDLE中断i2s噪声查MCLK和WS极性用示波器看WS频率是否等于采样率i2s变调查SCK频率SCK必须等于采样率×位宽×声道6.2 逻辑分析仪分析i2c数据的实操技巧逻辑分析仪是调试i2c的利器。接线的时候把通道0接SCL通道1接SDA地线一定要接。采样率至少设为1MHz才能看清400kHz的波形。触发条件设为SDA下降沿起始条件这样每次通信都能抓到。解码的时候大部分逻辑分析仪软件都自带i2c解码器。设置好地址位宽7位或10位和读写位软件会自动把波形翻译成地址、数据和ACK/NACK。如果解码结果和预期不符先检查采样率是否够高再检查上拉电阻是否合适。我遇到过一次逻辑分析仪解码i2c总是报错后来发现是采样率设成了100kHz而i2c跑的是400kHz欠采样导致波形失真。把采样率提到2MHz之后解码完全正常。所以逻辑分析仪的采样率至少要是信号频率的5倍以上。6.3 硬件设计阶段的预防措施很多通信问题在硬件设计阶段就可以避免。我的经验是i2c总线上每个设备旁边预留上拉电阻焊盘方便后期调整阻值。spi的时钟线走线尽量短如果超过10cm考虑加串阻匹配。uart的TX和RX之间预留跳线方便交叉连接。i2s的MCLK走线要远离高频信号避免串扰。所有通信接口的测试点都要引出来方便接逻辑分析仪。这些措施在画PCB的时候多花几分钟调试的时候能省几个小时。6.4 从项目实战中总结的选型决策树最后分享一个我在实际项目中用的选型决策树如果是音频数据流直接选i2s不要犹豫。如果需要高速数据传输1Mbps选spi。如果设备多、引脚紧张、速率要求不高选i2c。如果是点对点、跨板通信、或者需要电气隔离选uart。如果以上都不满足考虑用软件模拟时序但要做好时序余量。这个决策树不是绝对的但能覆盖80%的场景。剩下的20%就需要你根据具体芯片手册和项目约束来权衡了。我在实际使用中发现很多通信问题不是协议本身的问题而是配置细节和硬件设计的问题。把时序图看懂、把波形抓出来对比、把手册里的时序参数算清楚大部分问题都能迎刃而解。踩过几次坑之后你会发现自己对这四个协议的理解会从“知道怎么用”变成“知道为什么这么用”这才是真正值钱的经验。
返回列表