SPI、I2C、UART选型与时序调试:STM32F103实战避坑
2026/9/18 17:36:47 网站建设 项目流程

1. 三种接口的定位之争:选型错了,后面全是坑

搞硬件这行,SPI、UART、I²C 这三个词几乎是从入行第一天就刻进脑子里的。可真到项目上,很多人还是会卡在同一个问题上:手上这颗传感器、这颗存储芯片、这块屏,到底挂哪条总线?选 UART 吧,速率不够;选 I²C 吧,怕它卡总线;选 SPI 吧,又嫌它占引脚。最后往往是"上次那个项目用啥我就用啥",能跑通就收工,跑不通就开始怀疑人生。

我这几年做的板子从 STM32F103 这种经典款,到带屏、带 Flash、带一堆传感器的复合板都有,SPI、UART、I²C 三种接口的坑基本都踩过一遍。这篇文章想干的事很明确:把三种接口的选型逻辑讲透,把时序层面真正影响调试的那几个点挖出来,再配上一套可以直接照着复现的实操流程——CubeMX 配 SPI + DMA 读芯片数据、UART 调试链路打通并落盘日志、I²C 总线死锁恢复。不管你是刚上手画第一块板的学生,还是天天跟示波器打交道的工程师,看完至少能少熬两个通宵。

先说结论性的判断,方便你带着框架往下读:SPI 是"快、独占、线多",I²C 是"慢、共享、线少",UART 是"点对点、异步、最省事"。这三个定位没有优劣之分,只有场景匹配度。真正让人吃亏的从来不是"不知道用哪个",而是"用错了还硬调",把时间全耗在了本来就不该走的路上。

1.1 从物理层看:三根线、两根线、一根线的代价

SPI 的物理层是四根线打底:SCLK、MOSI、MISO、CS。全双工,主机给时钟,数据同时在两个方向上跑。每多挂一个从设备,就多一根 CS。挂八个从设备,光片选就八根线,这在小封装 MCU 上是真奢侈。但它的代价换来了收益——没有地址概念、没有仲裁、没有应答,时钟给多快数据就跑多快,协议开销几乎为零。

UART 是两根线:TX 和 RX。异步,没有时钟线,双方靠事先约定好的波特率对时。这个"约定"是它最大的优点也是最大的软肋:优点是线少、跨设备简单,一根 USB 转串口就能跟电脑说话;软肋是双方时钟必须足够接近,偏一点就采样错位,整帧数据崩掉。UART 天生只能点对点,想挂多个设备就得靠多路串口或者外扩芯片。

I²C 是两根线:SCL 和 SDA,而且都是开漏输出,靠上拉电阻把电平拉高。所有设备并联在同一对线上,靠 7 位地址区分彼此。这两根线理论上能挂一百多个设备,实际受总线电容限制,一般也就挂几个到十几个。它的"少线"是用"慢"和"复杂"换来的——开漏意味着上升沿是 RC 充电,速率上不去;共享总线意味着要处理地址冲突和仲裁。

注意:画原理图时最常见的低级错误,是把 I²C 的 SDA/SCL 当普通推挽 IO 用。开漏输出必须配上拉电阻,没有上拉,总线永远是低电平,芯片连 ACK 都发不出来。

1.2 速度、距离、拓扑与功耗的四维权衡

选型的时候我一般按四个维度过一遍,比单纯看速率靠谱得多。

速率上,SPI 在 STM32F103 上 SPI1 挂 APB2(72MHz),最高能到 18MHz;SPI2 挂 APB1(36MHz),最高 9MHz。I²C 标准模式 100kHz,快速模式 400kHz,F1 基本就到这。UART 常见 115200,高一点到 921600 甚至 2M 也能跑,但线路一长就掉链子。所以需要搬大量数据的场景,比如读 Flash、刷屏、接高速 ADC,SPI 几乎是唯一答案。

