实时操作系统mbedOS启动流程剖析

2020-10-19 04:40刘长勇王宜怀蔡闯华蒋建武
计算机工程与应用 2020年20期
关键词:主线内核调用

刘长勇,王宜怀 ,蔡闯华,蒋建武

1.武夷学院 数学与计算机学院,福建 武夷山 354300

2.苏州大学 计算机科学与技术学院,江苏 苏州 215006

3.认知计算与智能信息处理福建省高校重点实验室,福建 武夷山 354300

1 引言

2014 年ARM 公司推出了mbedOS,它是一种专为物联网(IoT)中的“物体”设计的开源嵌入式实时操作系统(Real-Time Operating System,RTOS)[1-2],能提供精确的实时控制,以保证系统的实时性需求[3],具有线程管理与调度、内存管理、时间管理、队列管理等基本功能要素,在协议栈和IP 网络组件[4]、通信技术和安全访问服务机制[5]、物联网设备平台[6]等方面得到广泛应用。基于RTOS 的嵌入式开发,对应用系统的稳定性、实时性和启动时间等都有严格的要求,在启动过程中要完成栈空间、堆空间、线程栈大小、时间嘀嗒、线程调度机制等方面的设置[7],具有启动时间短且复杂性高的特点。因此,充分理解RTOS 的启动流程,有助于开发人员设计出响应速度快、稳定性强的嵌入式系统。目前,有关操作系统启动的研究集中在Android 操作系统[8]、MQX 操作 系 统[9]、μC/OS-III 操 作 系 统[10]、Linux 操 作 系 统[11]、MTX 操作系统[12]以及ARM 嵌入式系统的启动过程[13]等方面,但对mbedOS的启动剖析研究方面缺乏相关资料。为此,本文将利用苏州大学与ARM 公司联合出品的嵌入式开发集成开发环境AHL-GEC-IDE 和金葫芦AHL-A 系列Cortex-M0+内核的KL36 微控制器[14](即AHL-AN100VL 型号开发板),基于SD-mbedOS 工程框架对mbedOS的启动流程进行分析,剖析其从芯片上电启动,到main函数,最终进入mbedOS启动的全过程,结合关键代码、宏定义函数、流程图、SVC中断[15]等分析其实现的原理,可为mbedOS的应用研究和在不同微控制器上的移植提供基础,也可为其他RTOS的启动分析提供借鉴参考。

2 SD-mbedOS工程框架启动流程

SD-mbedOS工程框架的启动过程分为芯片上电启动和实时操作系统mbedOS 启动两部分,如图1 所示。芯片上电启动包括堆栈指针初始化、启动复位向量、关中断、关闭看门狗、系统时钟初始化、开中断、复制初始化数据至RAM、清空BSS数据段、初始化标准库函数和运行主函数main,这一部分的内容与操作系统无关。当进入主函数main 中调用mbedOS_start 函数时,才会将系统控制权移交给mbedOS,由它完成线程的调度工作。

图1 SD-mbedOS工程框架启动流程

芯片上电启动是从调用startup_MKL36Z4.S这个启动文件开始,芯片内部机制首先从Flash 的0x00000000地址处取出中断向量表第一个表项的内容,赋给内核寄存器主栈指针MSP,即完成堆栈指针的设置;芯片内部机制从Flash 的0x00000004 地址处取出第二个表项(即复位向量Reset_Handler 的首地址),赋给程序计数器PC。然后,关中断、调用系统初始化函数SystemInit 完成系统时钟初始化和开中断。接着,将已初始化的数据复制到RAM 中、清空BSS 数据段和调用__libc_init_array 完成系统标准库函数的初始化。最后,转入main函数执行,调用mbedOS_start函数完成mbedOS启动。

3 mbedOS启动流程解析

从芯片上电启动到main函数后,将进行mbedOS启动。在该函数中会将主线程thd_main 和主线程执行函数app_init 作为参数传入mbedOS_start 函数,由它负责mbedOS 启动。mbedOS 启动过程包括定义临时变量、设置mbedOS 堆栈区、重定向中断向量表至RAM、内核初始化、设置主线程属性、创建主线程、启动内核等方面。

3.1 设置堆栈区和重定向中断向量表

(1)定义临时变量

定义三个临时变量用于创建主线程,变量main_obj用于存储线程控制块,变量main_attr用于存储将要创建的主线程属性,main_stack数组用来作为主线程的栈空间。

