2. BSP基础概念
做BSP移植这些年,我最大的感触就是:驱动模型决定了你后续80%的工作量。不管你用Linux、RT-Thread还是FreeRTOS,驱动模型的核心逻辑其实就那几件事——怎么抽象硬件、怎么分层、怎么给上层提供接口。但每个系统的实现方式又各有脾气,搞混了容易踩坑。
这一章,咱们就把这三个主流系统的驱动模型掰开揉碎,看看它们到底怎么设计的。我还会结合自己踩过的坑,告诉你哪些地方容易翻车。
2.1 驱动模型的核心:字符设备 vs 块设备
先聊个基础概念。你想想看,外设千千万,但抽象到驱动层面,其实就两大类:字符设备和块设备。
| 类型 | 特点 | 典型设备 | 访问方式 |
|---|---|---|---|
| 字符设备 | 按字节流顺序访问,不支持随机读写 | UART、SPI、I2C、GPIO | open/read/write/ioctl |
| 块设备 | 按固定大小块随机访问,支持缓存 | eMMC、NAND Flash、SD卡 | 块读写、挂载文件系统 |
我个人习惯把字符设备想象成水管——数据从一头流进去,从另一头流出来,你不能跳到中间去取数据。块设备则像书架——你可以直接抽第3本书,也可以放回第7本的位置。
关键点:在BSP移植中,90%的外设都是字符设备。块设备通常只有存储类外设。但RT-Thread和FreeRTOS对块设备的支持比Linux弱很多,如果你要移植SD卡驱动,得自己实现块设备层。
2.2 Linux驱动模型:最成熟的框架
Linux的驱动模型,说白了就是设备-总线-驱动三件套。内核维护一个设备链表和一个驱动链表,总线负责匹配它们。匹配上了,就调用驱动的probe函数初始化硬件。
我记得第一次看Linux驱动代码时,被那一堆结构体绕晕了。后来发现,核心就这几个东西:
- struct device:描述硬件设备,包含资源信息(地址、中断号等)
- struct device_driver:描述驱动,包含probe/remove等回调
- struct bus_type:描述总线,负责匹配设备和驱动
写一个简单的字符设备驱动,流程大概是:
// 1. 定义文件操作结构体
static struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
.write = my_write,
.release = my_release,
};
// 2. 注册字符设备
static int __init my_init(void) {
// 分配设备号
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
// 初始化cdev并添加到内核
cdev_init(&my_cdev, &my_fops);
cdev_add(&my_cdev, dev_num, 1);
// 创建设备节点(自动或手动)
return 0;
}
避坑指南:我曾经在移植SPI驱动时,忘了在设备树里配置pinctrl,结果probe函数死活不执行。排查了两天才发现——Linux的设备树里,引脚复用必须显式声明,不像RT-Thread那样可以在驱动里直接操作GPIO寄存器。
2.3 RT-Thread驱动模型:轻量级但五脏俱全
RT-Thread的驱动模型,我个人觉得是Linux的简化版。它没有设备树,没有复杂的总线匹配机制,但该有的抽象层一个不少。
RT-Thread的核心思路是:设备驱动框架 + 设备对象。每个设备都是一个对象,挂在内核的设备链表上。上层通过统一的接口访问:
- rt_device_find():查找设备
- rt_device_open():打开设备
- rt_device_read() / rt_device_write():读写
- rt_device_control():控制命令(相当于ioctl)
写一个RT-Thread的UART驱动,核心代码大概这样:
// 1. 定义设备操作接口
static rt_err_t uart_init(rt_device_t dev) {
// 初始化硬件寄存器
return RT_EOK;
}
static rt_size_t uart_read(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size) {
// 从FIFO读数据
return read_bytes;
}
static rt_size_t uart_write(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size) {
// 往FIFO写数据
return write_bytes;
}
// 2. 注册设备
static struct rt_device uart_device;
rt_device_register(&uart_device, "uart1", RT_DEVICE_FLAG_RDWR);
嗯,这里要注意:RT-Thread的设备注册是静态的,不像Linux那样动态分配设备号。你在BSP里定义好设备对象,调用rt_device_register就行。但这也意味着——如果你要支持多个同类型设备,得手动维护多个设备实例。
我曾经踩过的坑:在RT-Thread里移植I2C驱动时,忘了设置设备类型标志位。结果上层调用rt_device_control()时,框架直接返回-ENOSYS。排查了半天才发现——设备标志位必须明确指定,比如RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX。
2.4 FreeRTOS驱动模型:最原始的“裸奔”风格
FreeRTOS本身没有标准的驱动模型。它只提供内核(任务、队列、信号量),驱动层完全由开发者自己搭建。说白了,FreeRTOS的驱动就是在裸机驱动外面包一层RTOS的同步机制。
我刚开始用FreeRTOS做BSP移植时,特别不习惯——没有设备框架,没有统一接口,每个外设的驱动写法都不一样。后来总结出一套通用模式:
- 硬件初始化:配置寄存器、时钟、中断
- 创建队列或信号量:用于中断和任务之间的同步
- 实现中断服务函数:只做最少的处理,发信号给任务
- 实现任务级读写函数:在任务上下文中操作硬件
一个典型的FreeRTOS UART驱动框架:
// 1. 硬件初始化
void uart_hw_init(void) {
// 配置GPIO、时钟、波特率
// 使能中断
}
// 2. 中断服务函数
void UART_IRQHandler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t data = UART->DR;
// 把数据发到队列
xQueueSendFromISR(xRxQueue, &data, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 3. 任务级读函数
uint8_t uart_read(void) {
uint8_t data;
xQueueReceive(xRxQueue, &data, portMAX_DELAY);
return data;
}
核心差异:FreeRTOS的驱动没有“设备对象”的概念。每个外设的驱动都是独立的模块,接口函数名、参数类型全靠开发者自己约定。如果你要写一个可复用的BSP,建议自己封装一层抽象接口,比如定义一个结构体包含所有操作函数指针。
2.5 三大系统驱动模型对比
为了让你看得更清楚,我整理了一张对比表:
| 维度 | Linux | RT-Thread | FreeRTOS |
|---|---|---|---|
| 驱动框架 | 设备-总线-驱动模型 | 设备对象框架 | 无标准框架 |
| 设备描述 | 设备树 + struct device | struct rt_device | 自定义结构体 |
| 接口统一性 | VFS + file_operations | rt_device_xxx 系列 | 无统一接口 |
| 动态注册 | 支持(动态设备号) | 支持(但需静态定义对象) | 不支持 |
| 中断处理 | request_irq + tasklet/workqueue | rt_hw_interrupt_install | 标准ISR + 队列/信号量 |
| 适用场景 | 复杂SoC、功能丰富 | 资源受限、需要标准接口 | 极小系统、完全自定义 |
2.6 驱动框架的通用设计原则
不管用哪个系统,驱动框架的设计都有一些通用原则。这些原则是我在多个项目里摔打出来的,分享给你:
- 分层设计:硬件操作层、驱动核心层、接口层。每层只做自己的事,别混在一起。
- 接口统一:同类型设备用相同的接口函数名和参数。比如所有字符设备都提供open/read/write/ioctl。
- 资源管理:中断、DMA、内存等资源要统一申请和释放,避免冲突。
- 可配置性:通过宏或配置文件控制驱动特性,不要硬编码。
- 错误处理:每个操作都要考虑失败情况,返回明确的错误码。
我的经验:在BSP移植中,最容易被忽略的是电源管理。很多驱动只实现了基本功能,没考虑休眠/唤醒。结果产品做出来后,待机功耗超标。建议从一开始就把suspend/resume接口留好,哪怕暂时是空函数。
2.7 驱动模型核心逻辑图
下面这张图展示了三个系统驱动模型的核心逻辑对比。你可以看到,虽然实现方式不同,但本质都是应用层 → 抽象接口 → 硬件操作这条链路。
2.8 如何选择驱动模型?
最后聊点实际的。你在做BSP移植时,到底选哪个系统的驱动模型?我的建议是:
- 项目用Linux:直接跟着内核框架走,别自己发明轮子。设备树、platform驱动、regmap这些机制都是经过验证的。
- 项目用RT-Thread:优先使用官方提供的设备驱动框架。如果官方没有,参考同类型设备的实现方式,保持接口风格一致。
- 项目用FreeRTOS:自己封装一层抽象接口。我习惯定义一个
struct device_ops结构体,包含init/read/write/ioctl函数指针,所有驱动都按这个模板写。
重要提醒:不管选哪个模型,不要在一个项目里混用多种驱动风格。我见过有人把Linux的file_operations思路搬到FreeRTOS里,结果代码又丑又难维护。保持一致性,比追求“高级”更重要。