距离上,三个都是板内总线,超过几十厘米都要慎重。UART 相对皮实一点,配个 RS-485 收发器能拉上千米(那是另一套电气标准了)。I²C 对总线电容最敏感,官方建议总电容不超过 400pF,一条长排线走过去很容易就超了。SPI 高速时对走线阻抗和匹配更敏感,波形一塌糊涂的时候先看示波器。

拓扑上,SPI 是星型(每个从机一根 CS),I²C 是总线型(全并联),UART 是点对点。这个差异在 PCB 布局阶段就决定了你能不能省引脚。

功耗上,I²C 因为开漏加外部上拉,静态时总有电流从电阻流向被拉低的线,功耗比想象中大;SPI 的 CS 拉高后从机进休眠,整体更干净。低功耗产品里这点经常被忽略,实测能差出几百微安。

1.3 一张选型对照表,直接抄

维度SPII²CUART
信号线4 根起(SCLK/MOSI/MISO/CS)2 根(SCL/SDA)2 根(TX/RX)
拓扑星型,每从机一根 CS总线型,并联共享点对点
典型速率1M~18M100k / 400k9.6k~921.6k
双工全双工半双工全双工
寻址无,靠 CS 片选7 位地址
应答机制有 ACK/NACK无(可选校验)
引脚开销
典型器件Flash、LCD、ADC、无线模块传感器、EEPROM、IO 扩展GPS、蓝牙、模组、调试口

这张表我建议直接贴在工位上。选型时对着过一遍,多数纠结五分钟内就能定。

2. 把时序图看懂,调试就赢了一半

我见过太多人调不通接口就换芯片、换板子、怀疑买到假货,最后发现是 CPOL/CPHA 配错了,或者波特率算错了 0.5%。接口调试的 90% 问题,根子都在时序上。这一节把三种接口时序里最容易出错的地方逐个拆开。

2.1 SPI的四种模式,CPOL和CPHA到底怎么配

SPI 的四种模式来自两个参数:CPOL(时钟极性)和 CPHA(时钟相位)。CPOL 决定 SCK 空闲时是高还是低:CPOL=0 空闲低电平,CPOL=1 空闲高电平。CPHA 决定数据在哪个时钟边沿被采样:CPHA=0 在第一个边沿采样,CPHA=1 在第二个边沿采样。

组合起来就是四种模式:

  • Mode 0:CPOL=0,CPHA=0。SCK 空闲低,上升沿采样,下降沿输出。这是最常见的一种,绝大多数 SPI Flash 和传感器默认用它。
  • Mode 1:CPOL=0,CPHA=1。SCK 空闲低,下降沿采样,上升沿输出。
  • Mode 2:CPOL=1,CPHA=0。SCK 空闲高,下降沿采样。
  • Mode 3:CPOL=1,CPHA=1。SCK 空闲高,上升沿采样。用的人也很多,尤其是一些 ADC 和无线芯片。

怎么从手册里读出该用哪个?手册一般有两种写法。一种是直接写"支持 SPI Mode 0/3",那就照抄。另一种是描述边沿行为,比如"数据在 SCK 上升沿被采样,在下降沿被更新"。这时候要翻译:采样在上升沿,且 SCK 空闲状态决定了这是第几个边沿。如果芯片没说空闲电平,通常默认空闲低,那就是 CPOL=0、CPHA=0,即 Mode 0。

提示:CPOL 配错,波形看起来会"整体反相",数据全错;CPHA 配错,波形形状对但采样点偏了一个边沿,表现为数据稳定地错位一位。示波器抓 SCK 和数据线,量一下采样点落在数据眼图正中还是边缘,一眼就能定位是哪个参数的问题。

还有一个容易被忽略的点:First Bit 顺序。多数器件是 MSB First,但确实有 LSB First 的。这个配错的结果是数据整体比特反转,比如 0x01 读成 0x80,非常好认。

2.2 UART波特率误差怎么算,为什么差3%就乱码

UART 没有时钟线,接收端靠检测起始位的下降沿来对齐,然后在位周期中间采样。理想情况下,接收端在第 8、9 位(一帧第 0 位是起始位)附近采样,留出的容错余量大概是半个位周期。

