ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

GD32H759+RT-Thread SPI驱动实战:时序、设备框架与Cache一致性

GD32H759+RT-Thread SPI驱动实战:时序、设备框架与Cache一致性

做工业控制项目,手里这颗GD32H759已经把RT-Thread跑起来了,时钟树、GPIO、UART这些基础外设也都在前面几篇里趟过一遍。这一篇专门写SPI。SPI在工控里的出场率比很多人想象得高得多,参数存储Flash、高精度ADC、绝对值编码器、LCD屏幕、IO扩展芯片,全都是SPI的活儿。GD32H759这种主频600MHz的Cortex-M7核,如果还在用IO口软件模拟SPI,那基本等于浪费硬件资源。这篇把我在实际项目中用GD32H759加RT-Thread驱动SPI的完整套路讲清楚,包括时序参数设计、设备框架接入、接W25Q64存储芯片、接ADS1256高精度ADC的完整代码,以及在M7核上做SPI必须注意的Cache一致性问题。适合正在搞工控板卡、仪器仪表、数据采集系统的朋友参考,尤其是那些想把RT-Thread设备框架用明白的人。

1. 工控场景下为什么要选SPI,以及这套方案的整体思路

1.1 四类串行总线里的定位:SPI凭什么是主力

工控板上常见的串行总线无非UART、I2C、I2S、SPI这几类,它们分工完全不同。UART是异步协议,简单可靠但速率天花板低,一般用来做调试串口和低速透传;I2C只需要两根线,还能挂多个从机地址,但速率通常限制在400kHz到1MHz,而且有ACK应答机制,时序反而复杂;I2S专门给音频数据用,工控场景基本碰不到。SPI是同步全双工协议,一根时钟线SCK,一根主机输出从机输入MOSI,一根主机输入从机输出MISO,再加片选CS,速率能跑到几十乃至上百Mbps,天然适合大数据量交换。

在工控现场,SPI高频出现的原因有三条。第一是速率,高速ADC采集、编码器角度读取、Flash存储固件参数,都需要在短时间搬大量数据,UART和I2C扛不住;第二是全双工,主机在读数据的同时还可以继续发命令,协议简单透明,调试方便;第三是从机选通机制,每个设备一根CS直接控制,互不干扰,这种确定性在工控里非常重要。比如我这次要用W25Q64做参数掉电保存,用ADS1256做多路模拟量采集,一个SPI总线挂4线设备,每个从机单独片选,逻辑非常清晰。

1.2 整体方案架构:两条SPI总线分别挂什么设备

GD32H759片上集成了多组SPI外设,完全没必要把所有设备挤到一条总线上。我的方案是SPI0挂W25Q64,SPI1挂ADS1256,两条总线各自独立配置。这么设计的原因很直接:W25Q64可以跑几十MHz,ADS1256的SCLK上限通常不到2MHz,如果挂在同一条总线上,只能以最慢的那个从机为准,高速Flash就被拖累了。分总线以后,每条总线都能按自己的最优时序运行。

另外一个重要决策是使用RT-Thread的SPI设备框架,而不是直接在应用层操作寄存器。设备框架把总线、设备、片选Pin统一管理起来,应用层通过rt_spi_send、rt_spi_recv这些API访问从机,换一颗芯片或者换一个引脚,改动范围被限定在驱动层,应用代码基本不动。这对工控项目后期维护意义很大。RT-Thread从4.x到5.x版本一直维护GD32系列BSP,GD32H759的驱动框架是现成的,我们主要工作是配置引脚、挂设备、调时序参数。

2. 硬件设计与时序分析:动手前先搞懂这些参数

2.1 硬件SPI和软件模拟SPI,绝大多数情况选硬件

