RT-Thread Nano 是国内开源的一个实时内核库,RAM与ROM占用极小,而且该有的内核功能如:线程管理、线程同步与通信、时钟管理、中断管理、内存管理等功能一个不少。
下图是 RT-Thread Nano 的软件框架:

Finsh组件是 RT-Thread Nano 的控制台命令系统,可实现用户命令交互。
RT-Thread Nano 它与标准版本的区别就是它只留下实时内核相关的代码,去除了标准版的 device 框架和各种组件,还有不使用 Scons 构建系统,也没有了 Kconfing 已经 ENV 配置工具,所以代码会更简单。
获取 RT-Thread Nano 源码。可到点击下面这个网址下载。
https://github.com/RT-Thread/rtthread-nano/archive/refs/heads/master.zip
准备可以正常运行的裸机工程源码。我使用的是 STM32F407ZGT6 芯片,裸机工程源码可以使用 CubeMX 生成。
移植过程主要分为两个部分:libcpu 移植与板级移植。
实际上对于不同架构要移植的代码,RT-Thread 官方也已经帮我们把常见的CPU架构相关的代码写好了,我们到 libcpu 目录下找到对应自己芯片架构的代码即可。所以真正要我们移植提供的代码就只是板级硬件初始化相关的代码。
libcpu 移植
libcpu 向下提供了一套统一的 CPU 架构移植接口,是针对CPU架构(比如ARM,RISC-V)的移植。这部分接口包含了全局中断开关函数、线程上下文切换函数、时钟节拍的配置和中断函数、Cache 等等内容,RT-Thread 支持的 cpu 架构在源码的 libcpu 文件夹下。
板级移植
板级移植主要是针对 rt_hw_board_init() 函数内容的实现,该函数在板级配置文件 board.c 中,函数中做了许多系统启动必要的工作,其中包含:
#error TODO 1 的部分:#error "TODO 1: OS Tick Configuration.")1、把 RT-Thread Nano 下面目录的源码复制到我们准备好的裸机工程目录 rtthread_nano 下。
board.c 与 rtconfig.h。
2、使用 keil 打开我们事先准备好的裸机工程,新建 rtthread_nano 分组,并在该分组下添加以下源码:
添加工程下 rtthread/src/ 文件夹中所有文件到工程;
添加工程下 rtthread/libcpu/ 文件夹中相应内核的 CPU 移植文件及上下文切换文件: cpuport.c 以及 context_rvds.S;
注意:该目录下选择我们对应芯片架构的文件目录,比如我使用的是 STM32F407 ,那么就选择 Cortex-M4 架构里面的文件。
添加 rtthread/ 文件夹下的 board.c 。

因为源码目录只有两个目录(include和bsp目录)下有头文件,只添加这两个头文件路径即可。

添加文件和头文件路径后,编译报如下错误:
STM32F407_FreeRTOS_Template\STM32F407_FreeRTOS_Template.axf: Error: L6200E: Symbol HardFault_Handler multiply defined (by context_rvds.o and stm32f4xx_it.o).
报错说重复定义了。实际上 RT-Thread 接管异常处理函数 HardFault_Handler() 和悬挂处理函数 PendSV_Handler(),这两个函数已由 RT-Thread 实现,所以我们删除 stm32f4xx_it.c 文件定义的这两个中断函数(如果该文件没有定义这两个文件,则不用管)。
在 RT-Thread 启动的入口函数 rtthread_startup(void) 中,会调用一个初始化板级硬件的函数 rt_hw_board_init() ,这个函数就是需要我们在 board.c 中实现的板级初始化相关的代码。比如要完成系统的时钟配置,设备上要到的一些外设初始化等等,都要在这个函数里面实现。
我使用的是 CubeMX 生成的工程,已经生成了系统时钟的配置函数了,函数复制到 rt_hw_board_init() 调用即可,而且还用到了GPIOB和UART外设,所以初始化函数也一起放到这个函数里面调用。如下代码:
/**
* This function will initial your board.
*/
void rt_hw_board_init()
{
HAL_Init(); // HAL库初始化
SystemClock_Config(); // 系统时钟配置
MX_GPIO_Init(); // GPIO外设初始化
MX_USART1_UART_Init(); // USART外设初始化
/* System Clock Update */
SystemCoreClockUpdate();
/* System Tick Configuration */
_SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);
/* Call components board initial (use INIT_BOARD_EXPORT()) */
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
rt_system_heap_init(rt_heap_begin_get(), rt_heap_end_get());
#endif
}
另外,board.c 文件中已经实现了系统嘀嗒中断服务函数,为 RT-Thread 提供心跳。
void SysTick_Handler(void)
{
/* enter interrupt */
rt_interrupt_enter();
rt_tick_increase();
/* leave interrupt */
rt_interrupt_leave();
}
如果编译报错重复定义了这个函数的话,需要删除原来实现的 SysTick_Handler 中断服务程序,保留RT-Thread Nano 提供的这个函数。
系统内存堆的初始化在 board.c 中的 rt_hw_board_init() 函数中完成,内存堆功能是否使用取决于宏 RT_USING_HEAP 是否开启,RT-Thread Nano 是默认不开启内存堆功能。
当开启了内存堆功能之后,就可以使用动态内存分配了,也可以动态创建线程等等。RT-Thread Nano 实现了一套动态内存分配接口函数,如 rt_malloc、rt_free 等。
我们要使用内存堆功能,只需要在 rtconfig.h 文件中定义这个 RT_USING_HEAP 宏即可,然后内存堆初始化函数就会在 rt_hw_board_init 中被调用。