os_thread_t main_obj;

osThreadAttr_t main_attr;

__attribute__((aligned(8)))char main_stack[512];

(2)设置mbedOS堆栈区

在mbedOS中只有一个主栈,主栈的栈底位置通常设置在RAM 的最高地址加1 处,由高地址向低地址方向分配栈空间,主栈指针MSP 指向栈顶位置。堆通常用于存放临时变量,由程序员动态分配和释放,它一般采用链表的方式来管理变量,堆在内存中位于bss 区和栈区之间,堆是从RAM 的低地址向高地址方向使用。调用函数mbed_set_stack_heap 设置mbedOS 的堆与栈的起始位置和大小,主要是对已定义的四个变量进行初始化操作。

//取得空闲RAM起始地址与大小

unsigned char *free_start=HEAP_START;

uint32_t free_size=HEAP_SIZE;

//初始化栈大小与起始地址

mbed_stack_isr_size=ISR_STACK_SIZE<free_size?ISR_STACK_SIZE:free_size;

mbed_stack_isr_start=free_start+free_size-mbed_stack_isr_size;

free_size-=mbed_stack_isr_size;

//初始化堆大小与起始地址

mbed_heap_size=free_size;

mbed_heap_start=free_start;

(3)重定向中断向量表

在系统启动时中断向量表是在Flash中的,位于Flash的0x00000000地址处,通过函数mbed_cpy_nvic重定向中断向量表到RAM 中,实际上就是将中断向量表拷贝到RAM中。这样做的好处在于当用户程序需要改写中断服务程序时,可以将相应的中断向量指向用户改写后的中断服务程序,即将中断向量表中相应的表项改写为中断服务程序的地址。

//取得系统控制块VTOR寄存器的值

uint32_t*old_vectors=(uint32_t*)SCB->VTOR;

//内存地址起始地址:0x1FFFF800

uint32_t*vectors=(uint32_t*)NVIC_RAM_VECTOR_ADDRESS;

//将48个中断向量拷贝到内存地址中

for(int i=0;i<NVIC_NUM_VECTORS;i++)

vectors[i]=old_vectors[i];

//设置VTOR寄存器指向新的地址

SCB->VTOR=(uint32_t)NVIC_RAM_VECTOR_ADDRESS;

3.2 内核初始化

内核初始化过程主要由内核初始化函数osKernel-Initialize、SVC 触发封装函数__svcKernelInitialize、实际初始化函数svcRtxKernelInitialize 以及中断服务程序SVC_Handler 组成。其调用顺序为osKernelInitialize→__svcKernelInitialize→ 触 发 SVC 中 断 SVC_Handler→svcRtxKernelInitialize。

(1)SVC触发封装函数

内核初始化函数osKernelInitialize 功能是判断当前是否处于中断服务程序中或已经屏蔽了中断,若处于中断服务程序中或已经屏蔽了中断,则返回出错代码;否则调用SVC 触发封装函数。SVC 触发封装函数__svcKernelInitialize 是一个宏定义函数,展开后是C 语言与汇编语言混合编程代码,其功能是为触发SVC 中断服务程序做前期准备工作,主要有:①将要执行的实际内核初始化函数指针放入R7 寄存器中,即将svcRtxKernelInitialize函数地址给R7;②使线程栈指针PSP 中的值为触发SVC 中断后的栈顶;③触发SVC 中断;④将调用R7中函数得到的返回值存放在PSP栈中。其宏定义为SVC0_0M(KernelInitialize,osStatus_t),展开后如下:

#define SVC0_0M(f,t) //宏定义

__attribute__((always_inline)) //强制内联

//定义为静态内联函数

__STATIC_INLINE t __svc##f(void){

SVC_ArgN(0); //定义r0作为通用寄存器

//保存svcRtxKernelInitialize函数地址到R7中

SVC_ArgF(svcRtx##f);

//用于触发SVC中断服务程序

SVC_Call0M(SVC_In0,SVC_Out1,SVC_CL1);

return(t)__r0; } //函数返回值由r0传回

(2)SVC中断服务程序

SVC中断服务程序执行流程如图2所示,分为两部分:前一部分为调用内核初始化实际函数svcRtxKernel-Initialize 前的流程,主要完成对SVC 调用号的判断、读出准备调用函数的入口地址等工作;后一部分为调用内核初始化实际函数svcRtxKernelInitialize 函数(R7 寄存器存放该函数的地址)后的流程,主要完成恢复调用前的堆栈指针、函数调用后的返回值入栈、判断是否进行上下文切换、退出SVC中断等工作。