很多从单片机转过来的朋友习惯用GPIO翻转模拟SPI,因为灵活,想用哪个引脚都行,电平还能自己控制。但我要说清楚,在GD32H759这种高主频M7核上,模拟SPI有几个先天短板。第一,GPIO翻转本身有延迟,还要在每bit之间做延时控制,同样收发1000字节,模拟SPI占用的CPU时间可能是硬件SPI的几十倍;第二,模拟SPI没有FIFO,没有错误标志,没有DMA协同,M7核跑着RT-Thread,来了中断就得停,时序一致性很难保证。第三,模拟SPI的速率上去了,时钟抖动也跟着上去了,接高速Flash时信号质量很难看。

我的原则很明确:只要硬件SPI外设能用,绝不模拟。GD32H759的SPI外设带收发FIFO,配合DMA可以做到整块数据搬移不用CPU介入,这才是M7该有的用法。只有一种情况可以考虑模拟,就是某个从机对时序模式极其特殊,硬件SPI调不出来,那也只建议用在调试阶段。之前遇到一个项目,从机要求SCK高电平空闲且在第二个边沿采样,硬件SPI能配出Mode 3,根本不需要模拟。

2.2 SPI时序三件套:极性、相位、位序

SPI协议本身不复杂,但四根线什么时候放数据、什么时候采数据,全靠一对参数:CPOL和CPHA。CPOL决定SCK空闲电平,0是空闲低,1是空闲高。CPHA决定数据采样边沿是第一个边沿还是第二个边沿。组合起来就是Mode 0到Mode 3,很多从机的数据手册会直接写“支持Mode 0和Mode 3”,意思是这两种模式都能用,你选一种就行。

判断从机模式有个最实用的办法:看数据手册里时序图上“数据输出”和“数据采样”的标注。以Mode 0为例,CPOL=0,SCK空闲是低电平,第一个边沿是上升沿,主机在这个上升沿采样,从机在前一个下降沿把数据准备好。Mode 1就是CPOL=0、CPHA=1,空闲低,但是第二个边沿即下降沿才采样。ADS1256的数据手册写的是数据在SCLK下降沿输出、上升沿锁存,对应模式就是Mode 1,这个稍后实战部分会重点说。

还有个参数是位序MSB还是LSB first。绝大多数SPI设备都是MSB first,先发最高位,GD32H759的SPI外设里对应配置。万一遇到LSB first的设备,需要在SPI配置里单独设置,否则读出数据就是反的。RT-Thread的配置结构体里,这三个参数正好是对应的字段:RT_SPI_MODE_0到RT_SPI_MODE_3、RT_SPI_MSB、RT_SPI_LSB。

2.3 硬件片选还是软件片选,我为什么全面倒向软件

SPI外设一般都带有硬件NSS引脚,传输数据时硬件自动拉低CS,结束自动拉高。单从机场景这么用没问题,但工控里很少只有一个从机,而且很多时候一个从机的一次完整操作需要连续发多条消息,中间CS必须一直保持低电平。典型就是读Flash数据,先发命令字节和地址字节,再连续收数据,这期间CS不能断。如果用硬件NSS,CS的拉高拉低完全由外设管控,很难精确维持跨消息的持续时间。

所以我把所有SPI设备的片选都配成普通GPIO,用软件控制。CS作为GPIO输出,低电平选中,高电平释放,时序完全由代码控制,跨命令操作、多从机切换都很自由。GD32H759的GPIO翻转速度足够快,对SPI这种协议来说完全不是瓶颈。相应的代价是,每次访问从机前要先把CS引脚拉低,操作完再拉高,这个动作如果忘掉或者顺序错了,就会得到一堆0xFF或者读回全乱码。后面实战部分我会给出带CS控制的标准封装写法。

另外一个容易被忽略的点是CS高电平维持时间。从机数据手册里会有tSHSL之类的参数,表示CS释放到下次选通的最小间隔,通常是几十到几百纳秒。如果代码在一条消息结束后立刻拉低CS开始下一条消息,大概率没问题,但在高速模式下最好留一点余量。我的习惯是:需要跨消息保持的操作用同一段CS低电平,不需要的干脆拉高后再延时几百纳秒。

3. RT-Thread SPI设备框架接入细节

3.1 BSP驱动层的初始化和设备挂载

