为什么要正经做驱动框架
很多人上 RT-Thread 只是把它当一个带定时器的裸机框架用:所有传感器的读代码都堆在 main 线程里,延时靠 delay_ms,串口打印靠 rt_kprintf。这种写法在 demo 阶段没毛病,一旦上产品就会出现:加一个传感器要改三个文件、串口打印被采样打断、死机不知道是哪段代码在踩内存。
RT-Thread 的真正价值不是"能跑多线程",而是它提供了一套标准化的设备模型和组件生态,让你把硬件抽象掉,业务代码不用关心它接在哪条总线上。
这篇把 RT-Thread 的组件与驱动框架真正用起来:写一个温湿度传感器驱动、用消息队列把采样数据送到业务线程、再把常用组件接进来,形成一个能上产品的骨架。
RT-Thread 设备模型速览
RT-Thread 把所有硬件抽象成一类对象:块设备、字符设备、网络设备。你用 rt_device_find("sht30") 按名字拿到一个设备句柄,然后统一用 rt_device_read / rt_device_write 操作它。底层是 I2C、SPI 还是 GPIO,业务线程根本不关心。
- rt_device统一的设备结构体,挂在对象容器上。
- rt_device_register注册你的驱动到系统。
- rt_device_find业务层按名字取设备。
好处是:将来你把 SHT30 换成 SHT40,只要新驱动注册同一个名字,业务代码一行都不用改。
消息队列在传感器采集中的角色
典型的 IoT 设备有两类线程:采样线程(要周期跑、不能被打断)、上报线程(要发网络、可能阻塞几秒)。这两类线程之间不能靠全局变量传数据,必须用消息队列把一帧一帧的数据安全地递过去。
// 创建一个 8 帧、每帧 16 字节的队列
rt_mq_t mq = rt_mq_create("sens_mq", 16, 8, RT_IPC_FLAG_FIFO);
// 采样线程:每 2 秒发一帧
void sensor_thread_entry(void *arg) {
while (1) {
sens_frame_t f;
sht30_read(&f.temp, &f.humi);
rt_mq_send(mq, &f, sizeof(f));
rt_thread_mdelay(2000);
}
}
// 上报线程:阻塞等队列,有数据再发
void report_thread_entry(void *arg) {
sens_frame_t f;
while (1) {
rt_mq_recv(mq, &f, sizeof(f), RT_WAITING_FOREVER);
mqtt_publish(&f);
}
}
队列的价值是:采样线程永远不被网络波动拖住,上报线程慢一拍也不丢数据,队列满了再考虑丢策略。
写一个温湿度传感器驱动
下面是 SHT30 的最小驱动骨架:把它注册成字符设备,业务层就可以像读文件一样读温湿度。
static rt_err_t sht30_read(rt_device_t dev, rt_off_t pos,
void *buf, rt_size_t size) {
uint8_t cmd[2] = {0x2C, 0x06};
i2c_write_read(dev->parent.user_data, cmd, 2, buf, size);
return RT_EOK;
}
static rt_err_t sht30_init(rt_device_t dev) {
return i2c_probe(dev->parent.user_data, 0x44);
}
int rt_hw_sht30_init(const char *bus_name, uint16_t addr) {
static struct rt_i2c_client client;
client.bus = rt_i2c_bus_device_find(bus_name);
client.client_addr = addr;
rt_device_t dev = rt_calloc(1, sizeof(struct rt_device));
dev->type = RT_Device_Class_Char;
dev->init = sht30_init;
dev->read = sht30_read;
dev->parent.user_data = &client;
rt_device_register(dev, "sht30", RT_DEVICE_FLAG_RDONLY);
return RT_EOK;
}
业务层只用:rt_device_t d = rt_device_find("sht30"); rt_device_read(d, 0, &frame, 6);,跟硬件细节完全解耦。
线程优先级与资源隔离
RT-Thread 默认 32 个优先级,数字越小优先级越高。工程上常用分配:
- 中断里的延时函数走最高优先级(数字最小)。
- 传感器采样线程放中间,避免被网络线程饿死。
- 日志、LED 心跳这类后台任务放最低优先级。
- 所有共享资源(I2C 总线、串口)必须用 mutex 包起来,不能两个线程同时抢。
- 优先级反转低优先级线程持锁、高优先级线程等锁,被中间优先级线程卡死。用优先级继承 mutex 解决。
- 中断里 malloc内存管理函数不是可重入的,ISR 里一律用静态缓冲。
常用组件:AT 组件与 DFS
RT-Thread 真正省事的是它的软件包生态。接 4G 模组用 AT 组件,一行命令注册好 URC 回调;要存配置用 DDF 文件系统,片上 SPI Flash 一挂就是 /flash 目录。
- at_device统一管理 4G、Cat.1、WiFi 模组。
- fal把 SPI Flash 切成多分区,存固件、存配置。
- easyflash键值对存参数,掉电不丢。
- mbedtlsTLS 栈,连 MQTT over TLS 用。
这些组件都是 bsp 菜单里打个勾就自动拉下来的,不要自己从零写。
工程化落地清单
最后给一个能直接上产品的落地清单:
// 1. 每个硬件抽象成一个 rt_device,命名按业务含义
// 2. 采样线程周期发消息队列,业务线程阻塞收
// 3. 共享总线一律 rt_mutex_t 包住
// 4. 参数走 easyflash,不要写死在代码里
// 5. 固件双分区 + OTA,出问题能回滚
// 6. 日志走 ulog,分级打印,量产时关掉 DEBUG
RT-Thread 不是一个 API 手册学完就算完的东西,它是一种"把硬件当资源管"的思路。按这套骨架搭出来的项目,加新设备、改业务、换模组,都不会伤到主流程。
把这套骨架跑通,再看 BSP 移植、RISC-V 移植、SMP 多核,方向就很清楚了。