1. 简介
最近几年可以发现国产的实时操作系统越来越受欢迎了,本篇要移植的就是当中的翘楚------RT-Thread。
RT-Thread诞生于2006年,是国内以开源中立、社区化发展起来的一款高可靠实时操作系统 ,由睿赛德科技负责开发维护和运营 。并且在上一年度的市场欢迎程度中位列第一,第一次超过了老牌的FreeRTOS系统。
相比于FreeRTOS,它的优势是强大的第三方和官方软件库,这意味着在项目开发中可以大大减少软件库移植的时间,提高了开发效率。不过根据我以往的项目经验,这些库还是有不少bug存在的,所以RT-Thread社区对于库的维护和更新还是要更加努力才行。
RT-Thread库的代码风格、软件逻辑可以说是完全模仿Linux的 ,如果各位本身就有Linux系统的开发经验,那么RT-Thread可以说是轻松上手,甚至可以通过学习RT-Thread来反向学习Linux系统的原理(我就是这样的),毕竟Linux的官方文档确实一言难尽。对于新手,官网提供了比较完善的开发文档,我个人认为算是非常详尽的了。
2. 移植
RT-Thread为了尽可能地兼容多的单片机,它分为标准版、nano版和smart版。标准版就是给系统资源比较充足的单片机移植的,可以使用社区里面的所有库;nano版是给资源较少的单片机移植的,只能使用部分的官方库;smart版是给物联网设备移植,这个分支比较少用。
本篇我们移植的是标准版,目前最新的发行版本是5.1.0,在Github上面可以下载RT-Thread源码。
2.1 导入文件
下载源码后解压,RT-Thread要导入的文件还是非常多的,先讲一下源代码里面关键文件夹的代码内容。
bsp目录主要存放各个芯片、开发板的外设驱动。
components目录主要存放一些系统部件,如HAL层驱动、C库支持、shell命令行支持等等。
documentation目录主要存放一些说明文档。
examples目录主要存放一些例程。
include目录主要是内核的头文件。
libcpu目录主要存放是是不同处理器底层的驱动。
src目录存放的是内核的源文件。
tools目录主要存放的是一些Python的工具脚本。
要导入的文件大致就是:
- src目录的大部分文件;
- libcpu目录中Arm Cortex-M4相关的文件,参考路径libcpu\arm\cortex-m4;
需要注意的是,如果使用AC5编译器,.S文件要导入context_rvds.S;如果使用AC6编译器,.S文件要导入context_gcc.S这个。
- bsp目录中GD32F470相关的外设驱动,参考路径bsp\gd32\arm\libraries\gd32_drivers;
- components目录中外设HAL层驱动相关文件,参考路径components\drivers;
- components目录中C库相关文件,参考路径components\libc\compilers;
要导入的头文件路径也比较多,参考下面:
其他设置要注意的是,不要勾选"Use MicroLIB"这一个选项。
另外就是,因为RT-Thread内核已经写好了所需的所有底层驱动,所以标准固件库里面导入的像systick、gd32f4xx_it类似的文件就不需要了。
2.2 配置文件
RT-Thread的配置文件应该是我见过最复杂的,不算第三方库至少也有上千个配置项,然后你要根据自己的配置项导入对应的源文件,导错的话就会编译不过。一般来说,官方会推荐使用RT-Thread Studio开发或使用Kconfig工具进行配置。不过这里只是进行简单移植即可,能成功跑起来就行,更深入的可以以后再慢慢学。
对于第一次进行移植的可以参考源码项目里面的例程,下面是我根据例程精简过的配置文件。
cpp
#ifndef RT_CONFIG_H__
#define RT_CONFIG_H__
/* Automatically generated file; DO NOT EDIT. */
/* RT-Thread Configuration */
/* RT-Thread Kernel */
#define RT_NAME_MAX 8
#define RT_CPUS_NR 1
#define RT_ALIGN_SIZE 8
#define RT_THREAD_PRIORITY_32
#define RT_THREAD_PRIORITY_MAX 32
#define RT_TICK_PER_SECOND 1000
#define RT_USING_OVERFLOW_CHECK
#define RT_USING_HOOK
#define RT_HOOK_USING_FUNC_PTR
#define RT_USING_IDLE_HOOK
#define RT_IDLE_HOOK_LIST_SIZE 4
#define IDLE_THREAD_STACK_SIZE 256
#define RT_USING_TIMER_SOFT
#define RT_TIMER_THREAD_PRIO 4
#define RT_TIMER_THREAD_STACK_SIZE 512
/* kservice optimization */
#define RT_KSERVICE_USING_STDLIB
#define RT_USING_DEBUG
#define RT_DEBUGING_COLOR
#define RT_DEBUGING_CONTEXT
/* Inter-Thread communication */
#define RT_USING_SEMAPHORE
#define RT_USING_MUTEX
#define RT_USING_EVENT
#define RT_USING_MAILBOX
#define RT_USING_MESSAGEQUEUE
/* Memory Management */
#define RT_USING_MEMPOOL
#define RT_USING_SMALL_MEM
#define RT_USING_SMALL_MEM_AS_HEAP
#define RT_USING_HEAP
#define RT_USING_DEVICE
#define RT_USING_CONSOLE
#define RT_CONSOLEBUF_SIZE 128
#define RT_CONSOLE_DEVICE_NAME "uart0"
#define RT_VER_NUM 0x50100
#define RT_BACKTRACE_LEVEL_MAX_NR 32
/* RT-Thread Components */
#define RT_USING_COMPONENTS_INIT
#define RT_USING_USER_MAIN
#define RT_MAIN_THREAD_STACK_SIZE 2048
#define RT_MAIN_THREAD_PRIORITY 10
/* Device Drivers */
#define RT_USING_DEVICE_IPC
#define RT_UNAMED_PIPE_NUMBER 64
#define RT_USING_SERIAL
#define RT_USING_SERIAL_V1
#define RT_SERIAL_USING_DMA
#define RT_SERIAL_RB_BUFSZ 64
#define RT_USING_PIN
/* Using USB */
/* C/C++ and POSIX layer */
/* ISO-ANSI C layer */
/* Timezone and Daylight Saving Time */
#define RT_LIBC_USING_LIGHT_TZ_DST
#define RT_LIBC_TZ_DEFAULT_HOUR 8
#define RT_LIBC_TZ_DEFAULT_MIN 0
#define RT_LIBC_TZ_DEFAULT_SEC 0
/* POSIX (Portable Operating System Interface) layer */
/* Interprocess Communication (IPC) */
/* Socket is in the 'Network' category */
/* Network */
/* Memory protection */
/* Utilities */
/* RT-Thread Utestcases */
/* RT-Thread online packages */
/* IoT - internet of things */
/* Wi-Fi */
/* Marvell WiFi */
/* Wiced WiFi */
/* CYW43012 WiFi */
/* BL808 WiFi */
/* CYW43439 WiFi */
/* IoT Cloud */
/* security packages */
/* language packages */
/* JSON: JavaScript Object Notation, a lightweight data-interchange format */
/* XML: Extensible Markup Language */
/* multimedia packages */
/* LVGL: powerful and easy-to-use embedded GUI library */
/* u8g2: a monochrome graphic library */
/* tools packages */
/* system packages */
/* enhanced kernel services */
/* acceleration: Assembly language or algorithmic acceleration packages */
/* CMSIS: ARM Cortex-M Microcontroller Software Interface Standard */
/* Micrium: Micrium software products porting for RT-Thread */
/* peripheral libraries and drivers */
/* HAL & SDK Drivers */
/* STM32 HAL & SDK Drivers */
/* Kendryte SDK */
/* sensors drivers */
/* touch drivers */
/* AI packages */
/* Signal Processing and Control Algorithm Packages */
/* miscellaneous packages */
/* project laboratory */
/* samples: kernel and components samples */
/* entertainment: terminal games and other interesting software packages */
/* Arduino libraries */
/* Projects and Demos */
/* Sensors */
/* Display */
/* Timing */
/* Data Processing */
/* Data Storage */
/* Communication */
/* Device Control */
/* Other */
/* Signal IO */
/* Uncategorized */
#define __RT_KERNEL_SOURCE__
#define __RT_IPC_SOURCE__
/* Hardware Drivers Config */
#define SOC_SERIES_GD32F4xx
#define SOC_GD32470Z
/* Onboard Peripheral Drivers */
/* On-chip Peripheral Drivers */
#define BSP_USING_GPIO
#define BSP_USING_UART
#define BSP_USING_UART0
/* Board extended module Drivers */
#endif
除此之外,还需要添加一些全局宏定义------"RT_USING_LIBC, __CLK_TCK=RT_TICK_PER_SECOND, RTTHREAD,__STDC_LIMIT_MACROS, RT_USING_ARMLIBC"
至此,进行编译的话应该就能成功了。如果编译不成功的话,可以尝试通过错误打印查找原因,大部分都是因为文件导少了导多了、某个配置项没设置之类的。或者参考官方的例程,我的开发板例程在路径bsp\gd32\arm\gd32470z-lckfb下。
2.3 测试程序
我们也是简单写一个跑马灯程序,看看RT-Thread有没有移植成功。
cpp
#include "gd32f4xx.h"
#include "rtthread.h"
#include "rtdevice.h"
#include "board.h"
#define DBG_TAG "main"
#define DBG_LVL DBG_INFO
#include "rtdbg.h"
#define LED1_PIN GET_PIN(E, 3)
int main(void)
{
rt_pin_mode(LED1_PIN, PIN_MODE_OUTPUT);
while (1) {
rt_pin_write(LED1_PIN, PIN_HIGH);
LOG_I("LED1 ON");
rt_thread_mdelay(1000);
rt_pin_write(LED1_PIN, PIN_LOW);
LOG_I("LED1 OFF");
rt_thread_mdelay(1000);
}
}
RT-Thread自带一个调试子系统,感觉是模仿Easylogger的,需要导入rtdbg.h,还要定义DBG_TAG和DBG_LVL宏定义,前者是一个字符串标签,调试时会打印出来;后者是调试打印的等级。
这里我使用了pin子系统,这样定义GPIO管脚会方便很多,使用GET_PIN宏定义可以获取管脚,使用rt_pin_mode可以定义管脚,rt_pin_write可以设置管脚输出,感觉这个又是模仿Arduino的。
延时的话就调用rt_thread_mdelay即可,单位是毫秒。
细心的同学可能发现,为什么代码中没有创建线程这种操作?其实main函数就是一个线程。那是怎么做到的呢?这里简单分析一下源码,位于components.c文件。
RT-Thread在初始化系统的时候很巧妙地使用了MDK的Submain和Super$$main两个特性;前者是在main函数执行前插入一段代码,后者是跳转到main函数中。
cpp
extern int $Super$$main(void);
/* re-define main function */
int $Sub$$main(void)
{
rtthread_startup();
return 0;
}
在main函数执行前,插入了rtthread_startup函数,用于初始化RT-Thread系统。
cpp
int rtthread_startup(void)
{
#ifdef RT_USING_SMP
rt_hw_spin_lock_init(&_cpus_lock);
#endif
rt_hw_local_irq_disable();
/* board level initialization
* NOTE: please initialize heap inside board initialization.
*/
rt_hw_board_init();
/* show RT-Thread version */
rt_show_version();
/* timer system initialization */
rt_system_timer_init();
/* scheduler system initialization */
rt_system_scheduler_init();
#ifdef RT_USING_SIGNALS
/* signal system initialization */
rt_system_signal_init();
#endif /* RT_USING_SIGNALS */
/* create init_thread */
rt_application_init();
/* timer thread initialization */
rt_system_timer_thread_init();
/* idle thread initialization */
rt_thread_idle_init();
#ifdef RT_USING_SMP
rt_hw_spin_lock(&_cpus_lock);
#endif /* RT_USING_SMP */
/* start scheduler */
rt_system_scheduler_start();
/* never reach here */
return 0;
}
这个函数里面都是一些系统初始化的操作,重点在rt_application_init函数,main线程就是在这里面初始化的。
cpp
static void main_thread_entry(void *parameter)
{
extern int main(void);
RT_UNUSED(parameter);
#ifdef RT_USING_COMPONENTS_INIT
/* RT-Thread components initialization */
rt_components_init();
#endif /* RT_USING_COMPONENTS_INIT */
#ifdef RT_USING_SMP
rt_hw_secondary_cpu_up();
#endif /* RT_USING_SMP */
/* invoke system main function */
#ifdef __ARMCC_VERSION
{
extern int $Super$$main(void);
$Super$$main(); /* for ARMCC. */
}
#elif defined(__ICCARM__) || defined(__GNUC__) || defined(__TASKING__) || defined(__TI_COMPILER_VERSION__)
main();
#endif /* __ARMCC_VERSION */
}
void rt_application_init(void)
{
rt_thread_t tid;
#ifdef RT_USING_HEAP
tid = rt_thread_create("main", main_thread_entry, RT_NULL,
RT_MAIN_THREAD_STACK_SIZE, RT_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(tid != RT_NULL);
#else
rt_err_t result;
tid = &main_thread;
result = rt_thread_init(tid, "main", main_thread_entry, RT_NULL,
main_thread_stack, sizeof(main_thread_stack), RT_MAIN_THREAD_PRIORITY, 20);
RT_ASSERT(result == RT_EOK);
/* if not define RT_USING_HEAP, using to eliminate the warning */
(void)result;
#endif /* RT_USING_HEAP */
rt_thread_startup(tid);
}
可以看到main线程的实现函数中,调用了Super$main,将代码跳转到了实际的main函数里面。
不得不感叹RT-Thread的骚操作!!!
2.4 运行结果
RT-Thread系统启动时会打印版本信息,接下来就是用户的打印,因为RT-Thread的调试子系统是默认使用彩色打印的,所以最好使用支持彩色调试打印的串口助手,像我使用的MobaXterm,不然会出现一些乱码。
移植成功的话就会看到板子上的LED灯闪烁了。