CNXSoft:这是RT-Thread(即开源物联网操作系统)的特邀帖子,以GD32V RISC-V MCU开发板作为示例,解释了如何在其实时操作系统上创建你的第一个运行程序。
本文介绍了如何将RT-Thread Nano移植到RISC-V架构,使用Eclipse IDE、GCC工具链和Gigadevice GD32V103 MCU构成一个基本项目。
前言
RT-Thread是一个开源的嵌入式实时操作系统,它具有标准版本和Nano版本。标准版本包括内核层、组件和服务层以及IoT框架层,而Nano版本则具有非常小的占用空间和完善的硬实时内核,更适合于资源受限的MCU(微处理器单元)。
移植Nano的主要步骤如下:
- 准备一个基本的Eclipse项目并获取RT-Thread Nano源代码。
- 将RT-Thread Nano源代码添加到基础项目中,并添加相应的标头路径。
- 修改Nano,主要用于中断时钟、内存和应用程序,以实现移植。
- 最后,为你的应用程序配置Nano OS。Nano是可定制的,可以通过h文件针对系统进行定制。
准备工作
下载RT-Thread Nano源代码:
1 |
git clone https://github.com/RT-Thread/rtthread-nano |
在Eclipse中创建一个基本的裸机项目,例如一个闪烁的LED示例代码。
准备一个基础项目
在我们移植RT-Thread Nano之前,我们需要创建一个可工作的裸机项目。作为示例,本文使用基于GD32V103的闪烁LED程序。该程序的主要例程如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
int main(void) { gd_eval_led_init(LED1); while (1) { gd_eval_led_on(LED1); delay_1ms(1000); gd_eval_led_off(LED1); delay_1ms(1000); } } |
在我们的程序中,主要完成了系统初始化和LED闪烁功能。在编译下载程序后,可以看到开发板上的LED闪烁。你可以使用所需的芯片来完成类似的裸机项目。
将RT-Thread Nano添加到项目
添加Nano源文件
在Eclipse项目下创建一个新的rtthread文件夹,并将以下文件夹和文件添加到该文件夹中:
- Nano源代码中的include、libcpu和src文件夹。请注意,libcpu仅保留与芯片架构相关的文件,例如示例中使用的bumblebee和common。
- rtthread-nano / bsp中的配置文件:c和rtconfig.h。

重新打开Eclipse工作区,导入项目,并将rtthread文件夹加载到项目中:

RISC-V芯片内核代码:context_gcc.s和cpuport.c
内核文件包括:clock.c,components.c,device.c,idle.c,ipc.c,irq.c,kservice.c,mem.c,object.c,scheduler.c,thread.c,time.c
开发板配置代码和配置文件:board.c、rtconfig.h
添加头文件路径
右键单击项目,点击属性进入如下图所示界面,点击C / C ++ Build-> Settings,分别添加汇编程序和C头路径:将路径添加到rtconfig.h头文件的位置,将头文件路径添加到include文件夹下。然后点击C / C ++ General->路径和符号,添加相应的头文件,最后点击Apply按钮。

安装RT-Thread Nano
修改start.S
修改启动文件,使用启动函数rt_thread_startup()调用entry()函数从而实现引导RT-Thread的代码。启动文件start.S需要修改,以便在启动时跳转到entry()函数执行,而不是跳转到main(),从而启动RT-Thread。
1 2 3 4 5 |
/* How RT-Thread starts under GCC*/ int entry(void){ rtthread_startup(); return 0; } |
中断和异常处理
在系统没有实现此功能时RT-Thread提供了一个中断管理接口,该物理中断与用户的中断服务程序相关联。中断管理接口用于管理中断,以便在发生中断时可以触发相应的中断并执行中断服务程序。
该例程中的GD32VF103芯片在启动文件中提供了一个中断向量,用户可以使用中断向量提供的功能直接能实现相应的IRQ(中断请求)。触发中断时,处理器直接确定触发了哪个中断源,然后直接跳转到适当的固定位置进行处理,从而就无需自己再实施中断管理。
系统时钟配置
系统时钟配置(为MCU内核和外围设备提供工作时钟)和OS Tick配置(操作系统的心跳)在board.c文件中。
配置示例如下图所示,riscv_clock_init()配置系统时钟,和ostick_config()系统滴答。

