1. 为什么需要 IO 设备模型?
不同芯片厂商的驱动库存在差异。若应用层直接调用厂商驱动,后续更换芯片、升级驱动或替换外设时,应用代码也要跟着修改,可移植性很差。
RT-Thread 通过统一的 IO 设备模型,将应用程序与底层硬件驱动隔离。上层只调用统一接口,底层硬件如何实现由驱动负责。
2. 三层结构
应用程序
↓
IO 设备管理层
↓
设备驱动框架层
↓
具体设备驱动层
↓
硬件
-
IO 设备管理层:向应用提供统一的
open、read、write等接口。 -
设备驱动框架层:抽象同类设备的共同功能,例如串口、SPI、I2C。
-
设备驱动层:直接操作寄存器或厂商 HAL 库,完成硬件控制。
简单设备也可以跳过"设备驱动框架层",直接注册到设备管理器。
3. 设备对象
在 RT-Thread 中,设备也是内核对象。每个设备都有一个 struct rt_device 结构体,其中包含:
-
设备类型、名称、状态、打开次数等基本信息;
-
init、open、close、read、write、control等操作函数; -
接收完成
rx_indicate与发送完成tx_complete回调函数; -
驱动私有数据
user_data。
可以把它理解为:设备对象描述设备信息,函数指针决定设备如何工作。
4. 设备常用流程
应用层访问设备的一般流程:
查找设备 → 初始化 → 打开 → 读/写/控制 → 关闭
常用接口如下:
rt_device_t dev;
/* 1. 按名称查找设备 */
dev = rt_device_find("uart1");
/* 2. 打开设备 */
rt_device_open(dev, RT_DEVICE_OFLAG_RDWR);
/* 3. 写数据 */
rt_device_write(dev, 0, "hello\r\n", 7);
/* 4. 关闭设备 */
rt_device_close(dev);
代码解析: rt_device_find("uart1") 按名称获取串口设备句柄;得到 dev 后再以读写模式打开。rt_device_write() 中的 0 是偏移量,串口属于字符设备,通常传 0 即可;7 是字符串 "hello\r\n" 的字节数。设备用完必须关闭,以便驱动减少打开计数并在计数为 0 时释放硬件资源。实际项目应在每一步检查返回值或 dev != RT_NULL。
| 接口 | 作用 |
|---|---|
rt_device_find() |
根据名称获取设备句柄 |
rt_device_init() |
初始化设备 |
rt_device_open() |
打开设备 |
rt_device_read() |
从设备读取数据 |
rt_device_write() |
向设备写入数据 |
rt_device_control() |
使用命令控制设备 |
rt_device_close() |
关闭设备 |
5. 驱动开发流程
驱动层通常需要完成以下工作:
创建设备对象 → 实现操作函数 → 注册设备
注册接口:
rt_device_register(dev, "device_name", flags);
代码解析: 这是驱动层注册设备的接口。dev 是已创建并完成操作函数适配的设备对象;"device_name" 是应用层通过 rt_device_find() 查找时使用的唯一名称;flags 用于声明只读、读写、中断收发等能力。注册成功后,上层才能通过名称访问该设备。
注册后,应用层便能通过设备名称查找并使用该设备。
动态创建的设备,在不再使用时可通过 rt_device_destroy() 销毁;设备也可通过 rt_device_unregister() 从设备管理器中注销。
6. 读写接口的理解
rt_device_read(dev, pos, buffer, size);
rt_device_write(dev, pos, buffer, size);
代码解析: dev 指向目标设备,buffer 是读出数据的存放位置或待发送数据的位置,size 是期望读写的数据量。pos 对块设备表示块偏移,对串口等字符设备通常无意义。返回值是实际读写量;若返回 0,需进一步查看当前线程的 errno。
-
字符设备,如串口:
pos通常没有实际意义,可传0。 -
块设备,如 Flash、SD 卡:
pos表示块偏移,size通常以块为单位。 -
返回值表示实际读写的数据量;返回
0时,需要结合errno判断错误原因。
7. 数据收发回调
接收回调
设备收到数据后,可通过接收回调通知应用层:
rt_device_set_rx_indicate(dev, rx_callback);
代码解析: 将 rx_callback 注册为接收通知函数。驱动接收到数据时会调用它,并把收到的数据长度传给上层;回调中应只做轻量通知,实际数据读取应交给线程调用 rt_device_read() 完成。
回调触发后,上层线程应及时调用 rt_device_read() 读取数据。
发送完成回调
当 DMA 或 FIFO 等硬件完成发送后,可通过发送完成回调通知上层:
rt_device_set_tx_complete(dev, tx_callback);
代码解析: 将 tx_callback 注册为发送完成通知函数。DMA 或 FIFO 发送结束后,驱动调用该函数;此时上层才可以安全复用或释放对应的发送缓冲区。
上层可在回调中释放发送缓冲区,或准备下一次发送。
总结
RT-Thread IO 设备模型的核心价值是:用统一接口屏蔽不同硬件和驱动的差异。
对应用开发者来说,只需掌握"查找、打开、读写、控制、关闭";对驱动开发者来说,则需要实现设备操作函数并完成注册。这样既降低了应用与硬件的耦合,也让代码更容易移植和维护。