十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32 SPI与DMA通信实战:主从机数据传输与调试要点

STM32 SPI与DMA通信实战:主从机数据传输与调试要点 简介一套面向嵌入式开发者的STM32双机SPI通信例程重点解决两块STM32之间高速数据传输时CPU占用过高的问题。通过DMA通道自动搬运数据主控可以腾出时间处理其他任务显著提高系统整体利用率同时涉及LCD显示场景便于扩展人机交互界面。压缩包共包含111个文件其中34个.h头文件用于接口声明32个.s启动/汇编文件负责芯片初始化32个.c源文件实现驱动逻辑另有uvproj工程文件、map链接映射、dep依赖文件、3个bak备份与3个txt说明文件整体体积仅443KB结构清晰适合直接导入Keil工程查看。例程基于标准外设库组织代码读者可借此理解SPI主从模式配置、DMA请求与中断配合、双机通信时序设计等关键知识点并能直接套用工程模板进行二次开发。目前已有1272人学习下载对正在做嵌入式课程设计、项目实战或希望提升通信效率的开发者来说是一份简洁实用的参考范例。1. 项目拆解与通信架构设计做嵌入式开发的朋友多半会碰到这样的需求一块板子上的主控获取到数据需要把数据分发给另一块板子上的执行单元或者要在两块STM32之间做双向数据交换。常见的选项是串口UART、I2C、SPI、CAN而其中SPI DMA的组合在需要高速、大批量传输数据的场景下几乎是绕不开的经典方案。这篇文章就围绕“两块STM32之间通过SPI通信并引入DMA来完成数据传输”这个具体项目来展开拆解我从方案设计到上板调试的完整过程。1.1 主从角色与通信模式怎么定两只STM32做SPI通信首先要回答的一个问题是谁是主机谁是从机SPI协议本身是主从架构由主机产生SCK时钟信号从机被动跟随。这个角色分配通常在硬件设计阶段就要定下来不能像串口那样想切换就切换。我的项目场景是一块主控板收集传感器数据解析处理后周期性发送给一块显示板做数据刷新。所以我直接把主控板设为SPI主机显示板设为SPI从机。这样主机掌握通信节奏想发就发从机准备好接收缓冲区等着就行。如果你的场景是双向对等通信最简单可靠的做法仍然是固定一方做主机、一方做从机通过协议约定谁先发起、谁后应答而不是在运行过程中动态切换主从角色——动态切换在SPI里不仅实现复杂而且极易引入总线冲突问题。确定角色之后还需要明确通信模式。SPI是全双工接口主机发数据的同时也一定能收到从机发来的数据从机接收的同时它的MISO线上也会把数据返回给主机。这里就要稍微注意一下很多新手在用SPIDMA做主从通信时会想在主机端只开发送DMA、从机端只开接收DMA事实上这完全可以工作只要双方约定好数据长度和处理逻辑即可。1.2 为什么非要用DMA传输SPI本身就已经比UART快很多了在STM32F103这类主流芯片上SPI时钟可以跑到18MHz在更高主频的G系列或者H系列上跑到几十MHz也算家常便饭。那么问题来了既然SPI这么快为什么还要引入DMA直接原因很简单通信速率越高CPU被占用的时间就越长。一个典型的SPI发送流程每发一个字节要等待TXE位被置位然后在发送数据寄存器里填入下一个字节。如果你传输的数据量是1KB或者更大CPU在这段时间内几乎什么事情都干不了所有精力都耗在“等待发送寄存器空闲”这个循环里。如果系统里同时还有定时器中断、ADC采样、数据处理任务那其中任何一个优先级安排不当就会出现SPI传输空洞通信质量直接崩掉。DMA解决的就是这个“CPU干等”的问题。DMA控制器是独立于Cortex-M内核之外的一个硬件外设它可以在内存和外设之间直接搬运数据不需要CPU逐字节参与。CPU要做的只是配好DMA通道、指定数据源地址和目标地址、指定要传输的数据长度然后启动传输。剩下的SPI时钟产生、逐字节发送、接收端逐字节存入内存这些事情全部由DMA和SPI硬件自动完成传输完成后DMA会置一个传输完成标志或触发中断告知CPU。这个机制实际上就是把“CPU逐字节搬运数据”这个环节外包给了专职的硬件搬运工CPU只负责收发两端的总调度。2. 核心原理与关键参数解析2.1 SPI协议的关键参数与时序行为在做任何代码之前我建议先把SPI协议的关键参数理清楚否则后续调试时数据收到一堆乱码你根本不知道问题出在哪儿。SPI有四根核心信号线SCK时钟、MOSI主机输出从机输入、MISO主机输入从机输出、NSS片选。其中NSS有多种用法在只用一根片选线连接两块芯片的场合选“软件片选”管理方式就够了——由程序控制GPIO的电平来选中从机不用硬件NSS复用功能在需要连接多个从机的场合软件片选的优势会更明显因为各从机的片选管脚可以随便分配GPIO不占用固定的NSS引脚。决定SPI通信格式的是CPOL时钟极性和CPHA时钟相位两个参数。CPOL决定空闲时SCK的电平状态CPHA决定数据在SCK的上升沿还是下降沿被采样。这两个参数组合出SPI Mode 0~3。我在项目中用的是Mode 0即CPOL0、CPHA0空闲时SCK为低电平数据在上升沿被采样。这同时也是大多数SPI从设备默认的工作模式。需要注意的是不是说STM32的SPI速率标称有18MHz或更高你就能在实际通信中用满这个速率。线缆长度、PCB走线、两板之间的电平匹配都会制约实际能达到的速率。我实际测试中两块板子之间用约10cm杜邦线连接SPI时钟设为4.5MHz时波形稳定再往上到9MHz就能看到明显的数据抖动。这个细节在项目规划阶段就要心里有数。2.2 DMA的工作原理与触发方式DMA听起来高大上实际上理解起来并不复杂。STM32内部DMA控制器有多条通道具体数量看型号F103有DMA1和DMA2共12条通道G0系列则是DMA1共8条通道每个SPI外设的发送和接收事件会映射到对应的DMA通道上。在SPI传输方向上有两种常见的DMA请求一种是“SPI发送数据寄存器为空”这是请求DMA把内存中的数据搬运到SPI的数据寄存器另一种是“SPI接收数据寄存器非空”这是请求DMA把SPI数据寄存器中的值搬运到内存中指定的缓冲区。DMA正是在这些硬件事件触发下工作的整个过程CPU全程不干预。这里要强调一个容易踩坑的细节对于STM32的SPI外设当使能SPI时如果配置了SPI的接收DMA通道那么SPI外设会立刻产生一次接收事件并触发DMA请求。这意味着从机在初始化接收DMA时如果还开着中断可能一开始就收到一个无意义的数据。很多人的做法是先把SPI接收DMA通道使能再打开SPI总开关或者先让中断屏蔽等到真正传输时再介入。这个顺序一定要在设计时想清楚。还有一个重要概念就是DMA循环模式Circular Mode。DMA通道可以配置为Normal模式即传输完指定数量字节后停住需要再次软件启动也可以配置为Circular模式即数据长度到了之后自动回卷到起始地址继续传输适合做连续采集或者连续接收。我的项目里主机发固定长度的数据帧所以用的是Normal模式从机接收也是Normal模式每收到完整一帧后触发一次DMA传输完成事件。如果你的场景是持续的数据流比如SPI接口的麦克风或者ADC连续采样那DMA Circular模式会更合适甚至可以用DMA双缓冲配合乒乓操作一边填充数据一边处理数据效率更高。3. 硬件连接与工程初始化配置3.1 引脚分配与接线方案这个项目的硬件连接非常直接。主机和从机之间需要连接四根线SCK对SCK、MOSI对MOSI、MISO对MISO、GND对GND。MCU之间通信必须共地否则电平参考点不一致数据线上的信号漂移足以让通信完全不可用。我用的是STM32F103C8T6蓝色板级别的场景SPI1的引脚分配如下PA5为SCKPA7为MOSIPA6为MISO片选用PA2随意指定。从机这边也同样把对应的SPI1引脚接出来。有人可能会问为什么从机也需要接MOSI和MISO因为SPI是全双工协议虽然此次业务主要数据流向是主机到从机但为了后续容易扩展反向确认包接线时一次把全双工都接好成本和功夫多不了多少后面调试时反而方便。关于片选我使用的是软件片选方式主机和从机程序里都把NSS引脚配置为普通推挽输出GPIO主机发送数据前置低选中从机发送完成后拉高释放从机。从机的NSS引脚也接到这个GPIO上用于通知从机“准备接收一帧数据”。3.2 STM32CubeMX配置要点我习惯先用STM32CubeMX生成工程骨架再在生成的代码上调细节。打开CubeMX选择具体型号后按下面步骤配置将SPI1选为Full-Duplex Master模式配置参数CPOLLow、CPHA1Edge、数据宽度8bit、预分频根据所需时钟计算。比如系统时钟72MHz预分频16SPI时钟4.5MHz。将从机的SPI1选为Full-Duplex Slave模式同样参数配置。主机和从机分别将触发SPI发送和接收的DMA通道打开主机开SPI1_TX的DMA从机开SPI1_RX的DMA。DMA参数设为内存到外设、Normal模式、数据宽度字节一致并开启传输完成中断。这里有个值得注意的细节CubeMX生成的DMA初始化代码中DMA句柄与外设句柄的关联是通过__HAL_LINKDMA()宏完成的。如果你在调试时发现DMA和SPI句柄看起来各自独立怀疑两者有没有真正关联起来就去找这个宏确认它绑定了hdma_tx和hspi1的hdmatx字段以及hdma_rx和hspi1的hdmarx字段。很多人代码跑不通就是在这里新建了一个DMA句柄但没绑定进外设句柄里。4. 代码实现与核心传输流程4.1 主机端发送DMA代码实现CubeMX生成的工程中DMA和SPI的初始化已经就绪。关键是在应用层要正确调用HAL_SPI_Transmit_DMA这个函数。主机的发送逻辑大致长这样uint8_t tx_buffer[64]; uint8_t rx_buffer[64]; void SPI_SendFrame_DMA(uint8_t *data, uint16_t len) { HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET); // 拉低片选 HAL_SPI_Transmit_DMA(hspi1, data, len); // DMA方式发送 // 片选释放的操作放在DMA传输完成回调里做 } void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET); // 拉高片选 } }片选信号不要在调用HAL_SPI_Transmit_DMA之后立即拉高因为DMA传输是异步的函数返回的时候数据只是“开始传输”并没有“完成传输”。如果立刻拉高片选从机会认为帧提前终止了。片选释放的正确时机是在发送完成回调中操作这保证了从机收到的整帧数据是完整的。4.2 从机端接收DMA代码实现从机端的代码逻辑更简单因为从机不需要主动发起传输只要在初始化阶段就开启DMA接收等数据到了即可。核心代码如下uint8_t rx_buffer[64]; volatile uint8_t frame_ready 0; void SPI_StartReceiveDMA(void) { HAL_SPI_Receive_DMA(hspi1, rx_buffer, 64); } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { frame_ready 1; // 置位标志主循环处理 HAL_SPI_Receive_DMA(hspi1, rx_buffer, 64); // 继续开启下一轮接收 } }有个点我要特别说明使用HAL库的SPI从机DMA接收时有个高频问题如果从机在接收过程中又来了新的SCK时钟但此时DMA还没来得及重新启动从机的移位寄存器会被新时钟驱动数据被覆盖但不会再次触发接收完成事件。实际表现就是第一帧收得好好的第二帧开始就丢了。我的经验是收到一帧后尽量缩短从“接收完成”到“重新开启接收”之间的时间窗口在中断回调里立刻开启下一次接收如果数据帧间隔太长从机端的RXNE状态需要做额外清理否则会大概率出现第二帧接收无效。4.3 实际通信测试的完整代码流程一次完整的通信流程可以归结为以下几个步骤主机和从机上电各自完成时钟、GPIO、SPI、DMA初始化。从机调用SPI_StartReceiveDMA()让接收DMA通道处于使能状态等待SCK。主机把待发送数据填入tx_buffer数组调用SPI_SendFrame_DMA()。主机SPI外设产生SCK时钟TX DMA把tx_buffer中的数据不断填入发送数据寄存器从机在SCK驱动下RX DMA把接收数据寄存器中的值不断搬到rx_buffer。主机发送完64字节触发HAL_SPI_TxCpltCallback拉高片选。从机接收完64字节触发HAL_SPI_RxCpltCallback置位frame_ready并立刻重新开启下一轮接收。从机主循环检测到frame_ready为1时处理rx_buffer中的数据。流程上并不复杂真正复杂的是边界条件和异常行为。我在项目里还加了一个简单的帧校验机制固定帧头两个字节为0xAA55帧尾加一个8位累加和校验值。发送端填充数据时计算好校验接收端收到后校验不匹配直接丢弃并统计错误计数。这样一个轻量级的协议头尾在调试阶段帮了大忙能把“SPI没通信上”和“SPI通信了但数据不对”区分出来。5. 通信调试与常见问题排查实录5.1 排查思路一数据完全收不到先看硬件再看DMA使能顺序我在开发中遇到的第一个现象是主机发送完成标志正常触发但从机端DMA接收完成标志从来不触发。用示波器量主机SCK引脚波形确实存在。顺着这个现象排查最后发现原因出在从机SPI初始化后没有及时开接收DMACubeMX默认生成的代码里可能没有在初始化完成后自动调用HAL_SPI_Receive_DMA这一步确实需要应用层主动调用。解决方法是SPI和DMA初始化完成后立刻调用一次HAL_SPI_Receive_DMA把接收通道挂上之后再进主循环。这里还要提醒一个极易踩的坑对于SPI从机初始化的顺序应该是DMA接收通道先使能再使能SPI。如果反过来SPI已经使能了但DMA还没有开始接收主机的任何时钟活动都会让从机的RXNE置位DMA没接住数据就丢了。更麻烦的是SPI的RXNE标志如果不清除下次即使DMA通道打开了也可能因为溢出错误导致接收不工作。5.2 排查思路二数据能通但有错位、开头多一个字节另一个常见现象是从机收到的数据整体上内容是对的但开头莫名其妙多了一个字节或者第一个字节丢失后续数据整体左移。这种问题几乎都出在片选时序或者SPI模式配置不一致上。开头多一个字节的场景通常是从机在SPI初始化后由外设产生的第一个接收事件这个我在前面说过没有处理好DMA把第一个垃圾数据也当成了有效数据。解决办法是从机的主循环在上电后先做一次SPI接收DMA清理丢弃第一帧残留或者让主机在正式数据前不发任何时钟等从机准备好了再启动。开头丢一个字节的场景常见原因是软件片选拉低后主机立刻就开始SCK数据传输但此时从机的DMA通道还没能从“写GPIO拉低片选”的软件操作中切过来。虽然GPIO置低和DMA通道就绪都是微秒级操作但在高速SPI下这个时间差就直接反映成了第一个字节丢失。解决方法是主机拉低片选后做几条NOP指令或者一个空循环小延时给从机一点准备时间再启动DMA发送。这个问题如果换成硬件NSS自动管理时序会严格很多因为NSS的拉低和SPI引擎是同步的。但在大多数简单的两板通信场景里软件片选配合一个小延时已经完全够用硬件NSS的反作用是在多从机复杂应用上体现出来的。5.3 常见问题速查与避坑建议问题现象可能原因解决建议从机DMA接收从不触发没有主动调用HAL_SPI_Receive_DMA初始化后立即开启接收DMA接收数据整体错位主从机SPI模式不一致CPOL/CPHA不匹配用示波器量SCK和MOSI对照数据手册确认采样沿第一帧正常后续帧丢失从机重新开启DMA耗时过长在RxCplt回调中立刻重新开启接收或使用DMA双缓冲连续接收开头多一个字节从机SPI使能后RXNE残留被DMA搬走初始化后主动清RXNE或丢弃第一帧高速下数据出现偶发错误线长、电平匹配问题降低SPI时钟缩短接线或加外部电阻匹配DMA中断触发但数据不全DMA传输长度配置和实际数据长度不一致统一两端数据长度配置调试的过程我也分享一个经验与其在代码里反复猜测不如一开始就用逻辑分析仪或示波器抓一遍SCK、片选、MOSI三根线的时序看看波形和数据是不是符合预期。SPI时序图看懂了很多问题都不需要反复烧录测试才能定位。没有示波器的话可以先降低SPI时钟到几百kHz人为放大时序问题让片选和数据线的先后关系更加明显再逐步排查。另外关于Keil和调试工具我用Keil MDK配合ST-Link调试这种项目可以设置断点在DMA中断回调里观察接收缓冲区数据一旦发现缓冲区内容和发送端不一致马上确认是哪一根线的问题。如果要观察DMA寄存器的状态直接在调试界面的Peripherals菜单里找DMA控制器看通道的CNDTR寄存器这个寄存器直接反映还剩多少字节没传完排查“传输卡住”类问题非常直观。6. 实操过程中的心得与后续扩展建议6.1 代码架构的一点心得在实际项目里我建议把SPIDMA通信封装成独立模块对外提供发送和注册回调接口不要在应用层直接操作DMA句柄。我之前在一版代码里为了图快在业务逻辑里直接调用HAL_DMA_Start_IT结果后续想加一个“发送完成置位状态”的功能就不得不把所有调用点翻出来逐个改。后来重构成了类似这样的小接口void SpiDma_Init(void); void SpiDma_SendFrame(uint8_t *data, uint16_t len); int SpiDma_IsFrameReady(void); uint8_t *SpiDma_GetRxBuffer(void);这样一来业务层和底层通信逻辑解耦开后续无论从SPI换到UART底层做好接口适配就行应用层的调用方式基本不用动。使用DMA句柄时需要注意HAL库的DMA句柄和外设句柄是强关联的不要在接口外面随意调用HAL_DMA_Start这类底层函数避免和HAL_SPI_Transmit_DMA内部的DMA操作互相干扰。6.2 项目扩展的一个方向这个项目如果继续延展比较自然的两个方向是一是把两块板子从“主发”“从收”升级为双向通信主机和从机都开TX和RX两条DMA通道用DMA双缓冲实现全双工二是把通信的数据帧从纯裸数据升级为带帧头、长度、帧尾、CRC的通信协议提高抗干扰能力。这两个方向我在项目里已经做了预留回调函数和接收缓冲区的设计都按可扩展的方式写的后续改动的成本并不高。最后说一个我在实际操作中最深刻的体会SPI本身并不复杂SPIDMA也不是什么高深技术但这个组合非常考验对硬件时序的理解和对“异步”这个概念的真切把握。片选什么时候拉低、DMA什么时候真正完成、从机什么时候准备好这些环节在代码里可能只是几行在实测中每一个细节都可能成为通信失败的元凶。把这些问题从上到下过一遍SPIDMA这套组合在你的项目里基本就能做到心里有底了。本文还有配套的精品资源点击获取
返回列表