4. 设备模型基本概念

在裸机开发中,我们通常直接操作寄存器:打开时钟、配置引脚、写数据寄存器。这种方式简单直接,但当系统里出现多个相同外设、多个驱动需要复用、设备需要动态插拔时,代码会迅速变得难以维护。RTOS 设备模型的核心目标,就是把“硬件资源”和“操作硬件的方法”解耦,用统一的抽象让上层应用不再关心底层差异。

设备模型要回答四个问题:谁在操作硬件(驱动)、操作的是哪个硬件(设备)、硬件挂在哪里(总线)、这类硬件有什么共同行为(类)。四者各司其职,又通过注册与匹配机制连接起来。

一、四个核心概念

设备(Device)是对一个具体硬件实例的抽象。它描述“有一个 UART,位于 0x40004000,使用中断 12,波特率 115200”。设备对象通常包含名称、地址、中断号、私有数据指针以及指向所属驱动的指针。设备是“被操作的对象”。

驱动(Driver)是对一类硬件操作方法的封装。它描述“如何初始化 UART、如何发送一个字节、如何接收一个字节”。驱动提供 open、close、read、write、ioctl 等标准接口,是“操作的方法”。同一个驱动可以服务多个同型号设备实例。

总线(Bus)是设备与驱动相遇的场所,也是匹配规则的载体。在 Linux 中体现为 platform、I2C、SPI、USB 等总线;在 RTOS 中通常简化为“设备注册表 + 匹配键”。总线负责把设备挂上去、把驱动挂上去,然后按名称或 ID 进行配对。

类(Class)是对具有相同行为特征设备的进一步归类,例如串口类、块设备类、网络类、输入类。类向上层提供统一的操作接口,使应用可以用同一套 API 操作不同厂商的同类硬件。

二、RTOS 中的抽象方式

与 Linux 相比,RTOS 设备模型更轻量,但保留了核心思想。常见做法是:用一个全局设备链表保存所有已注册设备;每个设备结构体内嵌一个驱动操作集指针;驱动通过注册函数把操作集绑定到设备;应用通过设备名查找设备,再调用其操作集。

typedef struct rt_device {
    const char        *name;      /* 设备名,如 "uart1" */
    struct rt_device_ops *ops;    /* 驱动操作集 */
    void              *user_data; /* 设备私有数据 */
    rt_uint16_t        type;      /* 设备类:串口/块/网络 */
    rt_uint16_t        flag;      /* 读写权限、打开标志 */
    struct rt_device  *next;      /* 设备链表 */
} rt_device_t;

struct rt_device_ops {
    rt_err_t (*init)   (rt_device_t dev);
    rt_err_t (*open)   (rt_device_t dev, rt_uint16_t oflag);
    rt_err_t (*close)  (rt_device_t dev);
    rt_size_t (*read)  (rt_device_t dev, rt_off_t pos, void *buf, rt_size_t size);
    rt_size_t (*write) (rt_device_t dev, rt_off_t pos, const void *buf, rt_size_t size);
    rt_err_t (*control)(rt_device_t dev, int cmd, void *args);
};

这段结构体是设备模型的骨架:name 用于查找,ops 用于调用,type 用于归类,user_data 用于保存硬件相关上下文。应用层只持有 rt_device_t,不关心它背后是 STM32 的 USART 还是 NXP 的 LPUART。

三、注册、匹配与使用流程

  1. 设备注册:板级初始化时,把每个硬件实例填入设备结构体,调用 rt_device_register() 挂入全局链表。
  2. 驱动绑定:驱动模块把自己的操作集通过 rt_device_register() 或匹配函数绑定到对应设备。
  3. 查找设备:应用调用 rt_device_find("uart1"),按名称遍历链表返回设备句柄。
  4. 打开与操作:调用 rt_device_open() 触发驱动的 open,之后用 read/write/control 完成数据交互。
  5. 关闭与注销:不再使用时 close,系统退出时 unregister 释放资源。

下面这张图展示了设备、驱动、总线、类四者之间的关系,以及应用通过设备句柄访问硬件的路径。

RTOS 设备模型:设备 · 驱动 · 总线 · 类 应用层 Application 类 Class 串口类 / 块设备类 统一 API 接口 总线 Bus 设备链表 + 匹配键 platform / I2C / SPI 设备 Device uart1 @0x40004000 中断 12 · 私有数据 驱动 Driver init/open/read/write control 操作集 物理硬件 USART / GPIO / SPI 控制器 绑定 ops 寄存器读写 匹配 图例 调用 / 查找 匹配 / 绑定

四、典型场景:串口设备的完整生命周期

假设系统中有两个 UART:调试串口 uart0 和通信串口 uart1。板级代码先注册两个设备对象,驱动模块注册一份 UART 操作集。总线按名称把操作集绑定到两个设备上。应用调用 rt_device_find("uart1") 得到句柄,rt_device_open() 触发驱动的 open 完成时钟与引脚初始化,随后 rt_device_write() 把数据写入发送寄存器。整个过程应用只认识“串口类”的接口,不关心底层是哪个芯片。

概念职责RTOS 中的典型载体关键字段/接口
设备 Device描述一个具体硬件实例rt_device 结构体实例name、user_data、type
驱动 Driver封装一类硬件的操作方法rt_device_ops 操作集init、open、read、write、control
总线 Bus提供注册与匹配场所全局设备链表 / 匹配函数register、find、match
类 Class定义同类设备的统一行为设备 type 分类 + 统一 APIRT_Device_Class_Char 等

五、常见误区与复盘

误区一:把设备和驱动写成一个结构体。这样虽然能跑,但同型号的第二个设备就无法复用驱动代码,设备模型的意义就丢失了。
误区二:在应用层直接调用驱动函数。绕过设备句柄会让上层与具体驱动耦合,换芯片时应用需要大改。
误区三:忽略总线匹配的失败处理。设备注册了但驱动没绑定,rt_device_find 可能返回句柄而 open 失败,必须在 open 中检查 ops 是否为空。
复盘建议:每新增一个外设,先问自己三个问题——它的设备对象在哪里注册?它的操作集由哪个驱动提供?它属于哪个类、上层用哪套 API 访问?回答清楚,设备模型就用对了。

理解设备、驱动、总线、类这四个概念及其在 RTOS 中的轻量抽象,是后续学习具体驱动框架、设备树描述和电源管理的基础。设备模型不是额外负担,而是让驱动可复用、应用可移植的关键一层。