2. 开发环境搭建

各位同学,欢迎来到第二章。这一章我们聊聊 RT-Thread 的内核基础和启动流程。说实话,很多初学者一上来就急着写应用,结果系统跑飞了都不知道从哪查起。我个人习惯是,先把内核的“骨架”搞清楚,再去看代码怎么跑起来的。这样出了问题,你心里才有底。

RT-Thread 简介

RT-Thread 是一个开源的实时操作系统,主要面向物联网和嵌入式设备。它不像 Linux 那么重,也不像裸机那么“裸”。它提供了一套完整的线程管理、同步机制、内存管理,还有丰富的设备驱动框架。

我最早接触 RT-Thread 是在一个工业控制项目里。当时需要在 STM32F407 上同时处理传感器采集、通信协议栈和人机交互。裸机写中断太痛苦了,上 RTOS 是必然选择。选 RT-Thread 的原因很简单:它原生支持 STM32,而且社区活跃,遇到问题能找到人问。

RT-Thread 有几个关键特点:

  • 轻量级内核:最小配置下 ROM 不到 3KB,RAM 不到 1KB
  • 组件丰富:文件系统、网络协议栈、GUI 等可选组件
  • 标准 API:兼容 POSIX 接口,方便代码移植
  • 设备驱动框架:统一了外设驱动的编写规范

内核对象模型

RT-Thread 的内核设计围绕“对象”展开。说白了,内核里所有的资源——线程、信号量、互斥量、消息队列、事件、内存池——都是对象。它们都继承自一个基础结构体 rt_object。

为什么要这么设计?我举个例子。你在调试时想打印当前系统里所有线程的状态,如果每个资源都是独立的结构体,你得写不同的遍历函数。但有了对象模型,内核只需要维护一张对象容器表,遍历起来就统一了。

来看一下核心的数据结构:

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

每个对象都会被挂接到对应类型的链表上。内核通过 rt_object_get_information() 可以获取所有对象的信息。我在项目中就用这个接口写过一个小工具,把系统里所有线程和信号量的状态打印到串口,调试死锁问题特别管用。

对象类型用枚举定义,常见的有:

对象类型 说明 典型应用场景
RT_Object_Class_Thread 线程 任务调度、并发处理
RT_Object_Class_Semaphore 信号量 资源计数、同步
RT_Object_Class_Mutex 互斥量 保护共享资源,支持优先级继承
RT_Object_Class_MailBox 邮箱 线程间传递小数据
RT_Object_Class_MsgQueue 消息队列 线程间传递数据块
RT_Object_Class_MemPool 内存池 固定大小内存分配,无碎片
RT_Object_Class_Device 设备 外设驱动抽象
RT_Object_Class_Timer 定时器 软定时、超时处理
小技巧:在 rtconfig.h 中开启 RT_USING_OBJECT_INFO,就可以在 FinSH 控制台输入 list_thread、list_sem 等命令查看对象状态。调试利器,建议默认打开。

启动流程分析

RT-Thread 的启动流程,说白了就是一条从复位到应用入口的执行链。我把它拆成四个阶段:汇编启动、C 运行时初始化、板级初始化、应用入口。每个阶段都有明确的职责。

先看一张整体流程图:

RT-Thread 启动流程 阶段1:汇编启动 Reset_Handler 设置栈指针、中断向量 阶段2:C运行时初始化 entry / $Sub$$main BSS清零、数据段拷贝 阶段3:板级初始化 rt_hw_board_init 时钟、GPIO、串口 应用 自动初始化机制展开 INIT_BOARD_EXPORT 板级外设初始化 INIT_PREVICE_EXPORT 驱动框架初始化 INIT_DEVICE_EXPORT 设备注册初始化 INIT_APP_EXPORT 应用层初始化 关键说明 • 汇编阶段:由芯片复位向量直接跳转,与硬件强相关 • C运行时:完成全局变量初始化,是C环境的基础 • 板级初始化:配置系统时钟和必要外设,为内核运行做准备 • 自动初始化:按优先级顺序执行各模块的初始化函数 • 应用入口:进入用户编写的 main 函数,启动业务逻辑

1. 汇编启动阶段

芯片上电后,CPU 从复位向量表取出 Reset_Handler 的地址,然后跳转执行。这个阶段全部是汇编代码,在 startup_stm32xxxx.s 文件中。主要做三件事:

  • 设置主栈指针 SP
  • 初始化中断向量表
  • 调用 SystemInit 配置系统时钟