riscv_clock_init() 配置系统时钟,如下所示:
1 2 3 4 5 6 7 8 9 |
void riscv_clock_init(void) { SystemInit(); /* ECLIC init */ eclic_init(ECLIC_NUM_INTERRUPTS); eclic_mode_enable(); set_csr(mstatus, MSTATUS_MIE); } |
OS Tick是使用硬件计时器实现的,需要用户在board.c中实现硬件计时器的中断服务例程eclic_mtip_handler(),并调用RT-Thread rt_tick_increase()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/* This is the timer interrupt service routine. */ void eclic_mtip_handler(void) { /* clear value */ *(rt_uint64_t *)(TMR_CTRL_ADDR + TMR_MTIME) = 0; /* enter interrupt */ rt_interrupt_enter(); /* tick increase */ rt_tick_increase(); /* leave interrupt */ rt_interrupt_leave(); } |
由于eclic_mtip_handler()中断服务例程是由用户在board.c中重新实现的,并调用一个OS/系统tick,所以需要删除预定义的eclic_mtip_handler,以避免在编译时重复定义。如果项目成功编译且没有错误,则不需要进一步的修改。
内存堆初始化
系统内存堆的初始化是在board.c中的rt_hw_board_init() 函数中完成的。是否使用内存堆功能取决于宏RT_USING_HEAP是否开启了,并且在默认情况下RT-Thread Nano不启用内存堆功能。这样可以保持较小的尺寸,而不会为内存堆打开空间。
启用系统堆时允许你使用动态内存功能,例如使用rt_malloc、rt_free和各种其他函数的API来动态创建对象。如果你需要使用系统内存堆功能,你可以打开RT_USING_HEAP的宏定义,其中内存堆初始化函数rt_system_heap_init()将被调用,如下所示:

初始化内存堆需要堆的开始和结束地址。数组的大小可以手动更改,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#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 |
注意:打开堆动态内存功能时,堆默认值是很小的。使用时需要更改,否则可能会出现请求内存或创建线程失败的情况,修改方法如下:
你可以直接修改阵列中定义的RT_HEAP_SIZE大小,至少要超过每个动态请求的内存大小总和,但要小于总的芯片RAM的大小。
编写您的第一个应用程序
将RT-Thread Nano移植到RISC-V之后,你就可以开始编写第一个应用程序代码了。此时,main()函数已转换为RT-Thread操作系统中的线程了。并且经过一些修改后,板载闪烁的LED示例可以在RISC-V系统上运行:
- 首先,在文件顶部添加RT-Thread的相关头文件<rtthread.h>
- 在main()函数中(即在主线程中)实现闪烁LED代码,从而来初始化LED引脚并在一个钟循环打开/关闭LED。
- 将延迟函数替换为RT-Thread提供的延迟函数:rt_thread_mdelay()。此功能使系统安排并切换到另一个线程上来运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <rtthread.h> int main(void) { gd_eval_led_init(LED1); while (1) { gd_eval_led_on(LED1); rt_thread_mdelay(500); gd_eval_led_off(LED1); rt_thread_mdelay(500); } } |
将编译后的代码下载到芯片后,你应该会看到基于RT-Thread的程序正在运行,并且LED再闪烁。注意:添加RT-Thread后,裸机中的main()函数将自动成为RT-Thread系统中主线程的入口函数。因为线程不应该总是独占着CPU,所以在main()中使用while(1)时,应在代码中添加 rt_thread_mdelay()函数,以便为其他线程腾出CPU时间。
与裸机闪烁LED示例代码的区别
- 延迟功能不同。RT-Thread提供了用于调度和延迟的rt_thread_mdelay()函数。在给定线程中调用该函数时,调度程序将切换到另一个线程。相反,裸机的延迟功能一直在CPU上运行。
- 系统时钟的初始化是不同的。将RT-Thread Nano移植到你的目标上(在我们的示例中为RISC-V)后,由于RT-Thread已经准备就绪,因此无需在main()中配置系统(例如HAL初始化、时钟初始化等)。在系统启动时配置系统时钟。上部分已经对“系统时钟配置”进行了说明。
配置RT-Thread Nano
你可以根据需要,配置相应的功能。如下所示打开或关闭rtconfig.h文件中的宏定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
... // <h>IPC(Inter-process communication) Configuration // <c1>Using Semaphore // <i>Using Semaphore #define RT_USING_SEMAPHORE // </c> // <c1>Using Mutex // <i>Using Mutex //#define RT_USING_MUTEX // Opening this macro enables the use of Mutex // </c> // <c1>Using Event // <i>Using Event //#define RT_USING_EVENT // Opening this macro enables the use of Event set // </c> // <c1>Using MailBox // <i>Using MailBox //#define RT_USING_MAILBOX //Opening this macro enables the use of Mailbox // </c> // <c1>Using Message Queue // <i>Using Message Queue //#define RT_USING_MESSAGEQUEUE // Opening this macro enables the use of Message Queue // </c> // </h> // <h>Memory Management Configuration // <c1>Using Memory Pool Management // <i>Using Memory Pool Management //#define RT_USING_MEMPOOL // Opening this macro enables the use of Memory Pool ... |
默认情况下,RT-Thread Nano不启用宏RT_USING_HEAP,因此仅支持静态创建任务和信号量。要动态创建对象,你则需要打开rtconfig.h文件中的RT_USING_HEAP宏定义。

文章翻译者:Jacob,嵌入式系统测试工程师、RAK高级工程师,物联网行业多年工作经验,熟悉嵌入式开发、测试各个环节,对不同产品有自己专业的分析与评估。