第3章 内 核 基 础 使用任何一种操作系统都离不开进程/线程、IPC通信、时间管理、异常等核心内容,对于物联网操作系统,开发者还会用到中断、内存、网络协议栈等功能模块。 本章讲解LiteOS中的任务、中断、内存、异常等核心功能组件。 3.1LiteOS内核架构 本节介绍LiteOS基础内核组成,以及启动流程。 14min 3.1.1基础内核 LiteOS的内核包括基础内核和增强内核,绝大多数系统移植需要包含基础内核,而增强内核可以根据实际情况裁剪。基础内核又由一个不可裁剪的极小内核和其他可裁剪模块组成,如图31所示。 图31LiteOS内核 任务管理模块提供了任务的创建、删除、挂起、恢复等功能,同时还提供了任务的调度、锁定、解锁功能。LiteOS支持抢占式调度,即高优先级任务可以打断低优先级任务,对于优先级相同的任务支持时间片轮转调度。 内存管理模块支持内存的申请和释放,提供内存统计、内存越界检测功能。LiteOS提供静态内存和动态内存两种算法,目前支持的内存管理算法有Box算法、Bestfit算法、Bestfit_little算法。 中断管理模块提供了中断的创建、删除、使能及标志位的清除功能; 异常管理模块在系统运行异常之后,可以打印出当前发生异常的函数调用栈信息; 系统时钟模块负责时钟转换,LiteOS以Tick作为系统调度的基本单位。 IPC通信模块提供了信号量、互斥锁、事件、消息队列4种任务的通信机制,每种通信机制都可以单独进行配置。 注意: 在最新版本的LiteOS中,中断已经被系统全面接管,不再提供原始的裸机中断接口。 3.1.2代码结构 LiteOS官方仓库有若干分支,其中master分支的功能最为全面。在使用master分支进行开发之前,首先要了解其代码结构,见表31。 表31LiteOS目录结构 目录说明备注 arch架构支持,目前支持ARM64\ARMA\ARMM\RISCV勿动 build编译脚本,负责检测编译工具,组织编译代码macOS下需要微调,其他系统勿动 compat兼容性支持,主要是CMSIS接口勿动 component组件支持,包含AI、网络、文件系统等组件支持勿动 doc说明文档,关于LiteOS的一切知识都在这里在开发过程中可以参考 demos案例,官方提供的实例程序,例如MQTT、CoAP等供学习测试用,可以修改 drivers驱动,串口和定时器的一些代码,供调试使用勿动 include头文件勿动 Kernel内核,LiteOS核心功能勿动 lib第三方库支持,例如解压功能Zlib勿动 osdepend一些依赖勿动 out编译输出,以开发板名字命名自动生成 shellShell命令支持,类似Windows的命令行勿动 targets目标板支持,列出所有支持的开发板,开发者可以自己添加其他开发板开发者主要修改的地方 test、tests测试用案例勿动 tools编译和构建工具配置macOS下需要微调 3.1.3内核启动流程 在targets目录下列出了许多已经移植成功的开发板,每个开发板对应的目录下都有一个main.c文件,这里就是LiteOS的入口,代码如下: //第3章/main.c VOID BoardConfig(VOID){ g_sys_mem_addr_end = __LOS_HEAP_ADDR_END__; } INT32 main(VOID){ #ifdef __GNUC__ ArchStackGuardInit(); #endif OsSetMainTask(); OsCurrTaskSet(OsGetMainTask()); BoardConfig(); HardwareInit(); PRINT_RELEASE("\n********Hello Huawei LiteOS********\n" "\nLiteOS Kernel Version : %s\n" "build date : %s %s\n\n" "**********************************\n", HW_LITEOS_KERNEL_VERSION_STRING, __DATE__, __TIME__); UINT32 ret = OsMain(); if (ret != LOS_OK) { return LOS_NOK; } OsStart(); return 0; } 首先进行内存结束地址配置BoardConfig()、硬件初始化HardwareInit(),然后打印Huawei LiteOS的版本信息; 接着执行函数OsMain()初始化Huawei LiteOS内核及例程,在OsMain()函数中会创建用户任务,其任务处理函数为app_init(); 最后调用OsStart()开始任务调度,Huawei LiteOS开始正常工作,具体如图32所示。 图32LiteOS内核启动流程 注意: Huawei LiteOS提供了一套自有OS接口(例如LOS_TaskDelay),同时也支持POSIX接口(例如sleep)和CMSIS接口(例如osDelay),请勿混用这些接口,否则可能导致未知错误。例如用POSIX接口加锁互斥量,但用Huawei LiteOS接口解锁互斥量,最终可能导致互斥量无法解锁。开发驱动程序只能用Huawei LiteOS的自有接口,上层App建议用POSIX接口。 3.2任务 任务是系统的核心,整个操作系统都围绕着各种任务在运行。本节介绍LiteOS的任务管理体系。 16min 3.2.1任务的概念 1. 基本概念 在《UNIX高级编程》中,作者给出了两个概念: 进程是资源分配的最小单位; 线程是系统调度的最小单位。Huawei官网给出的任务概念是: 任务是竞争系统资源的最小运行单元。由此可以看出,在LiteOS中任务其实就是线程。 LiteOS的任务管理模块提供了任务的创建、删除、挂起、恢复等操作,可以为用户提供多个任务,每个任务享有独立的内存等资源空间,并且独立于其他任务运行。LiteOS的任务管理模块具有以下几个特性。 (1) 支持多任务。 (2) 支持抢占式调度,高优先级的任务可以打断低优先级的任务。 (3) 优先级相同的任务支持时间片轮转调度。 (4) 有32个优先级(0~31),0为最高优先级,31为最低优先级。 2. 任务状态 在绝大多数的物联网操作系统中都有任务的概念,无论任务代表的是线程还是进程都具有以下4种状态。 (1) 就绪态: 该任务在就绪队列中,只等待CPU。 图33任务状态切换 (2) 运行态: 该任务正在执行。 (3) 阻塞态: 该任务不在就绪队列中,包含任务被挂起(suspend状态)、任务被延时(delay状态)、任务正在等待信号量、读写队列或者等待事件等。 (4) 退出态: 该任务运行结束,等待系统回收资源。 任务的4种状态可以相互切换,如图33所示。 3. 其他任务相关概念 (1) 任务ID: 任务创建成功后通过参数返给用户,系统中的任务ID号是唯一的。用户可以通过任务ID对指定任务进行任务挂起、恢复、查询、删除等操作。 (2) 任务入口函数: 当任务得到调度后就会执行此函数。该函数由用户实现,在任务创建时,通过结构体TSK_INIT_PARAM_S设置。 (3) 任务优先级: 代表任务在执行过程中的优先顺序。当发生任务切换时,执行任务就绪队列中优先级最高的任务。 (4) 任务栈: 任务在运行过程中的独立空间。栈空间里保存的信息包含局部变量、寄存器、函数参数、函数返回地址等。 (5) 任务上下文: 任务在运行过程中使用的一些资源,如通用寄存器、程序状态字等。当一个任务被挂起时,其他任务继续执行,可能会修改寄存器中的值。如果任务切换时没有保存任务上下文,则可能会导致任务恢复后出现未知错误。任务上下文一般保存在任务栈里。 (6) 任务控制块TCB: 每个任务都包含一个任务控制块。TCB包含了任务上下文栈指针(Stack Pointer)、任务状态、任务优先级、任务ID、任务名、任务栈大小等信息。TCB可以反映出每个任务的运行情况,在任务调度时需要用到TCB。 24min 3.2.2创建和删除任务 函数LOS_TaskCreate()可以创建一个任务,当任务被创建之后进入就绪状态。如果就绪队列中没有优先级更高的任务,则运行该任务。函数LOS_TaskCreate()的各个参数说明见表32。 表32LOS_TaskCreate函数说明 返回值类型说明(头文件kernel/include/los_task.h) UINT32如果成功,则返回LOS_OK,如果失败,则返回LOS_ERRNO_TSK_XXX 参数类型说明 taskIdUINT32任务ID,当任务被创建之后自动回填 initParamTSK_INIT_PARAM_S任务参数,是一个结构体 参数initParam成员类型说明 pfnTaskEntryvoid *(*)(void *)任务的入口地址,这是一个函数指针,返回值为void *,参数也是void * usTaskPrioUINT16任务优先级,默认为10 uwStackSizeUINT32任务栈大小,默认为1536字节 pcNamechar *任务名称,方便在调测时使用 uwResvedUINT32保留字节 LiteOS还提供了另外一个创建任务的函数LOS_TaskCreateOnly(),任务被创建之后并不会进入就绪态,而是被挂起。 无论使用哪个函数创建任务,函数LOS_TaskDelete()都可以将任务删除,此函数没有参数。 LiteOS使用LOS_XXX来表示一个函数的返回值,例如LOS_OK代表成功。用户在创建任务时未必会成功,此时需要通过返回值查找原因。下面给出LiteOS任务模块的几个常见错误码,见表33。 表33任务模块错误码 返回值实 际 数 值说明(头文件kernel/include/los_task.h) LOS_ERRNO_TSK_ID_INVALID0x02000207无效的任务ID LOS_ERRNO_TSK_NO_MEMORY0x03000200内存不足,可以尝试设置更大的动态内存池,或者减少系统支持的最大任务数 LOS_ERRNO_TSK_PTR_NULL0x02000201initParam为空指针 LOS_ERRNO_TSK_PRIOR_ERROR0x02000203优先级参数不对,保证优先级在[0,31] LOS_ERRNO_TSK_ENTRY_NULL0x02000204任务入口为空,检查参数initParam.pfnTaskEntry LOS_ERRNO_TSK_NAME_EMPTY0x02000205任务名是空指针 LOS_ERRNO_TSK_STKSZ_TOO_SMALL0x02000206任务栈太小,增加initParam.uwStackSize LOS_ERRNO_TSK_ID_INVALID0x02000207无效的任务ID LOS_ERRNO_TSK_ALREADY_SUSPENDED0x02000208挂起任务时,任务已经被挂起 LOS_ERRNO_TSK_NOT_SUSPENDED0x02000209恢复任务时,任务未被挂起 LOS_ERRNO_TSK_DELAY_IN_INT0x0300020d在中断内使用任务延时 18min 3.2.3任务调度 一般情况下,系统按照时间片轮转法则进行任务调度。内核给每个任务分配固定的Tick,当任务的Tick时间到达后会进入就绪队列,内核从队列头部取出下一个任务执行。用户也可以通过系统提供的一些函数自由控制任务的执行状态,见表34。 表34任务状态控制 函数说明(头文件kernel/include/los_task.h) LOS_TaskDelay任务延时等待,释放CPU,等待时间到期后该任务会重新进入ready状态,单位Tick 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回LOS_ERRNO_XXX,见表33 参数: [IN] UINT32 tick,延时的时间 LOS_TaskYield当前任务释放CPU,并将其移到具有相同优先级的就绪任务队列的末尾,不可以在中断里使用 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回LOS_ERRNO_TSK_XXX,见表33 参数: 无 LOS_TaskSuspend挂起指定的任务,然后切换任务 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回LOS_ERRNO_TSK_XXX,见表33 参数: [IN] UINT32 taskId,任务ID号 LOS_TaskResume恢复挂起的任务,使该任务进入ready状态 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回LOS_ERRNO_TSK_XXX,见表33 参数: [IN] UINT32 taskId,任务ID号 图34任务配置 任务延时函数LOS_TaskDelay()以Tick为单位,默认每秒1000个Tick。在项目中,任务挂起和恢复应该成对使用; 如果使用函数LOS_TaskCreateOnly()创建任务,则函数LOS_TaskResume()会多出一个。 进行任务开发时首先要对当前LiteOS系统进行简单配置,在终端输入make menuconfig指令进入配置菜单,之后选择Kernel→Basic Config→Task,如图34所示。 3.2.4实战案例: 简单任务控制 本章的案例都存储在LiteOSmaster根目录下的mydemos文件夹中,所有的案例均使用小熊派STM32L431_BearPi开发板。为了后续实验操作更快捷, 图35选择目标开发板 这里首先对工程做统一配置,在VS Code中打开LiteOSmaster目录,进入终端,输入指令make menuconfig,选择Targets→Target→STM32L431_BearPi,如图35所示。 本节通过一个简单案例演示任务的操作,有关任务的基础配置,这里选择默认即可。 1. 案例描述 创建两个任务Task1和Task2,两个任务交替执行,每个任务的执行周期为3s。系统启动之后,首先执行任务Task1。 2. 操作流程 图36Task目录结构 1) 创建源文件 在LiteOS源码根目录下创建文件夹mydemos/task,在task文件夹下创建源文件task.c、头文件task.h,最终的目录结构如图36所示。 2) 编辑源代码 (1) 根据案例描述可以分析出,每个任务执行3s后需要挂起自己,然后恢复另一个任务,代码如下: //第3章/mydemos/task/task.c //任务计数器 UINT32 secs1, secs2; UINT32 task1_entry(VOID) { while (1) { //计数器加1 secs1++; printf("task1 demo\n"); if (secs1%3 == 0) { printf("==========\n"); //恢复任务2 LOS_TaskResume(task2_id); //挂起任务1 LOS_TaskSuspend(task1_id); } //延时1000Tick,即1s LOS_TaskDelay(1000); } return 0; } UINT32 task2_entry(VOID) { while (1) { //计数器加1 secs2++; printf("task2 demo\n"); if (secs2%3 == 0) { printf("==========\n"); //恢复任务1 LOS_TaskResume(task1_id); //挂起任务2 LOS_TaskSuspend(task2_id); } //延时1000Tick,即1s LOS_TaskDelay(1000); } return 0; } (2) 使用函数LOS_TaskCreate()创建Task1,为了保证不让Task2先运行,这里使用函数LOS_TaskCreateOnly()创建Task2,代码如下: //第3章/mydemos/task/task.c UINT32 demo_task(VOID) { UINT32 ret; TSK_INIT_PARAM_S param; //锁住任务调度,防止高优先级任务调度 LOS_TaskLock(); //任务名字 param.pcName = "task1"; //任务入口地址 param.pfnTaskEntry = (TSK_ENTRY_FUNC)task1_entry; //任务优先级 param.usTaskPrio = 10; //任务栈 param.uwStackSize = 0x800; //调用系统函数,创建任务。成功后任务处于就绪状态 ret = LOS_TaskCreate(&task1_id, ¶m); if (ret != LOS_OK) { printf("create task1 failed, errno = %x\n", ret); //如果创建任务失败,则直接返回 Goto exit_demo; } param.pcName = "task2"; param.pfnTaskEntry = (TSK_ENTRY_FUNC)task2_entry; param.usTaskPrio = 10; param.uwStackSize = 0x800; //创建任务2,成功后任务处于挂起状态 ret = LOS_TaskCreateOnly(&task2_id, ¶m); if (ret != LOS_OK) { printf("create task2 failed, errno = %x\n", ret); goto exit_demo; } exit_demo: //解锁任务调度 LOS_TaskUnlock(); return ret; } (3) 在头文件task.h文件中添加函数声明,代码如下: //第3章/mydemos/task/task.h #ifndef __TASK1_H #define __TASK1_H #include "los_task.h" #include "sys_init.h" UINT32 demo_task(VOID); #endif (4) 在源文件targets/STM32L431_BearPi/Src/user_task.c文件中调用函数demo_task(),代码如下: VOID app_init(VOID){ printf("app init!\n"); DemoEntry(); demo_task(); } 3) 修改Makefile 将文件task.c和task.h的相对路径添加到文件targets/STM32L431_BearPi/Makefile中,代码如下: #第3章/targets/STM32L431_BearPi/Makefile #将源文件task.c添加到LOCAL_SRCS LOCAL_SRCS += \ ... $(LITEOSTOPDIR)/targets/STM32L431_BearPi/Src/flash_adaptor.c \ $(LITEOSTOPDIR)/mydemos/task/task.c #将头文件路径追加到LOCAL_INCLUDE LOCAL_INCLUDE += \ ... -I $(LITEOSTOPDIR)/include \ -I $(LITEOSTOPDIR)/mydemos/task 图37运行结果 在Makefile中反斜杠“\”代表换行转义,因此以上代码片段实际上是两句话,代码如下: LOCAL_SRCS += ... $(LITEOSTOPDIR)/mydemos/task/task.c LOCAL_INCLUDE += ... -I $(LITEOSTOPDIR)/mydemos/task 3. 运行结果 在终端执行make指令,编译无误之后将代码下载到开发板,运行结果如图37所示。 3.3中断 中断是指在程序运行过程中出现了一个必须由CPU立即处理的事务,由此导致CPU暂停执行当前程序转而执行另外一个程序的过程。 19min 3.3.1LiteOS的中断机制 1. 中断相关结构体 Huawei LiteOS的中断有几个特性: 支持配置中断优先级、支持中断嵌套、支持配置中断个数、支持中断共享。与中断相关的核心头文件是kernel/base/include/los_hwi_pri.h,在这个头文件中定义了两个结构体,即HwiHandleInfo和HwiControllerOps。 HwiHandleInfo结构体用于记录中断处理程序的相关信息,包括中断处理程序、中断共享模式、共享中断链表、中断处理程序执行的次数,代码如下: //第3章/kernel/base/include/los_hwi_pri.h typedef struct tagHwiHandleForm { HWI_PROC_FUNC hook; /* 中断处理函数 */ union { HWI_ARG_T shareMode; /* UINT16,共享中断标记位,最高位1代表共享中断 */ HWI_ARG_T registerInfo; /* 用户注册的中断参数 */ }; #ifdef LOSCFG_SHARED_IRQ struct tagHwiHandleForm *next; /* 共享中断链表 */ #endif UINT32 respCount; /* 中断调用次数 */ } HwiHandleInfo; HwiControllerOps结构体定义与中断操作相关的函数,如触发中断、清除中断、使能中断、失能中断、设置中断优先级、获取当前中断号等,代码如下: //第3章/kernel/base/include/los_hwi_pri.h typedef struct { UINT32 (*triggerIrq)(HWI_HANDLE_T hwiNum); /* 触发中断 */ UINT32 (*clearIrq)(HWI_HANDLE_T hwiNum); /* 清除中断标志 */ UINT32 (*enableIrq)(HWI_HANDLE_T hwiNum); /* 使能中断 */ UINT32 (*disableIrq)(HWI_HANDLE_T hwiNum); /* 失能中断 */ UINT32 (*setIrqPriority)(HWI_HANDLE_T hwiNum, UINT8 priority); /* 设置中断优先级 */ UINT32 (*getCurIrqNum)(VOID); /* 获取当前执行的中断号 */ CHAR *(*getIrqVersion)(VOID); /* 获取中断版本 */ HwiHandleInfo *(*getHandleForm)(HWI_HANDLE_T hwiNum); /* 根据中断号获取中断处理程序信息. */ VOID (*handleIrq)(VOID); /* 中断处理句柄 */ #ifdef LOSCFG_KERNEL_SMP UINT32 (*setIrqCpuAffinity)(HWI_HANDLE_T hwiNum, UINT32 cpuMask); /* 设置CPU亲和性 */ UINT32 (*sendIpi)(UINT32 target, UINT32 ipi); /* 发送核间中断 */ #endif } HwiControllerOps; 2. 中断注册流程 以CortexM4架构为例,LiteOS启动时会调用源文件kernel/base/los_hwi.c中的函数OsHwiInit()进行中断初始化,接着调用源文件drivers/interrupt/arm_nvic.c中的函数ArchIrqInit()完成中断向量初始化,最后用函数OsHwiControllerReg()注册中断控制器操作句柄,如图38所示。 图38LiteOS中断初始化 3.3.2创建中断 LiteOS可以通过menuconfig对中断进行简单配置,进入菜单顶层目录后选择Kernel→Interrupt Management可以配置中断参数: []Enable Interrupt Share代表是否要打开共享中断,()Max Hardware Interrupts代表系统支持的最大中断数,()Interrupt Priority range代表最大中断优先级。 使用函数LOS_HwiCreate()创建中断,此函数有5个参数,见表35。 表35LOS_HwiCreate说明 返回值类型说明(头文件 kernel/include/los_hwi.h) UINT32如果成功,则返回LOS_OK,如果失败,则返回LOS_ERRNO_HWI_XXX,见表36 参数类型说明 hwiNumHWI_HANDLE_T中断号,硬件中断由芯片决定 hwiPrioHWI_PRIOR_T中断优先级,数字越大,优先级越低。不可以超过最大优先级 hwiModeHWI_MODE_T中断模式,0表示普通中断,IRQF_SHARED(0x8000)表示共享中断 hwiHandlerHWI_PROC_FUNC中断处理函数,用户自定义 irqParamHWI_IRQ_PARAM_S中断处理函数的参数,可以为空NULL 第5个参数irqParam是一个结构体,代码如下: //第3章/kernel/base/include/los_hwi_pri.h typedef struct tagIrqParam { int swIrq; /*子中断号 */ VOID *pDevId; /*用来标识产生中断的设备号*/ const CHAR *pName; /*中断的名字,方便用户使用 */ } HWI_IRQ_PARAM_S; 中断共享机制,支持不同的设备使用相同的中断号注册同一中断处理程序,但中断处理程序的入参pDevId(设备号)必须唯一,代表不同的设备,即同一中断号,同一dev只能挂载一次,但同一中断号,同一中断处理程序,如果dev不同,则可以重复挂载。 共享中断并不是可以随意使用的,它要求处理器在硬件上已经实现了中断共享。例如,STM32的PA0和PB0都可以挂载到外部中断0,但这并不是LiteOS的共享中断,因为同一时刻只可以挂载一个引脚,而PA5和PA6在硬件上已经同时挂载到EXTI9_5_IRQn,因此可以让PA5和PA6共享中断。 注意: 对于CortexM系列,0~15为系统中断,因此中断号应该类似EXTI1_IRQn+16。 创建中断未必总是成功的,需要通过返回值找到出错原因,LiteOS中断模块常见错误码见表36。 表36中断模块错误码 返回值十 六 进 制说明(头文件kernel/include/los_hwi.h) LOS_ERRNO_HWI_NUM_INVALID0x02000900中断号无效 LOS_ERRNO_HWI_PROC_FUNC_NULL0x02000901中断处理程序为空 LOS_ERRNO_HWI_NO_MEMORY0x02000903创建中断时内存不足,需要增加动态内存池 LOS_ERRNO_HWI_ALREADY_CREATED0x02000904中断号已经被创建。对于非共享中断,应仔细检查是否已经注册过; 对于共享中断,应检查中断入口参数的设备ID是否重复 LOS_ERRNO_HWI_PRIO_INVALID0x02000905中断优先级无效,需要参考硬件手册 LOS_ERRNO_HWI_MODE_INVALID0x02000906中断模式无效,应该是0或者1 LOS_ERRNO_HWI_SHARED_ERROR0x02000909创建共享中断时没有设置设备ID,或者要创建非共享中断,但是中断号已经被注册为共享中断 函数LOS_HwiDelete()可以删除中断,有两个参数: hwiNum是中断号,irqParam是中断入口参数。由于用户可能同时为一个中断号注册多个共享中断,因此删除时必须通过入口参数来判断要删除的中断是哪一个。 3.3.3中断控制 LiteOS的中断模块为用户提供了若干中断控制接口,具体见表37。 表37中断控制函数 函数说明(头文件kernel/include/los_hwi.h) LOS_IntLock关闭当前处理器的所有中断响应 返回值: UINT32,关中断之前的CPSR值 参数: 无 LOS_IntUnLock恢复当前处理器的所有中断响应 返回值: UINT32,打开中断之后的CPSR值 参数: 无 LOS_IntRestore让处理器恢复到使用LOS_IntLock之前的状态 返回值: 无 参数: [IN] UINT32 intSave,关中断之前的CPSR值 LOS_HwiEnable使能指定中断 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回LOS_ERRNO_HWI_XXX,见表36 参数: [IN] HWI_HANDLE_T hwiNum,中断号 LOS_HwiDisable失能指定中断 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回LOS_ERRNO_HWI_XXX,见表36 参数: [IN] HWI_HANDLE_T hwiNum,中断号 LOS_HwiSetPriority设置中断优先级 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回LOS_ERRNO_HWI_XXX,见表36 参数1: [IN] HWI_HANDLE_T hwiNum,中断号 参数2: [IN] HWI_PRIOR_T priority,优先级 如果在开发过程中使用函数LOS_IntLock()关闭了全局中断,则在调用函数LOS_IntUnLock()恢复中断响应之前首先要调用函数LOS_IntRestore()恢复处理器的状态,并且函数LOS_IntRestore()的参数必须是函数LOS_IntLock()的返回值。 虽然LiteOS提供了函数LOS_HwiClear()来清除中断标记位,但是笔者在STM32中反复测试此函数没有任何效果,因此推荐开发者使用HAL库自带的标记位清除函数。 注意: 中断处理函数不能耗时太长,否则会影响CPU对中断的响应; 另外中断处理程序也不能执行可能引起系统调度的函数,例如LOS_TaskDelay()。 21min 3.3.4实战案例: 独立中断 1. 案例描述 使用独立中断控制按钮,每按一次输出一条语句。笔者使用STM32L431_BearPi开发板,两个按钮分别连接在引脚PB2、PB3。关于中断的基础配置,这里使用系统默认值。 图39目录结构 2. 操作流程 1) 创建源文件 在mydemos目录下创建文件夹hwi,在hwi目录下创建源文件hwi.c和头文件hwi.h,最终的目录结构如图39所示。 2) 编辑源代码 (1) 利用PB2中断控制输出信息,首先要打开GPIOB时钟并且将PB2引脚初始化为外部中断状态,代码如下: //第3章/mydemos/hwi/hwi.c void hwi_hard_init(VOID){ GPIO_InitTypeDef GPIO_InitStruct; //打开GPIOB时钟 HAL_RCC_GPIOB_CLK_ENABLE(); //将PB2初始化为外部中断 GPIO_InitStruct.Pin = GPIO_PIN_2; GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); } (2) 为PB2创建中断,设置中断处理函数,代码如下: //第3章/mydemos/hwi/hwi.c void irq_handler(HWI_IRQ_PARAM_S param) { //清除标记位 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_2); //中断代码 printf("devid is %d\n", param.pDevId); HWI_IRQ_PARAM_S p; p.pDevId = 1; UINT32 ret; //删除中断 ret = LOS_HwiDelete(EXTI9_5_IRQn + 16, &p); if (ret != LOS_OK) printf("delete err %x\n", ret); //LOS_HwiDisable(EXTI2_IRQn+16); } void demo_hwi(VOID) { UINT32 ret; HWI_IRQ_PARAM_S param; //初始化硬件 hwi_hard_init(); //设备号,非共享中断可以不使用 param.pDevId = 123; //中断号,0~15系统使用,需要加16 HWI_HANDLE_T irq_num = EXTI2_IRQn + 16; //创建中断:中断号、优先级、是否共享、处理函数、参数传递给处理函数 ret = LOS_HwiCreate(irq_num, 3, 0, irq_handler, ¶m); if (ret != LOS_OK) { printf("hwi err %x\n", ret); return; } //使能中断 LOS_HwiEnable(irq_num); } (3) 在头文件hwi.h中声明函数,并加入需要用到的头文件,代码如下: //第3章/mydemos/hwi/hwi.h #ifndef __HWI_H #define __HWI_H #include "stdio.h" #include "los_hwi.h" #include "los_tick.h" #include "stm32l4xx_hal.h" void demo_hwi(VOID); #endif (4) 在源文件targets/STM32L431_BearPi/Src/user_task.c中调用函数demo_hwi(),代码如下: VOID app_init(VOID){ printf("app init!\n"); DemoEntry(); demo_hwi(); } 3) 修改Makefile 将文件hwi.c和hwi.h的相对路径添加到targets/STM32L431_BearPi/Makefile中,代码如下: #第3章/targets/STM32L431_BearPi/Makefile #将源文件hwi.c添加到LOCAL_SRCS LOCAL_SRCS += \ ... $(LITEOSTOPDIR)/targets/STM32L431_BearPi/Src/flash_adaptor.c \ $(LITEOSTOPDIR)/mydemos/hwi/hwi.c #将头文件路径追加到LOCAL_INCLUDE LOCAL_INCLUDE += \ ... -I $(LITEOSTOPDIR)/include \ -I $(LITEOSTOPDIR)/mydemos/hwi 图310运行结果 3. 运行结果 如果编译无误,则运行结果如图310所示。 从图310可以看到,按键每次被按下时都会输出信息。尽管在中断处理函数中调用LOS_HwiDelete(EXTI9_5_IRQn+16,&p)尝试删除中断,但是其参数p对应的pDevID是1,这与初始化时的函数pDevID(123)并不匹配,因此删除失败。 35min 3.3.5实战案例: 共享中断 1. 案例描述 将PB6和PB7设置为外部中断(下降沿模式),使用函数LOS_HwiCreate()创建共享中断,每个中断打印自己的设备号和中断号。由于笔者开发板的PB6和PB7引脚并没有连接按钮,所以使用杜邦线模拟按钮功能。将杜邦线一侧接GND,另一侧在PB6、PB7之间来回切换连接,这样就可以检测出下降沿。 2. 操作流程 (1) 本节案例可直接在3.3.4节中的代码进行修改。将PB6和PB7初始化为外部中断模式,检测下降沿中断,代码如下: //第3章/mydemos/hwi/hwi.c void hwi_hard_init_share(VOID) { GPIO_InitTypeDef GPIO_InitStruct; //使能GPIOB时钟 HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin = GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); } (2) 为PB6配置设备号1、子中断号GPIO_PIN_6,为PB7配置设备号2、子中断号GPIO_PIN_7。创建两个共享中断,使用同一个中断处理函数,代码如下: //第3章/mydemos/hwi/hwi.c void hwi_demo_share(VOID) { UINT32 ret; HWI_IRQ_PARAM_S param; //初始化硬件 hwi_hard_init_share(); //中断号,0~15系统使用,需要加16 HWI_HANDLE_T irq_num = EXTI9_5_IRQn + 16; param.pDevId = 1; //设备号自定义,不可重复 param.swIrq = GPIO_PIN_6; //以引脚为子中断号,方便处理 //创建共享中断 //参数依次为中断号、优先级、是否共享、处理函数、参数传递给处理函数 ret = LOS_HwiCreate(irq_num, 3, IRQF_SHARED, irq_handler_share, ¶m); if (ret != LOS_OK) { printf("hwi err %x\n", ret); return; } param.pDevId = 2; //设备号 param.swIrq = GPIO_PIN_7; //以引脚为子中断号 //创建中断 ret = LOS_HwiCreate(irq_num, 3, IRQF_SHARED, irq_handler_share, ¶m); if (ret != LOS_OK) { printf("hwi err %x\n", ret); return; } //使能中断 LOS_HwiEnable(irq_num); } (3) 设置中断处理函数,通过参数param.swIrq判断中断来源,代码如下: //第3章/mydemos/hwi/hwi.c void irq_handler_share(HWI_IRQ_PARAM_S param) { //清除标记位 HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_6 | GPIO_PIN_7); //中断处理代码 if (HAL_GPIO_ReadPin(GPIOB, param.swIrq) == 0) { //延时为了消除抖动 LOS_Udelay(20); if (HAL_GPIO_ReadPin(GPIOB, param.swIrq) == 0) { printf("INTTERRUPT %d\n", param.swIrq); } } } (4) 在头文件hwi.h中声明函数,代码如下: //第3章/mydemos/hwi/hwi.h #ifndef __HWI_H #define __HWI_H #include "stdio.h" #include "los_hwi.h" #include "los_tick.h" #include "stm32l4xx_hal.h" void demo_hwi(VOID); void demo_hwi_share(VOID); #endif (5) 在源文件targets/STM32L431_BearPi/Src/user_task.c中调用函数demo_hwi(),代码如下: VOID app_init(VOID){ printf("app init!\n"); DemoEntry(); demo_hwi_share(); } 图311运行效果 3. 运行结果 编译无误后在开发板运行代码,将杜邦线悬空的一侧在PB6、PB7之间来回切换,可以看到结果如图311所示。 16min 3.4内存 内存管理模块是操作系统的核心模块之一,主要负责内存资源的管理,包括内存的初始化、分配及释放。通过对内存的申请、释放使内存的利用率和使用效率达到最优,最大限度地解决系统的内存碎片问题。 LiteOS的内存管理分为静态内存管理和动态内存管理,提供了内存初始化、分配、释放等功能。 3.4.1静态内存 1. 运行机制 静态内存本质上就是一个静态数组,使用期间从静态内存池中申请一个块,用完释放即可。静态内存池由一个内存控制块和若干大小相同的内存块组成,控制块位于内存池的头部,如图312所示。 图312静态内存 内存控制块是一个结构体,内部记录了内存池的块个数、块大小、已经使用的块,还有一个记录Free节点的链表,代码如下: //第3章/kernel/include/los_membox.h typedef struct { UINT32 uwBlkSize; /*静态内存块大小 */ UINT32 uwBlkNum; /*内存池的总块数*/ UINT32 uwBlkCnt; /*已经被使用的块数 */ #ifdef LOSCFG_KERNEL_MEMBOX_STATIC LOS_MEMBOX_NODE stFreeList; /*单向链表,记录未使用的Free节点 */ #endif } LOS_MEMBOX_INFO; 每个Block并非按照用户的实际要求设定大小,系统在管理内存时会遵循芯片架构的对齐方式,例如STM32L431采用的是4字节对齐方式。Block的起始4字节是块标记位,如果Block被使用,则标记为0xa55a5aa5。 注意: 静态内存在使用时首先要进入menuconfig,选择Kernel→Memory Management使能Membox Management。 2. 静态内存API 静态内存使用前必须初始化内存池并设置块大小,初始化之后块大小不可以改变。内存管理模块为静态内存提供分配、释放等一系列操作,见表38。 表38静态内存API 函数说明(头文件kernel/include/los_membox.h) LOS_MemboxInit初始化静态内存池 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回LOS_NOK 参数1: [IN] VOID *pool,内存池首地址 参数2: [IN] UINT32 poolSize,内存池大小 参数3: [IN] UINT32 blkSize,块大小 LOS_MemboxAlloc申请一块静态内存 返回值: VOID *,如果成功,则返回内存地址,如果失败,则返回NULL 参数1: [IN] VOID *pool,内存池地址 LOS_MemboxFree释放一块静态内存 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回LOS_NOK 参数1: [IN] VOID *pool,内存池地址 参数2: [IN] VOID *box,要释放的内存块 LOS_MemboxClr清零指定内存块,无返回值。如果成功,则内存的内容为0 返回值: 无 参数1: [IN] VOID *pool,内存池地址 参数2: [IN] VOID *box,要清零的内存块 LOS_MemboxStatisticsGet获取指定静态内存池的信息,如果执行成功,则可从参数中获取内存信息 返回值: UINT32,如果成功,则返回LOS_OK,如果失败,则返回LOS_NOK 参数1: [IN] const VOID *boxMem,内存池地址 参数2: [OUT] UINT32 *maxBlk,内存池块的总个数 参数3: [OUT] UINT32 *blkCnt,已经使用的块个数 参数4: [OUT] UINT32 *blkSize,块的大小 LOS_ShowBox打印指定静态内存池的所有节点信息(打印等级是LOS_INFO_LEVEL),包括内存池起始地址、内存块大小、总内存块数量、每个空闲内存块的起始地址、所有内存块的起始地址 返回值: 无 参数: [IN] VOID *pool,内存池地址 3. 实战案例: 申请静态内存 1) 案例测试 初始化一个200字节的静态内存池并将块大小设置为10字节,申请两个内存块,对得到的内存块进行赋值、清零、释放等操作。在mydemos文件夹下创建源文件mem/mem.c、头文件mem/mem.h,代码如下: //第3章/mydemos/mem/mem.c void demo_static_mem(){ UINT32 ret, *mem1, *mem2; //初始化内存池,指定大小为200,块大小为10 ret = LOS_MemboxInit(&mem_box[0], box_size, block_size); if(ret != LOS_OK){ printf("静态内存池初始化失败\n"); return; } printf("内存池起始地址是 0x%x\n",mem_box); //打印控制块 printf("块大小 %d 块总数 %d 已经使用块数 %d 下一个可以分配的块地址 0x%x\n", mem_box[0], mem_box[1], mem_box[2], mem_box[3]); //申请mem1 mem1 = (UINT32 *)LOS_MemboxAlloc(mem_box); if(mem1 == NULL){ printf("申请静态mem1失败\n"); return;} printf("mem1 申请成功\n"); //打印控制块 printf("块大小 %d 块总数 %d 已经使用块数 %d 下一个可以分配的块地址 0x%x\n", mem_box[0], mem_box[1], mem_box[2], mem_box[3]); //申请mem2 mem2 = (UINT32 *)LOS_MemboxAlloc(mem_box); if(mem2 == NULL){ printf("申请静态mem2失败\n"); return;} printf("mem2 申请成功\n"); //打印控制块 printf("块大小 %d 块总数 %d 已经使用块数 %d 下一个可以分配的块地址 0x%x\n", mem_box[0], mem_box[1], mem_box[2], mem_box[3]); //内存赋值 *mem1 = 543; *mem2 = 123; printf("mem1 内容是 %d, 地址是 0x%x, 保护字是 0x%x\n", *mem1, mem1, *(mem1-1)); printf("mem2 内容是 %d, 地址是 0x%x, 保护字是 0x%x\n", *mem2, mem2, *(mem2-1)); //清空内存1 LOS_MemboxClr(mem_box, mem1); printf("mem1的内容是 %d, mem2的内容是 %d\n", *mem1, *mem2); //释放内存 ret = LOS_MemboxFree(mem_box, mem1); if(ret != LOS_OK){ printf("释放mem1失败\n");} ret = LOS_MemboxFree(mem_box, mem2); if(ret != LOS_OK){ printf("释放mem2失败\n");} return; } 参考3.2.4节内容,修改Makefile并在源文件user_task.c中调用函数demo_static_mem(),运行结果如图313所示。 图313运行结果 2) 结果分析 内存池首地址是0x20001e68,控制块是LOS_MEMBOX_INFO(参考3.4.1节)类型,占4x4字节。由于每个Block需要4字节保护字段,因此用户第1次分配到的内存地址是0x20002068+16+4=40x20001e7c。由于本次实验使用的芯片STM32L431采用4字节对齐方式管理内存,因此每个Block的实际大小是12+4=16。 从图313可以看到,控制块内存放的下一个节点地址并非用户得到的实际地址,因为要设置4字节的标志位,最终用户得到的地址是stFreeList+4。 3.4.2动态内存 动态内存属于堆内存,在内存资源足够的情况下,从一块连续的堆内存(动态内存池)中为用户分配任意大小的块。当用户不需要此内存时,又可以释放回收。很明显动态内存是按需分配的,但是容易出现内存碎片。 LiteOS动态内存支持BestFit、BestFit_Little两种算法。 1. BestFit算法 BestFit内存结构包含3部分: 内存池信息、管理节点、用户内存区域,如图314所示。 图314BestFit算法 第一部分记录了内存池起始地址和总大小。 第二部分本质上是一个数组,数组的每个元素都是一个双向链表,所有的Free节点都按照规定挂载到链表中。假设内存中允许的最小节点是2^min字节,则第1个双向链表挂载size为2^min:: target可以省略,pattern中必须包含字符“%”。针对上述案例,使用模式规则,代码如下: %.o:%.c gcc $< -o $@ target.bin:1.o 2.o 3.o gcc 1.o 2.o 3.o -o target.bin targetpattern中的%.o代表目标是以.o结尾的文件集合,dependpattern中的%.c表示取target中的%部分,然后追加后缀.c。 3. 函数 Makefile支持少量函数,方便开发者快速处理一些任务,例如字符串替换、取当前路径、执行Shell指令等,详情见表312。函数调用语法示例,代码如下: $(function arg1, arg2,...) function是函数名字,arg1、arg2是参数名,多个参数以逗号隔开,函数名和参数之间用空格分隔。函数调用以“$”开头,用小括号将函数名和参数包起来。 表312Makefile常见函数 变量说明 $(subst from,to,text)函数名: subst 功能: 字符串替换。将字符串text中的from替换为to 返回值: 替换后的字符串 示例: $(subst aa,bb,aabbcc) 示例结果: bbbbcc $(strip )函数名: strip 功能: 去掉空格。将字符串string中的空格去掉 返回值: 去掉空格后的字符串 示例: $(strip a b c) 示例结果: abc $(filter , )函数名: filter 功能: 过滤。将字符串text中不符合pattern的内容过滤掉 返回值: 过滤后的字符串 示例: $(filter %.c, 1.c 2.c 3.h) 示例结果: 1.c 2.c $(dir )函数名: dir 功能: 取目录。从文件名序列中取出目录部分。目录部分是指最后一个反斜杠“/”之前的部分。如果没有反斜杠,则返回“./” 返回值: 目录序列 示例: $(dir src/demo.c example.c) 示例结果: src/ ./ $(notdir )函数名: notdir 功能: 取文件名。从文件名序列中取出非目录部分。非目录部分是指最后一个反斜杠“/”之后的部分 返回值: 文件名序列 示例: $(notdir src/demo.c example.c) 示例结果: demo.c example.c $(basename )函数名: basename 功能: 取前缀。从文件名序列中取出文件名的前缀 返回值: 文件名前缀序列 示例: $(basename src/demo.c example.c) 示例结果: src/demo example $(addsuffix , )函数名: addsuffix 功能: 增加后缀。给中的文件添加指定的后缀 返回值: 修改后的文件名序列 示例: $( addsuffix .c, src/demo example) 示例结果: src/demo.c example.c $(addprefix , )函数名: addprefix 功能: 增加前缀。给中的文件添加指定的前缀 返回值: 修改后的文件名序列 示例: $(addprefix src/, demo.c example.c) 示例结果: src/demo.c src/example.c $(shell shellcommands)函数名: shell 功能: 执行Shell指令,参数就是Shell命令 返回值: Shell命令的返回值 示例: $(shell cat 1.c) 示例结果: 1.c文件中的内容 图323目录结构 3.6.3实战案例: 简单计算器 1. 案例描述 制作一个简单计算器: 定义4个函数add_int()、add_float()、sub_int()、sub_float(),将4个函数分别定义在不同的文件中,最后在cal.c文件中通过main()函数调用这4个函数。按照功能将文件分开存储,并定义对应的Makefile文件,最终的目录结构如图323所示。 注意: 在本案例中只有简单的数学运算和输出语句,因此使用GCC编译,并直接在PC运行编译生成的可执行文件。 2. 操作流程 1) C源码 编辑4个源代码文件,分别实现int加减法、float加减法,代码如下: //第3章/add/add_int/add_int.c int add_int(int a, int b){ return a+b; } //第3章/add/add_float/add_float.c float add_float(float a, float b){ return a+b; } //第3章/sub/sub_int/sub_int.c int sub_int(int a, int b){ return a-b; } //第3章/sub/sub_float/sub_float.c float sub_float(float a, float b){ return a-b; } //第3章/cal/cal.c int main(){ printf("test int add %d + %d = %d\n", 3, 2, add_int(3,2)); printf("test float add %f + %f = %f\n", 3.5, 2.5, add_float(3.5,2.5)); printf("test int sub %d + %d = %d\n", 3, 2, sub_int(3,2)); printf("test float sub %f + %f = %f\n", 3.5, 2.5, sub_float(3.5,2.5)); return 0; } 2) 顶层Makefile 顶层目录的主要工作是将所有的o文件链接生成bin文件(Windows系统下生成exe文件)。设置将所有编译结果输出到out目录下,最终生成的目标文件为cal.bin。将当前目录及输出目录导出,以供子目录下的Makefile使用。具体的代码如下: #第3章/Makefile #获取当前工作目录 SAMPLETOPDIR = $(CURDIR) #导出变量,以供其他Makefile使用 export SAMPLETOPDIR #目标文件 TARGET = $(OUT)/cal.bin #输出目录 OUT = out export OUT #伪目标 .PHONY:$(TARGET) clean #第1个目标,将所有的.o文件编译生成最终的.bin文件 $(TARGET): $(OUT) $(CC) $(wildcard out/obj/*.o) -o $(TARGET) #其他目标,out依赖其他目标,需要进入子目录执行Makefile $(OUT): cd add && make -w cd sub && make -w cd cal && make -w #清除out中的内容 clean: rm -rf out 3) 子目录Makefile 所有的子目录功能都一样,将子目录下的c文件编译生成o文件。变量LOCAL_SRCS为该目录下的所有c文件集合,变量LOCAL_INC为该目录下的头文件目录集合,变量OBJECTS为所有目标o文件集合。以add子目录为例,代码如下: #第3章/add/Makefile #源文件路径 LOCAL_SRCS += \ $(SAMPLETOPDIR)/add/add_int/add_int.c \ $(SAMPLETOPDIR)/add/add_float/add_float.c #头文件路径 LOCAL_INC += \ -I $(SAMPLETOPDIR)/add/add_int \ -I $(SAMPLETOPDIR)/add/add_float CFLAGS += $(LOCAL_INC) #目标文件,这里使用模式替换操作,取文件名操作,增加前缀操作 #最终得到 OBJECTS += ./out/obj/*.o OBJECTS += $(addprefix $(SAMPLETOPDIR)/$(OUT)/obj/,$(notdir $(LOCAL_SRCS:.c=.o))) #目标输出目录 OBJOUT = $(SAMPLETOPDIR)/$(OUT)/obj #设置c文件的搜索目录 vpath %.c $(sort $(dir $(LOCAL_SRCS))) #此文件的第1个目标 all: $(OBJECTS) #模式规则,目标为所有o文件,依赖为对应的c文件。使用gcc生成.o文件 $(OBJOUT)/%.o: %.c $(SAMPLETOPDIR)/$(OUT)/obj gcc $(CFLAGS) -c $< -o $@ #创建目录out/obj $(SAMPLETOPDIR)/$(OUT)/obj: mkdir -p $@ 3. 运行结果 在项目的根目录下执行make指令进行编译,由于TARGET目标依赖OUT,因此Makefile会跳转到OUT目标,接着执行OUT下的命令切换到子目录执行make。最终的运行结果如图324所示。 图324编译及运行结果 3.7本章小结 本章从启动代码开始逐步认识LiteOS,任务、中断、内存是操作系统的基础模块,多数项目需要用到这3个基础模块。借助异常管理功能可以迅速定位错误信息,一个合格的开发者应该习惯用Backtrace信息定位错误。3.6节介绍了Makefile工具,可帮助开发者更好地理解大型项目的代码结构。