STM32 SPI主从通信实战:从轮询到DMA的完整实现

发布时间:2026/10/4 18:34:37
STM32 SPI主从通信实战:从轮询到DMA的完整实现 做嵌入式这行SPI估计没人能绕开。Flash、TF卡、各种传感器、显示屏、无线模块十有八九都是SPI接口。很多朋友拿到开发板例程一跑通就觉得会了结果自己写两块芯片之间的SPI主从通信时数据死活对不上不是全FF就是错位又不知道从哪排查。这个项目我用一块STM32F103开发板把SPI1配成主机、SPI2配成从机两组SPI引脚直接对接完成一次完整的主机发数据、从机收数据再回数据的双向收发实验。整个实验涉及SPI协议细节、引脚复用、时钟树差异、轮询/中断/DMA三种收发方式的写法对比。做完这个实验你后面再对接任何SPI设备心里都会踏实很多。1. 实验思路与总体方案设计1.1 为什么选“SPI1主机 SPI2从机”的组合先说一个很多人走过的弯路刚开始调SPI时图省事直接把MOSI和MISO短接做SPI1的回环实验。数据自发自收串口一打印诶通了。但这个实验只能证明引脚复用和波特率配置没错有一个特别关键的东西没练到——主从之间的时序配合。真实项目里单片机从来不会自己跟自己通信永远是主机去访问某个从设备。从设备的片选逻辑、MISO输出时机、从模式下的数据移位方式这些只有真正接一个从机才能暴露出来。而用SPI1和SPI2对接就是想在不出板子的前提下模拟一次真实的主从通信。这个组合还有一个隐含的好处STM32F1的SPI1和SPI2挂载在不同的总线上硬件资源、引脚、中断向量、DMA通道全都是独立的。做一次双SPI实验等于把芯片手册里关于SPI外设的大部分内容都过了一遍。1.2 SPI协议核心要点回顾SPI是同步全双工通信物理上最少4根线SCK时钟线由主机产生。MOSIMaster Output Slave Input主机输出、从机输入。MISOMaster Input Slave Output主机输入、从机输出。NSS片选线低电平有效一般一个从机一根。这里有个新手特别容易踩的坑很多人觉得SPI和UART一样发送接接收、接收接发送。**SPI恰恰相反MOSI要接MOSIMISO要接MISO。**从机的“输入”本来就应该接主机的“输出”所以同名引脚直接对接。如果你按串口的习惯交叉去接数据永远出不来。另一个容易被忽略的点SPI没有应答机制。主机发出去一个字节它根本不知道从机有没有收到。I2C好歹有ACKSPI是纯靠时钟把数据移出去从机收没收对主机只能通过读回从机发来的数据来判断。所以这次实验我特意让从机把收到的东西原样返回用这种方式间接验证链路。还有CPOL和CPHA这两个参数决定了时钟空闲电平和数据采样边沿。四个组合对应SPI Mode 0~3主机和从机必须完全一致否则就会出现“偶尔能收到但数据是错位的”这种诡异现象。1.3 SPI1和SPI2在STM32F1上的硬件差异别看都是SPISPI1和SPI2在F103上的待遇完全不同我整理了一张表对比项SPI1SPI2挂载总线APB2APB1最大外设时钟72MHz36MHz默认引脚PA5(SCK)、PA6(MISO)、PA7(MOSI)PB13(SCK)、PB14(MISO)、PB15(MOSI)中断向量SPI1_IRQnSPI2_IRQnTX DMA通道DMA2_Channel3DMA1_Channel5RX DMA通道DMA2_Channel2DMA1_Channel4这个差异最大的影响就是波特率。同一个分频系数SPI1和SPI2实际得到的时钟是不同的。比如都用16分频SPI1在72MHz下能跑到4.5Mbps而SPI2在36MHz下只有2.25Mbps。很多人把SPI1的配置原封不动复制到SPI2上结果发现通信速率和预期不一致然后开始怀疑时钟初始化有问题。其实就是没注意到两个SPI所在的总线根本不一样。做双SPI对接实验时主机速率由SPI1决定但SPI2那边最好明白自己的时钟源是APB1避免后续移植到其他芯片时踩坑。2. 硬件连接与CubeMX工程配置2.1 引脚对接与片选处理我用的是STM32F103最小系统板加杜邦线飞线。两组SPI引脚的位置建议直接对着原理图找比记引脚号更稳妥。实际对接就3根线SPI1主机SPI2从机说明PA5PB13SCK时钟对接PA7PB15MOSI对接PA6PB14MISO对接很多教程会让你把PA4和PB12也接上作为NSS片选。但我这个实验里两个SPI都配置成软件NSS从机内部会自动把片选信号拉高外部NSS引脚不用接。这样接线更简单也少一个排查点。如果你非要用硬件片选那PA4接PB12当然可以但后面会多出很多NSS配置的坑我放在第5章单独讲。注意一点飞线尽量短杜邦线超过10cm之后在高速SPI下容易出现信号完整性问题。第一次跑通实验建议先别追求高速。2.2 CubeMX中的关键参数我用的STM32CubeMX版本不算新但SPI配置界面基本没什么变化。分别选中SPI1和SPI2按下表配置参数SPI1主机SPI2从机ModeFull-Duplex MasterFull-Duplex SlaveData Size8 Bit8 BitClock PolarityLowLowClock Phase1 Edge1 EdgeNSSSoftSoftBaudRate Prescaler3232First BitMSB FirstMSB First这里解释一下几个关键参数的选择时钟极性Low、相位1Edge就是SPI Mode 0这是目前大部分SPI设备默认的模式两边一致就不会出错。波特率预分频选32算下来SPI1是72/322.25MHzSPI2是36/321.125MHz。这个速度足够验证链路又不至于因为布线太长出信号问题。等这个速度跑通了再往下压分频系数逐步提速。还有个容易被忽略的地方时钟树。CubeMX默认配置一般是AHB72MHzAPB136MHzAPB272MHz。如果你手改了时钟树一定要确认APB1不要超过36MHz否则SPI2根本工作不正常。2.3 初始化代码生成后要核对的地方CubeMX生成的代码一般不会错但有几个点值得花30秒核对一下尤其是用旧版本CubeMX或者移植过工程的情况。第一使能时钟。找MX_SPI1_Init()和MX_SPI2_Init()函数前面确认这两个宏存在__HAL_RCC_SPI1_CLK_ENABLE(); __HAL_RCC_SPI2_CLK_ENABLE();第二GPIO配置。F1的SPI引脚在CubeMX里默认配置成复用推挽输出Alternate Function Push-Pull主机从机都一样。可能有朋友会问从机的MOSI明明是输入为什么也配置成复用推挽原因在于GPIO的输入功能不依赖输出配置SPI外设会接管引脚方向所以一个AF_PP配置在主从模式下都能正常工作。第三从机模式的NSS处理。这里有个隐藏很深的细节HAL库在初始化SPI从机并且NSS配置为Soft时会自动把SPI_CR1寄存器的SSI位置1。也就是说从机内部片选一直被拉高外部的NSS引脚根本不影响。如果你在某份代码里看到从机初始化后通信不正常第一步就要查SSI位有没有被正确置1。3. 收发实现轮询、中断与DMA三种方式3.1 阻塞式收发先保证数据链路是通的实验的第一步永远是用最笨、最直观的方式把链路跑通。阻塞式收发就是这样代码好写、逻辑好查就是CPU全程干等效率低。但调试阶段效率低点不是问题先把正确性做出来。主机SPI1这边的代码uint8_t spi1_tx[4] {0xA1, 0xA2, 0xA3, 0xA4}; uint8_t spi1_rx[4] {0}; HAL_StatusTypeDef status HAL_SPI_TransmitReceive(hspi1, spi1_tx, spi1_rx, 4, 100); if (status ! HAL_OK) { // 超时或通信错误在这里加错误处理 }从机SPI2那边uint8_t spi2_tx[4] {0x5A, 0x5B, 0x5C, 0x5D}; uint8_t spi2_rx[4] {0}; HAL_StatusTypeDef status HAL_SPI_TransmitReceive(hspi2, spi2_tx, spi2_rx, 4, 100);重点说一下为什么从机也用TransmitReceive而不是Receive。SPI是双工的每个时钟周期主机移出一位数据的同时必然从MISO移入一位数据。从机如果只接收不发送那它的MISO引脚就一直维持空闲状态主机收到的一堆FF或者00看起来就像“从机没反应”。所以写从机程序除非明确知道从机不用应答否则一律用TransmitReceive时刻准备一份要回给主机的数据。数据链路通没通判断标准很简单。把主机收到的spi1_rx打印出来应该是5A 5B 5C 5D把从机收的spi2_rx打印出来应该是A1 A2 A3 A4。这个结果同时验证了正向链路和反向链路比单SPI回环实验有价值得多。3.2 中断方式收发与回调处理轮询方式最大的问题就是CPU全程死等主从双方数据还没准备好CPU全都空转。改成中断方式收发过程交给SPI外设和NVICCPU可以腾出来干别的事比如处理按键、刷新显示。主机端改成两行volatile uint8_t spi1_done 0; HAL_SPI_TransmitReceive_IT(hspi1, spi1_tx, spi1_rx, 4);然后在中断回调里置标志位void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { spi1_done 1; } else if (hspi-Instance SPI2) { spi2_done 1; } }主循环里轮询这个标志位置1后就说明这一帧收发已经完成可以处理数据了。注意HAL_SPI_IRQHandler()这行它是在中断向量表里被调用的CubeMX生成代码时自动加好了不需要手写但如果你手动移植工程漏掉这一行中断方式会完全失效。实际项目中一般不止一个SPI所以回调里判断hspi-Instance是很常规的操作。还要注意一个细节如果同时开了两个SPI的中断在同一个回调里区分实例时别用hspi hspi1这种比较方式用hspi-Instance SPI1更稳妥因为句柄在初始化后地址是确定的但代码可读性和可维护性差不少。3.3 DMA收发到底需要几个DMA通道这个问题被问过太多次“STM32 SPI收发需要两个DMA吗”标准答案是如果你要同时收发那就需要两个DMA通道一个负责发送一个负责接收如果只是单向传输一个DMA通道就够了。SPI的数据寄存器只有16位每次收发一个字节都要CPU去读写DR寄存器。数据量小的时候无所谓但如果你要刷屏、读写大容量FlashCPU就会被频繁打断。DMA的作用就是让外设和内存直接搬运数据搬运完成后再通知CPU。F103上SPI的DMA通道映射见第1.3节的表。这里说下DMA配置时的两个大坑一是数据宽度。SPI配置成8位数据帧DMA的外设数据宽度必须配成Byte8位内存数据宽度也建议配成Byte否则数据会被拆开或者合并结果完全不对。二是缓冲区大小。DMA传输长度要和SPI收发长度一致比如一次收发64字节DMA就配64。如果配长了DMA会在SPI数据寄存器空闲时反复触发导致死等或溢出。使用HAL库时DMA方式和中断方式代码几乎一样HAL_SPI_TransmitReceive_DMA(hspi1, spi1_tx, spi1_rx, 4);不同的是之前要在CubeMX里把SPI1的TX和RX的DMA请求都enable并分配好DMA通道。DMA传输完成的回调还是用HAL_SPI_TxRxCpltCallback和中断方式是同一个。3.4 三种收发方式选型与对比方式优点缺点适用场景轮询逻辑简单易调试CPU占用率100%初始化验证、短帧低速通信中断不阻塞主流程响应及时每字节都产生中断高频时开销大中低速、帧长不固定的通信DMACPU几乎零负担速度快配置复杂占用DMA通道资源高速、大块数据搬运实际工程里我个人的习惯是调试阶段用轮询功能验证没问题再换DMA。中断方式反而用得少因为DMA的配置复杂度并没有比中断高多少但CPU的占用率却低得多。如果有RTOS环境DMA配合信号量或者事件标志组效果会更好。4. 实验验证与时序分析4.1 串口打印与双向数据校验链路通没通别猜直接看数据。我在代码里加了一个串口打印函数把主机发送数组、主机接收数组、从机接收数组、从机发送数组全部通过USART1打印出来。一次完整的运行输出长这样[HOST TX ] A1 A2 A3 A4 [HOST RX ] 5A 5B 5C 5D [SLAVE RX] A1 A2 A3 A4 [SLAVE TX] 5A 5B 5C 5D这个结果说明两件事一是主机发给从机的数据从机完整收到了二是从机回给主机的数据主机也完整收到了。双向数据校验通过。我建议你在自己的实验里不要只发一组固定数据可以发一组递增序列00 01 02 03 ... FF或者发一串带长度的数据帧这样更容易发现数据错位的问题。比如从机收到的如果是01 02 03 04而主机发的是00 01 02 03那就说明在某个地方丢掉了一个字节多半和起始时序有关。4.2 用逻辑分析仪观察SPI时序数据打印正常了实验基本就算完了。但如果你想真正搞懂SPI逻辑分析仪是必须上的一个工具哪怕是最便宜的24MHz 8通道逻辑分析仪配合开源上位机软件就能把SPI时序看得明明白白。把逻辑分析仪的四个通道分别接到SCK、MOSI、MISO、NSS可选抓一组数据。你会看到这样几个关键特征SCK空闲时是低电平CPOL0传输期间产生8的整数倍个时钟脉冲。在每个SCK的第一个边沿这里是上升沿MOSI上的数据被采样。和CPHA1的配置相比数据采样会相差半个时钟周期表现出来就是同一组数据读出来的bit顺序或者位置不同。MISO的数据变化时刻和MOSI类似只是由从机驱动。如果从机MISO数据一直处于高电平说明从机可能没被选中或者从机压根没开启发送。如果MISO有数据但时间上比主机采样边沿晚了半拍那大概率是CPHA配置不一致。NSS信号如果使能了应该是一段低电平的片选区间。对于我这个纯软件NSS的配置逻辑分析仪上NSS引脚是不动的因为软件NSS根本没有实际引脚输出。这也是为什么我说先别接NSS减少排查变量。5. 常见问题与排查速查5.1 收到全是0xFF或0x00这个现象我在调试SPI时见过无数回。最可能的原因有三个MISO没接好。从机MISO引脚虚接主机从MISO上读到的就是浮空电平一般是高也就是0xFF。重新检查杜邦线。从机没有准备好发送数据。从机那边没有调用TransmitReceive或者从机发送缓冲区是全FF主机会顺理成章地收到FF。从机NSS没有有效选中。如果你用了硬件NSS从机NSS引脚一直为高从机直接罢工。MISO输出高阻主机读到FF。检查一下NSS引脚电平。5.2 数据错位、乱码如果数据长度对得上但内容对不上比如主机发ABCD从机收到BCDA这种错位问题九成出在CPOL/CPHA配置不一致上。主机用了Mode 0从机用了Mode 3两边各自按自己的时钟沿去采样数据自然错位。用逻辑分析仪看一眼SCK空闲电平和采样时刻一下就定位了。另一种情况是数据中间掺了FF。这通常是从机接收速度跟不上DR寄存器还没被读走新数据又来了产生溢出错误。HAL库里有HAL_SPI_GetError接口能查到SPI_ERROR_OVR标志。5.3 从机收不到主机数据先确认从机SPI外设时钟有没有使能。把__HAL_RCC_SPI2_CLK_ENABLE()注释掉的低级错误我犯过不只一次。再看从机模式。有些朋友把SPI1初始化代码直接复制给SPI2忘了把Mode改成SPI_MODE_SLAVE结果两个SPI都想当主机。这种情况下数据线上根本没有主机产生时钟从机自然收不到任何东西。如果两个都是主机模式SCK根本不会由对方产生链路完全是死的因为是同相时钟对着干。还有一个隐蔽的点从机端要及时调用TransmitReceive或使能收发功能。如果从机程序一直没去读DR寄存器RXNE一直为1从机就会停止接收后续数据。这个本质上是对从机外设FIFO的读写节奏管理问题。5.4 硬件片选与软件片选的坑很多SPI从设备芯片比如Flash、传感器必须看到NSS拉低才开始响应。软件NSS和硬件NSS的核心区别在于片选信号由谁产生。硬件NSS模式下主机的NSS引脚由外设自动控制主机开始传输时NSS自动拉低传完自动拉高省心但只适合单从机。从机若配置成硬件NSS必须由外部真正给它一个低电平片选否则从机永远处于不选中状态。软件NSS模式下主机的NSS引脚退化成普通GPIO你想什么时候拉低就什么时候拉低从机的软件NSS则会通过内部SSI位模拟片选配置正确时从机忽略外部NSS引脚。我的建议是调试阶段一律用软件NSS少一个引脚少一个变数。等产品上要接多个从机时再考虑GPIO软件片选或者外部译码器方案每个从机分配一个GPIO控制NSS灵活度最高。5.5 问题快速定位速查表现象最可能的原因快速定位方法全0xFF/0x00MISO虚接、从机无发送数据、从机未选中万用表测MISO电平串口打印从机发送缓冲数据错位CPOL/CPHA不一致逻辑分析仪对比SCK采样边沿数据中间掺FFSPI溢出错误HAL_SPI_GetError检查OVR从机完全无响应从机模式配置错误或时钟未使能检查SPI_CR1的MSTR位、检查RCC时钟寄存器速率和预期不一致APB1/APB2总线频率混淆计算最大时钟PCLK/2我自己做SPI实验最大的体会是别上来就用DMA先把轮询跑通再换中断再上DMA。每一步的改动都要能明确对应到现象的变化。SPI这个外设本身不复杂绝大多数调不通的问题最后都能归结到主从配置不一致、片选没选对、时钟没使能这三类原因上。最后再分享一个实用小技巧调试SPI时准备一组16进制的幻数比如0xA5 0x5A 0xC3 0x3C这种有明显bit特征的字节比用00 01 02 03更容易在误码时一眼看出位偏移。这个习惯我保持了很多年在调各种SPI设备时都帮我省了不少时间。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询