4. 设备树与Kconfig实战

在nRF Connect SDK(NCS)中,设备树(Devicetree)与Kconfig是两条并行的配置主线:设备树描述“硬件长什么样、外设挂在哪个地址、引脚如何连接”,Kconfig决定“哪些驱动和子系统被编译进镜像、以什么策略运行”。Zephyr构建系统在CMake阶段把两者合并,生成zephyr.dts、autoconf.h与devicetree_generated.h,最终驱动代码通过DT_*宏和CONFIG_*宏访问这些信息。本节以nRF52840 DK为例,完成一次从设备树节点新增、GPIO与UART配置、Kconfig裁剪到硬件行为验证的完整闭环。

一、先看清构建期两条配置线的汇合点

设备树源文件(.dts/.dtsi/.overlay)经预处理、合并、展开后生成最终设备树;Kconfig通过prj.conf、板级defconfig、Kconfig依赖关系求解出配置符号。两者在编译期分别产出宏定义,供C代码使用。理解这条链路,才能判断“改了配置为什么没生效”。

设备树与Kconfig在Zephyr构建中的汇合路径 nRF52840 DK 板级 .dts nrf52840dk_nrf52840.dts 自定义 .overlay app.overlay / boards/ 设备树编译与合并 dtc + 预处理 zephyr.dts 最终展开设备树 devicetree_generated.h DT_* 宏 Kconfig 源文件 Kconfig / Kconfig.* prj.conf / defconfig CONFIG_* 赋值 Kconfig 依赖求解 menuconfig / 符号依赖 .config 最终配置符号 autoconf.h CONFIG_* 宏 驱动 / 应用代码 DT_* 与 CONFIG_* 联合使用 构建期两条配置线在驱动层汇合,任一环节出错都会导致外设行为异常

二、为nRF52840 DK添加自定义设备节点

nRF52840 DK的板级设备树已经定义了leds、buttons、uart0等节点。我们要新增一个自定义节点,例如把P1.00~P1.03定义为“四路状态指示器”,并挂在一个自定义兼容字符串下,便于应用层通过DT_NODELABEL引用。

在工程根目录创建boards/nrf52840dk_nrf52840.overlay,内容如下:

/ {
    status_leds: status-leds {
        compatible = "demo,status-leds";
        status-gpios = <&gpio1 0 GPIO_ACTIVE_LOW>,
                       <&gpio1 1 GPIO_ACTIVE_LOW>,
                       <&gpio1 2 GPIO_ACTIVE_LOW>,
                       <&gpio1 3 GPIO_ACTIVE_LOW>;
        label = "status-leds";
    };
};

&gpio1 {
    status = "okay";
};

这里的关键点有三处:第一,compatible使用自定义字符串"demo,status-leds",Zephyr不会自动为其绑定驱动,但应用层仍可通过设备树宏读取属性;第二,status-gpios是自定义属性名,值类型为phandle-array,每个元素由GPIO控制器引用、引脚号、标志位组成;第三,&gpio1必须显式status = "okay",否则节点存在但驱动不会初始化。nRF52840的P1口对应gpio1,P0口对应gpio0,这是Nordic SoC的固定映射。

三、配置GPIO与UART外设

GPIO部分,除了设备树节点,还需要在prj.conf中打开GPIO驱动:

CONFIG_GPIO=y
CONFIG_GPIO_NRFX=y

应用代码中通过DEVICE_DT_GET获取GPIO控制器,再用GPIO_DT_SPEC_GET从自定义节点提取引脚描述:

#define STATUS_NODE DT_NODELABEL(status_leds)

static const struct gpio_dt_spec led0 =
    GPIO_DT_SPEC_GET_BY_IDX(STATUS_NODE, status_gpios, 0);

int main(void)
{
    if (!gpio_is_ready_dt(&led0)) {
        return -1;
    }
    gpio_pin_configure_dt(&led0, GPIO_OUTPUT_INACTIVE);
    gpio_pin_set_dt(&led0, 1);
    return 0;
}

UART部分,nRF52840 DK默认使用uart0,引脚为P0.06(TX)和P0.08(RX),波特率115200。若要在自定义节点上启用第二路UART,可在overlay中追加:

&uart1 {
    status = "okay";
    current-speed = <115200>;
    tx-pin = <6>;
    rx-pin = <8>;
};

并在prj.conf中确认CONFIG_SERIAL=y与CONFIG_UART_INTERRUPT_DRIVEN=y。注意:nRF52840的UART1与SPI1、TWI1共享外设资源,若同时启用需检查CONFIG_NRFX_UARTE1与相关引脚冲突。

四、通过Kconfig裁剪功能

裁剪的目标是“只保留本次验证需要的功能”。一个典型的裁剪前后对比如下:

配置项裁剪前裁剪后影响
CONFIG_BTyn移除蓝牙协议栈,节省约120KB Flash
CONFIG_NETWORKINGyn移除网络子系统
CONFIG_LOGyy保留日志,便于验证
CONFIG_LOG_MODE_IMMEDIATEny日志立即输出,避免缓冲延迟
CONFIG_ASSERTyy保留断言,便于排障
CONFIG_GPIOyy保留GPIO驱动
CONFIG_SERIALyy保留串口驱动

裁剪后执行west build -b nrf52840dk_nrf52840,观察构建输出中的FLASH与RAM占用变化。若裁剪后编译报错,常见原因是某个驱动被应用代码直接引用但对应CONFIG_*已关闭,此时应检查Kconfig依赖链,而不是盲目重新打开全部配置。

五、编译并验证硬件行为

完整构建命令如下:

west build -b nrf52840dk_nrf52840 -p always
west flash

验证分三步。第一步,用nrfjprog --memrd或J-Link RTT Viewer观察日志输出,确认gpio_is_ready_dt返回真、UART打印出启动信息。第二步,用万用表或示波器测量P1.00电平,确认执行gpio_pin_set_dt(&led0, 1)后引脚由高变低(因为配置了GPIO_ACTIVE_LOW)。第三步,通过串口发送字符,确认UART回显正常。若GPIO无动作,优先检查status = "okay"是否遗漏;若UART无输出,检查current-speed与终端波特率是否一致。

设备树属性名中的连字符在C宏中会转换为下划线,例如status-gpios对应status_gpios;GPIO_DT_SPEC_GET_BY_IDX的索引从0开始,越界不会报编译错误,但运行时会读到无效phandle。
修改overlay后必须使用-p always或删除build目录重新构建,否则CMake可能复用旧的设备树缓存,导致“改了没生效”的假象。

本节完成了一次从设备树节点定义、GPIO/UART配置、Kconfig裁剪到硬件行为验证的完整实践。核心判断依据是:设备树决定“引脚和地址”,Kconfig决定“驱动是否编译”,两者缺一不可;验证时先看日志、再测电平、最后查串口,逐层缩小问题范围。