操作系统是用来管理系统硬件、软件及数据资源,控制程序运行,并为其它应用软件提供支持的一种系统软件。根据不同的种类,又可分为实时操作系统、桌面操作系统、服务器操作系统等。对于一些小型的应用,对系统实时性要求高,硬件资源有限等的情况下,应尽量避免使用复杂庞大的操作系统,使用小型的实时操作系统更能满足应用的需求。笔者此处就华为LiteOS物联网操作系统的移植作一个简单的介绍。

1. LiteOS概述

LiteOS是华为针对物联网领域推出的轻量级物联网操作系统,具有轻量级、低功耗、互联互通等特点,广泛应用于可穿戴设备、智能家居、车联网等领域。

LiteOS源码下载地址,https://github.com/LiteOS/LiteOS 。源码主要有以下几个目录:

LiteOS移植 - 图1

LiteOS源码目录

arch目录,该目录包含跟CPU体系结构相关的代码,LiteOS专为小内核架构设计,可以满足硬件资源受限的应用。当前的源码仅支持功耗、成本敏感的ARM Cortex-M以及MSP430这两种架构CPU。本文档以ARM7、ARM9、ARM11、Cortex-A的CPU架构为例,说明arch目录下CPU架构相关的接口移植。

components目录,该目录包含了一系列的组件。端云互通组件connectivity集成了LwM2M、nb_iot、mqtt等IoT互联互通协议栈。文件系统组件fs集成了vfs虚拟文件系统、fatfs、devfs等适合嵌入式使用的文件系统。net组件集成了lwip TCP/IP协议栈、at指令设备等。ota组件支持设备远程下载升级等等。

demos目录,该目录包含了LiteOS、components组件Demo代码。

doc目录,该目录包含了相应的文档说明。

include目录,该目录包含了components组件使用的头文件。

kennel目录,该目录包含LiteOS内核头文件和源码。

osdepends目录,该目录实现LiteOS封装CMSIS-RTOS接口标准,兼容CMSIS-RTOS应用程序。

targets目录,该目录包含了已经实现运行的相关板级工程。

tests目录,该目录包含了相应的测试代码。

2. 代码准备

Bootloader工程,Bootloader是s3c2416/50/51这系列arm9芯片在运行c代码main函数之前必须先运行的代码,启动代码支持sd、Nand启动,设置系统时钟,初始化内存,自动识别启动设备并搬移代码到RAM,MMU映射,中断管理等,只需专注于用c开发其它功能函数即可。关于Bootloader以及Bootloader的实现过程,笔者前面章节有非常详细的介绍。此处以MDK下移植LiteOS为讲解。

应用代码,用c开发的所有功能代码,其中,应用代码入口为main()函数,在这里实现LiteOS多任务运行代码。

3. LiteOS移植

3.1. LiteOS内核代码

把kernel目录拷贝到MDK工程,添加该目录如下C文件。

los_init.c文件。

base/core目录下所有C文件。

base/ipc目录下所有C文件。

base/mem/bestfit_little目录下所有C文件。

base/mem/common目录下所有C文件。

base/mem/membox目录下所有C文件。

base/misc目录下所有C文件

base/om目录下所有C文件

extended/tickless目录下所有C文件

LiteOS移植 - 图2

内核文件

配置工程内核头文件路径。

LiteOS移植 - 图3

内核头文件路径

3.2. OS_CONFIG目录

OS_CONFIG目录包含三个文件los_builddef.h、los_printf.h、target_config.h,其中los_builddef.h定义了内核编译的数据结构布局,相应的内核数据结构放在内核空间等等,无特殊需求可直接采用其它板级实现的该文件。los_printf.h定义了内核警告打印、报警输出等内核调试宏实现,可直接采用其它板级实现的文件。target_config.h为目标板级配置内核文件,该文件可以配置内核Tick、任务数、内存地址及大小等等,LiteOS是可裁减实时操作系统,可以根据实际的应用对内核未使用到的功能进行裁减以及配置,以进一步节省系统宝贵的硬件资源。