RT-Thread的GD32 BSP在drv_spi.c中实现了SPI总线驱动,应用层不用关心寄存器操作。首先要确认工程里已经使能了SPI驱动,menuconfig里选上对应的选项,然后确认板级初始化里为SPI外设分配了引脚。GD32H759的引脚复用比较灵活,SPI0的SCK、MOSI、MISO能映射到多组引脚,需要先在board或gpio配置里把复用功能设对,否则代码层面怎么调都白搭。

设备挂载采用rt_hw_spi_device_attach接口,它把一根总线上挂的从机抽象成独立设备。例如把W25Q64挂在spi0总线上,代码类似:

#include "drv_spi.h" static int rt_hw_spi_flash_init(void) { rt_hw_spi_device_attach("spi0", "spi00", GET_PIN(C, 4)); return RT_EOK; } INIT_COMPONENT_EXPORT(rt_hw_spi_flash_init);

这个函数做了三件事:根据总线名找到SPI总线、创建设备节点、把CS引脚和从机设备绑定。第三个参数GET_PIN(C, 4)是RT-Thread的引脚编号宏,表示GPIOC第4脚。如果CS引脚配置成了GPIO模式,框架内部会自动管理这个引脚的输出高低,应用层只需要知道从机设备名叫spi00就行。还有一点要注意,INIT_COMPONENT_EXPORT让这段初始化在系统组件初始化阶段执行,确保调用设备框架时驱动已经就绪。

3.2 应用层访问SPI设备的正确姿势

通过设备框架访问SPI从机,最常用的是这几个API。rt_spi_configure用来配置设备参数,包括主从模式、位宽、最大时钟和时序模式。rt_spi_send_then_recv最常用,它先通过CS发送一个命令,再在CS保持选中的情况下收数据,非常适合“发命令读响应”这种操作。rt_spi_send_then_send则适合连续发两段数据的场景。

直接收发数据的API底层都走消息结构体rt_spi_message,里面有个关键概念是cs_take和cs_release。这两个标志位控制消息发送前后的片选动作。比如一条消息设置了cs_take=1、cs_release=0,那么发送前自动拉低CS,发送后不拉高,接着下一条消息设置cs_take=0、cs_release=1,就能实现整段操作CS保持低电平。理解这个机制以后,你就能解释为什么单独调用rt_spi_send一次再调用rt_spi_recv一次,和调用rt_spi_send_then_recv一次的结果可以完全不同了。

我也见过不少人犯这个错:在应用里直接用rt_device_find找到SPI设备后,不做rt_spi_configure就发数据,出来的结果当然不稳定。SPI不是即插即用设备,每次从机上电后,至少要配置一次mode、max_hz、data_width。而且配置的速率别一上来就顶满,先低速验证通路,再往高速调,这是最基本的调试顺序。

3.3 多线程访问SPI总线的锁管理

RT-Thread是实时操作系统,工控项目里必然有多个线程,比如一个采集线程读ADC,一个配置线程写Flash。如果两个线程同时访问同一根SPI总线,消息会交错,从机必然乱套。设备框架为此提供了总线锁机制,rt_spi_take_bus和rt_spi_release_bus成对使用,拿锁期间其他线程无法操作这条总线上的任何设备。

设计多个线程共享SPI时,我建议每个从机操作封装成完整函数,内部先拿总线锁,再拿片选,等整段操作结束再释放片选、释放总线锁。别把锁放得太细,比如发命令拿一次锁、读数据再拿一次锁,中间CS已经释放,从机状态机早就乱了。以读W25Q64为例,一次完整读操作是发命令+地址+收数据,必须是一把锁内的事务。

另外要注意,不要在中断服务函数里调用rt_spi_send这类带锁的API,因为锁可能被别的线程占用,中断里无法阻塞等待,轻则丢失数据,重则直接死锁。如果需要从中断触发SPI操作,正确做法是中断服务里只释放信号量,让等待信号量的业务线程去执行SPI读写。

4. 实战一:用W25Q64做参数掉电存储

4.1 从读ID开始验证SPI通路

