☰
RT-Thread GPIO/PIN 设备模型(DM):全局虚拟引脚命名空间与设备树 `gpios` 解析实战
2026/10/7 20:26:27 网站建设 项目流程
  • 操作系统
  • 嵌入式
  • 物联网
  • 嵌入式OS
  • RTOS

【免费下载链接】rt-thread

RT-Thread is an open source IoT Real-Time Operating System (RTOS). https://rt-thread.github.io/rt-thread/

项目地址:https://gitcode.com/gh_mirrors/rt/rt-thread
点击查看免费下载

RT-Thread 在启用设备模型(Device Model,DM)后,PIN 框架不再依赖 BSP 里硬编码的GET_PIN(port, n)引脚表,而是由每个 GPIO 控制器按设备树注册一段连续的虚拟引脚号,应用统一通过rt_pin_mode/rt_pin_write/rt_pin_read操作,驱动侧则用rt_pin_get_named_pin按属性名(如reset、led)取引脚。本文以仓库内 dm.md 为骨架,结合components/drivers/pin/下的真实源码,完整讲解 DM 引脚框架的 Kconfig 配置、全局命名空间架构、平台驱动移植清单、设备树解析与 GPIO 中断(PIC)实现,以及常见踩坑点,帮助你在新平台上一口气打通 GPIO 的注册、解析与中断链路。


一、为什么需要 DM 引脚框架

传统 BSP 的 PIN 驱动(如drv_gpio.c)维护一张静态pins[]表,把“RT-Thread 引脚号”映射到具体的芯片端口/引脚,应用必须靠GET_PIN(port, pin)宏或查表才能拿到引脚号。这套方案在单控制器、引脚固定的场景下够用,但在多 GPIO 控制器(SoC 常有多组 GPIO bank)、需要与设备树(DT)联动的平台上就显得僵硬。

RT_USING_PIN与RT_USING_DM同时开启后,框架引入了一套全局 GPIO 命名空间:

  • 每个平台 GPIO 控制器在 probe 时注册一段连续虚拟引脚号范围;
  • 应用代码仍然使用rt_pin_mode/rt_pin_write/rt_pin_read(与经典 PIN API 完全一致,参见 pin.md);
  • 驱动代码使用rt_pin_get_named_pin从设备树gpios/xxx-gpios属性中解析出虚拟引脚,并可选使用GPIO IRQ domain(PIC)实现中断。

这一设计把“引脚是谁、在哪一段、电平极性如何”全部交给设备树描述,BSP 与上层驱动解耦,代码可以跨平台复用。

二、Kconfig:开启 DM 引脚路径

配置项作用
RT_USING_PINGPIO 框架总开关,默认y(components/drivers/pin/Kconfig中default y)
RT_USING_DM本篇文章描述的 DM 路径的前提
RT_PIN_PL061树内 ARM PL061 GPIO 控制器驱动(default n,依赖RT_USING_DM与RT_USING_PIN)
SOC_DM_PIN_DIRBSP 通过osource "$(SOC_DM_PIN_DIR)/Kconfig"引入各自 SoC 的 GPIO 驱动

从 Kconfig 源码可以看到框架的扩展方式:

menuconfig RT_USING_PIN bool "Using Generic GPIO device drivers" default y config RT_PIN_PL061 bool "ARM PL061" depends on RT_USING_DM depends on RT_USING_PIN default n if RT_USING_DM && RT_USING_PIN osource "$(SOC_DM_PIN_DIR)/Kconfig" endif

也就是说,通用框架只内置 PL061 作为参考驱动,其余 SoC GPIO 驱动由各 BSP 通过SOC_DM_PIN_DIR挂载,保证框架主体干净、平台差异下沉。

三、架构:全局虚拟引脚命名空间

