之前已经介绍过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等网络服务协议。
上述的几个IOT-OS中,RT-Thread算是当前国内最火、最成熟稳定和装机量最大的嵌入式开源操作系统,有着丰富的中文文档学习资源,同时也推出了RT-Thread开发者能力认证(RCEA),类Linux编程风格等,所以我选择RT-Thread作为学习IOT-OS的入口,借助丰富的中文文档,熟悉后还能顺便考取RCEA作为学习成果的凭证,还是挺不错的。重点还是学习IOT-OS的编程理念,方便后面轻松迁移到其它IOT-OS平台,毕竟现在IOT-OS还处于群雄混战阶段。
下面再重复展示下RT-Thread的结构框图:
它具体包括以下部分:
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文件夹,我们再看看bsp文件夹有哪些目录(以STM32系列芯片为例):
之前写过一篇博客介绍STM32的启动过程和固件移植,所以这里芯片启动到main函数的部分就不再赘述了,重点介绍下进入C语言main函数后的过程。由于RT-Thread涉及内核和丰富组件,使用前都要对这些资源先进行初始化,因此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函数内添加自己的应用。
接下来看组件初始化函数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 initialization */
#define INIT_DEVICE_EXPORT(fn) INIT_EXPORT(fn, "3")
/* components initialization (dfs, lwip, ...) */
#define INIT_COMPONENT_EXPORT(fn) INIT_EXPORT(fn, "4")
/* environment initialization (mount disk, ...) */
#define INIT_ENV_EXPORT(fn) INIT_EXPORT(fn, "5")
/* appliation initialization (rtgui application etc ...) */
#define INIT_APP_EXPORT(fn) INIT_EXPORT(fn, "6")
用来实现自动初始化功能的宏接口定义详细描述如下表所示:
初始化顺序 | 宏接口 | 描述 |
---|---|---|
1 | INIT_BOARD_EXPORT(fn) | 非常早期的初始化,此时调度器还未启动 |
2 | INIT_PREV_EXPORT(fn) | 主要是用于纯软件的初始化、没有太多依赖的函数 |
3 | INIT_DEVICE_EXPORT(fn) | 外设驱动初始化相关,比如网卡设备 |
4 | INIT_COMPONENT_EXPORT(fn) | 组件初始化,比如文件系统或者 LWIP |
5 | INIT_ENV_EXPORT(fn) | 系统环境初始化,比如挂载文件系统 |
6 | INIT_APP_EXPORT(fn) | 应用初始化,比如 GUI 应用 |
初始化函数主动通过这些宏接口申明,如 INIT_BOARD_EXPORT(rt_hw_usart_init),链接器会自动收集所有被申明的初始化函数,放到 RTI 符号段中,该符号段位于内存分布的 RO 段中,该 RTI 符号段中的所有函数在系统初始化时会被自动调用。
跟上面的自动初始化宏接口类似,RT-Thread还提供了另一套导出自定义命令的宏接口FinSH,下面先看看FinSH的宏接口定义:
// rtdef.h
#if !defined(RT_USING_FINSH)
/* define these to empty, even if not include finsh.h file */
#define FINSH_FUNCTION_EXPORT(name, desc)
#define FINSH_FUNCTION_EXPORT_ALIAS(name, alias, desc)
#define FINSH_VAR_EXPORT(name, type, desc)
#define MSH_CMD_EXPORT(command, desc)
#define MSH_CMD_EXPORT_ALIAS(command, alias, desc)
#elif !defined(FINSH_USING_SYMTAB)
#define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc)
#endif
这些FinSH宏接口定义是否也跟上面自动初始化机制类似,在一个符号段中维护一张函数表呢?
从上面的宏定义看,FinSH宏接口包括三类:Function、Variable、Command,其中Function与Command比较类似,FinSH可能需要维护两个符号段,代码如下:
#ifdef FINSH_USING_SYMTAB
#ifdef __TI_COMPILER_VERSION__
#define __TI_FINSH_EXPORT_FUNCTION(f) PRAGMA(DATA_SECTION(f,"FSymTab"))
#define __TI_FINSH_EXPORT_VAR(v) PRAGMA(DATA_SECTION(v,"VSymTab"))
#endif
#ifdef FINSH_USING_DESCRIPTION
#ifdef _MSC_VER
#define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \
const char __fsym_##cmd##_name[] = #cmd; \
const char __fsym_##cmd##_desc[] = #desc; \
__declspec(allocate("FSymTab$f")) \
const struct finsh_syscall __fsym_##cmd = \
{ \
__fsym_##cmd##_name, \
__fsym_##cmd##_desc, \
(syscall_func)&name \
};
#pragma comment(linker, "/merge:FSymTab=mytext")
#define FINSH_VAR_EXPORT(name, type, desc) \
const char __vsym_##name##_name[] = #name; \
const char __vsym_##name##_desc[] = #desc; \
__declspec(allocate("VSymTab")) \
const struct finsh_sysvar __vsym_##name = \
{ \
__vsym_##name##_name, \
__vsym_##name##_desc, \
type, \
(void*)&name \
};
#elif defined(__TI_COMPILER_VERSION__)
#define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \
__TI_FINSH_EXPORT_FUNCTION(__fsym_##cmd); \
const char __fsym_##cmd##_name[] = #cmd; \
const char __fsym_##cmd##_desc[] = #desc; \
const struct finsh_syscall __fsym_##cmd = \
{ \
__fsym_##cmd##_name, \
__fsym_##cmd##_desc, \
(syscall_func)&name \
};
#define FINSH_VAR_EXPORT(name, type, desc) \
__TI_FINSH_EXPORT_VAR(__vsym_##name); \
const char __vsym_##name##_name[] = #name; \
const char __vsym_##name##_desc[] = #desc; \
const struct finsh_sysvar __vsym_##name = \
{ \
__vsym_##name##_name, \
__vsym_##name##_desc, \
type, \
(void*)&name \
};
#else
#define FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc) \
const char __fsym_##cmd##_name[] SECTION(".rodata.name") = #cmd; \
const char __fsym_##cmd##_desc[] SECTION(".rodata.name") = #desc; \
RT_USED const struct finsh_syscall __fsym_##cmd SECTION("FSymTab")= \
{ \
__fsym_##cmd##_name, \
__fsym_##cmd##_desc, \
(syscall_func)&name \
};
#define FINSH_VAR_EXPORT(name, type, desc) \
const char __vsym_##name##_name[] SECTION(".rodata.name") = #name; \
const char __vsym_##name##_desc[] SECTION(".rodata.name") = #desc; \
RT_USED const struct finsh_sysvar __vsym_##name SECTION("VSymTab")= \
{ \
__vsym_##name##_name, \
__vsym_##name##_desc, \
type, \
(void*)&name \
};
#endif
#else
......
#endif /* end of FINSH_USING_DESCRIPTION */
#endif /* end of FINSH_USING_SYMTAB */
/**
* This macro exports a system function to finsh shell.
* @param name the name of function.
* @param desc the description of function, which will show in help.
*/
#define FINSH_FUNCTION_EXPORT(name, desc) \
FINSH_FUNCTION_EXPORT_CMD(name, name, desc)
/**
* This macro exports a system function with an alias name to finsh shell.
* @param name the name of function.
* @param alias the alias name of function.
* @param desc the description of function, which will show in help.
*/
#define FINSH_FUNCTION_EXPORT_ALIAS(name, alias, desc) \
FINSH_FUNCTION_EXPORT_CMD(name, alias, desc)
/**
* This macro exports a command to module shell.
* @param command the name of command.
* @param desc the description of command, which will show in help.
*/
#ifdef FINSH_USING_MSH
#define MSH_CMD_EXPORT(command, desc) \
FINSH_FUNCTION_EXPORT_CMD(command, __cmd_##command, desc)
#define MSH_CMD_EXPORT_ALIAS(command, alias, desc) \
FINSH_FUNCTION_EXPORT_ALIAS(command, __cmd_##alias, desc)
#else
#define MSH_CMD_EXPORT(command, desc)
#define MSH_CMD_EXPORT_ALIAS(command, alias, desc)
#endif
从上面的代码可以看出,FinSH自定义符号段FSymTab与VSymTab,分别保存一个导出函数/命令表和导出全局变量表,用户可以通过宏定义FINSH_FUNCTION_EXPORT_CMD(name, cmd, desc)与FINSH_VAR_EXPORT(name, type, desc) 分别往自定义符号段FSymTab与VSymTab中添加想要导出的自定义函数名和全局变量名。
看最后几个导出自定义函数/命令的宏定义,MSH(Module Shell)与FINSH(Function Shell)都是通过调用FINSH_FUNCTION_EXPORT_CMD宏接口实现的(有点类似于前面介绍的INIT_EXPORT),二者的区别是MSH导出的命令前相比FINSH多了__cmd_修饰。在用户使用导出的命令时,MSH导出的命令更接近我们在Linux或Windows上使用的shell命令(命令与参数间以空格分隔:command [arg1] [arg2] […]),FINSH导出的命令称为C语言解释器(C-Style)模式,命令格式类似于C语言的函数调用方式(必须带括号:list_thread()),两种模式的命令不通用。
如果在 RT-Thread 中同时使能了这两种模式,那它们可以动态切换,在 msh 模式下输入 exit 后回车,即可切换到 C-Style 模式;在 C-Style 模式输入 msh() 后回车,即可进入 msh 模式。由于 C-Style 模式占用体积较大,且msh模式更接近我们使用shell的习惯,所以常使用msh模式,如果想节省点空间占用资源可以在配置文件 rtconfig.h 中开启FINSH_USING_MSH_ONLY宏定义。
用来导出自定义函数/变量的宏接口定义详细描述如下表所示:
模式 | 宏接口 | 描述 |
---|---|---|
MSH | MSH_CMD_EXPORT(command, desc) | 将一个自定义命令导出到 msh 模式; |
MSH | MSH_CMD_EXPORT_ALIAS(command, alias, desc) |
导出一个自定义命令,并重定义显示到 msh模式的命令别名; |
C-Style | FINSH_FUNCTION_EXPORT(name, desc) | 将一个自定义命令导出到 C-Style 模式; |
C-Style | FINSH_FUNCTION_EXPORT_ALIAS( name, alias, desc) |
导出一个自定义命令,并重定义显示到 C-Style模式的命令别名; |
C-Style | FINSH_VAR_EXPORT(name, type, desc) | 将一个自定义变量导出到C-Style 模式, 需指定变量类型; |
导出的自定义函数名或变量名是为了方便我们在RT-Thead系统运行时动态调用执行,要解析我们输入的命令并执行需要FinSH组件的支持,该组件使用前自然需要初始化,FinSH组件的初始化就使用了前面介绍的自动初始化机制,FinSH组件初始化部分代码如下:
// shell.c
/*
* This function will initialize finsh shell
*/
int finsh_system_init(void)
{
rt_err_t result = RT_EOK;
rt_thread_t tid;
#ifdef FINSH_USING_SYMTAB
#if defined(__CC_ARM) || defined(__CLANG_ARM) /* ARM C Compiler */
extern const int FSymTab$$Base;
extern const int FSymTab$$Limit;
extern const int VSymTab$$Base;
extern const int VSymTab$$Limit;
finsh_system_function_init(&FSymTab$$Base, &FSymTab$$Limit);
#ifndef FINSH_USING_MSH_ONLY
finsh_system_var_init(&VSymTab$$Base, &VSymTab$$Limit);
#endif
......
unsigned int *ptr_begin, *ptr_end;
ptr_begin = (unsigned int *)&__fsym_begin;
ptr_begin += (sizeof(struct finsh_syscall) / sizeof(unsigned int));
while (*ptr_begin == 0) ptr_begin ++;
ptr_end = (unsigned int *) &__fsym_end;
ptr_end --;
while (*ptr_end == 0) ptr_end --;
finsh_system_function_init(ptr_begin, ptr_end);
#endif
#endif
#ifdef RT_USING_HEAP
/* create or set shell structure */
shell = (struct finsh_shell *)rt_calloc(1, sizeof(struct finsh_shell));
if (shell == RT_NULL)
{
rt_kprintf("no memory for shell\n");
return -1;
}
tid = rt_thread_create(FINSH_THREAD_NAME,
finsh_thread_entry, RT_NULL,
FINSH_THREAD_STACK_SIZE, FINSH_THREAD_PRIORITY, 10);
#else
shell = &_shell;
tid = &finsh_thread;
result = rt_thread_init(&finsh_thread,
FINSH_THREAD_NAME,
finsh_thread_entry, RT_NULL,
&finsh_thread_stack[0], sizeof(finsh_thread_stack),
FINSH_THREAD_PRIORITY, 10);
#endif /* RT_USING_HEAP */
rt_sem_init(&(shell->rx_sem), "shrx", 0, 0);
finsh_set_prompt_mode(1);
if (tid != NULL && result == RT_EOK)
rt_thread_startup(tid);
return 0;
}
INIT_APP_EXPORT(finsh_system_init);
FinSH组件通过finsh_system_init函数进行初始化,通过调用INIT_APP_EXPORT(finsh_system_init)宏接口,将初始化函数finsh_system_init添加到自定义符号段RTI的初始化函数表中,在RT-Thread系统启动时实现初始化。
finsh_system_init函数除了自定义两个符号段FSymTab与VSymTab外,主要启动了一个线程finsh_thread_entry并初始化一个信号量shell->rx_sem。下面看看这个线程的实现代码:
// shell.c
void finsh_thread_entry(void *parameter)
{
int ch;
......
#ifndef FINSH_USING_MSH_ONLY
finsh_init(&shell->parser);
#endif
#ifndef RT_USING_POSIX
/* set console device as shell device */
if (shell->device == RT_NULL)
{
rt_device_t console = rt_console_get_device();
if (console)
{
finsh_set_device(console->parent.name);
}
}
#endif
......
rt_kprintf(FINSH_PROMPT);
while (1)
{
ch = finsh_getchar();
......
/* handle end of line, break */
if (ch == '\r' || ch == '\n')
{
#ifdef FINSH_USING_HISTORY
shell_push_history(shell);
#endif
#ifdef FINSH_USING_MSH
if (msh_is_used() == RT_TRUE)
{
if (shell->echo_mode)
rt_kprintf("\n");
msh_exec(shell->line, shell->line_position);
}
else
#endif
{
#ifndef FINSH_USING_MSH_ONLY
/* add ';' and run the command line */
shell->line[shell->line_position] = ';';
if (shell->line_position != 0) finsh_run_line(&shell->parser, shell->line);
else
if (shell->echo_mode) rt_kprintf("\n");
#endif
}
......
}
......
} /* end of device read */
}
// finsh_init.c
int finsh_init(struct finsh_parser* parser)
{
finsh_parser_init(parser);
/* finsh init */
finsh_node_init();
finsh_var_init();
finsh_error_init();
finsh_heap_init();
return 0;
}
这里之所以介绍FinSH的初始化过程,主要是FinSH运行时需要设置好控制终端(本文以USART1作为FinSH的控制终端),控制终端涉及到命令或数据的输入、输出,跟底层硬件关系较密切,也是进行RT-Thread移植时需要考虑的重点。
继续看finsh_thread_entry代码,首先是finsh_init初始化命令解释器parser / node / var / error / heap等,这些都是finsh组件内资源,除了需要的heap空间初始化,其余的跟底层硬件关系不大,移植时可以暂时不关系其实现过程。接下来finsh获取/设置控制终端,这个跟底层硬件就有关系了,我们这部分代码:
// kservice.c
/**
* This function returns the device using in console.
* @return the device using in console or RT_NULL
*/
rt_device_t rt_console_get_device(void)
{
return _console_device;
}
RTM_EXPORT(rt_console_get_device);
/**
* This function will set a device as console device.
* After set a device to console, all output of rt_kprintf will be
* redirected to this new device.
* @param name the name of new console device
* @return the old console device handler
*/
rt_device_t rt_console_set_device(const char *name)
{
rt_device_t new, old;
/* save old device */
old = _console_device;
/* find new console device */
new = rt_device_find(name);
if (new != RT_NULL)
{
if (_console_device != RT_NULL)
{
/* close old console device */
rt_device_close(_console_device);
}
/* set new console device */
rt_device_open(new, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_STREAM);
_console_device = new;
}
return old;
}
RTM_EXPORT(rt_console_set_device);
/**
* This function will print a formatted string on system console
* @param fmt the format
*/
void rt_kprintf(const char *fmt, ...)
{
va_list args;
rt_size_t length;
static char rt_log_buf[RT_CONSOLEBUF_SIZE];
va_start(args, fmt);
length = rt_vsnprintf(rt_log_buf, sizeof(rt_log_buf) - 1, fmt, args);
if (length > RT_CONSOLEBUF_SIZE - 1)
length = RT_CONSOLEBUF_SIZE - 1;
#ifdef RT_USING_DEVICE
if (_console_device == RT_NULL)
{
rt_hw_console_output(rt_log_buf);
}
else
{
rt_uint16_t old_flag = _console_device->open_flag;
_console_device->open_flag |= RT_DEVICE_FLAG_STREAM;
rt_device_write(_console_device, 0, rt_log_buf, length);
_console_device->open_flag = old_flag;
}
#else
rt_hw_console_output(rt_log_buf);
#endif
va_end(args);
}
RTM_EXPORT(rt_kprintf);
// shell.c
/**
* This function sets the input device of finsh shell.
* @param device_name the name of new input device.
*/
void finsh_set_device(const char *device_name)
{
rt_device_t dev = RT_NULL;
RT_ASSERT(shell != RT_NULL);
dev = rt_device_find(device_name);
if (dev == RT_NULL)
{
rt_kprintf("finsh: can not find device: %s\n", device_name);
return;
}
/* check whether it's a same device */
if (dev == shell->device) return;
/* open this device and set the new device in finsh shell */
if (rt_device_open(dev, RT_DEVICE_OFLAG_RDWR | RT_DEVICE_FLAG_INT_RX | \
RT_DEVICE_FLAG_STREAM) == RT_EOK)
{
if (shell->device != RT_NULL)
{
/* close old finsh device */
rt_device_close(shell->device);
rt_device_set_rx_indicate(shell->device, RT_NULL);
}
/* clear line buffer before switch to new device */
memset(shell->line, 0, sizeof(shell->line));
shell->line_curpos = shell->line_position = 0;
shell->device = dev;
rt_device_set_rx_indicate(dev, finsh_rx_ind);
}
}
static rt_err_t finsh_rx_ind(rt_device_t dev, rt_size_t size)
{
RT_ASSERT(shell != RT_NULL);
/* release semaphore to let finsh thread rx data */
rt_sem_release(&shell->rx_sem);
return RT_EOK;
}
从上面的代码可以看出,finsh组件通过rt_console_get_device获取控制终端设备console_device,该设备通过rt_console_set_device被设置,所以在移植时如果要使用finsh组件,需要设置console_device,而且rt_kprintf命令也依赖console_device。
通过rt_console_get_device获取到console_device后,调用finsh_set_device打开console_device,并通过rt_device_set_rx_indicate设置接收指示函数finsh_rx_ind,其中finsh_rx_ind会在console_device接收到数据后被回调执行,finsh_rx_ind函数内只是释放了信号量shell->rx_sem,那么这个信号量被谁获取呢?
继续看finsh_thread_entry代码,配置完console_device就进入了循环体内部开始执行了(这里略去了使用finsh的认证过程),先看finsh_getchar的代码:
// shell.c
static char finsh_getchar(void)
{
#ifdef RT_USING_POSIX
return getchar();
#else
char ch;
RT_ASSERT(shell != RT_NULL);
while (rt_device_read(shell->device, -1, &ch, 1) != 1)
rt_sem_take(&shell->rx_sem, RT_WAITING_FOREVER);
return ch;
#endif
}
在finsh_getchar函数中等待获取被前面finsh_rx_ind函数释放的信号量shell->rx_sem,如果获取到信号量则调用rt_device_read读取前面配置的console_device接收到的数据。这里信号量的作用也就比较明显了,console_device未接收到数据则finsh_thread_entry循环体在finsh_getchar处一直等待信号量shell->rx_sem,待console_device接收到数据则通过回调执行rx_indicate(这里被设置为finsh_rx_ind)释放信号量,finsh_getchar获取到该信号量后就可以通过rt_device_read读取console_device接收的数据进行下一步的处理。
finsh_thread_entry后面对finsh_getchar获取的数据进行解析、编译、执行的过程跟BSP移植关系不大,这里就暂时略去了,对符号的解释、编译、执行也是finsh组件的核心,要理解这部分代码需要对编译原理有所了解,后面有机会再介绍。先推荐一篇别人关于finsh原理解析的博客供参考:RT-Thread下finsh原理浅析,下面给出FinSH命令执行流程供了解大概脉络:
下面介绍rt_hw_board_init过程,实现代码如下:
// .\bsp\stm32\libraries\HAL_Drivers\drv_common.c
/**
* This function will initial STM32 board.
*/
RT_WEAK void rt_hw_board_init()
{
#ifdef SCB_EnableICache
/* Enable I-Cache---------------------------------------------------------*/
SCB_EnableICache();
#endif
#ifdef SCB_EnableDCache
/* Enable D-Cache---------------------------------------------------------*/
SCB_EnableDCache();
#endif
/* HAL_Init() function is called at the beginning of the program */
HAL_Init();
/* System clock initialization */
SystemClock_Config();
rt_hw_systick_init();
/* Heap initialization */
#if defined(RT_USING_HEAP)
rt_system_heap_init((void *)HEAP_BEGIN, (void *)HEAP_END);
#endif
/* Pin driver initialization is open by default */
#ifdef RT_USING_PIN
rt_hw_pin_init();
#endif
/* USART driver initialization is open by default */
#ifdef RT_USING_SERIAL
rt_hw_usart_init();
#endif
/* Set the shell console output device */
#ifdef RT_USING_CONSOLE
rt_console_set_device(RT_CONSOLE_DEVICE_NAME);
#endif
/* Board underlying hardware initialization */
#ifdef RT_USING_COMPONENTS_INIT
rt_components_board_init();
#endif
}
/* SysTick configuration */
void rt_hw_systick_init(void)
{
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq() / RT_TICK_PER_SECOND);
HAL_SYSTICK_CLKSourceConfig(SYSTICK_CLKSOURCE_HCLK);
HAL_NVIC_SetPriority(SysTick_IRQn, 0, 0);
}
RT-Thread为我们提供了rt_hw_board_init,该函数被RT_WEAK修饰,允许我们重定义。在rt_hw_board_init中,先调用HAL_Init初始化HAL库,再调用SystemClock_Config配置系统时钟,SystemClock_Config函数在board.c(bsp\stm32\libraries\templates \stm32l4xx\board\board.c)文件中定义,如果我们重新配置RCC与时钟树,则需要修改该函数定义(可以通过CubeMX生成,可参考博客:HAL详解与CubeMX使用),然后通过rt_hw_systick_init调用HAL库函数初始化systick,通过rt_system_heap_init初始化堆区间,systick和heap配置主要通过设置宏定义即可,代码如下(包括系统滴答频率、SRAM1区间、FLASH区间等宏定义):
// bsp\stm32\libraries\templates\stm32l4xx\rtconfig.h
#define RT_TICK_PER_SECOND 1000
// bsp\stm32\libraries\templates\stm32l4xx\board\board.h
#define HEAP_BEGIN ((void *)&__bss_end)
#define HEAP_END STM32_SRAM1_END
#define STM32_SRAM1_SIZE (96)
#define STM32_SRAM1_START (0x20000000)
#define STM32_SRAM1_END (STM32_SRAM1_START + STM32_SRAM1_SIZE * 1024)
#define STM32_FLASH_START_ADRESS ((uint32_t)0x08000000)
#define STM32_FLASH_SIZE (512 * 1024)
#define STM32_FLASH_END_ADDRESS ((uint32_t)(STM32_FLASH_START_ADRESS + STM32_FLASH_SIZE))
到这里可以看出board.c与board.h两个文件(bsp\stm32\libraries\templates \stm32l4xx\board)在移植中还是比较重要的,一般需要修改board.c中的SystemClock_Config函数定义和board.h中的FLASH/RAM宏定义。
继续看rt_hw_board_init,接下来是rt_hw_pin_init与rt_hw_usart_init,看名字自然是初始化pin引脚和串口usart的,这两个也是系统移植的重点,等会在详细介绍。然后是调用rt_console_set_device配置console_device,也是通过宏定义配置的,该函数的实现代码前面已经介绍过,宏定义代码如下:
// bsp\stm32\libraries\templates\stm32l4xx\rtconfig.h
#define RT_CONSOLEBUF_SIZE 256
#define RT_CONSOLE_DEVICE_NAME "uart1"
最后的rt_components_board_init跟前面介绍过的rt_components_init原理一样,只是初始化函数表的位置和执行顺序有点区别,rt_components_board_init先完成其自定义符号段RTI内初始化函数表的遍历执行,待底层初始化完毕后,rt_components_init才继续完成其自定义符号段RTI内初始化函数表的遍历执行,该部分初始化函数可能依赖前面更底层函数的初始化,所以两者原理一致,执行顺序有先后。
下面看看rt_hw_pin_init与rt_hw_usart_init是怎样实现引脚与串口初始化的,代码如下:
// bsp\stm32\libraries\HAL_Drivers\drv_gpio.c
int rt_hw_pin_init(void)
{
#if defined(__HAL_RCC_GPIOA_CLK_ENABLE)
__HAL_RCC_GPIOA_CLK_ENABLE();
#endif
......
#if defined(__HAL_RCC_GPIOK_CLK_ENABLE)
__HAL_RCC_GPIOK_CLK_ENABLE();
#endif
return rt_device_pin_register("pin", &_stm32_pin_ops, RT_NULL);
}
const static struct rt_pin_ops _stm32_pin_ops =
{
stm32_pin_mode,
stm32_pin_write,
stm32_pin_read,
stm32_pin_attach_irq,
stm32_pin_dettach_irq,
stm32_pin_irq_enable,
};
// bsp\stm32\libraries\HAL_Drivers\drv_usart.c
int rt_hw_usart_init(void)
{
rt_size_t obj_num = sizeof(uart_obj) / sizeof(struct stm32_uart);
struct serial_configure config = RT_SERIAL_CONFIG_DEFAULT;
rt_err_t result = 0;
stm32_uart_get_dma_config();
for (int i = 0; i < obj_num; i++)
{
uart_obj[i].config = &uart_config[i];
uart_obj[i].serial.ops = &stm32_uart_ops;
uart_obj[i].serial.config = config;
#if defined(RT_SERIAL_USING_DMA)
if(uart_obj[i].uart_dma_flag)
{
/* register UART device */
result = rt_hw_serial_register(&uart_obj[i].serial,uart_obj[i].config->name,
RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX| RT_DEVICE_FLAG_DMA_RX ,&uart_obj[i]);
}
else
#endif
{
/* register UART device */
result = rt_hw_serial_register(&uart_obj[i].serial,uart_obj[i].config->name,
RT_DEVICE_FLAG_RDWR | RT_DEVICE_FLAG_INT_RX,&uart_obj[i]);
}
RT_ASSERT(result == RT_EOK);
}
return result;
}
static const struct rt_uart_ops stm32_uart_ops =
{
.configure = stm32_configure,
.control = stm32_control,
.putc = stm32_putc,
.getc = stm32_getc,
};
// components\drivers\include\drivers\serial.h
/* Default config for serial_configure structure */
#define RT_SERIAL_CONFIG_DEFAULT \
{ \
BAUD_RATE_115200, /* 115200 bits/s */ \
DATA_BITS_8, /* 8 databits */ \
STOP_BITS_1, /* 1 stopbit */ \
PARITY_NONE, /* No parity */ \
BIT_ORDER_LSB, /* LSB first sent */ \
NRZ_NORMAL, /* Normal mode */ \
RT_SERIAL_RB_BUFSZ, /* Buffer size */ \
0 \
}
初始化函数rt_hw_pin_init与rt_hw_usart_init主要是对相应的pin与usart设备配置一些默认参数(注意串口USART1的默认参数配置,连接该串口时需要配置一致,原理见博客:USART + DMA + HAL)后把设备注册到RT-Thread的设备驱动框架层。RT-Thread的设备对象和I/O设备模型框架层后面再详细介绍,这里先简单说下原理,位于components\drivers中的设备驱动框架层组件对用户提供统一的抽象设备操作接口,为了能操作具体的设备,需要用户实现相应设备的操作函数集合,并将其注册到设备驱动框架层中。下面简单看下RT-Thread 提供的 I/O 设备模型框架:
RT-Thread设备模型框架主要分为三层,从上到下分别是 I/O 设备管理层、设备驱动框架层、设备驱动层,各层大概作用如下:
// components\drivers\misc\pin.c
int rt_device_pin_register(const char *name, const struct rt_pin_ops *ops, void *user_data)
{
_hw_pin.parent.type = RT_Device_Class_Miscellaneous;
_hw_pin.parent.rx_indicate = RT_NULL;
_hw_pin.parent.tx_complete = RT_NULL;
#ifdef RT_USING_DEVICE_OPS
_hw_pin.parent.ops = &pin_ops;
#else
_hw_pin.parent.init = RT_NULL;
_hw_pin.parent.open = RT_NULL;
_hw_pin.parent.close = RT_NULL;
_hw_pin.parent.read = _pin_read;
_hw_pin.parent.write = _pin_write;
_hw_pin.parent.control = _pin_control;
#endif
_hw_pin.ops = ops;
_hw_pin.parent.user_data = user_data;
/* register a character device */
rt_device_register(&_hw_pin.parent, name, RT_DEVICE_FLAG_RDWR);
return 0;
}
// components\drivers\serial\serial.c
rt_err_t rt_hw_serial_register(struct rt_serial_device *serial,
const char *name,
rt_uint32_t flag,
void *data)
{
rt_err_t ret;
struct rt_device *device;
RT_ASSERT(serial != RT_NULL);
device = &(serial->parent);
device->type = RT_Device_Class_Char;
device->rx_indicate = RT_NULL;
device->tx_complete = RT_NULL;
#ifdef RT_USING_DEVICE_OPS
device->ops = &serial_ops;
#else
device->init = rt_serial_init;
device->open = rt_serial_open;
device->close = rt_serial_close;
device->read = rt_serial_read;
device->write = rt_serial_write;
device->control = rt_serial_control;
#endif
device->user_data = data;
/* register a character device */
ret = rt_device_register(device, name, flag);
#if defined(RT_USING_POSIX)
/* set fops */
device->fops = &_serial_fops;
#endif
return ret;
}
// src\device.c
rt_err_t rt_device_register(rt_device_t dev, const char *name, rt_uint16_t flags)
{
if (dev == RT_NULL)
return -RT_ERROR;
if (rt_device_find(name) != RT_NULL)
return -RT_ERROR;
rt_object_init(&(dev->parent), RT_Object_Class_Device, name);
dev->flag = flags;
dev->ref_count = 0;
dev->open_flag = 0;
#if defined(RT_USING_POSIX)
dev->fops = RT_NULL;
rt_wqueue_init(&(dev->wait_queue));
#endif
return RT_EOK;
}
RTM_EXPORT(rt_device_register);
// include\rtdef.h
struct rt_device_ops
{
/* common device interface */
rt_err_t (*init) (rt_device_t dev);
rt_err_t (*open) (rt_device_t dev, rt_uint16_t oflag);
rt_err_t (*close) (rt_device_t dev);
rt_size_t (*read) (rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size);
rt_size_t (*write) (rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size);
rt_err_t (*control)(rt_device_t dev, int cmd, void *args);
};
从上面的代码中可以看到,pin与usart设备的注册也是分层的,拿操作函数举例,pin设备的操作函数集是rt_pin_ops类型的_stm32_pin_ops,usart设备的操作函数集是rt_uart_ops类型的stm32_uart_ops,它们分别被注册到设备结构体rt_device_pin与rt_serial_device中,而这两个设备结构体的父设备都是rt_device,在rt_device中有供用户调用的统一I/O设备管理接口rt_device_ops。
这里可能会有疑问,设备管理接口rt_device_ops与其它的设备驱动函数集比如rt_pin_ops与rt_uart_ops并不一一对应怎么办?自然由设备框架层(components\drivers)实现这种转换,限于篇幅具体过程就不在这里展开了。跟底层移植相关的,主要是_stm32_pin_ops与stm32_uart_ops函数操作集的实现部分,这部分主要由设备驱动层实现(bsp\stm32\libraries\HAL_Drivers),我们重点看下各自的初始化函数代码:
// bsp\stm32\libraries\HAL_Drivers\drv_gpio.c
static void stm32_pin_mode(rt_device_t dev, rt_base_t pin, rt_base_t mode)
{
const struct pin_index *index;
GPIO_InitTypeDef GPIO_InitStruct;
index = get_pin(pin);
if (index == RT_NULL)
{
return;
}
/* Configure GPIO_InitStructure */
GPIO_InitStruct.Pin = index->pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
if (mode == PIN_MODE_OUTPUT)
{
/* output setting */
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
}
else if (mode == PIN_MODE_INPUT)
{
/* input setting: not pull. */
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_NOPULL;
}
else if (mode == PIN_MODE_INPUT_PULLUP)
{
/* input setting: pull up. */
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
}
else if (mode == PIN_MODE_INPUT_PULLDOWN)
{
/* input setting: pull down. */
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLDOWN;
}
else if (mode == PIN_MODE_OUTPUT_OD)
{
/* output setting: od. */
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
}
HAL_GPIO_Init(index->gpio, &GPIO_InitStruct);
}
// bsp\stm32\libraries\HAL_Drivers\drv_usart.c
static rt_err_t stm32_configure(struct rt_serial_device *serial, struct serial_configure *cfg)
{
struct stm32_uart *uart;
RT_ASSERT(serial != RT_NULL);
RT_ASSERT(cfg != RT_NULL);
uart = (struct stm32_uart *)serial->parent.user_data;
RT_ASSERT(uart != RT_NULL);
uart->handle.Instance = uart->config->Instance;
uart->handle.Init.BaudRate = cfg->baud_rate;
uart->handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;
uart->handle.Init.Mode = UART_MODE_TX_RX;
uart->handle.Init.OverSampling = UART_OVERSAMPLING_16;
switch (cfg->data_bits)
{
case DATA_BITS_8:
uart->handle.Init.WordLength = UART_WORDLENGTH_8B;
break;
case DATA_BITS_9:
uart->handle.Init.WordLength = UART_WORDLENGTH_9B;
break;
default:
uart->handle.Init.WordLength = UART_WORDLENGTH_8B;
break;
}
switch (cfg->stop_bits)
{
case STOP_BITS_1:
uart->handle.Init.StopBits = UART_STOPBITS_1;
break;
case STOP_BITS_2:
uart->handle.Init.StopBits = UART_STOPBITS_2;
break;
default:
uart->handle.Init.StopBits = UART_STOPBITS_1;
break;
}
switch (cfg->parity)
{
case PARITY_NONE:
uart->handle.Init.Parity = UART_PARITY_NONE;
break;
case PARITY_ODD:
uart->handle.Init.Parity = UART_PARITY_ODD;
break;
case PARITY_EVEN:
uart->handle.Init.Parity = UART_PARITY_EVEN;
break;
default:
uart->handle.Init.Parity = UART_PARITY_NONE;
break;
}
if (HAL_UART_Init(&uart->handle) != HAL_OK)
{
return -RT_ERROR;
}
return RT_EOK;
}
这两个初始化函数对于熟悉HAL库编程的同学应该眼熟吧(可以参考博客:HAL详解与CubeMX使用与USART + DMA + HAL),相当于RT-Thread已经为我们提供了设备驱动函数,而且也是通过调用HAL库函数实现的,我们只需要实现配置具体引脚的MspInit() / MspDeinit()函数即可,而这两个函数是可以靠CubeMX图形化配置后生成的。
看到这里,是不是觉得RT-Thread的移植简单多了,而且以后新增设备驱动也方便多了,我们只需要使用CubeMX图形化配置工具生成MspInit() / MspDeinit()代码,然后将该设备注册到RT-Thread的I/O设备模型框架中,该I/O设备模型框架就可以为我们开发应用程序提供一套统一便利的设备接口函数,对设备的访问一下子简单了许多。当然,实际中可能还需要配置一些宏定义,有些设备接口函数实现的并不完整,比如前面介绍的rt_pin_ops只实现了上层的read / write / control操作,如果要进行初始化配置还是需要调用stm32_pin_mode。
限于篇幅,RT-Thread系统CPU架构与BSP移植部分放到下一篇博客中介绍。