Zephyr-设备树原理和使用

目录

1 介绍

2 dts构建流程

3 dts binds

3.1 作用

3.2 如何匹配设备树节点

4 .overlays使用

4.1 重写节点属性

4.2 添加aliase和chosen节点

4.3 添加子节点

5 API使用

5.1 使用注意事项

5.2 获取节点标识符

5.3 DT_INST

5.4 获取节点属性

1)节点是否有该属性

2)简单属性获取

3)reg属性获取

4)获取struct device


1 介绍

        zephyr的设备树管理和linux类似,需要提前了解一些linux设备树的知识。设备树简单理解就是将硬件相关数据例如几个I2C,每个I2C控制器的寄存器地址等等,统一按设备树的结构独立于内核进行配置和修改,这里需要了解的就是设备树语法知识。有了设备树文件后,如何让内核代码读取这些信息,进行板级初始化了?linux的做法是编译成DTB文件,然后在内核启动时进行解析,逐一获取硬件数据。Zephyr系统设计的前提是资源受限的小型系统,这里将大量解析工作放到构建编译阶段,通过脚本将设备树文件中的各数据转换为头文件,使用各种宏替代,然后通过API给内核使用者调用。

2 dts构建流程

  • 每一个支持的board都有自己的默认.dts/.dtsi文件,用来描述硬件,一般都在boards///路径下
  • .overlay同样也是dts文件,是用来扩展或者修改默认配置的。主要使用场景:
    • 应用工程目录中使用.overlay文件,修改板级默认配置,并且仅仅作用于本应用工程。这样方便用户不修改内核原始代码的情况下,单独为某个应用的设备树进行扩展或者修改。
    • 在zephyr/Shields目录下,作为某些板级扩展
  • .dts/.dtsi和.overlay文件通过预处理合并成.dts.pre.temp文件,通过dts编译进行设备树的语法检查,提供报错信息
  • .dts.pre.temp将每个节点匹配到对应的binds文件(具体见后续dts binds分析)
  • .dts.pre.temp文件通过脚本文件生成zephyr.dts文件,做为设备树文件的最终产物。可方便用户查看设备树配置的对不对(后面转换为头文件,是各种宏,不方便像设备树文件这样直观的阅读)
  • .dts.pre.temp文件通过脚本文件输出devicetree.unfixed.h头文件,里面就是将设备树各节点信息转换为宏替代。
  • devicetree.fixup.h由于历史原因,现在已不建议使用。
  • 最终提供给内核用户的是devicetree.h,里面包含了devicetree.unfixed.h以及获取数据的API

3 dts binds

        设备树绑定描述了对节点内容的要求,并提供关于有效节点内容的语义信息。Zephyr设备树绑定是自定义格式的YAML文件(Zephyr不使用Linux内核使用的dt-schema工具)

3.1 作用

在配置阶段,构建系统尝试将设备树中的每个节点匹配到对应绑定文件。当此操作无误时,构建系统在验证节点的内容和为节点生成宏时都将使用绑定文件中的信息

  • 以官方举例说明
/* 这是一个设备树文件的一个节点信息 */
bar-device {
     compatible = "foo-company,bar-device";
     num-foos = <3>;
};

//这是该节点对应的binds文件
//通过属性compatible进行匹配
//约束了节点bar-device相关信息
compatible: "foo-company,bar-device"
properties:
  num-foos:
    type: int        //约束数据类型时int
    required: true   //约束了该节点必须有num-foos,否则构建时会出错

3.2 如何匹配设备树节点

  • 通过属性compatible将binds和节点进行匹配
  • 如果节点属性compatible有多个字符描述时,就逐一匹配

4 .overlays使用

  • .overlay同样也是dts文件,是用来扩展或者修改板级设备树配置的。主要使用场景:
    • 应用工程目录中使用.overlay文件,修改板级默认配置,并且仅仅作用于本应用工程。这样方便用户不修改内核原始代码的情况下,单独为某个应用的设备树进行扩展或者修改。
    • 在zephyr/Shields目录下,作为某些板级扩展使用

4.1 重写节点属性

//在<board>.dts有如下节点
/ {
        soc {
                serial0: serial@40002000 {
                        status = "okay";
                        current-speed = <115200>;
                        /* ... */
                };
        };
};

//在.overlays文件中有2种方式重新配置属性
/* Option 1 */
&serial0 {
     current-speed = <9600>;
};

/* Option 2 */
&{/soc/serial@40002000} {
     current-speed = <9600>;
};

4.2 添加aliase和chosen节点

//例如<board>.dts中有&serial0节点,在overlay文件中单独添加aliase和chosen,方便使用
/ {
     aliases {
             my-serial = &serial0;
     };
};

