只读分享解决方案文档原理方案设计📅 2026-10-05🔖 Rev 1.4⏳ 26天有效(至 2026-11-05)
🔧解决方案&应用市场分析 -> 原理方案设计 -> 嵌入式驱动&系统开发 -> RT-Thread_系统开发指南

RT-Thread_系统开发指南

生成时间 2026-10-04 11:49:50 有效期至 2026-11-05 15:32:37(26天有效(至 2026-11-05))
同系列 · 原理方案设计

RT-Thread 系统开发指南

编制日期 2026-10-05

面向 Cortex-M / RISC-V 的 RT-Thread 系统层开发规范 · 聚焦内核、线程、IPC、内存、组件、工程架构与调试 · 不含 BSP 与外设驱动

32 级
默认最大优先级
RT_THREAD_PRIORITY_MAX,可配 8 / 32 / 256
1000 Hz
默认系统节拍
RT_TICK_PER_SECOND 默认 1000(不是 100)
≥ 25%
栈水位余量
上线前压力测试实测,ps 命令可读
≥ 20%
堆最小剩余
rt_memory_info() 的 max_used 反推
6 级
自动初始化等级
BOARD → PREV → DEVICE → COMPONENT → ENV → APP

一句话结论:RT-Thread 的系统层设计,本质是用「优先级 + 阻塞 + 对象容器」把 CPU 与共享资源按截止时间分配出去。 它比 FreeRTOS 多了一套内核对象容器与自动初始化框架,让「模块注册」变成编译期行为,代价是初始化顺序必须显式设计。 线上故障最集中的三处:栈容量不足、中断里用了非 xxx_isr 的 API、拿信号量当互斥量导致优先级反转。管住这三处,系统稳定性解决八成。

这份文档解决什么

本文只覆盖 RT-Thread 系统层面:内核启动链路、线程与调度、IPC 选型、内存三方案、定时器、rtconfig.h 配置、FinSH / ulog 等系统组件、低功耗、健壮性设计与调试手段。 凡涉及具体外设的实现(SPI / I2C / UART / 定时器 / PIN 设备驱动、文件系统 DFS、网络协议栈的驱动适配)均归 BSP 与设备驱动文档,本文只在必要处说明「线程侧该如何与它协作」。

路线 1 · 新项目起架子
先定架构再写代码
第 1 章分层与对象模型 → 第 2 章启动与自动初始化 → 第 3 章线程与栈 → 第 8 章内存规划 → 第 10 章配置 → 第 16 章工程组织。
重点:对象容器(1.2)、初始化等级表(2.3)、栈水位(3.3)
路线 2 · 排查稳定性问题
死机 / 卡死 / 重启
第 17 章速查表定位现象 → 第 7 章死锁与优先级反转 → 第 15 章栈堆与 IPC 诊断 → 第 14 章看门狗策略。
重点:ps / free(15.1)、list_mutex(15.3)、硬故障定位(15.5)
路线 3 · 降功耗
电池产品必读
第 13 章 Tickless 与 PM 模式 → 第 3.5 节空闲钩子 → 第 14 章看门狗与唤醒源的冲突。
重点:tick 补偿(13.1)、睡眠模式申请 / 释放(13.2)
路线 4 · 提性能与降抖动
CPU 跑满 / 响应慢
第 4 章调度与切换开销 → 第 12 章临界区 → 第 15.4 节 CPU 占用统计 → 第 6 章零拷贝与邮箱。
重点:就绪表位图(4.1)、关中断上限(12.3)

使用约定

  • API 与宏以 RT-Thread 4.x / 5.x 为准,目标以 Cortex-M3/M4/M33 为主;RISC-V 与 M0/M0+ 的差异在对应小节单独标注。
  • 代码示例只保留系统层逻辑,凡涉及具体寄存器与外设的部分统一写成 /* BSP 提供 */ 注释。
  • 标 红线 的条目为强制约束,标 警惕 的为高频踩坑点,标 建议 的为经验推荐值。
  • 文中出现的默认数值(栈大小、优先级、阈值)均以官方 rtconfig.h 模板与内核源码为参考;不同 BSP 与版本可能微调,落地前请以工程内的 rtconfig.h 与内核源码为准。
  • 芯片参数、时钟频率、外设行为一律以官方 datasheet 与 RT-Thread 官方文档 / 源码为准。

1 · 系统架构与内核对象模型

这一章解决「系统切成几块、每块放什么、内核对象是怎么被管起来的」。RT-Thread 与 FreeRTOS 最大的结构性差异就在对象模型上,理解了它,后面的 list_sem / ps 为什么能列出所有对象就自然清楚了。

1.1 四层分层模型

RT-Thread 是类 UNIX 风格的分层 RTOS:内核提供线程、IPC、内存、定时器,组件层提供 FinSH / ulog / POSIX / 软件包,BSP 层提供设备驱动与 libcpu 移植。分层的核心目的与 FreeRTOS 一致——让内核源码保持零修改,业务与内核之间只通过 rtconfig.h 与 API 交互。

RT-Thread 系统分层架构(BSP / 设备驱动剥离,本指南不展开)① 应用业务层 Application业务线程Business状态机State Machine消息处理Msg Handler算法 / 协议Algorithm监控与喂狗Monitor② 系统组件层 Components / PackagesFinSH命令行ulog分级日志POSIXpthread软件包MQTT / JSONlibc标准库③ 内核层 Kernel(本指南主体)线程调度优先级 + 位图IPCsem/mutex/event内存管理heap / mempool定时器hard / soft对象容器rt_object中断管理 / 临界区内核定时器链表空闲线程 / 钩子启动与自动初始化调试与断言 RT_ASSERT④ BSP / 设备驱动层(本指南不展开)设备模型 I/O DevicePIN / UART / SPI / I2Clibcpu 移植(PendSV / SysTick)启动汇编与链接脚本硬件 MCU / SoC(Cortex-M · RISC-V · …)
图 1 · RT-Thread 系统分层架构:应用 / 组件 / 内核 / BSP 四层
  • ① 应用业务层:业务线程、状态机、消息处理。每个线程只做一类事,通过 IPC 交互,不直接读写别人的全局变量。
  • ② 系统组件层:FinSH、ulog、POSIX、libc、软件包。它们运行在内核之上、与业务平级,靠自动初始化宏挂进系统。
  • ③ 内核层:rt-thread/src 下的调度器、IPC、内存、定时器与对象容器。红线 不修改任何内核源文件,只通过 rtconfig.h 配置。
  • ④ BSP / 驱动层:libcpu 移植(PendSV / SysTick / 上下文栈帧)、设备模型与外设驱动。本指南不涉及。
