ARTICLE DETAIL

资讯详情

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

GD32H759 RT-Thread SPI驱动实战:工控场景下的时序、DMA与稳定性优化

GD32H759 RT-Thread SPI驱动实战:工控场景下的时序、DMA与稳定性优化 1. 从并口屏到SPI Flash工控场景下SPI总线的真实定位做工业控制这行十几年每次有人问我“SPI到底难在哪”我都会先反问一句你打算拿它接什么如果只是接个传感器读几个寄存器那确实不难初始化、读写、收工。但工控板上的SPI从来不是这么用的——它上面挂的是几MB的配置存储、是几十路模拟量的ADC采样链路、是跟FPGA之间跑的高速数据通道。一旦这些东西同时挂在一条SPI总线上问题就全冒出来了片选时序打架、DMA搬运错位、时钟相位对不上、长线传输误码。这些坑我在GD32H759这颗Cortex-M7的芯片上几乎踩了个遍。GD32H759是兆易创新目前主频最高的一颗通用MCUCortex-M7内核跑到600MHz带大容量Flash和SRAM外设资源也相当丰富——光SPI就有好几路支持硬件片选、DMA、多种时钟分频。RT-Thread作为国产RTOS里生态最成熟的一个设备驱动框架对SPI的支持也比较完整有spi_dev、spi_msg、spi_transfer_message这一套标准接口。但框架完整不代表用起来就顺尤其是工控场景对稳定性和实时性的要求跟消费电子完全不是一个量级。这篇内容我打算把GD32H759上跑RT-Thread时SPI驱动的实战经验完整梳理一遍。从总线选型、片选策略、DMA配置到实际调试中遇到的时序问题和排查方法都会讲到。适合正在用GD32系列做工业控制、或者准备把RT-Thread移植到高性能MCU上的朋友参考。不管你是刚接触SPI的新手还是已经用过STM32硬件SPI的老手这里面的坑和技巧应该都能帮你省不少时间。2. GD32H759的SPI外设到底给了我们什么2.1 几路SPI、各自的能力边界GD32H759的SPI资源在同级别MCU里算比较充裕的。它有多路SPI/I2S接口其中部分支持最高时钟分频到系统时钟的几分之一实际跑下来SCK频率可以做到几十MHz。对于工控场景来说这个速率接SPI Flash、接高速ADC、跟FPGA通信都够用了。但要注意一点不是所有SPI接口的能力都一样。有的SPI只支持标准模式有的支持TI模式、支持硬件CRC、支持双线/四线模式。你在分配外设的时候得先想清楚每路SPI要挂什么设备再决定用哪一路。比如你要接一个SPI Flash做配置存储那用普通SPI就够了但如果你要接一个高速ADC做同步采样那就得考虑SPI的时钟极性和相位能不能跟ADC对上以及DMA通道够不够用。我在项目里是这样分配的SPI0挂SPI Flash存配置参数和日志SPI1接外部ADC做模拟量采集SPI2留给跟FPGA的通信链路。这样分配的好处是各路的负载比较均衡不会出现某一路SPI既要跑高速又要兼顾多个设备的情况。2.2 硬件片选和软件片选的选择逻辑这是工控SPI里最容易被忽视、但影响最大的一个决策点。GD32H759的SPI支持硬件片选NSS由硬件自动控制也支持软件片选用普通GPIO手动拉低拉高。很多人图省事直接用硬件片选结果在多设备总线上就翻车了。硬件片选的机制是这样的SPI外设在发送数据的时候自动把NSS拉低发送完自动拉高。听起来很方便但它有个致命问题——它只适合总线上挂一个设备的情况。如果你总线上挂了多个从设备硬件片选没法区分该拉低哪一个因为它只管自己这一路SPI的NSS引脚。软件片选就灵活多了。你用任意GPIO作为片选信号在发起传输之前手动拉低对应的片选引脚传输完成后再拉高。这样总线上挂多少个设备都能管得过来。代价是你得自己管理片选时序确保在传输期间片选一直有效传输结束后及时释放。我的建议是工控项目里只要SPI总线上挂了超过一个设备一律用软件片选。硬件片选只在单设备、且对时序要求极高的场景下才考虑。2.3 SPI模式配置CPOL和CPHA不是随便选的SPI有四种模式由CPOL时钟极性和CPHA时钟相位组合而成。这个知识点看起来基础但实际项目中配错的概率非常高。原因很简单很多传感器的数据手册里写的是“支持SPI模式0和模式3”但没告诉你它默认是哪个模式你得自己去试。CPOL决定的是时钟空闲时的电平CPOL0表示空闲时SCK为低电平CPOL1表示空闲时SCK为高电平。CPHA决定的是数据在哪个时钟边沿被采样CPHA0表示在第一个边沿采样CPHA1表示在第二个边沿采样。我遇到过最坑的一次是接一个工业级ADC手册上写的是模式0但实际跑起来数据总是偏移一位。后来用逻辑分析仪抓波形才发现ADC实际工作在模式1下手册写错了。从那以后我养成了一个习惯不管手册怎么写第一次调试SPI设备的时候一定用逻辑分析仪抓一下波形确认CPOL和CPHA的实际配置。模式CPOLCPHA空闲电平采样边沿模式000低第一个边沿上升沿模式101低第二个边沿下降沿模式210高第一个边沿下降沿模式311高第二个边沿上升沿3. RT-Thread的SPI设备框架怎么跟GD32H759对接3.1 spi_dev的注册流程和底层驱动挂载RT-Thread的SPI框架分两层上层是spi_dev设备层提供统一的spi_transfer_message接口下层是SPI控制器驱动负责操作具体的硬件寄存器。在GD32H759上你需要做的是把GD32的SPI外设驱动注册到RT-Thread的SPI框架里。注册流程大致是这样的先定义一个spi_configure结构体配置好模式、数据位宽、时钟频率这些参数然后调用rt_spi_bus_register函数把SPI总线注册到系统中最后用rt_spi_bus_attach_device把具体的从设备挂到总线上。这里有个细节容易被忽略GD32H759的SPI时钟源来自哪个总线、分频系数怎么算这些在注册的时候就要确定好。如果你后面要动态改SPI速率得确保分频系数的计算逻辑是对的。我见过有人直接把分频系数写死结果换个晶振频率就跑不起来了。3.2 spi_msg和spi_transfer_message的使用要点RT-Thread的SPI传输用的是spi_msg结构体一个msg可以包含多个spi_msg每个msg里可以指定发送缓冲区、接收缓冲区、数据长度。spi_transfer_message会依次处理这些msg中间可以选择是否释放片选。这个设计的好处是支持“发送-接收-再发送”这种复合传输模式。比如很多SPI Flash的操作流程是先发命令字再发地址然后读数据。用spi_transfer_message可以把这三步放在一个传输序列里完成中间不释放片选保证操作的原子性。但要注意spi_transfer_message是阻塞式的传输期间会占用CPU。如果你要传的数据量比较大比如从SPI Flash里读几KB的配置数据最好配合DMA来做。RT-Thread的SPI框架支持DMA模式但需要底层驱动实现DMA的配置和回调。3.3 中断还是DMA工控场景下的取舍在工控场景下SPI传输用中断还是DMA取决于你的数据量和实时性要求。小数据量、低频次的传输用中断就够了实现简单调试也方便。但如果是高速ADC连续采样、或者大块数据搬运那就必须上DMA否则CPU会被SPI中断拖垮。GD32H759的DMA控制器支持外设到内存、内存到外设的搬运SPI的发送和接收都可以触发DMA请求。配置的时候要注意DMA通道的映射关系——不是所有DMA通道都能被SPI触发得查手册确认。另外DMA的传输完成中断要处理好确保在DMA搬完之后再释放片选否则最后几个字节可能还没发出去片选就没了。我在项目里的做法是SPI Flash的读写用DMA因为数据块比较大ADC的寄存器配置用中断因为每次就几个字节跟FPGA的通信链路用DMA加双缓冲保证连续传输不丢数据。4. 工控现场踩出来的SPI时序坑4.1 片选建立时间和保持时间不够导致的偶发误码这个问题折磨了我整整两天。现象是这样的SPI Flash在实验室里读写都正常但一到现场跑几个小时就会偶尔出现数据校验失败。一开始怀疑是Flash本身的问题换了芯片还是一样。后来用示波器抓片选和时钟的波形才发现问题出在片选的建立时间上。GD32H759的GPIO翻转速度很快软件片选在拉低之后几乎立刻就开始了SPI时钟。但SPI Flash要求片选拉低之后至少等待一定的时间通常是几十纳秒才能开始接收时钟。这个时间在实验室里因为温度、电压都正常勉强能满足到了现场温度变化、电压波动就偶尔不满足了。解决办法很简单在拉低片选之后、启动SPI传输之前插入一个短暂的延时。这个延时可以用空指令循环实现也可以用硬件定时器。我一般会留出几百纳秒的余量确保在各种工况下都能满足。/* 片选拉低后插入延时确保满足建立时间 */ rt_pin_write(CS_PIN, PIN_LOW); for (volatile int i 0; i 50; i); /* 约几百纳秒延时 */ /* 启动SPI传输 */4.2 长线传输时的信号完整性问题工控板上的SPI设备有时候不在同一块PCB上而是通过排线或者连接器连出去的。线一长信号完整性问题就来了时钟线上出现过冲和振铃数据线上出现串扰片选线上出现毛刺。这些问题在低速的时候不明显一旦SPI时钟超过10MHz就开始显现。我处理过的方案有这么几种一是降低SPI时钟频率牺牲速度换稳定性二是在时钟线和数据线上串接小电阻通常22到100欧姆抑制反射三是在接收端加RC滤波但要注意滤波不能影响时序。最根本的解决办法还是缩短线长、做好阻抗匹配。如果实在没法缩短那就把SPI时钟降下来工控场景对绝对速度的要求其实没那么高稳定可靠才是第一位的。4.3 多设备总线上的片选串扰前面说了多设备要用软件片选但软件片选也有坑。如果你在切换片选的时候没有确保前一个设备的片选已经完全释放就可能出现两个设备同时被选中的情况。这时候两个从设备都会往MISO线上驱动数据造成总线冲突。我遇到过一次这样的情况SPI总线上挂了一个Flash和一个ADC代码里先操作Flash再操作ADC但片选释放和下一个片选拉低之间没有足够的间隔。结果ADC在Flash还没完全释放MISO的时候就开始驱动了读出来的数据全是乱的。解决方法是在切换片选的时候确保前一个片选拉高之后至少等待一个SPI时钟周期再拉低下一个片选。另外MISO线最好加上拉电阻确保在没有设备驱动的时候处于确定的高电平状态。5. 用逻辑分析仪定位SPI问题的完整排查链路5.1 抓波形之前先确认这三件事很多人一遇到SPI问题就急着抓波形但其实在抓波形之前有三件事必须先确认清楚否则抓出来的波形你也看不懂。第一确认SPI的时钟频率和分频系数。GD32H759的SPI时钟是从系统时钟分频来的你要先算清楚实际输出的SCK频率是多少。如果分频系数配错了实际频率可能跟你预期差很多。第二确认CPOL和CPHA的配置。这个前面讲过了配错了波形完全对不上。第三确认数据位宽和字节序。SPI可以配成8位或16位数据宽度MSB先行还是LSB先行也要确认。这些配置错了读出来的数据会完全不对。5.2 从波形上读出片选、时钟、数据的对应关系逻辑分析仪抓SPI波形一般抓四根线CS、SCK、MOSI、MISO。看波形的时候先看CS什么时候拉低再看SCK在CS有效期间有几个完整的时钟周期然后对照MOSI和MISO上的数据。一个典型的SPI Flash读操作波形是这样的CS拉低然后MOSI上先出现命令字比如0x03表示读数据接着是24位地址然后MISO上开始输出数据。你要确认的是命令字和地址是不是在SCK的上升沿被Flash采样的数据是不是在SCK的下降沿被Flash输出的。如果发现数据偏移了一位那多半是CPHA配错了。如果发现数据完全不对那可能是CPOL配错了或者MOSI和MISO接反了。5.3 一个真实案例DMA搬运导致的最后字节丢失这个案例我印象很深。现象是SPI Flash读出来的数据最后一个字节总是0xFF。用逻辑分析仪抓波形发现最后一个字节的时钟周期确实发出了但MISO上的数据还没稳定片选就被拉高了。根本原因是DMA传输完成中断触发之后代码立刻拉高了片选但此时SPI移位寄存器里最后一个字节还没完全移出去。SPI外设的发送完成标志和DMA的传输完成标志不是一回事——DMA搬完数据只代表数据写进了SPI的发送缓冲区不代表数据已经从移位寄存器发出去了。解决办法是在DMA传输完成之后再等待SPI的发送完成标志比如GD32的SPI_STAT中的TBE标志确认移位寄存器空了之后再拉高片选。/* 等待DMA传输完成 */ while (dma_transfer_complete_flag 0); /* 等待SPI移位寄存器空 */ while (SPI_STAT(spi_periph) SPI_STAT_TRANS); /* 拉高片选 */ rt_pin_write(CS_PIN, PIN_HIGH);6. SPI Flash在工控参数存储中的实战配置6.1 为什么选SPI Flash而不是EEPROM工控板上存配置参数传统做法是用EEPROM容量小、速度慢但接口简单。现在越来越多的项目改用SPI Flash原因有几个容量大几MB到几十MB都有、速度快几十MHz的SPI时钟、成本低。对于需要存日志、存大量配置参数的工控场景SPI Flash几乎是唯一的选择。但SPI Flash有个特点它不能像EEPROM那样按字节擦写而是必须按扇区擦除。一个扇区通常是4KB擦除之后才能写入。这意味着你在设计参数存储方案的时候要考虑好擦写策略——不能每次改一个参数就擦一整个扇区那样Flash的寿命很快就耗尽了。我的做法是把参数分成几个区域频繁修改的参数放在一个专门的扇区里用磨损均衡算法管理不常改的参数放在另一个扇区只在需要的时候擦写。另外每次擦写之前先把数据备份到RAM里擦完之后再写回去避免掉电导致数据丢失。6.2 扇区擦除和页写入的时序要求SPI Flash的擦除和写入操作都有时序要求不是发完命令就完事了。以常见的W25Q系列为例扇区擦除需要几十毫秒到几百毫秒页写入需要几百微秒到几毫秒。在这段时间里Flash处于忙状态不会响应任何命令。所以你在擦除或写入之后必须轮询Flash的状态寄存器确认操作完成之后才能进行下一步。如果用的是RT-Thread可以在轮询的时候让出CPU避免阻塞其他线程。/* 发送扇区擦除命令 */ spi_flash_sector_erase(addr); /* 轮询状态寄存器等待擦除完成 */ while (spi_flash_is_busy()) { rt_thread_mdelay(1); /* 让出CPU */ }6.3 掉电保护工控场景下的数据安全底线工控设备最怕的就是掉电导致数据丢失。SPI Flash在擦写过程中如果掉电整个扇区的数据都可能损坏。所以掉电保护是必须做的。硬件上可以在电源线上加一个大电容保证掉电后MCU还有几十毫秒的时间完成当前操作。软件上可以采用“双备份”策略把参数存两份写的时候先写备份区写成功了再更新主区。这样即使写主区的时候掉电备份区还有完整的数据。另外每次上电的时候要检查数据的完整性可以用CRC校验。如果发现数据损坏就从备份区恢复。7. 跟FPGA通信时SPI链路的特殊处理7.1 FPGA侧SPI从机的实现差异跟FPGA通信的SPI链路跟接标准SPI设备不太一样。FPGA侧的SPI从机是你自己写的逻辑时序上可以灵活调整但也意味着你得自己保证跟MCU侧的SPI主机匹配。我在项目里用FPGA做SPI从机的时候遇到过几个问题。一是FPGA的SPI从机在片选拉低之后需要几个时钟周期来同步如果MCU这边片选拉低之后立刻就发时钟FPGA可能还没准备好。二是FPGA的SPI从机在发送数据的时候第一个字节的建立时间可能不够导致MCU读到的第一个字节是错的。解决办法是在FPGA侧加一个状态机片选拉低之后先进入一个等待状态等几个时钟周期之后再开始响应。MCU侧则在片选拉低之后插入一个短暂的延时给FPGA留出准备时间。7.2 数据帧格式的约定和校验MCU跟FPGA之间的SPI通信数据帧格式需要双方约定好。我一般会定义一个简单的帧结构帧头比如0xA5、命令字、数据长度、数据内容、CRC校验、帧尾。这样即使传输过程中出现误码也能通过CRC校验发现。帧头的作用是帮助接收方同步。如果接收方发现收到的数据不是以帧头开始就丢弃重新同步。这在长线传输或者干扰比较大的场景下很有用。7.3 双缓冲DMA实现连续数据传输如果MCU跟FPGA之间需要连续传输大量数据比如实时上传采样数据那就需要用双缓冲DMA。原理是这样的DMA配置两个缓冲区当一个缓冲区在传输的时候CPU可以往另一个缓冲区里填数据。DMA传完一个缓冲区之后自动切换到另一个并触发中断通知CPU。GD32H759的DMA控制器支持双缓冲模式配置起来不算复杂。关键是要处理好缓冲区切换时的数据一致性问题——确保CPU填数据的时候DMA没有在访问同一个缓冲区。8. 几个让我印象深刻的调试瞬间8.1 时钟相位配错导致ADC数据整体偏移有一次接一个工业级ADC手册上写的是SPI模式0我按模式0配置之后读出来的数据总是比实际值大一点。一开始以为是ADC的参考电压有问题查了半天。后来用逻辑分析仪抓波形发现ADC实际是在SCK的下降沿输出数据的也就是说它实际工作在模式1下。手册写错了。把CPHA改成1之后数据立刻就对了。这件事让我养成了一个习惯不管手册怎么写第一次调试SPI设备的时候一定用逻辑分析仪确认实际时序。8.2 片选信号被其他外设复用的冲突GD32H759的引脚功能是复用的一个引脚可以配成GPIO、SPI片选、UART的TX等等。我在项目里遇到过一个问题SPI的片选引脚跟另一个外设的引脚冲突了导致片选信号偶尔会异常翻转。排查的时候用示波器抓片选引脚发现它在不该翻转的时候出现了毛刺。查引脚配置表才发现这个引脚被另一个外设的初始化代码重新配置了。解决办法是在初始化的时候明确指定每个引脚的功能避免冲突。8.3 从机未就绪时主机强行发起传输的后果SPI是全双工同步通信主机发起传输的时候从机必须已经准备好。如果从机还没准备好主机发出去的时钟和数据就没人响应MISO上读到的就是无效数据。我在项目里遇到过从机一个传感器上电之后需要一段时间才能就绪但主机在初始化完成之后立刻就发起了传输。结果读到的数据全是0。后来在初始化流程里加了一个延时等传感器就绪之后再开始通信问题就解决了。这个坑的教训是SPI通信的初始化流程里一定要考虑从设备的就绪时间。不同设备的就绪时间不一样有的几毫秒有的几百毫秒得查手册确认。9. 关于SPI驱动稳定性的一些个人体会做了这么多年的工控SPI驱动我最大的体会是SPI本身不难难的是在各种异常工况下保持稳定。实验室里跑通不代表现场能用现场能用不代表长期可靠。真正要花心思的地方是那些边界条件和异常处理。比如片选时序你可能觉得多等几百纳秒是浪费但就是这几百纳秒决定了设备在高温低温下能不能稳定工作。比如DMA传输完成之后的等待你可能觉得多等几个时钟周期没必要但就是这几个周期决定了最后一个字节会不会丢。比如掉电保护你可能觉得工控设备不会经常掉电但就是那一次意外掉电可能让整个设备的数据全部丢失。这些东西在数据手册里不会写在应用笔记里也不会强调只有真正在工控现场跑过、被问题折磨过的人才会懂。我写这些内容的目的就是希望后来的人能少走一些弯路把时间花在更有价值的地方。SPI总线的调试工具也很重要。一个逻辑分析仪是必须的最好支持SPI协议解码能直接把波形翻译成数据。另外示波器也要有用来观察信号完整性。这些工具看起来是额外投入但跟调试时浪费的时间比起来这点投入太值了。最后说一个我常用的技巧在SPI驱动的关键路径上加一些调试输出比如片选拉低和拉高的时间戳、每次传输的数据长度和CRC校验结果。这些信息在出问题的时候非常有用能帮你快速定位是哪个环节出了错。平时这些调试输出可以关掉需要的时候再打开不影响正常运行。
返回列表