RT-Thread Nano移植与学习
此为本人初学RT-Thread的笔记,可以参考,如有错误,可以指正,谢谢。
RT-Thread简介
RT-Thread介绍
RT-Thread(全称 Real Time-Thread)是一款由国内团队开发的开源嵌入式实时操作系统。它的核心特点可以概括为:
- 不只是内核,更是一个生态:这是它和FreeRTOS最核心的区别。除了实时内核,它内置了丰富的中间件,比如FinSH命令行(你可以用串口输入命令调试系统)、虚拟文件系统、轻量级TCP/IP网络协议栈(LwIP),甚至图形用户界面(GUI)组件。这意味着用RTT开发复杂项目时,很多功能都"开箱即用"。
- 高度可裁剪:它采用模块化设计。针对资源受限的MCU,可以裁剪出只占用3KB Flash和1.2KB RAM的NANO版本;对于资源丰富的设备,又能像搭积木一样,从软件包中心添加各种功能组件,比如MQTT、加密库、传感器驱动等。
- 活跃的中文社区:RTT在国内有非常活跃的开发者社区和丰富的中文文档,对国内开发者很友好,遇到问题更容易找到解决方案。
RT-Thread版本
RT-Thread主要提供了三个版本,以适应从资源受限的MCU到需要复杂应用的高性能芯片等不同开发场景
| 版本 | 核心定位与特点 | 资源占用 | 适用场景 |
|---|---|---|---|
| Nano 版本 | 极简硬实时内核,只包含最基础的线程管理、同步通信等功能,没有设备驱动框架和组件。 | 极小(~3KB Flash, ~1.2KB RAM)。 | 资源极度受限的MCU,如STM32F0系列等入门级32位ARM芯片,常用于简单的家电、传感器节点等。 |
| 标准版本 | 功能完整的IoT OS平台,在Nano内核基础上集成了丰富的组件,如设备驱动框架、文件系统、网络协议栈(LwIP)以及强大的软件包生态。 | 根据配置的功能而定,通常在几十KB到几百KB的Flash和RAM。 | 资源丰富的IoT设备,需要联网、文件存储、图形界面等复杂功能的应用,是大多数开发者的首选。 |
| Smart 版本 | 混合操作系统,引入了用户态和内核态的隔离,支持独立的进程和地址空间(依赖MMU),应用和内核可以分开开发运行。 | 相比标准版更高,需要芯片支持MMU。 | 需要类似Linux开发模式的高性能应用,应用复杂、对安全性要求高,或需要从Linux平台移植代码的场景。常运行在ARM Cortex-A系列等应用处理器上。 |
FreeRTOS与RTT的差异:从 FreeRTOS 入门 RTT学习路径
有FreeRTOS基础再来学RT-Thread,其实思路会非常顺——很多内核概念(线程、信号量、消息队列)都是通用的。可以把RT-Thread看作一个功能更完整的"全家桶",而FreeRTOS更像一个轻量的"内核引擎"
| 对比维度 | FreeRTOS | RT-Thread |
|---|---|---|
| 定位 | 轻量级实时内核,专注任务调度与同步。 | 完整的IoT OS平台,集成了丰富的组件和服务。 |
| 核心功能 | 内核为主,网络、文件系统等需自行集成。 | 内核+FinSH控制台+设备驱动框架+文件系统+网络协议栈+UI等。 |
| 开发工具 | 依赖第三方IDE(如Keil、IAR)或配合VS Code等。 | 官方提供一站式IDE:RT-Thread Studio,集成了工程创建、配置、调试等功能 |
学习建议:三步走
-
第一步:抓核心差异,不要从头学线程、信号量这些概念。重点关注RTT特有或不同的地方,比如:
- FinSH控制台:这是RTT的特色。学会用它来调试系统,查看线程状态、内存使用,甚至执行自定义函数,对开发调试帮助很大。
- 设备驱动框架:RTT把各种外设(如UART、I2C、SPI)都抽象成了"设备"。学习如何通过统一的rt_device_xxx接口来访问硬件,而不是直接操作寄存器。
- 临界区保护机制:RTT和FreeRTOS在关中断的实现上略有不同(RTT操作PRIMASK寄存器,FreeRTOS操作BASEPRI寄存器),可以对比了解一下,以防踩坑。
-
第二步:上手实战,用项目驱动学习:找一块开发板(官方推荐STM32系列),直接用RT-Thread Studio创建一个带FinSH的工程。从一个简单的点灯任务开始,尝试创建一个线程,然后在FinSH里查看它。再试着用设备框架读取一个传感器(比如I2C的温湿度传感器),感受一下设备驱动的用法。
-
第三步:善用资源,加入社区:
- 官方文档中心:这是最权威、最全面的学习资料,内容覆盖了内核、组件、工具链的方方面面。
- 参考书籍:如果想系统学习,可以看《嵌入式实时操作系统RT-Thread原理与应用》这类书籍,它通常会结合具体硬件(如STM32)进行项目式讲解。
- 软件包生态:在RTT的软件包中心找一些你感兴趣的项目,比如MQTT通信、或者一个GUI Demo,看看别人是怎么组织代码和配置系统的,这是快速提升的捷径。
RTT Nano版本移植到STM32
1.首先需要有一个STM32工程(空工程就行,无论什么库),此处以STM32CubeMx+Keil工程移植。
2.下载RT-Thread源码库,官网:rt-thread.org/download.html

