内核基础与启动流程:RT-Thread 是怎么跑起来的
大家好,我是你们的嵌入式讲师。今天咱们聊聊 RT-Thread 的启动流程。说实话,很多初学者拿到开发板,点个灯、跑个线程就觉得完事了。但真要自己移植、调驱动,不懂启动流程,那可就抓瞎了。
我刚开始接触 RT-Thread 时,也踩过不少坑。有一次,板子死活跑不起来,查了半天,结果是汇编启动段里中断向量表没对齐。嗯,从那以后,我每次移植都会先过一遍启动流程。
从复位到 main:汇编启动阶段
芯片上电后,第一件事是什么?
不是 C 语言,不是 RT-Thread,而是汇编。STM32 的启动文件(startup_stm32xxxx.s)会先接管 CPU。它干这几件事:
- 设置堆栈指针——从向量表第一个字加载 SP
- 跳转复位向量——执行 Reset_Handler
- 初始化 .bss 段——清零全局未初始化变量
- 拷贝 .data 段——从 Flash 搬到 RAM
- 调用 SystemInit——配置时钟、PLL、Flash 等待周期
- 跳转 __main(C 库初始化)或直接跳 main
关键点:RT-Thread 的启动入口不是 main,而是 entry()。在汇编启动的最后,会调用 bl entry,而不是 bl main。
为什么会这样?因为 RT-Thread 需要接管系统控制权。main 函数在 RT-Thread 里只是一个普通线程入口,不是传统意义上的主函数。
C 语言初始化:RT-Thread 的“家底”怎么铺开的
进入 C 环境后,RT-Thread 会调用 rtthread_startup()。这个函数在 components.c 里,我建议你打开源码看看,逻辑非常清晰。
void rtthread_startup(void)
{
/* 关闭中断 */
rt_hw_interrupt_disable();
/* 板级初始化:时钟、GPIO、串口等 */
rt_hw_board_init();
/* 打印 RT-Thread 版本信息 */
rt_show_version();
/* 系统定时器初始化 */
rt_system_timer_init();
/* 调度器初始化 */
rt_system_scheduler_init();
/* 应用入口,自动初始化从这里开始 */
rt_application_init();
/* 定时器线程初始化 */
rt_system_timer_thread_init();
/* 空闲线程初始化 */
rt_thread_idle_init();
/* 启动调度器,不再返回 */
rt_system_scheduler_start();
}
你看,这个函数把 RT-Thread 的“五脏六腑”都初始化了。我个人习惯把 rt_hw_board_init() 看作硬件和软件的“握手环节”——在这里配置好系统时钟、初始化串口,后面才能打印调试信息。
注意:rt_system_scheduler_start() 之后,程序就不再返回了。如果在这之前有硬件没初始化好,后面想补救都来不及。我曾经在 rt_hw_board_init() 里漏配了 SysTick 中断优先级,结果调度器一启动就死机。
自动初始化机制:谁在帮你“自动”干活?
RT-Thread 有个很巧妙的设计——自动初始化。你想想看,如果每个驱动、每个组件都要手动调用初始化函数,那代码得多乱?
自动初始化靠的是 宏定义 + 链接段。RT-Thread 定义了几个初始化级别:
| 宏 | 调用顺序 | 说明 |
|---|---|---|
INIT_BOARD_EXPORT(fn) |
1 | 板级初始化,最早执行 |
INIT_PREV_EXPORT(fn) |
2 | 纯软件初始化,无硬件依赖 |
INIT_DEVICE_EXPORT(fn) |
3 | 设备驱动初始化 |
INIT_COMPONENT_EXPORT(fn) |
4 | 组件初始化,如 FinSH、DFS |
INIT_ENV_EXPORT(fn) |
5 | 环境初始化,如网络协议栈 |
INIT_APP_EXPORT(fn) |
6 | 应用层初始化,最晚执行 |
每个宏都会把函数指针放到特定的链接段里。启动时,RT-Thread 按段地址顺序遍历调用。说白了,就是编译器帮你排好队,系统按顺序点名。
小技巧:调试时,如果某个驱动没初始化,先检查它用的宏对不对。比如,你的驱动依赖 I2C 总线,那就不能用 INIT_BOARD_EXPORT,得用 INIT_DEVICE_EXPORT,否则 I2C 还没初始化,你的驱动就先跑了。
线程与调度器:RT-Thread 的“多任务”是怎么实现的
线程是什么?我习惯把它理解成“一个独立的执行流”。每个线程有自己的栈、优先级、入口函数。调度器就是那个“交通警察”,决定哪个线程该跑、哪个该等。
RT-Thread 的调度器是基于优先级的抢占式调度。什么意思?
- 优先级高的线程随时可以打断优先级低的线程
- 同优先级线程采用时间片轮转
- 空闲线程优先级最低,没事干时跑它
看一个最简单的线程创建例子:
static rt_thread_t tid1 = RT_NULL;
static void thread1_entry(void *parameter)
{
while (1)
{
rt_kprintf("thread1 is running\n");
rt_thread_mdelay(1000);
}
}
int thread_sample_init(void)
{
tid1 = rt_thread_create("thread1",
thread1_entry,
RT_NULL,
1024,
25,
10);
if (tid1 != RT_NULL)
rt_thread_startup(tid1);
return 0;
}
INIT_APP_EXPORT(thread_sample_init);
这里我用了 INIT_APP_EXPORT,确保线程在系统初始化完成后才创建。线程栈 1024 字节,优先级 25,时间片 10 个系统滴答。
经验之谈:线程栈大小怎么定?我一般先给 1024,跑起来后用 list_thread() 看栈使用率,再调整。给太大浪费 RAM,给太小会栈溢出——那种 bug 最难查,因为表现随机。
启动流程全景图
说了这么多,咱们用一张图把整个流程串起来:
这张图把整个启动流程分成了四个阶段。从汇编到 C,从单线程到多线程,每一步都有明确的分工。你移植驱动时,只要搞清楚你的初始化代码该放在哪个阶段,就不会出大问题。
避坑指南
最后,分享几个我踩过的坑:
- 中断向量表对齐:STM32 要求向量表 512 字节对齐,否则复位后直接跑飞。检查链接脚本里的
ALIGN(512)。 - 堆栈大小:汇编启动阶段用的栈是
__initial_sp,太小会导致 C 初始化时栈溢出。我一般给 2KB 以上。 - 自动初始化顺序:别在
INIT_BOARD_EXPORT里调用依赖操作系统的函数,那时调度器还没启动。 - 线程优先级:RT-Thread 支持 0-255 优先级,0 最高。别把用户线程设成 0,否则系统线程可能抢不过。
调试建议:在 rt_hw_board_init() 里加个 GPIO 翻转,用示波器看波形。如果波形出来了,说明汇编启动和板级初始化没问题;如果没出来,就从启动文件开始查。
嗯,启动流程就讲到这里。内容不少,但都是硬货。你把这章吃透了,后面移植驱动、调试系统就会顺手很多。