为什么建议再加一层「IPC 封装」

直接散落调用 rt_mq_send 的项目,半年后必然出现:超时参数五花八门、返回值无人判断、想加统计无从下手。

封装层只需 4 类接口——msg_post(dst,id,buf,len,timeout)、msg_recv(mq,buf,len,timeout)、sync_give(h)、sync_take(h,timeout)——内部统一记录失败次数、统一 RT_ASSERT、统一默认超时。见 16.4 消息中心模式。

1.2 内核对象容器 rt_object

这是 RT-Thread 的标志性设计:所有内核对象(线程、信号量、互斥量、事件集、邮箱、消息队列、内存池、定时器、设备…)都从一个公共基类 struct rt_object 派生,并被挂进一张按类型分类的对象容器表(rt_object_container[])。

struct rt_object {
    char       name[RT_NAME_MAX];   /* 对象名,调试与 FinSH 列出时用 */
    rt_uint8_t type;                /* 对象类型:Thread / Semaphore / Mutex / ... */
    rt_uint8_t flag;                /* 标志位:静态对象 / 动态对象 */
    rt_list_t  list;                /* 挂在容器链表上的节点 */
};
/* 派生方式:把 rt_object parent 作为第一个成员,即可安全强转 */
struct rt_thread {
    struct rt_object parent;        /* 必须是第一个成员 */
    /* ... 栈指针、优先级、状态、链表 ... */
};
对象容器带来的三个直接好处
  • 可枚举:FinSH 的 ps、list_sem、list_mq、list_timer 就是遍历这张表实现的,不需要额外登记。
  • 可按名查找:rt_object_find(name, type) 让模块之间可以通过字符串解耦引用(代价是字符串比较,别在高频路径用)。
  • 类型安全:rt_object_get_type() 可在封装层做入参校验,把「把信号量句柄传给互斥量 API」这类错误在运行时挡住。
对象类型枚举值含义静态创建 / 脱离动态创建 / 删除FinSH 查看命令
Thread线程rt_thread_init / detachrt_thread_create / deleteps
Semaphore信号量rt_sem_init / detachrt_sem_create / deletelist_sem
Mutex互斥量rt_mutex_init / detachrt_mutex_create / deletelist_mutex
Event事件集rt_event_init / detachrt_event_create / deletelist_event
MailBox邮箱rt_mb_init / detachrt_mb_create / deletelist_mb
MessageQueue消息队列rt_mq_init / detachrt_mq_create / deletelist_mq
MemPool内存池rt_mp_init / detachrt_mp_create / deletelist_mempool
Timer定时器rt_timer_init / detachrt_timer_create / deletelist_timer
Device设备对象(驱动侧注册)rt_device_createlist_device
静态对象与动态对象的唯一区别是内存来源

静态对象由调用者提供结构体(通常是全局 / static 变量),用 rt_xxx_init() 初始化、rt_xxx_detach() 脱离;动态对象由内核从堆里分配,用 rt_xxx_create()、rt_xxx_delete()。

两者 API 行为完全一致,只是内存来源与生命周期不同。量产项目默认走静态,理由见 8.5 与 17.1。

1.3 与 FreeRTOS 的架构差异

如果团队里有 FreeRTOS 背景,下表能快速对齐心智模型。完整对照见附录 D。

维度RT-ThreadFreeRTOS对开发的影响
内核对象管理统一对象容器,可按名查找、可枚举无容器,对象句柄需自行管理RT-Thread 调试可见性更好,代价是每对象多约 12~16 B
初始化框架自动初始化宏(6 级),编译期排序无,需手动在 main 里依次调用RT-Thread 要显式设计依赖顺序(2.4)
内存方案动态堆(小内存 / SLAB)+ 内存池 + memheapheap_1~heap_5RT-Thread 原生提供内存池与多区域堆
IPC 种类sem / mutex / event / mailbox / messagequeuequeue / sem / mutex / eventgroup / stream&message buffer / task notifyFreeRTOS 有任务通知(更轻);RT-Thread 有邮箱(4 字节,比队列省)
线程通知无独立任务通知机制,用邮箱或信号量替代Task Notification(最轻量)RT-Thread 高频唤醒场景建议用邮箱 + 静态池
命令行与日志内置 FinSH + ulog需外接 CLI / 第三方日志RT-Thread 开箱即用,但要记得裁剪(10.5)
调度位图就绪表 + 同优先级时间片轮转优先级链表 + 同优先级轮转行为基本一致,RT-Thread 优先级数默认 32
构建系统scons + Kconfig(menuconfig)Makefile / CMake 为主RT-Thread 裁剪靠 Kconfig,需熟悉 scons
最容易搞错的节拍默认值

不少资料写「RT-Thread 默认 RT_TICK_PER_SECOND = 100」,这是把 FreeRTOS 的 configTICK_RATE_HZ 默认值套过来了。

RT-Thread 官方模板的默认节拍是 1000(1 ms 粒度)。后果很实际:rt_thread_mdelay(1) 在 1000 Hz 下是 1 ms,在 100 Hz 下会被向上取整为 10 ms;定时器精度、超时判定全部随之变化。动手前先确认工程里的实际值。

1.4 线程划分原则