W25Q64是国产工控板上最常见的SPI NOR Flash,容量64Mbit,指令集是通用的。第一步先读JEDEC ID,命令是0x9F,返回三个字节,分别是厂商ID、设备类型、容量编码。这个操作最简单,专门用来验证SPI四根线有没有接对,SPI模式有没有配对。

先配置SPI设备参数:

#include <rtthread.h> #include <rtdevice.h> #include <drv_spi.h> #define SPI_FLASH_DEV_NAME "spi00" struct rt_spi_device *flash_dev = RT_NULL; static int spi_flash_config(void) { flash_dev = (struct rt_spi_device *)rt_device_find(SPI_FLASH_DEV_NAME); if (flash_dev == RT_NULL) { rt_kprintf("flash device not found.\n"); return -RT_ERROR; } struct rt_spi_configuration cfg; cfg.mode = RT_SPI_MODE_MASTER | RT_SPI_MSB | RT_SPI_MODE_0; cfg.data_width = 8; cfg.max_hz = 1000000; /* 初始跑1MHz,验证通过后再提高 */ rt_spi_configure(flash_dev, &cfg); return RT_EOK; } INIT_APP_EXPORT(spi_flash_config);

注意我用了RT_SPI_MODE_0,W25Q64支持Mode 0也支持Mode 3。然后读ID:

static rt_err_t w25q_read_jedec_id(rt_uint8_t id[3]) { rt_uint8_t cmd = 0x9F; rt_spi_send_then_recv(flash_dev, &cmd, 1, id, 3); return RT_EOK; }

rt_spi_send_then_recv内部会自动完成CS取用和释放,所以这段代码看起来干净。如果读回来ID是0xFF 0xFF 0xFF,那基本可以断定CS没选通、MOSI/MISO接反、速率太快或者引脚复用没配对。如果ID有效,说明通路无误,接下来就敢往Flash里写数据了。

4.2 擦除、写使能和页编程的完整事务

SPI NOR Flash写数据有个特点:只能把1写成0,要把0写成1必须先擦除。W25Q64擦除的最小单位是4KB扇区,对应命令0x20。但擦除前必须发写使能指令0x06,擦除或编程完成后还要轮询状态寄存器0x05里的WIP位,等到硬件自己忙完。这些步骤少一步都不行。

一个完善的扇区擦除函数是这样:

static rt_uint8_t w25q_read_sr(void) { rt_uint8_t cmd = 0x05; rt_uint8_t sr = 0; rt_spi_send_then_recv(flash_dev, &cmd, 1, &sr, 1); return sr; } static rt_err_t w25q_wait_busy(void) { while (w25q_read_sr() & 0x01) { rt_thread_mdelay(1); } return RT_EOK; } static void w25q_write_enable(void) { rt_uint8_t cmd = 0x06; rt_spi_send(flash_dev, &cmd, 1); } static rt_err_t w25q_sector_erase(rt_uint32_t sector_addr) { rt_uint8_t cmd[4]; cmd[0] = 0x20; cmd[1] = (sector_addr >> 16) & 0xFF; cmd[2] = (sector_addr >> 8) & 0xFF; cmd[3] = sector_addr & 0xFF; w25q_write_enable(); rt_spi_send(flash_dev, cmd, 4); return w25q_wait_busy(); }

这里的要点是写使能和擦除命令必须是两次独立传输,第一次发0x06,CS拉高,第二次发擦除命令,CS再拉低。这是Flash器件的要求,很多从单片机的判断固化成一步到位,结果经常出错。页编程命令0x02最多一次写256字节,如果数据跨页,必须拆成多次操作。还有一点,擦除粒度是4KB,我习惯把参数区规划成多个独立的4KB分区,每次更新只擦一个分区,另一个分区留作备份,掉电中途也不至于整份参数损毁。

4.3 直接用SFUD组件,Can't我们还是要省点事

