资讯动态

GD32 SPI全双工DMA配置实战:从原理到避坑指南

发布时间:2026/9/28 18:02:01 来源:尧图企业网站定制
1. 为什么SPI全双工配DMA是GD32项目里的硬骨头SPI本身是个很朴素的协议四根线一摆时钟一推数据就出去了。但一旦把速率拉高、数据量拉大再叠加上全双工这个要求事情就变得微妙起来。我见过太多项目在调试阶段用轮询方式跑SPI跑得好好的一上量产、一提高吞吐量CPU占用率直接飙到七八十主循环里其他任务全被拖垮。这时候DMA就成了绕不开的选择。GD32的SPI外设和STM32在寄存器层面高度相似但细节上又有不少自己的脾气。全双工模式下SPI的发送和接收是同时进行的——你往发送寄存器写一个字节接收寄存器里就会同步收到一个字节。这个特性决定了DMA的配置不能只做单向搬运必须收发两条通道同时开。很多人在这一步栽跟头只配了TX的DMARX用中断或者轮询去读结果高速率下要么丢数据要么OVR溢出标志频繁置位。这篇内容适合两类人看一类是刚接触GD32、想把SPIDMA跑通的嵌入式新手另一类是在项目里被SPI通信稳定性折磨过、想搞清楚底层机制的老手。我会从外设触发机制讲起把DMA请求映射、全双工收发通道配置、主从机角色差异、实测中的坑和排查方法全部拆开讲。代码基于GD32标准外设库但思路对HAL库同样适用。注意全双工SPI的DMA配置TX和RX必须同时使能且RX通道要先于TX通道使能否则第一个字节可能丢失。2. GD32的SPI与DMA请求映射关系拆解2.1 SPI外设的DMA请求是怎么产生的要理解DMA怎么搬SPI的数据得先搞清楚DMA请求是谁发出的。GD32的SPI外设内部有两个标志位TBE发送缓冲空和RBNE接收缓冲非空。当TBE置位时说明发送数据寄存器空了可以往里写新数据当RBNE置位时说明接收数据寄存器里有新数据等着被读走。DMA控制器就是盯着这两个标志位干活的。SPI的DMA发送请求连接到DMA控制器的某个通道接收请求连接到另一个通道。以GD32F103系列为例SPI0的TX请求映射到DMA0的通道3RX请求映射到DMA0的通道2。这个映射关系是硬件固定的不能随便改选错了通道DMA就不会触发。这里有个容易混淆的点GD32不同系列的DMA请求映射表不完全一样。F103和F303的映射就有差异F4系列更是换了一套DMA架构。所以你在移植代码的时候第一件事就是翻对应型号的用户手册找到DMA请求映射表确认SPI_TX和SPI_RX分别挂在哪个DMA的哪个通道上。2.2 全双工模式下收发通道的协同逻辑全双工SPI的DMA配置有个反直觉的地方接收通道要先使能。原因是这样的——SPI是全双工你发一个字节出去的同时就会收一个字节进来。如果TX通道先使能数据已经开始往外推了但RX通道还没准备好第一个收到的字节就会因为没人搬走而导致RBNE溢出。虽然不至于让整个通信崩溃但数据流会错位一个字节对于协议解析来说是致命的。正确的顺序是配置RX DMA通道设置好接收缓冲区地址和长度先使能RX通道配置TX DMA通道设置好发送缓冲区地址和长度再使能TX通道最后使能SPI的DMA发送和接收请求位SPI_CTL0寄存器里的DMATEN和DMAREN这个顺序我在多个项目里验证过RX先行使能是保证第一个字节不丢的关键。如果你用的是HAL库HAL_SPI_TransmitReceive_DMA内部会处理这个顺序但标准库需要自己控制。2.3 DMA通道优先级与仲裁的实战影响GD32的DMA控制器支持通道优先级配置有低、中、高、超高四档。SPI全双工场景下TX和RX两个通道同时工作如果优先级配置不当会出现某一方向的数据流被压制的情况。我的经验是RX通道优先级设高TX通道设中或低。理由很直接——接收是被动方数据来了你不及时搬走就会溢出发送是主动方稍微晚一点写数据寄存器问题不大SPI时钟会自然等待。把RX优先级提上去能显著降低高速率下的溢出概率。另外DMA仲裁器在多个通道同时请求时会按优先级裁决。如果你系统里还有ADC、UART等其他DMA通道在跑更要留意SPI_RX的优先级不能太低否则在总线繁忙时接收数据容易被延迟搬运。3. 主从机全双工DMA配置的差异与实操3.1 主机模式下的时钟与片选控制主机模式下SPI负责产生时钟和片选信号。用DMA搬运数据时片选的管理有两种做法硬件片选和软件片选。硬件片选由SPI外设自动控制NSS引脚传输开始拉低传输结束拉高。听起来省事但实际项目里我不太推荐。原因是硬件片选的时序是固定的在多从机场景下切换不够灵活而且有些GD32型号的硬件片选在DMA传输结束时的拉高时机和预期有偏差。软件片选就是自己用GPIO控制传输前拉低DMA传输完成中断里拉高。这种方式可控性强适合多从机切换。但要注意拉高片选的时机必须在DMA发送完成之后而不是发送缓冲区空的时候。DMA发送完成中断传输计数器归零才代表最后一个字节真正发出去了此时拉高片选才安全。主机时钟配置方面GD32的SPI时钟分频系数从2到256可选。实际选型时不要只看理论最大值要结合从机器件手册里的最高时钟频率、PCB走线质量、以及DMA搬运速度综合决定。我一般会先用较低分频跑通再逐步提高同时用逻辑分析仪抓波形确认没有畸变。3.2 从机模式的DMA响应机制从机模式下SPI的时钟由主机提供从机只管在时钟边沿采样和输出数据。从机用DMA的难点在于你无法预知主机什么时候开始传输。所以从机的RX DMA通道必须一直处于使能状态随时准备接收。从机TX方向稍微复杂一些。如果主机发来的数据长度不固定从机怎么知道该准备多少数据发回去常见的做法是预填充发送缓冲区DMA传输长度设成最大可能长度然后在传输完成中断里根据实际接收到的数据量做处理。另一种做法是用SPI的接收事件触发发送但GD32的SPI没有这种硬件联动需要软件介入。从机模式还有一个坑NSS引脚的管理。从机的NSS必须由主机控制如果从机NSS配置成软件模式且一直拉低从机会误以为一直被选中。正确做法是从机NSS配置成硬件模式由主机片选信号驱动。3.3 收发缓冲区设计与数据对齐DMA搬运的数据宽度可以配置为8位或16位。SPI数据帧长度也可以配成8位或16位。这两者必须匹配否则数据会错位。我见过一个典型的错误SPI配成8位数据帧DMA配成16位搬运结果每两个字节被合并成一个16位数据接收缓冲区里的数据全是乱的。排查了半天才定位到这个问题。缓冲区设计上建议收发各用一个独立的数组大小根据协议最大帧长来定。如果数据量很大可以考虑用双缓冲DMA双缓冲模式一个缓冲区在搬运时另一个缓冲区供CPU处理实现流水线操作。GD32的DMA支持双缓冲模式配置稍微复杂一些但在高吞吐场景下很值得。/* GD32 SPI0 全双工DMA配置示例主机模式 */ #define SPI_TX_BUFFER_SIZE 64 #define SPI_RX_BUFFER_SIZE 64 uint8_t spi_tx_buffer[SPI_TX_BUFFER_SIZE]; uint8_t spi_rx_buffer[SPI_RX_BUFFER_SIZE]; void spi_dma_full_duplex_init(void) { /* 使能DMA和SPI时钟 */ rcu_periph_clock_enable(RCU_DMA0); rcu_periph_clock_enable(RCU_SPI0); /* 配置DMA发送通道 - SPI0_TX映射到DMA0通道3 */ dma_deinit(DMA0, DMA_CH3); dma_init_struct.direction DMA_MEMORY_TO_PERIPHERAL; dma_init_struct.memory_addr (uint32_t)spi_tx_buffer; dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.memory_width DMA_MEMORY_WIDTH_8BIT; dma_init_struct.number SPI_TX_BUFFER_SIZE; dma_init_struct.periph_addr (uint32_t)SPI_DATA(SPI0); dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_8BIT; dma_init_struct.priority DMA_PRIORITY_MEDIUM; dma_init(DMA0, DMA_CH3, dma_init_struct); /* 配置DMA接收通道 - SPI0_RX映射到DMA0通道2 */ dma_deinit(DMA0, DMA_CH2); dma_init_struct.direction DMA_PERIPHERAL_TO_MEMORY; dma_init_struct.memory_addr (uint32_t)spi_rx_buffer; dma_init_struct.memory_inc DMA_MEMORY_INCREASE_ENABLE; dma_init_struct.memory_width DMA_MEMORY_WIDTH_8BIT; dma_init_struct.number SPI_RX_BUFFER_SIZE; dma_init_struct.periph_addr (uint32_t)SPI_DATA(SPI0); dma_init_struct.periph_inc DMA_PERIPH_INCREASE_DISABLE; dma_init_struct.periph_width DMA_PERIPHERAL_WIDTH_8BIT; dma_init_struct.priority DMA_PRIORITY_HIGH; dma_init(DMA0, DMA_CH2, dma_init_struct); /* 先使能接收通道再使能发送通道 */ dma_channel_enable(DMA0, DMA_CH2); dma_channel_enable(DMA0, DMA_CH3); /* 使能SPI的DMA收发请求 */ spi_dma_enable(SPI0, SPI_DMA_TRANSMIT); spi_dma_enable(SPI0, SPI_DMA_RECEIVE); }4. 实测中那些手册不会告诉你的坑4.1 第一个字节丢失的完整排查链路现象主机发送8字节数据从机接收到的第一个字节是0x00或者上一个传输的残留值后面7个字节正常。排查过程第一步确认RX DMA通道是否先于TX使能。我最初的代码是先使能TX再使能RX改成RX先行使能后问题依然存在。第二步用逻辑分析仪抓SPI的MOSI和SCK波形。发现主机发出的第一个字节在MOSI上是正确的但从机接收缓冲区里就是不对。说明问题出在从机侧。第三步检查从机的SPI初始化代码。发现从机的SPI使能SPI_CTL0的SPIEN位是在DMA配置之前做的。SPI已经使能了但DMA还没配好此时如果主机开始传输第一个字节就会因为DMA没准备好而丢失。修复方法先配置DMA再使能SPI。SPI使能放在最后一步确保DMA通道已经就绪。这个顺序在主机侧同样适用。提示SPI使能位SPIEN的置位时机很关键一定要在所有DMA配置完成之后。4.2 DMA传输完成中断与SPI忙状态的时序陷阱DMA传输完成中断触发时DMA的传输计数器已经归零但这不代表SPI的最后一个字节已经发送完毕。SPI的发送过程是数据从发送缓冲区搬到移位寄存器然后一位一位地移出去。DMA传输完成只说明数据都搬到了发送缓冲区移位寄存器里可能还有最后一个字节在发送中。如果你在DMA传输完成中断里立刻拉高片选或者关闭SPI最后一个字节就会不完整。正确的做法是在DMA传输完成中断里等待SPI的TRANS传输完成标志或者TBE和BUSY标志都清零再执行后续操作。void DMA0_Channel3_IRQHandler(void) { if(dma_interrupt_flag_get(DMA0, DMA_CH3, DMA_INT_FLAG_FTF)) { dma_interrupt_flag_clear(DMA0, DMA_CH3, DMA_INT_FLAG_FTF); /* 等待SPI最后一个字节真正发送完成 */ while(spi_i2s_flag_get(SPI0, SPI_FLAG_TRANS) RESET); /* 此时才安全地拉高片选 */ gpio_bit_set(GPIOA, GPIO_PIN_4); } }4.3 高速率下的OVR溢出与DMA搬运延迟当SPI时钟超过一定频率DMA的搬运速度可能跟不上SPI的接收速度导致RBNE标志还没来得及被DMA清除下一个字节就来了OVR溢出标志置位。这个问题的根源通常有两个一是DMA通道优先级太低被其他通道抢占了总线二是系统时钟配置有问题DMA时钟和SPI时钟的比例不协调。我的处理经验是先把SPI时钟降下来验证功能确认DMA配置无误后再逐步提高SPI时钟同时用示波器观察OVR标志是否置位。如果提高到一个频率后开始出现OVR说明已经到了DMA搬运的极限要么降低SPI时钟要么优化DMA优先级和总线占用。另外GD32的DMA和CPU共享总线矩阵如果CPU频繁访问内存DMA的搬运效率会受影响。在SPI DMA传输期间尽量减少CPU对内存的大批量访问能明显改善高速率下的稳定性。4.4 主从机时钟极性与相位的匹配验证SPI的时钟极性CPOL和时钟相位CPHA决定了数据在时钟的哪个边沿采样和输出。主从机必须配置一致否则数据全是错的。我遇到过一次诡异的情况主机CPOL0、CPHA0从机也是CPOL0、CPHA0但通信就是不稳定偶尔错一两个字节。后来用逻辑分析仪仔细看波形发现从机的实际采样边沿和主机不一致。原因是从机的SPI初始化代码里CPOL和CPHA的配置位写反了顺序虽然最终寄存器值看起来一样但中间过程产生了毛刺。排查方法用逻辑分析仪同时抓SCK、MOSI、MISO三根线对照SPI时序图确认数据在正确的时钟边沿上稳定。如果边沿对不上先检查CPOL/CPHA配置再检查GPIO的复用功能配置是否正确。5. 从轮询到DMA的迁移策略与性能对比5.1 什么场景下值得上DMA不是所有SPI应用都需要DMA。如果你的SPI通信数据量很小比如每次几个字节传输频率也不高轮询方式完全够用代码还简单。但以下几种场景DMA几乎是必选项单次传输数据量超过16字节且传输频率较高CPU需要同时处理其他任务不能长时间阻塞在SPI等待上需要实现高速连续数据传输比如SPI Flash读写、ADC数据采集系统对功耗敏感希望CPU尽快完成搬运后进入低功耗模式我做过一个对比测试GD32F103主频72MHzSPI时钟18MHz传输256字节数据。轮询方式下CPU占用时间约120微秒DMA方式下CPU只需要配置和启动占用时间不到5微秒剩下的时间可以处理其他逻辑。数据量越大DMA的优势越明显。5.2 迁移过程中的代码改造要点从轮询迁移到DMA代码改动主要集中在三个地方第一初始化部分增加DMA通道配置。注意DMA请求映射要和SPI外设对应通道优先级要合理设置。第二数据传输函数从写一个字节等一个字节改成配置DMA、启动传输、等待完成中断。这里要注意传输完成中断里不能做太耗时的操作否则会影响下一次传输的启动。第三错误处理要增加DMA相关的标志检查比如传输错误标志、FIFO错误标志等。轮询方式下这些错误通常不会出现但DMA方式下如果配置不当这些标志会频繁置位。5.3 实测性能数据与优化空间以下是我在GD32F103平台上的实测数据SPI时钟18MHz全双工模式收发各256字节传输方式CPU占用时间传输总耗时最大稳定速率轮询约120us约120us18MHzDMA单缓冲约5us约115us18MHzDMA双缓冲约3us约115us18MHz从数据看DMA主要节省的是CPU占用时间传输总耗时受限于SPI时钟本身提升不大。但CPU省下来的时间可以用于其他任务系统整体吞吐量提升明显。优化空间主要在两个方面一是提高SPI时钟频率但受限于从机器件和PCB质量二是使用DMA双缓冲让数据搬运和数据处理并行适合连续数据流场景。6. 调试工具与波形验证的实战方法6.1 逻辑分析仪抓SPI波形的正确姿势调试SPIDMA逻辑分析仪是必不可少的工具。抓波形时要注意几点采样率要足够高至少是SPI时钟的4倍以上。比如SPI时钟18MHz逻辑分析仪采样率至少要72MHz否则波形会失真边沿判断不准。触发条件设置成片选信号的下降沿这样能抓到完整的传输过程。如果片选是软件控制的触发条件设成SCK的第一个边沿。解码器配置成SPI模式设置正确的CPOL、CPHA、数据位宽和位序。解码出来的数据要和代码里发送的数据逐字节对比快速定位是发送端还是接收端的问题。6.2 用GPIO翻转做时间戳定位瓶颈在没有逻辑分析仪的情况下可以用GPIO翻转配合示波器来测量各阶段耗时。具体做法是在DMA启动前翻转一个GPIO在传输完成中断里再翻转同一个GPIO示波器上就能看到传输的总耗时。如果想测量更细的粒度比如DMA搬运时间和SPI移位时间的比例可以用多个GPIO分别标记不同阶段。这种方法虽然粗糙但在现场调试时非常实用。6.3 常见错误标志的读取与解读GD32的SPI状态寄存器里有几个关键标志位调试时经常需要读取RBNE接收缓冲非空DMA没及时搬走数据时会一直置位TBE发送缓冲空DMA没及时填充数据时会置位TRANS传输完成最后一个字节移出后置位OVR溢出错误接收缓冲还没被读走新数据就来了MODF模式错误NSS引脚被意外拉低调试时可以在传输完成后读取这些标志判断是否有异常。OVR和MODF置位通常意味着配置有问题需要重点排查。7. 多从机场景下的DMA管理思路7.1 片选切换与DMA重配置的时机多从机场景下每个从机的SPI参数可能不同时钟频率、CPOL/CPHA切换从机时需要重新配置SPI和DMA。这个切换过程要小心不能在传输过程中切换。我的做法是每次切换从机前先确认当前传输已完成TRANS标志置位然后关闭SPI的DMA请求重新配置SPI参数和DMA缓冲区地址再使能DMA和SPI。整个过程用状态机管理避免在中断里做重配置操作。7.2 用DMA链表实现多缓冲区管理如果从机数量多、数据量大可以考虑用DMA链表Linked List模式。GD32的部分型号支持DMA链表可以在一个DMA通道上挂多个传输描述符每个描述符对应一个从机的数据缓冲区。DMA自动按链表顺序搬运搬完一个自动切换到下一个CPU只需要在传输完成中断里处理数据即可。这种方式的优点是CPU干预少适合数据流连续的场景。缺点是配置复杂调试难度大建议在单缓冲模式跑通后再尝试。7.3 从机数量扩展时的总线负载评估每增加一个从机SPI总线上的负载就增加一份。负载包括电容负载和协议开销。电容负载过大会导致信号边沿变缓高速率下波形畸变。协议开销包括片选切换时间、从机响应时间等。评估总线负载时我一般会算一笔账假设每个从机的片选切换需要1微秒数据传输需要10微秒那么4个从机轮询一遍需要44微秒。如果系统要求每毫秒完成一轮那还有充足的余量。但如果从机数量增加到16个一轮就需要176微秒接近极限了。这时候要么提高SPI时钟缩短传输时间要么减少轮询频率要么改用其他总线方案。具体怎么选要看系统的实时性要求。8. 个人实操体会与后续扩展方向SPIDMA这套组合我前后在五六个项目里用过从GD32F103到F303再到F4系列都踩过坑。最大的体会是顺序比配置更重要。DMA通道的使能顺序、SPI的使能时机、中断里的操作顺序任何一个顺序错了现象都可能很诡异而且不容易通过读代码发现。另一个体会是不要迷信手册里的典型配置。手册给的是理想情况下的配置实际项目里PCB走线、电源质量、从机器件特性都会影响结果。我习惯在手册配置的基础上先用较低速率跑通再用逻辑分析仪逐项验证时序确认无误后再逐步提高速率。后续如果要把这套方案用到更复杂的场景比如SPI屏幕刷新、高速ADC数据采集可以考虑几个方向一是用DMA双缓冲实现零等待的数据流处理二是结合定时器触发SPI传输实现精确的采样率控制三是用DMA传输完成中断驱动状态机把整个通信流程做成事件驱动架构进一步降低CPU占用。最后分享一个小技巧在调试SPIDMA时如果怀疑是DMA搬运的问题可以先把DMA关掉用轮询方式跑同样的数据对比结果。如果轮询正常而DMA异常问题基本就锁定在DMA配置或时序上了。这个方法帮我省了不少排查时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