目录
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;
}