其实W25Q64这种标准SPI NOR Flash在RT-Thread里有现成的SFUD组件,全称Serial Flash Universal Driver。只要在menuconfig里使能sfud,然后指定SPI设备名,系统启动后会自动识别Flash型号,挂载成块设备,配合FAL分区和文件系统就能像操作普通磁盘一样读写文件。这比自己写命令层省事太多。

我用SFUD的经验是,先把基础驱动跑通,确认读ID正常,然后打开SFUD,让组件自己探测。SFUD对W25Q64这类常见型号自动识别成功以后,再去看挂载上的块设备。工控项目最怕自己造轮子轮子还有bug,能复用社区成熟的组件一定要复用。不过理解底层命令还是有必要的,调试文件系统异常时,比如擦写失败、分区超界,你得知道文件系统底层到底在Flash上做了什么操作。

4.4 传输提速的关键:从1MHz到30MHz

硬件SPI跑1MHz肯定不是目标,调试阶段确认通路没问题后,我会把max_hz逐步提高。GD32H759的SPI时钟源来自APB,具体分频系数取决于驱动实现。比如APB给到150MHz,SPI分频2就是75MHz,分频4是37.5MHz,分频8是18.75MHz。W25Q64官方标称支持上百MHz,但实际板子上跑多少还得看布线。

提速时的指标很简单:反复读写几十次,数据比对无错误。我的经验是,正式工控板用短线布线,30MHz基本稳定;如果用杜邦线飞线调试,超过10MHz就经常出现偶发错位。还有一个被忽略的因素是SPI Clock的高电平脉宽和低电平脉宽是否对称,如果GPIO驱动能力和负载不匹配,波形畸变会导致从机采样出错。调试中发现速率上不去,先别急着怪芯片,看看波形比什么都管用。

5. 实战二:ADS1256高精度ADC采集

5.1 一个最容易被忽略的坑:ADS1256不是Mode 0

ADS1256是一颗8通道24位Delta-Sigma ADC,内部还有个可编程增益放大器PGA,增益可以设到64倍,专门用于称重传感器、热电偶、多路模拟量采集这些高精度场景。它的控制寄存器要通过SPI配置,转换结果也要通过SPI读出来,但是它的SPI时序默认是Mode 1,不是大多数人习惯的Mode 0。

之前有朋友照着网上代码抄,SPI明明配置成Mode 0,结果是读回的数据全是乱的,纳闷了很久。看ADS1256数据手册的时序图,数据在SCLK下降沿输出,在上升沿被锁存,这就是CPOL=0、CPHA=1,对应SPI Mode 1。所以配置要写成:

struct rt_spi_configuration adc_cfg; adc_cfg.mode = RT_SPI_MODE_MASTER | RT_SPI_MSB | RT_SPI_MODE_1; adc_cfg.data_width = 8; adc_cfg.max_hz = 1920000; /* SCLK最高不能超过输入时钟的1/4 */ rt_spi_configure(adc_dev, &adc_cfg);

SCLK上限同样要提醒。ADS1256的输入时钟通常是7.68MHz晶振,SCLK上限等于输入时钟的四分之一,也就是1.92MHz。SPI总线配得太高,从机根本处理不过来,数据就会丢失或者错位。

5.2 寄存器配置顺序和命令族

ADS1256的控制命令是一组单字节命令,常见的有WAKEUP、RDATA、RDATAC、SDATAC、WREG、RREG、SELFCAL。配置寄存器的流程一般是:先发SDATAC退出连续读模式,然后通过WREG命令把MUX、ADCON、DRATE寄存器写进去,最后发SELFCAL做一次自校准。

简单示意一下写ADCON寄存器的过程:

rt_uint8_t wreg[3]; wreg[0] = 0x52; /* 11010010: WREG命令,目标寄存器ADCON(0x02) */ wreg[1] = 0x00; /* 寄存器数量减一,只写1个寄存器 */ wreg[2] = 0x00; /* ADCON默认值,PGA=1,内部时钟输出关闭 */ rt_spi_send(adc_dev, wreg, 3);