STM32 的波特率计算公式是:

USARTDIV = fCK / (16 × BaudRate) BRR = round(USARTDIV) 实际波特率 = fCK / (16 × USARTDIV整数化后的值)

以 STM32F103 为例,USART1 挂 APB2,fCK=72MHz。要 115200:

USARTDIV = 72000000 / (16 × 115200) = 39.0625 BRR 的整数部分 = 39,小数部分 = 0.0625 × 16 = 1 实际波特率 = 72000000 / (16 × (39 + 1/16)) = 115200

误差为 0。这就是为什么 72MHz 主频配 115200 特别稳。反过来,如果你用的是 8MHz 晶振,跑 115200:

USARTDIV = 8000000 / (16 × 115200) = 4.3403 取整数部分 4,小数部分 0.3403×16 = 5.44 → 取 5 实际 = 8000000 / (16 × (4 + 5/16)) = 111111 误差 = (111111 - 115200)/115200 ≈ -3.5%

这个误差已经超标了,接收端采样点会逐渐偏移到位的边缘,跑几帧就开始出错。解决办法是换合适的晶振频率,或者降波特率到 9600(误差会小很多)。

经验阈值:误差在 ±2% 以内安全,2%~3% 勉强,超过 3% 基本必乱码。而且这个误差是收发双方累积的,如果对端也不准,两边一叠加就更玄。所以调试串口出问题时,第一件事不是换线,是算一遍波特率误差。

2.3 I²C的开漏输出、上拉电阻与时钟拉伸

I²C 的三条铁律:开漏输出、上拉电阻、ACK 应答。

开漏输出意味着任何设备都只能把线拉低,不能主动拉高。高电平完全靠上拉电阻充上去,所以上升沿是个 RC 充电过程,波形是圆弧而不是陡峭的方波。这也是 I²C 速率上不去的根本原因。

时钟拉伸(Clock Stretching)是 I²C 独有的机制:从机如果处理不过来,可以把 SCL 拉低不放,强制主机等着。很多 MCU 的硬件 I²C 外设对这个机制支持不好,表现为通信卡死。STM32F1 的硬件 I²C 在遇到某些从机(尤其是反复拉伸时钟的传感器)时会出现总线挂死,这是老生常谈的问题。

还有一点,I²C 的 ACK 是接收方在第九个时钟周期把 SDA 拉低。很多新手用示波器抓波形,看到第九个脉冲后 SDA 没被拉低,就以为是数据错了,其实是"从机没应答",根因可能是地址写错、器件没上电、或者总线死锁。

3. STM32F103实操:CubeMX配置SPI+DMA读取芯片数据

理论讲完了,接下来是最实用的部分。我用 STM32F103 + CubeMX 演示一遍 SPI 配 DMA 读芯片数据的完整流程,这套方法适用于读 Flash、读传感器、读 ADC,换一下指令字节就能复用。

3.1 CubeMX里这8个参数决定成败

打开 CubeMX,选好 STM32F103C8 或者你手上的型号,配好时钟树(外部晶振 8MHz,PLL 倍频到 72MHz)。然后进 SPI1 配置:

参数设置值说明
ModeFull-Duplex Master全双工主机模式
Hardware NSS SignalDisable用软件控制 CS,后面细说
Data Size8 Bits绝大多数器件是 8 位
First BitMSB First除非手册特别说明
Prescaler872MHz/8 = 9MHz,先用低速调通再提速
Clock Polarity (CPOL)Low对应 Mode 0
Clock Phase (CPHA)1 Edge对应 Mode 0
CRC CalculationDisabled一般用不上,开了反而增加开销

Prescaler 这一项,调试阶段我强烈建议先降速。9MHz 甚至 4.5MHz 先跑通,确认数据对了,再一步步往上提到 18MHz。如果一上来就 18MHz 跑不通,你根本分不清是协议问题还是信号完整性问题。

