2. nRF GPIO外设驱动与板级适配

在 nRF 系列 SoC 上,GPIO 是最基础也最容易被低估的外设。它既是 LED、按键、继电器、传感器中断等板级功能的直接承载者,也是 pinctrl、SPI、I2C、UART 等外设引脚复用的底层资源。理解 GPIO 寄存器与 Zephyr GPIO API 的对应关系,再结合 pinctrl 设备树绑定,才能把“芯片引脚”真正落到“板级功能”上。

一、nRF GPIO 寄存器模型

nRF52/nRF53 的 GPIO 以 P0、P1 两个端口组织,每个端口有独立的寄存器组。核心寄存器包括:

  • OUT / OUTSET / OUTCLR / OUTINV:输出写、置位、清零、翻转。OUTSET 与 OUTCLR 是原子操作,避免读改写竞争。
  • IN:读取引脚当前电平。
  • DIR / DIRSET / DIRCLR:方向控制,1 为输出,0 为输入。
  • PIN_CNF[n]:每引脚配置,含 DIR、INPUT、PULL、DRIVE、SENSE。
  • DETECTMODE:选择沿检测来自 PIN_CNF 还是 LATCH。

其中 PIN_CNF 的 PULL 字段决定上下拉:0 为禁用,1 为下拉,3 为上拉;DRIVE 决定输出驱动能力(S0S1、H0H1、S0H1 等);SENSE 决定是否使能沿检测,配合 GPIOTE 使用。按键输入通常配置为 INPUT=1、PULL=上拉、SENSE=高电平或低电平触发。

二、Zephyr GPIO API 与寄存器映射

Zephyr 通过 gpio_pin_configure()、gpio_pin_set()、gpio_pin_get()、gpio_pin_interrupt_configure() 等 API 封装寄存器。以按键为例:

static const struct gpio_dt_spec key = GPIO_DT_SPEC_GET(DT_ALIAS(sw0), gpios);

gpio_pin_configure_dt(&key, GPIO_INPUT | GPIO_PULL_UP);
gpio_pin_interrupt_configure_dt(&key, GPIO_INT_EDGE_TO_ACTIVE);
gpio_init_callback(&cb, key_isr, BIT(key.pin));
gpio_add_callback(key.port, &cb);

这里 GPIO_DT_SPEC_GET 从设备树读取端口、引脚号与标志,gpio_pin_configure_dt 最终写入 PIN_CNF,gpio_pin_interrupt_configure_dt 则配置 SENSE 并连接 GPIOTE 通道。LED 输出则使用 GPIO_OUTPUT_ACTIVE 或 GPIO_OUTPUT_INACTIVE,通过 gpio_pin_set_dt() 控制亮灭。

三、引脚复用与 pinctrl 设备树绑定

nRF 的引脚复用不是通过 GPIO 寄存器完成,而是由 pinctrl 在设备树中声明。每个外设节点通过 pinctrl-0 引用一个 pinctrl 状态,状态内用 psels 指定引脚与功能,例如 UART0 的 TX/RX:

&uart0 {
    pinctrl-0 = <&uart0_default>;
    pinctrl-1 = <&uart0_sleep>;
    pinctrl-names = "default", "sleep";
};

&pinctrl {
    uart0_default: uart0_default {
        group1 {
            psels = <NRF_PSEL(UART_TX, 0, 6)>,
                    <NRF_PSEL(UART_RX, 0, 8)>;
        };
    };
};

关键点:同一引脚不能同时被两个外设占用;NRF_PSEL 的端口与引脚号必须与原理图一致;sleep 状态通常把引脚设为 NRF_PSEL_DISCONNECTED 以降低功耗。GPIO 本身不需要 pinctrl,但一旦某引脚被 pinctrl 分配给 UART/SPI,就不能再用作普通 GPIO。

nRF GPIO 驱动与板级适配关系图 设备树 (DTS) sw0: gpios = <&gpio0 13 GPIO_ACTIVE_LOW>; led0: gpios = <&gpio0 17 GPIO_ACTIVE_LOW>; uart0 pinctrl-0 → NRF_PSEL(UART_TX,0,6) Zephyr GPIO API gpio_pin_configure_dt() gpio_pin_set_dt() gpio_pin_get_dt() gpio_pin_interrupt_configure_dt() gpio_add_callback() gpio_init_callback() nRF GPIO 寄存器 PIN_CNF[n]: DIR/INPUT/PULL OUT / OUTSET / OUTCLR IN DIR / DIRSET / DIRCLR SENSE → GPIOTE DETECTMODE 按键 SW0 P0.13 上拉输入 低电平有效 LED0 P0.17 推挽输出 低电平点亮 UART0 P0.06 TX / P0.08 RX pinctrl 分配 GPIOTE 沿检测中断 回调分发 适配要点:① 引脚号必须与原理图一致;② 同一引脚不可被 GPIO 与 pinctrl 同时占用; ③ 按键需配置上拉与沿检测;④ LED 输出注意 ACTIVE_LOW 极性;⑤ sleep 状态断开引脚以省电。

四、按键与 LED 驱动实现

按键驱动通常采用“设备树别名 + 回调”的方式。以 sw0 为例,先通过 GPIO_DT_SPEC_GET 获取端口与引脚,再配置为输入上拉,最后注册中断回调。回调中应避免长时间阻塞,只做标志置位或消息投递,由工作队列处理消抖与业务逻辑。消抖可在回调中记录时间戳,若两次触发间隔小于 20 ms 则忽略。

LED 驱动更简单:配置为输出,通过 gpio_pin_set_dt() 控制。若设备树中标志为 GPIO_ACTIVE_LOW,则 gpio_pin_set_dt(&led, 1) 实际输出低电平点亮 LED,逻辑与物理电平解耦,这是板级适配中必须注意的极性约定。

五、自定义板级引脚映射实战

假设有一块自定义 nRF52840 板,按键接 P0.11(低有效),LED 接 P0.15(低有效),UART TX/RX 接 P0.06/P0.08。板级适配步骤如下:

  1. 在 boards/<vendor>/<board>/<board>.dts 中定义 leds、buttons 节点,并设置 gpios 属性与极性。
  2. 在 &pinctrl 中新增 uart0_default 与 uart0_sleep,用 NRF_PSEL 指定 P0.06 与 P0.08。
  3. 在 &uart0 中引用 pinctrl-0 与 pinctrl-1,并设置 pinctrl-names。
  4. 在 board.c 或应用层通过 DT_ALIAS(sw0)、DT_ALIAS(led0) 获取设备,调用 GPIO API 完成初始化。
  5. 编译后用 west build -b <board> 验证设备树是否通过,再用逻辑分析仪或示波器观察 P0.06 波形确认 UART 输出。
调试建议:若按键无响应,先确认 PIN_CNF 的 PULL 与 SENSE 是否与硬件一致;若 UART 无输出,检查 pinctrl 中引脚号是否与原理图一致,以及该引脚是否被其他外设占用。
常见错误:把 GPIO 与 pinctrl 同时分配给同一引脚,会导致外设行为异常;设备树中 GPIO_ACTIVE_LOW 与硬件实际极性不符,会造成 LED 逻辑反相。

掌握寄存器与 API 的对应关系、pinctrl 的绑定规则,以及按键/LED 的极性约定,就能把 nRF 的 GPIO 从“能亮能读”提升到“可维护、可移植”的板级驱动层。