2. RT-Thread概述

大家好,我是你们的嵌入式讲师。今天咱们聊聊 RT-Thread 的启动流程。说实话,很多初学者拿到开发板,点个灯、跑个线程就觉得完事了。但真要自己移植、调驱动,不懂启动流程,那可就抓瞎了。

我刚开始接触 RT-Thread 时,也踩过不少坑。有一次,板子死活跑不起来,查了半天,结果是汇编启动段里中断向量表没对齐。嗯,从那以后,我每次移植都会先过一遍启动流程。

从复位到 main:汇编启动阶段

芯片上电后,第一件事是什么?

不是 C 语言,不是 RT-Thread,而是汇编。STM32 的启动文件(startup_stm32xxxx.s)会先接管 CPU。它干这几件事:

  1. 设置堆栈指针——从向量表第一个字加载 SP
  2. 跳转复位向量——执行 Reset_Handler
  3. 初始化 .bss 段——清零全局未初始化变量
  4. 拷贝 .data 段——从 Flash 搬到 RAM
  5. 调用 SystemInit——配置时钟、PLL、Flash 等待周期
  6. 跳转 __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 最难查,因为表现随机。

启动流程全景图

说了这么多,咱们用一张图把整个流程串起来:

RT-Thread 启动流程全景图 阶段一:汇编启动(startup_stm32xxxx.s) 设置 SP → 跳转 Reset_Handler → 初始化 .bss/.data → SystemInit → 跳转 entry() 阶段二:C 语言初始化(rtthread_startup) 关中断 → 板级初始化 → 版本打印 → 定时器初始化 调度器初始化 → 应用初始化 → 空闲线程 → 启动调度器 阶段三:自动初始化(按段顺序调用) BOARD → PREV → DEVICE → COMPONENT → ENV → APP 阶段四:调度器运行(不再返回) 线程 1 ↔ 线程 2 ↔ ... ↔ 空闲线程

这张图把整个启动流程分成了四个阶段。从汇编到 C,从单线程到多线程,每一步都有明确的分工。你移植驱动时,只要搞清楚你的初始化代码该放在哪个阶段,就不会出大问题。

避坑指南

最后,分享几个我踩过的坑:

  • 中断向量表对齐:STM32 要求向量表 512 字节对齐,否则复位后直接跑飞。检查链接脚本里的 ALIGN(512)。
  • 堆栈大小:汇编启动阶段用的栈是 __initial_sp,太小会导致 C 初始化时栈溢出。我一般给 2KB 以上。
  • 自动初始化顺序:别在 INIT_BOARD_EXPORT 里调用依赖操作系统的函数,那时调度器还没启动。
  • 线程优先级:RT-Thread 支持 0-255 优先级,0 最高。别把用户线程设成 0,否则系统线程可能抢不过。

调试建议:在 rt_hw_board_init() 里加个 GPIO 翻转,用示波器看波形。如果波形出来了,说明汇编启动和板级初始化没问题;如果没出来,就从启动文件开始查。

嗯,启动流程就讲到这里。内容不少,但都是硬货。你把这章吃透了,后面移植驱动、调试系统就会顺手很多。