☰
GD32H759 上 RT-Thread SPI 驱动实战:DMA、片选与时序避坑指南
2026/9/28 19:49:03 网站建设 项目流程

1. 为什么在 GD32H759 上跑 RT-Thread 还要单独聊 SPI

GD32H759 这颗片子最近在工控圈子里热度不低,Cortex-M7 内核、主频拉到 600MHz、带 TCM 和 Cache、外设资源也够猛,很多人拿它替换原来 STM32H7 的方案。但真到项目里跑起来,你会发现一个很现实的问题:CPU 再快,数据进不来出不去也是白搭。而 SPI 恰恰是工控板子上最忙的那条总线——外挂 NOR Flash 存参数、接 ADC 采模拟量、连编码器读位置、驱动以太网控制器,甚至跟 FPGA 做高速数据交换,全靠它。

我在一个多轴运动控制项目里用 GD32H759 + RT-Thread 做主轴,板子上挂了 W25Q64 存配方、MT6816 磁编码器读电机角度、还有一片 ADC 做电流环采样,三条 SPI 链路同时跑。刚开始图省事,全用 RT-Thread 的rt_spi_transfer同步接口,结果电流环 10kHz 的采样节拍直接被 SPI 阻塞拖垮,电机一加速就丢步。后来把 DMA、片选、时钟相位这些细节一个个抠出来,才把总线吞吐和实时性同时稳住。

这篇就围绕GD32H759 平台下 RT-Thread 的 SPI 驱动实战展开,从框架分层、设备注册、DMA 配置到片选时序、常见故障排查,把工控场景里真正会踩的坑讲透。适合已经能点亮 RT-Thread、但一上真实外设就各种玄学问题的嵌入式工程师,也适合从 STM32 转 GD32 想快速摸清差异的朋友。看完你至少能搞清楚:SPI 设备怎么挂到 RT-Thread 设备框架上、DMA 收发为什么容易卡死、硬件片选和软件片选到底怎么选、以及示波器上那些"看起来对但就是不通"的波形到底错在哪。

2. GD32H759 的 SPI 外设与 RT-Thread 驱动框架怎么对上

2.1 先搞清楚 GD32H759 的 SPI 硬件底子

GD32H759 的 SPI 外设跟 STM32H7 系列思路接近但寄存器细节有差异,工控项目里常用的几个特性得先摸清:

  • SPI0/SPI1/SPI2/SPI3/SPI4/SPI5 多路,其中 SPI0/SPI1 挂在 APB2,SPI2~SPI5 挂在 APB1,时钟源不同,分频系数算出来差别很大。
  • FIFO 深度 16 级,支持 8/16 位数据帧,工控里读编码器常用 16 位帧一次搞定。
  • 硬件 NSS 管理,可以配置成输出模式自动拉片选,也可以软件控制。
  • DMA 请求映射,每个 SPI 都有独立的 TX/RX DMA 通道,GD32H7 的 DMAMUX 比老 GD32F4 灵活得多。
  • 最高时钟,SPI0 在 APB2 上理论能跑到 100MHz 以上,但实际外设(比如 W25Q64)撑死 80MHz,得按从设备手册降频。

这里有个容易翻车的点:GD32H759 开了 Cache 和 TCM 之后,DMA 访问的内存区域必须是非 Cache 或者手动维护一致性的。我一开始把 SPI DMA 的收发 buffer 放在普通 SRAM 里,CPU 写进去的数据 DMA 读出来是旧的,读回来的数据 CPU 又看不到,调了半天以为是 SPI 时序问题,其实是 Cache 没处理。后来把 buffer 放到RT_NONCACHEABLE段或者用SCB_CleanDCache_by_Addr手动刷,问题立刻消失。

2.2 RT-Thread 的 SPI 设备框架分了几层

RT-Thread 的 SPI 框架是典型的"总线-设备"模型,理解分层是写好驱动的第一步:

层级作用典型接口
应用层业务代码调用rt_spi_send、rt_spi_recv、rt_spi_transfer
设备层抽象 SPI 从设备rt_spi_device、rt_spi_configure
总线层管理 SPI 控制器rt_spi_bus、rt_spi_bus_attach_device
驱动层操作寄存器/DMAstruct rt_spi_ops里的configure、xfer