LiteOS移植 - 图4

OS_CONFIG目录

3.3. arch目录

LiteOS内核管理需要切换任务上下文,开关中断禁止任务抢占保护临界区,内核超时、计时、延时等时间处理需要硬件定时器实现系统Tick。这些都是跟CPU架构相关的,需要进行移植,移植文件如下:

LiteOS移植 - 图5

arch目录
3.3.1. los_hwi.h/los_hwi.c

los_hwi.h/los_hwi.c,该文件处理硬件中断相关的接口实现,如果配置LiteOS接管硬件中断,内核提供一套标准的中断管理API,实现中断初始化、中断注册、中断注销等操作。如果没有配置LiteOS接管硬件中断,或者应用层不使用这一套标准的中断管理接口,则该文件接口并不是必须实现的。

3.3.2. los_hw_tick.h/los_hw_tick.c

内核需要一个周期性硬件定时中断作为LiteOS的运行Tick,每个Tick需调用osTickHandler()来让内核管理时钟节拍。如果使能了内核Tickless功能,还需要实现重配置硬件定时器的接口,跟硬件定时器相关的接口在los_hw_tick.h/los_hw_tick.c文件实现。

3.3.2.1. osTickStart()接口

内核硬件定时器开启接口为osTickStart(),在接口中配置Timer4每秒钟产生100次(LOSCFG_BASE_CORE_TICK_PER_SECOND)系统Tick。

LiteOS移植 - 图6

3.3.3. los_hw.h/los_hw.c

内核需要实现任务调度、任务上下文切换、任务栈初始化、低功耗待机睡眠实现、开关中断,这些都是跟具体的硬件息息相关的。在los_hw.h/los_hw.c中实现相应的接口,由于任务上下文切换、开关中断,需要操作CPU内部寄存器,使用汇编语言才能处理,在los_dispatch_keil.S文件中实现这些汇编接口。

3.3.3.1. LOS_Schedule()接口

该接口决定任务是否进行调度,任务同步事件、IPC通信事件等可能会使更高优先级的任务就绪,从而需要调度。如果在中断执行过程中使高优先级任务就绪,此时只有中断上下文,不允许进行任务的调度。其他情况,只要有更高优先级的任务就绪,调度未上锁,则进行任务调度。

LiteOS移植 - 图7

3.3.3.2. osTskStackInit()接口

该接口用来初始化任务栈,使创建的任务具有可运行的任务上下文,任务上下文入栈与出栈必须一一对应。

LiteOS移植 - 图8

3.3.3.3. osEnterSleep()接口

LiteOS作为一款物联网操作系统,可以满足不同设备的低功耗需求。如果内核配置了低功耗选项,需要实现osEnterSleep()接口,内核在Idle任务将调用osEnterSleep()进入低功耗模式,等待中断、外部唤醒事件使CPU再次进入运行状态。

LiteOS移植 - 图9

3.3.4. los_dispatch_keil.S

任务上下文切换、中断上下文切换、开关中断,需要操作CPU内部寄存器,使用汇编语言才能处理,在los_dispatch_keil.S文件中实现这些汇编接口。

3.3.4.1. LOS_IntLock()和LOS_IntRestore()接口

对于可抢占式操作系统,有一小段关键代码必须独占访问,如果有一个任务正在访问临界代码,则其它任务不能再进入该段代码,直到占有访问权的任务退出这个临界区。LiteOS在访问内核临界区时是通过LOS_IntLock()/LOS_IntRestore()这两个接口来确保临界区不被破坏。

LiteOS移植 - 图10

3.3.4.2. LOS_StartToRun()接口

当用户通过LOS_Start()启动LiteOS内核进行管理时,LOS_Start ()会首先调用LOS_StartToRun ()来运行已创建任务中优先级最高的任务,LOS_StartToRun ()需完成以下工作:

(1) 禁止中断切换到管理模式,所有任务均工作在管理模式

(2) 标记LiteOS内核已启动运行,g_bTaskScheduled = 1

(3) 获取全局任务结构,把最新任务TCB设置为当前运行TCB

(4) 设置新的运行任务为运行状态

(5) 获取新运行任务栈指针,SP切换到任务栈

(6) 出栈SP中的任务栈,包括任务状态寄存器CPSR,R0-R12,LR,继续执行任务

LiteOS移植 - 图11

3.3.4.3. osSchedule()接口

LiteOS通过osSchedule ()函数进行任务的调度,任务切换函数osSchedule ()需完成以下的工作:

(1) 保存当前任务的上下文(R0-R12,LR,任务打断的PC地址,状态寄存器CPSR)到当前任务栈中

(2) 如果任务切换钩子函数不为空,先调用g_pfnTskSwitchHook()钩子函数

(3) 由全局任务结构,获得运行任务栈指针,并把运行任务SP栈保存进栈指针

(4) 清除当前运行任务的运行状态

(5) 把新任务TCB设置为当前运行TCB

(6) 设置新的运行任务为运行状态

(7) 获取新运行任务栈指针,SP切换到任务栈

(8) 出栈新任务的上下文,执行新任务

LiteOS移植 - 图12

3.3.4.4. IRQ_SaveContext()接口

任何异常发生时,均会打断任务,进入异常应先保存当前任务的上下文到当前任务栈中,之后再执行异常处理。IRQ异常也不例外,因为LiteOS需要一个定时器中断Tick,因此IRQ处理也是移植的一部分,IRQ_SaveContext()需完成以下工作:

(1) 临时性使用到一些寄存器,对用到的寄存器压栈到IRQ栈上

(2) 切换到管理模式,禁止中断,任务运行在管理模式,这步将切换SP到被中断打断的任务栈上

(3) 把被打断任务的上下文压入任务的栈

(4) 跟踪中断嵌套计数,判断是任务被中断还是中断嵌套,中断嵌套不用更新任务栈

(5) 非中断嵌套,根据当前任务TCB(任务控制块)获得栈指针,并把打断任务SP栈保存进栈指针

(6) 中断嵌套计数加1

(7) 切换到系统模式,并压栈LR,这步是为了使用系统模式栈来处理中断函数,减轻任务栈的使用

(8) 调用IRQ_Handler()函数实质处理IRQ中断服务,在中断服务中可再打开IRQ中断,支持中断嵌套

(9) 中断服务执行完后,出栈LR,并切换到管理模式,禁止中断,此时SP将切换到被打断任务的任务栈上

(10) 中断嵌套计数减1,如果中断嵌套计数OSIntNesting为0,则说明所有中断退出,将调用LOS_Schedule()进行新任务切换,继续执行任务

(11) 如果中断嵌套计数OSIntNesting不为0,中断未全部退出,则出栈上一个中断的上下文,执行被嵌套的上一级中断

LiteOS移植 - 图13

4. 用户代码

在main()函数中需调用LOS_KernelInit()初始化内核,创建任务后,再调用LOS_Start()把CPU管理权交给内核。内核即可正确地管理用户的任务。

#include "ProjectConfig.h"

#include "Speed.h"

#include "Uart.h"

#include "Timer.h"

#include "Gpio.h"

#include "math.h"



#include "los_base.h"

#include "los_task.ph"

#include "los_typedef.h"

#include "los_sys.h"



static UINT32 g_start_tskHandle;

static UINT32 g_task1_tskHandle;

static UINT32 g_task2_tskHandle;

static UINT32 g_task3_tskHandle;



void Task3(void *pdata)

{

      (void)pdata;

      while (1) {

            Gpio_LED3(1);

            LOS_TaskDelay(100); // LED3 1000ms闪烁

            Gpio_LED3(0);

            LOS_TaskDelay(100); // LED3 1000ms闪烁      

      }

}



