rtthread启动流程

RT-Thread

RT-Thread 启动流程

课堂笔记。对应源码:src/components.crtthread_startup)、src/timer.crt_system_timer_thread_init / _timer_thread_entry)、src/idle.c


一、启动总览

上电复位之后,不是直接进你写的 main(),而是:

c 复制代码
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 交给优先级最高的就绪线程。之后看到的 maintimertidle 都是普通线程,只是内核自己创建的。

为什么一进 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 打进来就是合法的:链表在、线程在、调度器在。

几条容易混的:

  1. 不是关到 board_init 结束就开。 一直关到调度器 start。中间创建线程、插入就绪队列,都属于「数据结构半成品」,不能被中断走进去。
  2. SysTick 使能 ≠ 中断已经在跑。 计数器硬件可以在关中断期间走;只是 TICKINT 被 PRIMASK 挡住,rt_tick 这段时间不加。少掉的那几毫秒启动阶段可以忽略。
  3. scheduler_start 不会返回 ,所以开头的 disable 不必在 rtthread_startup 里配对 enable。开中断是第一次线程切换的副作用。
  4. rt_hw_interrupt_disable() 还会把内核的中断锁层级放到已知状态。 后面成对的 disable/enable 不会因为复位后 PRIMASK 不确定,中途把中断提前打开。

二、汇编窗口进内核

汇编窗口进入:

启动函数入口:

Keil 里从复位单步,典型路径:

  1. Reset_Handler:设 MSP、调 SystemInit、跳到 C 入口。
  2. ARMCC 用 $Sub$$main 包一层:先跑 RT-Thread 启动,再把用户 main 塞进 main 线程。
  3. 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()mainreturn 之后这个线程就结束,系统继续跑其它线程 + 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 每次只做"报时":

  1. rt_tick++(系统时间往前走一格)
  2. 扫定时器链表:
    • 硬定时器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_entryrt_soft_timer_check()
回调能干什么 必须短、不能阻塞、不能随便调会挂起的 IPC 可以稍重:发信号量、申请内存、调阻塞 API(仍不建议特别慢)

为什么拆开:SysTick 常见 1ms 一次。ISR 里跑重活会拖长关调度/关中断的时间,别的线程和中断都跟着抖。软定时器把"闹钟到了"和"响了之后干什么"拆开------报时仍在中断,干活在 timer 线程

没用软定时器、关了 RT_USING_TIMER_SOFT 时,这个线程不会建;硬定时器照样走 SysTick,回调全部挤在中断里。


五、rt_system_timer_thread_init() 源码

c 复制代码
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 时执行,三步:

  1. 初始化 _soft_timer_list(按超时时间排序的软定时器链表,跳表/多级列表,加快"下一个到期"查询)。
  2. 静态 创建名为 "timer" 的线程:控制块 _timer_thread、栈 _timer_thread_stack 都在 BSS 里,不用堆。入口 _timer_thread_entry,优先级 RT_TIMER_THREAD_PRIO(一般比普通应用线程高,保证回调及时),时间片 10 tick。
  3. rt_thread_startup 把它挂到就绪队列;调度器启动后就会跑。

这就是静态线程那一套:rt_thread_init + rt_thread_startup,和自己写 thread1 的方式相同,只是对象是内核全局变量。


六、_timer_thread_entry():闹钟服务员

c 复制代码
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() 扫链表、调回调

细节

  1. _timer_list_next_timeout() 失败

    当前一个软定时器都没有。线程 rt_thread_suspend 自己,再 rt_schedule() 切走。之后有人 rt_timer_start() 一个软定时器,内核会把这个线程唤醒,循环从头再查。

  2. (next_timeout - current_tick) < RT_TICK_MAX / 2

    rt_tick_t 无符号,直接减会在溢出时绕回。这个不等式是内核判断"还没到期"的惯用写法(距离小于半圈就认为是未来)。还没到就 rt_thread_delay 睡到那个 tick。

  3. rt_thread_delay(next_timeout) 不是另起一套定时

    只是让 timer 线程睡 N 个 tick;这 N 个 tick 还是 SysTick 在减。中间如果有人新 rt_timer_start 了一个更早到期的软定时器,内核会提前把这个线程叫醒,再重新算下一个超时。

  4. rt_soft_timer_check()

    线程上下文取出所有已到期的软定时器,执行回调。这里可以调 IPC、申请内存------这是和硬定时器最大的区别。周期性软定时器会在这里被重新挂回链表。