关键点在于:总线(bus)和从设备(device)是分开注册的。总线对应 GD32H759 的一个 SPI 外设,从设备对应挂在它上面的 W25Q64、MT6816 这些具体芯片。一个 bus 上可以挂多个 device,各自有不同的 CS 引脚、时钟频率、SPI 模式。

我见过不少新手直接把 SPI 初始化写在应用里,绕过设备框架,结果多个任务同时访问同一条 SPI 就打架。用框架的好处是 RT-Thread 帮你做了总线互斥,rt_spi_transfer内部会拿总线锁,多任务访问自动串行化。

2.3 总线注册与从设备挂载的完整流程

在 GD32H759 上,标准流程分三步:

  1. 注册 SPI 总线:调用rt_hw_spi_init(),内部会填充rt_spi_bus结构体,绑定ops,然后rt_spi_bus_register()。
  2. 挂载从设备:用rt_spi_bus_attach_device()或rt_spi_bus_attach_device_cspin(),后者能直接指定 CS 引脚,框架自动管理片选。
  3. 配置设备参数:rt_spi_configure()设置模式(CPOL/CPHA)、数据位宽、时钟频率。

这里有个细节值得说:rt_spi_bus_attach_device_cspin是带片选管理的版本,它会注册一个 PIN 设备作为 CS,每次传输前自动拉低、传输后拉高。如果你的板子 CS 接的是普通 GPIO,用这个接口最省心。但如果 CS 接的是 SPI 外设自带的硬件 NSS 引脚,就得用不带 cspin 的版本,然后在configure里打开硬件 NSS。

3. SPI 模式、片选与时序:工控现场最容易翻车的三件事

3.1 SPI 四种模式到底怎么选

SPI 的 CPOL(时钟极性)和 CPHA(时钟相位)组合出四种模式,这是新手第一个大坑。工控外设里:

  • W25Q64 Flash:模式 0(CPOL=0, CPHA=0),空闲低电平,第一个边沿采样。
  • MT6816 磁编码器:模式 3(CPOL=1, CPHA=1),空闲高电平,第二个边沿采样。
  • 多数 ADC(如 AD7606):模式 2 或模式 0,看具体型号。

选错模式的典型症状是:读回来的数据整体偏移一位,或者全是 0xFF/0x00。因为采样边沿错了,MOSI 上的数据在错误的时刻被锁存。

我调 MT6816 的时候就栽过:手册上写"数据在时钟下降沿更新,上升沿采样",我按模式 0 配,读出来角度值一直在跳。后来拿示波器一看,MT6816 空闲时 SCLK 是高电平,必须配模式 3。改完立刻稳定。

提示:拿不准模式时,先看从设备手册的时序图,重点看 SCLK 空闲电平和第一个采样边沿。空闲高电平就是 CPOL=1,第一个边沿采样就是 CPHA=0。

3.2 硬件片选和软件片选的真实差异

这是工控项目里争论最多的话题之一。两种方式各有适用场景:

对比项硬件片选(NSS 自动)软件片选(GPIO 手动)
时序精度高,硬件自动拉依赖代码,有抖动
多从设备需要多个 NSS 或外部译码每个设备一个 GPIO,灵活
CPU 开销低每次传输要操作 GPIO
调试难度波形干净容易忘记拉高导致总线冲突
适用场景单从设备、高速传输多从设备、CS 时序有特殊要求

工控板子上通常挂多个 SPI 从设备,软件片选更常见。但软件片选有个致命细节:CS 拉低到第一个 SCLK 之间要有建立时间(tSU),最后一个 SCLK 到 CS 拉高之间要有保持时间(tHD)。W25Q64 要求 tSU 至少 5ns,看着很小,但如果你的 GPIO 操作和 SPI 传输之间没有延时,某些情况下会踩线。

我的做法是在rt_spi_bus_attach_device_cspin的基础上,如果从设备时序苛刻,就在xfer回调里手动加几个 NOP 或者用rt_hw_us_delay(1)保证建立时间。实测 W25Q64 在 80MHz 下不加延时也能跑,但 MT6816 在 10MHz 下必须加,否则偶尔读到错误角度。

3.3 时钟频率怎么算才不超从设备上限

GD32H759 的 SPI 时钟来自 APB 分频,公式是:

SPI_CLK = APB_CLK / (2^(PSC+1))

