RT-Thread 内核基础与启动流程

各位同学,咱们今天聊点实在的。RT-Thread 这个系统,说白了就是一个能帮你管理硬件资源、调度任务的“管家”。你写应用,它管底层。这一章,我带你把它的骨架摸清楚。

RT-Thread 架构概览

先看整体。RT-Thread 分三层:内核、组件、软件包。我习惯把这比作一个工具箱——内核是扳手螺丝刀,组件是电动工具,软件包就是各种专用钻头。

层级作用我常说的
内核线程调度、内存管理、IPC 通信系统的“心脏”
组件文件系统、网络协议栈、Shell官方配好的“外设”
软件包传感器驱动、AI 算法、云连接社区贡献的“插件”

内核是最小集,裁剪后可以跑在几 KB 的 RAM 里。组件和软件包按需加载。我在项目里经常只开内核 + FinSH 控制台,其他全关——资源紧张时,少就是多。

核心原则:内核只做一件事——让线程“看起来”在同时运行。其他都是附加服务。

启动流程分析

系统上电后,到底发生了什么?我拆成三段讲:汇编启动、C 语言初始化、自动初始化机制。

1. 汇编启动 —— 从复位向量到 C 环境

芯片复位后,PC 指针直接跳到 Reset_Handler。这段汇编代码干三件事:

  • 设置栈指针 SP(指向 __stack)
  • 清零 BSS 段(__bss_start 到 __bss_end)
  • 调用 SystemInit 配置时钟

然后跳进 entry 函数。嗯,这里有个坑——中断向量表必须 4 字节对齐。我曾经在 STM32F103 上因为链接脚本没配好,导致中断全跑飞,查了两天才发现是向量表偏移地址写错了。

; 简化版 Reset_Handler 伪代码
Reset_Handler:
    LDR   SP, =__stack        ; 设栈
    BL    SystemInit          ; 配时钟
    BL    entry               ; 进 C 世界

2. C 语言初始化 —— 从 entry 到 rtthread_startup

entry 函数里调 rtthread_startup。这个函数是 C 初始化的总入口,顺序固定:

  1. 硬件初始化:rt_hw_board_init() —— 配中断、定时器、串口
  2. 内核对象初始化:rt_system_scheduler_init()、rt_system_timer_init()
  3. 自动初始化:调用所有 INIT_XXX_EXPORT 宏注册的函数
  4. 创建主线程:启动 main 线程
  5. 启动调度器:rt_system_scheduler_start() —— 从此不再返回

个人习惯:我总在 rt_hw_board_init() 里加个 GPIO 翻转,用来示波器看启动耗时。这招排查启动卡死特别管用。

3. 自动初始化机制 —— 宏背后的魔法

RT-Thread 用宏把初始化函数分段注册。你写个 INIT_APP_EXPORT(my_init),它就在编译时把函数指针塞进特定段。启动时按优先级依次调用。

宏名称调用顺序典型用途
INIT_BOARD_EXPORT1板级硬件初始化
INIT_PREV_EXPORT2纯软件服务(如日志)
INIT_DEVICE_EXPORT3外设驱动注册
INIT_COMPONENT_EXPORT4组件初始化(如文件系统)
INIT_ENV_EXPORT5环境变量、参数配置
INIT_APP_EXPORT6应用层初始化

注意:自动初始化函数不能阻塞!我曾经在 INIT_DEVICE_EXPORT 里放了个延时等待传感器就绪,结果整个系统启动卡了 3 秒。正确做法是:注册设备,状态用线程去轮询。

下面这张图,把启动流程串起来了——

RT-Thread 启动流程 复位 → Reset_Handler 汇编:设栈、清 BSS SystemInit() 配时钟 entry → rtthread_startup rt_hw_board_init() 内核对象初始化 自动初始化调用 创建 main 线程 启动调度器 汇编阶段:关中断,裸机环境 C 阶段:开中断,多任务就绪 自动初始化:按优先级 依次调用注册函数

核心对象概念扫盲

你想想看,RTOS 里最常打交道的就三个东西:线程、信号量、消息队列。我一个个说。

线程 —— 任务的“执行者”

线程就是一段独立运行的函数,有自己的栈。创建时指定优先级、时间片、栈大小。我见过新手把栈开得特别大,512 字节够用的非要 2048——其实 栈大小靠算,不靠猜。

// 创建线程示例
rt_thread_t tid = rt_thread_create("led",
                                    led_entry,   // 入口函数
                                    RT_NULL,     // 参数
                                    512,         // 栈大小
                                    20,          // 优先级
                                    10);         // 时间片
if (tid != RT_NULL) rt_thread_startup(tid);

避坑指南:线程入口函数里不能有 return。一旦返回,线程就“自杀”了。我习惯用 while(1) 包住所有逻辑。

信号量 —— 资源的“计数器”

信号量本质是一个整数。取一次减一,给一次加一。常用于同步或互斥。二值信号量(0/1)就是一把锁,计数信号量可以管理多个资源。

举个例子:串口发送完一帧数据,中断里释放信号量,发送线程等信号量——这样就不需要轮询了。

rt_sem_t sem = rt_sem_create("tx_sem", 0, RT_IPC_FLAG_FIFO);
// 中断中:rt_sem_release(sem);
// 线程中:rt_sem_take(sem, RT_WAITING_FOREVER);

消息队列 —— 数据的“邮局”

消息队列传递的是数据副本,不是指针。每个消息有固定长度,队列有最大条数。适合一个线程发数据,另一个线程收数据的场景。

我做过一个项目,传感器数据通过消息队列发给显示线程。队列深度设了 10 条,结果发现偶尔丢数据——查了半天,是接收线程处理太慢,队列满了直接丢弃。后来把深度加到 32,问题解决。

注意:消息队列拷贝数据有开销。如果传递大数据(比如 1KB 以上),建议用邮箱传指针,或者用共享内存 + 信号量同步。

这三个对象的关系,我总结一下:

  • 线程是干活的人
  • 信号量是门禁卡——有卡才能进门
  • 消息队列是快递柜——放进去,别人来取

嗯,这一章的内容就到这。你先把启动流程理清楚,再把三个核心对象的概念刻在脑子里。后面讲驱动移植,全得靠这些基础。