在云栈社区的技术交流中,SPI 调试是个常见难题。今天我们将通过一根杜邦线,实现 GD32F450 的 SPI Loopback 测试,从设备树到应用层全解析。
SPI 驱动写好了,数据发出去却收不回来,到底是硬件问题还是软件问题?解决办法——Loopback 自发自收,一根线验证整个 SPI 链路,从此告别“玄学调试”!
一、为什么需要 Loopback 测试?
SPI 调试最大的痛点在于:无法确定问题出在哪一层。
- 设备树配错了?→ 外设时钟没开,引脚复用不对
- 驱动有问题?→ 数据帧格式、时钟极性/相位不匹配
- 硬件有问题?→ 接线松动、上拉缺失、信号干扰
- 从设备有问题?→ 芯片没响应、协议时序不对
Loopback(回环)测试的核心思路很简单:把 MOSI 和 MISO 短接,让发送的数据自己回到接收端。如果自发自收成功,说明从设备树到驱动到应用层的整条链路都是通的,问题一定在外部从设备或连线上。
这就像网络工程师用 ping 127.0.0.1 验证协议栈一样——先确认自己没问题,再排查外部。
二、GD32F450 的 SPI 资源一览
GD32F450 最多有 6 个 SPI 外设(SPI0~SPI5),其中 SPI0~SPI2 属于 APB2 总线(高速),SPI3~SPI5 属于 APB1 总线。
本文选用 SPI5,它在 Zephyr 的设备树中定义如下(位于 gd32f450.dtsi):
spi5: spi@40015400 {
compatible = "gd,gd32-spi";
reg = <0x40015400 0x400>;
interrupts = <86 0>;
clocks = <&cctl GD32_CLOCK_SPI5>;
resets = <&rctl GD32_RESET_SPI5>;
status = "disabled";
};
注意 status = "disabled"——Zephyr 的设备树默认关闭所有外设,必须通过 overlay 或板级 DTS 显式启用。
https://gitee.com/xiaofeng21/zephyr/blob/main/dts/arm/gd/gd32f4xx/gd32f450.dtsi
引脚映射
我们选择的引脚方案:
| 引脚 |
SPI 功能 |
复用功能 (AF) |
pinctrl 宏 |
| PG13 |
SCK (时钟) |
AF5 |
SPI5_SCK_PG13 |
| PG14 |
MOSI (主出从入) |
AF5 |
SPI5_MOSI_PG14 |
| PG12 |
MISO (主入从出) |
AF5 |
SPI5_MISO_PG12 |
| PG9 |
CS (片选) |
GPIO 软件控制 |
cs-gpios |
这些宏定义在 dt-bindings/pinctrl/gd32f450z(e-g-i-k)xx-pinctrl.h 中,由 HAL 模块自动生成,直接使用即可。
https://github.com/GD32-MCU-IOT/hal_gigadevice_zephyr/blob/main/include/dt-bindings/pinctrl/gd32f450z(e-g-i-k)xx-pinctrl.h
三、四步配置法:从零到 SPI 通信
Zephyr 的 SPI 配置遵循一个清晰的分层架构:
┌─────────────────────────────────────────┐
│ 应用层 (main.c) │ ← spi_transceive()
├─────────────────────────────────────────┤
│ Kconfig (prj.conf) │ ← CONFIG_SPI=y
├─────────────────────────────────────────┤
│ 设备树 Overlay (.overlay) │ ← 启用外设、配置引脚
├─────────────────────────────────────────┤
│ 芯片级 DTS (gd32f450.dtsi) │ ← SPI5 基地址、中断号
│ 板级 DTS (gd32f450z_eval.dts) │ ← pinctrl 定义
└─────────────────────────────────────────┘
第 1 步:设备树 Overlay——告诉硬件“谁该工作”
创建文件 boards/gd32f450z_eval.overlay:
/* 定义 SPI5 的引脚复用 */
&pinctrl {
spi5_default: spi5_default {
group1 {
pinmux = <SPI5_SCK_PG13>, <SPI5_MOSI_PG14>,
<SPI5_MISO_PG12>;
};
};
};
/* 启用 GPIOG 端口(PG9/PG12/PG13/PG14 都在 Port G 上) */
&gpiog {
status = "okay";
};
/* 启用 SPI5 并绑定引脚和 CS */
&spi5 {
status = "okay"; /* 关键:激活外设 */
pinctrl-0 = <&spi5_default>; /* 绑定引脚复用 */
pinctrl-names = "default";
cs-gpios = <&gpiog 9 GPIO_ACTIVE_LOW>; /* PG9 作为软件 CS */
};
逐行解读:
status = "okay" 是灵魂——没有它,驱动根本不会初始化
pinctrl-0 把 PG12/PG13/PG14 配置为 SPI5 的 AF5 复用功能,而非普通 GPIO
cs-gpios 指定 PG9 为软件片选,Zephyr 驱动会在每次传输前后自动拉低/拉高
第 2 步:Kconfig 配置——告诉编译系统“我需要什么”
prj.conf:
CONFIG_SPI=y # 启用 SPI 子系统(会自动选中 SPI_GD32 驱动)
CONFIG_GPIO=y # CS 引脚需要 GPIO 支持
CONFIG_SERIAL=y # 串口控制台
CONFIG_UART_CONSOLE=y
CONFIG_PRINTK=y # printk 输出
重点:CONFIG_SPI=y 会通过 depends on DT_HAS_GD_GD32_SPI_ENABLED 的依赖链自动选中 SPI_GD32 驱动——不需要手动配置驱动选项。
第 3 步:应用代码——五层结构写好 SPI 通信
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/devicetree.h>
#include <string.h>
核心数据结构关系(这是理解 Zephyr SPI API 的关键):
spi_config → 一次 SPI 通信的配置(频率、模式、CS)
│
├── spi_buf_set → 一组缓冲区的集合
│ │
│ └── spi_buf → 单个缓冲区(buf 指针 + len 长度)
│
└── spi_transceive(spi_dev, &spi_cfg, &tx_set, &rx_set)
关键代码片段:
/* ① 获取设备 */
static const struct device *spi_dev = DEVICE_DT_GET(DT_NODELABEL(spi5));
/* ② 配置 SPI 参数 */
static struct spi_config spi_cfg = {
.frequency = 1000000, /* 1 MHz */
.operation = SPI_OP_MODE_MASTER | /* 主机模式 */
SPI_WORD_SET(8) | /* 8-bit 字长 */
SPI_TRANSFER_MSB | /* MSB 优先 */
SPI_MODE_CPOL | /* CPOL=1 */
SPI_MODE_CPHA, /* CPHA=1 → Mode 3 */
.cs = {
.gpio = SPI_CS_GPIOS_DT_SPEC_GET(DT_NODELABEL(spi5)),
},
};
/* ③ 准备缓冲区 */
uint8_t tx_buf[16] = { /* 发送数据 */ };
uint8_t rx_buf[16] = {0};
struct spi_buf tx_spi_buf = { .buf = tx_buf, .len = 16 };
struct spi_buf rx_spi_buf = { .buf = rx_buf, .len = 16 };
struct spi_buf_set tx_set = { .buffers = &tx_spi_buf, .count = 1 };
struct spi_buf_set rx_set = { .buffers = &rx_spi_buf, .count = 1 };
/* ④ 执行传输 */
spi_transceive(spi_dev, &spi_cfg, &tx_set, &rx_set);
/* ⑤ 验证结果 */
if (memcmp(tx_buf, rx_buf, 16) == 0) {
printk("Loopback 成功!\n");
}
第 4 步:编译烧录
west build -b gd32f450z_eval -p # 全新构建
west flash # 烧录
四、SPI 四种模式,选哪个?
这是新手最容易困惑的地方。SPI 有 4 种工作模式,由 CPOL(时钟极性)和 CPHA(时钟相位)决定:
| 模式 |
CPOL |
CPHA |
空闲时钟 |
采样边沿 |
代码写法 |
| Mode 0 |
0 |
0 |
低电平 |
第一个边沿(上升沿) |
默认,不设 CPOL/CPHA |
| Mode 1 |
0 |
1 |
低电平 |
第二个边沿(下降沿) |
SPI_MODE_CPHA |
| Mode 2 |
1 |
0 |
高电平 |
第一个边沿(下降沿) |
SPI_MODE_CPOL |
| Mode 3 |
1 |
1 |
高电平 |
第二个边沿(上升沿) |
SPI_MODE_CPOL | SPI_MODE_CPHA |
本例选用 Mode 3,这是大多数 SPI Flash 和传感器的常用模式。如果通信不正常,首先检查模式是否匹配——这是排名第一的 SPI 排障项。
五、实操验证:一根杜邦线的魔法
硬件操作:用杜邦线将 PG14 (MOSI) 和 PG12 (MISO) 短接。
预期串口输出:
GD32F450 SPI5 Loopback Test
Please connect MOSI(PG14) <-> MISO(PG12)
CS GPIO (PG9) is ready
SPI5 device: spi@40015400 is ready
SPI frequency: 1000000 Hz, Mode: 3 (CPOL=1, CPHA=1)
Loop 0:
TX: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
RX: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
OK: Loopback data matches!
Loop 1:
TX: 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10
RX: 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10
OK: Loopback data matches!
如果 RX 全是 00 或 FF,说明数据没回来,检查:
- MOSI 和 MISO 是否真的短接了?
- 设备树中
pinctrl 的引脚宏是否正确?
- SPI 模式是否匹配?

