U-Boot DM(Driver Model)驱动框架

DM 的核心目标:

统一驱动写法、支持设备树、支持平台数据、总线抽象、懒加载(bind 和 probe 分离),替代老 U-Boot 分散、硬编码、无统一管理的驱动模式。

一、什么是DM?

1、早期 U-Boot 驱动

① 驱动没有统一管理,UART、ETH、I2C、SPI 代码各自一套,没有标准接口;

② 硬件信息硬写在板级代码(board.c)里,换板子就要大量改代码;

③ 不支持设备树 DTS,很难复用内核 DTS;

④ 设备实例、私有数据、平台参数管理混乱;

⑤ 没有总线抽象(I2C 总线设备、SPI 从设备没法统一管理)。

2、DM(Driver Model)

U-Boot 引入的一套面向对象风格驱动框架,从 U-Boot 2014 左右引入,2016 之后逐步成熟,2021 版本已经是主力驱动模型,我们看到的 GMAC、UART、PHY、I2C 都是 DM 驱动。

一句话核心思想:

① 把「驱动」「设备实例」「设备类别 (uclass)」「总线 (bus)」拆成独立对象;

② 先 bind 绑定,再 probe 硬件初始化,延迟初始化。

两个最核心的阶段,一定要区分开

  • bind(绑定):只在内存里构建设备节点、建立链表关系,不碰硬件寄存器
  • probe(探测):真正初始化硬件、读寄存器、使能外设、分配私有资源

二、DM 里面 4 大核心结构体(基础四元组)

1. struct driver 驱动(驱动代码本身)

代表一个驱动程序,写驱动的人定义这个结构体,里面放:

  1. name:驱动名字(lists_driver_lookup_name()就是按这个名字查找)
  2. id:匹配的 uclass 编号
  3. of_match:设备树 compatible 匹配表(DTS 里compatible = "xxx"就和这里匹配)
  4. bind:可选 bind 回调
  5. probe:最重要!硬件初始化回调函数(probe 时调用)
  6. remove:反初始化
  7. flags:DM_FLAG_*
  • plat_auto:自动分配 platdata(平台数据,硬件静态参数:波特率、phy 地址、RGMII delay 等)

  • priv_auto:自动分配 priv(驱动私有数据,运行时状态、寄存器基地址、缓存等)

    struct driver {
    const char *name;
    enum uclass_id id;
    const struct udevice_id *of_match;
    int (*bind)(struct udevice *dev);
    int (*probe)(struct udevice *dev);
    int (*remove)(struct udevice *dev);
    uint flags;
    };

2. struct udevice 设备实例(一个硬件设备)

每一个 DTS 节点 / 平台设备,bind 之后就生成一个udevice,代表一个硬件实例,是 DM 里最常见的对象。 成员重点:

  • name:设备名
  • driver:指向上面的 struct driver
  • uclass:归属的 uclass
  • parent:父设备(总线控制器,比如 I2C 控制器是 I2C 设备的 parent)
  • platdata:平台静态数据(DTS 解析出来的配置)
  • priv:驱动私有数据
  • parent_priv:总线给子设备的私有数据
  • flags:状态标记(DM_FLAG_BOUND、DM_FLAG_PROBED,标记有没有 bind、有没有 probe)
  • node:设备树节点引用(fdt 节点偏移)

重点:udevice 只是一个 "设备描述符",bind 完就存在;probe 成功后才打上 PROBED 标记,硬件才可用。

3. struct uclass 设备类(uclass)

uclass = 一类功能相同的设备抽象,相当于 "设备类型"。 比如:

  • UCLASS_SERIAL:所有串口
  • UCLASS_ETH:所有以太网 GMAC
  • UCLASS_I2C:I2C 控制器
  • UCLASS_PHY:PHY 芯片
  • UCLASS_GPIO:GPIO 控制器

struct uclass_driver 是 uclass 对应的驱动,定义 uclass 的行为:提供统一接口、遍历、查找、get_device 等。

uclass 提供统一 API:uclass_get_device(),上层不用关心底层是哪个芯片的驱动,只要拿同类设备。 比如:uclass_get_device(UCLASS_SERIAL, 0, &dev) 获取第 0 路串口。

4. struct dm_bus /bus 总线

总线是特殊的 uclass 驱动(I2C、SPI、PCI),负责:

  • 管理子设备
  • 子设备的地址翻译、总线访问(I2C 发 start、SPI 片选)
  • 子设备的 probe 时机控制

总线设备是 parent,挂在总线上的外设是 child。

三、链接器列表 linker-list

我们看到的 lists_driver_lookup_name 的底层

1、U-Boot DM 驱动不是手动放到数组里注册,而是用宏U_BOOT_DRIVER(),这个宏会把struct driver放到一个专门的.u_boot_driver段(linker section)。

2、编译后所有驱动集中在这个段,运行时遍历这个段,就是全部注册的驱动。

3、lists_driver_lookup_name() 就是遍历这个链接段,按 name 查找 driver。

复制代码
U_BOOT_DRIVER(xxx_gmac) = {
	.name	= "xxx_gmac",
	.id	= UCLASS_ETH,
	.of_match = xxx_gmac_ids,
	.probe = xxx_gmac_probe,
	.flags = DM_FLAG_ALLOC_PRIV | DM_FLAG_PLAT_AUTO,
};

四、DM 完整初始化时序

和 board_init_r 里面调用链路对齐

