rtthread启动流程
RT-Thread
RT-Thread 启动流程
课堂笔记。对应源码:src/components.c(rtthread_startup)、src/timer.c(rt_system_timer_thread_init / _timer_thread_entry)、src/idle.c。
一、启动总览

上电复位之后,不是直接进你写的 main(),而是:
Reset_Handler(汇编)
→ SystemInit() // 时钟、FPU 等芯片相关
→ $Sub$$main / entry // 编译器把原 main 劫持到 RT-Thread 入口
→ rtthread_startup()
→ rt_hw_interrupt_disable() // 关中断,一直关到调度器 start
→ rt_hw_board_init() // 板级:时钟、堆、串口、SysTick(会启动 tick 硬件)
→ rt_show_version()
→ rt_system_timer_init() // 硬定时器链表
→ rt_system_scheduler_init() // 优先级队列、位图
→ rt_application_init() // 创建 main 线程
→ rt_system_timer_thread_init() // 软件定时器线程(开了才有)
→ rt_thread_idle_init() // 空闲线程
→ rt_system_scheduler_start() // 切第一个线程,这里才开中断;函数不再返回
调度器一旦启动,CPU 交给优先级最高的就绪线程。之后看到的 main、timer、tidle 都是普通线程,只是内核自己创建的。
为什么一进 rtthread_startup() 先关中断
不是「调用 rtthread_startup 之前」关的,而是这个函数第一句 rt_hw_interrupt_disable()。Cortex-M 复位后 PRIMASK=0,CPU 层面中断其实是开着的,只是 SysTick、UART 这些源大多还没使能,所以从 Reset_Handler 走到这里,表面上很安静。
真正危险的是下一句 rt_hw_board_init():板级初始化会配时钟、开堆、开串口,并且把 SysTick 跑起来(LOAD / TICKINT / ENABLE)。如果这时全局中断还开着,SysTick 马上就能打进 ISR。而此刻:
- 定时器链表还没
rt_system_timer_init() - 就绪队列 / 优先级位图还没
rt_system_scheduler_init() main/timer/idle线程都还不存在rt_current_thread还是空的
SysTick ISR 却已经会做 rt_tick++、rt_timer_check() 扫链表、必要时 rt_schedule()。链表是空的、当前线程是空的,结果就是野指针 / HardFault,看起来像随机复位。
所以关中断是把两件事错开:硬件可以开始报时,但 ISR 暂时进不来,直到内核能接住这记报时。
关中断
→ board_init:SysTick 硬件开始数,ISR 被 PRIMASK 挡住
→ 定时器链表、调度器、三个系统线程全部建好
→ rt_thread_startup() 往就绪队列挂线程(中间绝不能被 tick 切走半截)
→ rt_system_scheduler_start()
→ 选出优先级最高的就绪线程(一般是 main)
→ rt_hw_context_switch_to() 切过去
→ 这里才 cpsie i / 开 PRIMASK
此后 tick 打进来就是合法的:链表在、线程在、调度器在。
几条容易混的:
- 不是关到 board_init 结束就开。 一直关到调度器 start。中间创建线程、插入就绪队列,都属于「数据结构半成品」,不能被中断走进去。
- SysTick 使能 ≠ 中断已经在跑。 计数器硬件可以在关中断期间走;只是 TICKINT 被 PRIMASK 挡住,
rt_tick这段时间不加。少掉的那几毫秒启动阶段可以忽略。 scheduler_start不会返回,所以开头的 disable 不必在rtthread_startup里配对 enable。开中断是第一次线程切换的副作用。rt_hw_interrupt_disable()还会把内核的中断锁层级放到已知状态。 后面成对的 disable/enable 不会因为复位后 PRIMASK 不确定,中途把中断提前打开。
二、汇编窗口进内核
汇编窗口进入:

启动函数入口:

Keil 里从复位单步,典型路径:
Reset_Handler:设 MSP、调SystemInit、跳到 C 入口。- ARMCC 用
$Sub$$main包一层:先跑 RT-Thread 启动,再把用户main塞进 main 线程。 - GCC 工程常见入口是
entry(),内部同样调rtthread_startup()。
启动流程(rtthread_startup 调用链):

记住两件和“谁在跑”有关的事:
- SysTick 在
rt_hw_board_init()里配好。它只负责报时:rt_tick++,并检查定时器链表。它不是一个线程。 - 用户
main()此时还没跑。rt_application_init()只是把 main 线程init+startup进就绪队列;真正执行要等到rt_system_scheduler_start()。
三、调度起来之后的三个系统线程
| 线程名 | 怎么来的 | 干什么 |
|---|---|---|
main |
rt_application_init() |
跑用户 main(),应用入口 |
timer |
rt_system_timer_thread_init() |
软件定时器回调(开 RT_USING_TIMER_SOFT 才有) |
tidle / tidle0 |
rt_thread_idle_init() |
空闲钩子、对象清理、可选低功耗;优先级最低 |
1. main 函数线程

- 静态创建,栈大小
RT_MAIN_THREAD_STACK_SIZE,优先级RT_MAIN_THREAD_PRIORITY(一般偏低,把 CPU 让给业务线程)。
- 入口里调用户
main()。main里return之后这个线程就结束,系统继续跑其它线程 + idle。 - 所以:你在
main()里看到的第一行代码,已经是线程上下文,调度器、SysTick、idle 都已经在了。
2. 定时器线程(软件定时器)

timer 线程处理的是软件定时器,不是第二套时钟。时间基准仍然是 SysTick 累加的 rt_tick。软/硬的差别不在“用不用 SysTick”,而在到期后回调跑在哪。
3. 空闲线程