线程划分是最重要也最不可逆的一步。经验准则:线程不宜过多,每个线程单一职责,按时间特性而非按功能模块拆。

  1. 先按时间特性切:硬周期(采样、控制)与突发(通信、按键)必须分开;周期不同就不是一个线程。
  2. 再按功能独立性切:两个功能无共享状态、可独立测试,就可以分开。
  3. 高优先级线程必须短:只做「取数据 → 判条件 → 投递消息 / 置事件」,耗时活交给低优先级线程。
  4. 禁止一个大 while(1) 塞满所有业务:那等于把裸机超级循环搬进 RTOS,抢占优势全部失效。
  5. 同优先级可合并:时间特性相同、逻辑耦合紧密的,合并比拆开更省栈、更少同步。
  6. 一次性创建,运行期不删:见 3.1 与 3.6,这是量产项目的默认姿态。
线程类型时间特性建议优先级(32 级)单次执行上限典型错误
硬实时控制固定周期,抖动敏感0 ~ 4< 1 ms在循环里做浮点滤波,拖垮全系统
中断后置处理突发,由 ISR 触发5 ~ 8< 5 ms把处理逻辑写在 ISR 里
通信协议栈突发 / 半周期8 ~ 12< 10 ms在协议回调里做阻塞等待
周期采集固定周期10 ~ 16< 5 ms用 rt_thread_mdelay 做周期,误差累积
业务状态机事件驱动12 ~ 18< 20 ms状态机里塞满阻塞调用,外部事件不响应
日志 / 存储低频,可延迟18 ~ 24可长与通信线程抢同一把锁且顺序不一致
UI / 慢速轮询低频20 ~ 26< 20 ms忙等延时,占满 CPU,功耗降不下来
FinSH 命令行交互20(默认)—栈给太小,敲长命令就溢出
系统监控 / 喂狗固定周期24 ~ 28(不最低)< 1 ms优先级设为最高,死锁时照样喂狗,掩盖故障
空闲线程 idle无其他线程就绪时31(RT_THREAD_PRIORITY_MAX-1)—在空闲钩子里做阻塞操作
线程数量的经验区间

几十 KB RAM 的 Cortex-M3/M4 上,6~12 个业务线程是舒适区间(不含 idle / timer / tshell)。超过 20 个时,栈总量与调度开销明显上升,人的理解成本更是急剧增加——此时应反思是不是按「功能模块」而不是按「时间特性」拆的。

1.5 优先级分档方法论

优先级的本质是截止时间(deadline)的紧急程度,不是功能的重要程度。日志很重要,但可以晚 100 ms 写,优先级就该低。

  • 速率单调(Rate Monotonic, RM):周期越短,优先级越高。一批纯周期线程的理论最优静态分配。
  • 截止时间优先(Deadline Monotonic):按「从触发到必须完成的时限」排序,比 RM 更通用,适合混合突发线程的场景。
  • 留出空档:不要 0/1/2/3 连续排满,给每档之间留 2~3 级空位,方便后期插入新线程而不用整体重排。
档位(32 级制)定位约束
0 ~ 4硬实时闭环:电机 / 电源控制、紧急停机执行时间必须 < 1 ms,禁止任何阻塞调用
5 ~ 8中断后置处理:把 ISR 里的事搬出来做只做搬运与解析,禁止长耗时运算
8 ~ 12通信协议栈:接收、组包、重传禁止在回调里阻塞,超时必须有限
10 ~ 20业务主体:状态机、采集、算法这是绝大多数业务线程的落点
20 ~ 26日志、存储、UI、FinSH可延迟,允许被长时间抢占
24 ~ 28系统监控与喂狗优先级不能最高,否则死锁被掩盖
31空闲线程(RT_THREAD_PRIORITY_MAX-1)禁止阻塞;只能做快速统计与低功耗进入

警惕 时间片轮转不会给低优先级线程公平份额:只要高优先级线程一直就绪,低优先级线程就一直得不到运行(饿死)。因此高优先级线程里绝不能有忙等循环。

2 · 启动流程与自动初始化

RT-Thread 把「模块注册」做成了编译期行为:INIT_APP_EXPORT(fn) 一行就把函数挂进启动链。便利背后是硬约束——依赖关系必须由等级显式表达,顺序错了就是随机死机。

2.1 rtthread_startup 全链路

上电后,RT-Thread 通过 ARM 编译器的 $Sub$$main 机制,在用户 main() 之前插入 rtthread_startup()。完整链路如下:

rtthread_startup() 启动链路:从复位到调度器启动① 复位向量 / 启动汇编关中断 · 初始化栈与 .data/.bss② rt_hw_board_init()BSP:时钟、中断向量表、堆地址、串口、SysTick③ rt_system_heap_init()把 bss 段剩余空间交给内核堆(或 memheap)④ rt_scheduler_init()初始化各优先级就绪链表与位图⑤ rt_system_timer_init()初始化定时器链表⑥ rt_thread_idle_init()创建空闲线程(最低优先级)⑦ rt_system_scheduler_start() 之前:自动初始化按等级依次执行 INIT_*_EXPORT 注册的函数⑧ rt_application_init()创建 main 线程 → 用户 main()⑨ rt_system_scheduler_start()找到最高优先级线程,切换栈并启动调度
图 2 · rtthread_startup 启动链路:从复位到调度器启动
int rtthread_startup(void)
{
    rt_hw_interrupt_disable();          /* 1. 关中断,启动期不允许打扰 */

    rt_hw_board_init();                 /* 2. BSP:时钟、中断、堆地址、串口、SysTick */

    rt_show_version();                  /* 3. 打印版本号(RT_DEBUG 下) */

    rt_system_heap_init(HEAP_BEGIN, HEAP_END);  /* 4. 初始化系统堆 */

    rt_scheduler_init();                /* 5. 调度器数据结构 */
    rt_system_timer_init();             /* 6. 定时器链表(HARD 模式) */
    rt_thread_idle_init();              /* 7. 空闲线程(最低优先级) */

    rt_hw_spin_lock_init();             /* SMP 下才需要 */

    rt_system_scheduler_start();        /* 8. 之后:自动初始化 + main 线程 → 启动调度 */
    return 0;
}
堆的来源要看 BSP

