拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Zephyr BSP: 11-Zephyr多实例生成

Zephyr BSP: 11-Zephyr多实例生成 对,这一篇11 — Zephyr 多 Device Instance很关键,因为到了这里,你真正开始看到:一个 Driver 源文件,为什么可以自动生成 UART0 / UART1 / UART2 三个不同的 struct device。而这正是以后你把公司 SoC 外设驱动批量接入 Zephyr时最重要的机制之一。摘要:本文深入剖析 Zephyr 多 Device Instance 的核心机制DT_INST_FOREACH_STATUS_OKAY()。通过一个 UART 驱动实例,从 Devicetree 节点定义、DT_INST()宏语义、X-Macro 编译期展开,到config/data按 instance 复制,完整演示一个 Driver 源文件如何自动生成 UART0/UART1/UART2 多个struct device。同时厘清DT_INST()与DT_NODELABEL()的区别、status = "disabled"的过滤行为,以及这套机制对批量接入公司 SoC 外设驱动的关键意义。Zephyr 多 Device Instance:DT_INST_FOREACH_STATUS_OKAY() 到底如何一次生成 UART0/UART1/UART2前面已经建立了:Devicetree ↓ Binding ↓ DT_NODELABEL / DT_INST ↓ Driver ↓ struct device这一篇继续往下追:Devicetree │ ┌───────────┼───────────┐ ↓ ↓ ↓ uart0 uart1 uart2 │ │ │ └───────────┼───────────┘ ↓ DT_INST_FOREACH_STATUS_OKAY()↓ DRIVER_DEFINE(inst)↓ ┌───────────┼───────────┐ ↓ ↓ ↓ device0device1device2核心问题只有一个:DT_INST_FOREACH_STATUS_OKAY() 到底是怎么把一个函数/宏"执行"三次的?1. 先从最简单的 UART Devicetree 开始假设你的 SoC 有三个 UART:uart0{status="okay";};uart1{status="okay";};uart2{status="okay";};对应节点可能是:uart0: serial@40000000{compatible="mycompany,my-uart";reg=lt;0x40000000 0x1000gt;;interrupts=lt;10gt;;status="okay";};uart1: serial@40001000{compatible="mycompany,my-uart";reg=lt;0x40001000 0x1000gt;;interrupts=lt;11gt;;status="okay";};uart2: serial@40002000{compatible="mycompany,my-uart";reg=lt;0x40002000 0x1000gt;;interrupts=lt;12gt;;status="okay";};这里最重要的是:compatible="mycompany,my-uart";三个节点拥有相同的 compatible。因此 Zephyr 可以把它们视为:my-uart instance0my-uart instance1my-uart instance2也就是:DT_INST(0, mycompany_my_uart)DT_INST(1, mycompany_my_uart)DT_INST(2, mycompany_my_uart)2. DT_INST() 到底是什么?你经常会看到:DT_INST(0, mycompany_my_uart)它的意思不是:“UART0”而是:拿到 compatible 为 mycompany,my-uart 的第 0 个 instance 对应的 Devicetree node。例如:DT_INST(0, mycompany_my_uart)对应:uart0而:DT_INST(1, mycompany_my_uart)对应:uart1以及:DT_INST(2,mycompany_my_uart)对应:uart2所以:DT_INST(0,...)↓ uart0 DT_INST(1,...)↓ uart1 DT_INST(2,...)↓ uart23. 这里有一个非常重要的区别不要把:instance number理解成:Devicetreenodeaddress也不要理解成:UART 硬件编号它实际上是:Zephyr 根据某个 compatible 找出来的节点列表中的索引。例如:compatible="mycompany,my-uart"instance │ ┌────────┼────────┐ ↓ ↓ ↓012│ │ │ uart0 uart1 uart23.5 DT_INST() 与 DT_NODELABEL() 对比一览既然你已经理解了DT_INST()是"按 compatible + 编号索引",那它和DT_NODELABEL()到底差在哪?下面这张表从用途、语法、适用场景、实例编号规则等维度做了对比:对比维度DT_INST()DT_NODELABEL()用途按compatible + instance 编号定位节点直接定位某个具体节点(按 node label)语法两个参数:DT_INST(编号, compatible),如DT_INST(0, mycompany_my_uart)一个参数:DT_NODELABEL(label),如DT_NODELABEL(uart0)定位方式在 compatible 对应的节点集合中取第 N 个通过label = uart0直接找到该节点实例编号规则编号 = 该 compatible 下所有status = "okay"节点的顺序索引(从 0 开始,disabled跳过、不占编号)没有编号概念,直接用节点 label 点名适用场景通用 Driver 编写,不写死具体节点,配合DT_INST_FOREACH_STATUS_OKAY()批量生成应用层/驱动里明确知道要操作哪个外设(如DEVICE_DT_GET(DT_NODELABEL(uart0)))示例DT_INST(0, mycompany_my_uart)→uart0节点DT_NODELABEL(uart0)→uart0节点优点与具体节点解耦,一个 Driver 源文件可复用生成多个 instance,便于 SoC BSP 扩展语义直观、可读性强,一眼看出操作哪个外设缺点需要理解 instance 编号与节点的映射关系,初学有一定心智负担写死了节点名,Driver 无法通用;节点 label 变了就要改代码简要说明:DT_INST()是"按序取":你告诉 Zephyr “我要 compatible 为mycompany,my-uart的第 0 个节点”。它不关心节点叫什么名字,只关心"这是第几个 instance",因此适合写通用 Driver。DT_NODELABEL()是"点名":你直接告诉 Zephyr “我要uart0这个节点”,适合在应用代码或明确指定某个外设的场合使用。两者可能指向同一个节点(如DT_NODELABEL(uart0)和DT_INST(0, mycompany_my_uart)都指向uart0),但语义完全不同:一个是按 label 点名,一个是按 compatible 编号索引。写 SoC Driver 时优先用DT_INST():因为 Driver 不应该关心节点叫什么名字,只关心"这是第几个 instance",这样DT_INST_FOREACH_STATUS_OKAY()才能自动展开生成多个 device。因此:DT_INST(0,...)不是说:address=0而是:compatible 对应节点集合中的第0个节点4. 为什么 Driver 不需要写三遍?最笨的方法当然可以这样写:static const struct device\*uart0;static const struct device\*uart1;static const struct device\*uart2;然后:DEVICE_DT_DEFINE(DT_INST(0,...));DEVICE_DT_DEFINE(DT_INST(1,...));DEVICE_DT_DEFINE(DT_INST(2,...));但是这显然不适合 SoC Driver。因为真实 SoC 可能有:UART0 UART1 UART2 UART3 UART4 UART5... UART15甚至:GPIO0 ~ GPIO10 SPI0 ~ SPI7 I2C0 ~ I2C5 TIMER0 ~ TIMER15如果每个 Driver 都手工写:DEVICE_DT_DEFINE(...)DEVICE_DT_DEFINE(...)DEVICE_DT_DEFINE(...)...整个 BSP 会非常难维护。所以 Zephyr 使用了一个非常漂亮的机制:DT_INST_FOREACH_STATUS_OKAY(...)5. 先看最简单的例子假设我们写:#defineMY_UART_INIT(inst)\\DEVICE_DT_DEFINE(DT_INST(inst,mycompany_my_uart));DT_INST_FOREACH_STATUS_OKAY(MY_UART_INIT)你可以先不要把它理解成复杂的 C 宏。先把它理解成:找到所有 status="okay"的 mycompany,my-uart instance 然后: MY_UART_INIT(0)MY_UART_INIT(1)MY_UART_INIT(2)也就是:MY_UART_INIT(0);MY_UART_INIT(1);MY_UART_INIT(2);6. 然后宏展开假设:# define MY_UART_INIT(inst) \\DEVICE_DT_DEFINE(DT_INST(inst, mycompany_my_uart));那么:DT_INST_FOREACH_STATUS_OKAY(MY_UART_INIT)逻辑上相当于:MY_UART_INIT(0)MY_UART_INIT(1)MY_UART_INIT(2)进一步展开:DEVICE_DT_DEFINE(DT_INST(0, mycompany_my_uart));DEVICE_DT_DEFINE(DT_INST(1, mycompany_my_uart));DEVICE_DT_DEFINE(DT_INST(2, mycompany_my_uart));于是:这就是整个魔法。7. 这其实就是"X-Macro / 重复展开"的思想所以看到:DT_INST_FOREACH_STATUS_OKAY(MY_UART_INIT)不要觉得它是某种神秘的运行时机制。它不是运行时循环。这是非常重要的一点。它不是:for(int i=0;i3;i++){...}而是:C Preprocessor 在编译之前,把代码展开成多份。7.5 如何验证编译期确实展开了三份代码7.6 GPIO 多实例对比:DT_INST_FOREACH_STATUS_OKAY() 在 GPIO 驱动中的应用前面我们用 UART 完整走了一遍DT_INST_FOREACH_STATUS_OKAY()的展开过程。但你可能会有个疑问:这套机制是不是只适用于 UART?当然不是。DT_INST_FOREACH_STATUS_OKAY()是 Zephyr 驱动框架的通用机制,任何 compatible 匹配多个节点的外设驱动都能用它批量生成 device 实例。这一节我们就用 GPIO 驱动再走一遍,看看同一套机制在不同外设驱动里是如何复用的,并对比 UART 与 GPIO 在config/data结构体定义上的差异。第一步:GPIO 的 Devicetree 节点定义假设你的 SoC 有 11 个 GPIO 控制器(GPIO0 ~ GPIO10),它们拥有相同的 compatible:gpio0: gpio@50000000 { compatible = "mycompany,my-gpio"; reg = 0x50000000 0x1000; interrupts = 20; gpio-controller; #gpio-cells = 2; status = "okay"; }; gpio1: gpio@50001000 { compatible = "mycompany,my-gpio"; reg = 0x50001000 0x1000; interrupts = 21; gpio-controller; #gpio-cells = 2; status = "okay"; }; /* ... 以此类推,直到 gpio10 ... */ gpio10: gpio@5000a000 { compatible = "mycompany,my-gpio"; reg = 0x5000a000 0x1000; interrupts = 30; gpio-controller; #gpio-cells = 2; status = "okay"; };和 UART 一样,Zephyr 会把它们视为mycompany,my-gpio的 11 个 instance:DT_INST(0, mycompany_my_gpio)→ gpio0 DT_INST(1, mycompany_my_gpio)→ gpio1... DT_INST(10, mycompany_my_gpio)→ gpio10第二步:GPIO 驱动的 config/data 结构体GPIO 驱动和 UART 驱动最大的不同在于:GPIO 需要管理"引脚"这一层。每个 GPIO 控制器通常有 32 个引脚,驱动需要记录每个引脚的方向、上下拉、中断触发方式等状态。因此config和data的结构体定义会比 UART 复杂一些:/* GPIO 驱动的 config 结构体:编译期填充,运行时只读 */structmy_gpio_config{uintptr_tbase;/* 寄存器基地址,来自 reg 属性 */intirq;/* 中断号,来自 interrupts 属性 */uint32_tngpios;/* 引脚数量,来自 ngpios 属性 */};/* GPIO 驱动的 data 结构体:每个 instance 一份,保存运行时状态 */structmy_gpio_data{uint32_tpin_dir;/* 每个引脚的方向:1=输出,0=输入 */uint32_tpin_pull;/* 每个引脚的上/下拉配置 */uint32_tpin_int_en;/* 每个引脚的中断使能状态 */sys_slist_tcb_list;/* 引脚中断回调链表 */};第三步:GPIO 驱动的 instance initializer 宏和 UART 的MY_UART_INIT几乎一模一样,只是把结构体名和 compatible 换成了 GPIO 的:#defineMY_GPIO_INIT(inst)\staticconststructmy_gpio_configgpio_config_##inst={\.base=DT_INST_REG_ADDR(inst),\.irq=DT_INST_IRQN(inst),\.ngpios=DT_INST_PROP(inst,ngpios),\};\\staticstructmy_gpio_datagpio_data_##inst;\\DEVICE_DT_DEFINE(\DT_INST(inst,mycompany_my_gpio),/* ① 节点标识 */\my_gpio_init,/* ② 初始化函数 */\NULL,/* ③ PM 指针 */\gpio_data_##inst,/* ④ data 指针 */\gpio_config_##inst,/* ⑤ config 指针 */\POST_KERNEL,/* ⑥ 初始化级别 */\CONFIG_GPIO_INIT_PRIORITY,/* ⑦ 初始化优先级 */\my_gpio_driver_api/* ⑧ API 指针 */\);DT_INST_FOREACH_STATUS_OKAY(MY_GPIO_INIT)第四步:编译期展开结果DT_INST_FOREACH_STATUS_OKAY(MY_GPIO_INIT)在编译期会展开成 11 份(假设全部status = "okay"):/* 第 0 份:MY_GPIO_INIT(0) */staticconststructmy_gpio_configgpio_config_0={.base=0x50000000,.irq=20,.ngpios=32,};staticstructmy_gpio_datagpio_data_0;DEVICE_DT_DEFINE(DT_INST(0,mycompany_my_gpio),my_gpio_init,NULL,gpio_data_0,gpio_config_0,POST_KERNEL,CONFIG_GPIO_INIT_PRIORITY,my_gpio_driver_api);/* 第 1 份:MY_GPIO_INIT(1) */staticconststructmy_gpio_configgpio_config_1={
返回列表