接着配 DMA。在 DMA Settings 里点 Add,选 SPI1_RX,Mode 选 Normal(单次)或 Circular(循环,适合持续采集),Data Width 都选 Byte,Priority 选 High。然后在 NVIC Settings 里把对应的 DMA 通道中断打开。

最后配一个 GPIO 作为 CS,比如 PA4,设为 Output Push-Pull,初始电平 High(片选空闲时是高电平)。

3.2 DMA接收代码落地与数据错位的根因

CubeMX 生成的初始化代码不用动,我们只需要加几行自己的逻辑。先定义缓冲区和标志位:

#define SPI_RX_SIZE 64 uint8_t spi_rx_buf[SPI_RX_SIZE]; volatile uint8_t spi_rx_done = 0; #define CS_LOW() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define CS_HIGH() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

启动 DMA 接收:

void SPI_Start_DMA_Read(void) { spi_rx_done = 0; CS_LOW(); HAL_SPI_Receive_DMA(&hspi1, spi_rx_buf, SPI_RX_SIZE); }

接收完成回调:

void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { CS_HIGH(); /* 必须在数据收完之后再抬 CS */ spi_rx_done = 1; } }

如果你要读某个寄存器,标准做法是发两个字节(命令 + 空字节),用全双工收发:

uint8_t Read_Reg(uint8_t reg) { uint8_t tx[2] = { reg | 0x80, 0x00 }; /* 读命令,最高位置1 */ uint8_t rx[2] = { 0 }; CS_LOW(); HAL_SPI_TransmitReceive(&hspi1, tx, rx, 2, 100); CS_HIGH(); return rx[1]; }

这里面第一个大坑是CS 拉高的时机。用HAL_SPI_TransmitReceive时,函数返回说明两个字节都收发完了,此时抬 CS 没问题。但用 DMA 时,函数是立即返回的,如果你在启动 DMA 后马上抬 CS,从机会认为传输结束,后面收的数据全是垃圾。必须在RxCpltCallback里抬 CS,这是数据错位最常见的根因。

第二个坑是DMA 传输长度和实际数据长度不匹配。有些器件读命令发完之后,还得等一小段时间才有数据出来,这时候需要中间插入延时,或者用两个独立的HAL_SPI_TransmitHAL_SPI_Receive分开处理。

第三个坑是SPI 的 RX 和 TX 是同一个时钟驱动。即使你只想读数据,也得给时钟,而给时钟就会同时发送数据。所以"只读"的场景必须给 TX 送空字节(0x00 或 0xFF,看器件要求),不能啥都不发。

3.3 硬件片选和软件片选怎么选

CubeMX 里 Hardware NSS Signal 有两个选项:Disable(软件控制)和 Hardware NSS Output(硬件自动控制)。

软件片选是我 90% 场景下的选择。原因很简单:可控。你可以精确控制 CS 拉低到第一个时钟之间的时间、最后一个时钟到 CS 拉高之间的时间,而这两个时间在时序要求严格的器件(比如某些 Flash 要求 tSLCH、tCHSH 有最小值)里非常关键。另外多从机场景下,软件片选能让你灵活切换。

硬件片选适合严格的单主机单从机、速度极高、CPU 不想介入的场景。它的优点是 NSS 由硬件自动拉低拉高,时序精确到时钟周期,缺点是灵活性差,而且 STM32F1 的硬件 NSS 在某些配置下会有奇怪的行为(比如需要配置 SSM/SSI 位,否则会误进从机模式)。

经验:如果发现 SPI 明明配了主机模式,时钟却不输出,先看 NSS 的配置。硬件 NSS 模式下,如果 NSS 引脚被拉低,SPI 会认为有别的设备在选中自己,于是退出主机模式,SCLK 停止输出。这是个非常隐蔽的坑,多数人第一次遇到会以为是 SPI 外设坏了。

4. UART链路打通:从驱动安装到调试日志落盘