rt_system_heap_init(begin, end) 的两个地址由 BSP 提供,常见做法是把链接脚本里 bss 段结束到 RAM 末尾的整块空间交给内核堆。这意味着:

  • 堆大小 = RAM 总量 − 静态数据 − 栈 − 各线程栈,是算出来的,不是配出来的;
  • 若 BSP 用的是 rt_memheap 或多段内存(如片外 SRAM),需要额外调用 rt_memheap_init() 注册(见 8.3)。

2.2 $Sub$$main 与 main 线程

很多初学者的疑问是「我的 main() 去哪了」。答案是:你的 main() 变成了 main 线程的入口函数。

/* libcpu / components 中的钩子(ARMCC / GCC 均支持) */
extern int $Super$$main(void);
int $Sub$$main(void)
{
    rtthread_startup();                 /* 1. 先把内核跑起来 */
    return $Super$$main();              /* 2. 再进入用户 main —— 此时它已是 main 线程 */
}
项默认值 / 行为说明
main 线程优先级RT_MAIN_THREAD_PRIORITY(默认 RT_THREAD_PRIORITY_MAX / 3,32 级时为 10)中高档。业务线程若希望比 main 更先跑,优先级要小于它
main 线程栈RT_MAIN_THREAD_STACK_SIZE(常见 2048 字节)在 main 里定义大数组极易溢出;业务重活应另建线程
main 返回之后线程退出并被回收(动态创建)红线 不要让 main 返回:用 while(1) 阻塞在事件上,或末尾 rt_thread_mdelay
main 里能不能创建线程可以,但不推荐业务线程建议用 INIT_APP_EXPORT 注册,与 main 解耦
main 里做重活的两个隐患
  • 栈不够:main 线程栈默认 2 KB 左右,一旦在里面写协议解析或放局部缓冲,极易溢出,且溢出位置离现场很远,很难查。
  • 初始化顺序不可控:main 线程与 INIT_APP_EXPORT 模块是并发竞争关系——main 优先级为 10,比它低的 APP 模块还没跑完,main 就可能已经去用它们的资源了。

推荐做法:main 只做「等待系统就绪 → 打印启动完成 → 阻塞」,所有业务用 INIT_APP_EXPORT 注册。

2.3 自动初始化六等级

RT-Thread 用链接器段排序实现自动初始化:宏把函数指针放进指定的段,启动时按段顺序依次调用。等级从低到高:

自动初始化六等级:注册顺序 = 执行顺序,被依赖者必须先跑1INIT_BOARD_EXPORT板级最底层:时钟树、引脚复用、堆之前依赖的东西2INIT_PREV_EXPORT依赖 board 但早于设备的初始化(如中断框架、日志后端)3INIT_DEVICE_EXPORT设备注册与初始化(驱动侧,本文不涉及,但顺序上它在这里)4INIT_COMPONENT_EXPORT系统组件:FinSH、ulog、libc、文件系统挂载5INIT_ENV_EXPORT运行环境:网络初始化、环境变量、需要组件就绪的中间件6INIT_APP_EXPORT应用业务:线程创建、消息队列建立、业务模块注册同一等级内部:按链接顺序(源码文件在 scons 中的排列)执行,不可依赖同等级内的先后。
图 3 · 自动初始化六等级与推荐放置的内容
/* 典型业务模块注册方式 */
static int app_msg_center_init(void)
{
    msg_center_init();                  /* 建消息队列 */
    rt_thread_create("msg", ...);       /* 建业务线程 */
    return RT_EOK;
}
INIT_APP_EXPORT(app_msg_center_init);   /* 挂到等级 6 */

/* 需要参数的初始化(5.x 支持传参) */
INIT_APP_EXPORT_WITH_PARAM(fn, param);
放置规则速记
  • 纯软件、不依赖任何外设(算法表、状态机初始化)→ INIT_COMPONENT_EXPORT 或 INIT_ENV_EXPORT。
  • 要打开设备 / 访问总线 → 必须在 INIT_DEVICE_EXPORT 之后,即 INIT_COMPONENT_EXPORT 及以上。
  • 要用 FinSH / ulog 打印 → 必须在 INIT_COMPONENT_EXPORT 之后。
  • 业务线程与消息队列 → INIT_APP_EXPORT,这是绝大多数业务模块的落点。

2.4 组件依赖顺序设计

等级只解决「大类」的顺序,模块之间的细粒度依赖需要自己设计。三步法:

  1. 画依赖表:列出每个模块「初始化时依赖谁」。例:日志模块依赖 ulog 后端;业务线程依赖消息队列;协议栈依赖 UART 设备 + 消息队列。
  2. 映射等级:被依赖者等级必须严格小于依赖者。同等级内的先后不可依赖(链接顺序不确定)。
  3. 加运行时守卫:跨等级边界的调用点加 RT_ASSERT(handle != RT_NULL),顺序错了立刻暴露,而不是跑飞。
依赖场景错误做法正确做法
业务线程要发日志线程放 INIT_COMPONENT_EXPORT,ulog 还没起来线程放 INIT_APP_EXPORT,日志后端放 COMPONENT 或更早
两个线程共享一个消息队列两个线程都在 INIT_APP_EXPORT,谁先跑不确定队列创建放 INIT_ENV_EXPORT,两个线程都放 INIT_APP_EXPORT
模块 A 的初始化要用模块 B 的设备A、B 同等级B 放 DEVICE,A 放 COMPONENT 及以上
线程创建后立刻 send 消息接收方线程还没建好接收方先建;或发送用超时 + 重试,队列容量预留
初始化里调用 rt_thread_mdelaydelay 依赖调度器已启动自动初始化阶段调度器尚未启动,见下方红线
自动初始化函数里不能阻塞

INIT_*_EXPORT 注册的函数,全部在 调度器启动之前执行。此时调用 rt_thread_mdelay()、rt_sem_take()、rt_mq_recv() 等会触发调度的 API,行为未定义——轻则该函数挂起导致整个启动卡死,重则调度器数据结构被破坏。