其中 PSC 是预分频系数(0~7)。假设 SPI0 挂在 APB2,APB2 时钟 200MHz,要得到 10MHz 的 SCLK:

10MHz = 200MHz / (2^(PSC+1)) 2^(PSC+1) = 20 PSC+1 ≈ 4.32

取整后 PSC=3,实际 SCLK = 200/16 = 12.5MHz。如果从设备上限是 10MHz,就得取 PSC=4,SCLK = 200/32 = 6.25MHz。

工控项目里宁可降频也不要超频。SPI 超频的后果不是立刻坏,而是偶发误码,在电磁干扰大的现场(变频器、伺服驱动器旁边)尤其明显。我一般会把计算出来的频率再降一档,留 20% 余量。

4. DMA 收发配置:让 SPI 不再阻塞你的实时任务

4.1 为什么工控场景必须上 DMA

回到开头那个电流环的例子。10kHz 采样意味着每 100us 要完成一次 SPI 传输。如果一次传输 16 位数据,SPI 时钟 10MHz,传输时间约 1.6us,看着不长。但 RT-Thread 的同步rt_spi_transfer是阻塞的,加上任务切换、总线加锁的开销,实际占用可能到 5~10us。如果电流环任务周期是 100us,SPI 就吃掉了 10% 的 CPU 时间,再加上其他任务,调度就开始抖。

上 DMA 之后,CPU 只需要配置 DMA 描述符、启动传输,然后就可以去干别的。传输完成通过中断或信号量通知。CPU 占用从 10% 降到接近 0,实时性立刻改善。

4.2 GD32H759 的 DMA 配置要点

GD32H759 用的是 DMAMUX + DMA 控制器架构,配置步骤比老款复杂:

  1. 使能 DMA 控制器时钟:rcu_periph_clock_enable(RCU_DMA0)。
  2. 配置 DMAMUX 请求源:把 SPI 的 TX/RX 请求映射到具体 DMA 通道。
  3. 配置 DMA 通道:方向、数据宽度、地址增量、优先级、传输数量。
  4. 使能 SPI 的 DMA 请求:spi_dma_enable(SPI0, SPI_DMA_TRANSMIT)。
  5. 启动传输:dma_channel_enable()。

关键坑点:GD32H759 的 DMA 访问 TCM 内存有限制。TCM(紧耦合内存)虽然快,但 DMA 不一定能访问。我一开始把 buffer 放在 TCM 里,DMA 死活不工作,查手册才发现 TCM 不在 DMA 可访问地址范围内。后来改放到 AXI SRAM 的非 Cache 段,问题解决。

4.3 RT-Thread 下 DMA 与设备框架的整合

RT-Thread 的 SPI 框架本身不强制用 DMA,但rt_spi_ops里的xfer回调可以自己实现 DMA 版本。我的做法是:

static rt_ssize_t gd32_spi_xfer(struct rt_spi_device *device, struct rt_spi_message *message) { struct gd32_spi *spi = (struct gd32_spi *)device->bus->parent.user_data; /* 配置片选 */ if (message->cs_take) rt_pin_write(device->cs_pin, PIN_LOW); /* 判断是否用 DMA */ if (message->length >= DMA_THRESHOLD) { gd32_spi_dma_transfer(spi, message); } else { gd32_spi_poll_transfer(spi, message); } if (message->cs_release) rt_pin_write(device->cs_pin, PIN_HIGH); return message->length; }

这里设了个DMA_THRESHOLD,短数据传输用轮询更快(省去 DMA 配置开销),长数据才走 DMA。工控里读 Flash 一页 256 字节走 DMA,读编码器 2 字节走轮询,实测效率最高。

注意:DMA 传输完成后必须等SPI_FLAG_TRANS和SPI_FLAG_RX_BUFFER_NOT_EMPTY都清零,再拉高 CS。我见过有人 DMA 中断一触发就拉 CS,结果最后一个字节还在移位寄存器里没发完,从设备收到半个字节。

5. 从 W25Q64 到 MT6816:三个真实外设的接入实录

5.1 W25Q64 Flash 的读写与 DMA 优化

W25Q64 是工控板上的常客,存配方、日志、固件备份。接入步骤:

  1. 挂载设备:rt_spi_bus_attach_device_cspin(&flash_dev, "spi10", "spi0", CS_PIN)。
  2. 配置模式 0、8 位、时钟 40MHz(W25Q64 上限 80MHz,留余量)。
  3. 读 ID 验证:发 0x9F,读回 0xEF4017。