DT: gpio-controller node (#gpio-cells, interrupt-controller optional) | | platform probe v struct rt_device_pin + rt_pin_ops (per controller) | +-- pin_api_init(gpio, pin_nr) → global pin_start..pin_start+nr-1 +-- pin_pic_init(gpio, irq) → optional per-line PIC domain +-- rt_dm_dev_bind_fwdata → rt_ofw_data(np) = gpio | v First controller also: rt_device_pin_register("gpio", pin_api_dm_ops) | v Consumers: rt_pin_get_named_pin(dev, "foo", index, &mode, &val) rt_pin_mode(pin, mode); rt_pin_write(pin, val);

核心概念对照:

概念含义
Local pin传入rt_pin_ops回调的引脚索引,范围为0 … pin_nr-1,即控制器内部的相对编号
Virtual pinpin_start + local,是rt_pin_*系列 API 与rt_pin_get_named_pin返回并使用的全局值
pin_device_find由虚拟引脚反查所属的struct rt_device_pin,完成“虚拟号 → 控制器 + 本地号”的映射

在 dev_pin_dm.c 中可以看到这一机制的具体实现。全局状态由三部分组成:

static rt_size_t pin_total_nr = 0; static RT_DEFINE_SPINLOCK(pin_lock); static rt_list_t pin_nodes = RT_LIST_OBJECT_INIT(pin_nodes);

pin_device_find遍历pin_nodes链表,判断虚拟引脚是否落在某个控制器的[pin_start, pin_start + pin_nr)区间内:

rt_list_for_each_entry(gpio_tmp, &pin_nodes, list) { if (pin >= gpio_tmp->pin_start && pin - gpio_tmp->pin_start < gpio_tmp->pin_nr) { gpio = gpio_tmp; break; } }

所有控制器都挂在pin_nodes链表上,pin_total_nr只增不减(引脚号区间不回收),因此虚拟引脚号在整个系统生命周期内保持稳定、不重复分配。正是这个“先注册先分配、区间单调增长”的策略,保证了设备树解析出的引脚号长期有效。

pin_api_init还负责为第一个注册的控制器注册全局"gpio"设备(dev_pin_dm.c):

if (rt_list_isempty(&pin_nodes)) { rt_spin_unlock(&pin_lock); rt_device_pin_register("gpio", &pin_api_dm_ops, RT_NULL); rt_spin_lock(&pin_lock); } gpio->pin_start = pin_total_nr; gpio->pin_nr = pin_nr; pin_total_nr += pin_nr; rt_list_init(&gpio->list); rt_list_insert_before(&pin_nodes, &gpio->list);

pin_api_dm_ops是 DM 层的shim(适配层):所有rt_pin_*调用先进到这一层,由pin_device_find找到控制器后,再把“虚拟引脚 → 本地引脚”转换,最终落到具体控制器的rt_pin_ops回调上,例如:

static void pin_api_write(struct rt_device *device, rt_base_t pin, rt_uint8_t value) { struct rt_device_pin *gpio = pin_device_find(pin); if (gpio && gpio->ops->pin_write) { gpio->ops->pin_write(&gpio->parent, pin - gpio->pin_start, value); } }

四、平台驱动移植清单(Platform driver checklist)

要让一个新 GPIO 控制器接入 DM 引脚框架,按以下步骤实现(参考驱动为 pin-pl061.c 的pl061_probe):

  1. 定义struct rt_device_pin:可以直接使用,也可以把它嵌入私有结构体的第一个成员,便于用rt_container_of从struct rt_device反查私有数据。PL061 的做法即是如此:

    struct pl061 { struct rt_device_pin parent; /* 必须位于第一个成员 */ int irq; void *base; struct rt_clk *pclk; struct rt_spinlock spinlock; }; #define raw_to_pl061(raw) rt_container_of(raw, struct pl061, parent)
  2. 实现struct rt_pin_ops:至少实现pin_mode、pin_write、pin_read三个回调(PL061 的pl061_pin_ops还额外提供了pin_irq_enable与pin_irq_mode)。

  3. probe函数:完成寄存器 iomap、时钟使能、rt_dm_dev_bind_fwdata(dev, RT_NULL, &gpio->parent)绑定固件数据,这样设备树节点rt_ofw_data(np)才能取到struct rt_device_pin *。PL061 的 probe 流程是:rt_dm_dev_iomap→rt_dm_dev_get_irq→rt_clk_get_by_name/rt_clk_prepare_enable→rt_dm_dev_bind_fwdata。

  4. pin_api_init(&gpio->parent, gpio_count):分配pin_start,把控制器挂上全局链表(见第三节)。

  5. 如果 GPIO 共享一根中断线(如 PL061 的 8 个引脚共享一个 IRQ):

    • irq = rt_dm_dev_get_irq(dev, 0)取中断号;
    • pin_pic_init(&gpio->parent, irq)建立逐引脚 PIC domain;
    • 硬件 ISR 中循环读取 pending 位,逐位调用pin_pic_handle_isr(&gpio->parent, local_pin)分发;
    • 用rt_hw_interrupt_install(irq, …)安装 ISR 并rt_hw_interrupt_umask(irq)使能。

    PL061 的中断处理是标准模板(pin-pl061.c):

    static void pl061_isr(int irqno, void *param) { ... pending = pl061_read(pl061, PL061_MIS); if (pending) { for (int pin = 0; pin < PL061_GPIO_NR; ++pin) { if (pending & RT_BIT(pin)) { mask |= RT_BIT(pin); pin_pic_handle_isr(&pl061->parent, pin); } } pl061_write(pl061, PL061_IC, mask); /* 清中断 */ } }

    而 probe 末尾的安装动作则是:

    pin_api_init(&pl061->parent, PL061_GPIO_NR); pin_pic_init(&pl061->parent, pl061->irq); rt_hw_interrupt_install(pl061->irq, pl061_isr, pl061, "gpio-pl061"); rt_hw_interrupt_umask(pl061->irq);
  6. 可选实现pin_parse:当#gpio-cells不止 1 个(例如带 flags 的 2-cell 描述)时,用于把args[]解码为本地引脚 +flags,flags 语义来自dt-bindings/pin/pin.h。未实现时框架默认取args[0]为本地引脚。

最后注册平台驱动(PL061 通过INIT_SUBSYS_EXPORT在子系统初始化阶段注册,compatible 为"arm,pl061"):

static const struct rt_ofw_node_id pl061_ofw_ids[] = { { .compatible = "arm,pl061" }, { /* sentinel */ } }; static struct rt_platform_driver pl061_driver = { .name = "pin-pl061", .ids = pl061_ofw_ids, .probe = pl061_probe, };

五、struct rt_pin_ops:DM 相关的回调集合

struct rt_pin_ops定义了控制器需要向框架提供的全部能力(定义见 dev_pin.h):

回调作用
pin_mode设置输入/输出/上拉/开漏等模式
pin_write/pin_read写/读电平
pin_attach_irq直接绑定逐引脚 ISR(可选)
pin_detach_irq移除处理函数
pin_irq_enable使能/禁止线路中断
pin_irq_mode使用 legacy 中断路径时配置边沿/电平触发
pin_parse解码#gpio-cells→ 本地引脚 +flags
pin_get名字 → 虚拟引脚(可选)
pin_debounce设置消抖时间

这里有一个重要的双路径设计:如果控制器没有实现pin_attach_irq,dev_pin_dm.c会把中断处理函数存进legacy_isr[]数组,并依赖pin_irq_mode+pin_pic_handle_isr来工作。看 dev_pin_dm.c 的pin_api_attach_irq:

if (!gpio->ops->pin_attach_irq) { rt_err_t err; struct rt_pin_irq_hdr *legacy_isr; if ((err = gpio->ops->pin_irq_mode(&gpio->parent, pin_index, mode))) return err; legacy_isr = &gpio->legacy_isr[pin_index]; legacy_isr->pin = pin_index; legacy_isr->mode = mode; legacy_isr->hdr = hdr; legacy_isr->args = args; return RT_EOK; }

legacy_isr数组由pin_pic_init通过rt_calloc(gpio->pin_nr, sizeof(*gpio->legacy_isr))分配,因此每个本地引脚都有一个struct rt_pin_irq_hdr槽位。pin_pic_handle_isr在分发时同时走 PIC 路径和 legacy 回调(dev_pin_dm.c):

pirq = rt_pic_find_irq(&irqchip->parent, pin_index); if (pirq->irq >= 0) err = rt_pic_handle_isr(pirq); legacy_isr = &gpio->legacy_isr[pin_index]; if (legacy_isr->hdr) legacy_isr->hdr(legacy_isr->args);

六、设备树gpios属性解析

设备树中的 GPIO 引用属性由 dev_pin_ofw.c 中的rt_ofw_get_named_pin(np, propname, index, out_mode, out_value)负责解析。属性名查找顺序:

  • 若传入propname(如"reset"):依次尝试{propname}-gpios、{propname}-gpio;
  • 若propname为NULL:依次尝试gpios、gpio。

这一点在源码中有明确体现:

static const char * const gpio_suffixes[] = { "gpios", "gpio" };

完整解析流程:

  1. rt_ofw_parse_phandle_cells(..., "#gpio-cells", …)解析 phandle + cells,取出 GPIO provider 节点与args[];
  2. 若 provider 尚未 probe,调用rt_platform_ofw_request主动请求加载平台驱动(这正是“Provider not probed”坑的官方解法);
  3. 调pin_parse或按默认规则取args[0]为本地引脚;
  4. 将flags转换成PIN_MODE_*与有效电平(active level);
  5. 返回local + pin_dev->pin_start,即全局虚拟引脚。

其中 flags 的转换逻辑(dev_pin_ofw.c)值得细看:

value = PIN_LOW; mode = PIN_MODE_OUTPUT; if (pin_dev->ops->pin_parse) pin = pin_dev->ops->pin_parse(&pin_dev->parent, &pin_args, &flags); else /* We always assume that the args[0] is the pin number if driver not implemented `pin_parse`. */ pin = pin_args.args[0]; if (out_mode) { if (flags & PIN_OPEN_DRAIN) mode = PIN_MODE_OUTPUT_OD; switch (flags & RT_GENMASK(6, 4)) { case PIN_PULL_UP: mode = PIN_MODE_INPUT_PULLUP; break; case PIN_PULL_DOWN: mode = PIN_MODE_INPUT_PULLDOWN; break; case PIN_PULL_DISABLE: mode = PIN_MODE_INPUT; break; } } if (out_value) { if ((flags & 1) == PIN_ACTIVE_HIGH) value = PIN_HIGH; else if ((flags & 1) == PIN_ACTIVE_LOW) value = PIN_LOW; }

可以看到:flags的 bit0 表示有效电平极性(PIN_ACTIVE_HIGH/PIN_ACTIVE_LOW),bit4~6 表示上下拉(PIN_PULL_UP/PIN_PULL_DOWN/PIN_PULL_DISABLE),这些常量定义在dt-bindings/pin/pin.h。也就是说,设备树里“低有效复位脚”这种描述,会被自动换算成PIN_MODE_OUTPUT+PIN_LOW,应用层无需关心极性细节。

设备(device)侧封装提供两个便捷 API(dev_pin_dm.c):

rt_ssize_t rt_pin_get_named_pin(struct rt_device *dev, const char *propname, int index, rt_uint8_t *out_mode, rt_uint8_t *out_value); rt_ssize_t rt_pin_get_named_pin_count(struct rt_device *dev, const char *propname);

rt_pin_get_named_pin内部以dev->ofw_node为节点调用rt_ofw_get_named_pin,因此只要驱动持有struct rt_device *且其节点有 GPIO 属性即可解析;返回值为负数时表示错误(如-RT_ENOSYS/-RT_EINVAL)。

典型 probe 用法:

pin = rt_pin_get_named_pin(dev, "reset", 0, &mode, &active); if (pin < 0) return pin; rt_pin_mode(pin, mode); rt_pin_write(pin, active); /* 按设备树 flags 写有效或无效电平 */

仓库内可参考的现成实例:phye-generic-usb.c(reset属性)、gpio-restart.c、led-gpio.c(子节点用法)、backlight-gpio.c。

七、GPIO 中断与 PIC(Interrupt Controller)

struct rt_pin_irqchip被内嵌在struct rt_device_pin中(紧跟parent之后,头文件注释明确要求“MUST keep the order member after parent”,见 dev_pin.h):

struct rt_device_pin { struct rt_device parent; #ifdef RT_USING_DM struct rt_pin_irqchip irqchip; /* PIC 域 */ rt_base_t pin_start; rt_size_t pin_nr; rt_list_t list; struct rt_pin_irq_hdr *legacy_isr; #endif const struct rt_pin_ops *ops; };

相关 API 与职责:

API作用
pin_pic_init(gpio, irq)调用rt_pic_linear_irq建立线性 PIC 域、分配legacy_isr数组、挂载pin_dm_ops
pin_pic_handle_isr(gpio, local_pin)分发PIC路径和/或legacy_isr回调(见第五节代码)

对逐引脚(per-line)的设备树中断(2 cells:pin + flags),使用pin_dm_ops.irq_parse/irq_map:

  • irq_parse把args[0]作为 hwirq(本地引脚)、args[1] & RT_IRQ_MODE_MASK作为触发模式(dev_pin_dm.c);
  • irq_map中rt_pic_cascade(pirq, gpio->irqchip.irq)把 GPIO 块的中断级联到控制器通过rt_dm_dev_get_irq取得的总中断线,实现“多引脚共享一条物理 IRQ”的经典场景(dev_pin_dm.c)。

pin_dm_ops的掩码/去掩码直接映射到控制器的pin_irq_enable:

static void pin_dm_irq_mask(struct rt_pic_irq *pirq) { struct rt_device_pin *gpio = pirq->pic->priv_data; gpio->ops->pin_irq_enable(&gpio->parent, pirq->hwirq, 0); } static void pin_dm_irq_unmask(struct rt_pic_irq *pirq) { struct rt_device_pin *gpio = pirq->pic->priv_data; gpio->ops->pin_irq_enable(&gpio->parent, pirq->hwirq, 1); }

触发模式的转换在pin_dm_irq_set_triger_mode中完成(RT_IRQ_MODE_EDGE_RISING→PIN_IRQ_MODE_RISING,RT_IRQ_MODE_EDGE_BOTH→PIN_IRQ_MODE_RISING_FALLING,RT_IRQ_MODE_LEVEL_HIGH/LOW→PIN_IRQ_MODE_HIGH_LEVEL/LOW_LEVEL)。

需要特别注意的是:对于调用rt_pin_attach_irq(virtual_pin, …)但设备树该行没有 interrupt 描述的情况,legacy 路径仍然生效——框架在 attach 时通过pin_irq_mode配置触发模式、把回调存入legacy_isr[],硬件使能则依靠pin_irq_enable。因此这类控制器(如 PL061)即便没有实现pin_attach_irq,应用层依然能正常使用中断回调。

八、应用与驱动侧的使用方式

DM 系统中,应用/驱动应优先使用命名解析,而不是从旧的 BSP 引脚表抄GET_PIN(port, n):

/* Not GET_PIN(port, n) from old BSP tables */ pin = rt_pin_get_named_pin(&pdev->parent, "led", 0, &mode, NULL); rt_pin_mode(pin, mode);

此后对该pin(虚拟引脚)的rt_pin_write/rt_pin_read/rt_pin_attach_irq调用与经典 PIN API 完全一致,用户态代码无需感知控制器数量与引脚布局。经典 API 的完整能力(设置模式、读写电平、绑定/使能中断)参见 pin.md。

需要澄清适用范围:GET_PIN/drv_gpio.c静态引脚表只适用于非 DM 的 BSP;一旦启用RT_USING_DM,引脚号来源就切换为设备树 +pin_api_init的全局分配。

九、常见坑与规避(Pitfalls)

问题后果规避方法
跳过pin_api_initrt_ofw_get_named_pin会把错误的pin_start加上去(pin_start为 0/未初始化),返回错误的虚拟引脚probe 中必须调用pin_api_init(&gpio->parent, pin_nr)
缺少pin_parse仅使用args[0];当#gpio-cells> 1 且带 flags 时,引脚/极性解析错误为多 cell 控制器实现pin_parse,解码本地引脚与 flags
有中断却不调用pin_pic_init共享中断线控制器无法逐引脚分发,rt_pic_find_irq找不到 IRQ必须调用pin_pic_init,并在 ISR 中逐位调用pin_pic_handle_isr
pin_attach_irq为 NULL 时漏掉使能回调已注册但中断不触发attach 之后必须通过pin_irq_enable打开对应引脚的中断使能
GPIO provider 尚未 probert_ofw_data(np)为 NULL,解析返回-RT_ERROR确保注册了平台驱动;框架也会自动调用rt_platform_ofw_request请求加载

十、源码路径速查

文件作用
dev_pin_dm.c全局引脚表、pin_api_init、pin_pic_init、DMrt_pin_opsshim、rt_pin_get_named_pin设备侧封装
dev_pin_ofw.crt_ofw_get_named_pin/rt_ofw_get_named_pin_count,gpios/gpio属性解析与 flags 转换
dev_pin_dm.hpin_api_init/pin_pic_init/pin_pic_handle_isr/pin_gpio_request(RT_USING_PINCTRL)声明
dev_pin.c无 DM 控制器时的 legacy 单gpio设备路径
pin-pl061.c参考平台驱动(arm,pl061),完整展示了 probe、ISR、PIC 集成
dev_pin.hstruct rt_device_pin、struct rt_pin_irqchip、PIN 模式/电平宏定义
KconfigRT_USING_PIN、RT_PIN_PL061、SOC_DM_PIN_DIR配置入口

综上,DM 引脚框架的落地路径非常清晰:Kconfig 开配置 → 控制器实现rt_pin_ops→ probe 里pin_api_init+pin_pic_init→ 设备树gpios属性驱动上层命名解析 → PIC 级联承载中断。对照 pin-pl061.c 移植新控制器,再配合rt_pin_get_named_pin编写消费驱动,即可完全摆脱静态引脚表,让 GPIO 能力随设备树“即插即用”。

  • 操作系统
  • 嵌入式
  • 物联网
  • 嵌入式OS
  • RTOS

【免费下载链接】rt-thread

RT-Thread is an open source IoT Real-Time Operating System (RTOS). https://rt-thread.github.io/rt-thread/

项目地址:https://gitcode.com/gh_mirrors/rt/rt-thread
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询