UART 是调试阶段用得最多的接口。板子一出问题,第一反应就是接个串口看它有没有在跑。但这条链路看着简单,坑一点不少。

4.1 USB转串口芯片的驱动坑

板子上的 UART 是 TTL 电平(3.3V),电脑上是 USB,中间要有个桥接芯片。市面上常见的几款:

芯片特点常见问题
CH340便宜,国产,兼容性好老系统上驱动签名问题,部分版本连不上
CP2102稳定,原厂驱动完善假货多,便宜的模块可能是翻新片
CP2104支持更低功耗,带 GPIO驱动和 CP2102 不通用,别装错
FT232R老牌,稳定性好驱动要认准原厂,第三方驱动常出问题

装驱动的顺序很关键:先装驱动,再插模块。反过来的话,系统会先按默认的未知设备处理,之后即使装了驱动也可能要手动在设备管理器里更新。装完之后在设备管理器里看端口号,正常应该显示 "USB-SERIAL CH340 (COMx)" 或者 "Silicon Labs CP210x USB to UART Bridge (COMx)"。

如果设备管理器里出现黄色感叹号,八成是驱动没匹配上。这时候别急着换模块,先卸载设备(勾选"删除驱动程序软件"),拔掉模块,重装驱动,再插上。Windows 的驱动缓存很顽固,不彻底删干净会一直用错的那个。

4.2 串口助手参数与乱码排查顺序

串口调试助手的参数必须和固件完全一致:波特率、数据位(通常 8)、停止位(通常 1)、校验位(通常 None)、流控(通常 None)。这五项里有任何一项不一致,收到的都是乱码。

乱码排查我有个固定顺序,从高概率到低概率:

  1. 核对波特率。90% 的乱码是波特率不对。先在助手和固件两边都确认一遍数值。
  2. 算一遍波特率误差。按 2.2 节的公式算,超过 3% 就是配置问题,换晶振或降速。
  3. 看 TX/RX 是不是接反了。板子 TX 接模块 RX,板子 RX 接模块 TX,交叉接。接反的表现通常是完全收不到数据,而不是乱码。
  4. 确认共地。USB 转串口模块和板子必须共地,不共地的时候电平参考不一样,收到的数据是随机的。这是新手最容易忽略的一条。
  5. 看电平是否匹配。3.3V 的板子接 5V 的模块,或者反过来,可能收不到或者烧芯片。用万用表量一下空闲时 TX 的电平,正常应该和供电电压一致。

如果收到的数据是"能看懂一部分,中间夹杂乱码",那多半是波特率接近但不完全一致,或者有电源干扰。这种情况把波特率降一档通常能解决。

4.3 调试信息同时打印和存盘的做法

调试的时候经常遇到这种情况:屏幕上刷得飞快,问题一闪而过,想回看却没了。所以"同时打印到终端和写入日志文件"是个刚需。

最省事的办法是给调试串口加一层封装。在固件里定义:

#include <stdarg.h> #include <stdio.h> void log_printf(const char *fmt, ...) { char buf[256]; va_list ap; va_start(ap, fmt); int n = vsnprintf(buf, sizeof(buf), fmt, ap); va_end(ap); if (n > 0) { HAL_UART_Transmit(&huart1, (uint8_t *)buf, n, 100); } }

所有调试输出都走log_printf,这样做的好处是后面想加时间戳、加等级(INFO/WARN/ERR)、加模块名,都只改一个函数。

电脑这一侧,用串口助手自带的"保存日志"功能也行,但更灵活的是写个几行的 Python 脚本,同时打印和写文件:

import serial import datetime port = 'COM7' baud = 115200 logname = 'uart_%s.log' % datetime.datetime.now().strftime('%Y%m%d_%H%M%S') ser = serial.Serial(port, baud, timeout=1) f = open(logname, 'a', encoding='utf-8', errors='replace') print('logging to', logname) while True: data = ser.read(128) if not data: continue text = data.decode('utf-8', errors='replace') print(text, end='') f.write(text) f.flush()