调用顺序:initr_dm() → dm_init_and_scan() → dm_init() + dm_scan() dm_scan() = dm_scan_plat() + dm_extended_scan() + dm_scan_other()

阶段 1:dm_init ()

------ 搭建 DM 框架,不扫描设备树、不创建设备

① INIT_LIST_HEAD 初始化全局链表头(uclass 链表、设备链表)

② 遍历 linker-list 里所有uclass_driver,注册所有 uclass

③ 遍历 linker-list 里所有driver,把驱动挂到全局驱动链表

④ 结果:驱动和 uclass 都登记在册,还没有任何 udevice 实例。

⑤ 不解析 DTS、不 bind、不 probe、不碰硬件。

阶段 2:dm_scan_plat ()

------ 扫描平台数据设备(platdata 设备,无 DTS)

① 老平台、早期板级硬编码设备,用 C 代码构造 platdata,直接 bind 设备。

② 现在新 SOC ARM几乎很少用,大部分设备走 DTS。

阶段 3:dm_extended_scan ()

------ 扫描设备树 DTS,生成 udevice(重点!)

内部核心 dm_scan_fdt() / dm_scan_fdt_node(),递归遍历 DTS 节点:

① 读取 DTS 节点的 compatible 属性;

② 在已经注册的 driver 列表里匹配(lists_driver_lookup_name 或者 of 匹配);

③ 匹配成功 → 调用 device_bind_by_name() → device_bind_common();

④ bind 成功:分配 udevice、platdata、priv(根据plat_auto/priv_auto),加入链表;

⑤ 递归扫描子节点(总线下面的从设备)。

这里只 bind,不 probe!扫描完 DTS,内存里一大堆 udevice,但是硬件完全没初始化。

bind 完成之后:所有 DTS 节点都变成 udevice,打上 DM_FLAG_BOUND 标记。

阶段 4:按需 probe

------懒加载,DM 最经典的设计

1、什么时候 probe?

  • 上层调用 uclass_get_device() / dm_dev_probe() 的时候;
  • 比如 initr_net() 里面拿 ETH 设备,触发 GMAC 的 probe;
  • 串口打印:console 初始化的时候拿 SERIAL 设备,触发 UART probe。

2、probe 流程:

  • 检查是否已经 probe,防止重复初始化;
  • 如果有 parent 总线,先 probe 父设备(比如 I2C 设备,先 probe I2C 控制器);
  • 调用 driver->probe 回调函数(驱动自己写的硬件初始化:寄存器、时钟、复位、PHY);
  • 标记 DM_FLAG_PROBED,设备可用。

懒加载好处:不需要的外设可以不初始化,节省启动时间、减少上电噪声。

五、两条设备来源路径

------plat 设备 vs FDT 设备

1、plat 设备:dm_scan_plat ()

板级 C 代码静态构造 platdata,不用 DTS,老方案。

2、fdt 设备:dm_extended_scan ()

从设备树节点生成,现代 SOC 主流,大多数都是这种。

六、U-Boot DM 和 Linux kernel 设备驱动模型(devicetree/driver core)对比

1、思路相似:

DTS、device、driver、bus 匹配;

2、巨大差异:

Linux 内核驱动模型支持热插拔、电源管理、子系统、中断子系统;

3、U-Boot DM 极简裁剪:

为启动速度服务,懒加载、没有内核的复杂子系统,只满足启动阶段需求,probe 之后一般不会 remove,很少热插拔。

七、SOC ARM里U-Boot DM整个启动流里的位置

复制代码
board_init_f()
    -> initf_dm()        // 早期DM(可选,很少放硬件)
重定位
board_init_r()
    -> initr_dm()
        -> dm_init_and_scan()
            dm_init()           // 注册uclass、driver
            dm_scan_plat()
            dm_extended_scan()  // 解析DTS,bind所有GMAC/UART/I2C设备
    -> initr_net()              // 去uclass里拿ETH设备,触发GMAC probe
    -> initr_serial()           // 拿串口设备,probe UART
    -> bootcmd 启动kernel
相关推荐
硅基造物主2 小时前
STM32 从 0 到 1 做智能家居(一):先想清楚要做什么
stm32·mqtt·嵌入式·智能家居·hal库·esp-12f
凉、介3 小时前
深入理解 ARMv7-M | 异常/中断处理
笔记·学习·嵌入式·arm
凉、介3 小时前
深入理解 ARMv7-M|处理器模式与寄存器
笔记·学习·嵌入式·arm
LCG米13 小时前
STM32 DMA 多通道链式传输实测:请求仲裁顺序错位根因、半满中断乒乓与高速并发搬运落地
stm32·单片机·嵌入式·1024程序员节
Sakura_j_h_18 小时前
STM32F103 定时器的三种用法:PWM 输出、输入捕获与编码器接口
stm32·嵌入式·输入捕获·定时器·pwm·编码器·1024程序员节
pipixia2003319 小时前
C++ 面试高频:namespace 的 6 个知识点,从 using 到匿名命名空间
c语言·学习·算法·嵌入式·命名空间·c++面试·匿名命名空间
Zevalin爱灰灰20 小时前
Linux入门第一部分(Ubuntu入门篇) 第二章 浅尝Ubuntu(二)
linux·嵌入式·1024程序员节
Zevalin爱灰灰1 天前
Linux入门第一部分(Ubuntu入门篇) 第二章 浅尝Ubuntu(一)
linux·嵌入式·1024程序员节
Zwawa2 天前
一盏 LED 不够用了:让同一盏灯说出不同的话
c语言·单片机·嵌入式