- 优先级 =
RT_THREAD_PRIORITY_MAX - 1(数字最大,最低)。 - 就绪队列空了才跑:执行
rt_thread_idle_sethook()挂的钩子、回收RT_THREAD_CLOSE的动态线程、有的 BSP 在这里进 WFI。 - 不要在 idle 钩子里做长时间阻塞;它一堵,系统就再也进不了“没事可做”的状态。
四、同一只挂钟,两种响法
SysTick ISR 每次只做“报时”:
rt_tick++(系统时间往前走一格)- 扫定时器链表:
- 硬定时器(
RT_TIMER_FLAG_HARD)到期 → 在 ISR 里直接调回调 - 软定时器(
RT_TIMER_FLAG_SOFT)到期 → 唤醒名为"timer"的线程,回调放到线程里再跑
- 硬定时器(
软定时器没有自己的晶振,也没有第二套 tick。rt_timer_start() 里写的 timeout,单位就是系统 tick;_timer_thread_entry 里的 rt_tick_get()、rt_thread_delay(next_timeout),数的也是同一根尺子。
硬定时器 RT_TIMER_FLAG_HARD |
软定时器 RT_TIMER_FLAG_SOFT |
|
|---|---|---|
| 时间从哪来 | 都是 rt_tick(通常由 SysTick 加) |
同上 |
| 谁发现到期 | SysTick ISR | ISR 只负责“到点了”,真正干活的是 timer 线程 |
| 回调跑在哪 | 中断上下文 | 线程上下文(_timer_thread_entry → rt_soft_timer_check()) |
| 回调能干什么 | 必须短、不能阻塞、不能随便调会挂起的 IPC | 可以稍重:发信号量、申请内存、调阻塞 API(仍不建议特别慢) |
为什么拆开:SysTick 常见 1ms 一次。ISR 里跑重活会拖长关调度/关中断的时间,别的线程和中断都跟着抖。软定时器把“闹钟到了”和“响了之后干什么”拆开——报时仍在中断,干活在 timer 线程。
没用软定时器、关了 RT_USING_TIMER_SOFT 时,这个线程不会建;硬定时器照样走 SysTick,回调全部挤在中断里。
五、rt_system_timer_thread_init() 源码
void rt_system_timer_thread_init(void)
{
#ifdef RT_USING_TIMER_SOFT
int i;
for (i = 0;
i < sizeof(_soft_timer_list) / sizeof(_soft_timer_list[0]);
i++)
{
rt_list_init(_soft_timer_list + i);
}
/* start software timer thread */
rt_thread_init(&_timer_thread,
"timer",
_timer_thread_entry,
RT_NULL,
&_timer_thread_stack[0],
sizeof(_timer_thread_stack),
RT_TIMER_THREAD_PRIO,
10);
/* startup */
rt_thread_startup(&_timer_thread);
#endif /* RT_USING_TIMER_SOFT */
}
只在开了 RT_USING_TIMER_SOFT 时执行,三步:
- 初始化
_soft_timer_list(按超时时间排序的软定时器链表,跳表/多级列表,加快“下一个到期”查询)。 - 静态创建名为
"timer"的线程:控制块_timer_thread、栈_timer_thread_stack都在 BSS 里,不用堆。入口_timer_thread_entry,优先级RT_TIMER_THREAD_PRIO(一般比普通应用线程高,保证回调及时),时间片 10 tick。 rt_thread_startup把它挂到就绪队列;调度器启动后就会跑。
这就是静态线程那一套:rt_thread_init + rt_thread_startup,和自己写 thread1 的方式相同,只是对象是内核全局变量。
六、_timer_thread_entry():闹钟服务员
static void _timer_thread_entry(void *parameter)
{
rt_tick_t next_timeout;
while (1)
{
/* get the next timeout tick */
if (_timer_list_next_timeout(_soft_timer_list, &next_timeout) != RT_EOK)
{
/* no software timer exist, suspend self. */
rt_thread_suspend(rt_thread_self());
rt_schedule();
}
else
{
rt_tick_t current_tick;
current_tick = rt_tick_get();
if ((next_timeout - current_tick) < RT_TICK_MAX / 2)
{
next_timeout = next_timeout - current_tick;
rt_thread_delay(next_timeout);
}
}
/* check software timer */
rt_soft_timer_check();
}
}
永远循环,逻辑可以压成三句话:
查下一个软定时器何时到期
没有定时器 → 自己挂起,让出 CPU
有、且还没到点 → rt_thread_delay(剩余 tick)
到点 / delay 被提前唤醒 → rt_soft_timer_check() 扫链表、调回调
细节
-
_timer_list_next_timeout()失败
当前一个软定时器都没有。线程rt_thread_suspend自己,再rt_schedule()切走。之后有人rt_timer_start()一个软定时器,内核会把这个线程唤醒,循环从头再查。 -
(next_timeout - current_tick) < RT_TICK_MAX / 2
rt_tick_t无符号,直接减会在溢出时绕回。这个不等式是内核判断“还没到期”的惯用写法(距离小于半圈就认为是未来)。还没到就rt_thread_delay睡到那个 tick。 -
rt_thread_delay(next_timeout)不是另起一套定时
只是让timer线程睡 N 个 tick;这 N 个 tick 还是 SysTick 在减。中间如果有人新rt_timer_start了一个更早到期的软定时器,内核会提前把这个线程叫醒,再重新算下一个超时。 -
rt_soft_timer_check()
在线程上下文取出所有已到期的软定时器,执行回调。这里可以调 IPC、申请内存——这是和硬定时器最大的区别。周期性软定时器会在这里被重新挂回链表。
七、和启动顺序对上号(容易混的点)
-
rt_system_timer_init()≠rt_system_timer_thread_init()- 前者:硬/软定时器链表都建好,给 SysTick ISR 用。
- 后者:只建
"timer"线程,专门跑软定时器回调。
-
SysTick 中断比
timer线程更早就在工作。
板级初始化里已经rt_hw_systick了;只是调度器没 start 之前,即便软定时器到期也还没有线程可唤醒。所以内核把 timer 线程的创建放在rt_application_init()之后、scheduler_start之前。 -
调试时
list_thread看到timer长期suspend是正常的:没有软定时器时它就睡。一rt_timer_start(SOFT)它会被叫醒。 -
自己写超时优先用软定时器;只有“必须在 1ms 中断里立刻拨脚/置标志”才用硬定时器,回调里不要
printf、不要调会阻塞的 API。
八、INIT_BOARD_EXPORT 与 INIT_APP_EXPORT
这两个宏不是“函数调用”,而是把初始化函数指针塞进指定的链接段,启动时内核按段顺序扫一遍、挨个调用。差别只在哪一段、什么时候扫。
INIT_BOARD_EXPORT(fn) |
INIT_APP_EXPORT(fn) |
|
|---|---|---|
| 展开成 | INIT_EXPORT(fn, "1") |
INIT_EXPORT(fn, "6") |
| 函数指针放哪 | .rti_fn.1 |
.rti_fn.6 |
| 谁来扫 | rt_hw_board_init() → rt_components_board_init() |
main 线程入口 → rt_components_init() |
| 调度器 | 还没 start,关着中断 | 已经在跑,你在 main 线程里 |
| 能干什么 | 挂设备、配引脚、注册串口 | 建线程、用 IPC、rt_thread_delay |
| 不能干什么 | 创建线程、延时、等信号量(会踩空) | 依赖“板级还没就绪”的硬件(应放到 BOARD) |
fn 原型必须是:int fn(void);,返回 0 表示成功。自动 init 要开 RT_USING_COMPONENTS_INIT(Studio 默认开)。Nano 内核常常没有这套,就得自己在 main 里调。
1. 宏怎么展开
rtdef.h 里(开了 RT_USING_COMPONENTS_INIT):
typedef int (*init_fn_t)(void);
#define INIT_EXPORT(fn, level) \
rt_used const init_fn_t __rt_init_##fn rt_section(".rti_fn." level) = fn
#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1")
#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2")
#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3")
#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4")
#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5")
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
rt_section / rt_used 就是编译器属性:
#define rt_section(x) __attribute__((section(x)))
#define rt_used __attribute__((used)) /* 防止没被引用被链接器丢掉 */
所以你写:
int uart_hw_init(void) { ...; return 0; }
INIT_BOARD_EXPORT(uart_hw_init);
预处理器变成(大意):
__attribute__((used, section(".rti_fn.1")))
const init_fn_t __rt_init_uart_hw_init = uart_hw_init;
编译器在 .rti_fn.1 里放了一个函数指针常量,不是调用 uart_hw_init。谁都不用写“把这个函数登记到启动表”——写宏就登记了。开了 RT_DEBUG_INIT 时,段里放的是 { "函数名", 函数指针 },启动日志能打印谁在 init。原理一样。
2. 链接器把各文件的段拼成一张有序表
各 .c 里的 .rti_fn.1、.rti_fn.6 本来是散的。链接脚本(GCC 常见写法):
.rti_fn : {
PROVIDE(__rt_init_start = .);
KEEP(*(SORT(.rti_fn*)))
PROVIDE(__rt_init_end = .);
} > CODE
SORT(.rti_fn*) 按段名字符串排序,内存里变成:
.rti_fn.0 rti_start ← 内核自己放的哨兵
.rti_fn.0.end rti_board_start
.rti_fn.1 ← 所有 INIT_BOARD_EXPORT
.rti_fn.1.end rti_board_end
.rti_fn.2 INIT_PREV_EXPORT
.rti_fn.3 INIT_DEVICE_EXPORT
.rti_fn.4 INIT_COMPONENT_EXPORT
.rti_fn.5 INIT_ENV_EXPORT
.rti_fn.6 ← 所有 INIT_APP_EXPORT
.rti_fn.6.end rti_end
这就是一张按 level 排好的函数指针数组。同 level 内顺序一般按目标文件/符号名,不要依赖同级谁先谁后。
内核在 components.c 里自己塞了几个空函数当边界:
static int rti_start(void) { return 0; }
INIT_EXPORT(rti_start, "0");
static int rti_board_start(void) { return 0; }
INIT_EXPORT(rti_board_start, "0.end");
static int rti_board_end(void) { return 0; }
INIT_EXPORT(rti_board_end, "1.end");
static int rti_end(void) { return 0; }
INIT_EXPORT(rti_end, "6.end");
3. 启动时扫两段,不是扫一整张表
/* 只扫 BOARD:.rti_fn.0.end 到 .rti_fn.1.end(不含 1.end 自己) */
void rt_components_board_init(void)
{
const init_fn_t *fn;
for (fn = &__rt_init_rti_board_start + 1;
fn < &__rt_init_rti_board_end;
fn++)
{
(*fn)(); /* 调你的 INIT_BOARD_EXPORT 函数 */
}
}
/* 扫 PREV ~ APP:.rti_fn.1.end 之后到 .rti_fn.6.end */
void rt_components_init(void)
{
const init_fn_t *fn;
for (fn = &__rt_init_rti_board_end + 1;
fn < &__rt_init_rti_end;
fn++)
{
(*fn)(); /* PREV / DEVICE / COMPONENT / ENV / APP */
}
}
(具体是 + 1 还是开区间,以你这版 components.c 为准,意思是:两个哨兵之间的指针逐个解引用。)
挂到启动链上:
rtthread_startup()
关中断
rt_hw_board_init()
时钟 / 堆 / 串口控制台 / SysTick
rt_components_board_init() ← INIT_BOARD_EXPORT 在这里跑
定时器链表、调度器、main/timer/idle 线程
rt_system_scheduler_start() ← 开中断,切到 main 线程
main_thread_entry()
rt_components_init() ← INIT_APP_EXPORT 在这里跑
main() ← 你写的 main
所以:
- BOARD:还在
rtthread_startup()里,中断关着、没有当前线程。 - APP:已经是
main线程,可以当普通线程代码写。
和前面“先关中断再 board_init”是同一件事:INIT_BOARD_EXPORT 跑的时候 rt_current_thread 还是空的,一 rt_thread_delay / rt_sem_take 就会 rt_schedule(),当前线程空 → HardFault。INIT_APP_EXPORT 跑的时候调度器、tick、堆都在了。
硬件必须先于驱动、驱动必须先于应用,靠的就是 "1" < "6" 的段名排序,不是谁 #include 谁。
4. 例子
例 1:板级 —— 注册串口设备(BOARD)
调度器还没起来,只能碰寄存器、往设备框架里挂对象:
#include <rtthread.h>
#include <rtdevice.h>
static struct rt_serial_device s_uart1;
int rt_hw_uart_init(void)
{
uart1_gpio_clock_init();
s_uart1.ops = &stm32_uart_ops;
s_uart1.config.baud_rate = 115200;
rt_hw_serial_register(&s_uart1, "uart1",
RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,
RT_NULL);
return 0;
}
INIT_BOARD_EXPORT(rt_hw_uart_init);
控制台 rt_console_set_device("uart1") 也常在 BOARD 或 rt_hw_board_init() 里,这样后面 rt_kprintf 才有设备。
不要在 BOARD 里:
rt_thread_mdelay(10); /* 没有线程,挂 */
rt_thread_create("x", ...); /* 调度器没 start,即使 create 了也不会跑 */
例 2:应用 —— 自动拉起业务线程(APP)
#include <rtthread.h>
static void led_entry(void *param)
{
rt_pin_mode(LED0_PIN, PIN_MODE_OUTPUT);
while (1)
{
rt_pin_write(LED0_PIN, PIN_HIGH);
rt_thread_mdelay(200);
rt_pin_write(LED0_PIN, PIN_LOW);
rt_thread_mdelay(200);
}
}
int led_app_init(void)
{
rt_thread_t tid;
tid = rt_thread_create("led", led_entry, RT_NULL, 512, 20, 10);
if (tid == RT_NULL)
return -RT_ERROR;
rt_thread_startup(tid);
return 0;
}
INIT_APP_EXPORT(led_app_init);
效果:你在 main() 里什么都不写,调度起来后 rt_components_init() 会调到 led_app_init(),LED 线程自己起来。这就是组件“声明一下就自动启动”。
例 3:同一模块拆两级
传感器:BOARD 里只保证 I2C 控制器在;APP 里再创建采样线程。
/* drv_i2c.c —— 板级:挂 i2c1 设备 */
int rt_hw_i2c_init(void)
{
stm32_i2c_register("i2c1");
return 0;
}
INIT_BOARD_EXPORT(rt_hw_i2c_init);
/* app_sht30.c —— 应用:打开设备、建线程 */
int sht30_app_init(void)
{
rt_device_t i2c = rt_device_find("i2c1");
if (i2c == RT_NULL)
return -RT_ENOSYS;
rt_thread_t tid = rt_thread_create("sht30", sht30_entry, i2c, 1024, 15, 10);
if (tid)
rt_thread_startup(tid);
return 0;
}
INIT_APP_EXPORT(sht30_app_init);
BOARD 保证 rt_device_find("i2c1") 在 APP 时已经有了。如果把 rt_hw_i2c_init 也写成 INIT_APP_EXPORT,和 sht30_app_init 同级,谁先跑不一定,find 可能失败。
例 4:中间那几档(知道即可)
| 宏 | level | 典型用途 |
|---|---|---|
INIT_BOARD_EXPORT |
"1" |
时钟、GPIO、UART/I2C 控制器注册 |
INIT_PREV_EXPORT |
"2" |
纯软件、不依赖设备(有的版本做对象系统补充) |
INIT_DEVICE_EXPORT |
"3" |
依赖总线的从设备:传感器、Flash |
INIT_COMPONENT_EXPORT |
"4" |
DFS、LWIP、USB 协议栈 |
INIT_ENV_EXPORT |
"5" |
文件系统挂载、网络启动 |
INIT_APP_EXPORT |
"6" |
你的业务线程、GUI、协议栈应用 |
5. 和 main() 的关系
main 线程入口
→ rt_components_init() // 所有 APP(以及 2~5)先跑完
→ main() // 然后才是你的 main
所以 INIT_APP_EXPORT(fn) 比 main() 更早。main 里 rt_device_find("uart1") 看得到 BOARD 注册的设备;APP 里创建的线程也可能已经在跑。
不要在 BOARD 函数里假设 main 已经执行;也不要在 main 里再注册一遍 BOARD 已经挂过的设备(除非你没开自动 init)。
6. 容易混的点
- 宏不“调用”函数,只“投放”指针。 没被扫到(没开
RT_USING_COMPONENTS_INIT、链接脚本没KEEP(*(SORT(.rti_fn*)))),函数永远不会跑,还可能被 GC 掉;所以要rt_used。 - 同 level 不要排依赖。 I2C 和传感器都
INIT_APP_EXPORT就会偶发find失败。设备放 BOARD/DEVICE,应用放 APP。 - BOARD 里能用堆吗? 取决于
rt_system_heap_init()是否已经在rt_hw_board_init()里、并且在rt_components_board_init()之前。官方 BSP 一般是先堆再扫 BOARD。不要在 BOARD 里rt_thread_create(动态线程还要调度器)。 - 和
MSH_CMD_EXPORT是同一套路:命令表也是section+KEEP+ 启动/shell 扫描。自动 init 扫的是.rti_fn.*,shell 扫的是另一段。 - 函数必须全局、签名对。
static int foo(void)可以(符号变成__rt_init_foo在段里);写成void foo(void)或带参数,类型和init_fn_t对不上。
对照前面那句:关中断是为了让 SysTick 先别进 ISR;INIT_BOARD_EXPORT 正好跑在这扇门里面——硬件可以配,线程 API 还不能用。INIT_APP_EXPORT 跑在门打开之后,就是普通线程里的自动 main 前缀。

浙公网安备 33010602011771号