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

1. LiteOS概述

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

源码主要有以下几个目录:

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的实现过程,笔者前面章节有非常详细的介绍( https://www.softool.cn/docs/embedded_linux/embedded_linux_20072403.html ),此处以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文件

内核文件:

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

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是可裁减实时操作系统,可以根据实际的应用对内核未使用到的功能进行裁减以及配置,以进一步节省系统宝贵的硬件资源。

OS_CONFIG目录

3.3 arch目录

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

arch目录
  • 3.3.1 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。

  • 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通信事件等可能会使更高优先级的任务就绪,从而需要调度。如果在中断执行过程中使高优先级任务就绪,此时只有中断上下文,不允许进行任务的调度。其他情况,只要有更高优先级的任务就绪,调度未上锁,则进行任务调度。

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

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

  • 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()这两个接口来确保临界区不被破坏。

    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,继续执行任务

    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) 出栈新任务的上下文,执行新任务

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,中断未全部退出,则出栈上一个中断的上下文,执行被嵌套的上一级中断

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, 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
", 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
");
      }

      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
");
      }

      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
");
      }

      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
", get_ARMCLK()/1000000);
      printf("       Fclk = %dMHz, Hclk = %dMHz, Pclk = %dMHz
", 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

5. 附录

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

作者联系方式

作者 QQ QQ群
象棋小子 1048272975 636564526(嵌入式开发)