3.解压,然后在工程那里新建一个OS文件夹,把解压的RTT文件夹复制到OS文件夹


bsp -->示例代码和配置文件
components -->组件
docs -->文档
include -->头文件
libcpu -->CPU移植文件
src -->Nano内核源码
4.然后删除不需要的文件
rt-thread/bsp文件夹只保留两个文件,其余全部删除

rt-thread/libcpu根据芯片选择对应文件,其余删除,包括risc-v文件夹,STM32F103C8T6 的核心是ARM架构 Cortex-M32

进入cortex-m4文件,这里只需要context_rvds.s和cpuport.c文件(因为是keil,可以根据你自己的编译环境选择保留),其余文件删除
| 文件 | 一句话作用 |
|---|---|
| context_gcc.S | 为 GCC 编译器提供线程切换、开关中断的汇编代码。 |
| context_iar.S | 为 IAR 编译器提供同样的功能,只是汇编语法不同。 |
| context_rvds.S | 为 Keil MDK(ARM Compiler)提供同样的功能。 |
| cpuport.c | 用 C 语言实现线程栈初始化和 HardFault 异常捕获等核心功能。 |
简单说,你用的什么编译器(Keil/IAR/GCC),就把对应的 context_xxx.S 文件加入工程,cpuport.c 则是通用的、必须添加的。

然后把这些的文件也删除

5.打开工程,做好工程基础配置,然后建立分组
RTT/src添加src目录下全部的.c文件

添加bsp目录下的board.c和rtconfig.h文件到App中(注意选择All Files才能看到文件),因为这两个文件属于配置文件和应用层开发的文件,我们会对这两个文件进行修改
RTT/ports添加这两个文件,rt-thread\libcpu\arm\cortex-m3

RTTt/finsh添加rt-thread\components\finsh的全部文件

6.添加工程路径,把所添加文件的路径全部包含即可

7.编译,会报错。此时我们先不急,先添加一个头文件。然后编译错误会少很多

8.注释两个中断服务函数(RTT内部已实现)void HardFault_Handler(void)和void PendSV_Handler(void),后面重新生成代码也要注释。


9.此时还有报错,找到borad.c文件,可以看到报错的地方,我们把报错的内容删除掉,随后添加 SysTick_Config( SystemCoreClock / RT_TICK_PER_SECOND ); 和在文件开头添加 #include "stm32f1xx.h",根据自己的芯片选择头文件

再次编译发现0报错和0警告
10.给RTT添加心跳,时基。这个很关键。
打开stm32f1xx_it.c文件,找到SysTick_Handler函数,在函数内部添加rt_os_tick_callback();之后编译即可

FreeRTOS需要占用systick,芯片时基需要选择其他定时器。但是RTT也这样做的话,我发现时基会有所不对,rt_os_tick_callback();要与系统systick一起调用的中断才准确。也就是HAL的时基与RTT的时基需要一起。