f.flush()这一句很关键,不加的话 Python 会攒一批才写盘,程序崩了日志就丢了。加上之后每收到一段就立刻落盘,断电都不怕。

提示:在 Keil 里调试的时候,如果你想在 Watch 窗口看到结构体变量的实时值,记得把编译优化等级设成 -O0 或者 -Og。开 -O2 之后局部变量可能被优化进寄存器,Watch 窗口显示 "cannot read memory",会让人误以为程序跑飞了。另外涉及中断里改、主循环里读的变量,一定要加volatile,否则优化后主循环可能永远读到缓存里的旧值。

5. I²C总线疑难:上拉计算、地址冲突与总线死锁

I²C 是所有接口里"看起来最简单、出问题最缠人"的一个。两根线一接,代码一调,运气好跑通,运气不好就是无尽的卡死。

5.1 上拉电阻不是随手焊一个4.7k

很多人画 I²C 电路,上拉电阻直接从参考设计里抄个 4.7k。这个值在多数场景下没问题,但它是算出来的,不是拍脑袋定的。

上拉电阻有两个约束,一个是下限(不能太小),一个是上限(不能太大)。

下限由灌电流决定。开漏输出的器件把线拉低时,电流从上拉电阻灌进芯片。这个电流不能超过器件的 IOL 能力(一般 3mA)。公式:

Rmin = (VDD - VOL_max) / IOL_max

3.3V 供电,VOL 取 0.4V,IOL 取 3mA:

Rmin = (3.3 - 0.4) / 0.003 ≈ 967Ω

所以上拉电阻不能小于约 1kΩ。

上限由上升时间决定。I²C 标准规定标准模式(100kHz)上升时间不超过 1000ns,快速模式(400kHz)不超过 300ns。上升时间公式:

tr ≈ 0.847 × R × C

其中 C 是总线总电容,包括引脚电容、走线电容、器件电容,官方建议不超过 400pF。快速模式下:

R ≤ 300ns / (0.847 × 400pF) ≈ 885Ω

也就是说,如果总线电容真的到了 400pF 上限,快速模式的上拉电阻得小于 900Ω,4.7k 根本不够。实际项目中总线电容通常只有几十 pF,比如 100pF:

R ≤ 300ns / (0.847 × 100pF) ≈ 3542Ω

这时候 4.7k 就超了,得用 2.2k 或 3.3k。所以结论是:跑 400kHz 时,4.7k 只有在总线电容很小(小于约 75pF)的情况下才安全。板子上一堆器件挂上去,很容易就超了。

我自己的做法是:100kHz 用 4.7k 没问题;400kHz 起步用 2.2k,走线长或者挂的设备多就用 1.5k,同时用示波器量上升沿,确保在 300ns 以内。

5.2 总线被拉死之后的恢复手法

I²C 最经典的故障是总线死锁:某个从机在传输过程中被打断(比如主机复位了),它还在等下一个时钟,于是死死把 SDA 拉低不放。这时候主机重新初始化 I²C,发现 SDA 一直是低,连起始条件都发不出去,整个总线就废了。

标准恢复手法是手动发 9 个时钟脉冲,让从机把剩下的位发完,然后补一个 STOP 条件。代码可以这么写:

void I2C_Bus_Recover(void) { GPIO_InitTypeDef g = {0}; /* 先把 I2C 外设停掉,引脚切成开漏输出 */ HAL_I2C_DeInit(&hi2c1); g.Pin = GPIO_PIN_6 | GPIO_PIN_7; /* SCL=PB6, SDA=PB7 */ g.Mode = GPIO_MODE_OUTPUT_OD; g.Pull = GPIO_PULLUP; g.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, &g); /* 先确保 SDA 释放 */ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); /* 打 9 个时钟 */ for (int i = 0; i < 9; i++) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); } /* 手动产生 STOP: SCL 高时 SDA 由低变高 */ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); HAL_Delay(1); /* 重新初始化 I2C 外设 */ HAL_I2C_Init(&hi2c1); }

