文章目录
一、RT-Thread简介
1.1 IOT-OS简介
之前已经介绍过RTOS(Real Time Operating System)的原理并分析过UCOS的源码(系列博客链接:https://github.com/StreamAI/UCOS_STM32),这里介绍的IOT-OS(Internet of Things Operating System)跟RTOS有什么区别呢?
简单讲,传统RTOS只是一个IOT-OS的内核,IOT-OS也属于广义的RTOS。IOT-OS在RTOS基础上,为了满足IOT设备连接Internet的需求,提供了比较丰富的组件,特别是对无线通讯协议(比如WLAN)和互联网协议(比如TCP/IP)的支持,同时也包含其它辅助组件(比如文件系统)。之前的嵌入式设备资源非常受限,无法运行操作系统,到后来只能运行最基本的RTOS,只提供多任务调度和任务间同步通信的功能,随着IOT设备硬件资源的丰富和接入Internet甚至获取云端智能服务的需求,有丰富组件的IOT-OS逐渐流行。
首先看下IOT-OS的横向场景和纵向架构,横向场景大致可分为B2B和B2C两大类,纵向架构大致可分为云端、边缘端、终端、芯片端四大类:
目前嵌入式领域ARM架构比较流行,我们先看看ARM推出的Mbed OS系统组件都有哪些:
Mbed OS对网络支持能力挺强的,对于IP联网、非IP联网、TLS安全加密传输都有丰富的支持,同时也对文件系统和设备抽象有不错的支持,得益于ARM的芯片设计能力,Mbed OS对ARM架构芯片的支持更友好强大。
下面再看看国内推出的类Linux风格的RT-Thread系统组件:
RT-Thread也支持丰富的组件,主要可分为三层:接入云端的Web软件包、设备端的组件服务层、底层的内核/BSP层。RT-Thread作为国产IOT-OS,中文资料比较丰富,国内也有不错的市场占有率,仿Linux编程风格,支持类似Shell的FinSH控制台、POSIX API、WebSocket、C++ API等让熟悉Linux的开发者比较有亲切感。
下面再看看注重云端服务的AliOS Things结构框图:
AliOS Things比较得益于阿里云的优势,靠近云端的支持能力比较强,对底层硬件的兼容性就没那么强了。
最后,再看下风头正劲的华为开发的LiteOS结构框图如下:
LiteOS得益于华为对通信技术的研究,其对通信协议特别是LPWA(Low Power Wide Area)的支持更强,同时对自家麒麟芯片的支持也更友好,但对其它芯片的支持可能就差一些。
目前流行的国内外IOT-OS系统还有很多,这里只介绍了几个国内流行的IOT-OS,便于跟传统RTOS对比,看IOT-OS增加了哪些服务组件的支持。各家厂商都互有优势,但还没有哪家在市场中像安卓或windows那样占据绝对优势地位。
IOT-OS为了便于移植,基本都增加了设备抽象层便于管理设备(有点类似Linux Device Tree的理念),既然联网有数据传输需求,少不了对数据的管理,一般也都有文件系统FS的支持。剩下的就是最核心的网络通信协议、接入云端上传数据、从云端获取数据服务等的能力,比如LwIP协议、WLAN、WPAN、LPWA、TLS等通信协议,HTTP、WebSocket、MQTT、CoAP等网络服务协议。
1.2 RT-Thread简介
上述的几个IOT-OS中,RT-Thread算是当前国内最火、最成熟稳定和装机量最大的嵌入式开源操作系统,有着丰富的中文文档学习资源,同时也推出了RT-Thread开发者能力认证(RCEA),类Linux编程风格等,所以我选择RT-Thread作为学习IOT-OS的入口,借助丰富的中文文档,熟悉后还能顺便考取RCEA作为学习成果的凭证,还是挺不错的。重点还是学习IOT-OS的编程理念,方便后面轻松迁移到其它IOT-OS平台,毕竟现在IOT-OS还处于群雄混战阶段。
下面再重复展示下RT-Thread的结构框图:
它具体包括以下部分:
- 内核层:RT-Thread 内核,是 RT-Thread的核心部分,包括了内核系统中对象的实现,例如多线程及其调度、信号量、邮箱、消息队列、内存管理、定时器等;libcpu/BSP(芯片移植相关文件 / 板级支持包)与硬件密切相关,由外设驱动和 CPU 移植构成。
- 组件与服务层:组件是基于 RT-Thread 内核之上的上层软件,例如虚拟文件系统、FinSH命令行界面、网络框架、设备框架等;采用模块化设计,做到组件内部高内聚,组件之间低耦合。
- RT-Thread 软件包:运行于 RT-Thread物联网操作系统平台上,面向不同应用领域的通用软件组件,由描述信息、源代码或库文件组成。RT-Thread提供了开放的软件包平台,这里存放了官方提供或开发者提供的软件包,该平台为开发者提供了众多可重用软件包的选择,这也是 RT-Thread生态的重要组成部分。软件包生态对于一个操作系统的选择至关重要,因为这些软件包具有很强的可重用性,模块化程度很高,极大的方便应用开发者在最短时间内,打造出自己想要的系统。RT-Thread已经支持的软件包数量已经达到 60+,如下举例:
- 物联网相关的软件包:Paho MQTT、WebClient、mongoose、WebTerminal 等等。
- 脚本语言相关的软件包:目前支持 JerryScript、MicroPython。
- 多媒体相关的软件包:Openmv、mupdf。
- 工具类软件包:CmBacktrace、EasyFlash、EasyLogger、SystemView。
- 系统相关的软件包:RTGUI、Persimmon UI、lwext4、partition、SQLite 等等。
- 外设库与驱动类软件包:RealTek RTL8710BN SDK。
RT-Thread文档中心:https://www.rt-thread.org/document/site/
RT-Thread源代码:https://github.com/RT-Thread/rt-thread
RT-Thread编程指南:https://github.com/RT-Thread/rtthread-manual-doc
Env开发辅助工具(为RT-Thread工程提供编译构建环境scons、图形化系统配置menuconfig及软件包管理pkgs功能):https://github.com/RT-Thread/env/releases
本文使用的IOT开发板资源:https://github.com/RT-Thread/IoT_Board
本文我们选择使用最新发布的RT-Thread_V4.0.1版本学习,先看看下载下来的源码目录结构:
RT-Thread源码各目录大概内容如下:
- bsp:板级支持文件,包括HAL库文件、驱动文件、常见开发板移植文件等;
- components:RTT的各种组件(cplusplus–C++ API支持库,dfs–设备文件系统,drivers–设备驱动框架,finsh–命令行控制终端,libc–POSIX API支持库,net–网络协议栈及框架等);
- include:RTT内核头文件;
- libcpu:对各种不同类型架构cpu芯片的支持文件;
- scr:RTT的核心代码;
- tools:自动化构建、编译工具;
移植的重点在bsp文件夹,我们再看看bsp文件夹有哪些目录(以STM32系列芯片为例):
- docs:包含BSP移植及外设添加教程;
- libraries:包含HAL库文件(对于L475芯片是STM32L4xx_HAL),以及RT-Thread为了兼容各芯片新增的驱动层库HAL_Drivers,该驱动层对下调用相应的芯片固件库文件(对于STM32L4芯片是HAL库),对上为设备驱动框架提供统一的接口;
- templates:提供给用户的移植模板,下面以stm32l475-atk-pandora为例展示该文件夹下的目录;
- xxx / applications:用户应用程序文件;
- xxx / board:板级配置文件、链接脚本文件等,也是系统移植的重要文件夹;
- project.uvprojx / project.eww: keil 5工程文件 / IAR工程文件;
- rtconfig.h: 系统裁剪相关的一个头文件;
- Kconfig:图形化系统配置工具menuconfig配置文件;
- SConstruct / SConscript:编译构建环境scons配置文件;
二、RT-Thread启动过程
之前写过一篇博客介绍STM32的启动过程和固件移植,所以这里芯片启动到main函数的部分就不再赘述了,重点介绍下进入C语言main函数后的过程。由于RT-Thread涉及内核和丰富组件,使用前都要对这些资源先进行初始化,因此RT-Thread启动主要包含了芯片、板级、内核、组件等的资源初始化过程。
2.1 RT-Thread启动过程
前面介绍STM32 HAL库时了解到,在main函数中要先调用HAL_Init对HAL库进行初始化,同样的我们也可以在main函数中调用rtthread_startup对RT-Thread内核及组件进行初始化。我们想在main函数中直接编写代码,能否在进入main函数之前完成系统启动和初始化呢?
MDK 提供了扩展功能 $Sub$$
和 $Super$$
(其它平台也有类似的扩展功能,这里以最常用的MDK为例说明),可以给 main 添加 $Sub$$
的前缀符号作为一个新功能函数 $Sub$$main
,这个 $Sub$$main
可以先调用一些要补充在 main 之前的功能函数(这里添加 RT-Thread 系统初始化功能),再调用 $Super$$main
转到 main() 函数执行,这样可以让用户不用去管 main() 之前的系统初始化操作(详见ARM® Compiler v5.06 for µVision® armlink User Guide)。下面看RT-Thread启动过程如下图所示:
相关的实现代码如下:
// components.c
int $Sub$$main(void)
{
rt_hw_interrupt_disable();
rtthread_startup();
return 0;
}
int rtthread_startup(void)
{
rt_hw_interrupt_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
/* 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_hw_board_init,这也是我们移植时要重点实现的函数;另一个是跟应用程序相关的rt_application_init,把应用作为一个线程来执行,完成组件初始化并进入main函数。其余的主要是RT-Thread系统内核资源的初始化,比如定时器、调度器、信号、初始化定时器线程、初始化空闲线程等,最后启动调度器开始运行系统。
rt_hw_board_init放到下面的系统移植部分再进行详解,下面介绍rt_application_init部分,该函数的实现代码如下:
// components.c
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_stack, sizeof(main_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_thread_startup(tid);
}
/* the system main thread */
void main_thread_entry(void *parameter)
{
extern int main(void);
extern int $Super$$main(void);
/* RT-Thread components initialization */
rt_components_init();
#ifdef RT_USING_SMP
rt_hw_secondary_cpu_up();
#endif
/* invoke system main function */
#if defined(__CC_ARM) || defined(__CLANG_ARM)
$Super$$main(); /* for ARMCC. */
#elif defined(__ICCARM__) || defined(__GNUC__)
main();
#endif
}
rt_application_init创建了一个主线程main_thread_entry,在该线程内部调用了组件初始化函数rt_components_init,然后进入main函数,开始执行用户代码,用户可以在main函数内添加自己的应用。
2.2 RT-Thread 自动初始化机制
接下来看组件初始化函数rt_components_init,之所以在这里专门介绍,是跟RT-Thread的自动初始化机制有关,先看rt_components_init的实现代码:
// components.c
/**
* RT-Thread Components Initialization
*/
void rt_components_init(void)
{
#if RT_DEBUG_INIT
int result;
const struct rt_init_desc *desc;
rt_kprintf("do components initialization.\n");
for (desc = &__rt_init_desc_rti_board_end; desc < &__rt_init_desc_rti_end; desc ++)
{
rt_kprintf("initialize %s", desc->fn_name);
result = desc->fn();
rt_kprintf(":%d done\n", result);
}
#else
const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_end; fn_ptr < &__rt_init_rti_end; fn_ptr ++)
{
(*fn_ptr)();
}
#endif
}
// rtdef.h
/* initialization export */
#ifdef RT_USING_COMPONENTS_INIT
typedef int (*init_fn_t)(void);
#ifdef _MSC_VER /* we do not support MS VC++ compiler */
#define INIT_EXPORT(fn, level)
#else
#if RT_DEBUG_INIT
struct rt_init_desc
{
const char* fn_name;
const init_fn_t fn;
};
#define INIT_EXPORT(fn, level) \
const char __rti_##fn##_name[] = #fn; \
RT_USED const struct rt_init_desc __rt_init_desc_##fn SECTION(".rti_fn."level) = \
{ __rti_##fn##_name, fn};
#else
#define INIT_EXPORT(fn, level) \
RT_USED const init_fn_t __rt_init_##fn SECTION(".rti_fn."level) = fn
#endif
#endif
#else
#define INIT_EXPORT(fn, level)
#endif
从上面的代码可以看出rt_components_init函数依次调用执行RT-Thread自定义RTI符号段SECTION(".rti_fn."level)内从__rt_init_desc_rti_board_end到__rt_init_desc_rti_end的命令或函数,用户可以通过调用宏定义INIT_EXPORT(fn, level)将需要在启动时进行初始化的函数指针放到该RTI符号段中,形成一张初始化函数表(可以类比STM32的中断向量表)。
RT-Thread也正是借助宏定义INIT_EXPORT(fn, level)实现自动初始化机制,也即初始化函数不需要被显式调用,只需要在初始化函数定义处通过该宏定义进行申明,该函数就会被添加到RTI符号段的初始化函数表中,在系统启动过程中通过rt_components_init遍历RTI符号段的初始化函数表,并依次调用表中的函数,达到自动初始化的目的。
RT-Thread还针对不同的level给出了相应的宏定义,代码如下:
// rtdef.h
/* board init routines will be called in board_init() function */
#define INIT_BOARD_EXPORT(fn) INIT_EXPORT(fn, "1")
/* pre/device/component/env/app init routines will be called in init_thread */
/* components pre-initialization (pure software initilization) */
#define INIT_PREV_EXPORT(fn) INIT_EXPORT(fn, "2")
/* device ini