MUX寄存器用来选通道,比如要测AIN0和AIN1之间的差分电压,MUX写0x01;要测单端AIN0相对于AINCOM,MUX写0x0C。DRATE寄存器配置采样率,具体数值要查数据手册的对应表,不同主频下同一个值对应的SPS也不同。这几个寄存器不配置对,后面读出来的数据要么是固定值,要么数值大得离谱。

配置完以后,我习惯读一遍寄存器回来校验。RREG命令是0x10加上寄存器地址,后面跟一个字节表示读几个寄存器减一,然后从设备里读出数据。把写进去的值读回来比对一致,再往下走,能省很多排查时间。

5.3 DRDY引脚与中断信号量配合的采集线程

ADS1256有一个DRDY引脚,低电平表示一次转换结果已经就绪。轮询这个引脚是最简单的,但工控里讲究实时性,转换完成到读取数据的延迟会影响有效采样率,所以我用RT-Thread的信号量配合外部中断。

先创建一个信号量,并把DRDY引脚配置成下降沿中断:

static struct rt_semaphore drdy_sem; static void drdy_irq_callback(void *args) { rt_sem_release(&drdy_sem); rt_interrupt_enter(); rt_interrupt_leave(); } void adc_drdy_init(void) { rt_sem_init(&drdy_sem, "drdy", 0, RT_IPC_FLAG_FIFO); rt_pin_attach_irq(DRDY_PIN, PIN_IRQ_MODE_FALLING, drdy_irq_callback, RT_NULL); rt_pin_irq_enable(DRDY_PIN, PIN_IRQ_ENABLE); }

实际使用中,外部中断回调里尽量不调复杂API,我这里的写法是典型的RT-Thread风格,中断里只释放信号量,实际SPI读写放在业务线程里。

读取一次转换结果的业务代码如下:

static rt_err_t adc_read_once(rt_uint8_t *stat, rt_uint8_t data[3]) { rt_uint8_t cmd = 0x01; /* RDATA命令 */ rt_sem_take(&drdy_sem, RT_WAITING_FOREVER); rt_spi_take_bus(adc_dev); rt_spi_configure(adc_dev, &adc_cfg); rt_spi_take_cs(adc_dev); rt_spi_send(adc_dev, &cmd, 1); rt_spi_recv(adc_dev, stat, 1); rt_spi_recv(adc_dev, data, 3); rt_spi_release_cs(adc_dev); rt_spi_release_bus(adc_dev); return RT_EOK; }

这里特意先take_bus再配置是因为多线程并发时,配置动作和发送动作必须是一体的,不能让别的线程在这个线程配置完成后插入操作。返回值里有状态字节,一般不用管,但三字节有效数据是24位二进制补码格式,转换公式是:

电压值 = 原始码 / 8388608.0 * (参考电压 / PGA增益)

假设参考电压2.5V,PGA=1,那么满量程就是±2.5V,24位补码最高位符号位,所以读回的值用int32_t处理后除8388608再乘满量程就行。

5.4 多通道切换需要注意的事情

ADS1256的8个输入通道通过MUX寄存器在转换前选通。我的做法是,在需要切换通道时,先退出连续读模式,写MUX寄存器,然后触发一次SELFCAL或者至少等待几个输出周期让内部滤波器稳定,再开始读数据。如果一写完MUX立刻读,大概率读到的还是上一个通道的残留数据。

工控采集系统经常会遇到干扰问题,尤其是ADC前端接近电机驱动或者开关电源时。PCB上SPI信号线要短,DRDY这类状态引脚可以加个小上拉,模拟输入端加RC滤波。软件上我还会做一步:对连续多次读到的原始值做中值滤波或者滑动平均,根据噪声水平调整。高精度ADC不是把芯片选好就完事,全局的布局布线占一半以上成败。

6. 踩坑实录与排查工具箱

6.1 数据全是0xFF:从最基础的链路开始查