/ {
     chosen {
             zephyr,console = &serial0;
     };
};

4.3 添加子节点

        经常会在SPI,I2C这样的bus node中添加子设备节点,此时就可以使用到.overlay文件添加.

//例如<board>.dts中有&spi1节点,下面就可以在其添加子设备节点
/* SPI device example */
&spi1 {
     my_spi_device: temp-sensor@0 {
             compatible = "...";
             label = "TEMP_SENSOR_0";
             /* reg is the chip select number, if needed;
              * If present, it must match the node's unit address. */
             reg = <0>;

             /* Configure other SPI device properties as needed.
              * Find your device's DT binding for details. */
             spi-max-frequency = <4000000>;
     };
};

5 API使用

        前面已经讲过,和linux设备树最大的差异就是,zephyr在构建过程中将设备树文件各节点信息转换为宏定义,最终生成头文件devicetree.h,提供相应的API给内核使用者调用。

5.1 使用注意事项

  • 设备树中的非字母数字的字符,在使用API时都需要转换为下划线 _
  • 大写字母都需要转换为小写字母

5.2 获取节点标识符

1)几种方式

获取一个节点的办法有如下几种:

  • DT_PATH():通过节点的全路径(从根节点开始)获取
  • DT_NODELABEL():通过节点的NodeLabel获取
  • DT_ALIAS():通过alisa属性获取
  • DT_CHOSEN():通过/chosen 属性获取
  • DT_INST():通过实例号来获取
  • 举例说明如何使用
/dts-v1/;

/ {

	aliases {
		sensor-controller = &i2c1;
	};

	soc {
		i2c1: i2c@40002000 {
			compatible = "vnd,soc-i2c";
			label = "I2C_1";
			reg = <0x40002000 0x1000>;
			status = "okay";
			clock-frequency = < 100000 >;
		};
	};
};

//如何获取i2c@40002000节点了?
//注意:非字母数字的字符都需改为下划线,大写字母都需改为小写字母
DT_PATH(soc, i2c_40002000)
DT_NODELABEL(i2c1)
DT_ALIAS(sensor_controller)
DT_INST(x, vnd_soc_i2c)

2)注意事项

  • API返回的节点标识符不是一个值,代码实际使用时使用宏替代,如下
//下面的写法会编译出错
void *i2c_0 = DT_INST(0, vnd_soc_i2c);
unsigned int i2c_1 = DT_INST(1, vnd_soc_i2c);
long my_i2c = DT_NODELABEL(i2c1);

//应该使用宏替代
#define MY_I2C DT_NODELABEL(i2c1)

#define INST(i) DT_INST(i, vnd_soc_i2c)
#define I2C_0 INST(0)
#define I2C_1 INST(1)

5.3 DT_INST

其他的API比较好理解,这里单独详细描述下DT_INST()的使用。

在设备树中,具有相同compatible属性的节点,会通过实例号来区分,一般常用与驱动描述中。但存在实例号分配不确定性,看下面的举例就明白了

//具设备树有同样compatible的3个节点,通过实例号0,1,2来区分。
//首先看节点状态,由于节点serial@40002000和serial@40003000是使能的,所有会分配实例号0和1,但不保证谁一定分配0或者1。
//节点serial@40001000状态是不使能的,所有分配最大实例号2.
serial@40001000 {
        compatible = "vnd,soc-serial";
        status = "disabled";
        current-speed = <9600>;
        ...
};

serial@40002000 {
        compatible = "vnd,soc-serial";
        status = "okay";
        current-speed = <57600>;
        ...
};

serial@40003000 {
        compatible = "vnd,soc-serial";
        current-speed = <115200>;
        ...
};

        PS:这里的实例号分配不确定性是官方文档描述的

5.4 获取节点属性

1)节点是否有该属性

  • 可使用DT_NODE_HAS_PROP()
//节点DT_NODELABEL(i2c1)是否有clock_frequency或者not_a_property属性
DT_NODE_HAS_PROP(DT_NODELABEL(i2c1), clock_frequency)  /* expands to 1 */
DT_NODE_HAS_PROP(DT_NODELABEL(i2c1), not_a_property)   /* expands to 0 */

        PS:该API不能对boolean类型的属性使用

2)简单属性获取

  • 使用DT_PROP()获取属性值
  • 使用DT_PROP_LEN()获取属性长度
//某节点描述如下
foo: foo@1234 {
        a = <1000 2000 3000>; /* array */
        b = [aa bb cc dd];    /* uint8-array */
        c = "bar", "baz";     /* string-array */
};

//获取一些简单属性方式
#define FOO DT_NODELABEL(foo)