图2 SVC中断处理程序执行流程

3.3 创建主线程

内核初始化之后,需要创建一个自启动线程,以便内核启动后执行它,由它创建其他用户线程,这个自启动线程称为“主线程(thd_main)”。创建主线程的过程主要由变量定义、创建线程函数osThreadNew、带上下文创建线程函数osThreadContextNew、SVC触发封装函数__svcThreadNew、实际创建线程函数svcRtxThreadNew以及中断服务程序SVC_Handler构成。

其调用顺序为osThreadNew→osThreadContextNew→__svcThreadNew→触发SVC中断服务程序SVC_Handler→svcRtxThreadNew。

(1)创建主线程的准备工作

在mbedOS内核中,使用线程控制块TCB指针来表示一个线程。因此,创建主线程前要给TCB 和栈分配空间,并对属性结构体main_attr进行初始化。

main_attr.stack_mem=main_stack;//栈指针

main_attr.stack_size=sizeof(main_stack);//栈大小

main_attr.cb_mem=&main_obj;//控制块指针

main_attr.cb_size=sizeof(main_obj);//控制块大小

main_attr.priority=osPriorityNormal;//优先级为 24

main_attr.name="main_thread";//名称

(2)主线程的创建

主线程的创建最终是通过触发SVC中断服务程序调用svcRtxThreadNew 函数来完成的,该函数的主要任务包括定义临时变量、判断参数及内存空间的合法性、初始化主线程TCB和线程栈、调用线程提交服务程序、设置主线程状态并放入就绪队列中等方面,其执行流程如图3所示。

图3 主线程创建执行流程

3.4 内核启动

在主线程创建成功后,mbedOS 会把主线程加入到就绪队列中等待调用,接着将进行内核的启动,为操作系统的运行做最后的准备工作。启动内核主要由内核启动函数osKernelStart、SVC触发封装函数__svcKernelStart、实际内核启动函数svcRtxKernelStart 及中断服务程序SVC_Handler 组成。其调用顺序为osKernelStart→__svcKernelStart→触发SVC中断服务程序SVC_Handler→svcRtxKernelStart。

(1)内核启动的实际执行函数

最终实现内核启动的是svcRtxKernelStart 函数,其主要任务包括为线程的调度做好所有必要的准备、创建必要的功能线程、设置时间嘀嗒、使能定时器中断、线程调度、切换栈指针、修改内核状态等方面,其执行流程如图4所示。

图4 内核启动执行流程

(2)运行到主线程

在mbedOS 启动过程中,通过调用svcRtxKernel-Start函数来启动内核。在内核启动期间,先后建立了主线程main_thread(优先级为24)、空闲线程osRtxInfo.thread.idle(优先级为1)和定时器线程osRtxInfo.timer.thread(优先级为40),这三个线程的状态都为就绪态,都被放到就绪队列中,并按优先级高低排列就绪,即定时器线程、主线程和空闲线程。当svcRtxKernelStart 函数执行完成后返回到SVC 中断时,会在SVC 中断中进行上下文切换,此时由于有一个优先级最高的线程(即定时器线程)处于激活态,它的线程控制块指针被放在了osRtxInfo.thread.run.next 中,当前线程与下一线程是不同的(即osRtxInfo.thread.run.curr≠osRtxInfo.thread.run.next),这时就会进行上下文切换,将定时器线程切换为当前线程,当从SVC 中断返回时就会转到定时器线程中执行。在定时器线程osRtxInfo.timer.thread 启动后,先创建一个消息队列,再从消息队列取消息,由于此时消息队列是空的,定时器线程被阻塞。之后mbedOS会进行线程调度,从就绪队列中选择优先级最高的线程(此时为主线程main_thread),将其状态设置为激活态,准备运行。至此,CPU的控制权转交给主线程,接着将由主线程执行函数app_init(定义在08_mbedOsPrgapp_init.cpp 文件中)负责创建用户线程。图5 展示了从定时器线程切换到主线程运行这一过程中的函数调用关系,从中可以看出最终转到app_init函数执行。

4 存储器使用情况分析

在mbedOS 启动过程中,涉及到中断向量表、程序代码、常量、变量、堆、栈等空间分配问题,下面先给出KL36 微控制器结构,然后分析mbedOS 启动过程中Flash和RAM空间的使用情况。

