文章目录
1. RT-Thread Nano简单介绍
RT-Thread Nano 是国内开源的一个实时内核库,RAM与ROM占用极小,而且该有的内核功能如:线程管理、线程同步与通信、时钟管理、中断管理、内存管理等功能一个不少。
下图是 RT-Thread Nano 的软件框架:
Finsh组件是 RT-Thread Nano 的控制台命令系统,可实现用户命令交互。
RT-Thread Nano 它与标准版本的区别就是它只留下实时内核相关的代码,去除了标准版的 device 框架和各种组件,还有不使用 Scons 构建系统,也没有了 Kconfing 已经 ENV 配置工具,所以代码会更简单。
2. 移植准备工作
-
获取 RT-Thread Nano 源码。可到点击下面这个网址下载。
https://github.com/RT-Thread/rtthread-nano/archive/refs/heads/master.zip
-
准备可以正常运行的裸机工程源码。我使用的是 STM32F407ZGT6 芯片,裸机工程源码可以使用 CubeMX 生成。
3. 移植哪些文件
移植过程主要分为两个部分:libcpu 移植与板级移植。
实际上对于不同架构要移植的代码,RT-Thread 官方也已经帮我们把常见的CPU架构相关的代码写好了,我们到 libcpu
目录下找到对应自己芯片架构的代码即可。所以真正要我们移植提供的代码就只是板级硬件初始化相关的代码。
-
libcpu 移植
libcpu 向下提供了一套统一的 CPU 架构移植接口,是针对CPU架构(比如ARM,RISC-V)的移植。这部分接口包含了全局中断开关函数、线程上下文切换函数、时钟节拍的配置和中断函数、Cache 等等内容,RT-Thread 支持的 cpu 架构在源码的
libcpu
文件夹下。 -
板级移植
板级移植主要是针对
rt_hw_board_init()
函数内容的实现,该函数在板级配置文件 board.c 中,函数中做了许多系统启动必要的工作,其中包含:- 配置系统时钟。
- 实现 OS 节拍。(其中步骤 1 和 2 为 3.1.5 版本中
#error TODO 1
的部分:#error "TODO 1: OS Tick Configuration."
) - 初始化外设:如 GPIO/UART 等等,若需要请在此处调用。
- 初始化系统内存堆,实现动态堆内存管理。
- 板级自动初始化,使用 INIT_BOARD_EXPORT() 自动初始化的函数会在此处被初始化。
- 其他必要的初始化,如 MMU 配置(需要时请自行在 rt_hw_board_init 函数中调用应用函数实现)。
4. 动手移植
4.1 添加 Nano 源码到keil工程
1、把 RT-Thread Nano 下面目录的源码复制到我们准备好的裸机工程目录 rtthread_nano 下。
- Nano 源码中的 include、libcpu、src 文件夹。
- 配置文件:源码代码 rtthread/bsp 文件夹中的两个文件:
board.c
与rtconfig.h
。
2、使用 keil 打开我们事先准备好的裸机工程,新建 rtthread_nano 分组,并在该分组下添加以下源码:
-
添加工程下 rtthread/src/ 文件夹中所有文件到工程;
-
添加工程下 rtthread/libcpu/ 文件夹中相应内核的 CPU 移植文件及上下文切换文件:
cpuport.c
以及context_rvds.S
;注意:该目录下选择我们对应芯片架构的文件目录,比如我使用的是 STM32F407 ,那么就选择 Cortex-M4 架构里面的文件。
-
添加 rtthread/ 文件夹下的
board.c
。
4.2 添加头文件路径
因为源码目录只有两个目录(include和bsp目录)下有头文件,只添加这两个头文件路径即可。
4.3 解决编译报错
添加文件和头文件路径后,编译报如下错误:
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
文件定义的这两个中断函数(如果该文件没有定义这两个文件,则不用管)。
4.4 配置系统时钟和外设初始化
在 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 提供的这个函数。
4.5 内存堆初始化配置
系统内存堆的初始化在 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空间的最大地址
5. 移植验证
当 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 正常运行起来了。