讲真,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_PULLUP,attach_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 = ®_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 pin,enable靠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 = ® 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条件会异常。加了这个标志让总线保持占有状态。类似这种细节,文档不会告诉你,得自己踩过才知道。
