1. 四种总线的本质差异先搞清楚它们各自在解决什么问题很多刚接触嵌入式开发的朋友看到 I2C、I2S、SPI、UART 这四个名词第一反应是“都是通信协议背下来就行”。但真到了项目里要选型的时候就懵了——传感器用哪个音频用哪个调试口用哪个为什么有的要上拉电阻有的要片选线有的连时钟线都没有我做了十多年硬件和底层驱动踩过的坑比看过的数据手册还多。这篇文章不打算给你背教科书而是从“它们各自为什么被设计出来”这个角度把四者的本质差异讲透。你只有理解了设计动机选型时才能不纠结调试时才能一眼看出问题在哪。先说一个最核心的判断维度这四种总线本质上是在“引脚数量、通信速率、传输距离、拓扑结构”这四个约束之间做不同的取舍。没有哪个是万能的每个都有自己的甜点区。UART 是最古老的它解决的是“两个设备之间点对点、异步、低速”的通信需求。注意“异步”这个词——它没有时钟线靠双方约定的波特率来同步。这就好比两个人约好每分钟说一个字没有节拍器全靠各自掐表。所以 UART 对时钟精度的要求比较高波特率误差超过 2% 到 3% 就可能收到乱码。I2C 解决的是“多个设备共享两根线”的问题。它用开漏输出加外部上拉电阻的方式实现了真正的多主多从总线。你可以挂几十个传感器在同一个 I2C 总线上每个设备有自己的地址。代价是速率上不去标准模式 100kHz快速模式 400kHz高速模式 3.4MHz 但很少用。SPI 解决的是“高速、全双工、点对点或一主多从”的需求。它有独立的时钟线所以速率可以拉到几十 MHz 甚至上百 MHz。但它没有地址机制每个从设备需要一根独立的片选线。挂 8 个从设备就是 8 根片选线加 4 根信号线引脚开销很大。I2S 则是专门为音频数据流设计的。它脱胎于 SPI 的时序思路但针对音频做了专门优化——独立的左右声道时钟、精确的帧同步信号、固定 16/24/32 位数据宽度。你可以把它理解为“为 PCM 音频量身定制的 SPI 变体”。一个常见的误解很多人以为 I2S 就是 SPI 的一种。严格来说它们的物理层确实很像但协议层完全不同。I2S 有自己独立的规范不能直接拿 SPI 控制器去驱动 I2S 设备反之亦然。理解了这四个设计动机后面的对比就顺理成章了。下面我逐个拆解每种总线的时序细节、硬件设计要点和典型应用场景。2. UART没有时钟线的异步通信到底怎么保证不丢数据2.1 起始位、数据位、校验位、停止位——帧格式的每一个bit都有意义UART 的帧结构看起来简单但每个字段的设计都有讲究。一个典型的 UART 帧是1 个起始位低电平 5 到 9 个数据位 0 或 1 个校验位 1 或 2 个停止位高电平。起始位的核心作用是“唤醒接收方”。因为 UART 没有时钟线接收方平时处于空闲状态高电平一旦检测到下降沿就知道有数据来了然后按照约定的波特率开始采样。这里有个关键细节接收方通常在起始位的中间时刻开始采样之后每隔一个波特率周期采样一次。这就是为什么波特率误差不能太大——误差累积到数据位中间时采样点就偏了。校验位分奇校验和偶校验。奇校验保证数据位加校验位中 1 的个数为奇数偶校验则为偶数。说实话在实际项目中校验位用得越来越少因为它的检错能力很弱——只能检测奇数个 bit 翻转偶数个翻转检测不出来。很多场景直接关掉校验位靠上层协议做 CRC。停止位的作用是“帧结束标志”同时给接收方一个缓冲时间。1 个停止位是最常见的2 个停止位用在一些对时序要求更宽松的老设备上。2.2 波特率误差计算为什么 115200 比 9600 更容易出问题波特率误差的来源是时钟源的分频。假设你的 MCU 主频是 72MHz要产生 115200 的波特率分频系数是 72,000,000 / 115200 625。这个数是整数所以误差为 0。但如果主频是 25MHz25,000,000 / 115200 217.01分频系数只能取 217实际波特率变成 25,000,000 / 217 115,207误差约 0.006%可以接受。问题出在一些“奇怪”的主频上。比如 8MHz 主频要产生 1152008,000,000 / 115200 69.44取 69 的话实际波特率是 115,942误差 0.64%。这个误差在 10 位帧1 起始 8 数据 1 停止下累积到第 10 位时采样点偏移了约 6.4% 的位宽已经接近容限了。如果双方误差方向相反加起来超过 10%就会丢数据。实操建议如果你的项目对 UART 稳定性要求高尽量选主频能被常用波特率整除的晶振。比如 11.0592MHz 就是经典选择它能整除 9600、19200、38400、57600、115200 等所有常用波特率。2.3 硬件流控 RTS/CTS什么时候必须加什么时候可以省UART 的硬件流控用两根额外的线RTSRequest To Send和 CTSClear To Send。发送方拉低 RTS 表示“我要发数据了”接收方拉低 CTS 表示“我准备好了你发吧”。什么时候必须加流控当接收方的处理速度跟不上发送方时。比如你用 UART 接一个低速单片机它收到数据后需要时间处理如果发送方一直发接收方的缓冲区就溢出了。加了流控后接收方缓冲区快满时拉高 CTS发送方就暂停。但大多数场景下流控是可以省的。因为现代 MCU 的 UART 都有 DMA 和足够大的 FIFO接收方基本不会溢出。省掉流控可以少接两根线PCB 布线也简单。2.4 UART 调试实战逻辑分析仪抓波形怎么看用逻辑分析仪抓 UART 波形时第一步是确认波特率。如果你不知道对方波特率可以测量起始位下降沿到第一个数据位中间的时间这个时间就是一个波特率周期。比如测出来是 8.68 微秒那波特率就是 1 / 8.68us ≈ 115200。第二步是看数据位。从起始位之后开始每个波特率周期采样一次得到 8 个 bit。注意 UART 是低位先发LSB first所以第一个数据位是 bit0最后一个是 bit7。第三步是看停止位。停止位应该是高电平如果抓到低电平说明帧格式不匹配或者线路有问题。我遇到过一种情况波形看起来完全正确但接收方就是收不到数据。后来发现是地线没接。UART 是单端信号双方必须共地否则电平参考不一致接收方可能把高电平识别成低电平。这个坑很隐蔽因为用万用表测电压看起来是对的但波形就是不对。3. I2C两根线挂几十个设备开漏输出和上拉电阻是灵魂3.1 为什么 I2C 必须用开漏输出加外部上拉这是 I2C 最核心的设计也是面试高频题。I2C 的 SDA 和 SCL 两根线都要求开漏Open-Drain输出不能推挽。原因在于 I2C 是多主多从总线。如果两个主机同时推挽输出一个输出高电平一个输出低电平就会形成电源到地的直通路径瞬间大电流烧毁引脚。开漏输出则不会——开漏只能拉低不能拉高。高电平靠外部上拉电阻提供。这样即使多个设备同时拉低也只是并联导通不会短路。上拉电阻的取值是个权衡。阻值太小功耗大低电平时灌电流大阻值太大上升沿变缓高速通信时波形还没到高电平就被拉低了。标准模式 100kHz 下4.7kΩ 是经典值快速模式 400kHz 下通常用 2.2kΩ 或 1.5kΩ。计算公式是这样的上升时间 t_r ≈ 0.847 × R_pullup × C_bus。C_bus 是总线电容包括 PCB 走线电容和器件引脚电容一般每根线 10 到 50pF。如果 C_bus 50pFR_pullup 4.7kΩt_r ≈ 0.847 × 4700 × 50e-12 ≈ 199ns。I2C 标准要求 100kHz 下上升时间小于 1000ns400kHz 下小于 300ns。所以 4.7kΩ 在 100kHz 下没问题400kHz 下就偏大了。我踩过的坑有一次用 10kΩ 上拉跑 400kHz波形上升沿明显变缓逻辑分析仪解码时好时坏。换成 2.2kΩ 后立刻稳定。所以上拉电阻小了不通信是伪命题大了才不通信。热词里说的“i2c上拉电阻小了不通信”可能是把因果关系搞反了。3.2 I2C 时序图精读起始、地址、ACK、数据、停止I2C 的完整时序是这样的起始条件SCL 高电平时SDA 从高变低。这个特殊组合不会出现在数据位中所以接收方可以据此判断帧开始。地址帧7 位地址 1 位读写标志。读写标志 0 表示写1 表示读。ACK/NACK每发送 8 位后接收方拉低 SDA 表示 ACK保持高电平表示 NACK。数据帧8 位数据 ACK/NACK可以连续发多个字节。停止条件SCL 高电平时SDA 从低变高。这里有个容易忽略的细节数据位在 SCL 低电平时变化在 SCL 高电平时必须保持稳定。因为接收方是在 SCL 高电平期间采样 SDA 的。如果你在 SCL 高电平时改变 SDA接收方可能采到错误的值甚至误判为起始或停止条件。3.3 I2C 读写 EEPROM 的完整代码流程以 24C02 为例24C02 是最经典的 I2C EEPROM很多新手拿它练手。写一个字节的流程是// 伪代码基于 STM32 硬件 I2C void EEPROM_WriteByte(uint8_t addr, uint8_t data) { I2C_Start(); I2C_SendByte(0xA0); // 设备地址 写标志 I2C_WaitAck(); I2C_SendByte(addr); // 内部地址 I2C_WaitAck(); I2C_SendByte(data); // 数据 I2C_WaitAck(); I2C_Stop(); HAL_Delay(5); // 等待 EEPROM 内部写入完成 }注意最后的HAL_Delay(5)。24C02 在收到停止条件后内部会启动一个写周期大约 5ms 内不响应任何总线操作。如果你紧接着发下一个起始条件它会 NACK。所以要么延时要么用“应答查询”方式——反复发起始条件加设备地址直到收到 ACK 为止。读一个字节的流程稍微复杂需要先写内部地址再重启读uint8_t EEPROM_ReadByte(uint8_t addr) { uint8_t data; I2C_Start(); I2C_SendByte(0xA0); // 写标志 I2C_WaitAck(); I2C_SendByte(addr); // 内部地址 I2C_WaitAck(); I2C_Start(); // 重启 I2C_SendByte(0xA1); // 读标志 I2C_WaitAck(); data I2C_ReadByte(); I2C_SendNack(); // 最后一个字节回 NACK I2C_Stop(); return data; }3.4 I2C 扩展与 PMBus什么时候该考虑换方案I2C 的地址空间只有 7 位理论上最多 112 个设备去掉保留地址。实际项目中地址冲突是常见问题。比如你挂两个同型号的温湿度传感器它们的地址是固定的就冲突了。解决方案有几个一是用 I2C 多路复用器比如 TCA9548A一路变八路二是用带地址选择引脚的器件通过拉高拉低改变地址三是换用 SPI片选线天然解决冲突。PMBus 是在 I2C 基础上发展出来的电源管理协议。它的物理层和 I2C 兼容但协议层定义了电源相关的命令集比如读电压、读电流、设置输出电压等。如果你在做多路电源管理PMBus 比裸 I2C 方便得多。4. SPI速度最快的总线但片选和时序模式是两大坑4.1 四根线的分工与全双工的本质SPI 有四根线SCLK时钟、MOSI主出从入、MISO主入从出、CS片选。它的全双工本质是每个时钟周期主机和从机同时交换一位数据。主机通过 MOSI 发一位给从机同时从机通过 MISO 发一位给主机。所以 SPI 的“发送”和“接收”是同一个动作你发一个字节出去必然收到一个字节回来。这个特性带来一个常见困惑只想发数据不想收数据怎么办答案是收就收了忽略即可。但要注意如果你不发数据从机也不会发数据给你因为时钟是你控制的。4.2 CPOL 和 CPHA 的四种组合模式 0 到模式 3SPI 的时序模式由两个参数决定CPOL时钟极性和 CPHA时钟相位。CPOL 0空闲时 SCLK 为低电平CPOL 1空闲时 SCLK 为高电平CPHA 0在第一个时钟边沿采样CPHA 1在第二个时钟边沿采样组合起来就是四种模式模式CPOLCPHA采样边沿空闲电平000上升沿低101下降沿低210下降沿高311上升沿高最常用的是模式 0 和模式 3。大部分 Flash 芯片用模式 0部分传感器用模式 3。调试 SPI 不通信时第一个要检查的就是模式是否匹配。我见过太多人因为模式设错抓波形看起来有数据但解码全是乱的。4.3 硬件片选与软件片选的取舍硬件片选是 SPI 控制器自动控制 CS 引脚发送数据前拉低发送完拉高。软件片选是你用普通 GPIO 手动控制。硬件片选的优点是时序精确不占用 CPU。缺点是片选引脚必须接到 SPI 控制器的专用 CS 引脚上布线受限。软件片选则灵活得多任何 GPIO 都能当片选而且可以在两次传输之间插入自定义延时。我的经验如果从设备对 CS 建立时间和保持时间有特殊要求优先用软件片选。比如某些 ADC 芯片要求 CS 拉低后等待 100ns 才能发时钟硬件片选可能满足不了软件片选加个__NOP()就解决了。4.4 STM32F103 SPI 通过 DMA 读取芯片数据的完整配置用 DMA 做 SPI 传输是 STM32 的经典操作。以 STM32F103 标准库为例配置步骤如下// 1. 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE); RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. 配置 SPI 引脚 GPIO_InitStructure.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; // SCK, MOSI GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; // MISO GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // 3. 配置 SPI SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); // 4. 配置 DMA DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)SPI1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)rxBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize len; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel2, DMA_InitStructure); // 5. 启动传输 SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE); DMA_Cmd(DMA1_Channel2, ENABLE);这里有个关键点SPI 是全双工DMA 接收的同时必须有人发时钟。所以通常还要配置一个 DMA 发送通道发一堆 0xFF 或者实际命令才能产生时钟让从机把数据吐出来。4.5 SPI 通信不生效的排查链路SPI 不通信按这个顺序查片选有没有拉低用万用表或逻辑分析仪看 CS 引脚。如果一直是高从机根本不理你。时钟有没有出来看 SCLK 引脚。如果没有波形检查 SPI 使能位和 DMA 配置。模式对不对对照从机数据手册的 CPOL/CPHA 要求确认模式匹配。MISO 有没有数据如果 SCLK 和 MOSI 都正常但 MISO 一直是高或一直是低检查从机供电和片选。速率是不是太快降低波特率预分频试试。有些从机在高速下时序余量不足。我遇到过一次 SPI Flash 读不出 ID 的情况查了半天发现是WP 引脚和 HOLD 引脚没拉高。这两个引脚悬空时Flash 可能进入保护状态不响应任何命令。数据手册上写了要拉高但原理图漏了。这个坑很典型建议画原理图时把 Flash 的所有控制引脚都加上拉。5. I2S音频专用总线和 SPI 长得像但完全不是一回事5.1 I2S 的三根信号线BCLK、LRCLK、SDI2S 有三根核心信号线BCLKBit Clock位时钟每个 bit 一个脉冲。LRCLKLeft/Right Clock左右声道时钟也叫 WSWord Select。低电平表示左声道高电平表示右声道。SDSerial Data串行数据。如果是主机模式还需要MCLKMaster Clock通常是采样率的 256 倍或 384 倍给外部 DAC 或 ADC 提供参考时钟。I2S 的数据格式有个特点LRCLK 变化后的第二个 BCLK 边沿才开始传输数据。这是为了给接收方一个建立时间。具体来说LRCLK 在 BCLK 的下降沿变化数据在下一个 BCLK 的上升沿被采样。这个规则是 I2S 标准规定的和 SPI 的 CPHA1 有点像但不完全一样。5.2 I2S 与 SPI 的本质区别帧同步 vs 片选很多人觉得 I2S 就是 SPI 加了个左右声道信号。这个理解不算错但忽略了一个关键区别SPI 用片选线选择从设备I2S 用帧同步信号区分声道。SPI 的片选是“选设备”同一时刻只有一个从设备被选中。I2S 的 LRCLK 是“选声道”左右声道的数据交替传输不需要额外的片选。而且 I2S 的数据流是连续的没有“帧”的概念——你发多少个采样点都行LRCLK 一直翻转。另一个区别是数据宽度。SPI 的数据宽度可以是 4 到 16 位任意值I2S 通常是 16、24 或 32 位。I2S 的 BCLK 频率 采样率 × 声道数 × 数据位宽。比如 48kHz 采样率、双声道、16 位数据BCLK 48000 × 2 × 16 1.536MHz。5.3 音频项目中的 I2S 配置要点做音频项目时I2S 配置有几个容易出问题的地方第一主从模式要匹配。如果你的 MCU 是主机DAC 是从机那 MCU 要输出 BCLK 和 LRCLK。如果搞反了两边都不出数据。第二MCLK 要不要。有些 DAC 需要 MCLK 才能工作有些不需要。看数据手册。如果需要但你没接DAC 可能输出噪声或者完全没声音。第三数据对齐方式。I2S 有标准对齐、左对齐、右对齐三种。标准对齐就是前面说的“LRCLK 变化后第二个 BCLK 边沿开始传输”。左对齐是 LRCLK 变化后第一个边沿就开始。右对齐则相反。如果对齐方式不匹配声音会失真或者左右声道互换。我踩过的坑有一次调试音频声音听起来正常但左右声道反了。查了半天发现是 LRCLK 的极性设反了。I2S 标准规定 LRCLK 低电平是左声道但有些芯片可以配置成高电平左声道。改一个寄存器就好了。6. 选型决策拿到一个需求怎么快速确定用哪种总线6.1 按速率需求筛选先看你的数据速率要求低于 1MbpsUART 或 I2C 都行。如果设备多、引脚紧张选 I2C如果点对点、需要简单选 UART。1Mbps 到 10MbpsSPI 是首选。I2C 快速模式勉强到 400kHz高速模式 3.4MHz 但器件少。10Mbps 以上SPI 或 I2S。SPI 可以到 50MHz 甚至更高I2S 的 BCLK 通常几 MHz。音频流I2S没有第二个选择。6.2 按拓扑结构筛选点对点UART 最简单两根线TX/RX搞定。一主多从设备少SPI每个设备一根片选。一主多从设备多I2C两根线挂几十个。多主多从I2C 是唯一选择。SPI 不支持多主。6.3 按引脚预算筛选假设你要挂 4 个传感器UART每个传感器一对 TX/RX共 8 根线。但 UART 通常不支持一主多从所以实际不可行。I2C2 根线所有传感器共享。SPI4 根信号线 4 根片选 8 根线。I2S不适用。所以引脚紧张时I2C 优势明显。但 I2C 的速率和地址冲突是代价。6.4 一张表总结四种总线的核心参数特性UARTI2CSPII2S信号线数2TX/RX2SDA/SCL4 NCS3 MCLK时钟异步同步同步同步全双工是半双工是是音频流多主否是否否典型速率9600~1Mbps100k~3.4MHz1~100MHz1~10MHz上拉电阻不需要需要不需要不需要片选不需要地址寻址片选线帧同步典型应用调试口、GPS、蓝牙传感器、EEPROMFlash、ADC、屏幕音频编解码6.5 混合使用场景一个项目里四种总线同时存在实际项目中四种总线经常同时出现。比如一个智能音箱UART 接蓝牙模块用于调试和固件升级。I2C 接温湿度传感器和触摸屏控制器。SPI 接 Flash 存储音频文件。I2S 接音频 DAC 和麦克风。这时候 PCB 布线要注意I2S 的时钟线要远离模拟音频线否则 BCLK 的谐波会串到音频输出里产生可闻的噪声。我一般把 I2S 走线包地处理或者在 BCLK 上串一个 22Ω 电阻减缓边沿。7. 调试工具与方法逻辑分析仪怎么用才高效7.1 逻辑分析仪选型采样率至少是信号频率的 5 倍选逻辑分析仪第一个看采样率。要抓 10MHz 的 SPI 时钟采样率至少 50MHz最好 100MHz。采样率不够波形会混叠解码全错。第二个看通道数。I2C 要 2 通道SPI 要 4 通道I2S 要 3 到 4 通道。如果同时抓多种总线8 通道起步。第三个看协议解码支持。主流逻辑分析仪软件都支持 I2C、SPI、UART 解码但 I2S 解码支持参差不齐。买之前确认一下。7.2 抓 I2C 数据时怎么触发抓 I2C 最常用的触发条件是“起始条件”。设置 SDA 下降沿触发同时 SCL 为高电平。这样每次总线启动时都会触发不会漏掉任何一帧。如果想抓特定地址的数据可以设置“地址匹配”触发。比如只抓地址 0x50 的 EEPROM 操作其他忽略。这个功能在调试多设备总线时非常有用。7.3 抓 SPI 数据时怎么对齐SPI 解码的关键是模式匹配。在逻辑分析仪软件里设置 CPOL 和 CPHA和你的 SPI 配置一致。如果设错了解码出来的数据全是乱的。另外SPI 的片选信号也要接到逻辑分析仪上。有些软件用片选作为帧边界没有片选的话它不知道一帧数据从哪开始到哪结束。7.4 UART 波形分析从起始位反推波特率前面提过测起始位下降沿到第一个数据位中间的时间可以反推波特率。具体操作是在逻辑分析仪软件里放大波形把光标放在起始位下降沿另一个光标放在第一个数据位的中间读时间差。然后用 1 除以这个时间差就是波特率。如果测出来是 104 微秒1 / 104us ≈ 9615接近 9600。如果测出来是 8.68 微秒1 / 8.68us ≈ 115200。这个方法在不知道对方波特率时特别管用。8. 那些数据手册不会告诉你的实战经验8.1 I2C 上拉电阻的功耗计算上拉电阻不是随便选的要考虑功耗。假设总线电压 3.3V上拉电阻 2.2kΩ当 SDA 被拉低时灌电流 3.3V / 2.2kΩ 1.5mA。如果总线上有 10 个设备同时拉低总灌电流就是 15mA。这个电流会流过每个设备的开漏引脚可能超过引脚的灌电流能力。所以挂的设备越多上拉电阻要越大。但大了又影响上升时间。折中方案是设备少用 2.2kΩ设备多用 4.7kΩ 甚至 10kΩ同时降低速率。8.2 SPI 长距离传输的注意事项SPI 设计时是板级总线不适合长距离传输。如果非要用长线比如 50cm 以上要注意降低速率。10MHz 在长线上反射严重降到 1MHz 可能就稳了。串接电阻。在 SCLK 和 MOSI 上串 22Ω 到 100Ω 电阻减缓边沿减少反射。用屏蔽线。MISO 和 SCLK 容易互相干扰屏蔽层接地能改善。8.3 UART 阻塞和非阻塞发送的区别UART 发送分阻塞和非阻塞。阻塞发送是HAL_UART_Transmit()CPU 一直等到发完才返回。非阻塞是HAL_UART_Transmit_IT()或HAL_UART_Transmit_DMA()发完触发中断或 DMA 完成回调。阻塞发送的优点是简单缺点是浪费 CPU。如果你在主循环里发 100 字节115200 波特率下要等约 8.7ms这期间 CPU 什么都干不了。非阻塞发送则可以把这 8.7ms 用来处理其他任务。我的建议调试阶段用阻塞发送简单直接。产品阶段用 DMA 发送CPU 占用率低。但 DMA 发送要注意缓冲区生命周期——DMA 还没发完你不能修改缓冲区内容。8.4 STM32 半双工 SPI 的使用场景STM32 的 SPI 支持半双工模式MOSI 和 MISO 合并成一根线。这个模式适合单线双向通信的场景比如某些单线 SPI 的传感器。配置半双工时SPI_Direction设为SPI_Direction_1Line_Tx或SPI_Direction_1Line_Rx。发送时用 Tx 模式接收时切换到 Rx 模式。注意切换时要先禁用 SPI改完再使能。8.5 逻辑分析仪分析 I2C 数据的常见误判用逻辑分析仪解码 I2C 时最常见的误判是“把起始条件识别成数据”。如果上拉电阻太大上升沿太缓SDA 在 SCL 高电平期间还没完全拉高软件可能误判为起始条件。解决办法是调整触发电平。默认触发电平是 1.65V3.3V 的一半如果波形边沿缓可以调到 1.0V 或 2.0V 试试。另外降低采样率有时反而能减少误判因为采样点少了不容易采到边沿抖动。8.6 I2C 编码器的读取技巧I2C 编码器比如 AS5600是最近比较火的位置传感器。它的 I2C 地址固定读取角度寄存器的流程是先写寄存器地址再重启读两个字节。注意 AS5600 的角度寄存器是 12 位的高 4 位在第一个字节的高 4 位低 8 位在第二个字节。拼接的时候要小心移位uint16_t angle ((rxBuffer[0] 0x0F) 8) | rxBuffer[1];如果忘了 0x0F高 4 位的状态位会混进来角度值就错了。这个坑我踩过查了一下午才发现。8.7 SPI Flash 的固件加载流程用 SPI Flash 加载固件是嵌入式常见需求。以 RTL9071CP-VB 为例流程是上电后MCU 通过 SPI 读取 Flash 的前几个字节判断是否有有效固件。如果有把固件从 Flash 搬到 RAM 或者直接 XIP 执行。如果没有进入 USB 或 UART 升级模式接收新固件写入 Flash。这里的关键是SPI 的读取速率。如果 Flash 是 50MHz 的MCU 的 SPI 也要能跑到 50MHz。STM32F103 的 SPI1 最高 36MHzSPI2 最高 18MHz。如果 Flash 支持 50MHz 但 MCU 只能跑 18MHz那就只能降速用。8.8 鸿蒙开发板上的 HID over I2C鸿蒙开发板支持 HID over I2C意思是触摸屏、键盘等 HID 设备通过 I2C 总线连接。这个场景下I2C 的速率要求不高100kHz 足够但中断响应要快。触摸屏的中断引脚接到 MCU 的 EXTI一有触摸就触发中断MCU 再通过 I2C 读坐标。注意 HID over I2C 的地址分配。鸿蒙系统对 HID 设备的 I2C 地址有规定不能随便设。具体看开发板的文档。9. 常见问题快查表问题现象可能原因排查方法I2C 完全无响应上拉电阻缺失或太大测 SDA/SCL 空闲电平应为高I2C 偶尔 NACK总线电容太大上升沿缓减小上拉电阻或降低速率SPI 读回全 0xFFMISO 未接或从机未供电测从机 VCC 和 MISO 波形SPI 读回全 0x00片选未拉低或模式错误检查 CS 和 CPOL/CPHAUART 收到乱码波特率不匹配逻辑分析仪测位宽反推波特率UART 完全无数据TX/RX 接反或未共地交换 TX/RX确认共地I2S 无声音MCLK 缺失或 LRCLK 极性反检查 MCLK 输出和声道配置I2S 声音失真数据对齐方式错误改为标准对齐或左对齐这张表是我多年调试经验的浓缩遇到问题先查表能省不少时间。10. 最后分享几个我常用的调试习惯第一个习惯每次调试新总线先用逻辑分析仪抓一次波形。不要凭感觉猜波形不会骗人。抓一次波形确认时序、电平、模式都对再写代码。第二个习惯上拉电阻和片选线在原理图阶段就标清楚。I2C 的上拉电阻画在总线两端SPI 的片选线标注每个从设备的编号。这些细节在调试阶段能救命。第三个习惯保留一个已知能工作的参考配置。比如你调通了一个 SPI Flash把配置参数记下来。下次调另一个 Flash先套用参考配置再改差异部分。这样比从头配快得多。第四个习惯遇到问题先降速。SPI 不通信降到 1MHz 试试I2C 不通信降到 100kHz 试试。很多问题都是速率太快导致的降速能快速排除时序余量问题。这四个习惯看起来简单但坚持下来调试效率至少翻倍。我见过太多人一上来就怀疑芯片坏了、代码有 bug结果查了半天发现是上拉电阻没焊。先抓波形先降速先查硬件这三板斧能解决 80% 的总线通信问题。