第2章 内核对象与启动流程

各位同学,今天我们来聊聊RT-Thread的“骨架”和“第一口气”。说白了,就是两件事:内核里那些对象是怎么组织的,以及系统上电后是怎么一步步跑起来的。搞懂这个,后面调试起来心里才有底。

2.1 内核对象模型

RT-Thread的内核,本质上是一个面向对象的系统。它把线程、信号量、消息队列、事件、互斥量、定时器、内存池等等,都抽象成了“内核对象”。

为什么要这么干?我刚开始接触的时候也觉得多此一举。直到有一次,我需要遍历系统中所有线程来排查一个死锁问题——嗯,有了统一的对象模型,这件事就变得非常优雅。

2.1.1 对象分类

内核对象分为两大类:

类别 包含对象 特点
静态对象 静态线程、静态信号量、静态事件等 编译时分配内存,速度快,适合资源受限场景
动态对象 动态线程、动态信号量、动态消息队列等 运行时从堆分配,灵活,但需注意内存碎片
我的习惯:在STM32这类资源不算太宽裕的MCU上,我倾向于把关键任务(比如控制环路)做成静态对象。动态对象留给那些“可能创建也可能不创建”的模块,比如调试用的日志线程。

2.1.2 对象控制块

每个内核对象都有一个控制块,结构大致如下:

struct rt_object
{
    char       name[RT_NAME_MAX];   // 对象名称
    rt_uint8_t type;                // 对象类型
    rt_uint8_t flag;                // 对象标志
    rt_list_t  list;                // 对象链表节点
};

你看,所有对象都挂在一个全局链表上。这意味着什么?意味着你可以通过遍历这个链表,找到任何一个对象。我在项目中就靠这个特性,写了一个“对象查看器”调试命令,直接打印出所有线程的状态和栈使用率。

2.1.3 对象容器

RT-Thread用“对象容器”来管理同一类对象。每个容器对应一种对象类型,比如线程容器、信号量容器。

struct rt_object_information
{
    enum rt_object_class_type type;  // 对象类型
    rt_list_t                 object_list; // 对象链表头
    rt_size_t                 object_size; // 对象大小
};
关键点:对象容器是系统启动时初始化的。如果你在初始化之前就创建对象,系统会崩溃——我踩过这个坑,后面会细说。

2.2 系统启动流程

系统上电后,到底发生了什么?很多同学直接写应用代码,从来没关心过启动过程。但一旦出了问题,比如系统卡在某个地方起不来,你就得顺着这条线去查。

启动流程分三步走:Reset_Handler → entry → rtthread_startup。

2.2.1 Reset_Handler

这是芯片复位后的第一个入口。它做的事情很简单:

  1. 设置栈指针(SP)
  2. 调用SystemInit()初始化时钟和硬件
  3. 跳转到entry

这部分代码通常在startup_stm32xxxx.s汇编文件里。我建议你至少看一遍,不用背,但要知道它干了什么。

2.2.2 entry

entry是C语言环境的入口。它主要做两件事:

  • 初始化ZI段(清零)
  • 拷贝RW段到RAM

做完这些,C语言就能正常跑了。然后它调用rtthread_startup()。

2.2.3 rtthread_startup

这是RT-Thread内核初始化的核心函数。我把它拆成几个关键步骤:

步骤 函数/操作 说明
1 rt_hw_interrupt_disable() 关闭全局中断,防止初始化被打断
2 rt_hw_board_init() 初始化板级硬件(时钟、GPIO、串口等)
3 rt_system_scheduler_init() 初始化调度器
4 rt_application_init() 创建主线程(main线程)
5 rt_system_scheduler_start() 启动调度器,开始多任务
我曾经踩过的坑:在rt_hw_board_init()里,我调用了rt_kprintf()来打印调试信息。但当时串口还没初始化完,结果系统直接卡死。后来才明白,rt_kprintf依赖串口驱动,而串口驱动是在rt_hw_board_init()后半段才初始化的。顺序很重要!

2.3 自动初始化机制

RT-Thread有一个非常实用的特性——自动初始化。你不需要手动调用每个模块的初始化函数,系统会自动帮你按顺序执行。

2.3.1 初始化宏

RT-Thread定义了几个宏,用来标记初始化函数:

INIT_BOARD_EXPORT(fn)      // 板级初始化,最早
INIT_PREV_EXPORT(fn)       // 纯软件初始化
INIT_DEVICE_EXPORT(fn)     // 设备驱动初始化
INIT_COMPONENT_EXPORT(fn)  // 组件初始化
INIT_ENV_EXPORT(fn)        // 环境初始化
INIT_APP_EXPORT(fn)        // 应用初始化,最晚

这些宏会把函数地址放到特定的段里。系统启动时,会按段顺序依次调用。

2.3.2 执行顺序

我画了一张图,帮你理解这个顺序:

RT-Thread 自动初始化执行顺序 INIT_BOARD_EXPORT INIT_PREV_EXPORT INIT_DEVICE_EXPORT INIT_COMPONENT_EXPORT INIT_ENV_EXPORT INIT_APP_EXPORT 说明 越靠上,初始化越早 板级初始化最先执行 应用初始化最后执行 同级别按链接顺序执行 依赖关系必须保证 比如: 设备驱动依赖板级初始化 组件依赖设备驱动 应用依赖所有底层

2.3.3 使用示例

假设你写了一个LED驱动,想让它自动初始化:

#include <rtthread.h>

static int led_hw_init(void)
{
    // 初始化LED的GPIO
    rt_pin_mode(LED_PIN, PIN_MODE_OUTPUT);
    rt_kprintf("LED hardware initialized\n");
    return 0;
}
INIT_BOARD_EXPORT(led_hw_init);

static int led_app_init(void)
{
    // 创建LED控制线程
    rt_thread_t tid = rt_thread_create("led",
                                       led_entry,
                                       RT_NULL,
                                       512,
                                       10,
                                       20);
    if (tid != RT_NULL)
        rt_thread_startup(tid);
    return 0;
}
INIT_APP_EXPORT(led_app_init);
避坑指南:我曾经把LED的GPIO初始化放在了INIT_APP_EXPORT里,结果系统启动后LED不亮。查了半天才发现,GPIO时钟在INIT_BOARD_EXPORT阶段才使能。记住:硬件相关的初始化,一定要用INIT_BOARD_EXPORT。

2.4 本章小结

这一章我们讲了三个核心内容:

  • 内核对象模型——系统里所有资源都是对象,通过统一的方式管理
  • 系统启动流程——从Reset_Handler到rtthread_startup,每一步都有明确分工
  • 自动初始化机制——用宏标记初始化函数,系统按顺序自动执行

理解这些,你就掌握了RT-Thread的“骨架”。后面讲线程、信号量、消息队列的时候,你会发现它们都是内核对象的一种。启动流程和自动初始化,则是你排查“系统起不来”这类问题的第一把钥匙。