1. 从两根线说起:SPI与SCI到底在解决什么问题
做嵌入式开发的朋友,早晚会被一个问题追上:两个芯片之间那点数据,到底该怎么传。我第一次独立负责板级联调时,主控和一颗传感器之间只有四根线可用,其中两根还是电源。当时脑子里第一反应是"串口不就行了",结果速率要求一算,常规异步串口在时钟偏差和波特率误差上吃不消,最后还是回到SPI。类似的分岔路口,几乎每个项目都会遇到:SPI和SCI(在很多芯片手册里就是UART同步/异步串行口的那套模块)看起来都是"串行通信",但它们的性格完全不同,选错了后面全是返工。
先把概念摆正。SPI是同步串行接口,靠一根独立的时钟线(SCK)来定节拍,主机发时钟,从机跟着节拍收发,数据在MOSI和MISO两根单向线上同时流动,天然全双工。SCI通常是串行通信接口的统称,在多数MCU里对应UART/USART模块,支持异步通信(不需要共享时钟),也常带一个同步模式可以输出时钟。异步的关键在于双方事先约定好波特率,靠起始位对齐每一帧,靠停止位收尾,中间再插校验位做粗粒度保护。
这两套东西解决的是同一类需求:板上或板间的短距离数据搬运。但它们对"谁来定节拍"的回答完全不同。SPI把节拍权交给主机,代价是多一根线和更复杂的时序约定;SCI异步模式把节拍权交给双方各自的时钟源,代价是对时钟精度敏感、抗累积误差能力弱。理解了这一点,后面的寄存器配置、时序模式选择、避坑思路,全都顺理成章。
这篇文章面向的是已经能点亮GPIO、会看芯片手册、准备或正在做板级通信联调的读者。无论你手里的芯片是哪个系列,SPI和SCI模块的核心逻辑是共通的:一套控制寄存器定工作模式,一套状态寄存器反映当前进度,一套数据寄存器承载收发内容,外加一堆中断和DMA挂载点。我不会依赖某一个具体型号的寄存器地址,而是把"为什么这么配"讲透,让你拿到任何一份手册都能自己对应上。这也是我这些年最深的体会:寄存器位是可以查的,但配置思路查不到,只能靠理解。
2. 寄存器不是用来背的:把SPI控制块拆成五个功能域
2.1 控制寄存器里到底藏了几件事
很多人第一次看SPI章节,会被密密麻麻的位定义吓退。其实把SPI控制寄存器按功能归类,无非五件事:使能、主从角色、时钟极性相位、数据帧格式、片选行为。这五件事分开看,每一位都有明确的物理含义,根本不需要死记。
使能位(常写作SPE/SPIEN之类)是总闸,置位后模块才开始驱动引脚。这里有个新手常犯的错:先配好其他位、最后再开使能,这个顺序是对的;但反过来,在模块运行中改时钟极性或帧长度,很多芯片要求先关使能再改,否则行为未定义。手册里通常会在相关位描述里写一句"仅在SPEx=0时可写",这句话千万别跳过。
主从角色位决定谁输出时钟。主机模式下SCK是输出,从机模式下SCK是输入。这里容易踩的坑是引脚的复用方向:配置成从机后,如果你还用主机时的GPIO初始化代码把SCK设成推挽输出,硬件上就出现了两个输出对顶,轻则通信失败,重则引脚发热。我的习惯是主从切换时,把四个引脚的初始化函数单独抽出来,跟着角色走,不共用一份配置。
时钟极性与相位(CPOL/CPHA)两位组合出四种模式,这是SPI最核心也最容易配错的地方,后面单独展开。数据帧格式包含字长(常见8位或16位)和位序(MSB先行或LSB先行)。字长要和收发缓冲区的数据类型对齐,位序要和从机器件手册一致,这两个配错的表现是"数据能收到但完全对不上",属于典型的软性错误。
片选行为分硬件片选和软件片选。硬件片选由模块自动拉低拉高,适合多从机场景;软件片选用普通GPIO手动控制,灵活但要自己管时序。我一般优先用软件片选,原因很实际:调试时可以用逻辑分析仪清楚看到片选和时钟的先后关系,多从机切换时也便于插入自己的延时。
2.2 状态寄存器是排错的第一现场
状态寄存器里那几个标志位,是我排查通信问题的第一站。常见的包括发送缓冲区空、接收缓冲区非空、忙标志、溢出标志、模式错误标志。它们的价值在于把"通信不工作"这个模糊描述拆成可定位的具体环节。
举个例子:忙标志一直为1,说明模块还在处理上一笔数据,此时写数据寄存器会被丢弃或覆盖;接收溢出标志置位,说明上一笔数据还没被读走,新数据已经到达,通常是中断响应太慢或主循环里读得太晚;模式错误标志在多主机总线上才有意义,单主单从场景下一般不会触发,但一旦触发往往意味着引脚配置或角色设置有冲突。
提示:状态标志大多是"读后清零"或"写1清零",具体行为必须查手册。我曾因为把读后清零的标志当成写1清零,在中断里反复读到同一个旧状态,白白排查了半天。
2.3 数据寄存器与FIFO的深度陷阱
数据寄存器在简单实现里只有一个,写入即发送、读出即接收。但现在的芯片普遍带发送和接收FIFO,深度从4级到16级不等。FIFO带来一个问题:你以为在连续发送,实际上数据都堆在FIFO里还没出去。
这直接影响时序判断。比如你用软件片选,在写完最后一个字节后立刻拉高片选,如果FIFO里还有数据没发完,从机就会在半个字节处被掐断。正确做法是等待"发送完成"标志或"FIFO空且总线空闲"标志,而不是等"数据寄存器可写"标志。这两个标志的区别是:前者表示数据已经完整移出到引脚上,后者只表示缓冲区腾出了位置。我在一个读取外部存储器的项目里被这个差异坑过,读回的数据总是最后两个字节错位,查了很久才发现是片选提前释放。
2.4 中断与DMA的挂载点选择
中断和DMA不是必须的,但它们决定了通信的实时性和CPU占用。我的判断标准很朴素:数据量小、频率低,用轮询;数据量中等、需要及时响应,用中断;数据量大、要求连续高速,上DMA。
中断源通常包括发送完成、接收非空、溢出错误、模式错误。配置时要注意中断使能是两级结构:模块级使能位加全局中断使能,漏掉任何一级都不会进中断。DMA请求则要注意触发条件,一般是"发送缓冲区空"触发一次搬运,或"接收缓冲区非空"触发一次搬运,通道配置里的传输宽度必须和SPI字长一致,8位字长配16位传输宽度,数据会错位。
2.5 时钟分频:速率不是越高越好
SPI的时钟来自系统总线时钟经过分频器。分频系数通常是2的幂次或者可编程偶数。选速率时不能只看从机器件的最高频率,还要算上板级因素:走线长度、是否有缓冲器、上拉电阻阻值。
一个实用的估算方法:把从机手册里的最高SCK频率打七折作为初始配置,跑通后再逐步往上试。我见过太多人一上来就拉满,结果在实验室常温下能跑,到了现场温度变化就偶发误码。信号完整性在长走线、多负载的情况下对速率非常敏感,而这个衰减不是线性的,往往在某个频率点之后迅速恶化。
3. 四种时序模式配错的典型症状与判定方法
3.1 CPOL与CPHA组合出的四种采样关系
SPI的时钟极性CPOL决定空闲时SCK是高还是低,时钟相位CPHA决定数据在第一个边沿还是第二个边沿被采样。两者一组合,就是模式0到模式3:
| 模式 | CPOL | CPHA | 空闲电平 | 采样边沿 | 数据移出边沿 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 低 | 上升沿 | 下降沿 |
| 1 | 0 | 1 | 低 | 下降沿 | 上升沿 |
| 2 | 1 | 0 | 高 | 下降沿 | 上升沿 |
| 3 | 1 | 1 | 高 | 上升沿 | 下降沿 |
记忆的窍门不是背表,而是抓住本质:CPOL决定"空闲在哪儿",CPHA决定"第一次采样在第一个边沿还是第二个边沿"。CPHA为0时,数据必须在第一个边沿前就准备好,所以主机在片选拉低后就要把第一位推出去;CPHA为1时,第一个边沿只负责移出,第二个边沿才采样。
3.2 模式不匹配为什么表现为"能通但数据乱"
主从模式不一致时,最迷惑人的现象是通信看起来在工作:时钟有、数据有、偶尔还能读到正确值。原因是采样点偏移了半个时钟周期,落在数据跳变的边沿附近,采样结果就取决于走线延迟和噪声。温度变一点、电压变一点、换个批次板子,结果就变了。
判定方法很直接:拿逻辑分析仪同时抓SCK、MOSI、片选,看数据跳变相对SCK边沿的位置。如果在采样边沿处数据正在变化,就是模式错。更省事的办法是把四种模式依次试一遍,每种模式连续读同一个寄存器地址,能稳定读回正确值的那个就是对的。这个"穷举法"听起来粗暴,但在没有从机详细时序图时是最高效的定位手段。
3.3 从机器件手册里的时序参数怎么读
从机手册一般给一张时序图加一张参数表,我们要关注四个值:SCK周期、时钟高电平时间、时钟低电平时间、片选有效到第一个时钟边沿的建立时间,以及最后一个时钟边沿到片选释放的保持时间。
前三个值约束的是速率上限,后两个约束的是片选控制精度。如果你的软件片选拉低后立刻发数据,中间没有任何延时,而nRF系列或某些存储器件要求片选建立时间达到几十纳秒,就会出现第一个字节丢失或误码。解决办法是在片选拉低后插入一个短延时,或者干脆用硬件片选让模块自己处理。
3.4 用示波器还是逻辑分析仪
我的实际搭配是:逻辑分析仪看协议层,示波器看电气层。逻辑分析仪能把SCK、MOSI、MISO、CS四条线同时抓下来,直接解码出字节,判断模式错、位序错、片选时序错都非常快。示波器则用来看上升沿是否过缓、有无过冲振铃、电平是否达到从机的VIH门限。
有一次通信在1MHz以下一切正常,超过2MHz就开始丢包,逻辑分析仪显示时序完全正确。换示波器一看,SCK上升沿在2MHz时已经变形,从机输入端的实际高电平勉强够到门限,加了一个22欧姆的串联电阻抑制振铃后问题消失。这个案例说明,协议对不代表电气对,两者必须分开验证。
3.5 多从机场景下的片选与总线争用
多个从机挂在同一组SCK/MOSI/MISO上时,片选是唯一的总线仲裁手段。这里有两个硬性要求:任一时刻只能有一个片选有效;未被选中的从机,其MISO必须处于高阻态。
第二个要求经常被忽略。有些器件在未被选中时MISO不是高阻而是推挽输出低电平,一旦总线上有两个以上这类器件,MISO线就被强行拉低,主机会读到全0。排查办法是逐个焊接从机,单独测试每个器件的MISO在未被选中时的电平状态。如果确实是推挽输出,就得加外部缓冲器或者改用独立的总线。
4. SCI/USART的帧结构:异步通信的可靠性全在约定里
4.1 起始位、数据位、校验位、停止位这四段
异步通信没有共享时钟,接收方靠起始位的下降沿重新对齐内部位定时器,然后在每一位的中间位置采样。一帧的结构是:1个起始位(低电平)、5到9个数据位、可选的1个校验位、1到2个停止位(高电平),空闲时线路保持高电平。
接收方对齐的原理值得说清楚,因为它解释了所有波特率误差问题的根源。检测到起始位下降沿后,接收方并不是立刻采样,而是等待半个位周期,确认起始位仍为低(这叫起始位校验,能滤掉毛刺),然后在每个位周期的中间采样。问题是,接收方的位周期由自己的时钟分频得到,和发送方的位周期存在微小差异,这个差异会随着帧内位数累积。到第8位时,如果累积偏移超过半个位周期,采样就会落到错误的位上。
4.2 波特率误差的计算与容忍边界
这就是为什么波特率误差必须算。假设发送方和接收方的时钟源都有误差,且方向相反,最坏情况下的总误差是两者之和。一般经验是总误差控制在2%以内比较安全,超过3%就很容易在长帧、高温、低电压场景下出现偶发错误。
计算实例:系统时钟为48MHz,目标波特率115200。分频值等于48,000,000除以115200,约等于416.67。取整为417,实际波特率是48,000,000/417,约等于115107,误差约-0.08%,完全可接受。但如果系统时钟是8MHz,目标115200,分频值约等于69.44,取整69得115942,误差+0.64%,取70得114285,误差-0.79%,都在可接受范围。
真正麻烦的是高波特率配低主频。比如8MHz主频跑921600,分频值只有8.68,取整8得到1M波特率,误差+8.5%,必然失败。这种场景要么换晶振、要么用带小数分频的模块、要么降到能支持的波特率。我在一个老项目上就遇到过必须跑460800但主频只有12MHz的情况,最后靠模块的过采样倍率调到8倍(而不是常规16倍)才把误差压下来。
注意:很多芯片的波特率寄存器分为整数部分和小数部分,小数部分不是随便填的,要按手册给的公式换算成分数位。别拿浮点数直接取整填进去。
4.3 校验位能防什么、不能防什么
校验位分奇校验和偶校验,作用是检测单比特错误。它防不住偶数个比特同时出错,也防不住整帧错位。实际工程里,如果链路质量差,单靠一位校验基本不够,通常要在应用层再加CRC或者帧头帧尾做二次校验。
我的做法是:短帧、低风险数据(比如配置参数)用偶校验即可;涉及控制指令或大数据块的,一律在协议层加CRC16,校验位只当最后一道粗筛。这个成本很低,但能避免很多"数据偶尔莫名其妙"的疑难问题。
4.4 校验位错误、帧错误、溢出错误分别意味着什么
接收状态寄存器里通常有三个错误标志:校验错误(PE)、帧错误(FE)、溢出错误(ORE)。它们的含义和排查方向完全不同,混在一起看就会浪费时间。
校验错误说明数据位加校验位的1的个数不符合约定,通常是线路干扰或波特率轻微不匹配。帧错误说明停止位没被检测到低电平被破坏,几乎总是波特率不匹配或线路被拉低。溢出错误说明接收数据寄存器还没被读走新数据就到了,属于软件响应不及时,跟硬件无关。
排查顺序应该是:先看帧错误,因为它指向波特率这个基础问题;帧错误排除后再看校验错误;溢出错误单独处理,检查中断优先级和主循环耗时。
5. 双模块协同:为什么我坚持SPI走数据、SCI走日志
5.1 一个真实的分工方案
在一个数据采集系统里,我的典型分工是:SPI负责和高速外设通信(传感器、存储器、显示驱动),SCI负责和上位机通信(上报数据、接收指令、输出调试日志)。这个分工不是随便定的,背后的逻辑是两类通信的"性格"匹配度。
SPI是主机主导、速率高、全双工、无协议开销,适合"我要立刻拿到某个寄存器的值"这种确定性访问。SCI异步是双方各自定节拍、有帧开销、速率中等,但胜在引脚少(两根)、支持长线(加收发器后可以拉很远)、几乎所有调试工具都支持。把日志走SCI,好处是即使SPI外设通信崩溃,日志通道依然能工作,你能看到崩溃前的最后信息。如果把日志也挂在SPI上,外设一挂就什么都看不到了,这是排错时最忌讳的单点依赖。
5.2 两个模块共享中断时的优先级安排
SPI和SCI同时工作时,中断优先级要仔细安排。我的原则是:SPI接收中断优先级高于SCI,因为SPI通常没有流控,数据到达后必须尽快读走,否则溢出;SCI接收可以稍缓,因为对方会等,而且SCI往往有FIFO缓冲。
如果两者都用DMA,要注意DMA通道的优先级和总线仲裁。高优先级的DMA通道会抢占低优先级通道的总线访问权,如果SPI的DMA被SCI的DMA频繁打断,SPI的连续传输可能出现间隙,在高速场景下导致从机超时。解决办法是给SPI的DMA更高优先级,或者把SCI的DMA传输拆小一点。
5.3 引脚复用冲突的检查清单
引脚复用是板级联调时最常见的低级错误。我的检查清单是:每个通信引脚在引脚复用表里对应的功能编号;复用功能是否需要额外的上拉或下拉配置;同一个引脚是否被两个模块同时申请;调试口是否和通信口冲突。
最后一条值得单独说。很多芯片的调试引脚(比如SWD)和某些串口引脚是复用的,一旦在代码里把复用功能切过去,调试器立刻断开,之后只能靠擦除芯片或者用启动模式引脚恢复。我踩过这个坑,现在的习惯是在初始化代码里把调试口相关引脚的保护逻辑写在最前面,并且注释写清楚"此引脚不可复用"。
6. 联调现场:六个高频故障的完整排查链路
6.1 完全无波形:从使能位查到引脚方向
现象:逻辑分析仪上SCK、MOSI一条线都不动,片选也不动。
排查链路:先确认模块使能位是否置位;再确认引脚复用是否切到了通信功能(很多人只配了GPIO模式,忘了切复用);然后确认引脚方向,主机模式下SCK和MOSI必须是输出,MISO是输入;最后确认是否有其他代码在初始化之后又把引脚改回去了。
我遇到过一次,初始化函数里配置正确,但主循环里有一处"喂狗前统一复位所有外设引脚"的遗留代码,把SPI引脚全改成了模拟输入。这种问题靠读代码很难发现,靠"注释掉可疑代码二分法"最快。
6.2 有波形但数据全为0或全为1
数据全0常见于MISO线被拉低或者从机没有响应。先量从机供电和片选是否真的到达从机引脚。数据全1常见于MISO线被上拉但无人驱动,也就是从机根本没输出。这两种情况都要先怀疑片选时序:片选没拉低,从机就不会驱动MISO。
还有一种隐蔽情况:位序配反了。如果发送0x01(二进制00000001),MSB先行时实际发出的是高位先出,LSB先行时最低位先出,接收方按相反位序解析就会得到0x80。这种错误的表现是"某些值能对、某些值不对",比如0x00和0xFF永远正确,对称值也正确,其他全错。
6.3 低速正常高速出错
前面提过电气层的原因。排查顺序建议是:先用逻辑分析仪确认协议时序在高速下是否仍然正确(采样点是否偏移);再降速验证是否与速率相关;然后用示波器看波形质量;最后检查从机是否有最高速率限制被忽略。
还要考虑一个软件因素:高速时中断来不及处理,接收溢出。如果发送端连续发多个字节,接收端每次进中断只读一个字节,中断延迟稍大就会丢数据。解决办法是用FIFO触发阈值中断,或者在DMA模式下让硬件搬运。
6.4 通信一段时间后卡死
卡死的典型原因是状态标志等待死循环。比如等待发送完成标志,但从机因为某个原因不响应(片选没接、从机复位中),标志永远不置位,程序就停在那里。这类代码必须加超时。
我的做法是给所有状态等待加一个计数上限,超时后返回错误码并记录当前状态寄存器的值。这个习惯救过我很多次,因为寄存器快照能直接告诉我卡在哪个环节。
6.5 多从机时只有一个能通
先测片选信号:每个从机的片选是否真的独立、是否都接到了正确的引脚。再测MISO:未被选中的从机MISO是否高阻。如果所有从机MISO都并在一起而某个器件不是高阻输出,就会拖垮整条线。
还有一种情况是片选切换间隔太短。前一个从机的片选刚拉高、后一个立刻拉低,中间没有总线空闲时间,某些慢速器件来不及释放MISO。加几个微秒延时通常能解决。
6.6 波特率看起来对但偶尔丢字节
这种问题最折磨人。排查方向:一是实际波特率误差是否在边界上,用示波器量一位的时间算实际波特率;二是接收中断是否有被更高优先级中断长时间屏蔽的情况;三是发送方是否有连续发送而接收方处理不过来的情况。
我印象最深的一次,是系统里有一个定时器中断每毫秒执行一次较长的计算,把串口接收中断压住了接近200微秒,正好在高速波特率下导致溢出。最后是把串口接收改成DMA加空闲中断的方式,让硬件把数据搬完再通知CPU,问题彻底消失。
7. 几个让代码更抗造的工程习惯
7.1 把收发封装成带超时的原子操作
不管底层用轮询还是中断,对外都提供一个"发送一帧并等待完成,超时返回失败"的函数。这样上层调用者不需要关心底层细节,也不会因为某个标志没等到而卡死。我的封装里通常会带上重试次数,偶发错误自动重试一次往往就能过。
7.2 为通信模块保留一份可打印的诊断快照
在出错时把控制寄存器、状态寄存器、引脚状态、当前波特率配置、错误计数全部打印出来。这份快照在实验室里有逻辑分析仪时不觉得重要,到了现场没有仪器时就是救命稻草。我现在的习惯是每个通信模块都有一个dump函数,通过日志口输出,成本很低。
7.3 参数配置集中管理,避免散落各处
波特率、SPI模式、字长、片选引脚号这些参数,全部集中在一个配置头文件里,并且用宏做合法性检查。散落在各个函数里的魔法数字是维护噩梦,改一个波特率要翻五个文件的情况我见得太多了。
7.4 上电初始化的顺序不能随意
我的固定顺序是:系统时钟配置、引脚复用配置、模块参数配置、中断配置、使能模块、最后才开始收发。任何一步提前都会导致不可预期的行为,尤其是"使能模块"这一步,一定要放在参数配置完成之后。
7.5 给时序留余量,别卡在手册的极限值上
手册给的是保证器件工作的边界,不是推荐工作点。片选建立时间手册写10纳秒,你就留50纳秒;最高时钟手册写10MHz,你就先跑7MHz。产品要面对温度、批次、电源波动,实验室里卡极限能跑,量产就是灾难。
我自己在这些年做下来,最大的感受是:SPI和SCI的寄存器配置其实一两天就能学会,真正花时间的是理解"为什么这样配"以及在异常现象面前保持有条理的排查路径。把状态寄存器当第一现场、把逻辑分析仪当常规工具、把超时和诊断快照当默认习惯,这三件事做到了,通信问题基本都能在半天内定位。剩下那些真正诡异的偶发故障,往往不是协议本身的问题,而是电源、地线、走线或者某个被忽略的初始化顺序在作怪。