SPI和SCI这两个外设,几乎每颗控制器上都会带,但真正能把它们配到“稳定跑满速、连续跑几天不丢一个字节”的,比例并不高。我见过太多项目卡在“示波器上有波形、数据就是不对”这一步,最后复盘发现是 CPHA 配反了,或者片选拉高的时机早了半个字节。SPI 与 SCI 的寄存器配置、时序理解、排障经验,这三样东西凑齐了才算真正会用,缺一样,你就是在赌运气。
这篇内容我想把这两条通信链路从头到尾拆一遍:寄存器里每个位为什么这么设、时序图上每条边沿对应哪次采样、波特率分频怎么算误差、出问题拿什么顺序去查。适合刚接手驱动移植的新人照着抄配置,也适合已经写过几版驱动但总在偶发丢包上打转的朋友对一遍思路。所有寄存器命名我会用通用的缩写形式,各家平台的叫法不一样,但位域含义大差不差,你对着自己那本手册能一一对上。
1. 先把这两个外设摆到台面上:它们到底差在哪
1.1 四根线与两根线,本质上是两种同步哲学
SPI 的物理层是 SCK、MOSI、MISO、CS 四根线(单向通信可以省掉一根数据线),全双工、同步、主从结构。它的核心特征是有一根独立的时钟线,收发双方都跟着这个时钟节拍走,数据在哪个边沿变化、哪个边沿被采样,由 CPOL 和 CPHA 两个位约定。
SCI 的物理层只有 TX、RX 两根线,异步、点对点(多机场景靠地址位区分)。它没有时钟线,双方靠事先约定的波特率各自计时,靠每一帧开头的起始位重新对齐相位。
这个差别决定了两个外设的“脆弱点”完全不一样。SPI 不怕时钟不准,因为采样时刻由硬件时钟边沿决定,只要 SCK 频率不超过从设备上限、走线延迟不至于让数据和时钟错位,速率可以拉得很高。SCI 恰恰相反,它对时钟精度极其敏感,因为接收方是在每一位的中间去采样,一旦双方波特率有偏差,误差会随着帧内位数累积,采到第 8 位时可能已经偏到相邻位去了。
打个生活化的比方:SPI 像是两个人跟着同一个鼓点传纸条,鼓点快慢无所谓,只要两个人听的是同一个鼓;SCI 像是两个人各自看表,约好每 5 秒递一次纸条,表要是不准,递到第十次就错位了。
1.2 选型判断:什么场景必须上 SPI,什么场景串口就够
我一般按这几个维度判断:
| 维度 | SPI | SCI |
|---|---|---|
| 典型速率 | 1 MHz ~ 50 MHz | 9600 ~ 921600 bps,高配可到几 Mbps |
| 线数 | 4 根(多从机再加 CS) | 2 根(加流控 4 根) |
| 拓扑 | 一主多从,靠 CS 选择 | 点对点为主,多机靠地址 |
| 时钟要求 | 从设备只接收时钟,无精度要求 | 双方时钟误差累计不能超半个位宽 |
| 应答机制 | 无,发出去就当成功 | 无硬件应答,但起始位天然帧同步 |
| 适合距离 | 板内,几厘米到几十厘米 | 板内到几十米(加收发器) |
| 典型外设 | Flash、传感器、显示屏、ADC、无线模块 | 上位机通信、调试口、GPS、蓝牙模块 |
有个点很多人忽略:SPI 没有应答机制。主机把数据推到移位寄存器、时钟打完,硬件就认为发送完成了,但从设备到底收没收到、CRC 对不对,SPI 硬件层面完全不知道。所以用 SPI 驱动 Flash 做写入时,必须靠“读状态寄存器”这种协议层手段确认,不能靠“我发完了”就认为成功。串口虽然有起始位做帧同步,但同样没有确认机制,工程上通常靠应用层加校验和与重传。
判断逻辑很简单:需要高吞吐、板内、外设本身就带 SPI 接口的,直接上 SPI;需要跨板、跨机箱、要和 PC 或模块通信、线束成本敏感的,用 SCI。两者都要用的时候,注意别把 SPI 的高频时钟线布到串口线旁边,串扰会让串口偶发帧错误——这个坑我在后面第 5 章还会展开。
2. SPI 寄存器配置:把时序参数翻译成寄存器值
2.1 控制寄存器里的六个关键位
SPI 的控制寄存器看起来位很多,真正决定通信能不能跑通的就那么几个:
- 主从选择位(MSTR):置 1 为主机。这一位配错是最常见的“完全没波形”原因之一,从机模式下 SCK 是输入,你等它输出时钟永远等不到。
- 时钟极性(CPOL)与时钟相位(CPHA):决定四种工作模式,下一章专门讲。
- 数据位序(LSBFIRST):先发最低位还是最高位。绝大多数器件是 MSB first,但个别传感器和显示驱动是 LSB first,配错了数据会整体位序颠倒,看起来像“数据全错但长度对”。
- 数据宽度(DS):4 位到 16 位可配。8 位最常用,但有些 ADC 是 12 位、有些显示屏命令是 9 位,宽度配错会导致多打或少打时钟沿。
- 波特率分频(BR):下一节细算。
- 片选管理模式(SSM/SSOE):决定 CS 由硬件自动产生还是软件用 GPIO 控制。
关于片选,我的习惯是一律用 GPIO 做软件片选,除非平台有硬性限制。原因有三:硬件片选在多从机场景下时序不好控,某些平台主机模式下 NSS 被外部拉低会触发模式错误(MODF);软件片选可以在拉低之后插入可控延时,应对从设备建立时间偏长的情况;还有一点很实用,软件片选可以同时拉低多个 CS 做广播式写入,某些显示驱动和多路 DAC 就吃这一套。
代价是要自己管理时序:拉低 CS、发数据、等发送真正完成、拉高 CS,这四步一步都不能省,尤其是第三步,后面会讲为什么。
2.2 波特率分频:算清楚你能取到哪个值
SPI 的波特率公式,通用形式是:
f_sck = f_pclk / DIVDIV 在不同平台上的取值规则差别很大:有的是 2 的幂(2、4、8、16、32、64、128、256),有的是 2 的整数倍(2、4、6、8……),还有的是任意整数分频。这个限制直接决定了你能跑多快。
举个例子,总线时钟 72 MHz,目标 SCK 9 MHz:72 / 9 = 8,正好是 2 的幂,DIV 取 8,实际速率 9 MHz,误差 0。
再比如总线时钟 100 MHz,目标 10 MHz:理论上 DIV = 10,但如果这颗芯片只支持 2 的幂分频,你只能取 8 或 16。取 8 得到 12.5 MHz,超过从设备上限;取 16 得到 6.25 MHz,性能打了六折。这种情况下要么换主频配置,要么接受 6.25 MHz。
| 总线时钟 | 目标 SCK | 理论分频 | 可用分频 | 实际 SCK | 处理方式 |
|---|---|---|---|---|---|
| 72 MHz | 9 MHz | 8 | 8 | 9 MHz | 直接用 |
| 100 MHz | 10 MHz | 10 | 8 / 16 | 12.5 / 6.25 MHz | 降速到 6.25 MHz |
| 48 MHz | 12 MHz | 4 | 4 | 12 MHz | 直接用 |
| 80 MHz | 8 MHz | 10 | 8 | 10 MHz | 超速,改取 16 得 5 MHz |
有个经验必须说:SCK 不是越快越好。从设备手册标的最高频率是在理想条件下测的,实际板子上有电平转换芯片、有排线、有多个从设备并联在总线上,信号边沿会变缓。我一般的做法是先用目标速率的 1/4 跑通功能,再逐步提上去,每提一档都做一次长时间收发测试。直接上手满速,出了问题你分不清是配置错还是信号完整性问题,排查成本翻倍。
还有个细节:示波器量 SCK 频率时,要在 CS 有效的窗口内量,而且注意探头电容对高频的影响。低速时量出来很准,20 MHz 以上用普通探头量出来的波形可能有明显过冲和振铃,那是探头的问题,不一定是板子的问题。
2.3 状态寄存器的轮询逻辑:别被 TXE 骗了
SPI 状态寄存器里常见的几个标志:
- TXE(发送缓冲空):数据从发送缓冲搬到了移位寄存器,缓冲可以装下一个字节了。
- RXNE(接收缓冲非空):收到了一个完整字节,可以读走了。
- BSY(总线忙):移位寄存器还有数据在移,或者 CS 还在有效状态。
- OVR(溢出):新数据来了但旧数据还没读走,数据丢了。
- MODF(模式错误):多主冲突或者 NSS 被意外拉低。
新手最容易踩的坑是:只等 TXE 就认为发送完成,然后立刻拉高 CS。TXE 置位只说明发送缓冲空了,最后一个字节还在移位寄存器里一位一位往外移呢。这时候拉高 CS,从设备会认为帧提前结束,最后一个字节丢一半。
正确的做法是等 BSY 清零,或者等专门的总线空闲标志。伪代码大概是这样:
/* 发送一个字节并等待真正完成 */ SPI_DR = byte; while (!(SPI_SR & SPI_SR_TXE)) { } /* 缓冲可写,可以送下一个字节 */ /* 全部发完后,等总线真正空闲 */ while (SPI_SR & SPI_SR_BSY) { } GPIO_CS_HIGH(); /* 此时才允许拉高片选 */连续发多个字节时,前 n-1 个字节只等 TXE 就够了,最后一个字节必须等 BSY。这个区别在高速通信时特别明显,因为移位寄存器的最后 8 个时钟周期可能只有几百纳秒,逻辑上稍不注意就漏了。
OVR 标志的清除也有讲究。有些平台的 OVR 需要“先读状态寄存器、再读数据寄存器”这个特定序列才能清掉,只读数据清不掉,会导致后续所有接收都被卡住。这个细节各家手册里都写了,但藏在状态位的描述段落里,很容易漏。
中断和 DMA 场景下同样的道理:DMA 传输完成中断只代表数据都写进了发送缓冲,不代表最后一个字节已经移出。我通常会在 DMA 完成中断里先等 BSY 清零,再拉高 CS,或者在 DMA 配置里用“传输完成 + 总线空闲”双条件触发。
3. SPI 时序详解:四种模式到底怎么对上
3.1 CPOL 与 CPHA 的组合表,记住规律比背表强
CPOL 决定空闲时时钟是高还是低,CPHA 决定在第一个边沿还是第二个边沿采样。
| 模式 | CPOL | CPHA | 空闲电平 | 采样沿 | 数据变化沿 |
|---|---|---|---|---|---|
| 模式 0 | 0 | 0 | 低 | 第 1 个沿(上升) | 下降沿 |
| 模式 1 | 0 | 1 | 低 | 第 2 个沿(下降) | 上升沿 |
| 模式 2 | 1 | 0 | 高 | 第 1 个沿(下降) | 上升沿 |
| 模式 3 | 1 | 1 | 高 | 第 2 个沿(上升) | 下降沿 |
记忆方法:CPHA = 0 时,第一个边沿就采样,数据在第一个边沿之前就已经准备好了;CPHA = 1 时,第一个边沿是数据变化沿,第二个边沿才采样。换句话说,CPHA 决定了“先有数据还是先有时钟沿”。
模式 0 和模式 3 是最常用的两种,因为它们在“数据在采样沿之前已经稳定”这一点上都做得很自然。工程上有个很实用的经验:如果模式 0 读出来全是 0xFF 或 0x00,先别急着查硬件,换成模式 3 试一次。很多器件手册写的是模式 0,但在实际布线上因为时钟上升沿较缓,模式 3 反而更稳。反过来也一样,模式 3 不通就试模式 0。这两者互换能救活的场景,我遇到过不止一次。
模式 1 和模式 2 用得少,但不是没有,某些老式 ADC 和特定封装的存储器件就是这两种,别默认只有 0 和 3。
3.2 从设备手册里快速定位模式
拿到一份陌生的器件手册,怎么在半分钟内确定该用哪个模式?我的顺序是:
- 翻到“时序特性”或“SPI 接口时序”那几页,找 SCK 的波形图。
- 看 SCK 在 CS 无效时是高电平还是低电平。高就是 CPOL = 1,低就是 CPOL = 0。
- 看数据线(MOSI/MISO)上的数据是在 SCK 的哪个边沿被画成“稳定区间”。如果数据在第一个边沿就已经稳定、第二个边沿之后才变化,那第一个边沿是采样沿,CPHA = 0。
- 如果手册直接写了“Mode 0”或“SPI Mode 3”,以手册为准,波形图作为交叉验证。
有个坑值得单独提:有些手册写的“时钟极性”是从从设备角度看 SCK 的,和主设备寄存器的 CPOL 定义可能反着来。我碰到过一次,手册写“SCK idle high”,实际要配 CPOL = 0 才通,因为那颗器件的 SCK 引脚内部有反相器。这种情况下波形图比文字描述可靠,实在对不上就四种模式轮一遍,反正是试出来的。
3.3 片选、时钟、数据的三角关系:建立与保持时间
四种模式配对只是第一步,真正让通信“偶发失败”的往往是片选时序。
关键参数有这么几个:
- tCSS(CS 建立时间):CS 拉低到第一个 SCK 沿之间的最小间隔。
- tCSH(CS 保持时间):最后一个 SCK 沿到 CS 拉高之间的最小间隔。
- tCS(CS 最小高电平时间):两次独立访问之间 CS 必须拉高多久。
- tSU / tH(数据建立/保持时间):数据相对采样沿的稳定窗口。
大多数情况下主设备的硬件时序是够的,但有两个场景会出问题:
第一个是首字节。CS 刚拉低,从设备内部可能还在做上电复位、唤醒或者从低功耗模式恢复,需要几十微秒甚至几毫秒才能响应。这时候直接发数据,首字节必丢。解决办法是 CS 拉低后插一段软件延时,或者先发一个无效字节把时钟“热身”一下。
第二个是末字节。最后一个字节的最后一个时钟沿之后,从设备可能还需要时间把接收到的数据锁存到内部寄存器,或者把要返回的数据推到 MISO 上。如果 CS 立刻拉高,从设备可能认为帧不完整。我的做法是最后一个字节发完后,等 BSY 清零,再额外插 1~2 个微秒延时,然后才拉高 CS。这个延时在多从机、长走线的情况下尤其重要。
/* 一次完整访问的时序骨架 */ GPIO_CS_LOW(); delay_us(2); /* 满足 tCSS,给从设备准备时间 */ for (i = 0; i < len; i++) { SPI_DR = buf[i]; while (!(SPI_SR & SPI_SR_TXE)) { } while (SPI_SR & SPI_SR_RXNE) { rbuf[i] = SPI_DR; } /* 全双工,读回数据 */ } while (SPI_SR & SPI_SR_BSY) { } /* 等最后一个字节移完 */ delay_us(1); /* 满足 tCSH */ GPIO_CS_HIGH();注意上面代码里读 RXNE 的位置:SPI 是全双工的,你每发一个字节,同时就会收到一个字节。哪怕这个字节是垃圾数据,也必须读走,否则接收缓冲满了会置 OVR,后续通信就乱了。这是新手写 SPI 驱动时最常犯的错误之一——只发不收,跑几十个字节之后突然全部失败。
4. SCI 配置:看似简单的串口,寄存器里全是坑
4.1 波特率分频与误差:这是串口通信最大的雷区
SCI 的波特率公式,过采样 16 倍时是:
baud = f_pclk / (16 × DIV)DIV 可以是整数,也可以带小数(很多平台支持 4 位小数,即 1/16 的精度)。问题在于,不是所有平台都支持小数分频,早期的控制器往往只有整数分频。
举个经典例子:主频 8 MHz,目标 115200。
DIV = 8,000,000 / (16 × 115200) = 4.34如果分频器只支持整数,取 4,实际波特率 = 8,000,000 / (16 × 4) = 125000,误差 +8.5%。这已经远远超过异步串口能容忍的范围,接上去就是满屏乱码。
换成 11.0592 MHz:
DIV = 11,059,200 / (16 × 115200) = 6.00正好整除,误差 0。这就是为什么很多老方案坚持用 11.0592 MHz 这种“奇怪”的晶振频率——它是为了方便串口分频而选的,能被常见的波特率整除。
| 主频 | 目标波特率 | 理论分频 | 实取分频 | 实际波特率 | 误差 |
|---|---|---|---|---|---|
| 8 MHz | 115200 | 4.34 | 4 | 125000 | +8.5% |
| 8 MHz | 9600 | 52.08 | 52 | 9615 | +0.16% |
| 11.0592 MHz | 115200 | 6.00 | 6 | 115200 | 0 |
| 12 MHz | 115200 | 6.51 | 6.5 | 115385 | +0.16% |
| 16 MHz | 115200 | 8.68 | 8.6875 | 115108 | -0.08% |
误差能容忍多少?一帧 10 位(1 起始 + 8 数据 + 1 停止),接收方在每一位的中间采样,理论容限是累计偏差不超过半个位宽,也就是 5%。但这是理论极限,实际要留噪声余量、要算上双方晶振各自的偏差(比如两颗 ±1% 的晶振,最坏叠加 2%),工程上我一般把单边误差控制在 2% 以内,双边合计不超过 4%。
还有个容易忽略的点:高波特率下分频值变小,同样的绝对误差占比更大。11.0592 MHz 跑 921600,理论分频 = 11,059,200 / (16 × 921600) = 0.75,小于 1,过采样 16 根本做不到。这时候要么提高主频,要么切到过采样 8 倍模式(分频 1.5,勉强可行),要么降波特率。所以做方案选型时,主频和最高波特率要一起算,别等驱动写完才发现跑不上去。
4.2 帧格式、校验与硬件流控
帧格式的几个要素:
- 数据位:8 位最通用,9 位常用于多机通信的地址帧区分,或者带校验的私有协议。
- 停止位:1 位够用,2 位主要用于和老设备兼容,或者长线传输时给接收方多一点处理时间。
- 校验位:奇校验、偶校验、无校验。注意硬件校验只能查单比特错误,多比特错误查不出来,关键数据还是要应用层加 CRC。
硬件流控(RTS/CTS)什么时候必须开?我的判断标准是:对端处理速度不确定、接收缓冲浅、波特率超过 460800,三者占一个就建议开。没有流控的情况下,对端来不及处理,数据就丢了,而且丢得毫无征兆——你不知道是丢了中间一段还是整个尾巴。开了流控,发送方在 CTS 无效时会自动暂停,硬件层面就解决了背压问题。
代价是多两根线,而且有些模块的 RTS/CTS 是复用引脚,要重新配置。
4.3 中断、FIFO 与 DMA:不定长帧怎么收才不乱
串口接收最麻烦的不是定长帧,是不定长帧。你根本不知道对方什么时候发完,用“收够 N 个字节”的逻辑必然出错。
三种常见做法:
第一种是空闲中断(IDLE)。串口在接收完一帧后,如果总线空闲超过一个字符时间,硬件会置 IDLE 标志。配合 DMA 使用,流程是:DMA 一直往缓冲区搬数据,IDLE 中断触发时读一下 DMA 剩余计数,就能算出这一帧收了多少字节,处理完重新配置 DMA。这是目前最优雅的方案,CPU 占用极低。
第二种是定时器超时。平台没有 IDLE 中断的话,每收到一个字节就重置一个定时器,定时器溢出说明这一帧结束了。缺点是每个字节都要进中断,高波特率下 CPU 负担很重。
第三种是协议层定长或带长度字段。这是最省事的,但要求你能改协议。
FIFO 门限的设置也有讲究。接收 FIFO 一般可以设 1/4、1/2、3/4 触发中断。设太低(比如 1 字节)中断太频繁;设太高,最后一帧不满门限的数据会一直留在 FIFO 里,要等到超时才能取出来。我一般设 1/2,再配一个字符超时中断兜底。
/* DMA + 空闲中断收不定长帧的核心逻辑 */ void USART_IRQHandler(void) { if (USART_SR & USART_SR_IDLE) { (void)USART_DR; /* 先读 DR 清 IDLE,顺序不能反 */ uint16_t recv_len = BUF_SIZE - DMA_GetRemain(DMA_CH); RingBuffer_Push(rx_buf, recv_len); DMA_Disable(DMA_CH); DMA_SetCount(DMA_CH, BUF_SIZE); /* 重新装填 */ DMA_Enable(DMA_CH); } }这段代码里有个细节:清 IDLE 标志必须先读状态寄存器再读数据寄存器,顺序反了清不掉,中断会一直进,直接把 CPU 拖死。这是我在实际项目里栽过的坑,症状是“一上电就死机”,查了半天才发现是中断服务函数写错了顺序。
5. 实战避坑:那些只在示波器上才看得见的问题
5.1 常见故障速查表
| 现象 | 最可能的原因 | 排查动作 |
|---|---|---|
| 完全没有 SCK 波形 | 主从位配反、外设时钟未使能、引脚复用没配 | 先查引脚复用寄存器,再看 MSTR 位 |
| 有 SCK 无数据 | MOSI 引脚方向配错、数据寄存器没写 | 量 MOSI 静态电平,看发送缓冲是否有值 |
| 数据整体位序颠倒 | LSBFIRST 配错 | 改位序位重试 |
| 收到的全是 0x00 或 0xFF | 模式不匹配、MISO 悬空、从设备未选中 | 换模式 0/3 试,量 MISO 空闲电平 |
| 首字节正确后续全错 | 接收缓冲未读导致 OVR | 检查每次发送后是否读 DR |
| 偶发丢字节,间隔无规律 | 中断被更高优先级抢占、无流控 | 提高中断优先级,开流控或降速 |
| 低速正常,高速出错 | 信号完整性、走线过长、无源探头负载 | 降速验证,换短线上示波器看边沿 |
| 上电后首次通信失败,之后正常 | 从设备复位未完成 | CS 拉低后加延时 |
| 串口满屏乱码 | 波特率误差超限、时钟源不准 | 算分频误差,换晶振或用内部高精度时钟 |
5.2 硬件层的坑:地回流、上拉和电平不匹配
软件配得再对,硬件有问题一样白搭。这几个是我踩过最多次的:
MISO 悬空。SPI 是多从机共享 MISO 的,从设备未被选中时 MISO 应该是高阻态。如果总线上没有任何上拉,主设备在读的时候会读到浮空电平,表现为随机数据。解决办法很简单:MISO 加一个 10 kΩ 上拉到主设备电源,或者在软件上只在不关心返回值的命令里忽略 MISO。
地回流路径太长。两块板子用排线连 SPI,信号线走得漂亮,地线只给了一根细的。结果就是高速下偶发错误,降速就好。原因是对比单端信号,返回电流必须有一条低阻抗路径,地线太细太长,回流路径阻抗高,信号参考电平就漂了。解决办法是排线里多给几根地线,最好信号线和地线交错排列。串口跑长线也有这个问题,RX 偶发帧错误、校验错,优先查地。
电平不匹配。3.3 V 主设备直连 5 V 从设备,短时间可能“能用”,但长期跑会有隐患:主设备的输入引脚承受 5 V,虽然很多引脚标称耐压 5 V,但那是有限条件的。反过来 5 V 主设备连 3.3 V 从设备,从设备可能识别不了高电平。这种场景老老实实加电平转换芯片,别省这两块钱。
CS 线串扰导致误选中。多从机场景下,CS 线和 SCK 线平行走很长一段,SCK 跳变时会耦合到 CS 上,产生毛刺。如果从设备对 CS 毛刺敏感(有些器件会把毛刺当成一次帧起始),就会出现“莫名其妙被唤醒”的问题。解决办法是 CS 线加 RC 滤波,或者拉开与 SCK 的间距,中间走地线隔离。
5.3 上电时序与从设备复位
这个坑很隐蔽:主设备先上电,从设备后上电,或者两者同时上电但从设备复位时间更长。主设备这时候往从设备发数据,从设备还在复位状态,引脚可能是高阻或输出低,两者打架。
更麻烦的是,主设备的 MOSI/CS 输出高电平,会通过从设备的输入保护二极管灌电流到从设备的电源轨上,导致从设备“半上电”状态,行为完全不可预测。
处理办法:在主设备的初始化流程里,从设备上电后延时 10~100 ms 再开始通信;如果有从设备的状态引脚(比如 READY 或 INT),先等它拉高;软件上做一次“通信自检”,读从设备的 ID 寄存器,读不到就重试,别硬发。
串口这边同样有这个问题:某些模块上电后会先输出一段启动信息,如果你在它输出之前就发命令,命令会被淹没或者被当成配置数据。稳妥做法是先等一段静默,或者等到收到预期的启动帧再开始。
6. 调试手法:用最少的工具定位问题
6.1 逻辑分析仪抓包:参数设置比设备本身更重要
逻辑分析仪是排查 SPI/SCI 最有效的工具,但设置不对,看到的数据全是错的。几个关键参数:
采样率。至少要是 SCK 频率的 4 倍,实际建议 10 倍以上。SCK 跑 10 MHz,采样率至少 100 MSa/s。采样率不够,抓到的波形边沿位置不准,解码出来的数据会错位一位。
触发条件。SPI 用 CS 下降沿触发,串口用 RX 下降沿(起始位)触发。别用“任意边沿”,否则抓到的第一帧可能是半截。
解码器配置。SPI 解码器要设:位序(MSB/LSB)、时钟模式(0~3)、位宽(8/16)、CS 极性(低有效还是高有效)。这几项有一项设错,解码结果就是废的。我一般先用已知数据(比如读 ID 命令)比对,确认解码器配置对了,再去看真实的通信数据。
串口解码器要设波特率、数据位、校验、停止位,注意有些解码器支持“自动波特率检测”,但对不定长帧不友好,还是手填靠谱。
6.2 GPIO 翻转做时间戳:没有逻辑分析仪时的救命招
手上没有逻辑分析仪怎么办?拿一个空闲 GPIO,在关键代码位置翻转它,然后用示波器(哪怕是几十块钱的简易示波器)看这个 GPIO 的波形,就能量出代码执行时间、中断响应延迟、帧间隔。
比如想确认“CS 拉低到第一个时钟沿”到底隔了多久,就在 GPIO_CS_LOW() 后面立刻翻转一个调试引脚,示波器上量两个通道的时间差。这个手法不需要任何额外设备,一个 IO 加一台示波器就够,而且量出来的是真实执行时间,比看手册估算准得多。
我在调一个对时序要求很严的外设时,用这个办法发现编译器优化等级从 -O0 改到 -O2 之后,CS 拉低到发数据的间隔从 3 微秒缩到了 0.8 微秒,从设备的建立时间不够了。这种问题靠看代码根本发现不了。
6.3 环回自测:先证明外设本身是好的
怀疑外设配置有问题时,最快的验证是环回。
SPI 环回:把 MOSI 和 MISO 短接(有些平台还支持内部环回位,不用飞线),发什么应该收到什么。收到的不对,问题在配置;收到的对,问题在外设或走线。
串口环回:TX 短接 RX,自发自收。这一招能立刻区分“配置问题”和“外部设备问题”。
我习惯在驱动开发的第一个阶段就把环回测试代码写好,作为一个独立的测试函数。后面每次改配置,先跑一遍环回,确认底层没问题,再去接真实外设。这样排查范围能缩小一半。
6.4 分阶段降速法:从最保守的参数开始
最后分享一个排查策略:当你面对一个完全跑不通的通信链路,不要一上来就用目标参数试。按这个顺序往下退:
- 时钟降到最低档(SPI 用 100 kHz,串口用 9600)。
- 模式选最常见的(SPI 模式 0,串口 8N1)。
- 关掉所有中断和 DMA,用纯轮询。
- 单字节收发,确认一个字节对了再加长度。
- 逐项恢复:先提速率,再开中断,再上 DMA。
每改一项测一次,出问题立刻知道是哪一项引入的。这套流程看起来慢,实际比“一把梭配上去然后盲猜哪里错”快得多。我在带新人的时候反复强调这一点,因为大部分“调了两天没调通”的案例,复盘下来都是因为一开始就同时开了太多变量,把问题淹没在噪声里了。
我个人在实际项目里的体会是,SPI 和 SCI 这类外设的调试成本,八成花在“不确定是硬件还是软件”的内耗上。真正有效的做法不是把手册背熟,而是建立一套能快速二分定位的流程:先用环回证明外设本身没问题,再用低速证明时序逻辑没问题,再用逻辑分析仪证明波形没问题,最后才去怀疑从设备和走线。这个顺序反过来做,往往会在一堆变量里绕很久。另外提醒一句,凡是要长期运行的设备,SPI 的片选时序和串口的波特率误差这两项,一定要留足余量,别卡着手册的极限值设计,温度一变、批次一换,问题就冒出来了。