正确的做法:初始化函数只做「创建对象」,把「等待 / 交互」放到线程入口里做。

2.5 启动期典型故障

现象根因定位与处置
串口无任何输出,程序像没跑rt_hw_board_init 里串口未初始化;或堆地址越界导致后续初始化崩在 board_init 最前面点灯 / 打一个字符;核对链接脚本中的 HEAP_BEGIN/END
打印版本后死机堆空间太小或地址非法,第一个 rt_malloc 就崩核对 RAM 大小与 bss 占用;用 RT_ASSERT 捕获;调小静态缓冲
卡在某个 INIT 函数里初始化函数里调用了阻塞 API见上方红线;把阻塞逻辑搬进线程
main 里的代码没执行前面某个 INIT_APP_EXPORT 函数没返回;或 main 线程栈溢出逐个注释定位;加大 RT_MAIN_THREAD_STACK_SIZE
ps 看不到自己创建的线程线程创建失败(返回 RT_NULL)或创建后立即退出判断 rt_thread_create 返回值;检查入口函数是否直接 return
list_sem / list_mq 列不出对象未开启 RT_USING_FINSH 对应的 list 命令,或对象名超长检查 rtconfig.h 与对象 name 长度(RT_NAME_MAX)
启动期排错的最小手段

在串口还没起来时,最可靠的调试手段是点灯 + 读复位状态寄存器:在 board_init 各阶段翻转一个 GPIO,用示波器量脉宽即可定位卡在哪一步。等串口可用后,立刻打开 ulog 与 RT_DEBUG_INIT,让初始化过程可打印。

3 · 线程管理

线程创建只是一次函数调用,但栈给多大、静态还是动态、删不删,决定了半年后会不会在客户现场死机。

3.1 静态 init / 动态 create

RT-Thread 为每个内核对象都提供了静态与动态两套 API,命名规律统一:rt_xxx_init/detach(静态)与 rt_xxx_create/delete(动态)。

/* ====== 动态:内核从堆里分配 TCB 与栈 ====== */
rt_thread_t rt_thread_create(const char *name,
                             void (*entry)(void *parameter),
                             void       *parameter,
                             rt_uint32_t stack_size,   /* 单位:字节 */
                             rt_uint8_t  priority,     /* 0 最高 */
                             rt_uint32_t tick);        /* 时间片长度(Tick 数) */
rt_err_t    rt_thread_delete(rt_thread_t thread);

/* ====== 静态:TCB 与栈由调用者提供 ====== */
rt_err_t rt_thread_init(struct rt_thread *thread,
                        const char       *name,
                        void (*entry)(void *parameter),
                        void             *parameter,
                        void             *stack_start,
                        rt_uint32_t       stack_size,
                        rt_uint8_t        priority,
                        rt_uint32_t       tick);
rt_err_t rt_thread_detach(rt_thread_t thread);
stack_size 的单位是字节,不是 word

RT-Thread 的 stack_size 单位是字节;FreeRTOS 的 usStackDepth 单位是 word(Cortex-M 上 1 word = 4 字节)。从 FreeRTOS 迁移时直接照搬数字会把栈配小 4 倍,是最隐蔽的迁移坑之一。

例:rt_thread_create("t", f, RT_NULL, 1024, 10, 20) 表示 1 KB 栈、优先级 10、时间片 20 个 Tick。

/* 静态线程:量产项目的默认写法(编译期即可从 map 文件看到占用,无堆碎片) */
#define THREAD_SAMPLE_PRIO      12
#define THREAD_SAMPLE_STACK     1024      /* 字节 */
#define THREAD_SAMPLE_TIMESLICE 10

static struct rt_thread  g_tSample;                        /* TCB:全局 / static */
ALIGN(RT_ALIGN_SIZE)
static rt_uint8_t        g_tSampleStack[THREAD_SAMPLE_STACK]; /* 栈:全局 / static */
static rt_thread_t       g_hSample = RT_NULL;

static void sample_entry(void *parameter)
{
    for (;;) {
        /* 业务代码,禁止 return */
    }
}

int sample_thread_init(void)
{
    rt_err_t err = rt_thread_init(&g_tSample, "sample",
                                  sample_entry, RT_NULL,
                                  &g_tSampleStack[0], sizeof(g_tSampleStack),
                                  THREAD_SAMPLE_PRIO, THREAD_SAMPLE_TIMESLICE);
    if (err != RT_EOK) return err;
    g_hSample = &g_tSample;
    return rt_thread_startup(g_hSample) == RT_EOK ? 0 : -1;   /* init 之后必须 startup */
}
INIT_APP_EXPORT(sample_thread_init);
init 之后必须 startup,create 则不必

rt_thread_init() 只是把线程挂到对象容器并置为 INIT 状态,不会加入就绪表;必须再调 rt_thread_startup() 才会进入 READY。

rt_thread_create() 内部已完成这两步,返回的句柄即可运行。这是两套 API 最容易漏的一步。

对比项rt_thread_init(静态)rt_thread_create(动态)
内存来源调用者提供的全局 / static 数组内核堆(rt_malloc)
失败可能参数合法即成功(返回 RT_EOK)堆不足时返回 RT_NULL
是否需 startup需要额外调用 rt_thread_startup不需要,创建即就绪
碎片无反复创建删除会碎片化
RAM 可见性编译期可从 map 文件核对运行时才知道够不够
删除 / 脱离rt_thread_detach(不回收内存)rt_thread_delete(回收到堆,由 idle 完成)
适用量产产品、功能安全项目的默认选择原型验证、线程数可变的场景

警惕 不推荐在运行期频繁创建 / 删除线程。反复从堆里切块与回收会留下无法合并的碎片;需要「临时干活」的场景,改成常驻线程 + 消息队列唤醒(阻塞时不占 CPU),或用静态线程池。

3.2 线程状态机

RT-Thread 的状态枚举与 FreeRTOS 有一个关键区别:阻塞与挂起共用一个状态。