读SPI设备返回0xFF,是最常见的故障现象,也最容易让人一头雾水。我总结了一条排查链路。第一步用示波器或逻辑分析仪看CS引脚,发送期间有没有拉低;如果CS一直是高,说明片选控制没生效,检查设备挂载时的引脚编号,以及GPIO是否被复用成了别的功能。第二步看SCK,有没有时钟输出;没时钟或者时钟纹波太大,检查SPI外设时钟使能和GPIO复用配置,重点看GD32H759的复用寄存器有没有设成AF功能。第三步看MOSI和MISO,MISO始终高大概率是电平不匹配或从机没被激活。

如果线路看起来都对,把SPI速率降到100kHz试试,很多从机在上电不稳或者时钟太快时会直接不响应,低速一下就正常了。我习惯把所有SPI调试都从最低速率开始,不是胆小,而是为了把变量数降到最低。

6.2 数据偶发错位、多一拍少一拍的疑难杂症

全对的情况下,偶发错位是最难定位的。有一种常见情况是CS拉高时机不对。有些SPI从机要求在最后一个SCK边沿之后再让CS变为高电平,甚至要求保持几十纳秒。如果用多个rt_spi_send拼操作,框架会在每条消息结束自动拉高CS,那问题就来了。解决办法是手动构造含媒体将由一个消息结构体的工具类消息,把整段时序放在一个CS事务里。

还有一种错位来自SPI外设的接收FIFO。主机发送字节和从机返回字节并不是同一拍完成的,对某些从机来说,最后一个发送字节要从机响应的数据可能在CS拉高时才真正准备好,软件立刻读FIFO就少了一个字节。解决办法是增加一个空闲时钟周期,比如发送完命令后再伪发送一个0x00,用来把从机的最后一个响应字节推出来。逻辑分析仪抓一下收发时序,错位问题一般立即显形。

6.3 Cortex-M7特有的Cache一致性坑

GD32H759是Cortex-M7核,带了D-Cache,这是好事,但对SPI加DMA传输来说是个大坑。CPU写数据到发送缓冲区,可能只有一部分进了DMA能看到的主存,其余还躺在Cache里;DMA接收的数据同样可能只写进了Cache尚未同步到主存,CPU去读缓冲内存时,读到的就是旧数据或者垃圾。症状是SPI用轮询方式完全正常,一换成DMA传输,数据就偶发不对。

解决办法分两种。一种是在DMA发送前调用清Cache,DMA接收后先无效化Cache再读数据。另一种更彻底,用MPU把DMA缓冲区所在内存区域配置成非缓存属性,这样DMA和CPU访问同一块内存时不会经过Cache,天然一致。工控项目里我倾向用MPU方式,省得每次传输都要在代码里手动维护Cache,减少出错概率。这个知识点在非M7核的单片机里根本碰不到,是从STM32F4这类M4核转到GD32H759最容易踩的坑。

6.4 排查工具和RT-Thread调试小技巧

调试SPI最好用的工具是带协议解析的逻辑分析仪,16通道以上最好,采样率至少20MHz。抓线就抓CS、SCK、MOSI、MISO四根,触发条件设成CS下降沿。然后根据抓到的波形核对三件事:CS低电平时长、SCK空闲电平、数据翻转边沿和采样边沿。只要这三点和从机数据手册完全对应,问题基本就缩小到具体寄存器配置了。

在RT-Thread环境里,我会在MSH命令行里注册一个简单的SPI测试命令,比如读一次W25Q64的ID。命令函数里固定速率跑一遍,然后把ID打印出来,以后每次改动配置都可以快速验证,不用重新烧录完整固件。用MSH的好处是可以实时操作,方便对比不同速率下的现象。开发阶段多花半小时把调试命令做好,后面排查问题的时间能省一大半。

最后分享一个我自己的心得:SPI调试中,绝大多数问题都不是芯片坏了,而是时序参数、片选控制和Cache一致性中的某一环没对上。所以遇到异常,不要急着改硬件、换芯片、骂驱动,老老实实拿逻辑分析仪把四根线的波形抓出来,对着数据手册逐行核对,问题通常都会很快水落石出。这个习惯帮我排掉了无数次工控板上的疑难杂症,也希望能帮到你。

返回列表