内存堆的默认设置大小,使用的是一个数组给定的一个固定大小了的。如下代码:
#if defined(RT_USING_USER_MAIN) && defined(RT_USING_HEAP)
#define RT_HEAP_SIZE 1024
static uint32_t rt_heap[RT_HEAP_SIZE]; // heap default size: 4K(1024 * 4)
RT_WEAK void *rt_heap_begin_get(void)
{
return rt_heap;
}
RT_WEAK void *rt_heap_end_get(void)
{
return rt_heap + RT_HEAP_SIZE;
}
#endif
上面代码实际上只是定义了1024字节大小的数组作为内存堆,但是我们可以通过散列文件或者链接脚本得知还剩下多少内存空间,可以作为内存堆使用。
获取内存堆最大可用RAM大小的方法:
实际上 ZI数据段 之后的所有内存空间,都可以作为内存堆使用,这样就这样设置最大的内存堆大小了。关于ZI数据段的结束地址,我们可以通过链接脚本来获取。对于 keil 来说,代码如下:
#define STM32_SRAM1_START (0x20000000)
#define STM32_SRAM1_END (STM32_SRAM1_START + 20 * 1024) // 结束地址 = 0x20000000(基址) + 20K(RAM大小)
#if defined(__CC_ARM) || defined(__CLANG_ARM) // 编译器判断
// RW_IRAM1就是 keil 散列文件定义的RAM空间大小,ZI就是获取该数据段的结束地址。从ZI段之后的所有内存空间都可以作为内存堆使用
extern int Image$$RW_IRAM1$$ZI$$Limit;
#define HEAP_BEGIN ((void *)&Image$$RW_IRAM1$$ZI$$Limit) // 获取到 ZI 数据段的结束地址,作为内存堆的起始地址
#endif
#define HEAP_END STM32_SRAM1_END // 内存堆结束地址就是芯片可以RAM空间的最大地址
当 RT-Thread Nano 启动起来之后,会创建一个 main 线程的,我们在main函数中添加实验代码。如下:
int main(void)
{
while (1)
{
rt_thread_mdelay(1000);
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_4);
HAL_UART_Transmit(&huart1, "hello world.\r\n", strlen("hello world.\r\n"), 10);
}
}
上面 main 函数里面没没有调用系统时钟配置,外设初始化等函数。这是因为在调用 main 函数之前,就先调用了 rt_hw_board_init() 函数,在这个函数里面配置了系统时钟、板级外设的初始化等工作了。而 main 函数现在其实是 RT-Thread Nano 创建的一个main线程函数了,它参与线程的调度过程。
上面代码就只是让LED闪烁,和向串口输出一行字符串功能。打开串口终端,可以看到打印出的字符串如下:

说明 RT-Thread Nano 正常运行起来了。