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 初始化的总入口,顺序固定:
- 硬件初始化:
rt_hw_board_init()—— 配中断、定时器、串口 - 内核对象初始化:
rt_system_scheduler_init()、rt_system_timer_init() - 自动初始化:调用所有
INIT_XXX_EXPORT宏注册的函数 - 创建主线程:启动
main线程 - 启动调度器:
rt_system_scheduler_start()—— 从此不再返回
个人习惯:我总在 rt_hw_board_init() 里加个 GPIO 翻转,用来示波器看启动耗时。这招排查启动卡死特别管用。
3. 自动初始化机制 —— 宏背后的魔法
RT-Thread 用宏把初始化函数分段注册。你写个 INIT_APP_EXPORT(my_init),它就在编译时把函数指针塞进特定段。启动时按优先级依次调用。
| 宏名称 | 调用顺序 | 典型用途 |
|---|---|---|
INIT_BOARD_EXPORT | 1 | 板级硬件初始化 |
INIT_PREV_EXPORT | 2 | 纯软件服务(如日志) |
INIT_DEVICE_EXPORT | 3 | 外设驱动注册 |
INIT_COMPONENT_EXPORT | 4 | 组件初始化(如文件系统) |
INIT_ENV_EXPORT | 5 | 环境变量、参数配置 |
INIT_APP_EXPORT | 6 | 应用层初始化 |
注意:自动初始化函数不能阻塞!我曾经在 INIT_DEVICE_EXPORT 里放了个延时等待传感器就绪,结果整个系统启动卡了 3 秒。正确做法是:注册设备,状态用线程去轮询。
下面这张图,把启动流程串起来了——
核心对象概念扫盲
你想想看,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 以上),建议用邮箱传指针,或者用共享内存 + 信号量同步。
这三个对象的关系,我总结一下:
- 线程是干活的人
- 信号量是门禁卡——有卡才能进门
- 消息队列是快递柜——放进去,别人来取
嗯,这一章的内容就到这。你先把启动流程理清楚,再把三个核心对象的概念刻在脑子里。后面讲驱动移植,全得靠这些基础。