
1. 为什么SPI和SCI总是“看起来简单用起来翻车”SPI和SCI很多人习惯叫它UART大概是每个嵌入式从业者入门后最早接触、也最容易被轻视的两条总线。说它简单是因为协议本身确实不复杂SPI四根线、同步移位SCI两根线、起始位加数据位加停止位。说它容易翻车是因为真正决定通信能不能跑通的往往不是协议概念而是寄存器里那几个位有没有配对、时序参数有没有压在器件手册的边界内、片选和时钟的先后顺序有没有摆对。我见过太多项目原理图连通、代码编译通过、单步调试也正常一上整机就间歇性丢包或者花屏最后查出来是CPHA配反了或者波特率误差累积超了采样窗口。这篇内容围绕SPI与SCI通信的寄存器配置、时序细节和实战避坑展开面向已经能写基础驱动、但被偶发故障折腾过的嵌入式开发者也适合刚接手板级通信调试、想把底层逻辑一次性理顺的朋友。我不会只贴一段初始化代码了事而是把每个控制位背后的意图、每个时序参数为什么要留余量、每种异常波形对应的硬件或配置原因都拆开讲。读完之后你至少应该能做到两件事拿到一款新MCU的参考手册能独立把SPI和SCI的初始化顺序排对接上逻辑分析仪能看懂波形哪里不对、该往哪个寄存器上找原因。先给一个整体判断SPI的难点在“同步”二字SCI的难点在“异步”二字。同步意味着有时钟线兜底理论上只要时钟和数据沿对齐就能传但正因为有时钟极性、相位、片选建立时间这些细节会被放大异步意味着没有共享时钟收发双方全靠各自的波特率发生器“对表”误差、采样点、帧格式任何一项偏了数据就会错位。把这两条主线抓住后面所有的寄存器配置和排查动作都有落脚点。1.1 两种总线的本质差异与选型逻辑先把两者的定位说清楚不然后面配置时很容易把思路串掉。SPI是同步、全双工、主从结构的总线典型四线SCLK时钟、MOSI主出从入、MISO主入从出、CS片选。它没有地址概念靠片选选从机靠时钟同步移位速率可以做到几十兆赫兹适合板内短距离、高速率、数据量大的场景比如外部Flash、LCD屏、高速ADC、传感器阵列。代价是线数多、没有应答机制、没有硬件差错重传主设备发出去的数据从设备有没有收到协议层是不管的得靠上层自己校验。SCI是异步、全双工、点对点的串行接口典型两线TX和RX加上可选的硬件流控RTS/CTS。它没有时钟线收发双方约定同一个波特率靠起始位下降沿对齐靠停止位收尾。速率通常从几千到几兆波特适合板间通信、连接上位机、接GPS/蓝牙/工业仪表这类外设。它的优势是线少、距离相对远、几乎每款MCU都带缺点是速率上不去、点对点为主、多机场景要额外做地址识别或软件协议。选型时我一般按这几个维度过一遍距离——板内几厘米到十几厘米优先SPI超过半米基本考虑SCI或差分总线速率——需要几兆以上的连续数据流SPI更稳线数预算——排线紧张、连接器针脚有限SCI两线更划算是否需要多从机——SPI靠片选扩展SCI要靠地址位或软件轮询实时性要求——SPI是全双工同步收发同时进行SCI全双工但收发独立缓冲中断响应要及时。这里有个容易被忽略的点很多人以为SPI一定比SCI快其实要看有效吞吐。SPI虽然时钟高但如果每次只传一个字节、片选频繁拉高拉低加上软件开销实际吞吐未必比一个配置良好的SCI高多少。反过来SCI如果波特率误差控制得好、配合DMA搬运做几百KB级别的数据回传也很稳。所以选型不是背结论而是把上面几个维度列出来打分。1.2 新手最容易踩的三个认知误区误区一SPI只要把时钟分频配好就能通信。实际上CPOL和CPHA决定了时钟空闲电平和采样沿配错一个位数据就会整体移一位或者采样到跳变沿上现象是收到全0、全1或者规律性错位。更隐蔽的是有些从器件手册写的是“模式0”但它的时序图里片选拉低到第一个时钟沿之间有最小时间要求配置对了模式、片选时序没留够照样偶发失败。误区二SCI只要两边波特率数字一样就能通。波特率是“标称值”真正的误差来自系统时钟分频后的实际频率。两边各自有误差误差会累积到采样点上。SCI接收端通常做16倍过采样、在中间几位做多数表决理论上能容忍百分之二点几的偏差但如果你用的晶振本身精度差、分频系数又取得粗实际偏差摸到临界值短帧能通、长帧就丢。我调试时就遇到过一个案例单字节收发测试全对连续发两百字节偶尔错一个最后算下来误差接近百分之二点八换了分频系数就恢复了。误区三寄存器写完就立刻生效。很多外设的使能位、缓冲空标志、错误标志有固定的清除顺序。比如发送数据寄存器写进去之后要等状态寄存器的发送空标志置位才能写下一个字节接收标志读完数据寄存器才清除。顺序反了要么丢字节要么中断反复进出。还有一种情况是配置寄存器在模块使能状态下修改无效必须先关使能、改配置、再重新使能。这些细节参考手册里都有但散落在不同章节不系统梳理一遍很容易漏。2. SPI寄存器配置全拆解从时钟极性到数据位宽2.1 SPI控制寄存器的位域逐项解析不同MCU的SPI寄存器命名不一样但核心位域大同小异。我按功能把它们归成四组使能与主从组SPE模块使能、MSTR主从选择、时钟组CPOL极性、CPHA相位、波特率分频位、数据格式组数据位宽、LSB/MSB先行、中断与片选组发送空中断、接收满中断、片选输出使能。初始化时建议按“先清控制寄存器、再配时钟和数据格式、最后开使能和中断”的顺序走避免模块在半配置状态下被意外触发。以常见的8位数据、模式0、主机、分频系数4为例一段典型的初始化代码长这样void spi_master_init(void) { SPI_CR1 0x00; /* 先清零确保模块处于已知状态 */ SPI_CR1 ~(1 6); /* SPE0配置期间关闭模块 */ SPI_CR1 | (1 4); /* MSTR1主机模式 */ SPI_CR1 ~(1 3); /* CPOL0空闲低电平 */ SPI_CR1 ~(1 2); /* CPHA0第一个边沿采样 */ SPI_CR1 ~(1 1); /* 8位数据宽度 */ SPI_CR1 ~(1 0); /* MSB先行 */ SPI_BR 0x01; /* 分频系数4具体编码查手册 */ SPI_CR1 | (1 6); /* SPE1使能模块 */ }这段代码看起来平平无奇但每一行都有讲究。先清零是为了清掉复位后可能残留的默认位有些MCU复位值并不是全0。配置期间关闭SPE是因为部分型号在模块运行时修改分频或模式位会不生效甚至产生异常时钟。MSTR必须在片选有效之前置位否则主机没进入主模式时钟根本不会输出你看到的波形就是一条平线。最后开SPE的时机也有说法如果从器件对片选和时钟的先后有要求最好先把片选拉高无效开SPE之后再按协议拉低片选启动传输。状态寄存器这边重点关注三个标志发送缓冲空可以写下一个字节、接收缓冲满有数据可读、模式错误主机模式下片选被外部拉低说明多主冲突。模式错误一旦置位很多MCU会强制退出主模式必须软件清标志再重新配置这个坑在多设备共板、片选走线干扰大的场景里很常见。数据寄存器是收发共用的写操作填发送缓冲读操作取接收缓冲。全双工的本质是你写一个字节出去的同时从器件也在往MISO上移一个字节回来。所以主机发数据时不要以为“只发不收”接收缓冲该读还得读不读会溢出。2.2 CPOL/CPHA四种模式与时序对应关系CPOL决定时钟空闲时是高还是低CPHA决定在第一个边沿还是第二个边沿采样。两个位组合出四种模式这是SPI调试里最高频的翻车点。我用一张表把它固定下来模式CPOLCPHA空闲电平采样沿数据移位沿000低上升沿下降沿101低下降沿上升沿210高下降沿上升沿311高上升沿下降沿记忆方法别死背表用“CPHA0看第一个边沿CPHA1看第二个边沿”这条规则推。模式0和模式3是最常用的大部分Flash和传感器用模式0或模式3。模式1和模式2相对少但不是没有接之前务必翻从器件手册的时序图看它在时钟的哪个沿上把数据摆稳、哪个沿上要求主设备采样。生活化一点类比SPI通信像两个人按节拍传球。CPOL是约定“不说话的时候手放低还是放高”CPHA是约定“哨声响第一下就传还是等第二下再传”。两个人约定不一致球就掉地上。你配的模式和从器件要求的不一致示波器上能看到数据和时钟错开半个周期解码出来就是整体移位的乱码。这里有一个实操细节改模式之前先把片选拉高、等时钟稳定几个周期再改。有些从器件在片选有效期间收到异常时钟沿内部状态机会乱表现是改完模式第一次通信正常、第二次开始错。保险做法是配置阶段片选一直保持无效配置完成后再按正常流程拉低启动。2.3 波特率分频计算与实战取值SPI的时钟来自系统总线时钟分频公式通常是SCLK f_bus / 分频系数分频系数多为2的幂次2、4、8、16、32、64、128或者2、4、6、8这种偶数序列具体看型号。计算时先确认两件事从器件支持的最高SCLK以及主机在这个时钟下PCB走线能不能撑住信号完整性。举个例子系统总线时钟48MHz从器件手册标最高20MHz分频系数取4得12MHz留了余量稳。如果取2得24MHz超了从器件上限轻则通信不稳重则从器件锁死。再比如总线时钟25MHz想跑10MHz25/102.5没有这个分频档只能取4得6.25MHz或者取2得12.5MHz。取12.5MHz虽然没超很多器件上限但走线长、又没有端接电阻时钟边沿振铃明显采样容易出错。我一般会在满足吞吐的前提下把时钟往低取一档尤其是排线连接、非板内直连的场景。还有一个容易漏的点分频寄存器写入后如果模块已经使能新分频可能要等到当前传输结束才生效。连续传输中间改分频会出现前半段一个速率、后半段另一个速率从器件的时序窗口未必兼容。需要动态调速率时老老实实拉高片选、关模块、改分频、再开模块。2.4 片选管理与多从机拓扑片选看着最简单一根GPIO或者硬件片选输出实际上它是SPI稳定性的关键。片选的建立时间和保持时间必须满足从器件要求片选拉低到第一个时钟沿之间要有最小延时最后一个时钟沿到片选拉高之间也要有最小延时。这两个参数在从器件手册的时序表里通常叫 tCSS 和 tCSH值可能只有几十纳秒但软件模拟片选时如果写完GPIO立刻启动传输中间没有插入延时就可能踩线。多从机有两种常见接法。独立片选是每个从机一根CS主机同一时刻只拉低一根其余保持高。这种方式逻辑简单但GPIO占用随从机数量线性增长。菊花链是所有从机串起来数据从一个进去、经过每个器件移位、从最后一个出来片选共用。菊花链省引脚但要求从器件支持而且传输长度等于所有器件位宽之和软件上要按整条链组织数据。两种方式都别混着用我见过有人片选独立、数据线却又并联后没处理好MISO冲突两个从机同时驱动MISO读回来全是中间电平。软件片选还有一个实践建议片选拉低和拉高尽量用位带操作或者寄存器直接写不要在中间夹打印、夹延时函数调用。调试阶段加了串口打印片选保持时间被拉得老长从器件本来能接受换到正式环境去掉打印反而时序变了。反过来如果调试时用软件延时凑时序量产换主频之后延时不准也会出问题。片选时序要么靠硬件保证要么用定时器精确控制别靠“大概等一会儿”。3. SCIUART配置要点波特率、帧格式与中断3.1 SCI寄存器组与初始化顺序SCI的寄存器组通常分三块波特率寄存器高位和低位、控制寄存器收发使能、中断使能、帧格式、状态寄存器发送空、发送完成、接收满、溢出、帧错误、校验错误。初始化顺序我习惯按这个走先关收发和中断再配波特率再配帧格式数据位、停止位、校验最后开接收、开发送、按需开中断。顺序反了比如先开接收再配波特率模块会用一个默认或者残留的波特率去采样起始位都识别不对直接进帧错误。void sci_init(uint32_t baud) { SCI_CR1 0x00; /* 关闭收发和中断 */ SCI_BDH (uint8_t)(div 8); /* 波特率高位 */ SCI_BDL (uint8_t)(div 0xFF);/* 波特率低位 */ SCI_CR1 | (1 4); /* 8位数据 */ SCI_CR1 ~(1 3); /* 无校验 */ SCI_CR1 ~(1 2); /* 1位停止位 */ SCI_CR1 | (1 1); /* 接收使能 */ SCI_CR1 | (1 0); /* 发送使能 */ SCI_CR2 | (1 5); /* 接收中断使能按需 */ }这里有个细节波特率寄存器写入通常要等当前传输结束才生效初始化阶段模块还没开始传直接写没问题运行中改波特率必须先关收发。另外发送使能和接收使能是独立的只做发送的场合没必要开接收中断省得进中断读空数据。状态寄存器的标志清除方式也要记牢接收满标志读数据寄存器自动清发送空标志写数据寄存器自动清溢出和帧错误标志一般是“先读状态寄存器、再读数据寄存器”两步清除。顺序错了标志清不掉中断会一直进程序看起来像死循环。这个细节我在早期项目里栽过接收中断里先读数据没读状态溢出标志一直挂着主循环被中断拖死。3.2 波特率误差计算与晶振选型SCI没有时钟线误差直接决定采样点会不会飘出数据位窗口。分频公式一般长这样分频值 f_bus / (16 × 波特率)有的型号是8倍或13倍过采样以手册为准。算出来的分频值取整再反算实际波特率误差 实际 - 目标/ 目标 × 100%。接收端通常16倍过采样在起始位下降沿之后数到第8、9、10个采样点做多数表决确定起始位中心之后每个数据位按16个采样点取中间三个表决。理论上单边能容忍的误差大约是半个位宽再打点折扣工程上我按**总误差不超过2%**来控制接近2.5%就要警惕超过3%基本不能指望长帧稳定。晶振选型是误差的源头。经典的11.0592MHz之所以在串口时代流行就是因为11.0592M除以16再除以常见波特率9600、19200、115200能得到接近整数的分频值。换成8MHz晶振算115200的分频值是 8M/(16×115200)≈4.34取4反算实际波特率125000误差约8.5%根本不能用取5反算100000误差13%更离谱。这种时候要么换晶振要么用支持小数分频的型号要么把系统时钟倍频到一个对波特率友好的频率。我整理过一张常见组合的误差对照配置前先查表能省很多事系统时钟目标波特率分频值(取整)实际波特率误差11.0592MHz11520061152000%11.0592MHz96007296000%8MHz11520041250008.5%8MHz96005296150.16%16MHz11520091111113.5%16MHz960010496150.16%表格看下来结论很清楚低速波特率对时钟不挑剔高速波特率必须挑时钟。115200在8MHz和16MHz下都不能直接用整数分频要么倍频到能整除的频率要么换晶振。实际项目里如果板子已经定了8MHz晶振又必须跑115200优先考虑把总线时钟倍频到48MHz48M/(16×115200)≈26.04取26反算115384误差0.16%完全可用。3.3 收发中断与DMA配合中断方式适合低速、数据量小的场景。接收中断里第一时间把数据寄存器读出来存进环形缓冲别在中断里做解析。发送中断里从缓冲取一个字节写数据寄存器缓冲空了就关发送中断避免空中断反复触发。这里的关键是中断服务程序要短读、写、清标志三步走完就退出解析和业务逻辑放主循环。数据量大或者波特率高的时候中断开销会吃掉不少CPU。115200波特率下每个字节间隔约87微秒如果中断里还做别的事很容易跟不上。这种场景上DMA更稳接收DMA配成循环模式或者配合空闲中断发送DMA搬完一帧再触发完成中断。空闲中断的思路是——总线上连续一段时间没有新数据判定一帧结束这时候去DMA缓冲里取整包。这个方案做不定长协议特别好用比逐字节中断超时判断干净得多。不管用中断还是DMA缓冲区的读写指针要防并发。中断里写指针、主循环里读指针简单场景用“读指针追写指针”的环形结构复杂场景加临界区保护。我见过因为指针没保护、主循环读到半写状态的数据解析出一堆乱码查了半天以为是波特率问题其实是并发。3.4 帧格式、校验与流控帧格式三要素数据位常见7或8、停止位1或2、校验位无、奇、偶。两边必须完全一致少配一位都收不对。8N1是默认选项绝大多数场景够用。需要校验的场合硬件奇偶校验能挡掉一部分单比特错误但挡不住成片干扰重要数据还是要在应用层加CRC或校验和。硬件流控RTS/CTS解决的是“接收方缓冲快满时通知发送方暂停”。CTS是输入低有效表示可以发RTS是输出低有效表示自己准备好了。线序交叉接本方RTS接对方CTS本方CTS接对方RTS。不接流控又高速连续传输接收方处理不过来就会溢出。软件流控用XON/XOFF字符实现简单但会占用数据字符空间二进制数据里出现这两个字符就麻烦了一般不推荐。还有个实际经验TX和RX的引脚复用和上下拉。TX配成推挽输出RX配成输入并开上拉防止悬空时收到随机噪声。板间连接如果线缆较长TX串一个小电阻22到100欧姆能压一压过冲RX端加对地小电容滤高频毛刺具体取值看速率和线长速率越高电阻越小、电容越小。4. 时序详解示波器下的真实波形与理论差异4.1 SPI时序关键参数与建立保持时间SPI的时序表里主设备要保证的参数主要有片选有效到第一个时钟沿的建立时间 tCSS、最后一个时钟沿到片选无效的保持时间 tCSH、时钟周期 tSCLK、时钟高低电平宽度、数据输出相对采样沿的建立时间 tSU 和保持时间 tH。从器件手册给的是它“需要”的最小值主设备配出来的实际值要大于这些最小值并且留余量。最容易忽略的是数据建立和保持时间。主机在移位沿把数据摆到MOSI上从器件在采样沿读取中间隔半个时钟周期。时钟频率越高这半个周期越短。比如12.5MHz时钟半周期40纳秒减去PCB走线延迟、引脚输出延迟、从器件输入建立时间余量可能只剩十几纳秒。走线一长、负载电容一大边沿变缓实际到达从器件的有效窗口更窄就开始偶发错位。示波器上能明显看到时钟和数据边沿不再陡峭甚至数据在采样沿附近还在爬坡。我一般的做法是先用较低时钟把功能跑通再逐步提频同时用逻辑分析仪看解码误码率。提到某个频率开始出错就退回上一档这就是这块板子在当前走线下的实际上限。手册标的最高频率是器件能力不是你的板子能力。4.2 SCI异步时序与采样点SCI接收的时序核心是“对齐”。空闲时线路高电平起始位是一个下降沿接收端检测到这个下降沿后启动内部采样计数在起始位中间确认电平仍为低然后每隔一个位时间采一次直到停止位。16倍过采样的意义是把每个位分成16份取中间几份表决抗噪能力强。误差的影响可以这样理解发送方和接收方的位时间有微小差异传的位数越多采样点偏离位中心越远。传10位1起始8数据1停止时误差累积到接近半个位宽就开始采错。这就是为什么长帧对波特率误差更敏感。示波器上如果看到接收方的采样点逐渐往数据位边缘漂基本就是误差偏大。帧错误FE标志就是接收端在停止位位置采到低电平说明帧没对齐或者线路被干扰。溢出OR标志是上一帧还没读走、下一帧已经收完软件处理太慢。这两个标志在调试时非常有用连续出现帧错误先查波特率连续出现溢出先查软件读取频率。4.3 用逻辑分析仪验证配置逻辑分析仪的协议解码功能是排查SPI和SCI的利器。SPI解码要设对模式CPOL/CPHA、位宽、位序设错了解出来是乱码别急着怀疑硬件。SCI解码设对波特率、数据位、校验、停止位解码窗口里能直接看到每一帧的字节值、帧错误标记。抓波形时重点看三样时钟质量有没有振铃、过冲、边沿变缓、数据与时钟的相对位置数据跳变是不是发生在采样沿附近、片选时序片选拉低到第一个时钟沿够不够、最后一个时钟沿到片选拉高够不够。振铃严重时可以在源端串电阻或者降低时钟数据贴采样沿太近检查模式配置和走线延迟片选时序不够软件里插入几个空操作或者用硬件片选。5. 实战避坑指南典型故障与排查路径5.1 通信无响应的排查清单通信完全没反应按这个顺序过一遍能覆盖九成情况供电和地——从器件有没有电、地有没有共、电平是否匹配3.3V器件接5V主机要电平转换引脚复用——GPIO有没有切到外设功能很多MCU默认是普通IO不配复用功能时钟根本出不来模块时钟——外设时钟使能位有没有开这个漏了寄存器写进去也不生效片选——软件片选有没有拉低、硬件片选极性对不对模式——SPI的CPOL/CPHA、SCI的波特率和帧格式使能位——SPE、TE、RE这些总开关有没有置位。排查时我习惯先量电平再抓波形万用表量片选、时钟、数据线的静态电平和预期对比再用逻辑分析仪抓一次传输看有没有时钟、有没有数据跳变。有时钟没数据查MOSI配置和发送缓冲有数据没时钟查主从模式和使能全都没有回到供电和复用。5.2 数据错位与位翻转数据整体移一位十有八九是CPHA配反了采样沿和移位沿对调收到的数据等于发送数据左移或右移一位。规律性错位比如每隔几位错一次看位宽配置8位器件按16位传高低字节顺序就乱了。随机位翻转优先查信号完整性时钟太快、走线太长、没有地回流、旁边有开关电源干扰。降频能改善就是信号完整性问题降频不改善再查配置。SCI这边数据位错位常见于波特率误差偏大或者停止位配错。收到的字节值稳定地偏某个固定模式算一下误差偶尔错一个字节且伴随帧错误标志查线路干扰和共地。5.3 多从机场景的片选时序多从机最容易出的是片选竞争一根片选还没拉高另一根已经拉低两个从机同时驱动MISO读回来是中间电平或者互相拉扯的波形。软件上保证“先拉高当前片选、插入短延时、再拉低目标片选”。硬件上片选走线尽量等长避免到达时间差太多。菊花链场景还要注意数据长度对齐整条链的位宽是各器件之和少发一位后面全错位。配置时把链上每个器件的位宽列清楚发送缓冲按总长度组织。5.4 常见问题速查表现象优先怀疑排查动作SPI完全无时钟输出模块未使能、非主模式、引脚未复用查SPE、MSTR、复用寄存器SPI收到全0或全1片选未拉低、MISO未接、模式错误量片选电平、查CPOL/CPHASPI数据整体移位CPHA配反、位序配反对照从器件时序图改模式SPI偶发错位时钟过快、走线过长、无地回流降频验证、查信号完整性SCI收不到数据波特率误差大、TX/RX接反、未开接收算误差、交叉线序、查RE位SCI偶发帧错误波特率临界、干扰、停止位不符查误差表、加滤波、核对帧格式SCI接收溢出软件读取太慢、中断被阻塞提高读取频率、上DMA、查中断优先级多从机读回异常片选竞争、MISO冲突查片选切换时序、加延时6. 我在实际项目里沉淀的几条经验第一条先把时序图打印出来贴在工位上。寄存器配置可以查、代码可以抄但时序图里的 tCSS、tCSH、tSU、tH 这些参数是这块板子能不能稳的根。配置前对着图把每个参数的实际值算一遍比出问题后再回头翻手册省太多时间。第二条任何通信驱动都先做回环测试。SPI把MOSI和MISO短接发什么收什么验证移位逻辑和中断流程SCI把TX和RX短接验证波特率和帧格式。回环能过再接真实器件能把“驱动问题”和“器件问题”分开排查范围直接砍一半。第三条示波器和逻辑分析仪别等出问题才用。功能刚跑通的时候就抓一版正常波形存下来后面出异常拿来对比一眼就能看出是时钟变了、数据位置偏了还是片选时序短了。没有基准波形异常波形看起来都差不多。第四条配置参数留余量别贴着手册上限跑。时钟取低一档、波特率误差控制在百分之二以内、片选延时多给几十纳秒这些余量在实验室看不出来到了温度变化、电源波动、线缆变长的现场就是稳定和不稳定的分界线。嵌入式通信的很多“玄学问题”本质都是余量不够。第五条错误标志要真的去看。SPI的模式错误、SCI的帧错误和溢出错误这些标志平时不声不响出问题时就是最直接的线索。中断里顺手读一下状态寄存器把异常计数存起来比事后猜要靠谱得多。这套习惯我保持了很多年后来接手别人代码时第一件事也是看它的错误处理有没有把标志读全。