首页 / 资讯中心 / 文章详情

GD32F303 SPI+DMA全双工高速板间通信实战

GD32F303 SPI+DMA全双工高速板间通信实战 ★ FEATURED ARTICLE
1. 项目缘起与整体设计思路两片MCU之间要跑高速数据交换SPI加DMA这套组合几乎是绕不开的方案。我这次做的是一个基于GD32F303的板间通信项目主机负责采集前端传感器数据从机负责驱动执行机构并回传状态两边需要以固定周期互传数据帧单帧长度128字节刷新率要求稳定在1kHz以上。一开始用中断方式收发CPU占用率直接飙到六成稍微加点业务逻辑就开始丢帧。换成SPI加DMA全双工之后CPU占用降到个位数数据流稳得像一条直线。这个项目的核心目标很明确利用GD32的SPI外设配合DMA控制器实现主从机之间的全双工高速数据交换让CPU从繁琐的字节搬运中解放出来专注于数据处理和业务逻辑。适合有一定单片机基础、正在做板间通信或外设扩展的嵌入式开发者参考尤其是那些被中断收发折磨过、想上DMA又担心踩坑的朋友。先说清楚一个概念SPI的全双工本质上是移位寄存器的环形交换。主机往发送寄存器写一个字节时钟线上就会产生八个脉冲同时把从机移位寄存器里的数据移进来。也就是说你发一个字节的同时必然会收到一个字节这是硬件机制决定的不存在只发不收或者只收不发的情况。DMA要做的就是把这个“写发送寄存器”和“读接收寄存器”的动作自动化让数据在内存和外设之间直接搬运不经过CPU。为什么选DMA而不是中断算一笔账就明白了。128字节的数据帧如果每收一个字节进一次中断一帧就是128次中断1kHz刷新率下每秒12.8万次中断。每次中断进出栈加上处理逻辑保守估计20个时钟周期72MHz主频下光中断开销就吃掉不少CPU时间。而DMA方式下整帧数据搬完才触发一次传输完成中断中断次数直接降到千分之一。这个差距在高速通信场景下是决定性的。GD32的DMA控制器和STM32的用法高度相似但细节上有差异比如通道映射、寄存器命名、中断标志清除方式等。我选GD32F303系列是因为它主频够高、外设资源丰富SPI接口支持的最高时钟可以到PCLK的二分之一做高速通信绰绰有余。下面从硬件连接到软件配置把整个实现过程拆开来讲。2. 硬件连接与SPI工作模式选型2.1 主从机引脚对接与片选策略SPI是四线制通信主机和从机之间需要连接SCK、MOSI、MISO、CS四根线。SCK是时钟由主机产生MOSI是主机输出从机输入MISO是主机输入从机输出CS是片选低电平有效。两片GD32对接的时候主机的MOSI接从机的MOSI主机的MISO接从机的MISOSCK和CS直连即可。注意这里不是交叉连接SPI的命名规则是以主机为参照的所以同名引脚直接对接。片选信号的处理有两种方式硬件片选和软件片选。硬件片选是SPI外设自动控制CS引脚在数据传输开始时拉低传输结束后拉高。软件片选是用普通GPIO手动控制在通信前后自己拉低拉高。我这次用的是软件片选原因是硬件片选在DMA连续传输模式下时序不太好控制容易出现CS提前拉高导致最后一个字节还没移完就被截断的情况。软件片选虽然多占一个GPIO但时序完全可控调试起来也直观。注意如果从机的CS引脚在通信间隙浮空容易受到干扰产生误触发。建议在从机CS引脚上加一个上拉电阻确保空闲时保持高电平。2.2 SPI模式选择与时钟极性相位配置SPI有四种工作模式由时钟极性CPOL和时钟相位CPHA组合决定。CPOL决定空闲时SCK是高还是低CPHA决定数据在时钟的哪个边沿采样。模式0是CPOL0、CPHA0空闲低电平第一个边沿采样模式3是CPOL1、CPHA1空闲高电平第二个边沿采样。这两种模式最常用具体选哪个要看从机设备的要求。我这次两片都是GD32主从机都配置成模式0也就是CPOL0、CPHA0。为什么选模式0因为GD32的SPI外设在模式0下时序最稳定而且大部分SPI从设备都支持模式0。主从机的模式必须一致否则数据会错位收到的全是乱码。这一点在调试的时候要特别注意如果通信不成功先检查两边的CPOL和CPHA是否匹配。时钟频率方面主机SPI时钟由PCLK分频得到分频系数可以选2、4、8、16、32、64、128、256。我用的PCLK是72MHz分频系数选8SPI时钟就是9MHz。为什么不选更高的因为板间通信走的是排线不是PCB走线信号完整性没那么好9MHz下波形已经有点圆角了再高就容易出错。如果两块板子在同一块PCB上走线短且阻抗可控可以尝试18MHz甚至36MHz。2.3 数据帧格式与位序设置SPI的数据帧长度可以配置为8位或16位。我选的是8位因为数据帧本身就是按字节组织的用8位帧处理起来最方便。位序方面SPI支持MSB先行和LSB先行默认是MSB先行。这个设置主从机必须一致我保持默认的MSB先行。从机的NSS引脚管理需要特别注意。GD32的SPI从机模式下NSS引脚有两种管理方式硬件管理和软件管理。硬件管理下NSS引脚必须由外部拉低才能通信软件管理下NSS引脚的电平由SPI_CTL0寄存器的SWNSS位控制。我选的是硬件管理因为软件片选信号直接接到从机的NSS引脚上硬件管理响应更快不需要额外的寄存器操作。3. DMA通道配置与传输机制详解3.1 GD32的DMA请求映射关系GD32F303的DMA控制器有两个DMA0和DMA1每个控制器有7个通道。每个通道可以配置不同的外设请求SPI的收发请求映射到固定的通道上。以SPI0为例发送请求映射到DMA0通道3接收请求映射到DMA0通道2。这个映射关系是硬件固定的不能随意更改配置的时候必须查手册确认。我见过有人把SPI0的发送配到DMA0通道2上结果DMA死活不触发查了半天以为是代码问题最后发现是通道选错了。所以第一步一定是翻参考手册的DMA请求映射表把通道号确认清楚。SPI1的映射又不一样发送是DMA0通道4接收是DMA0通道3。SPI2的发送是DMA1通道1接收是DMA1通道0。这些细节在GD32的参考手册里都有配置前务必核对。3.2 DMA传输方向与数据宽度配置DMA的传输方向有三种外设到存储器、存储器到外设、存储器到存储器。SPI发送用的是存储器到外设把内存缓冲区里的数据搬到SPI的发送寄存器SPI接收用的是外设到存储器把SPI接收寄存器里的数据搬到内存缓冲区。两个方向各用一个DMA通道互不干扰这就是全双工DMA的基础。数据宽度方面外设端和存储器端都配置成8位和SPI的8位帧格式对应。如果SPI配的是16位帧DMA的数据宽度也要改成16位否则数据会错位。地址递增模式也要注意存储器端地址递增因为要遍历整个缓冲区外设端地址固定因为SPI的发送和接收寄存器地址是不变的。传输数量设置为缓冲区长度我这里是128。DMA传输完成后传输数量寄存器会自动清零如果需要连续传输要在中断里重新装载传输数量。这里有个坑GD32的DMA在传输完成后通道会自动关闭必须手动重新使能才能开始下一次传输。这一点和STM32的循环模式不太一样STM32可以配成循环模式自动重载GD32虽然也有循环模式但在SPI全双工场景下用循环模式容易出问题建议还是用普通模式加中断重载。3.3 优先级与中断配置DMA通道的优先级有四级最高、高、中、低。SPI接收的优先级应该比发送高因为接收是被动的如果接收不及时导致溢出数据就丢了发送是主动的稍微延迟一点问题不大。所以我把接收通道配成最高优先级发送通道配成高优先级。中断方面我使能了接收通道的传输完成中断。为什么只使能接收的因为SPI全双工的特性决定了发送和接收是同步进行的发送完成的时候接收也一定完成了。使能接收中断就够了发送中断反而会增加不必要的开销。中断服务函数里要做三件事清除中断标志、重新装载DMA传输数量、置位数据就绪标志供主循环处理。提示GD32的DMA中断标志清除需要先读状态寄存器再写清除寄存器顺序不能反。我一开始直接写清除寄存器标志死活清不掉中断反复触发查了好久才发现是这个顺序问题。4. 主从机代码实现与关键环节剖析4.1 主机端SPI与DMA初始化流程主机端的初始化分三步先配GPIO再配SPI最后配DMA。GPIO配置要把SCK、MOSI、MISO配成复用推挽输出CS配成普通推挽输出。注意MISO在主机端是输入但在GD32里配成复用推挽也没问题因为SPI外设会自动控制方向。CS引脚初始状态拉高确保空闲时从机不被选中。SPI配置的关键参数主模式、模式0、8位帧、MSB先行、软件NSS管理、分频系数8。使能SPI之前先把所有参数配好最后置SPI_CTL0寄存器的SPIEN位。DMA配置先配发送通道再配接收通道两个通道都配好后一起使能。发送通道的存储器地址指向发送缓冲区接收通道的存储器地址指向接收缓冲区。初始化完成后先拉低CS然后使能DMA接收通道再使能DMA发送通道最后使能SPI的DMA收发请求。这个顺序很重要必须先使能DMA通道再使能SPI的DMA请求否则SPI可能会在DMA还没准备好时就产生请求导致第一个字节丢失。我一开始顺序搞反了每次第一帧的第一个字节都是0后来调整顺序就正常了。4.2 从机端配置差异与注意事项从机的SPI配置和主机大部分相同区别在于主从模式选择从模式NSS管理选择硬件模式。从机的SCK、MOSI、MISO都配成复用推挽但方向是反的——从机的MOSI是输入MISO是输出。不过同样不需要手动配方向SPI外设会自动处理。从机的DMA配置和主机对称发送通道把内存数据搬到SPI发送寄存器接收通道把SPI接收寄存器搬到内存。从机的DMA请求映射和主机一样因为SPI外设的DMA请求是固定的不分主从。从机有一个特殊点从机的SPI时钟是由主机提供的所以从机不需要配置分频系数配了也没用。从机的SPI使能后会一直等待主机的时钟信号主机不发时钟从机就不工作。调试的时候如果从机没反应先检查主机的SCK有没有波形再看从机的NSS有没有被拉低。4.3 全双工数据交换的时序控制全双工DMA传输的时序是这样的主机拉低CS使能DMA收发然后SPI开始产生时钟。每个时钟周期主机和从机同时移出一位数据八位之后完成一个字节的交换。DMA在后台自动把发送缓冲区的数据搬到SPI发送寄存器同时把接收寄存器的数据搬到接收缓冲区。128个字节全部交换完成后接收DMA产生传输完成中断主机在中断里拉高CS处理接收到的数据然后准备下一帧。这里有个关键点CS的拉高时机。必须在DMA传输完成中断里拉高CS不能提前。如果提前拉高最后一个字节可能还没移完就被截断从机会收到不完整的数据。我试过在发送DMA完成中断里拉高CS结果发现发送完成的时候接收还没完成最后一个字节丢了。后来改成在接收DMA完成中断里拉高CS问题解决。注意如果通信速率很高中断响应延迟可能导致CS拉高不及时。可以在DMA传输完成中断里直接操作GPIO拉高CS不要经过任何函数调用或条件判断把延迟降到最低。4.4 数据缓冲区管理与双缓冲思路单缓冲区的做法是主机和从机各有一个发送缓冲区和一个接收缓冲区DMA直接在这两个缓冲区上操作。主循环处理完接收数据后把新的发送数据写入发送缓冲区然后触发下一次传输。这种做法简单直接但有个问题如果主循环处理速度跟不上通信速率就会出现数据覆盖。双缓冲的思路是准备两组缓冲区DMA在A组上传输的时候主循环处理B组的数据并准备B组的发送数据。DMA传输完成后切换到B组主循环再处理A组。这样主循环和DMA交替使用两组缓冲区互不干扰。实现上需要在DMA完成中断里切换缓冲区指针并重新装载DMA的存储器地址和传输数量。我一开始用的是单缓冲1kHz刷新率下主循环处理时间大概300微秒理论上够用但实际跑起来偶尔会丢帧。换成双缓冲之后丢帧问题彻底消失。双缓冲的代价是多占一倍内存128字节乘4个缓冲区也就512字节对GD32F303的48KB SRAM来说完全不是问题。5. 调试过程与常见问题排查实录5.1 SPI通信不生效的排查思路SPI通信不生效是最常见的问题排查要按顺序来。第一步查硬件连接用万用表量SCK、MOSI、MISO、CS四根线是否导通有没有虚焊或接反。第二步查时钟用示波器看主机的SCK有没有波形频率对不对。如果SCK没波形说明SPI没使能或者配置有问题。第三步查CS看通信时CS有没有被拉低从机的NSS有没有收到低电平。如果硬件和时钟都正常但数据还是不对就要查SPI模式。主从机的CPOL和CPHA必须一致位序也必须一致。我遇到过一次主从机模式不匹配的情况主机发0x55从机收到0xAA正好是位序反了。后来把从机的位序改成和主机一样就正常了。还有一种情况是数据偶尔出错不是全错。这种一般是时序问题可能是时钟太快导致建立时间不够或者CS拉高太早导致最后一个字节被截断。降低时钟频率或者调整CS拉高时机通常能解决。5.2 DMA传输不触发或只传一次的解决方法DMA传输不触发先查通道映射对不对。SPI0的发送是DMA0通道3接收是DMA0通道2这个必须严格对应。然后查DMA使能顺序必须先使能DMA通道再使能SPI的DMA请求。最后查传输数量寄存器如果配成0DMA不会传输任何数据。DMA只传一次就停了大概率是传输完成后没有重新装载传输数量。GD32的DMA在普通模式下传输完成后通道自动关闭传输数量寄存器清零。下一次传输前必须重新设置传输数量并重新使能通道。我一开始没在中断里重载结果只收到第一帧数据后面全是空的。在中断里加上重载逻辑后就正常了。还有一种可能是中断标志没清干净导致中断反复触发但实际传输没进行。GD32的DMA中断标志清除需要先读状态寄存器再写清除寄存器这个顺序不能错。我见过有人直接写清除寄存器标志清不掉中断一直进但DMA传输数量已经是0了所以实际没数据。5.3 数据错位与丢字节的典型原因数据错位一般和SPI模式或位序有关。如果收到的数据整体偏移一位比如发0x01收到0x02那是位序问题。如果收到的数据高低字节颠倒那是帧格式问题可能一边配了8位一边配了16位。如果收到的数据完全乱码那是CPOL或CPHA不匹配。丢字节的原因比较多。CS拉高太早会丢最后一个字节这个前面说过。DMA传输数量设置不对也会丢字节比如缓冲区是128字节但传输数量设成了127最后一个字节就不会被搬运。还有一种情况是DMA优先级太低被其他高优先级DMA打断导致SPI接收寄存器溢出。把SPI接收DMA的优先级设成最高可以避免这个问题。我遇到过一次丢字节的情况查了半天发现是主循环里有一段时间关了全局中断导致DMA完成中断没及时响应CS拉高晚了从机以为通信还在继续结果下一帧的CS拉低被从机当成了数据。后来把关中断的时间缩短问题就没了。5.4 常见问题速查表问题现象可能原因排查方法解决措施SCK无波形SPI未使能或GPIO配置错误检查SPI_CTL0的SPIEN位和GPIO复用配置使能SPI重新配置GPIO为复用模式数据全错CPOL/CPHA不匹配对比主从机SPI模式配置统一主从机为模式0或模式3数据位序颠倒MSB/LSB设置不一致检查SPI_CTL0的LF位统一主从机位序设置DMA不触发通道映射错误或使能顺序不对查手册确认通道号检查使能顺序修正通道号先使能DMA再使能SPI请求DMA只传一次传输完成后未重载检查中断里是否重载传输数量在中断中重设传输数量并重新使能通道丢最后一个字节CS拉高太早示波器观察CS和SCK的时序关系在接收DMA完成中断里拉高CS偶发丢帧主循环处理超时或中断延迟测量主循环处理时间和中断响应时间改用双缓冲缩短关中断时间接收数据溢出DMA优先级太低检查DMA通道优先级配置将接收DMA设为最高优先级6. 性能优化与实战经验总结6.1 提高通信速率的几个关键调整通信速率上不去先从SPI时钟入手。GD32F303的SPI0挂在APB2上PCLK2最高72MHz分频系数最小2理论最高36MHz。但实际能跑多快取决于板间连接的质量。我实测在排线连接下9MHz很稳18MHz偶尔出错36MHz基本不能用。如果两块板子在同一PCB上走线短且包地处理36MHz可以稳定运行。DMA的传输效率也会影响整体速率。GD32的DMA在AHB总线上传输一个字节需要一个AHB时钟周期。72MHz下传输128字节大概需要1.8微秒相对于SPI传输时间可以忽略。但如果总线上有其他高优先级DMA在跑比如ADC连续采样就会抢占带宽。把SPI的DMA优先级设高一些可以缓解。中断响应时间也是瓶颈。DMA完成中断从触发到进入服务函数大概需要十几个时钟周期。如果系统里还有其他中断比如定时器中断可能会延迟响应。把SPI的DMA中断优先级设成最高可以保证及时响应。另外中断服务函数要尽量短只做标志清除、缓冲区切换和传输重载数据处理放到主循环里做。6.2 双缓冲与零拷贝的取舍双缓冲解决了数据覆盖问题但增加了内存拷贝的开销。如果主循环处理数据时需要把接收缓冲区的数据再拷贝到另一个地方那就多了一次拷贝。零拷贝的思路是让主循环直接处理DMA接收缓冲区里的数据处理完再切换缓冲区。这样省掉了拷贝但要求主循环处理时间必须小于一帧的通信时间。我这次用的是双缓冲加零拷贝DMA接收缓冲区有两组主循环直接处理当前空闲的那组处理完置位标志DMA完成中断里检查标志并切换。这样既避免了数据覆盖又省掉了拷贝开销。代价是主循环的处理时间必须控制在一帧通信时间内1kHz刷新率下就是1毫秒。实测主循环处理128字节的数据大概300微秒余量充足。提示双缓冲的切换逻辑要小心处理边界情况。如果DMA完成中断到来时主循环还在处理上一组数据说明处理超时了这时候要么丢帧要么等待。我的做法是置一个溢出标志主循环下次运行时优先处理溢出情况避免数据错乱。6.3 实际项目中的踩坑记录第一个坑是DMA中断标志清除顺序。GD32的DMA中断标志清除需要先读DMA_INTF寄存器再写DMA_INTC寄存器顺序反了标志清不掉。我一开始没注意中断反复触发程序直接卡死。后来查手册才发现这个细节改过来就好了。第二个坑是SPI的DMA请求使能时机。SPI_CTL0寄存器里有SPI_DMA_TX和SPI_DMA_RX两个使能位必须在DMA通道使能之后再置位。如果先置位SPI的DMA请求SPI会在DMA还没准备好时就发出请求导致第一个字节丢失。我一开始就是先使能SPI的DMA请求再使能DMA通道结果每次第一帧的第一个字节都是0。第三个坑是从机的NSS管理。从机配成硬件NSS管理时NSS引脚必须由外部拉低才能通信。我一开始把从机的NSS配成了软件管理但CS信号又接到了NSS引脚上结果从机根本不响应。后来改成硬件管理就正常了。软件NSS管理下从机的NSS电平由寄存器控制外部信号不起作用这个容易搞混。第四个坑是DMA传输数量寄存器的重载。GD32的DMA在普通模式下传输完成后通道自动关闭传输数量寄存器清零。下一次传输前必须重新设置传输数量并重新使能通道。我一开始只在初始化时设置了一次传输数量结果只收到第一帧数据。后来在中断里加上重载逻辑问题解决。6.4 不同GD32系列的适配要点GD32F303的DMA请求映射和STM32F103基本一致但GD32F103和GD32F303的映射又有些差异。比如SPI0的发送请求GD32F303是DMA0通道3GD32F103也是DMA0通道3但GD32E230的映射就完全不同了。换芯片型号的时候第一件事就是查参考手册的DMA请求映射表把通道号确认清楚。GD32的固件库和STM32的标准库用法相似但函数名不同。比如使能DMA通道STM32是DMA_CmdGD32是dma_channel_enable。中断标志清除STM32是DMA_ClearITPendingBitGD32是dma_interrupt_flag_clear。移植代码的时候这些函数名都要改不能直接复制粘贴。GD32的SPI外设和STM32的SPI外设在寄存器层面也有差异。比如GD32的SPI_CTL0寄存器里有一个SWNSS位控制软件NSS管理STM32的对应位叫SSM。GD32的SPI_STAT寄存器里有一个TRANS位表示收发完成STM32的对应位叫BSY。这些差异在调试的时候要注意看手册的时候别拿STM32的手册来查GD32的寄存器。7. 写在最后的几句实在话这套SPI加DMA的全双工方案我从立项到跑通大概花了三天时间其中两天半在踩坑。真正核心的代码量并不大主机和从机加起来也就两百多行但要把每个细节都配对确实需要耐心。我的建议是先用低速率把通信跑通确认数据收发正常再逐步提高速率。不要一上来就配最高速出了问题很难定位是速率问题还是配置问题。调试工具方面示波器是必须的逻辑分析仪更好。SPI的时序问题用示波器看波形最直观CS、SCK、MOSI、MISO四根线一起抓一眼就能看出问题在哪。如果没有逻辑分析仪至少要有示波器否则出了问题只能靠猜效率太低。最后说一个实用技巧在通信协议里加一个帧头和一个校验字段。帧头用来做帧同步校验用来检测数据错误。我这次用的是0xAA55做帧头CRC16做校验。虽然SPI本身有硬件校验机制但在DMA全双工模式下硬件校验不太好用软件校验更灵活。加了帧头和校验之后即使偶尔有干扰导致数据出错也能及时发现并丢弃不会把错误数据传给业务逻辑。
阅读完成 · 觉得有帮助?
咨询建站