读数据用0x03命令,页编程用0x02,扇区擦除用0x20。DMA 优化点在读大块数据:一次读 4KB 固件,轮询要几毫秒,DMA 只要几百微秒。

踩过的坑:W25Q64 写之前必须先擦除,而且擦除是按扇区(4KB)的。我有次直接写没擦,读回来全是旧数据,以为是 SPI 问题,查了半天才发现是 Flash 特性。

5.2 MT6816 磁编码器的角度读取

MT6816 是 14 位磁编码器,SPI 模式 3,读角度命令是 0x8300(16 位)。关键点:

  • 必须用 16 位数据帧,8 位帧读出来是错的。
  • CS 拉低后要等至少 1us 再发时钟,MT6816 需要准备时间。
  • 读回的数据高 2 位是状态位,低 14 位才是角度。
uint16_t mt6816_read_angle(void) { uint16_t cmd = 0x8300; uint16_t resp; rt_spi_transfer(mt6816_dev, &cmd, &resp, 1); return resp & 0x3FFF; /* 取低 14 位 */ }

实测在 10MHz 下,角度更新率能到 100kHz 以上,足够伺服环用。

5.3 多从设备共存的总线仲裁

三条 SPI 链路挂同一条总线时,片选管理是核心。RT-Thread 的总线锁保证同一时刻只有一个设备在传输,但前提是每个设备都正确配置了 CS。如果某个设备的 CS 忘记拉高,其他设备传输时它会误响应,导致数据错乱。

我的经验是:每个从设备的 CS 引脚单独定义宏,初始化时统一检查一遍。另外,如果两个设备时钟频率差很多(比如 Flash 40MHz、编码器 10MHz),每次切换设备都要重新rt_spi_configure,否则编码器会被 40MHz 时钟打挂。

6. 常见问题速查与排查技巧

6.1 SPI 通信故障排查表

现象可能原因排查方法
读回全 0xFFMISO 悬空或从设备未响应查 CS 是否拉低、从设备供电
读回全 0x00MOSI 没数据或时钟没输出示波器看 SCLK、MOSI
数据偏移一位SPI 模式错误核对 CPOL/CPHA
偶发误码时钟超频或干扰降频、加屏蔽、缩短走线
DMA 不工作buffer 在 TCM 或 Cache 未刷换非 Cache 段、手动刷 Cache
多设备冲突CS 未正确管理检查每个设备 CS 时序

6.2 示波器抓波形的三个关键点

调 SPI 离不开示波器,重点看三处:

  1. CS 和第一个 SCLK 的间隔:小于从设备 tSU 就会丢第一个位。
  2. SCLK 空闲电平:确认 CPOL 配置对不对。
  3. MISO 采样点:数据是否在正确的边沿稳定。

我一般用四通道示波器同时抓 CS、SCLK、MOSI、MISO,一眼就能看出问题在哪。

6.3 几个血泪教训

  • 不要在中断里调用阻塞式 SPI 传输,会拖垮整个系统。用 DMA + 信号量。
  • SPI 总线加锁后不要做耗时操作,否则其他任务全卡住。
  • 从设备手册的时序参数一定要看,tSU、tHD、最大时钟这些不是摆设。
  • GD32H759 的 Cache 和 DMA 是天生一对冤家,buffer 位置一定要规划好。

7. 我在 GD32H759 + RT-Thread SPI 实战中的几点体会

调 SPI 这件事,说到底就是时序、片选、内存三件事。时序对了,数据才能正确移位;片选对了,多设备才能和平共处;内存对了,DMA 才能跑得欢。GD32H759 性能强,但 Cache 和 TCM 带来的内存一致性问题,是它跟老款 GD32F4 最大的差异点,也是从 STM32 转过来的朋友最容易忽略的地方。

我现在做新项目,SPI 部分的标准动作是:先拿示波器确认波形,再上 RT-Thread 设备框架,最后根据数据量决定轮询还是 DMA。这套流程走下来,基本不会出大问题。如果非要说一个最容易被低估的点,那就是片选时序——它不像时钟频率那样显眼,但现场偶发故障十有八九跟它有关。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询