七、和启动顺序对上号(容易混的点)

  1. rt_system_timer_init()rt_system_timer_thread_init()

    • 前者:硬/软定时器链表都建好,给 SysTick ISR 用。
    • 后者:只建 "timer" 线程,专门跑软定时器回调。
  2. SysTick 中断比 timer 线程更早就在工作。

    板级初始化里已经 rt_hw_systick 了;只是调度器没 start 之前,即便软定时器到期也还没有线程可唤醒。所以内核把 timer 线程的创建放在 rt_application_init() 之后、scheduler_start 之前。

  3. 调试时 list_thread 看到 timer 长期 suspend 是正常的:没有软定时器时它就睡。一 rt_timer_start(SOFT) 它会被叫醒。

  4. 自己写超时优先用软定时器;只有"必须在 1ms 中断里立刻拨脚/置标志"才用硬定时器,回调里不要 printf、不要调会阻塞的 API。


八、INIT_BOARD_EXPORTINIT_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):

c 复制代码
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 就是编译器属性:

c 复制代码
#define rt_section(x)   __attribute__((section(x)))
#define rt_used         __attribute__((used))   /* 防止没被引用被链接器丢掉 */

所以你写:

c 复制代码
int uart_hw_init(void) { ...; return 0; }
INIT_BOARD_EXPORT(uart_hw_init);

预处理器变成(大意):

c 复制代码
__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 常见写法):

ld 复制代码
.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 里自己塞了几个空函数当边界:

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. 启动时扫两段,不是扫一整张表

c 复制代码
/* 只扫 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)

调度器还没起来,只能碰寄存器、往设备框架里挂对象:

c 复制代码
#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 里:

c 复制代码
rt_thread_mdelay(10);          /* 没有线程,挂 */
rt_thread_create("x", ...);    /* 调度器没 start,即使 create 了也不会跑 */

例 2:应用 ------ 自动拉起业务线程(APP)

c 复制代码
#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 里再创建采样线程。

c 复制代码
/* 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() 更早mainrt_device_find("uart1") 看得到 BOARD 注册的设备;APP 里创建的线程也可能已经在跑。

不要在 BOARD 函数里假设 main 已经执行;也不要在 main 里再注册一遍 BOARD 已经挂过的设备(除非你没开自动 init)。

6. 容易混的点

  1. 宏不"调用"函数,只"投放"指针。 没被扫到(没开 RT_USING_COMPONENTS_INIT、链接脚本没 KEEP(*(SORT(.rti_fn*)))),函数永远不会跑,还可能被 GC 掉;所以要 rt_used
  2. 同 level 不要排依赖。 I2C 和传感器都 INIT_APP_EXPORT 就会偶发 find 失败。设备放 BOARD/DEVICE,应用放 APP。
  3. BOARD 里能用堆吗? 取决于 rt_system_heap_init() 是否已经在 rt_hw_board_init() 里、并且在 rt_components_board_init() 之前 。官方 BSP 一般是先堆再扫 BOARD。不要在 BOARD 里 rt_thread_create(动态线程还要调度器)。
  4. MSH_CMD_EXPORT 是同一套路 :命令表也是 section + KEEP + 启动/shell 扫描。自动 init 扫的是 .rti_fn.*,shell 扫的是另一段。
  5. 函数必须全局、签名对。 static int foo(void) 可以(符号变成 __rt_init_foo 在段里);写成 void foo(void) 或带参数,类型和 init_fn_t 对不上。

对照前面那句:关中断是为了让 SysTick 先别进 ISR;INIT_BOARD_EXPORT 正好跑在这扇门里面------硬件可以配,线程 API 还不能用。INIT_APP_EXPORT 跑在门打开之后,就是普通线程里的自动 main 前缀。