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)
任务函数原型:
/* 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, uint8_t msg, void *pData, uint16_t dataSize)
任务消息接收:
一旦有消息发给任务,对应的任务函数就会被调度,并且通过任务函数的形参收到消息.
任务消息处理:
任务被调度时,可以从形参msg获取消息, pData获取携带的数据, dataSize获取数据长度,任务根据消息内存做出相应的处理,处理完成后返回,系统再次调度其它任务.
当任务在处理消息发生新的消息,任务可以向自己发送另外一个消息.
void mainTask(uint8_t taskId, uint8_t msg ,void *pData, uint16_t dataSize)
{
switch(msg)
{
/* 系统初始化 */
case TASK_INIT:
{
adcInit(>CfgAdcVol);
}break;
/* 按键 */
case MAIN_KEY_MSG:
{
switch((uint32_t)pData)
{
case KEY_0:
case KEY_1:
{
data = (uint8_t)(uint32_t)pData;
printf("key=%d",data );
}break;
}
}
}
}
osDelay:
任务中通常不允许Delay,大多Delay可以由延时事件来完成,如果必需用到Delay,那么可以使用OS提供的osDelayMs函数和taskDelayMs函数;
/*
** 系统延时并发数
** 加大此时,在任务中用到bOsDelayMs(),会增加栈的空间消耗,注意调整栈大小,并控制使用bOsDelayMs().
*/
#define OS_DELAY_MAX_CONCURRENT 1u
/*
** 系统延时
** 最小单位1/2个TICK周期;延时时可调度其它任务和事件.
** 需要注意,延时中的函数可能会被其它任务或者事件调用,
** 要防止函数递归调用时全局变量或静态变量被修改,造成程序异常.
*/
/
/* 特别声明:由于vOsDelayMs时当前运行的函数会阻塞,
** 但为了做多任务,系统在函数阻塞时,调度其它任务和事件.
** 同时可能调用已经阻塞的任务,发生递归调用的情况;
** 这样压栈的数据就会大大增加,严重时发生栈溢出,导致程序崩溃.
** 为了解决此问题,当已经有任务在延时的,不允许有任务再延时,
** 这样可能会对某个任务造成异常,但保护了系统不会崩溃.
** 用户调用此延时程序时,需要判断返回值,如果返回值为fals,说明延时失败,
** 此时用户需要自行设法解决
*/
bool osDelayMs(uint16_t usDelayMs)
/
/* 特别声明:由于bTaskDelayMs时所有任务会阻塞,
** 任务阻塞时,事件会被调度.
*/
bool taskDelayMs(uint16_t usDelayMs)
/*
** 系统指令延时,调用此函数,会使所有任务和事件阻塞,非必要情况下尽量少用
*/
void osDirectiveDelay(uint16_t usDelay)
/*
** 较精准延时,但最小延时不得小于TICK;
*/
void osDirectiveDelayMs(uint16_t usDelayMs)
当调用osDelayMs时系统会调用其它任务或者事件(这需要消耗大量的栈空间,还可以能因为栈空间不足导致异常,甚至因为这里发生了递归调用,造成后面处理的逻辑顺序错误,故osDelayMs谨慎使用,最好不用);
当调用taskDelayMs所有任务会被卡死,不再调度任务,而一直在调度事件Event,这样可以把一些优先级高的,以事件的方式进行处理,然后任务调用taskDelayMs,优先处理事件.
递归调用实现多任务同时运行的假象
将#define OS_DELAY_MAX_CONCURRENT 的定义改为非0时;当一个任务调用osDelayMs时,其它任务可能就会被调用,由于OSAL没有任务切换功能,那里这里就是采用递归调用的方法实际了多任务的假象,使用时务必要留心,递归调用会极大的增加栈空间的消耗,严重造成栈空间不足而程序异常。
故osDelayMs在非必要是不要使用。
任务优先级
由于OSAL没有任务切换,不能实现多任务,故没有任务优先级的说法.
只有一个任务退出,其它任务才能被调度,所有任务不能长时间被占用,更不能写成while(1) 。
空闲任务:
当所有任务都处于空闲时,如果在使能空闲任务的情况下,系统会调度空闲任务,用户可以在空闲任务处理扫描事件,或者让CPU休眠.
##osEvent事件:
事件配置:
在”osConfig.h”文件定义了任务最大数
#define EVNT_CONCURRENT 16u
需要开发者根据情况需求调整大小,此值越大所占内成RAM就越大.
注意:此值并不是可以创建事件的数量,而是事件同时运行的并发数量.事件一旦调度运行后,就会释放出内存来给其它事件使用.
事件函数的原型:
/* Defining task function pointer array types */
/* 定义事件函数指针数组类型*/
typedef void(*tpEventFunc)(uint32_t eventId, uint32_t param);
void closeLed(uint32_t eventId, uint32_t param)
{
close(param)
}s
事件创建:
调用函数createEvent和createEvents可以创建一个事件,
/**
* function: Create Event
* 采用回调函数方式操作事件,同一个回调函数不能被创建多次
* 作用: 创建一个事件,可以是及时的,也可以是延时的.
* uint32_t uiTime:
* 参数1: 设置延时时间,以mS为单位.
* tpfTimerFunc pFunction:
* 参数2: 定时事件回调函数
* uint32_t param
* 参数3: 传递给回调函数的参数
* return : true or false
* 返回:
* 注意:软件定时器是一个相对定时器,不能完全保证精确度,另外尽量不要用最小单位来创建定时器,那样会非常不准
**/
bool createEvent(uint32_t delayMs, tpEventFunc pFunction, uint32_t param)
/**
* function: usCreateEvents
* 采用事件ID方式操作事件,同一个回调函数可能被创建多次
* 作用: 创建一个事件,可以是及时的,也可以是延时的.
* uint32_t uiTime:
* 参数1: 设置延时时间,以mS为单位.
* tpfTimerFunc pFunction:
* 参数2: 定时事件回调函数
* uint32_t param
* 参数3: 传递给回调函数的参数
* return :事件ID
* 返回:
* 注意:软件定时器是一个相对定时器,不能完全保证精确度,另外尽量不要用最小单位来创建定时器,那样会非常不准
**/
uint32_t createEvents(uint32_t delayMs, tpEventFunc pFunction, uint32_t param)
createEvent:
根据回调函数指针作为唯一标识创建事件,同一个回调函数反复创建只会发生一次回调,第二次创建时,上一次所有参数会被覆盖,通常可以用创建来刷新延时时间。
第一个参数delayMs:是事件延时时间,以mS为单位,传0为即时事件,
第二个参数pFunction:为事件回调函数,第三个参数是传递给回调函数的参数,即是传递给事件的消息.返回创建成功与失败.
第三个参数param:传入给事件函数的参数
createEvents:
采用事件ID方式操作事件,同一个回调函数被创建多次,就会产品不同的ID,同一个函数会被多次回调。
此方法创建事件会返回一个ID,做为事件的唯一标识,为便于日后的删/查/改此ID需要保存下来。
参数与createEvent相同.
事件调度:
事件创建后,延时时间到时,系统会自动调度任务,任务只会运行一次,如果需要周期运行,需要在任务函数中再次创建.
事件删除:
事件创建后可调用cleanEvent和cleanEventForId,删除已经创建的事件;
- 函数原型bool cleanEvent(tpfEventFunc pfFunction), 此方法适用于使用createEvent创建的事件,参数为事件的回调函数指针,返回成功或失败.
查找事件
- 函数原型bool cleanEventForId(uint32_t eventId), 此方法适用于用于用createEvents创建的事件,参数为创建事件时返回的事件ID,返回成功或失败.
使用createEvent创建的事件,无法被查找出来,通常也不需要.
- 使用createEvents创建的事件,可以使用ID号查找此事件是否被创建.
- 函数原型bool searchEventForId(uint32_t eventId);, 此方法适用于用于用createEvents创建的事件,ID号是创建时返回的ID,返回true即存在此事件,否则事件不存在
重置事件:
事件被创建后,在事件还未回调前,可以重置事件的计时.
- 可调用createEvent和rstEventForId,重置已经创建的事件.
- 函数原型bool createEvent(uint32_t delayMs, tpfEventFunc pfFunction, uint32_t param),此方法适用于使用createEvent创建的事件,与创建事件是同一个API,使用方法也相同.
- 函数原型bool rstEventForId(uint32_t eventId, uint32_t delayMs),此方法适用于用于用createEvents创建的事件, eventId是创建事件时返回的ID, delayMs是延时时间
注意事项:
采用createEvent和createEvents创建的事件,在重置和清除需要用对应的API,不可混用.
- createEvent创建事件,同一个回调函数只能对应一个事件.
- createEvents创建事件,同一个回调函数可能被创建成多个事件,可用参数中eventId的判断是哪一个事件.
- OSAL最好用的就是Event,这个用于可以让你的MCU开发效率得到大幅度的提升,同时可以简化复杂的逻辑。
Event的常见用法:
例1:我们需要点一个灯,但延迟一定时间后就关掉,传统有两个方法解决:
- 常见的第一个方法开灯后死延时一定时间,然后再关灯.在这个期间MCU就处理不了其它事情,如以下代码
led_on();
delay_ms(100);
led_off();
- 常见的第二个是开一个定时器,创建一个全局变量,将变量放在定时器中自减,开灯后设置变量,再一直判断这个变量,当变量到达预期的值,去关闭这个灯.
led_on();
led_off_time = 100;
void timer(void)
{
if(led_off_time)
{
if(--led_off_time == 0)
led_off();
}
}
- 以上两个方法虽然能实现但过程比较麻烦,而采用Event后,代码变成如下方法:
- 写一个关闭灯的回调函数,打开灯->createEvent注册事件,然后就不管了;时间到了,灯就会自动关闭;
led_on();
osCreatEvent(ledOff)
void ledOff(uint32_t eventId, uint32_t param)
{
led_off();
}
- 这样处理逻辑简单明了,解决了第一个方法死等,CPU资源浪费;简捷了第二个方法变量漫天飞的情况.
osTimer软件定时器(以下简称定时器):
定时器配置:
在”osConfig.h”文件定义了任务最大数 #define MAX_TIMER_NUM 3u,
需要开发者根据情况需求调整大小,此值越大所占内成RAM就越大.
定时器回调函数原型
/* 定义定时器函数指针数组类型*/
typedef void(*tpfTimerFunc)(void);
void secTimer(void)
{
static uint8_t ucVoltageCount=0;
static uint16_t usHourCount=0;
if(++ucVoltageCount == 1)/* 每3秒检查一次当前工作电压 */
{
ucVoltageCount = 0;
sendMsgToTask(gMainTaskId, MAIN_CHECK_CURRUCT_VOLTAGE_MSG, NULL, 0);
}
if(++usHourCount == 3600)/* 每1小时检查一次最低工作电压 */
{
usHourCount = 0;
sendMsgToTask(gMainTaskId, MAIN_CHECK_MIN_VOLTAGE_MSG, NULL, 0);
}
}
定时器工作原理:
软件定时器是基于TICK中断的回调实现的,其是在中断中运行,并分像RTOS那样是由任务Task在运行,故使用定时器时避免耗时太多。
定时器创建:
调用函数createTimer可以创建一个定时器.
/**
* function: Create timer
* 作用: 创建一个定时器,此定时器属于硬件定时器,避免定时器回调函数中有大量代码.
* 定时器一旦创建后,在删除前,就会周期性运行;如果需要单次定时器,请用延时事件来实现.
* 参数1: 定时时间,1mS为单位,0-65535
* 参数2: 回调函数指针,即任务函数指针,任务函数原型 void vTimerFunc(void *param);
* 返回: false or true
* 任务创建失败时,请检查osConfig中任务最大数量"MAX_TASK_NUM",或者定时器已经被创建.
**/
bool createTimer(uint32_t timeMs, tpfTimerFunc pfFunction)
//函数原型bool createTimer(uint32_t timeMs, tpfTimerFunc pfFunction):
//第一个参数timeMs:设定定时器的时间,以mS为单位
//第二个参数pfFunction:定时器回调函数.
//返回:创建成功与失败,false or true
此方法创建定时器,是用回调函数做为味一标识;同一个回调函数只能创建一个定时器、
调用使用同一回调函数支持创建定时器,回调函数并不会被多个定时器调用,只能更新其参数。
定时器调度:
定时器设置定时器到后,会在定时器中断中直接回调函数处理定时器,这样达到抢占CPU的目的,但正由于在中断中处理,定时器中不能写太多程序,以免影响定时器正常运行.
重置定时器:
调用函数createTimer可以重置一个定时器的时间.
定时器删除:
事件创建后可调用delTimer,删除已经创建的事件;
函数原型delTimer(tpfTimerFunc pfFunction),参数为定时器的回调函数指针,返回成功或失败.
osQueue消息队列:
消息队列是OSAL的基础,任务的通讯和调度可以说就是基于消息队列来实现的,用户根据需要可自由创建各种类型的队列。
队列的底层是链表,研究队列可适合初始化学习链表。
消息队列创建方法:
定义消息结构
根据用户根据需求创建一个队列缓冲区,队列可以是单个字节的数据类型,可以是一个复杂的结构体类型,可参考任务部分创建的消息类型.
示例:
/* Define task structure */
/* 定义任务消息结构 */
typedef struct strTaskMessage
{
uint8_t taskId; //任务ID
uint8_t message; //消息值(标题)
uint16_t dataSize; //消息携带的数据大小
void *pData; //消息携带的数据缓冲区指针
}tsTaskMsg;
创建消息队列缓冲区(根据消息结构)
static tsTask gtTaskBuffer[MAX_TASK_MSG_BUF_NUM];
创建消息队列对象
使用结构体tsQueue,创建一个变量.以后读队列和取队列均通过此变量操作.
tsQueue gtTaskQueue;
创建队列(使用消息缓冲区和已创建的消息队列对象)
/**
* function:
* 作用: 创建一个消息队列,队列可以是任意形式的
* tsQueue *psQueue: Queue structure pointer
* 参数1: 消息队列对象指针,实际是一个机构体变量,用于存放链表.
* void *pBuf: Buffer pointer for storing messages
* 参数2: 用于存放消息的缓冲区指针,
* 缓冲区可以是结构体类型BUF,可以是一个基本类型的BUF.
* uint8_t deep: sizeof(buf)/sizeof(buf[0])
* 参数3: 缓冲区的深度,sizeof(buf)/sizeof(width);
* uint8_t width: Length of message data
* 参数4: 消息数据宽度;sizeof(data)
* return : false of true
* 返回: 成功 或 失败
**/
bool createQueue(tsQueue *psQueue, void *pBuf, uint8_t deep, uint8_t width)
使用bool createQueue(tsQueue *psQueue, void *pBuf, uint8_t bufLen, uint8_t dataLen); 来创建队列
- psQueue:消息队列对象指针,实际是一个机构体变量,用于存放链表.
- pBuf:消息队列的数据缓冲区,用于存放消息,用户根据需求动态申请或者用静态或者全局变量.
- deep:消息队列深度, sizeof(buf)/sizeof(dataLen),可以看成最多可以装多少组消息.
- width消息队列宽度,可以看成单组消息的数据大小.
- 返回值:创建成功与否
createQu(S_QUEUE, P_BUF):使用了宏定义将队列进行了简化
- S_QUEUE:队列的对象,宏定义将对象取地址操作来创建队列。
- P_BUF:队列的缓冲区,用户需要根据需要创建大小合适的缓冲区(该缓冲区必须是一个静态变量或者全局变量,然后将缓冲区传递进来,采用宏定义的缓冲区不能使用动态内存分配,因为宏定义无法判断出动态分配的内存大小。
/* 创建消息队列 */
createQu(sgTaskQueue,gsTaskBuffer);
发送消息
/***
** Function name: PushQueue
** Descriptions: 数据入队(正常入队)
** Param[input]: psQueue 队列指针
** Param[input]: vpBuf 数据指针
** Param[input]: num 消息的数量,注意:这里并非数据长度,而是装入消息的数量
** Returned value: 操作结果
***/
bool pushQueue(tsQueue *psQueue,void *pBuf, uint8_t num);
bool pushQueue(tsQueue *psQueue,void *pBuf,uint8_t ucLen):
第一个参数psQueue:消息队列对象指针,此对象是在创业消息队列前已经被创建。
第二个参数pBuf:是发送数据的指针,
第三个参数num:消息的数量,注意:这里并非数据长度,而是装入消息的数量,每个消息的大小是固定的。
返回值:发送成功与否
使用宏定义来简化消息的发送。
#define pushQuBuf(S_QUEUE, P_BUF, NUM) pushQueue(&S_QUEUE, (void *)P_BUF, NUM)
#define pushQuOne(S_QUEUE, S_BUF) pushQueue(&S_QUEUE, (void *)&S_BUF, 1)
pushQuOne()发送一个消息,
pushQuBuf()用来发数多个消息
消息的插入
/**
** Function name: PushQueueHead
** Descriptions: 数据入队(插队),此数据会优先出队
** Param[input]: psQueue 队列指针
** Param[input]: vpBuf 数据指针
** Param[input]: num 数据长度
** Param[output]: node
** Returned value: 操作结果
**/
bool pushQueuePrior(tsQueue *psQueue,void *pbuf,uint8_t num)
消息队列采用先进先出策略,有时有希望部分消息能各得到优先处理,这里就可以采用插入的方式处理。
bool pushQueuePrior(tsQueue *psQueue,void *pbuf,uint8_t num)
第一个参数psQueue:消息队列对象指针,此对象是在创业消息队列前已经被创建。
第二个参数pBuf:是发送数据的指针,
第三个参数num:消息的数量,注意:这里并非数据长度,而是装入消息的数量,每个消息的大小是固定的。
返回值:发送成功与否
接收消息
判断是否有消息
uint8_t getQueueDataNum(tsQueue *psQueue)可以判断队列中是否有消息和消息的条数.
/*
* 获取当前队列数据个数
*/
uint8_t getQueueDataNum(tsQueue *psQueue)
宏定义#define getQuDataNum(S_QUEUE) getQueueDataNum(&S_QUEUE)简化操作
#define getQuDataNum(S_QUEUE) getQueueDataNum(&S_QUEUE)
取消息
/*
** Function name: PullQueue
** Descriptions: 数据出队(队头出队队)
** Param[input]: psQueue 队列指针
** Param[input]: pbuf 数据指针
** Param[input]: num 消息的数量(或称为条数)
** Returned value: 取出消息的条数
*********************************************************************************************************/
uint8_t pullQueue(tsQueue *psQueue, void *pbuf, uint8_t num)
使用uint8_t pullQueue(tsQueue *psQueue,void *pbuf,uint8_t len)可以取出消息:
第一个参数psQueue:消息队列对象指针
第二个参数pbuf:为取消息缓冲区,
第三个参数num:要取的消息条数
返回:实际期出消息的条数。
系统的移植:
系统的移植较为简单, 只需要定义全局中断开关、创建一个定时器,并移植看门狗程序即可运行.
- 在osHardware.c 中可以看到void sysTickInit(void)定时器初始化函数,根据不同的平台就行修改并初始化定时器.
- 修改中断入口void SYSTEM_TIME_ISR (void).
- osHardware.h中规定了#define TICK_RATE_HZ 1000u定时器的运行频率,尽量使用此参数来自动计算定时器的初值,改部此参数可以达到修改定时器的目的;同时定时器和事件的事件参数均为自然时间,系统通过此参数转变为相对时间.故此参数需要准确.
- osHardware.h中定义了#define DISABLE_IRQ() __disable_irq()和#define ENABLE_IRQ() __enable_irq(),打开全局中断和关闭全局中断,使部分程序进入临界段进行保护,避免被中断服务打断。
- 系统默认调用了WDT功能,不同的MCU需要自己修改实现.
- 系统目前是在ARM_M0和ARM_M23(这两个平台TICK基本上是兼容的)的平台上开的,如果移植到其它平台时编译器未提供 #include <stdint.h> #include <stdbool.h>这两个库,需要自己定义,定义文件见”osTypedef.h”.
- 部分MCU在初始化TICK有特殊要求,建议移植时一定要看芯片用户手册.
任务延时和多任务
OSAL非抢占式,不带任务切换,故在任务中不能写死循环,任务执行某事件完成后必需立即返回,以便其它使用使用CPU,这样就大大提高了系统实时性,感觉系统像一个多任务在运行.
- 当某任务实际情况确实需要延时,可调用系统提供的bOsDelayMs,在当前任务延时的时候,系统会调度其它任务,达到多任务的目的,但由于bOsDelayMs可能会再次调度被延时的任务,造成递归调用,而使栈空间资源过多的消耗,故不建议bOsDelayMs同时被多个任务使用.在万不得以的情况下需要使用,需要适当增加栈区大小,并设置bOsDelayMs的调用限制.
HAL
为了更好的实现模块化设计,彻底解除应用与硬件的耦合,OSAL设计了HAL层.
bspGPIO
驱动做为HAL的基础驱动库,大多外设均需要依赖于bspGPIO来实现。
采用HAL的bspGPIO最大的好处就是不需要调用复杂的函数去初始化GPIO,而是采用简单的配置即可初始化GPIO,极的方便地程序的开发,也便于在同一个MCU上开发不同的程序,大大提高了工作效率。
typedef struct
{
rcu_periph_enum gpioRcu; //RCU_GPIOAx
uint32_t gpioPeriph; //GPIOx
uint32_t pin; //GPIO_PIN_x
uint32_t mode; //GPIO_MODE_INPUT,GPIO_MODE_OUTPUT,GPIO_MODE_AF,GPIO_MODE_ANALOG
uint32_t pull; //GPIO_PUPD_NONE,GPIO_PUPD_PULLUP,GPIO_PUPD_PULLDOWN
uint32_t otype; //GPIO_OTYPE_PP,GPIO_OTYPE_OD
uint32_t speed; //GPIO_OSPEED_2MHZ,GPIO_OSPEED_10MHZ,GPIO_OSPEED_50MHZ
uint32_t af; //GPIO_AF_x
}tsCfgGpio;
void gpioConfig(tsCfgGpio *cfg)
{
/* enable clock */
rcu_periph_clock_enable(cfg->gpioRcu);
/* connect port to cfg */
gpio_af_set(cfg->gpioPeriph, cfg->af, cfg->pin);
gpio_mode_set(cfg->gpioPeriph, cfg->mode, cfg->pull, cfg->pin);
gpio_output_options_set(cfg->gpioPeriph, cfg->otype, cfg->speed, cfg->pin);
}
bspUART
#define UART_TX {RCU_GPIOA, GPIOA, GPIO_PIN_3, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_OTYPE_OD, GPIO_OSPEED_10MHZ, GPIO_AF_1)
例:初始化GPIO
/* 配置GPIO */
#define UART_TX {RCU_GPIOA, GPIOA, GPIO_PIN_3, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_OTYPE_OD, GPIO_OSPEED_10MHZ, GPIO_AF_1)
tsCfgGpio gtCfgTx = UART_TX; //定义UART_TX脚对象
gpioConfig(>CfgTx);//初始化GPIO
typedef struct
{
rcu_periph_enum rcu;
uint32_t com;
uint32_t nvicIrq;
uint32_t nvicIrqPriority;
uint32_t nvicIrqSubPriority;
tsCfgGpio txIo;
tsCfgGpio rxIo;
}tsCfgUart;
#define DEV_UART0_0 /*UART0_A2A3, AF1*/ \
{RCU_USART0, USART0, USART0_IRQn, 2, 2,/*COM*/\
RCU_GPIOA, GPIOA, GPIO_PIN_2, GPIO_MODE_AF, GPIO_PUPD_NONE, GPIO_OTYPE_PP, GPIO_OSPEED_10MHZ, GPIO_AF_1, /*TX*/\
RCU_GPIOA, GPIOA, GPIO_PIN_3, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_OTYPE_OD, GPIO_OSPEED_10MHZ, GPIO_AF_1 /*RX*/}
typedef void (*pUartCallBackFun)(uint8_t *pBuf, uint16_t size);
/*!
\brief init Uart function
\param[in] cfg: USARTx(x = 0,1,2)
\param[in] baudRate: 2400,9600... 115200
\param[in] ePort: teUartPort
\param[in] bufSize: 0~65535
\param[in] pCallBack: UART Call Back Funce
\retval none
GPIO_AF:参考 datasheet 2.6.7 GD32E230xx pin alternate functions
注意:uart1不支持超时定时器,串口成帧需要应用层自行实现
*/
void uartInit(tsCfgUart *cfg, uint32_t baudRate,uint8_t *pReceBuf, uint32_t bufSize, pUartCallBackFun pCallBack)
接下来我看看我们是如何快速初始化UART的
/* UART接收回调函数 */
void rxCcb(uint8_t *pucBuf, uint16_t usSize)
{
uint8_t *pPtr;
bool bRet ;
pPtr = (uint8_t *)osMalloc(usSize+1);
if(NULL == pPtr)
return;
memcpy(pPtr,pucBuf,usSize);
bRet = sendMsgToTask(TaskId, MSG, (void *)pPtr, usSize);
if(false == bRet)
osFree(pPtr);
}
tsCfgUart gtCfgUart = DEV_UART0_0; //UART对象
uartInit(>CfgUart,9600, rxBuffer, BUF_UART_SIZE, rxCcb); //初始化UART
uartWrite(>CfgUart, txBuffer, 128); //UART发送
通过以上代码,您是否发现,现在我们操作UART是如此的简单;已经做到应用程序和UART硬件完全的隔离,这使得同一个应用可以被运行在不同的MCU,而不需要花太多的精力去修改;您需要做的只是移植驱动程序。