RT-Thread 内核基础与启动流程
各位同学,欢迎来到第二章。这一章我们聊聊 RT-Thread 的内核基础和启动流程。说实话,很多初学者一上来就急着写应用,结果系统跑飞了都不知道从哪查起。我个人习惯是,先把内核的“骨架”搞清楚,再去看代码怎么跑起来的。这样出了问题,你心里才有底。
RT-Thread 简介
RT-Thread 是一个开源的实时操作系统,主要面向物联网和嵌入式设备。它不像 Linux 那么重,也不像裸机那么“裸”。它提供了一套完整的线程管理、同步机制、内存管理,还有丰富的设备驱动框架。
我最早接触 RT-Thread 是在一个工业控制项目里。当时需要在 STM32F407 上同时处理传感器采集、通信协议栈和人机交互。裸机写中断太痛苦了,上 RTOS 是必然选择。选 RT-Thread 的原因很简单:它原生支持 STM32,而且社区活跃,遇到问题能找到人问。
RT-Thread 有几个关键特点:
- 轻量级内核:最小配置下 ROM 不到 3KB,RAM 不到 1KB
- 组件丰富:文件系统、网络协议栈、GUI 等可选组件
- 标准 API:兼容 POSIX 接口,方便代码移植
- 设备驱动框架:统一了外设驱动的编写规范
内核对象模型
RT-Thread 的内核设计围绕“对象”展开。说白了,内核里所有的资源——线程、信号量、互斥量、消息队列、事件、内存池——都是对象。它们都继承自一个基础结构体 rt_object。
为什么要这么设计?我举个例子。你在调试时想打印当前系统里所有线程的状态,如果每个资源都是独立的结构体,你得写不同的遍历函数。但有了对象模型,内核只需要维护一张对象容器表,遍历起来就统一了。
来看一下核心的数据结构:
struct rt_object
{
char name[RT_NAME_MAX]; // 对象名称
rt_uint8_t type; // 对象类型
rt_uint8_t flag; // 对象标志
rt_list_t list; // 对象链表节点
};
每个对象都会被挂接到对应类型的链表上。内核通过 rt_object_get_information() 可以获取所有对象的信息。我在项目中就用这个接口写过一个小工具,把系统里所有线程和信号量的状态打印到串口,调试死锁问题特别管用。
对象类型用枚举定义,常见的有:
| 对象类型 | 说明 | 典型应用场景 |
|---|---|---|
| RT_Object_Class_Thread | 线程 | 任务调度、并发处理 |
| RT_Object_Class_Semaphore | 信号量 | 资源计数、同步 |
| RT_Object_Class_Mutex | 互斥量 | 保护共享资源,支持优先级继承 |
| RT_Object_Class_MailBox | 邮箱 | 线程间传递小数据 |
| RT_Object_Class_MsgQueue | 消息队列 | 线程间传递数据块 |
| RT_Object_Class_MemPool | 内存池 | 固定大小内存分配,无碎片 |
| RT_Object_Class_Device | 设备 | 外设驱动抽象 |
| RT_Object_Class_Timer | 定时器 | 软定时、超时处理 |
rtconfig.h 中开启 RT_USING_OBJECT_INFO,就可以在 FinSH 控制台输入 list_thread、list_sem 等命令查看对象状态。调试利器,建议默认打开。
启动流程分析
RT-Thread 的启动流程,说白了就是一条从复位到应用入口的执行链。我把它拆成四个阶段:汇编启动、C 运行时初始化、板级初始化、应用入口。每个阶段都有明确的职责。
先看一张整体流程图:
1. 汇编启动阶段
芯片上电后,CPU 从复位向量表取出 Reset_Handler 的地址,然后跳转执行。这个阶段全部是汇编代码,在 startup_stm32xxxx.s 文件中。主要做三件事:
- 设置主栈指针
SP - 初始化中断向量表
- 调用
SystemInit配置系统时钟
最后跳转到 entry 或 $Sub$$main,进入 C 环境。
__iar_program_start,MDK 是 $Sub$$main。移植时一定要确认启动文件里的符号名。
2. C 运行时初始化
进入 C 代码后,第一件事是初始化运行时环境。这部分由编译器提供的启动代码完成:
- BSS 段清零:把未初始化的全局变量全部置零
- 数据段拷贝:把初始化的全局变量从 Flash 拷贝到 RAM
- 堆栈初始化:设置堆的起始地址和大小
完成后,调用 rt_hw_board_init() 进入板级初始化。
3. 板级初始化
rt_hw_board_init() 在 board.c 中实现。它的职责是让硬件“活”起来:
- 配置系统滴答定时器
SysTick,提供系统心跳 - 初始化调试串口,方便打印信息
- 初始化动态内存堆
- 调用
rt_hw_interrupt_enable()开启全局中断
我记得第一次移植 RT-Thread 到新板子时,卡在 SysTick 配置上。芯片的主频没设对,导致系统时钟快了 10 倍,线程调度完全乱套。后来老老实实对着数据手册重新算了一遍分频系数才搞定。
4. 应用入口
板级初始化完成后,内核调用 rt_application_init(),最终进入用户的 main() 函数。这时候调度器还没启动,你在 main 里可以创建线程、信号量等内核对象。最后调用 rt_thread_startup() 启动调度器,系统开始多任务运行。
自动初始化机制
RT-Thread 有一个很巧妙的设计——自动初始化。你想想看,一个复杂的系统可能有几十个模块,每个模块都需要初始化。如果全写在 main 函数里,那 main 函数会变得又臭又长。自动初始化机制解决了这个问题。
它的原理很简单:利用编译器的段属性,把初始化函数指针放到指定的段里。系统启动时,按优先级顺序遍历这些段,依次调用函数。
RT-Thread 定义了四个初始化级别:
| 宏定义 | 优先级 | 调用时机 | 典型用途 |
|---|---|---|---|
INIT_BOARD_EXPORT(fn) |
1(最高) | 板级初始化中 | GPIO、时钟、外设底层初始化 |
INIT_PREVICE_EXPORT(fn) |
2 | 板级初始化后 | 驱动框架、组件初始化 |
INIT_DEVICE_EXPORT(fn) |
3 | 设备初始化阶段 | 具体设备注册、配置 |
INIT_APP_EXPORT(fn) |
4(最低) | 应用初始化阶段 | 应用层模块、业务逻辑 |
使用方式很简单,比如你想在系统启动时自动初始化一个传感器驱动:
#include <rtthread.h>
static int sensor_hw_init(void)
{
// 初始化传感器硬件
rt_pin_mode(10, PIN_MODE_OUTPUT);
rt_pin_write(10, PIN_HIGH);
rt_kprintf("sensor hardware initialized\n");
return 0;
}
INIT_DEVICE_EXPORT(sensor_hw_init);
这样写,你不需要在 main 函数里显式调用 sensor_hw_init()。系统会在设备初始化阶段自动执行它。
我曾经在一个项目中踩过坑:两个模块都用 INIT_DEVICE_EXPORT,但模块 A 依赖模块 B 先初始化。结果模块 A 先被执行了,导致初始化失败。后来我查了源码才发现,同一级别的初始化顺序取决于链接顺序,不能保证先后。解决方案是把依赖关系强的模块放到不同级别,或者显式在函数内部做依赖检查。
自动初始化的实现核心在 components.c 中:
static int rt_iops_auto_init(void)
{
const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
// ... 其他级别类似
return 0;
}
这里的 __rt_init_rti_board_start 和 __rt_init_rti_board_end 是链接器生成的符号,标记了对应段的起始和结束地址。系统通过指针遍历,逐个调用初始化函数。
嗯,这一章的内容就到这里。启动流程和自动初始化机制是理解 RT-Thread 运行逻辑的基础。搞清楚了这些,后面学设备驱动框架、线程间通信就会顺畅很多。