int a[] = DT_PROP(FOO, a);           /* {1000, 2000, 3000} */
unsigned char b[] = DT_PROP(FOO, b); /* {0xaa, 0xbb, 0xcc, 0xdd} */
char* c[] = DT_PROP(FOO, c);         /* {"foo", "bar"} */

size_t a_len = DT_PROP_LEN(FOO, a); /* 3 */
size_t b_len = DT_PROP_LEN(FOO, b); /* 4 */
size_t c_len = DT_PROP_LEN(FOO, c); /* 2 */

3)reg属性获取

  • 首先了解reg基本格式,一般为做为一个block。
  • 如果只有一个block可使用DT_REG_ADDR()和DT_REG_SIZE()获取
//某设备树节点如下
rcc: rcc@40021000 {
    compatible = "st,stm32-rcc";
    #clock-cells = < 0x2 >;
    reg = < 0x40021000 0x400 >;
};

//获取addr和size
#define RCC DT_NODELABEL(rcc)

DT_REG_ADDR(RCC) //返回0x40021000
DT_REG_SIZE(RCC) //返回0x400

  • 如果多个block的情况可先使用DT_NUM_REGS(node_id)获取block数量,再使用DT_REG_ADDR_BY_IDX(node_id, idx)和DT_REG_SIZE_BY_IDX(node_id, idx)分别获取addr和size

        PS:idx不能传递一个变量,必须是一个固定值。

//某设备树节点如下
rcc: rcc@40021000 {
    compatible = "st,stm32-rcc";
    #clock-cells = < 0x2 >;
    reg = < 0x40021000 0x400 0x40022000 0x500>;
};

//获取addr和size
#define RCC DT_NODELABEL(rcc)

DT_NUM_REGS(RCC) //返回2

DT_REG_ADDR_BY_IDX(RCC, 0)    //返回0x40021000
DT_REG_SIZE_BY_IDX(RCC, 0)    //返回0x400

//如下写法会报错,DT_REG_ADDR_BY_IDX(node_id, idx)中的idx不能为变量
for (size_t i = 0; i < DT_NUM_REGS(node_id); i++) {
        size_t addr = DT_REG_ADDR_BY_IDX(node_id, i);
}

4)获取struct device

        1.首先获得节点标识符,可以回顾 “获取节点标识符”

/ {
        soc {
                serial0: serial@40002000 {
                        status = "okay";
                        current-speed = <115200>;
                        /* ... */
                };
        };

        aliases {
                my-serial = &serial0;
        };

        chosen {
                zephyr,console = &serial0;
        };
};

//1. 获取节点标识符,下面罗列了4种方式
/* Option 1: by node label */
#define MY_SERIAL DT_NODELABEL(serial0)

/* Option 2: by alias */
#define MY_SERIAL DT_ALIAS(my_serial)

/* Option 3: by chosen node */
#define MY_SERIAL DT_CHOSEN(zephyr_console)

/* Option 4: by path */
#define MY_SERIAL DT_PATH(soc, serial_40002000)

        2.有两种方式获取device:

          2.1 典型的用法是通过device_get_binding(DT_LABEL(节点标识符))

//PS:DT_LABEL()参数传入设备标识符即可,无需一定使用DT_NODELABEL()获取的标识符
const struct device *uart_dev = device_get_binding(DT_LABEL(MY_SERIAL));

          2.2 第二种是使用DEVICE_DT_GET(节点标识符)

const struct device *uart_dev = DEVICE_DT_GET(MY_SERIAL);

if (!device_is_ready(uart_dev)) {
        /* Not ready, do not use */
        return -ENODEV;
}

        3 异常检查

        获取struct device切记调用一些API确认该dev能够使用。

//通过DT_NODE_HAS_STATUS()验证
#define MY_SERIAL DT_NODELABEL(my_serial)

#if DT_NODE_HAS_STATUS(MY_SERIAL, okay)
const struct device *uart_dev = device_get_binding(DT_LABEL(MY_SERIAL));
#else
#error "Node is disabled"
#endif

//或者通过device_is_ready()
const struct device *uart_dev = DEVICE_DT_GET(MY_SERIAL);

if (!device_is_ready(uart_dev)) {
        /* Not ready, do not use */
        return -ENODEV;
}