这段代码我基本每个用硬件 I²C 的项目都会加上,放在HAL_I2C_ErrorCallback里或者上电初始化时先跑一遍,能省掉大量"莫名其妙就不通信了"的现场问题。

另外,找设备地址的时候别一个个猜。写个扫描循环,从 0x01 扫到 0x7F,哪个地址有 ACK 就是有设备:

void I2C_Scan(void) { for (uint8_t addr = 1; addr < 0x80; addr++) { if (HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 2, 10) == HAL_OK) { log_printf("I2C found: 0x%02X\r\n", addr); } } }

扫描一遍,两秒钟就知道总线上挂了谁、地址是多少。地址冲突(两个器件地址一样)也会在这里暴露出来——表现为扫描到的设备数量比实际挂的少,这时候就得靠器件上的地址选择引脚(A0/A1/A2)来改地址,或者用 I²C 多路复用器分总线。

6. 问题速查表与几条用血换来的经验

6.1 高频问题速查表

现象最可能的原因排查动作
SPI 完全没有波形主机模式没进、NSS 配置错查 NSS 配置,切软件片选
SPI 数据整体错位一位CPHA 配反换 CPHA 重试
SPI 数据整体反相CPOL 配反换 CPOL 重试
SPI 数据比特反转First Bit 顺序错切 LSB First
SPI DMA 收数据全乱CS 抬早于传输完成回调里再抬 CS
UART 全是乱码波特率不匹配或误差超 3%核对参数、算误差
UART 完全收不到TX/RX 接反或没共地交叉接线、检查地线
UART 偶发错帧电源干扰或线太长降速、缩短线、加滤波
I²C 扫描不到设备没上拉、供电没上量 SDA/SCL 空闲电平
I²C 通信一会儿就卡死总线死锁、时钟拉伸跑 9 时钟恢复程序
I²C 高速下偶发 NACK上拉太大、上升沿太慢减小上拉、示波器量上升时间

6.2 几个反常识的经验

第一条,SPI 不是越快越好。我刚开始做项目的时候,觉得 18MHz 能配就配 18MHz,结果接一个便宜的传感器,数据时对时错。后来降到 4.5MHz 一切正常,查手册发现那个器件的最高时钟只有 10MHz,而且对上升沿有要求。速度提上去之前,先把手册的时序参数表翻出来对照一遍,别想当然。信号完整性也是同理,长走线上 18MHz 的方波可能已经变成一条斜线了,用示波器看一眼就知道。

第二条,I²C 的硬件外设不一定比软件模拟靠谱。STM32F1 的硬件 I²C 在某些从机上有已知的卡死问题(错误标志位卡住、总线状态机跑飞),很多人最后改用 GPIO 模拟时序,反而稳定。这不是说硬件外设不好,而是说当你在硬件 I²C 上折腾超过半天还没进展时,及时切换到软件模拟是个理性的选择,别死磕。

第三条,调试信息永远要留后路。板子上只留一个 UART 口,固件里只留一个 printf,一旦 UART 挂了就完全瞎眼。我的习惯是至少留两条调试通道,一条串口打日志,一条用 GPIO 打时间戳(关键节点翻转电平,示波器一抓就知道时序),两条互补,成本几乎为零。

第四条,"换个芯片试试"应该是最后的手段。接口调不通,95% 是配置问题、时序问题、硬件连接问题,只有 5% 是真的器件坏了。我见过太多人换了三片芯片,最后发现是 CPOL 配错。调试的正确顺序永远是:先算参数、再看波形、再查连接、最后才怀疑器件。

最后分享一个我一直在用的小习惯:每调通一个接口,就把这次的配置参数(CPOL/CPHA、时钟分频、上拉阻值、波特率、地址)记在一个自己的清单里,注明器件型号和日期。下次遇到同类器件,直接查清单,五分钟搞定,比重新翻手册快得多。这份清单攒两三年之后,价值比任何一本书都高,因为它全是你在真实板子上验证过的参数。

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

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

立即咨询