最后跳转到 entry 或 $Sub$$main,进入 C 环境。

注意:如果你用的是 IAR 或 MDK,入口函数名可能不一样。IAR 是 __iar_program_start,MDK 是 $Sub$$main。移植时一定要确认启动文件里的符号名。

2. C 运行时初始化

进入 C 代码后,第一件事是初始化运行时环境。这部分由编译器提供的启动代码完成:

  • BSS 段清零:把未初始化的全局变量全部置零
  • 数据段拷贝:把初始化的全局变量从 Flash 拷贝到 RAM
  • 堆栈初始化:设置堆的起始地址和大小

完成后,调用 rt_hw_board_init() 进入板级初始化。

3. 板级初始化

rt_hw_board_init() 在 board.c 中实现。它的职责是让硬件“活”起来:

  • 配置系统滴答定时器 SysTick,提供系统心跳
  • 初始化调试串口,方便打印信息
  • 初始化动态内存堆
  • 调用 rt_hw_interrupt_enable() 开启全局中断

我记得第一次移植 RT-Thread 到新板子时,卡在 SysTick 配置上。芯片的主频没设对,导致系统时钟快了 10 倍,线程调度完全乱套。后来老老实实对着数据手册重新算了一遍分频系数才搞定。

4. 应用入口

板级初始化完成后,内核调用 rt_application_init(),最终进入用户的 main() 函数。这时候调度器还没启动,你在 main 里可以创建线程、信号量等内核对象。最后调用 rt_thread_startup() 启动调度器,系统开始多任务运行。

自动初始化机制

RT-Thread 有一个很巧妙的设计——自动初始化。你想想看,一个复杂的系统可能有几十个模块,每个模块都需要初始化。如果全写在 main 函数里,那 main 函数会变得又臭又长。自动初始化机制解决了这个问题。

它的原理很简单:利用编译器的段属性,把初始化函数指针放到指定的段里。系统启动时,按优先级顺序遍历这些段,依次调用函数。

RT-Thread 定义了四个初始化级别:

宏定义 优先级 调用时机 典型用途
INIT_BOARD_EXPORT(fn) 1(最高) 板级初始化中 GPIO、时钟、外设底层初始化
INIT_PREVICE_EXPORT(fn) 2 板级初始化后 驱动框架、组件初始化
INIT_DEVICE_EXPORT(fn) 3 设备初始化阶段 具体设备注册、配置
INIT_APP_EXPORT(fn) 4(最低) 应用初始化阶段 应用层模块、业务逻辑

使用方式很简单,比如你想在系统启动时自动初始化一个传感器驱动:

#include <rtthread.h>

static int sensor_hw_init(void)
{
    // 初始化传感器硬件
    rt_pin_mode(10, PIN_MODE_OUTPUT);
    rt_pin_write(10, PIN_HIGH);
    rt_kprintf("sensor hardware initialized\n");
    return 0;
}
INIT_DEVICE_EXPORT(sensor_hw_init);

这样写,你不需要在 main 函数里显式调用 sensor_hw_init()。系统会在设备初始化阶段自动执行它。

核心思想:自动初始化本质上是“声明式编程”。你只需要告诉系统“我这个函数需要在什么时候执行”,剩下的由框架搞定。这大大降低了模块间的耦合度。

我曾经在一个项目中踩过坑:两个模块都用 INIT_DEVICE_EXPORT,但模块 A 依赖模块 B 先初始化。结果模块 A 先被执行了,导致初始化失败。后来我查了源码才发现,同一级别的初始化顺序取决于链接顺序,不能保证先后。解决方案是把依赖关系强的模块放到不同级别,或者显式在函数内部做依赖检查。

自动初始化的实现核心在 components.c 中:

static int rt_iops_auto_init(void)
{
    const init_fn_t *fn_ptr;

    for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
    {
        (*fn_ptr)();
    }
    // ... 其他级别类似
    return 0;
}

这里的 __rt_init_rti_board_start 和 __rt_init_rti_board_end 是链接器生成的符号,标记了对应段的起始和结束地址。系统通过指针遍历,逐个调用初始化函数。

嗯,这一章的内容就到这里。启动流程和自动初始化机制是理解 RT-Thread 运行逻辑的基础。搞清楚了这些,后面学设备驱动框架、线程间通信就会顺畅很多。