ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SPI总线通信详解:时序模式、片选、STM32 DMA与Flash实战

SPI总线通信详解:时序模式、片选、STM32 DMA与Flash实战 SPI总线这东西说简单也简单四根线一接时钟一给数据就出去了说复杂也复杂我见过太多人卡在“为什么读出来全是0xFF”上耗掉一整个下午。SPI协议本身没有地址概念、没有应答机制、没有仲裁它把所有的复杂度都推给了设计者——时序模式选哪个、片选谁来管、要不要上拉、DMA开几个通道这些决策做错一个波形就是不对。这篇内容我打算把SPI总线从物理层到驱动层完整捋一遍重点讲清楚那些手册上写得含糊、只有真正接过逻辑分析仪才明白的细节。不管你是刚上手单片机、准备用CubeMX点第一个SPI外设还是已经在做SPI Flash、总线舵机、车载模块这类具体项目这里面的时序判读、片选取舍、DMA配置和排查思路都能直接拿去用。适合有一定C语言和单片机基础、需要在真实硬件上把SPI跑通的人看。1. SPI总线的本质四根线为什么能撑起板级通信先从最根上的问题说起SPI为什么是四根线而不是像串口那样两根线就够。这四根线分别是SCLK时钟、MOSI主出从入、MISO主入从出、CS片选有的场合还会多一根但核心就是这四个。理解SPI总线本质上就是理解“时钟由谁产生、数据往哪个方向走、什么时候有效”这三件事怎么被这四根线表达出来的。1.1 同步全双工这个设计选择背后的算盘串口UART是异步的收发双方各自用自己的时钟靠起始位和波特率约定来对齐这就要求两边波特率误差不能太大一般控制在2%以内跑高了容易丢帧。SPI走的是另一条路时钟线由主机单独拉出来从机不产生时钟只管跟着这个时钟节拍走。这样一来双方的“时间基准”是同一个物理信号不存在波特率误差累积的问题理论上时钟能跑多快只受限于器件本身的建立保持时间和走线质量几十兆赫兹很常见。全双工是第二个关键点。很多新手以为SPI是“发完再收”其实不是。移位寄存器是环形结构主机每给一个时钟沿MOSI上推出一位出去MISO上同时就进来一位一字节交换在8个时钟周期内同时完成。这就是为什么SPI Flash的读指令要“发一个字节、同时读一个字节”你发的是命令或地址收回来的可能是状态或数据它们在同一时刻并行发生。理解这一点非常重要它解释了很多“我明明只想读为什么还要发数据”的困惑——因为在这个环形移位模型下你不发时钟、不推数据从机就没法把数据吐出来。注意SPI没有“只读”或“只写”的物理区分读操作本质上也是“发一堆空字节通常是0xFF换时钟把从机的数据顶回来”。这个认知一旦建立看波形就顺了。同步全双工带来的代价是引脚多、没有硬件应答。从机收到数据对不对SPI协议层完全不告诉你。所以像SPI Flash、SPI传感器这类器件都会额外定义状态寄存器、CRC校验或者命令回读机制用软件层的约定来补协议层的缺失。这是SPI设计哲学的一贯风格底层做减法把可靠性交给上层自己决定。1.2 串口、IIC、SPI通信协议到底怎么选做项目选通信方式绕不开USART、UART、I2C、SPI这四兄弟的比较。UART严格说是异步串行USART多了同步模式I2C是两线半双工带地址SPI是四线全双工无地址。它们解决的问题不一样不能简单说谁好谁坏。对比项UARTI2CSPI线数2TX/RX2SCL/SDA4SCLK/MOSI/MISO/CS时钟异步同步同步双工全双工半双工全双工寻址无有7/10位地址无靠片选最高速率通常几Mbps标准100k/400k快速1M几十Mbps常见多从机点对点为主一条总线挂多个每个从机一根CS典型场景调试口、模块通信传感器、EEPROMFlash、显示屏、ADC、射频模块选型的逻辑其实很朴素需要高速、需要双向同时传、从机数量少且固定就上SPI需要一条总线挂一堆低速器件、省引脚用I2C只是点对点传个日志、连个模组UART最省事。我在实际项目里最常见的组合是SPI接Flash和屏幕I2C接温湿度传感器和EEPROMUART接调试和通信模组。三者不是竞争关系是分工关系。顺便说一句SPI和CAN总线经常被拿来对比其实完全不是一个层级的东西。CAN总线带仲裁、带错误帧、带自动重发是为多节点、强干扰的车载环境设计的SPI是板内短距离、点对点的芯片互连。车载总线工程师需要懂的技能和单片机工程师刷SPI Flash是两个技术栈别混着学。2. SPI时序的四张面孔CPOL与CPHA组合怎么读波形时序是SPI最容易被忽视、也最容易踩坑的地方。SPI协议规定了两个参数CPOL时钟极性和CPHA时钟相位组合起来就是四种模式Mode 0到Mode 3。很多“数据全错位”“读回来是0x00或0xFF”的问题根子就在这四种模式没对上。2.1 四种模式下的采样边沿到底在哪CPOL决定空闲时SCLK是高还是低CPOL0空闲低电平CPOL1空闲高电平。CPHA决定数据在第几个边沿被采样CPHA0在第一个边沿采样CPHA1在第二个边沿采样。这里的“第一个边沿”指的是从空闲电平跳变出去的那个边沿也叫前沿第二个边沿是回到空闲电平的那个边沿也叫后沿。模式CPOLCPHA空闲电平采样边沿移位边沿Mode 000低上升沿下降沿Mode 101低下降沿上升沿Mode 210高下降沿上升沿Mode 311高上升沿下降沿记这张表有个窍门CPHA0时采样永远发生在“前沿”也就是时钟从空闲状态跳出去的那一下CPHA1时采样发生在“后沿”也就是时钟转回空闲状态的那一下。而CPOL只负责定义哪个边沿是前沿。Mode 0和Mode 3是最常用的两个大部分SPI Flash和传感器支持Mode 0也有器件明确要求Mode 3比如某些射频芯片和ADC。错配模式的典型症状很好认如果主机用Mode 0、从机要Mode 3读出来的数据会整体移位本来是0x5A可能读成0xB4或者别的因为采样点偏了一个半个周期。这时候不用怀疑硬件先拿逻辑分析仪把波形抓出来对着表核对采样点落在哪。2.2 用逻辑分析仪抓波形SPI波形怎么判读光看时序图还是抽象真正调试时必须上逻辑分析仪或者带SPI解码的示波器。我一般的操作顺序是这样先接SCLK、MOSI、MISO、CS四路探头采样率至少是被测时钟频率的10倍以上比如SPI跑10MHz采样率要设到100MHz以上否则边沿会被抹掉。然后触发方式选CS下降沿这样每次通信都能完整抓到一帧。抓到波形后看三个东西。第一CS拉低之后SCLK是不是才开始动CS拉高之前最后一个时钟沿是不是已经走完。如果CS在时钟还没结束就抬起来从机的最后一位就丢了。第二看采样点的位置是落在时钟的高电平中间还是低电平中间这决定了模式对不对。第三看MOSI和MISO的每一位是不是在采样点附近稳定如果数据线在采样点附近还在跳变说明建立保持时间不够要么降速要么查走线。实操心得逻辑分析仪解码SPI时一定要在软件里手动指定CPOL、CPHA和位序MSB/LSB first。自动模式有时候判断不准尤其是位序一旦设反解出来全是乱码会让你误以为硬件有问题。还有一个新手常问的点SPI的位序。绝大多数器件是MSB first也就是最高位先出少数器件支持LSB first。这个在初始化的时候要设对HAL库里对应的是FirstBit参数。设错了字节会整个比特反转0x01变成0x80很好辨认。2.3 SPI时钟频率怎么算出来的SPI的速度不是随便写的它由主机的时钟源分频得到。以STM32为例SPI挂载在APB总线上APB总线的时钟叫PCLKSPI的波特率 PCLK / 分频系数。分频系数通常是2、4、8、16、32、64、128、256这些2的幂。举个具体的例子。假设芯片主频72MHzAPB2总线SPI1挂在这条上也是72MHz你希望SPI跑到9MHz左右。那分频系数 72 / 9 8正好是2的幂选8分频实际波特率 72 / 8 9MHz。如果选4分频就是18MHz可能超出从机承受范围了。选择频率的原则是以从机器件的最大时钟频率为上限然后留出余量。SPI Flash比如GD25Q128E读数据时能跑到80MHz甚至更高但擦除写操作要慢得多而且读状态寄存器时通常限制在低速。传感器类器件往往只能到10MHz甚至1MHz。所以一个稳妥的做法是初始化时先跑低速比如1MHz把器件ID读出来确认通了再根据器件手册把速度提上去。这里有个坑分频系数只能在初始化时配置如果总线上挂了多个最大速率不同的器件要么整个总线按最慢的那个来要么每次切换器件时重新配置SPI的波特率。我一般倾向于后者用一个SPI_SetSpeed函数在片选前动态调整。3. 硬件片选与软件片选一根引脚引出的取舍片选CS/NSS是SPI里看起来最简单、实际上很讲究的一根线。它的作用是“告诉从机现在轮到你了”。因为SPI总线上没有地址多个从机共享SCLK、MOSI、MISO三根线谁被选中谁才响应。片选错了要么多个从机同时驱动MISO打架要么从机根本没理你。3.1 片选信号的物理本质与争用问题片选有效电平通常是低电平也就是CS拉低表示选中。为什么是低有效这是早期TTL逻辑的惯例低电平驱动能力更强、抗干扰更好沿用至今。片选拉低后从机的MISO引脚从高阻态切换成输出态开始往总线上送数据片选拉高后MISO回到高阻态把总线让出来。如果两个从机的片选同时有效两个MISO同时输出一个推高一个拉低轻则数据错乱重则长期短路发热。所以硬件上有个基本纪律任意时刻只允许一个从机的CS有效。软件上要保证片选操作成对出现——拉低、通信、拉高中间不能被打断。我在裸机里见过有人用中断在SPI通信中途去操作另一个SPI器件结果两边片选叠在一起波形上MISO直接糊成一片。3.2 硬件片选和软件片选怎么选STM32的SPI外设带一个硬件NSS引脚可以配置成硬件管理或软件管理。所谓硬件片选就是NSS引脚直接接从机的CS由SPI外设自动在通信时拉低、结束时拉高软件片选则是把NSS功能关掉配置成软件管理自己用任意一个GPIO去手动控制CS。对比项硬件片选软件片选控制方SPI外设自动用户代码手动灵活性差只能一对一高任意GPIO都能当CS多从机不方便需要额外译码天然支持一器件一引脚时序可控性固定不好插延时完全可控可加延时常见用法单从机简单场景绝大多数实际项目实际项目里我几乎都用软件片选。原因很直接一是多从机场景硬件片选基本没法用二是有些器件对CS拉低到第一个时钟沿之间、最后一个时钟沿到CS拉高之间有时间要求比如CS建立时间和保持时间硬件片选不给你插延时的机会软件片选可以在拉低后加几个微秒延时保证器件准备好。三是一些SPI Flash在CS拉高之后需要一段时间完成内部写周期手动控制更清楚。注意配软件片选时一定要记得把SPI的NSS配置成“软件管理”模式否则外设可能因为NSS引脚悬空被误触发导致莫名其妙的通信失败。3.3 SPI Flash、TF卡这些器件的上拉电阻问题经常有人问“TF卡走SPI需要上拉吗”答案是分情况。SPI的四根线上CS和MISO通常建议加上拉电阻一般10kΩ级别。CS上拉是为了保证上电复位期间、主机GPIO还没初始化成输出的时候片选处于无效的高电平避免从机被误选中MISO上拉是为了在从机高阻态时给这条线一个确定的电平防止悬空导致的翻转和误读。MOSI和SCLK一般不需要上拉因为主机始终在驱动它们。但如果走线很长、干扰大也可以加代价是稍微增加功耗和边沿变缓。I2C是必须上拉的开漏结构SPI是推挽结构上拉更多是“保险”性质。TF卡走SPI模式时因为卡座插拔、供电时序复杂我建议CS和MISO都加上拉实测下来能减少不少上电初始化失败的情况。4. STM32 HAL库CubeMXSPI DMA收发完整落地方案讲完原理落到最实际的场景用STM32CubeMX配一个SPI再用HAL库的DMA方式收发。这套流程是我日常开发用得最多的因为DMA能彻底把CPU从逐字节搬运里解放出来。下面以STM32F103为例把配置和代码一步步说清楚。4.1 CubeMX里SPI的关键配置项打开CubeMX选好芯片比如STM32F103C8T6在Connectivity里找到SPI1。模式选Full-Duplex Master因为我们要做主站全双工。Hardware NSS Signal选Disable我们用软件片选。然后在Parameter Settings里配这几个参数。Frame Format选MotorolaData Size选8 BitsFirst Bit选MSB FirstClock Polarity和Clock Phase按从机器件手册来不明确就先用Low和1 Edge即Mode 0。Prescaler就是分频系数先选大一点比如256分频把速度降下来等通了再提。Baud Rate会实时显示出来对着算一下对不对。CRC Calculation关掉除非器件要求。关键在DMA Settings这个标签页。点Add给SPI1_TX加一个DMA请求方向选Memory To Peripheral再给SPI1_RX加一个方向选Peripheral To Memory。优先级都选Medium或High。Mode选Normal还是Circular取决于用途——一次性收发用Normal连续循环采集用Circular。4.2 轮询、中断、DMA三种收发方式随时切换HAL库给了三套收发API各有各的场景。轮询方式HAL_SPI_Transmit/HAL_SPI_Receive/HAL_SPI_TransmitReceive最直白调用后阻塞到收发完适合低速、小数据量、初始化阶段。中断方式带_IT后缀在收发完成时触发回调不阻塞主循环适合中等数据量。DMA方式带_DMA后缀由硬件搬运数据几乎不占CPU适合大数据量、需要高吞吐的场景比如刷SPI屏幕、批量读Flash。方式函数后缀CPU占用适合数据量典型场景轮询无高阻塞几字节读ID、写寄存器中断_IT中几十字节传感器读取DMA_DMA极低几百字节以上屏幕刷新、Flash读写有一点要特别注意HAL_SPI_TransmitReceive_DMA是同时收发适合全双工场景而HAL_SPI_Transmit_DMA只发不收HAL_SPI_Receive_DMA只收不发。很多人读Flash时用HAL_SPI_Receive_DMA结果读出来全是0xFF就是因为接收阶段没有主机去发时钟发空数据从机根本没有时钟可跟随自然不会吐数据。正确做法是用HAL_SPI_TransmitReceive_DMA发送缓冲区填0xFF接收缓冲区收数据。4.3 SPI的DMA模式代码与循环模式那些坑下面给一段实际能跑的读Flash代码骨架用的是TransmitReceive的DMA方式。// 读取SPI Flash数据先发命令地址再读数据 uint8_t cmd[4] {0x03, addr 16, addr 8, addr}; // 0x03是读命令 uint8_t dummy_tx[256]; uint8_t rx_buf[256]; memset(dummy_tx, 0xFF, sizeof(dummy_tx)); // 第一步轮询发送命令和地址 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 4, 100); // 第二步DMA收发TX填0xFF换时钟RX收数据 HAL_SPI_TransmitReceive_DMA(hspi1, dummy_tx, rx_buf, 256); // 等待DMA完成实际项目里建议用回调而不是死等 while (hspi1.State ! HAL_SPI_STATE_READY); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);这里有个大坑while死等DMA完成在裸机里勉强能用但如果这个SPI还在被别的中断或任务使用就容易出问题。更规范的做法是重写HAL_SPI_TxRxCpltCallback回调在回调里拉高CS、置标志位主循环去查标志。这样DMA完成事件和片选操作就不会错位。关于DMA的循环模式Circular它适合“一直在收、缓冲区自动回绕”的场景比如连续采集SPI传感器数据。但循环模式下DMA永远不会停你要自己管理缓冲区的读写指针防止主循环读到正在被DMA改写的区域。我一般用“双缓冲”或者“半满中断”来处理收到半缓冲区满就处理前半段全满就处理后半段这样读写不打架。4.4 SPI到底需要两个DMA吗spi需要两个dma吗是一个高频问题。答案取决于你的通信方向。全双工收发同时进行TX和RX各自需要一条DMA通道所以要两个。如果只发不收一个TX通道就够只收不发而且从机能被外部时钟驱动这种情况很少见一个RX通道就够。STM32的DMA控制器里SPI1的TX和RX请求通常绑定到不同的DMA通道或流上CubeMX会自动分配你按提示配置就行。如果发现只配了一个通道TransmitReceive_DMA会报错或者只工作一半。这也是很多人“DMA收发只收到一半数据”的原因之一。实操心得DMA通道冲突是隐蔽性很高的坑。如果两个外设比如SPI和USART被分到同一条DMA通道上只有一个能工作。CubeMX的DMA Request Mapping界面会标红冲突配置时一定要扫一眼有没有红字。5. SPI FlashGD25Q128E读写例程拆解SPI Flash是把SPI总线用得最极致的器件之一也是最好的学习对象。拿GD25Q128E举例128Mbit也就是16MB容量SPI接口支持标准SPI和四线QSPI。我们先用标准SPI打通读写全流程。5.1 指令集与状态寄存器的正确打开方式GD25Q128E有一套指令集常用就这么几条0x9F读器件ID0x03读数据0x02页写0x20扇区擦除4KB0x06写使能0x05读状态寄存器。每条命令的格式不一样读ID是发一条命令然后读3字节读数据是发命令3字节地址若干字节数据页写是发命令3字节地址最多256字节数据。这里最关键的是状态寄存器。Flash的写和擦除不是瞬间完成的内部有个写周期通常几毫秒。你发了擦除命令后不能在写周期内接着发下一条命令必须轮询状态寄存器的BUSY位bit0等它变0才能继续。// 等待Flash内部操作完成 void Flash_WaitBusy(void) { uint8_t cmd 0x05; // 读状态寄存器命令 uint8_t status; do { HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, cmd, 1, 100); HAL_SPI_Receive(hspi1, status, 1, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); } while (status 0x01); // BUSY位为1就一直等 }5.2 擦除、页写、读数据的完整流程写Flash的铁律是只能把1写成0不能把0写成1。所以改写数据之前必须先擦除擦除会把整个扇区4KB全部置为0xFF。写操作每次最多写一页256字节跨页要分段写。完整的写入流程是写使能0x06→ 扇区擦除0x20→ 等待BUSY结束 → 写使能 → 页写0x02→ 等待BUSY结束。注意每次擦除和写入之前都要重新发写使能命令因为写使能标志会在一次操作完成后自动清除。读流程简单得多发0x03 3字节地址然后用DMA收发把数据读回来读操作不受写周期限制可以跑到很高频率。5.3 时序上的几个硬要求GD25Q128E对时序有明确要求最容易忽略的是CS的建立和保持时间。手册里通常规定CS拉低到第一个SCLK上升沿之间至少要有几个纳秒到几十纳秒的延时最后一个SCLK沿到CS拉高之间也要有类似延时。跑低速时这些时间自然满足一旦跑到几十MHz不加延时就可能违例。软件片选可以在拉低CS后加一个短的忙等或者几个__NOP()把这个时间补上。另一个要求是写操作时的地址对齐。页写的地址如果不是页起始地址低8位不为0跨页时数据会回绕到当前页开头覆盖前面的数据。这是硬件行为不是bug写代码时要做地址对齐判断。6. 常见问题与排查技巧实录这一节是我这些年踩过的坑里挑出来的都是那种“手册不会写、只有实际接板子才会遇到”的问题整理成速查表方便对照。6.1 SPI故障速查表现象可能原因排查方向读回来全是0xFF没发时钟/只Receive没Transmit从机没被选中改用TransmitReceive检查CS读回来全是0x00MISO没接或从机未上电模式错配查MISO连线核对CPOL/CPHA数据整体错位CPHA设反位序设反抓波形看采样点核对FirstBit前几个字节对后面对不上CS在通信中途被拉高速率过高抓CS波形降低分频偶发通信失败CS建立/保持时间不够总线上有多个片选叠加加延时检查片选逻辑DMA只收到一半数据只配了一个DMA通道确认TX/RX各一个通道高速下不稳定走线过长、无终端匹配、地回流差缩短走线降速验证6.2 几个只有实际调试才会懂的细节第一个是“先降速再提速”的调试策略。新手接器件喜欢一步到位配高速结果波形全是问题分不清是接线、模式还是速度。我的习惯是初始化统一256分频跑通了读ID确认模式和接线都对再逐档提速每一档都读ID验证。这样能把问题范围缩小到“速度相关”。第二个是关于总线舵机的经验。总线舵机和普通PWM舵机不一样它内部就是一颗单片机走的是串行总线有的是TTL串口有的是类SPI时序。很多人拿它当SPI器件接发现不对。要点是总线舵机的“总线”通常是单线半双工收发共用一根线靠方向切换来分时收发不能直接接到标准SPI的MOSI/MISO上。如果你要用SPI主站驱动这类器件得额外做电平方向控制和时序适配。第三个是FPGA上的SPI实现。FPGA做SPI主站和单片机思路完全不同没有现成的SPI外设全靠自己写状态机用时钟计数来生成SCLK的各个边沿。好处是时序完全可控可以精确到单个时钟周期想加多少延时加多少。坏处是调试全靠仿真和逻辑分析仪看不到中间变量一旦状态机卡住很难定位。我一般会先写一个小状态机只发固定数据用逻辑分析仪看波形对不对再往上加逻辑。第四个是cubemx stm32103 spi dma接收数据代码这类需求背后最常见的误解。很多人以为“DMA接收”就是配好RX通道然后等数据自己进来。但SPI是被动器件从机不会主动发数据必须主机先给时钟。所以纯“接收”在SPI里没有意义一定要有发送动作去产生时钟。这个认知不建立代码怎么写都是错的。关于APB总线和SPI的关系也值得提一句。SPI的时钟源来自APB总线配置SPI分频之前要先确认APB总线的时钟是多少。STM32F103的SPI1挂APB2SPI2/SPI3挂APB1两条总线的频率可能不一样分频算出来的实际波特率也不同。搞错总线速度就差一倍很多“为什么波特率和算的不一样”都是这个原因。我个人在项目里养成的习惯是任何一个新的SPI器件接入先在纸上把四根线的连接、模式、最大速率、片选逻辑写清楚再上电调试。逻辑分析仪和解码软件常备任何一次通信异常第一反应是抓波形而不是改代码。因为SPI的问题百分之八十能在波形上直接看出来——采样点偏了、CS时序不对、数据线没翻转一目了然。代码可以骗人波形不会。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进