☰
SPI与SCI通信配置、时序与排障实战
2026/10/31 21:08:46 网站建设 项目流程

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,什么场景串口就够

我一般按这几个维度判断:

维度SPISCI
典型速率1 MHz ~ 50 MHz9600 ~ 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 / DIV

DIV 在不同平台上的取值规则差别很大:有的是 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 MHz9 MHz889 MHz直接用
100 MHz10 MHz108 / 1612.5 / 6.25 MHz降速到 6.25 MHz
48 MHz12 MHz4412 MHz直接用
80 MHz8 MHz10810 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 决定在第一个边沿还是第二个边沿采样。

模式CPOLCPHA空闲电平采样沿数据变化沿
模式 000低第 1 个沿(上升)下降沿
模式 101低第 2 个沿(下降)上升沿
模式 210高第 1 个沿(下降)上升沿
模式 311高第 2 个沿(上升)下降沿

记忆方法:CPHA = 0 时,第一个边沿就采样,数据在第一个边沿之前就已经准备好了;CPHA = 1 时,第一个边沿是数据变化沿,第二个边沿才采样。换句话说,CPHA 决定了“先有数据还是先有时钟沿”。

模式 0 和模式 3 是最常用的两种,因为它们在“数据在采样沿之前已经稳定”这一点上都做得很自然。工程上有个很实用的经验:如果模式 0 读出来全是 0xFF 或 0x00,先别急着查硬件,换成模式 3 试一次。很多器件手册写的是模式 0,但在实际布线上因为时钟上升沿较缓,模式 3 反而更稳。反过来也一样,模式 3 不通就试模式 0。这两者互换能救活的场景,我遇到过不止一次。

模式 1 和模式 2 用得少,但不是没有,某些老式 ADC 和特定封装的存储器件就是这两种,别默认只有 0 和 3。

3.2 从设备手册里快速定位模式

拿到一份陌生的器件手册,怎么在半分钟内确定该用哪个模式?我的顺序是:

  1. 翻到“时序特性”或“SPI 接口时序”那几页,找 SCK 的波形图。
  2. 看 SCK 在 CS 无效时是高电平还是低电平。高就是 CPOL = 1,低就是 CPOL = 0。
  3. 看数据线(MOSI/MISO)上的数据是在 SCK 的哪个边沿被画成“稳定区间”。如果数据在第一个边沿就已经稳定、第二个边沿之后才变化,那第一个边沿是采样沿,CPHA = 0。
  4. 如果手册直接写了“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 MHz1152004.344125000+8.5%
8 MHz960052.08529615+0.16%
11.0592 MHz1152006.0061152000
12 MHz1152006.516.5115385+0.16%
16 MHz1152008.688.6875115108-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 分阶段降速法:从最保守的参数开始

最后分享一个排查策略:当你面对一个完全跑不通的通信链路,不要一上来就用目标参数试。按这个顺序往下退:

  1. 时钟降到最低档(SPI 用 100 kHz,串口用 9600)。
  2. 模式选最常见的(SPI 模式 0,串口 8N1)。
  3. 关掉所有中断和 DMA,用纯轮询。
  4. 单字节收发,确认一个字节对了再加长度。
  5. 逐项恢复:先提速率,再开中断,再上 DMA。

每改一项测一次,出问题立刻知道是哪一项引入的。这套流程看起来慢,实际比“一把梭配上去然后盲猜哪里错”快得多。我在带新人的时候反复强调这一点,因为大部分“调了两天没调通”的案例,复盘下来都是因为一开始就同时开了太多变量,把问题淹没在噪声里了。

我个人在实际项目里的体会是,SPI 和 SCI 这类外设的调试成本,八成花在“不确定是硬件还是软件”的内耗上。真正有效的做法不是把手册背熟,而是建立一套能快速二分定位的流程:先用环回证明外设本身没问题,再用低速证明时序逻辑没问题,再用逻辑分析仪证明波形没问题,最后才去怀疑从设备和走线。这个顺序反过来做,往往会在一堆变量里绕很久。另外提醒一句,凡是要长期运行的设备,SPI 的片选时序和串口的波特率误差这两项,一定要留足余量,别卡着手册的极限值设计,温度一变、批次一换,问题就冒出来了。

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

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

立即咨询