简介:RT-Thread Nano是由RT-Thread开源实时操作系统裁剪出的轻量级内核,面向物联网、智能家居、工业控制等资源受限的嵌入式场景,解决小内存微控制器上多任务调度与实时响应问题。该压缩包共含692个文件,约10.65兆字节,以C语言源码和头文件为主体,另有汇编启动代码、链接脚本、PDF参考手册、工程配置文件及编译生成结果,便于对照阅读和二次构建。包内提供的示例工程和说明文档覆盖定时器、串口、中断处理等常见功能,并可理解任务优先级抢占、信号量与互斥锁等RTOS核心机制;移植到具体电路板时可复用驱动代码,快速验证内核启动流程与任务通信流程。对学习实时操作系统或进行低资源嵌入式项目开发的工程师,这是一份包含源码、驱动、示例与文档的实用资料。目前已有473人浏览学习,适合作为入门到进阶的参考。
1. 拿到 rt-thread-3.1.3(RT-Thread Nano).rar,先别当普通 SDK 解压
拿到 rt-thread-3.1.3(RT-Thread Nano).rar,先别急着解压就想编译。这个包不是打开就能用的 SDK,解压出来的是一整套被裁到内核级的实时操作系统源码:没有设备驱动框架、没有文件系统、没有命令行调试组件,只留下多线程调度、信号量、邮箱、消息队列、软件定时器、内存管理这几样最接近裸机的东西。
它解决的问题很具体:裸机 while(1) 加标志位的写法已经撑不住业务复杂度,而手上 MCU 的 RAM 又窄得塞不下完整版 RT-Thread。Nano 的存在就是让几十 KB 内存的单片机也能体面地跑多任务。我按这份 3.1.3 源码说清楚:目录里各是什么、怎么接进自己的工程、rtconfig.h 哪些参数要动、移植时最容易翻车的几个位置。第一次从裸机转 RTOS 的开发者能跟着做,老手也能拿来做版本选型的参考。
2. 解开压缩包看门道:Nano 源码目录结构与你和内核的分工
解开 rar 后,常见的包里是一个顶层源码目录,名字通常就叫 rt-thread-3.1.3。它不是工程文件,不需要也不能直接被 IDE 打开编译。你要做的是把它当成“零件库”,挑出内核本体、架构相关的汇编文件和配置文件,搬进自己的工程。源码的核心区域一般有四个:src、include、libcpu,以及一个可选的 components 目录。
解开后第一件事不是看代码,而是分清“哪部分是内核的、哪部分必须由你写”。内核负责线程管理、调度、IPC、定时器和内存分配;你负责提供板级初始化、系统时钟(tick)、内存堆和业务线程。这条边界一旦清楚,后面的移植就是照着接口填空。
2.1 src 与 include:内核本体和配置文件模板
src 目录是内核的主体,里面按功能拆成一组 .c 文件,常见的有:
- scheduler.c:线程调度器核心,管理就绪队列、优先级抢占、时间片轮转。
- thread.c:线程的创建、初始化、启动、退出,线程状态迁移。
- timer.c:系统软定时器管理,配合 tick 实现超时和周期回调。
- clock.c:系统节拍(tick)的管理,所有延时和超时都建立在它上面。
- ipc.c:信号量、互斥量、邮箱、消息队列、事件集的实现。
- mem.c 或 memheap.c:小内存管理算法,负责动态分配和释放。
- object.c:内核对象体系,统一管理线程、信号量等对象的挂接与查找。
include 目录里最重要的两个头文件是 rtthread.h 和 rthw.h。前者是内核 API 的声明,你的业务代码和驱动代码都离不开它;后者是硬件中断、上下文切换相关接口的声明,一般只有 board.c 和 libcpu 里的代码会包含它。rtconfig.h 通常也放在 include 里,或者由包内单独提供模板,它是整个 Nano 的配置开关,第 4 章会专门讲。
看到这里你应该明白一件事:Nano 的“内核”不是一个大而全的 lib,而是一堆按功能切好的源文件。加进工程时,你可以全加,也可以根据 rtconfig.h 的裁剪决定哪些 .c 不需要参与编译。但新手阶段我建议全加,先把系统跑起来再谈裁剪。
2.2 libcpu:换一块芯片,主要就是换这一层
libcpu 目录是处理器架构相关的移植代码,一般按 ARM、RISC-V 等分类,ARM 下还会按 cortex-m3、cortex-m4 细分。这层主要包含两类东西:
一类是上下文切换的汇编文件。Cortex-M 在 MDK 工程里通常叫 context_rvds.S,GCC 工程里叫 context_gcc.S,IAR 工程里叫 context_iar.S。不同工具链的汇编语法不一样,选错了文件编译会直接报错。另一类是 cpuport.c 和 interrupt.c,负责栈初始化、中断进入/退出计数(rt_interrupt_enter / rt_interrupt_leave),以及 PendSV 触发线程切换的底层动作。
这部分你基本不需要读懂汇编细节,但必须保证“文件选对”。选错架构或选错工具链,编译都过不去;即使编译过了,也可能因为寄存器保存不完整,导致线程一切换就 HardFault。我在自己的工程里习惯把这层单独放一个目录,并在编译脚本里用宏控制,换芯片时只动目录引用,不改业务代码。
2.3 Nano 与标准版的分界:省掉的组件由谁补位
把 Nano 称为“最小内核”不是夸张的说法。标准版 RT-Thread 里常见的设备驱动框架(rt_device)、finsh/MSH 命令行控制台、网络协议栈、文件系统、AT 组件等,在 Nano 里默认都不存在。这是设计选择,不是阉割:Nano 本身就是给资源受限场景用的,它只保留“内核”和“内核对象”,把一切业务相关的东西留给使用者。
这就带来一个实际分工问题。你原来的裸机工程如果有串口驱动、I2C/SPI 驱动、GPIO 控制代码,这些代码不会被 RT-Thread “接管”,Nano 也不提供任何新的驱动接口。你要做的只是把驱动函数保留原样,然后决定在哪个线程里调用它们。常见做法是把一个外设驱动包成一个模块,对外只留若干独立函数,线程里通过信号量或消息队列来驱动它。驱动层的代码依然完全是你的,Nano 只负责让你能并行地跑多个任务,并在任务之间传递信号和数据。
分界清楚了,编译时就不会有“为什么找不到 rt_device_init”这类疑问。内存管理、IPC、调度是内核管的;时钟初始化、中断向量、外设驱动、业务逻辑是你管的。把这两个域分清楚,接下来移植的每一步都会非常顺。
3. 在 Cortex-M 上移植 3.1.3 Nano:从空工程到线程跑起来
移植本质上是三个动作:把内核源码交给编译器、给内核提供时钟和内存、把自己的业务代码塞进线程里。我以一块常见的 Cortex-M3 开发板(自带 LED 和串口)为例,讲一套我常用的移植路径。
3.1 拷源码进工程:文件不齐之前,编译错一半
先在工程目录里建一个 rtos 文件夹,把内核源文件和头文件拷贝进去,再把 libcpu 下对应芯片架构的文件也拷进去。GCC 或 MDK 工程都可以按这个思路处理,差异只在文件名后缀。
# 假设工程根目录是 my_project,解压后的源码目录是 rt-thread-3.1.3 mkdir -p my_project/rtos # include 和 src 是整个内核的公共部分,直接整个拷 cp -r rt-thread-3.1.3/include my_project/rtos/include cp -r rt-thread-3.1.3/src my_project/rtos/src # libcpu 按芯片架构选,这里以 arm/cortex-m3 为例 mkdir -p my_project/rtos/libcpu cp rt-thread-3.1.3/libcpu/arm/cortex-m3/*.S my_project/rtos/libcpu/ cp rt-thread-3.1.3/libcpu/arm/cortex-m3/*.c my_project/rtos/libcpu/ # 把 rtconfig.h 拷贝到 include 下,之后直接改这份 cp rt-thread-3.1.3/include/rtconfig.h my_project/rtos/include/rtconfig.h这段命令有几个点说明一下。include 和 src 必须成对拷贝,只拷 src 不拷 include,编译时头文件路径会一片飘红。.S 汇编文件必须加入工程编译,很多新手只把 .c 文件加进 MDK,结果链接阶段报一堆上下文切换符号未定义,其实就是汇编文件缺席。rtconfig.h 拷出来单独改,后面所有裁剪调参都在这份文件里做,尽量不要动包内原件,方便以后对比。
拷贝完成之后,在 IDE 里把 rtos/src 下的 .c 和 rtos/libcpu 下的 .S 添加进工程,头文件路径指向 rtos/include。第一次编译大概率会报错,主要集中在缺少 board.c、缺少 rtconfig.h 里的宏定义,这是正常的,下一步就是把这些缺的补上。
3.2 板级入口:写一个能跑的 board.c
board.c 是移植里唯一躲不掉的自定义文件,它要向内核提供两个基础资源:系统节拍和内存堆。系统节拍决定 rt_thread_mdelay(1000) 是否真的延时一秒;内存堆决定 rt_thread_create 能不能动态创建线程。下面是一份精简实现,适用于有 CMSIS 的 Cortex-M 芯片。
/* board.c */ #include "rtthread.h" #include "rthw.h" /* 用静态数组作为动态内存堆,具体大小在 rtconfig.h 里配 */ static rt_uint8_t rt_heap_pool[RT_HEAP_SIZE]; /* 系统节拍初始化:使用 SysTick 产生周期中断 */ static void rt_hw_systick_init(void) { /* SystemCoreClock 由芯片的时钟初始化代码提供,必须是实际运行的时钟频率 */ SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND); /* SysTick 优先级给一个较低值,业务外设中断可以抢占它 */ NVIC_SetPriority(SysTick_IRQn, 0x0F); /* PendSV 必须设为最低优先级:数值越大优先级越低 */ NVIC_SetPriority(PendSV_IRQn, 0xFF); } /* 内核启动时调用的板级初始化入口 */ void rt_hw_board_init(void) { /* 如果你有系统时钟配置代码,比如从外部晶振倍频到主频,在这里先调用 */ /* SystemClock_Config(); */ rt_hw_systick_init(); /* 向内核注册内存堆的起止地址 */ rt_system_heap_init(rt_heap_pool, &rt_heap_pool[RT_HEAP_SIZE]); } /* SysTick 中断服务函数:每进来一次,系统 tick 加 1 */ void SysTick_Handler(void) { /* 通知内核进入中断状态,中断嵌套计数要配对 */ rt_interrupt_enter(); rt_tick_increase(); rt_interrupt_leave(); }这份代码要注意三个地方。
SysTick_Config 的参数是两次中断之间的 tick 数,用 SystemCoreClock 除以 RT_TICK_PER_SECOND 得到。如果芯片实际主频和 SystemCoreClock 不一致,延时就会整体漂移,比如调 1000ms 实际只等了 800ms。PendSV 优先级必须设成最低,上下文切换依赖它,设错会在实时性要求高的场景下偶发异常。rt_system_heap_init 的调用时机要早于任何动态创建线程的代码,否则 rt_thread_create 拿不到堆地址,返回空指针。
3.3 用户入口与线程:把裸机 main 变成一个启动壳子
Nano 的启动方式我很喜欢:它不要求你把业务代码改成花花绿绿的线程模型,只需要把 main() 当成启动入口,在 main 里调起内核,然后在内核启动序列里注册业务线程。下面这段放在一个单独的 app_rtos.c 里,作用是“拉起内核”。
/* app_rtos.c */ #include "rtthread.h" /* 内核初始化与启动:main 里只调用这一个函数 */ void rtthread_startup(void) { rt_hw_interrupt_init(); /* 中断进入/退出计数初始化 */ rt_hw_board_init(); /* 板级初始化:时钟、tick、内存堆 */ rt_system_timer_init(); /* 软定时器初始化 */ rt_system_scheduler_init(); /* 调度器初始化 */ rt_application_init(); /* 创建业务线程,这个函数由用户实现 */ rt_system_scheduler_start(); /* 启动调度,永不返回 */ } /* 业务入口:内核启动调度前会调用它来创建线程 */ void rt_application_init(void) { rt_thread_t tid; tid = rt_thread_create("led", led_entry, RT_NULL, 512, /* 栈大小,单位字节 */ 3, /* 优先级,0 最高,31 最低 */ 20); /* 时间片,20 个 tick */ if (tid != RT_NULL) rt_thread_startup(tid); } /* LED 线程:业务代码的实际载体 */ static void led_entry(void *param) { while (1) { /* 这里放一个 GPIO 翻转,或者直接侯串口打印观察 */ rt_kprintf("led thread alive, tick=%d\n", rt_tick_get()); rt_thread_mdelay(500); } }main.c 里的写法就非常简单了:
/* main.c */ #include "rtthread.h" int main(void) { rtthread_startup(); /* 所有初始化都在这里,之后不再返回 */ return 0; }关键点在于 rt_application_init。它会在调度器启动之前被执行,你可以在这里创建任意多个线程,也可以只创建一个“业务入口线程”,再从这个线程派生其他线程。优先级建议先给一个中间值(比如 3 到 5),等系统跑通之后再调。栈大小 512 字节对 LED 翻转够用,但如果线程里要做 printf 或者放一个较大局部结构体,建议直接给 1024。
如果你拿到的 Nano 包已经自带了一套 rtthread_startup 的调用链,优先以包内为准;我这里展示的是在自己工程里验证过的等价写法,跑通的最小闭环是一致的。
3.4 验证点:看 tick、看线程切换,确认内核真的在转
移植完成最怕的不是编译出错,而是“看起来能编译,实际没逻辑问题”。我的习惯是上电后先看两个东西:串口有没有周期性输出,以及每次输出的 tick 数值是否持续增长。
在 led_entry 里每 500ms 打印一次 tick,理论上每次打印的 tick 差应该稳定在 500 左右。如果 tick 不增长,说明 SysTick 中断没有接进来,检查 SysTick_Handler 是否真的进入了中断。如果 tick 在增长,但数值跳变完全没规律,先怀疑 SystemCoreClock 和实际主频不一致,再用调试器在 SysTick_Config 处读取时钟配置做比对。
确认 tick 正常之后,再加第二个线程,两个线程交替打印。比如一个打印 A,一个打印 B,优先级相同、时间片相同,观察输出是否交替出现。这一步过了,说明调度器在线程之间真正做了轮转,移植闭环就算跑通了。后面的优化才是裁剪体积和调整优先级的事。
4. rtconfig.h 参数调节奏:3.1.3 Nano 的内存换功能开关
Nano 的配置哲学非常直接:所有功能都默认关掉,你用哪个宏就打开哪个宏。3.1.3 这一代还没有后来 4.x 版本那套完整的 menuconfig 图形化配置,改参数就是手动编辑 rtconfig.h,改完重新编译全量生效。好消息是宏数量不多,核心就十来个,真正影响行为的更少。我把一份常用的对照表放在下面,动任何一个宏之前先看一眼它的“代价”。
4.1 必调宏对照表:动一个宏,看一个效果
| 宏名 | 作用 | 常见取值 | 备注 |
|---|---|---|---|
| RT_THREAD_PRIORITY_MAX | 系统支持的最大优先级数 | 8 / 32 | 数值越小越省内存,线程 priority 参数必须小于它 |
| RT_TICK_PER_SECOND | 每秒心跳数 | 100 / 1000 | 1000 时一个 tick 是 1ms,精度高但中断更频繁 |
| RT_ALIGN_SIZE | 内存对齐字节数 | 4 / 8 | Cortex-M 上常用 8,配合 64 位变量的自然对齐 |
| RT_USING_HEAP | 动态内存堆总开关 | 1 建议开 | 关闭后只能静态创建线程和 IPC 对象 |
| RT_HEAP_SIZE | 动态堆大小,单位字节 | 2048 起 | 太小会导致创建对象失败,返回 RT_NULL |
| RT_USING_TIMER_SOFT | 软件定时器内核线程 | 0 / 1 | 不用 rt_timer_create 可以关掉 |
| RT_USING_SEMAPHORE | 信号量 | 0 / 1 | 业务里有任务同步就开 |
| RT_USING_MUTEX | 互斥量 | 0 / 1 | 有优先级继承需求才开,哑点比信号量略大 |
| RT_USING_MAILBOX | 邮箱 | 0 / 1 | 适合小数据块传递 |
| RT_USING_MESSAGEQUEUE | 消息队列 | 0 / 1 | 适合不定长或较大数据块传递 |
| RT_MAIN_THREAD_STACK_SIZE | 初始线程栈大小 | 1024 / 2048 | 如果业务入口要跑复杂逻辑,给 2048 起步更稳 |
| RT_USING_HOOK | 钩子函数支持 | 0 / 1 | 做 CPU 占用率统计、栈检查时会用到 |
这张表的规律是:每个宏都在拿内存换功能。RT_USING_MESSAGEQUEUE 一旦打开,内核会为消息队列管理分配额外的内存池开销;RT_USING_TIMER_SOFT 打开后,系统会额外创建一个软定时器线程,这个线程本身要栈、要优先级。所以裁剪的第一步永远是关掉业务里用不到的东西,而不是把栈调小。
4.2 三套可复用的配置组合
我实际接触过的项目基本可以归到三套组合里。
第一套是“极限裁剪版”,面向只有 8 到 16 KB RAM 的老芯片。配置是 RT_THREAD_PRIORITY_MAX 给 8,RT_TICK_PER_SECOND 给 100,关掉 RT_USING_HEAP,关掉所有 IPC,只保留线程创建和 mdelay。线程全部用静态方式创建,信号量用静态初始化。这套组合能跑起来多线程,但任何动态分配和消息传递都不可用,适合非常轻量的控制逻辑。
第二套是“标准均衡版”,也是我默认的起点。RT_THREAD_PRIORITY_MAX 给 32,RT_TICK_PER_SECOND 给 1000,打开 RT_USING_HEAP 并把 RT_HEAP_SIZE 设成 4096 或 8192,打开信号量和消息队列,软定时器按需开。这套配置跑大多数传感器采集、控制输出、串口透传类产品都够用,内存占用也还在可控范围内。
第三套是“功能验证版”,面向刚把 Nano 移植到新板子上的阶段。什么都开:堆给到 16384,所有 IPC 打开,软定时器打开,RT_USING_HOOK 打开。目的是先把系统所有能力验证一遍,再根据实际业务逐项关掉。我见过不少人在这个阶段反过来,一上来就极致裁剪,结果后面加业务功能时经常要往回翻改配置,反而耽误时间。
4.3 裁剪的第一刀和第二刀:边界在哪
第一刀通常砍向 IPC。业务里只有线程没有复杂的任务间通信时,信号量、互斥量、邮箱、消息队列这些全都可以关。每个 IPC 组件都会引入几十到几百字节的代码和数据结构,关掉它们是最干净的内存回收。
第二刀是内存堆。RT_USING_HEAP 关闭后,所有动态创建接口都不可用,但你可以用静态接口替代。rt_thread_create 变成 rt_thread_init,rt_sem_create 变成 rt_sem_init,业务代码只需要在初始化阶段把对象和控制块挂上去。这个改动会带来一定的代码冗余,但能省掉整个动态内存池。
裁剪的边界就是“你还敢不敢为未来留余地”。如果你判断这个项目后续一定会加通信功能,建议别把邮箱和消息队列都关了,否则哪天需要在两个线程间传一份数据,又没有共享内存方案,真是拿后悔药都没处找。我的习惯是保留信号量和消息队列,关掉邮箱和事件集,这两个组件在很多业务里是可以互相替代的,留一个就够。
5. 移植避坑手记:五个一上来就翻车的现场
Nano 移植的坑,大多不在源码本身,而在“你以为配好了,实际没配对”的角落。下面五条是我反复看到的翻车现场,每一条都按现象、原因、解决的顺序拆开。你移植时如果卡住了,优先对照这几条排查。
5.1 tick 不走:心跳根本没接进内核
现象:线程能创建,但 rt_thread_mdelay 之后线程永远不醒,或者串口打印的 tick 数值卡住不动。
原因:SysTick_Handler 没有真正被中断向量表链接到内核的实现。常见情况是旧工程里已经有一个 SysTick_Handler,被裸机延时库占据,你新写的版本根本没有参与链接;也有的情况是中断向量表用的是芯片厂商的启动文件,里面写的是 SysTick_Handler 的另一个别名。
解决:在调试器里给 SysTick_Handler 打断点,看程序运行后是否每毫秒进一次。进不来,就去查启动文件里的中断向量表名称有没有拼写差异;能进来但 tick 不增长,检查函数内是否调用了 rt_tick_increase,以及这个符号有没有被链接器优化掉。还有一招是把 SysTick_Handler 里的代码临时改成 GPIO 翻转,用示波器看频率,能最快确认中断真的在跑。
5.2 PendSV 优先级错了,任何切换都是雷
现象:系统在 idle 和业务线程之间切换时偶发 HardFault,并且故障位置飘忽不定,用调试器看 PC 经常落在 PendSV_Handler 附近。
原因:Cortex-M 的 PendSV 必须是最低优先级。如果在移植时没有设,或者设成了比某个外设中断更高的优先级,外设中断就可能打断 PendSV,线程切换时保存寄存器到一半又被掐断,栈就乱了。
解决:在 rt_hw_board_init 里显式设置 PendSV 的优先级为最低,Cortex-M 上数值越大优先级越低,给 0xFF 让内核把高四位保留即可。同时检查是否有别的地方覆盖了这个设置,比如外设库初始化代码里顺手改了 PendSV 优先级。一些芯片的启动汇编会在 reset 阶段设置默认优先级,如果你的脚本代码里也设过一次,两者要以最终覆盖的一次为准。
5.3 线程栈给太小:随机 HardFault 的典型来源
现象:板子刚上电能跑几十秒,一旦某个线程走了复杂的执行路径,系统就 HardFault,复位后又能跑一会儿。故障出现时间和线程执行次数正相关,而不是固定位置。
原因:线程栈空间不够。Nano 在创建线程时不会一次性把所有栈都检查完,栈溢出通常发生在函数调用层数最深的那一刻,也就是局部变量最多、嵌套最深时。512 字节的栈放一个 printf 加两个局部结构体,很容易直接盖到相邻内存。
解决:先把疑似线程的栈翻倍,比如 512 改到 1024,观察是否稳定。稳定后如果还想省内存,再逐步往回降,每次只降 128 字节,每档跑半天压测。同时打开 RT_USING_STACK_CHECK 或 RT_USING_HOOK,Nano 内核会在线程切换时检查栈底标记,溢出时能给出一个更明显的断言线索。
5.4 动态创建全部返回空:先查堆开关和大小
现象:rt_thread_create 返回 RT_NULL,线程没启动,但编译链接都正常。有的工程连 rt_sem_create 也返回空。
原因:最常见的是 RT_USING_HEAP 没有在 rtconfig.h 里打开,或者 RT_HEAP_SIZE 太小,堆在创建几个线程之外已经耗尽。另一个隐蔽原因是 rt_system_heap_init 没有被调用,或调用时传了错误的地址范围,导致堆管理器的可用区域为 0。
解决:先确认 rtconfig.h 里 RT_USING_HEAP 已定义。再确认 rt_hw_board_init 里 rt_system_heap_init 的参数是数组首地址和末地址。最后在失败返回的地方临时加打印,输出当前堆的剩余空间。Nano 内核有 rt_memory_usage 或其他内存信息接口可以用,但没有统一的调试打印,最简单的是把堆数组尺寸先临时给大,比如 16384,排除容量问题后再回头压缩。
5.5 中断里动用了不该用的 API:系统直接卡死
现象:串口接收中断里调用了 rt_sem_release,线程被唤醒后表现正常,但偶尔整个系统卡住;或者中断里调用了 rt_kprintf 打印调试信息,打印几次后线程全都不跑了。
原因:中断上下文和线程上下文是两套运行环境。Nano 的中断会走 rt_interrupt_enter/leave 的嵌套路径,在中断里调用阻塞型 API、或者调用可能触发调度的函数时,调度时机和中断嵌套会产生冲突。rt_kprintf 这种也可能在中断里被外设状态卡住,尤其串口驱动本身不被 RT-Thread 接管时。
解决:中断里只做两类事:一是通过 rt_sem_release、rt_mb_send 这类明确支持中断上下文调用的接口发信号或发数据;二是把裸机数据写进一个共享缓冲,回到线程再处理。任何可能阻塞的等待都不要出现在中断里。如果必须在中断里快速通知高优先级线程,也只通过信号量“踢一下”,主体逻辑放到线程中执行。这条是我调 RTOS 时被坑得最狠的一条,排查了整整两天才发现是中断里多打了一行日志。
6. 进阶一招:用空闲线程钩子测 CPU 占用率
没有 Trace 工具的时候,怎么判断系统还剩多少余量?Nano 在 3.1.3 里已经支持钩子功能,其中最有用的一个就是空闲线程钩子。空闲线程是系统中优先级最低、没有其他线程可运行时才执行的线程,统计它的执行频率,就能反推 CPU 被业务占用的大致比例。
/* 打开 rtconfig.h 里的 RT_USING_HOOK 和 RT_USING_IDLE_HOOK */ #include "rtthread.h" static volatile rt_uint32_t idle_cnt; /* 空闲钩子执行次数 */ static rt_uint32_t idle_ref; /* 全空载基准值,启动早期采样 */ /* 空闲线程每转一圈,回调一次 */ static void idle_hook(void) { idle_cnt++; } /* 统计线程:每 1 秒算一次 CPU 占用率 */ static void stat_entry(void *param) { rt_uint32_t last_cnt = idle_cnt; rt_tick_t last_tick = rt_tick_get(); while (1) { rt_thread_mdelay(1000); /* 统计窗口 1 秒 */ rt_uint32_t delta = idle_cnt - last_cnt; rt_uint32_t busy; if (delta >= idle_ref) busy = 0; /* 空闲执行比基准还多,说明负载很轻 */ else busy = 100 - delta * 100 / idle_ref; rt_kprintf("busy=%d%%\n", busy); last_cnt = idle_cnt; } } /* 在 rt_application_init 里创建这个统计线程 */ /* 空闲线程钩子的注册要在调度器启动后完成,这里示范注册方法 */idle_ref 的采样很关键。我通常会在系统启动早期、业务线程还没开始干活时,先跑一个 1 秒窗口,把那个窗口里 idle_cnt 的增量记下来作为“全空载基准”。之后每个窗口用同样的时间长度去比,业务越忙,空闲钩子执行的次数越少,busy 越接近 100。注意空闲钩子运行在空闲线程上下文里,不能调用 rt_thread_mdelay、不能申请内存,只能做轻量计数,否则会把系统拖死。
这个技巧帮我解决过不少玄学问题。曾经有个只有“看起来正常、偶尔反应慢”的现场,用钩子一测,实际 busy 常年 95% 以上,一直在临界边缘跑,后来把两个高负载线程的周期错开,占用率立刻降下来。我现在的习惯是任何新板上跑 Nano,第一件事就是把这套统计代码做进去,先摸清余量再开发业务,而不是等出问题了再回去猜。希望帮到你。
本文还有配套的精品资源,点击获取