RT-Thread 线程状态机:INIT / READY / RUNNING / SUSPEND(BLOCK) / CLOSEINIT 初始已创建未启动READY 就绪在就绪表中等待调度RUNNING 运行占用 CPUSUSPEND 挂起等 IPC / 延时 / suspendCLOSE 关闭资源待回收startup调度选中被抢占 / 时间片到阻塞等待deletedeleteSUSPEND 在 RT-Thread 中同时承担「阻塞」与「挂起」两种语义:· 阻塞:rt_sem_take / rt_mq_recv / rt_thread_delay —— 事件到达自动唤醒· 挂起:rt_thread_suspend —— 只能由 rt_thread_resume 显式恢复因此 rt_thread_suspend 后若无人 resume,就是永久卡死(无超时兜底)。RT_THREAD_BLOCK 与 RT_THREAD_SUSPEND 在内核中是同一个枚举值,调试时无法仅凭状态区分二者。
图 4 · RT-Thread 线程状态机:INIT / READY / RUNNING / SUSPEND / CLOSE
  • INIT:已创建但尚未 startup(静态 init 后的状态)。
  • READY:在就绪表中等待被调度。
  • RUNNING:占用 CPU,同一时刻单核上只有一个。
  • SUSPEND:等待 IPC / 延时,或被 rt_thread_suspend() 显式挂起。前者会被事件自动唤醒,后者只能靠 rt_thread_resume()。
  • CLOSE:已退出,等待回收(动态线程由空闲线程回收 TCB 与栈)。
rt_thread_suspend 没有超时兜底

rt_thread_suspend() 挂起的线程不会因为任何事件自动恢复,必须显式 rt_thread_resume()。漏掉就是永久卡死,而且 ps 里看起来只是「状态为 suspend」,不仔细看不出来。

需要「等待某个条件」时,一律用带超时的 IPC(信号量 / 事件集 / 消息队列),不要用 suspend/resume 手工控制。

3.3 栈大小估算与水位

栈必须留余量,因为最坏路径往往不在正常测试里出现(最深调用链 + 最坏中断嵌套 + printf 同时发生)。

线程栈布局与水位:Cortex-M 上栈自高地址向下增长,水位线是历史最深使用点线程栈(stack_size 字节)stack_addr(低地址)未使用空间≥ 25% 为安全余量水位线(历史最深)已使用(最坏路径)溢出区stack_addr+stack_size(高地址)SP 向下走栈容量估算清单(逐项相加后 × 1.3)函数调用链最深路径静态分析 / 反汇编测量;每层 16 ~ 160 Bprintf / sprintf 家族300 ~ 800 B,是最贵的单项浮点格式化 %f / strtod200 ~ 500 B最坏中断嵌套栈帧8 word × 嵌套层数(Cortex-M 硬件自动压栈)浮点上下文(带 FPU)额外约 68 B(惰性压栈)局部大数组 / 结构体直接等于其 sizeof,禁止超过 64 B 放栈上线程切换上下文约 16 ~ 18 word(64 ~ 72 B)水位 = stack_size − 历史最大已用;目标:水位 ≥ stack_size × 25%
图 5 · 线程栈布局与水位:Cortex-M 上栈自高地址向下增长
  1. 先给宽松值:例如 1024 字节,先跑起来。
  2. 跑满所有路径:异常分支、错误打印、最坏中断并发、参数边界。
  3. 读水位:FinSH 执行 ps,看 sp 列显示的使用百分比;或用下方代码自算。
  4. 反推配置值:已用 = stack_size − 剩余;目标 stack_size ≈ 已用 × 1.3,且剩余 ≥ 25%。
  5. 回归锁死:写成具名宏(如 THREAD_SAMPLE_STACK 1024),并在监控线程里周期性检查,低于阈值上报。
/* 自算栈使用(成员名以所用版本内核源码为准) */
static rt_size_t stack_used(rt_thread_t th)
{
    if (th == RT_NULL) return 0;
    /* sp 指向当前栈顶;栈区间 [stack_addr, stack_addr + stack_size) */
    return (rt_size_t)((char *)th->stack_addr + th->stack_size - (char *)th->sp);
}

/* 开启 RT_USING_OVERFLOW_CHECK 后,内核提供栈溢出检查接口 */
#ifdef RT_USING_OVERFLOW_CHECK
    if (rt_thread_stack_check(th) != RT_EOK) { fault_record("STACK_OVERFLOW", th->name); }
#endif
栈消耗来源典型量级(Cortex-M4)说明
线程切换上下文64 ~ 72 BR4-R11 + R0-R3 + R12 + LR + PC + xPSR
函数调用帧每层 16 ~ 160 B参数多、局部数组大时更贵
printf / sprintf 家族300 ~ 800 B最贵的单项,依赖 libc 实现
浮点格式化 %f / strtod200 ~ 500 B建议禁用或改用轻量实现
浮点上下文(FPU)额外约 68 B惰性压栈下的扩展帧
最坏中断嵌套帧32 B × 嵌套层数硬件自动压栈部分
局部大数组等于其 sizeof头号杀手,见下方红线
局部大数组是栈溢出的头号杀手

void f(void){ rt_uint8_t buf[512]; ... } 一次性吃掉 512 B 栈,若该函数还被嵌套调用、或同时发生中断,栈瞬间见底。

规则:大于 64 字节的缓冲一律声明为 static 或全局;如果该缓冲会被并发访问,改用内存池或消息队列传递。

栈开发的三条纪律
  • 开发期打开 RT_USING_OVERFLOW_CHECK,并让监控线程周期性检查水位,而不是只在启动时看一次。
  • 栈大小写成具名宏并在文档里登记,禁止散落的魔法数字。
  • FinSH 线程、main 线程、timer 线程的栈同样要核对——它们的默认值不一定够用(尤其是打开 ulog 异步输出后)。

3.4 常用 API 与规则

