RT-Thread 和 FreeRTOS 有什么不同
FreeRTOS 是一个纯内核:给你任务调度、队列、信号量,剩下的文件系统、网络协议栈、设备模型都要自己拼。RT-Thread 在实时内核之上,还带了一套完整的组件生态:设备框架、文件系统、网络协议栈、Shell 命令行、自动初始化机制,开箱就能跑一个带命令行的物联网设备。
FreeRTOS 像一块裸机芯,RT-Thread 像一台装好常用软件的小电脑。你写产品代码时更多是“接组件”而不是“造轮子”。
RT-Thread 分标准版和 Nano 版:Nano 版只有几 KB,和 FreeRTOS 定位类似;标准版带了完整的组件,适合资源稍宽裕的联网设备。它由国内团队主导开发,中文文档和社区资料非常全,遇到问题搜得到答案,这对国内嵌入式工程师是实打实的便利。
线程管理:创建、优先级与时间片
RT-Thread 里的“任务”叫线程。创建线程用 rt_thread_create,可以静态或动态方式。它同样是抢占式调度,相同优先级线程按时间片轮转。一个值得注意的区别是:RT-Thread 的线程栈单位直接是字节,不像 FreeRTOS 那样以字为单位。
#include <rtthread.h>
static rt_thread_t tid_sensor, tid_upload;
static void sensor_entry(void *param)
{
rt_tick_t last = rt_tick_get();
while (1) {
read_sensor();
/* 严格周期延时 */
rt_thread_mdelay(10);
}
}
static void upload_entry(void *param)
{
while (1) {
/* 等队列里有数据再发 */
rt_mb_recv(sample_mb, &sample, RT_WAITING_FOREVER);
do_upload(&sample);
}
}
int main(void)
{
/* 动态创建:名称、入口、栈大小、优先级、时间片 */
tid_sensor = rt_thread_create("sensor", sensor_entry, RT_NULL,
1024, 10, 20);
rt_thread_startup(tid_sensor);
tid_upload = rt_thread_create("upload", upload_entry, RT_NULL,
2048, 15, 20);
rt_thread_startup(tid_upload);
return 0;
}
线程创建后不会立刻运行,必须调用 rt_thread_startup 把它挂到就绪队列。RT-Thread 的 main 线程本身也是一个普通线程,main 返回后它会被自动清理,这一点和裸机 while(1) 结构不同,写的时候要留意。
线程间通信:消息队列与信号量
RT-Thread 提供了一套和 FreeRTOS 思路类似但命名不同的通信原语:消息队列(rt_mq)传数据,信号量(rt_sem)传事件,互斥量(rt_mutex)保护资源,事件集合(rt_event)等多个条件。
#include <rtdef.h>
static rt_mq_t sample_mb; /* 消息队列 */
static rt_sem_t dma_sem; /* 信号量 */
static rt_mutex_t bus_mutex; /* 互斥量 */
struct sample { rt_uint32_t ts; float temp; float hum; };
static void producer(void *arg)
{
struct sample s;
while (1) {
s.ts = rt_tick_get();
s.temp = read_temp();
s.hum = read_hum();
/* 队列满了等 10 个 tick,写不进就丢 */
if (rt_mq_send(sample_mb, &s, sizeof(s)) != RT_EOK) {
g_drop++;
}
rt_thread_mdelay(100);
}
}
static void bus_write(const rt_uint8_t *buf, rt_size_t len)
{
rt_mutex_take(bus_mutex, RT_WAITING_FOREVER);
spi_transfer(buf, len);
rt_mutex_release(bus_mutex);
}
- 传数据用消息队列自带拷贝语义,收发双方解耦,是最常用的线程间通道。
- 传事件用信号量中断里用 rt_sem_release 通知线程处理,避免在中断里干重活。
- 保护资源用互斥量带优先级继承,比信号量更适合锁总线、锁外设。
设备驱动框架:I/O 设备模型
RT-Thread 最有特色的是它的 I/O 设备模型。你写的应用代码只需要 rt_device_find、rt_device_read、rt_device_write 这几个统一接口,不管底层是 UART、SPI 还是 I2C,上层用法都一样。驱动开发者把硬件注册成一个 rt_device,应用层完全感知不到底层差异。
#include <rtdevice.h>
static rt_device_t uart;
int console_init(void)
{
/* 按名字找设备:注册时叫 "uart1" */
uart = rt_device_find("uart1");
if (!uart) return -RT_ERROR;
rt_device_open(uart, RT_DEVICE_OFLAG_RDWR);
/* 设置接收回调:收到数据就调这个函数 */
rt_device_set_rx_indicate(uart, uart_rx_cb);
return RT_EOK;
}
static rt_err_t uart_rx_cb(rt_device_t dev, rt_size_t size)
{
/* 中断上下文里只发信号量,不读数据 */
rt_sem_release(uart_rx_sem);
return RT_EOK;
}
/* 在单独线程里真正读数据 */
static void uart_thread(void *arg)
{
char buf[64];
while (1) {
rt_sem_take(uart_rx_sem, RT_WAITING_FOREVER);
rt_size_t n = rt_device_read(uart, 0, buf, sizeof(buf));
process_line(buf, n);
}
}
这种模型的好处是:换一块芯片、换一种外设,只要驱动层把设备注册好,应用层一行都不用改。这也是 RT-Thread 被称为“物联网操作系统”而不仅仅是“内核”的原因。
组件生态:POSIX、网络与文件系统
RT-Thread 标准版最吸引人的地方是组件能按需裁剪。你想要网络,就挂上 LwIP 组件;想要文件系统,就挂上 Elm-FatFs 或 LittleFS;想要命令行,直接打开 FinSH 组件,串口敲个 ls、ps 就能看系统状态。
| 组件 | 作用 | 资源开销 |
|---|---|---|
| FinSH | 串口命令行,调试利器 | 几 KB |
| LwIP | 轻量级 TCP/IP 协议栈 | 几十 KB RAM |
| DFSG / Elm-FatFs | FAT 文件系统 | 十几 KB |
| mbedTLS | TLS 加密,联网必备 | 上百 KB Flash |
| AT 组件 | 通过 AT 指令驱动蜂窝/WiFi 模块 | 中等 |
自动初始化机制是 RT-Thread 另一个好用的设计:用宏 INIT_APP_EXPORT 把函数标记一下,系统启动时会按顺序自动调用,不需要你在 main 里手写一长串 init。驱动、应用模块各写各的,启动顺序由链接段自动管理。
内存管理与定时器
RT-Thread 提供了几种内存管理方案:小内存设备用简单的内存堆,资源宽裕的用带碎片整理的算法,还有静态内存池适合固定大小对象(比如网络包缓冲)。软件定时器和 FreeRTOS 类似,回调在定时器线程上下文执行,不能阻塞。
/* 静态内存池:固定大小块,适合频繁申请释放的网络缓冲 */
#define BLK_SZ 128
#define BLK_NUM 32
static rt_uint8_t pool_mem[BLK_SZ * BLK_NUM];
static rt_mp_t mp;
int mp_init(void)
{
mp = rt_mp_init("pkt", pool_mem, sizeof(pool_mem), BLK_SZ);
return mp ? RT_EOK : -RT_ERROR;
}
/* 周期性软件定时器 */
static rt_timer_t tmr;
static void tmr_cb(void *p)
{
/* 这里是定时器线程,别干重活 */
g_flag_report = 1;
}
int tmr_setup(void)
{
tmr = rt_timer_create("rep", tmr_cb, RT_NULL,
rt_tick_from_millisecond(5000),
RT_TIMER_FLAG_PERIODIC);
rt_timer_start(tmr);
return RT_EOK;
}
从裸机迁移到 RT-Thread 的步骤
最后给一条从裸机代码迁到 RT-Thread 的稳妥路径,按这个顺序走不容易翻车。
- 第一步:先在板子上跑通 RT-Thread BSP,用 FinSH 敲出线程列表和内存信息,确认内核活着。
- 第二步:把裸机的 main 循环拆成两个线程——一个周期采集,一个处理上报,先用消息队列把它们连起来。
- 第三步:把硬件操作封装成 rt_device,应用层改用统一接口访问,顺便改掉一堆全局变量。
- 第四步:按需加组件,先加文件系统存历史数据,再加网络模块上报。
- 第五步:打开看门狗和栈溢出检测,长时间跑压力测试,观察线程高水位。
RT-Thread 的学习曲线比 FreeRTOS 平缓一些,因为很多底层都封装好了。真正要花心思的还是并发思维:把串行的 while(1) 拆成协作的线程,这一步迈过去,后面就是组件拼装的快乐了。
建议先用一块带网口或 WiFi 的开发板做实验,把 FinSH、消息队列和一个联网上报跑通。一旦体验过“敲命令就能看系统状态”的便利,就回不去裸机调试了。