图5 从定时器线程切换到主线程运行的函数调用关系

4.1 KL36微控制器结构

KL36 微控制器包括ARM Cortex-M0+内核、存储器模块、外设模块及相关总线等,存储器模块与32位的高性能系统总线相连,外设模块与32位外设总线相连,还提供扩展总线连接其他外围设备,其结构如图6所示。

图6 KL36微控制器结构

4.2 Flash使用情况分析

KL36 片内 Flash 大 小 为 64 KB,地址范围为0x00000000~0x0000FFFF,一般用来存放中断向量、程序代码、常数等。mbedOS启动后Flash中各个区的地址范围、大小及作用如表1 所示(表中数据采用十六进制表示,下同)。

4.3 RAM使用情况分析

(1)mbedOS启动后RAM使用情况分析

KL36片内RAM为静态随机存储器SRAM,大小为8 KB,地址范围为0x1FFFF800~0x200017FF,一般用来存储全局变量、静态变量、临时变量(堆栈空间)等。该芯片栈空间的使用方向是从大地址向小地址方向进行的。因此,栈空间的栈顶设置在RAM 地址的最大值+1处。而堆空间的使用方向是从小地址向大地址方向进行的,这样可以减少重叠错误。mbedOS启动后RAM中各个段的地址范围、大小及作用如表2所示。

(2)各线程RAM分配情况分析

在mbedOS 的启动过程中,先后建立了主线程main_thread、空闲线程osRtxInfo.thread.idle、定时器线程osRtxInfo.timer.thread,并启动定时器SysTick 中断。在切换到主线程函数app_init 执行之前,这三个线程的RAM 分配情况如表3 所示,在链表中的关系如图7 所示。表中的成员名来源于线程控制块结构体和线程属性结构体,sp的值等于stack_mem+stack_size-64(这个64 Byte 的固定区域是用于在线程进行上下文切换时,保存线程的上下文,即R0~R12、R14、R15、xPSR等16个寄存器),0x1FFFF8E0地址表示就绪队列头指针。

当定时器线程启动之后就被阻塞,转由主线程控制CPU 的使用权,在主线程函数app_init 中分别建立红灯线程thd_redlight、蓝灯线程thd_bluelight 和绿灯线程thd_greenlight三个用户线程,当这三个用户线程启动完后,主线程进入阻塞状态。此时,系统中有四个线程,分别是空闲线程、红灯线程、蓝灯线程和绿灯线程,这四个线程的RAM分配如表4所示,在链表中的关系如图8所示。

表1 Flash中的各区地址范围、大小及作用

表2 RAM中的各段地址范围、大小及作用

表3 执行app_init之前系统线程的RAM分配情况表

图7 系统线程之间的关系

表4 执行app_init之后线程的RAM分配情况表

图8 用户线程之间的关系

5 结束语

mbedOS 的启动过程是一个极其复杂的过程,涉及到操作系统运行时所需的栈空间、堆空间、线程控制块等资源的初始化,系统时钟的设置,就绪队列、延时队列和等待队列等的管理,以及对线程的调度,涉及到函数调用关系也极为复杂。本文通过对SD-mbedOS工程框架启动流程的分析,简要地给出了芯片上电启动过程,通过源码、宏定义函数、SVC中断、流程图等方式来着重剖析mbedOS 的启动过程,最后分析了mbedOS 的存储器使用情况。通过剖析,有助于读者快速理解mbedOS的启动过程、调度机制和整体架构,为简化启动流程、优化执行过程、提升启动速度等进一步研究工作提供研究基础,也为mbedOS在不同微控制器上的移植提供了技术基础。本文涉及到的工程可到苏州大学嵌入式学习社区网站(http://sumcu.suda.edu.cn)的“教学培训-教学资料-mbedOS”位置,下载“SD_mbedOS_Start(KL36)”查看。

猜你喜欢
主线内核调用
多内核操作系统综述①
强化『高新』内核 打造农业『硅谷』
活化非遗文化 承启设计内核
核电项目物项调用管理的应用研究
人物报道的多维思考、主线聚焦与故事呈现
更加突出主线 落实四个到位 推动主题教育取得实实在在成效
Linux内核mmap保护机制研究
数字主线
基于系统调用的恶意软件检测技术研究
下沉和整合 辽宁医改主线