API作用规则与注意
rt_thread_mdelay(ms)毫秒级相对延时最常用;延时起点是调用时刻,循环累积会漂移
rt_thread_delay(tick)按系统节拍延时tick 数;等价于 mdelay 的底层版本
rt_thread_sleep(tick)按节拍睡眠与 delay 语义基本一致,按工程习惯二选一
rt_thread_yield()让出 CPU 给同优先级就绪线程同优先级无就绪线程时立刻返回,不会白白等一个 Tick
rt_thread_startup(h)把线程置为就绪仅静态 init 后需要;对已运行线程调用会出错
rt_thread_suspend(h) / resume(h)挂起 / 恢复慎用,无超时兜底;见 3.2 红线
rt_thread_control(h, cmd, arg)控制线程:改优先级 / 关闭 / 启动cmd 见 RT_THREAD_CTRL_*;运行期改优先级会打乱优先级设计
rt_thread_self()取当前线程句柄常用于「谁在跑」的日志与统计
rt_thread_find(name)按名字查找线程字符串比较,别在高频路径调用
rt_thread_delete(h) / detach(h)删除 / 脱离删除前必须自己释放持有的资源与 IPC

固定周期线程的正确写法。RT-Thread 内核没有与 vTaskDelayUntil 完全等价的通用 API(部分版本提供 rt_thread_periodic_delay(),以所用内核源码为准),推荐用绝对时刻补偿自行实现,避免周期漂移:

void sample_entry(void *parameter)
{
    rt_tick_t last = rt_tick_get();                       /* 基准时刻 */
    const rt_tick_t period = rt_tick_from_millisecond(10);/* 10 ms 周期 */

    for (;;) {
        rt_tick_t now  = rt_tick_get();
        rt_int32_t wait = (rt_int32_t)(last + period - now);   /* 有符号比较,可正确处理回绕 */
        if (wait > 0) {
            rt_thread_delay((rt_tick_t)wait);
            last += period;                                /* 正常:按基准推进,不累积误差 */
        } else {
            last = now;                                    /* 过载:重新对齐,并上报一次超时 */
            g_sample_overrun++;
        }
        sample_once();                                     /* 必须远短于 period */
    }
}
周期过载必须被观测到

若 sample_once() 的执行时间超过周期,上面的写法会退化成「立刻再跑一次」,线程变成 100% 占用,而其他一切看起来都正常。必须把 g_sample_overrun 这样的计数暴露给监控线程 / FinSH,否则过载永远是隐形的。

禁止在线程里写忙等循环

while (flag == 0) { } 或 while (1) { rt_thread_mdelay(1); } 会让线程永远占用 CPU:同优先级的拿不到时间片,更低优先级的彻底饿死,功耗也降不下来。

一切等待都必须走阻塞 API:信号量、互斥量、事件集、邮箱、消息队列、rt_thread_mdelay。

3.5 空闲线程与钩子

空闲线程(idle)由内核在启动阶段创建,优先级固定为 RT_THREAD_PRIORITY_MAX - 1(32 级时为 31),永远处于就绪状态,保证调度器总有线程可跑。

  • 职责:回收被 delete 的线程资源(TCB / 栈);提供空闲钩子;低功耗进入点。
  • 空闲钩子:rt_thread_idle_sethook(hook),只在 CPU 空闲时执行,可做 CPU 占用统计、进入低功耗、喂狗之外的轻量巡检。
  • 红线 空闲钩子里不能阻塞:一旦钩子阻塞,资源回收与低功耗都会停摆,系统会「假死」——所有业务线程都在等,而 idle 卡住了。
static rt_uint32_t g_idle_cnt = 0;

static void idle_hook(void)
{
    g_idle_cnt++;                     /* 只做极轻量统计,禁止阻塞、禁止长耗时 */
    /* pm_enter();  —— 低功耗进入点由 BSP / PM 组件提供,本文不展开 */
}

int idle_hook_init(void)
{
    rt_thread_idle_sethook(idle_hook);
    return 0;
}
INIT_COMPONENT_EXPORT(idle_hook_init);
用空闲计数粗测 CPU 占用

固定窗口内统计 g_idle_cnt 的增量,与「idle 满速时的理论增量」相比,即可得到粗略的 CPU 空闲率。精度不如硬件计数器,但不需要任何额外外设,在资源紧张的项目里很实用(见 15.4)。

3.6 线程设计五原则

  1. 单一职责:一个线程只处理一类业务,通过 IPC 与其他线程交互,不直接读写别人的全局变量。
  2. 高优先级线程执行时间尽可能短:只做判定与投递,耗时活交给低优先级线程。
  3. 禁止忙等:一切等待走阻塞 API;禁止 while(1) 空轮询。
  4. 局部大数组不进栈:大于 64 B 的缓冲声明为 static / 全局,或改用内存池。
  5. 一次性创建、运行期不删:系统启动后不再动态创建 / 删除线程,是最省心也最省 RAM 的架构。
线程入口函数的标准骨架
static void xxx_entry(void *parameter)
{
    /* 1. 初始化本线程私有资源(失败要能安全退出或重试) */
    /* 2. 通知外界「我已就绪」(可选:发一个信号量 / 事件) */
    for (;;) {
        /* 3. 阻塞等待事件(带超时) */
        /* 4. 处理事件(短) */
        /* 5. 异常自恢复(超时 / 失败计数,超过阈值复位或降级) */
    }
    /* 永不 return */
}

4 · 调度机制

理解调度器怎么切线程,才能回答「为什么我的高优先级线程会被卡住 10 ms」这类问题。

4.1 优先级就绪表与位图

RT-Thread 的调度器由一张按优先级分桶的链表数组 + 一个位图组成:

/* 概念结构(具体定义见内核源码 src/scheduler.c) */
rt_list_t  rt_thread_priority_table[RT_THREAD_PRIORITY_MAX];  /* 每级一条就绪链表 */
rt_uint32_t rt_thread_ready_priority_group;                   /* 位图:bit i = 优先级 i 有就绪线程 */

