4. nRF SPIM/SPIS外设驱动与板级适配

nRF52/nRF53系列SoC中的SPIM与SPIS是带EasyDMA的增强型SPI主/从外设,它们与传统的SPI(SPI0~SPI3)共享同一组物理引脚,但寄存器接口和DMA机制完全不同。理解SPIM/SPIS的关键在于三点:EasyDMA如何搬运数据、CSN片选如何被硬件自动控制、以及Zephyr的SPI驱动如何把这些能力映射成统一的API。

SPIM/SPIS与EasyDMA的工作机制

SPIM(SPI Master with EasyDMA)在发送时,CPU只需把TX缓冲区地址写入 TXD.PTR、长度写入 TXD.MAXCNT,然后触发 TASKS_START,外设就会通过EasyDMA自动从RAM读取数据并逐字节移出;接收时同理,把RX缓冲区地址写入 RXD.PTR,硬件在SCK驱动下把MISO上的数据写入RAM。整个过程CPU不参与逐字节搬运,因此在高频SCK(如8 MHz)下也不会丢字节。SPIS(SPI Slave with EasyDMA)方向相反:由外部主机提供SCK和CSN,SPIS在CSN有效期间用EasyDMA完成收发,并通过 ACQUIRE/RELEASE 机制与CPU交换缓冲区所有权。

需要特别注意:SPIM/SPIS的EasyDMA要求缓冲区位于RAM中,且对地址对齐没有强制要求,但长度寄存器是8位或16位(视型号),单次传输最大长度受限。此外,SPIM的CSN可以由硬件自动控制(PSEL.CSN 指定引脚),也可以由软件通过GPIO手动拉低,这决定了片选是“外设自动”还是“驱动手动”。

nRF SPIM 主模式 EasyDMA 数据通路与片选控制 CPU / RAM TX Buffer (RAM) RX Buffer (RAM) TXD.PTR / RXD.PTR EasyDMA 自动搬运 TX/RX 无需 CPU 逐字节 MAXCNT 长度控制 SPIM 外设 SCK / MOSI / MISO CSN (PSEL.CSN) TASKS_START / STOP PTR 数据 外部从设备 Flash / 传感器 SCK/MOSI/MISO/CSN SPI 总线 CSN 片选控制 硬件自动:PSEL.CSN 软件手动:GPIO 拉低 Zephyr SPI API spi_write / spi_read spi_transceive 设备树配置(板级适配核心) spi1: &spi1 { compatible = "nordic,nrf-spim"; status = "okay"; cs-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>; clock-frequency = <8000000>; } 片选引脚、频率、模式均在设备树中声明,驱动自动解析

Zephyr SPI API与设备树片选配置

Zephyr把SPI控制器抽象为 spi_config 和 spi_cs_control 两层。控制器节点(如 spi1)在设备树中声明 compatible = "nordic,nrf-spim",并指定 clock-frequency、cs-gpios 等属性。每个挂载在SPI总线上的从设备作为子节点,通过 reg 属性选择 cs-gpios 数组中的第几个片选。例如:

&spi1 {
    compatible = "nordic,nrf-spim";
    status = "okay";
    sck-pin = <27>;
    mosi-pin = <26>;
    miso-pin = <28>;
    cs-gpios = <&gpio0 12 GPIO_ACTIVE_LOW>,
               <&gpio0 13 GPIO_ACTIVE_LOW>;
    clock-frequency = <8000000>;

    w25q64: flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <8000000>;
        size = <0x800000>;
    };

    lsm6dso: sensor@1 {
        compatible = "st,lsm6dso";
        reg = <1>;
        spi-max-frequency = <10000000>;
    };
};

这里 flash@0 的 reg = <0> 对应 cs-gpios 的第一个元素(GPIO0_12),sensor@1 的 reg = <1> 对应第二个(GPIO0_13)。驱动在调用 spi_transceive() 时,会根据设备指针自动拉低对应的CSN引脚,传输结束后再拉高。如果使用SPIM的硬件CSN(PSEL.CSN),则需要在设备树中把 cs-gpios 留空或使用 cs-gpios = <&spi1_csn> 这类特殊引用,具体取决于Nordic HAL的适配方式。

在nRF52840 DK上,SPI1的默认引脚为SCK=P0.27、MOSI=P0.26、MISO=P0.28。若使用SPIM3,注意其最大SCK频率可达32 MHz,但需要确认从设备支持。

连接外部Flash与传感器的实战要点

以W25Q64 Flash为例,它支持标准SPI模式0(CPOL=0, CPHA=0)和模式3,最高80 MHz。在Zephyr中,jedec,spi-nor 驱动会自动识别JEDEC ID并注册为MTD设备。初始化后可通过 flash_read()、flash_write()、flash_erase() 操作。关键参数是 spi-max-frequency,它会被驱动用来设置实际SCK频率;如果设置过高导致读取ID失败,可逐步降低到4 MHz或1 MHz排查。

对于LSM6DSO这类传感器,通常需要先读取 WHO_AM_I 寄存器(地址0x0F)确认通信正常,返回值应为0x6C。然后配置 CTRL1_XL(0x10)设置加速度计ODR和量程,配置 CTRL2_G(0x11)设置陀螺仪。每次读写通过 spi_transceive() 完成,注意传感器寄存器地址的最高位为读写标志:读操作时地址需或上0x80,写操作时保持原地址。

/* 读取 WHO_AM_I 示例 */
uint8_t tx_buf[2] = {0x0F | 0x80, 0x00};
uint8_t rx_buf[2];
struct spi_buf tx = {.buf = tx_buf, .len = 2};
struct spi_buf rx = {.buf = rx_buf, .len = 2};
struct spi_buf_set tx_set = {.buffers = &tx, .count = 1};
struct spi_buf_set rx_set = {.buffers = &rx, .count = 1};

spi_transceive(spi_dev, &spi_cfg, &tx_set, &rx_set);
/* rx_buf[1] 应为 0x6C */

常见问题与排查思路

SPIM/SPIS调试中最常见的问题是“读回全0或全0xFF”。全0通常意味着MISO没有正确连接或从设备未上电;全0xFF则可能是CSN未拉低、SCK无输出或从设备未响应。排查时先用逻辑分析仪抓取SCK、MOSI、MISO、CSN四根线,确认CSN在传输期间保持低电平、SCK有对应数量的时钟沿、MOSI上的地址与预期一致。如果使用EasyDMA,还要检查缓冲区是否在RAM中、长度是否超过MAXCNT限制。

另一个典型问题是SPIS从模式下的数据错位。SPIS依赖外部主机提供的CSN和SCK,如果主机在传输中途拉高CSN,SPIS会提前结束并触发 EVENTS_END。此时需要检查 AMOUNT 寄存器确认实际收发字节数,并在下一次传输前重新 ACQUIRE 缓冲区。Zephyr的SPIS驱动已经封装了这些状态机,但理解底层事件有助于定位时序问题。

SPIM与SPI(旧版)不能同时使用同一组引脚。如果设备树中同时使能了 spi0 和 spim0,编译会报引脚冲突。建议统一使用SPIM/SPIS节点。

板级适配的最后一步是验证频率与信号完整性。在8 MHz以上,走线过长或上拉电阻过大会导致SCK边沿变缓,从而在MISO上采样出错。此时可降低频率、缩短走线或减小上拉电阻(通常4.7 kΩ~10 kΩ)。通过 spi_transceive() 的返回值(0表示成功,负值表示错误)和实际读回的数据,可以判断通信是否稳定。