此为本人初学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​,集成了工程创建、配置、调试等功能

学习建议:三步走

  1. 第一步:抓核心差异,不要从头学线程、信号量这些概念。重点关注RTT特有或不同的地方,比如:

    • ​FinSH控制台​:这是RTT的特色。学会用它来调试系统,查看线程状态、内存使用,甚至执行自定义函数,对开发调试帮助很大。
    • ​设备驱动框架​:RTT把各种外设(如UART、I2C、SPI)都抽象成了"设备"。学习如何通过统一的rt_device_xxx​接口来访问硬件,而不是直接操作寄存器。
    • ​临界区保护机制​:RTT和FreeRTOS在关中断的实现上略有不同(RTT操作PRIMASK寄存器,FreeRTOS操作BASEPRI寄存器),可以对比了解一下,以防踩坑。
  2. ​第二步:上手实战,用项目驱动学习​:找一块开发板(官方推荐STM32系列),直接​用RT-Thread Studio创建一个带FinSH的工程​。从一个简单的点灯任务开始,尝试创建一个线程,然后在FinSH里查看它。再试着用设备框架读取一个传感器(比如I2C的温湿度传感器),感受一下设备驱动的用法。

  3. ​第三步:善用资源,加入社区​:

    • ​官方文档中心​:这是最权威、最全面的学习资料,内容覆盖了内核、组件、工具链的方方面面。
    • ​参考书籍​:如果想系统学习,可以看《嵌入式实时操作系统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.添加串口初始化

功能正常,可以使用命令

基本的移植与使用就到这里了。

Logo

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

更多推荐