找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

5089

积分

0

好友

695

主题
发表于 4 小时前 | 查看: 2| 回复: 0

在云栈社区的技术交流中,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 全是 00FF,说明数据没回来,检查:

  1. MOSI 和 MISO 是否真的短接了?
  2. 设备树中 pinctrl 的引脚宏是否正确?
  3. SPI 模式是否匹配?

SPI5 Loopback 测试结果截图:循环 93-96 的 TX/RX 数据对比验证无误


六、常见踩坑清单

踩坑点 现象 解决方案
忘写 status = "okay" device_is_ready() 返回 false overlay 中必须启用外设
忘配 pinctrl 引脚没有 AF 复用,信号出不来 添加 pinctrl-0pinctrl-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 配置完全不用改。更多类似案例可参考开源实战中的嵌入式项目。





上一篇:DeepSeek V4 / GPT-5.5 / Qwen3.6 大模型横评:长上下文与RAG实战
下一篇:看懂银狐源码需哪些安全知识?完整学习书单与路线
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-27 23:42 , Processed in 0.883444 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表