IOT-OS之RT-Thread(一)--- 系统启动与初始化过程

STM32 同时被 3 个专栏收录
20 篇文章 9 订阅
26 篇文章 12 订阅
91 篇文章 43 订阅

一、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两大类,纵向架构大致可分为云端、边缘端、终端、芯片端四大类:
IOT-OS应用场景

目前嵌入式领域ARM架构比较流行,我们先看看ARM推出的Mbed OS系统组件都有哪些:
Mbed-OS体系结构
Mbed OS对网络支持能力挺强的,对于IP联网、非IP联网、TLS安全加密传输都有丰富的支持,同时也对文件系统和设备抽象有不错的支持,得益于ARM的芯片设计能力,Mbed OS对ARM架构芯片的支持更友好强大。

下面再看看国内推出的类Linux风格的RT-Thread系统组件:
RT-Thread体系结构
RT-Thread也支持丰富的组件,主要可分为三层:接入云端的Web软件包、设备端的组件服务层、底层的内核/BSP层。RT-Thread作为国产IOT-OS,中文资料比较丰富,国内也有不错的市场占有率,仿Linux编程风格,支持类似Shell的FinSH控制台、POSIX API、WebSocket、C++ API等让熟悉Linux的开发者比较有亲切感。

下面再看看注重云端服务的AliOS Things结构框图:
AliOS Things结构框图
AliOS Things比较得益于阿里云的优势,靠近云端的支持能力比较强,对底层硬件的兼容性就没那么强了。

最后,再看下风头正劲的华为开发的LiteOS结构框图如下:
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 内核,是 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目录结构
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启动过程如下图所示:
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 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")

用来实现自动初始化功能的宏接口定义详细描述如下表所示:

初始化顺序宏接口描述
1INIT_BOARD_EXPORT(fn)非常早期的初始化,此时调度器还未启动
2INIT_PREV_EXPORT(fn)主要是用于纯软件的初始化、没有太多依赖的函数
3INIT_DEVICE_EXPORT(fn)外设驱动初始化相关,比如网卡设备
4INIT_COMPONENT_EXPORT(fn)组件初始化,比如文件系统或者 LWIP
5INIT_ENV_EXPORT(fn)系统环境初始化,比如挂载文件系统
6INIT_APP_EXPORT(fn)应用初始化,比如 GUI 应用

初始化函数主动通过这些宏接口申明,如 INIT_BOARD_EXPORT(rt_hw_usart_init),链接器会自动收集所有被申明的初始化函数,放到 RTI 符号段中,该符号段位于内存分布的 RO 段中,该 RTI 符号段中的所有函数在系统初始化时会被自动调用。

2.3 FinSH初始化过程

跟上面的自动初始化宏接口类似,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宏定义。

用来导出自定义函数/变量的宏接口定义详细描述如下表所示:

模式宏接口描述
MSHMSH_CMD_EXPORT(command, desc)将一个自定义命令导出到 msh 模式;
MSHMSH_CMD_EXPORT_ALIAS(command,
alias, desc)
导出一个自定义命令,并重定义显示到
msh模式的命令别名;
C-StyleFINSH_FUNCTION_EXPORT(name, desc)将一个自定义命令导出到 C-Style 模式;
C-StyleFINSH_FUNCTION_EXPORT_ALIAS(
name, alias, desc)
导出一个自定义命令,并重定义显示到
C-Style模式的命令别名;
C-StyleFINSH_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命令执行流程供了解大概脉络:
FinSH命令执行流程

2.4 hw_board初始化过程

下面介绍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 设备模型框架:
I/O设备模型框架
RT-Thread设备模型框架主要分为三层,从上到下分别是 I/O 设备管理层、设备驱动框架层、设备驱动层,各层大概作用如下:

  • 应用程序:通过 I/O 设备管理接口获得正确的设备驱动,然后通过这个设备驱动与底层 I/O 硬件设备进行数据(或控制)交互;
  • I/O 设备管理层:实现了对设备驱动程序的封装,应用程序通过 I/O
    设备层提供的标准接口访问底层设备,设备驱动程序的升级、更替不会对上层应用产生影响。这种方式使得设备的硬件操作相关的代码能够独立于应用程序而存在,双方只需关注各自的功能实现,从而降低了代码的耦合性、复杂性,提高了系统的可靠性;
  • 设备驱动框架层:是对同类硬件设备驱动的抽象,将不同厂家的同类硬件设备驱动中相同的部分抽取出来,将不同部分留出接口,由驱动程序实现;
  • 设备驱动层:是一组驱使硬件设备工作的程序,实现访问硬件设备的功能。它负责创建和注册 I/O 设备,对于操作逻辑简单的设备,可以不经过设备驱动框架层,直接将设备注册到 I/O 设备管理器中,使用序列图如下图所示:
    设备驱动注册访问流程
    下面继续看设备pin与usart的注册过程,代码如下:
// 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移植部分放到下一篇博客中介绍。

更多文章:

  • 8
    点赞
  • 0
    评论
  • 46
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值