RT-Thread的时钟频率则通常由rtconfig.h中的RT_TICK_PER_SECOND决定,SysTick的中断处理函数直接调用rt_tick_increase(),逻辑更直接
RT-Thread 和 FreeRTOS 对 SysTick 的使用方式不同:
| 特性 | FreeRTOS | RT-Thread |
|---|---|---|
| SysTick 所有权 | 独占 | 可共享 |
| HAL 时基 | 必须改用其他定时器 | 可以和 RTT 共用 SysTick |
| SysTick_Handler | 只调用 xPortSysTickHandler() | 可以同时调用 HAL_IncTick() + rt_os_tick_callback() |
FreeRTOS 的 xPortSysTickHandler() 会进行任务切换,如果在中断里同时调用 HAL_IncTick(),可能导致:
- 中断嵌套冲突
- 时间基准混乱
- 任务切换时栈指针错乱
所以 FreeRTOS 要求 HAL 库使用其他定时器(如 TIM1)作为时基源。
RT-Thread 的 rt_os_tick_callback() 只做节拍计数(rt_tick_increase()),不进行任务切换。任务切换是在 PendSV_Handler 中完成的(优先级最低)。
所以:
- SysTick_Handler:只更新系统节拍计数,非常轻量
- PendSV_Handler:执行实际的任务上下文切换
SysTick 中断里可以安全地调用 HAL_IncTick(),不会影响 RTT 的调度。
11.创建一个简单的线程并运行(记得配置对应引脚使用)
//线程任务函数
void led_task(void*param)
{
rt_tick_t tick = rt_tick_from_millisecond(500);//转换对应节拍数
while(1)
{
HAL_GPIO_TogglePin(GPIOA,GPIO_PIN_1);
rt_thread_mdelay(tick);//延时,
}
}
void led_task2(void*param)
{
rt_tick_t tick = rt_tick_from_millisecond(500);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_SET);
rt_thread_mdelay(tick);
while(1)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8);
rt_thread_mdelay(tick);
}
}
//主函数入口
void main()
{
/* 创建LED闪烁线程 */
rt_thread_t led_thread = rt_thread_create(
"led", // 线程名称
led_task, // 线程入口函数
RT_NULL, // 入口函数参数
1024, // 线程栈大小
5, // 线程优先级(数字越小优先级越高,范围0~31)
25 // 时间片
);
rt_thread_t led_thread2 = rt_thread_create(
"led2", // 线程名称
led_task2, // 线程入口函数
RT_NULL, // 入口函数参数
1024, // 线程栈大小
5, // 线程优先级(数字越小优先级越高,范围0~31)
25 // 时间片
);
/* 如果创建成功,启动线程 */
if(led_thread != RT_NULL)
{
rt_thread_startup(led_thread);
}
if(led_thread != RT_NULL)
{
rt_thread_startup(led_thread2);
}
while(1)//会执行到这里,这里的优先级不定义的话默认是10,如果不让出cpu那么低于其优先级的任务将无法得到执行
{
rt_thread_mdelay(10);
}
}

函数介绍:
rt_thread_t rt_thread_create(const char *name,
void (*entry)(void *parameter),
void *parameter,
rt_uint32_t stack_size,
rt_uint8_t priority,
rt_uint32_t tick);
| 参数 | 你的值 | 含义 | 建议 |
|---|---|---|---|
| name | "led" | 线程名称 | 起个有意义的名字 |
| entry | led_task | 入口函数 | 必须有 while(1) + 让出CPU |
| parameter | RT_NULL | 传给入口函数的参数 | 需要传参时用地址 |
| stack_size | 1024 | 栈大小(字节) | LED任务 512 足够,1024 更安全 |
| priority | 0 | 优先级(0最高) | 改为 10 或 15(不建议用0) |
| tick | 20 | 时间片(节拍数) | 默认 10~20 即可 |
| 特性 | rt_thread_delay(tick) | rt_thread_mdelay(ms) |
|---|---|---|
| 单位 | 系统时钟节拍(tick) | 毫秒(ms) |
| 本质 | 底层实现函数 | 对 rt_thread_delay 的封装 |
| 效果 | 让出CPU,延时指定节拍数 | 让出CPU,延时指定毫秒数 |
基础移植已完成。
增加rt_kprintf功能
一个串口打印组件,底层需要配置实际的串口,我这里配置USART1:PA9,PA10引脚
1、打开RT_USING_CONSOLE宏,可以使用图形化配置
找到rtconfig.h文件,打开RT_USING_CONSOLE宏,随后编译