<think>好的,我需要回答关于nRF52832在NCS v2.6.2下进入Deep Sleep前是否需要配置GPIO为低功耗模式的问题。首先,我得回忆一下nRF52832的硬件特性。这款芯片的GPIO在未配置时默认是高阻态,可能产生漏电流,特别是在外部电路有电压差的情况下。所以,理论上需要配置GPIO状态以减少功耗。 接下来,考虑NCS(Nordic Connect SDK)的配置部分。用户使用的是2.6.2版本,我需要确认在SDK中是否有相关的API或配置选项。通常,Zephyr RTOS提供电源管理功能,比如pm_device_state_set,可能涉及设备状态管理。另外,GPIO的配置可能需要通过设置上下拉电阻或者输出电平来确保低功耗状态。 然后,需要分析具体应用场景。如果GPIO连接的外部设备在睡眠时不需要供电,可能需要将GPIO设置为低电平或高电平,避免浮空输入导致的漏电。此外,配置GPIO的上下拉电阻,比如使能下拉电阻,可以固定电平,减少功耗。 再者,参考Nordic的官方文档或应用笔记。例如,有些资料建议在进入睡眠模式前将所有未使用的GPIO配置为输入并启用上下拉,或者设置为输出并驱动到固定电平。对于关键外设,如SPI、I2C等,可能需要关闭其时钟或电源。 还需要考虑用户可能遇到的常见问题,比如未正确配置GPIO导致睡眠电流偏高。这时候建议使用测量工具检查电流,并结合GPIO状态进行调试。此外,使用Zephyr的电源管理API正确进入Deep Sleep模式,例如调用pm_system_suspend()函数。 最后,总结关键步骤:检查所有GPIO状态,配置为输出驱动或输入加上下拉,关闭未使用外设,使用SDK提供的电源管理接口,以及实际测量验证。同时提醒用户注意硬件设计中的潜在漏电路径,比如LED或其他元件在GPIO未正确配置时的电流消耗。 整个过程中需要确保回答结构清晰,分点说明,同时结合理论实际操作建议,帮助用户系统地解决问题。</think>关于nRF52832在NCS v2.6.2进入Deep Sleep前的GPIO配置问题,以下是关键步骤原理说明: --- ### 一、**必须配置GPIO的原因** 1. **漏电流风险** nRF52832的GPIO在默认高阻态时,若外部电路存在电压差(如悬空引脚、未断电的外设),可能产生μA级漏电流,导致睡眠功耗上升。 2. **硬件设计要求** 若GPIO连接LED、传感器电源控制等外围电路,需明确输出电平状态以避免意外供电。 --- ### 二、**GPIO配置规范** 1. **基本原则** - **未使用引脚**:配置为**输入模式+内部下拉**(避免浮空) ```c gpio_pin_configure(port, pin, GPIO_INPUT | GPIO_PULL_DOWN); ``` - **驱动外部设备**:根据硬件设计设为**输出模式+固定电平** ```c gpio_pin_configure(port, pin, GPIO_OUTPUT_LOW); // 关闭外部电源 ``` 2. **特殊场景处理** - **保留唤醒功能**:若GPIO用于唤醒(如按键),需保持中断使能并配置为**输入+上拉/下拉** ```c gpio_pin_configure(port, pin, GPIO_INPUT | GPIO_PULL_UP); gpio_pin_interrupt_configure(port, pin, GPIO_INT_EDGE_FALLING); ``` --- ### 三、**NCS 2.6.2中的实现步骤** 1. **电源管理API调用** 使用Zephyr的电源管理接口进入Deep Sleep: ```c pm_system_suspend(); // 触发系统挂起 ``` 2. **外设电源关闭** 通过设备树关闭非必要外设(如UART、SPI): ```c &uart0 { status = "disabled"; }; ``` 3. **低功耗验证工具** - 使用**电流表**测量睡眠电流(正常应<2μA) - 通过`CONFIG_PM_DEBUG=y`启用电源管理调试日志 --- ### 四、**常见问题排查** 1. **功耗偏高** - 检查GPIO是否全部配置(尤其是P0.00-P0.31P1.00-P1.15) - 确认未保留高频率时钟源(如HFCLK未关闭) 2. **唤醒失效** - 确保唤醒引脚未配置为输出模式 - 检查`CONFIG_GPIO_ENABLE_DISABLE_INTERRUPT`是否启用 --- ### 五、**参考代码片段** ```c void enter_deep_sleep(void) { // 配置所有未使用GPIO为输入+下拉 for (int pin = 0; pin < 32; pin++) { gpio_pin_configure(device_get_binding("GPIO_0"), pin, GPIO_INPUT | GPIO_PULL_DOWN); } // 配置关键控制引脚为输出低电平 gpio_pin_configure(device_get_binding("GPIO_0"), LED_PIN, GPIO_OUTPUT_LOW); // 进入Deep Sleep pm_system_suspend(); } ``` --- ### 总结 **必须**在进入Deep Sleep前配置GPIO状态,这是确保低功耗的关键步骤。需结合硬件电路设计,综合使用输入上下拉、输出固定电平等方式,并通过实际电流测量验证配置效果。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值