六、常见踩坑清单
| 踩坑点 |
现象 |
解决方案 |
忘写 status = "okay" |
device_is_ready() 返回 false |
overlay 中必须启用外设 |
| 忘配 pinctrl |
引脚没有 AF 复用,信号出不来 |
添加 pinctrl-0 和 pinctrl-names |
| 忘开 GPIO 端口 |
CS 引脚无法控制 |
overlay 中启用 &gpiog { status = "okay"; } |
| SPI 模式不匹配 |
数据错乱或全 0xFF |
确认从设备的 CPOL/CPHA 要求 |
| CS 极性错误 |
从设备不被选中 |
大多数从设备低电平有效(GPIO_ACTIVE_LOW) |
| 缓冲区未对齐 |
DMA 模式下数据异常 |
非 DMA 模式通常无此问题 |
七、从 Loopback 到真实项目
Loopback 验证通过后,就可以接入真实从设备了。只需修改 overlay 添加子节点:
&spi5 {
status = "okay";
pinctrl-0 = <&spi5_default>;
pinctrl-names = "default";
cs-gpios = <&gpiog 9 GPIO_ACTIVE_LOW>;
/* 接入 SPI Flash */
nor_flash: gd25q16@0 {
compatible = "jedec,spi-nor";
reg = <0>;
spi-max-frequency = <4000000>;
jedec-id = [c8 40 15];
};
};
应用层改用 Flash API 读写即可——底层 SPI 配置完全不用改。更多类似案例可参考开源实战中的嵌入式项目。