EventDrivenClassOSAL详解
Event Driven Class OSAL
基于事件驱动的模拟操作系统
##前言
什么是OSAL
- OSAL为:Operating System Abstraction Layer,即“操作系统抽象层”,支持多任务运行,它并不是一个传统意义上的操作系统,但是实现了部分类似操作系统的功能。
- 以上原文来自于网络
- OSAL概念是由TI公司在ZIGBEE协议栈引入,他的意思是"操作系统抽象层",我认为叫做"模拟操作系统"更为合适,它并非一个真正的OS,而是模拟OS的一些方法为广大编程者提供一种编写MCU程序的方法,是一个架构,一种思维模式;当有一个事件发生的时候,OSAL负责将此事件分配给能够处理此事件的任务,然后此任务判断事件的类型,调用相应的事件处理程序进行处理。
- OSAL_EventDrivenClass借鉴了TI OSAL/NXP RTOS/FreeRTOS/RT-Thread等操作系统优势,兼顾了代码小/逻辑简单/等优势,适用于低端MCU开发使用.
- 网络上有大量把OSAL叫做“操作系统抽象层”,而我认为“操作系统抽象层”这个词应该说是为了抽象不同操作系统的API,将系统API统一,然后我们所看到OASL并非此功能,这里不做过多讨论。
OSAL和RTOS的区别 - 本人理解OSAL只是一个裸机编码框架,并非操作系统,然后OSAL实现了让裸机写程序,就像在操作系统上写程序一样简单,所以OSAL是一个适用于低端MCU的编程框架;为了简化后面文档,后面把OSAL当做操作系统处理。
- RTOS实时操作系统,开源的RTOS主要包括RT-Thread、Huawei LiteOS、AliOS Things、TencentOS-tiny、FreeRTOS、Arm Mbed OS、MS-RTOS、Zephyr、Contiki-NG、NuttX、RIOT、Apache Mynewt、Drone、eCos、F9 Microkernel、Tock、Mark3、Atomthreads、Trampoline等。
- RTOS和OSAL本质上的区别:
- RTOS具备任务(线程)调度和切换功能,具备任务优先级和任务抢占等功能,把一个CPU划分的弱干个片运行,让开发者把一个CPU看成多个CPU来处理。
- OSAL不具备多任务(线程)调度和切换功能,任务无优先级和抢占功能,采用轮询方式调用函数,利用TICK或TIME定时,实现了部分OS的功能。
EventDrivenClassOSAL:
- 是一个由事件驱动类的OSAL,顾名思义此OSAL完全由事件进行驱动,没有事件任务就没有事干,那么OSAL就会调度空闲任务,等待事件的发生.
EventDrivenClassOSAL特点: - 此系统完全由C语言编写,不会涉及汇编,且代码量非常少,整个系统不足1000行;适合初学者学习和使用。
- 内存占用小,特别是对栈的占用级低,完成可以做到同裸机编程占用一样的栈空间;适用于各类低端MCU。
- OSAL实现了类似RTOS的编程思路,使代码更利于模块化设计,一个应用程序有多个独立的小功能模块组成,可使模块代码偶合度极低,便于多人协作开发,模块的重复使用。
EventDrivenClassOSAL适用性: - 依赖于一个定时器,可适用于各种MCU,包含传统8051。
- 此OSAL适用于对实时性不严格,对产品成本严格控制,使用较低端的MCU的场景.可适于以上场景的用绝大部分应用.
关于内存占用情况(V1.x版本): - 基于新唐NUVOTON M0-58MCU硬件平台,使用官方标准库,一个示例代码(代码包括4个按键驱动示例程序,一个软件BUZZ驱动程序,2个任务,用来作按键消息分发和按键消息处理,可以说一个简单的应用已经完成80%.)内存使用情况如下:
- Program Size: Code=6132 RO-data=472 RW-data=64 ZI-data=672
- 可以看出OSAL内存是占用远远于一般的OS,并且OSAL已经实现任务的消息队,列把任务/事件/定时器的堆空间已经包括,在写应用代码时不会在重复创建.
可移植性: - 目前我公司将OSAL成功的在GD/STM32/HC32等众多MCU上应用。
陷阱: - 由于OSAL只是一个简单的编程架构,为了节能代码量和资源的占用,提供代码运行效率,没有过多考虑安全性,请不要用于对安全性要求较高的产品。
- OSAL非多线程多任务,实际上还一个单任务在死循环,所以要理解系统的工作原理,以免因不明白运行原理给自己挖些陷阱。
鸣谢: - 特别感谢我的同事“罗天浩”提供了OSAL的部分框架,特别是万能的消息队列,使OS部得更简洁.
- 感谢业界各位朋友提供宝贵的意见和建议,如“ Seven Pounds 发现队列未进入临界保护等”.
声明: - 文档中部分见解属于个人见解,未经过验证(如:OSAL定义),如果错误敬请谅解,欢迎批评指导.
愿景: - 帮助使用低端MCU的嵌入式开始从业者,不断提关工作效率,实现代码的复用性。
源代码下载: - 最新的版本,请加企鹅群413012273,在共享文件中获得。
软件整体架构
OASL采用3层架构:硬件层,OASL+HAL层,应用层
第一层
LIB层:为芯片原创提供的库文件
第二层
- OSAL:为系统层,为用户提供编程框架.
- HAL:为驱动抽象层,抽象标准驱动库,让应用与硬件隔离.
- HAL不依赖于OSAL,是一个独立的框架,用户在不需要OSAL的时候也可单独使用HAL
- OSAL不依赖于HAL,用户可以跳过HAL支持操作LIB层,实现一些特定的功能.
第三层
应用层,用户自己应用程序.
OSAL详解
概要
OSAL框架
系统硬件层(sHardware):
TICK(心跳):为系统时间节拍使用;osTimer(软件定时器)、osEvent(事件)、osDelay(系统延时)均基于此定时器来运行;故此定时器是整个OSAL的心脏。
- WDT(看门狗):使用芯片主看门口狗,用户根据不同的芯片移植看门狗程序。
- 硬件层也是OS适配不同处理器需要移植的一层,默认使用的ARM处理器,目前发现ARM的M0、M23、M3、M4都可以兼容;其它处理器需要自行移植。
- 系统移植方法在后续章节中详细讲解。
系统调度器(osScheduler): - 系统调度器主要用于调度event(事件)、task(任务),是OSAL的大脑,应用程序的运行就是这里来调度的。
任务(osTask):
任务是一个程序的入口(这里说的程序是指一个较小的功能模块),任务通常指所接受的工作,所担负的职责,是指为了完成某个有方向性的目的而产生的活动。
- 一个较大型的程序可以划分为弱干的小功能模块,比如我们开发一个由按键和数码管组成的产品,我们可以用一个任务专门负责按键的扫描;另外一个任务处理数码管的显示;两个任务不直接关联,而是采用消息队列来通讯,那么这两个代码模块就是相对独立的程序,不用某个模块是,直接把相应的文件删除,而不影响其它模块和整个代码的运行。
- 任务一旦被创建就会一直存在,因为考虑系统设计的原因,没有删除任务,只要没有消息触发任务,那么任务就不会运行.
事件(osEvent):
osEvent是思路来源于NXP的BLE模块思路,使用他可以即时使或者延时回调指定的函数。
- osEvent引入极大的方便我们处理一些需要延时做的事情,或者需要在中断中较长时间处理的事情。
- 举个列子,我们驱动一个I2C触摸按键,当有按键按下时,触摸芯片将INT脚拉低,MCU中入中断,无需在中断中调用I2C去获取按键,而是创建一个即时事件后,就退出中断;退出中断后,系统会调度创业的时间函数,在事件中在调用I2C读取按键值;这样开发的好处避免了中断的长期时间占用。
- 再举个例子,我们需要控制一个LED的亮和灭,传统的做法是打开LED后,写一个死延时,等延时到后再去关闭LED,那么在这个延时期间,MCU就无法处理其它事务;而引入了osEvent后,打开LED时,只需要创建一个延时的Event,当时间后到,自动回调关闭LED的函数;应用程序就不需要关心LED关闭的事情了。
- 事件一般是临时突发的,不可预期的,需要快速响应处理的一类活动,事件与项目,任务的显著区别就是事件是没有明确的目的的,完全不可预期。
- 事件的显著特性就是其临时性和突发性,可能并不会经常发生,只是偶然性,以致不可预期。
- 事件可以是即时事件,也可以是延时事件,任务创建后只会运行一次,如果希望事件能各周期运行,那么只需要在事件运行时,重新创建一次就行.
- 事件在未执行时再次被创建时,系统不会再次创建一个新的事件,而是将已经创建但未执行的事件延时重新设置.
- 事件在未执行前可以被清除.
软件定时器(osTimer):
定时器属于用软件实现了多个硬件定时器;
- 定时器主要是用来定期产生中断,方便应用程序开发需要定时完成的工作。
- 定时器基于系统定时器(TICK)来运行的.
- 定时器一旦被创建将会周期性自动运行,不需要重装初值,直到被删除.
- 如果需要一次性定时器,推荐使用事件来完成.
- 由于定时器是在中断里面完成,所有定时器处理的事件不能太久,以免影响系统的正常运行.
队列(osQueue): - 任务创建时已经为任务创建一个队列来传递消息给任务.
- 用于如果需要自定义不各种格式的队列,可以自行创建.
内存管理(heap_4): - 这里直接使用了FreeRTOS的内存管理,当然也可以用C库或者其它的内存管理。
硬件抽象层(HAL): - V2.1版本引入了HAL层,将硬件进行抽象设计,应用不再需要直接调用芯片的库或者寄存器。
- 本HAL不同于其它操作系统的HAL的地方是,HAL是独立于OS,可以直接用于裸机的编程;如果其它朋友有需要也可以直接使用。
osTask任务:
任务配置:
- 在osConfig.h文件定义了任务最大数 #define MAX_TASK_NUM 8u,
- 在osConfig.h文件定义了任务消息的最大条数#define MAX_TASK_MSG_BUF_NUM (MAX_TASK_NUM*10),
- 需要开发者根据情况需求调整大小,此值越大所占内成RAM就越大,
/** define Max Task Num
* 定义最大任务数 MAX<0xFFFF ,用户根据项目实际大小修改,
* 任务数越多,所占用的RAM就越多.
**/
#define MAX_TASK_NUM (IDLE_TASK_NUM+8u)
/** define Max Task Message Buffer Num
* 定义最大任务消息缓冲数,
* 建议与MAX_TASK_NUM相等或者更大
**/
#define MAX_TASK_MSG_BUF_NUM (MAX_TASK_NUM*3)
任务函数原型:
- 在osTadk.h中定义了任务函数的原型
/* Defining task function pointer array types */
/* 定义任务函数指针数组类型
* uint8_t id:任务的ID,向任务发送消息时,需要用到此ID
* uint8_t msg: 消息号
* void *pData: 消息所携带的数据
* uint16_t dataSize: 数据大小
*/
typedef void(*tpfTaskFunc)(uint8_t id, uint8_t msg, void *pData, uint16_t dataSize);
void idleTask(uint8_t taskId, uint8_t msg, void *pData, uint16_t dataSize)
任务创建:
调用函数createTask创建一个任务,函数原型uint8_t createTask(tpfTaskFunc pfFunction), pfFunction是任务的函数指针;创建成功返回任务的ID,创建失败返回0;
uint8_t createTask(tpfTaskFunc pfFunction)
任务删除
有时创建的任务工作任务已经完成,可以被删除,调用uint8_t delTask(tpfTaskFunc pfFunction)可以删除任务.
uint8_t delTask(tpfTaskFunc pfFunction)
注意被删除后,应用层无法对应用是否删除进行判断,此时用户要避免还要此任务发送消息.
任务初始化:
任务创建时系统会自动发一个初始化消息给任务,任务接是到此消息时可以初始化各变量,或者硬件资源.
/* 给当前任务发送初始化消息 */
sendMsgToTask(gtTask.taskIdMax, TASK_INIT, NULL, 0);
/* 系统初始化 */
case TASK_INIT:
{
adcInit(>CfgAdcVol);
createTimer(1000,secTimer);
}break;
任务调度:
当有消息发给任务时,任务调度器(osScheduler)会从任务消息取出消息,并回调任务函数。
任务消息发送:
调用sendMsgToTask可以发送消息给指定的任务.
/**
* function:
* 作用: 创建一个任务
* parame 1:
* 参数1: taskId为接收消息的任务ID
* parame 2:
* 参数2: msg为发送的消息内容
* parame 3:
* 参数3: pData传递的数据,
* parame 4:
* 参数4: dataSize传递数据的大小
* return :
* 返回: 消息发送成功返回true,否则返回false.
**/
bool sendMsgToTask(uint8_t taskId,