void Task2(void *pdata)

{

      int n;    

      (void)pdata;

      n = 1;

      while (1) {

            printf("Task2: n=%d, square root %f\n", n, sqrt(n));

            n += 1;

            LOS_TaskDelay(700);

      }

}



void Task1(void *pdata)

{

#define PI 3.14159f  

      float r;  

      (void)pdata;

      r = 1.0f;

      while (1) {

            printf("Task1: r=%.1f, s=%.2f\n", r, PI*r*r);

            r += 1.0f;

            LOS_TaskDelay(200);

      }

}



void TaskStart(void)

{

      UINT32 uwRet;

      TSK_INIT_PARAM_S task_init_param;

      Gpio_Init();



      task_init_param.usTaskPrio = 1;

      task_init_param.pcName = "task1";

      task_init_param.pfnTaskEntry = (TSK_ENTRY_FUNC)Task1;

      task_init_param.uwStackSize = 0x1000;

      uwRet = LOS_TaskCreate(&g_task1_tskHandle, &task_init_param);

      if (uwRet != LOS_OK) {

            printf("task1 failed\n");

      }



      task_init_param.usTaskPrio = 1;

      task_init_param.pcName = "task2";

      task_init_param.pfnTaskEntry = (TSK_ENTRY_FUNC)Task2;

      task_init_param.uwStackSize = 0x1000;

      uwRet = LOS_TaskCreate(&g_task2_tskHandle, &task_init_param);

      if (uwRet != LOS_OK) {

            printf("task2 failed\n");

      }



      task_init_param.usTaskPrio = 1;

      task_init_param.pcName = "task3";

      task_init_param.pfnTaskEntry = (TSK_ENTRY_FUNC)Task3;

      task_init_param.uwStackSize = 0x1000;

      uwRet = LOS_TaskCreate(&g_task3_tskHandle, &task_init_param);

      if (uwRet != LOS_OK) {

            printf("task3 failed\n");

      }



      while (1) {

            Gpio_LED2(1);

            LOS_TaskDelay(50); // LED2 500ms闪烁

            Gpio_LED2(0);

            LOS_TaskDelay(50); // LED2 500ms闪烁

      }
}



int  main (void)

{   

      UINT32 uwRet;

      TSK_INIT_PARAM_S task_init_param;



      Uart_Init();

      printf("CPU:   S3C2416@%dMHz\n", get_ARMCLK()/1000000);

      printf("       Fclk = %dMHz, Hclk = %dMHz, Pclk = %dMHz\n",

                  get_FCLK()/1000000, get_HCLK()/1000000, get_PCLK()/1000000);



      uwRet = LOS_KernelInit();

      if (uwRet != LOS_OK) {

            return LOS_NOK;

      }



      task_init_param.usTaskPrio = 0;

      task_init_param.pcName = "start";

      task_init_param.pfnTaskEntry = (TSK_ENTRY_FUNC)TaskStart;

      task_init_param.uwStackSize = 0x1000;

      uwRet = LOS_TaskCreate(&g_start_tskHandle, &task_init_param);

      if(LOS_OK != uwRet) {

            return uwRet;

      }



      LOS_Start();

      return 0;

}

LiteOS移植 - 图14

运行LiteOS

5. 附录

本篇LiteOS接口部分的移植对于ARM7、ARM9、ARM11、Cortex-A都是适用的,不同型号的CPU只需加入定时器产生系统Tick,通过调用osTickHandler()来让内核管理时钟节拍。总的来说,熟悉一款操作系统的工作原理,了解其任务调度、信号量同步、临界区访问等概念,对学习其它操作系统、多线程编程等均是有很较大的帮助的。
源码:https://pan.baidu.com/s/19Xv3VESKKF88RAJJ6LSqkw 提取码:wvy8