/* 取最高优先级:一次 __rt_ffs 即可,O(1) 且与线程总数无关 */
highest = __rt_ffs(rt_thread_ready_priority_group) - 1;
next    = rt_list_entry(rt_thread_priority_table[highest].next, struct rt_thread, tlist);
就绪表 rt_thread_priority_table[] + 位图 rt_thread_ready_priority_group:一次 __rt_ffs 定位最高优先级位图(32 级示例)012345678910111213141516171819202122232425262728293031bit = 1 表示该优先级链表非空;__rt_ffs(group) 直接返回最低置位位(数值最小 = 优先级最高)优先级链表(每个优先级一条双向链表,同优先级按 FIFO 或时间片轮转)优先级 2T_ctrl →(就绪)优先级 5T_comm → T_comm2优先级 12T_sample优先级 20T_log优先级 31idle(永不为空)调度一次的成本:查位图 O(1) → 取链表头 → 若与当前线程不同则触发 PendSV 切换。256 级优先级时位图退化为字数组(先定位字、再定位位),仍是常数时间,但多一次内存访问。
图 6 · 优先级就绪表与位图:一次 __rt_ffs 定位最高优先级
为什么这个结构重要

调度复杂度是 O(1),与线程数量无关——这是 RT-Thread 在几十个线程的工程里依然能保持稳定响应延迟的原因。相应地,优先级数量直接决定这张表的静态 RAM 开销(见 4.5)。

4.2 抢占与时间片轮转

机制触发条件行为
抢占高优先级线程变为就绪(IPC 释放、ISR 唤醒、定时器到期)当前线程立刻被挂起,高优先级线程马上运行
时间片轮转同优先级有多个就绪线程,且当前线程时间片耗尽当前线程 remaining_tick 归零 → 重置时间片 → 移到链表尾 → 运行下一个
主动让出rt_thread_yield()同优先级内切换;若无同优先级就绪线程则立即返回继续运行
阻塞让出调用带超时的 IPC 或 delay移出就绪表,进入 SUSPEND,事件到达或超时后回到就绪表末尾
/* rt_thread_create 的最后一个参数就是时间片长度(Tick 数) */
rt_thread_create("worker", worker_entry, RT_NULL,
                 1024,              /* 栈:字节 */
                 15,                /* 优先级:数值越小越高 */
                 20);               /* 时间片:20 个 Tick(1000 Hz 下 = 20 ms) */

警惕 两个易错点:

  • 时间片只在同优先级之间生效:低优先级线程不会因为「高优先级用了太久」而得到补偿,只要高优先级一直就绪,它就一直饿死。
  • 时间片长度不是周期:它是「同优先级连续占用 CPU 的最长时长」,线程阻塞时会立刻让出,不消耗完整个时间片。

4.3 调度器锁与 rt_schedule

API作用约束
rt_enter_critical() / rt_exit_critical()锁调度器:禁止线程切换,但不关中断可嵌套;中断照常响应,因此 ISR 里仍能调 xxx_isr 版本号 Rev 1.4
rt_schedule()主动触发一次调度判定应在线程上下文调用;中断里要用 rt_schedule_isr 的等价路径(由内核内部处理)
rt_hw_interrupt_disable() / enable()关 / 开全局中断最强也最贵,见第 12 章
rt_interrupt_enter() / leave()标记中断嵌套,供内核判定上下文BSP 的 ISR 入口/出口调用;业务代码一般不需要
调度器锁不等于互斥量

rt_enter_critical() 只阻止线程切换,不阻止中断,也不阻止另一个核(SMP)。因此它只能保护「线程之间」的短临界资源,不能保护「线程与 ISR 之间」的共享数据——后者必须关中断或用信号量。

另外,调度器锁期间不能调用任何可能阻塞的 API:一旦阻塞,锁永远解不开,系统死锁。

4.4 Cortex-M 上下文切换

与 FreeRTOS 一致:Cortex-M 上的切换由 SysTick(判定该不该切)与 PendSV(真正执行切换)协作完成,PendSV 被设为最低优先级,保证所有 ISR 处理完后再切换。

Cortex-M 上下文切换:SysTick 判定 → PendSV(最低优先级)执行切换线程 A(低优先级)内核(SysTick)PendSV(切换)线程 B(高优先级)线程 A 运行Tick时间片-1置 PendSV保存 A / 恢复 B线程 B 运行线程 A 恢复PendSV 被设为最低优先级:保证所有其他 ISR 处理完之后才做切换,不拖慢中断响应。首次启动线程由 rt_system_scheduler_start() 直接切换栈,不经过 PendSV。
图 7 · Cortex-M 上下文切换:SysTick 判定 → PendSV 执行
切换开销的量化

一次上下文切换 ≈ 保存/恢复 R4-R11 与栈指针操作,Cortex-M3/M4 上约 1~3 µs(与编译优化、栈位置、是否有 FPU 上下文有关)。1000 Hz Tick 下,即使每个 Tick 都切换,开销占比也很小。

真正的 CPU 杀手从来不是切换本身,而是过长的临界区、高频无意义的 Tick 唤醒与ISR 里做重活。

4.5 优先级数量与开销

RT_THREAD_PRIORITY_MAX就绪表静态 RAM适用说明
8约 8 × 8 B = 64 B极小资源 MCU(几 KB RAM)档位太少,业务一复杂就容易挤在同一档
32(默认)约 32 × 8 B = 256 B绝大多数项目推荐:档位够用,开销可忽略
256约 256 × 8 B = 2 KB大型系统 / 多子系统混跑位图退化为字数组,多一次内存访问;2 KB 对小 RAM 不划算
选型建议
  • 默认保持 32,不要为了「省 RAM」降到 8——64 B 与 256 B 的差别远小于档位不足带来的架构扭曲。
  • 只有在「多个第三方组件各自定义优先级区间」的大型系统里才考虑 256。
  • 改这个宏会改变空闲线程优先级(RT_THREAD_PRIORITY_MAX - 1)与 RT_MAIN_THREAD_PRIORITY,务必同步复核所有优先级分配。
扫码打开本页
微信扫码 · 展会可扫

再分享给同事

同事可直接打开这份资料《RT-Thread_系统开发指南》,在线阅读;完整章节请下载芯参谋。