找到board.c文件,删除相关的报错内容,其中uart_init是我们rt_kprintf打印功能初始化的部分,这里我们用USART1进行打印,只需要调用串口 初始化函数即可。rt_kprintf函数会调用rt_hw_console_output进行数据输出,这里我们在这个函数内部进行串口1输出即可。

写代码:
static int uart_init(void)
{
return 0;
}
INIT_BOARD_EXPORT(uart_init);
void rt_hw_console_output(const char *str)
{
rt_enter_critical();
while (*str != '\0')
{
// 处理换行:\n -> \r\n
if (*str == '\n')
{
uint8_t ch = '\r';
// 发送回车符
HAL_UART_Transmit(&huart1, &ch, 1, 100);
}
// 发送当前字符
HAL_UART_Transmit(&huart1, (uint8_t*)str, 1, 100);
str++; // 移动到下一个字符
}
rt_exit_critical();
}
调用
void led_task2(void*param)
{
rt_tick_t tick = rt_tick_from_millisecond(100);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_8,GPIO_PIN_SET);
rt_thread_mdelay(tick);
static uint32_t led_togggle_num=0;
while(1)
{
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_8);
led_togggle_num++;
rt_kprintf("led_togggle_num=%d\n",led_togggle_num);//打印LED翻转次数
rt_thread_mdelay(tick);
}
}

增加FinSH功能
RT-Thread 的 FinSH 功能就是建立在这个基础上的交互式命令行组件(Shell)。它让你的设备能通过串口接受命令并执行,是开发和调试的利器
1、添加finsh宏定义
打开rtconfig.h文件,在开头添加如下宏定义:
// 控制台相关
#define RT_USING_CONSOLE // 启用控制台
#define RT_CONSOLE_DEVICE_NAME "uart1" // 使用串口1
//#define RT_CONSOLEBUF_SIZE 128 // 控制台缓冲区大小
// FinSH 相关
#define RT_USING_FINSH // 启用 FinSH
#define FINSH_USING_MSH // 使用 msh 模式
#define FINSH_THREAD_NAME "tshell" // FinSH 线程名称
#define FINSH_USING_HISTORY // 启用命令历史
#define FINSH_HISTORY_LINES 5 // 历史命令行数
#define FINSH_USING_SYMTAB // 启用符号表
#define FINSH_CMD_SIZE 80 // 命令缓冲区大小
#define FINSH_THREAD_PRIORITY 20 // FinSH 线程优先级
#define FINSH_THREAD_STACK_SIZE 4096 // FinSH 线程栈大小
#define RT_MAIN_THREAD_PRIORITY 25 //main函数线程的优先级
记得添加finsh文件的路径
2.增加rt_hw_console_getchar函数,使用串口1进行输入操作
//ch=-1似乎会导致异常,可以把-1改为0试试
//这其实是一个
char rt_hw_console_getchar(void)
{
char ch = -1;//-1似乎会导致异常,可以把-1改为0试试
uint8_t rx_data = 0;
int level = rt_hw_interrupt_disable();
// 检查是否有数据可读
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET)
{
rx_data = (uint8_t)(huart1.Instance->DR & 0xFF);
ch = (char)rx_data;
__HAL_UART_CLEAR_FLAG(&huart1, UART_FLAG_RXNE);
} else
{
// 检查溢出错误
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_ORE) != RESET)
{
__HAL_UART_CLEAR_OREFLAG(&huart1);
}
}
rt_hw_interrupt_enable(level);
// 没有数据时让出CPU
if (ch == -1) //-1似乎会导致异常,可以把-1改为0试试
{
rt_thread_mdelay(1);
}
return ch;
}
3.添加串口初始化


功能正常,可以使用命令

基本的移植与使用就到这里了。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)