讲真,RT-Thread的设备驱动框架让我又爱又恨

发布时间:2026/6/30 20:21:45

讲真,RT-Thread的设备驱动框架让我又爱又恨 之前做个网关项目STM32F429 RT-Thread板上挂了四个传感器、一个LCD、一个以太网。刚开始信心满满——RT-Thread不是号称国内最活跃的RTOS么设备驱动框架肯定稳。结果翻车了。翻得很彻底。先说说我爱它的地方RT-Thread这套 device framework 是真的有想法。它不像FreeRTOS那样基本就是个调度器加消息队列你要啥都得自己搭。RT-Thread直接给你铺好了路static int rt_hw_sensor_init(void) { int result 0; struct rt_sensor_config cfg; cfg.intf.type RT_SENSOR_INTF_I2C; cfg.intf.dev_name i2c1; cfg.irq_pin.pin SENSOR_INT_PIN; cfg.irq_pin.mode PIN_MODE_INPUT_PULLUP; result rt_hw_sht30_init(sht30, cfg); if (result ! RT_EOK) { LOG_E(sht30 init failed: %d, result); return result; } return RT_EOK; }看到没rt_hw_sht30_init、rt_hw_bme280_init这些函数只要注册好上层直接用 sensor framework 统一读取。一个struct rt_sensor_data结构体就把温度、湿度、气压全包了。写应用的人根本不需要知道底层是I2C还是SPI。这设计真的舒服。但坑也在这里问题出在设备树和pin设备的配合上。RT-Thread引用了一套类似Linux device tree的机制叫设备树。想法很好——硬件描述和驱动代码分离。但实际用起来我踩了一个大坑。当时有个GPIO中断死活不触发。代码长这样struct rt_device_pin_mode mode; mode.pin GET_PIN(B, 1); mode.mode PIN_MODE_INPUT_PULLUP; rt_device_control(pin_dev, RT_DEVICE_CTRL_PIN_SET_MODE, mode); rt_pin_attach_irq(GET_PIN(B, 1), PIN_IRQ_MODE_FALLING, my_irq_callback, NULL); rt_pin_irq_enable(GET_PIN(B, 1), PIN_ENABLE);检查了三遍电路确认外部确实有下降沿。拿逻辑 analyzer 抓了波形干干净净。后来发现是什么问题RT-Thread的pin设备在使能中断前要先设置pin的模式为中断模式——但这里有个隐藏条件rt_pin_attach_irq内部其实已经帮你做了模式切换。问题是我在前面rt_device_control设了PIN_MODE_INPUT_PULLUPattach_irq 又被覆盖了。顺序反了。正确姿势rt_pin_attach_irq(GET_PIN(B, 1), PIN_IRQ_MODE_FALLING, my_irq_callback, NULL); rt_pin_irq_enable(GET_PIN(B, 1), PIN_ENABLE);把rt_device_control那句删掉就好了。因为 attach_irq 内部已经处理了 pin mode。驱动分层是好事但别被抽象绕晕RT-Thread的I2C驱动分层也让我绕了一阵。三层I2C核心层、I2C总线设备驱动层、I2C从设备驱动层。你要操作一个从设备理论上调rt_i2c_master_send/rt_i2c_master_recv就行了。但如果你像我一样手贱去看源码会发现struct rt_i2c_msg里的flags字段有很多讲究struct rt_i2c_msg { rt_uint16_t addr; rt_uint16_t flags; rt_uint16_t len; rt_uint8_t *buf; };flags可以组合RT_I2C_WR、RT_I2C_RD、RT_I2C_NO_START、RT_I2C_IGNORE_NACK、RT_I2C_NO_READ_ACK。我一开始以为NO_START是不发start后来看了代码才发现它是在多消息传输中复用上一个消息的起始条件——用来做重复起始条件repeated start。举个例子读一个寄存器必须先写寄存器地址再读数据struct rt_i2c_msg msgs[2]; msgs[0].addr addr; msgs[0].flags RT_I2C_WR; msgs[0].buf reg_addr; msgs[0].len 1; msgs[1].addr addr; msgs[1].flags RT_I2C_RD; msgs[1].buf data; msgs[1].len 2; rt_i2c_transfer(bus, msgs, 2);这跟Linux内核的i2c_msg结构体几乎一样但RT-Thread文档里这个例子写得不够明显。我一开始忘了这是 写地址→重启动→读数据 的典型流程还以为每次transfer都是独立的起始和停止。结果读出来的数据永远不对。最后还是妥协了不过说句公道话RT-Thread这套框架对上层的封装是真的好用。比如sensor框架你只要写一个驱动注册进去应用层开一个线程轮询rt_device_read就完事。而且它那个rt_sensor_get_info能自动识别传感器类型省了很多事。唯一想吐槽的是文档更新速度——API接口变了文档没跟上。比如pin设备那块的rt_pin_irq_enable函数旧版本的传入参数是rt_base_t pin和rt_bool_t enable新版本改成了传rt_base_t pinenable靠rt_pin_irq_enable(pin, 0)的第二个参数控制。不看源码真不知道。反正摸爬滚打一圈下来RT-Thread的设备驱动框架确实值得花时间搞懂。API设计思路比裸机写寄存器舒服太多了只是文档和实际代码之间偶尔有gap源码就是最好的文档——这话在RT-Thread上尤其正确。最后贴一个我常用的I2C传感器读取封装算是自己总结的最佳实践static rt_err_t read_sensor_reg(struct rt_i2c_bus_device *bus, rt_uint8_t slave_addr, rt_uint8_t reg, rt_uint8_t *data, rt_uint16_t len) { struct rt_i2c_msg msgs[2]; msgs[0].addr slave_addr; msgs[0].flags RT_I2C_WR; msgs[0].buf reg; msgs[0].len 1; msgs[1].addr slave_addr; msgs[1].flags RT_I2C_RD | RT_I2C_NO_STOP; msgs[1].buf data; msgs[1].len len; if (rt_i2c_transfer(bus, msgs, 2) ! 2) return -RT_ERROR; return RT_EOK; }为什么要加RT_I2C_NO_STOP因为我接的某个传感器在连续读取时如果收到stop条件会异常。加了这个标志让总线保持占有状态。类似这种细节文档不会告诉你得自己踩过才知道。

相关新闻