Event Driven Class OSAL 基于事件驱动的模拟操作系统

2023-11-08

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(&gtCfgAdcVol);         
    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(&gtCfgAdcVol);           
        }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层.

  • 引入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(&gtCfgTx);//初始化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(&gtCfgUart,9600, rxBuffer, BUF_UART_SIZE, rxCcb);    //初始化UART
uartWrite(&gtCfgUart, txBuffer, 128);    //UART发送

通过以上代码,您是否发现,现在我们操作UART是如此的简单;已经做到应用程序和UART硬件完全的隔离,这使得同一个应用可以被运行在不同的MCU,而不需要花太多的精力去修改;您需要做的只是移植驱动程序。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Event Driven Class OSAL 基于事件驱动的模拟操作系统 的相关文章

  • 功能强大的国产Api管理工具

    前言 如果你是一名Java后端开发工程师 像Swagger Postman RAP这些工具 应该再熟悉不过了吧 为我们的接口开发工作带来了很多的便捷 不过因为这些都是独立的框架 之间并不存在互通性 因此在多个框架间协调的时候 不可避免的会带
  • Anaconda下Jupyter Notebook执行OpenCV中cv2.imshow()报错(错误码为1272)网上解法汇总记录和最终处理方式

    零 我设备的相关信息 Python 3 8 8 Anaconda3 2021 05 查询匹配python3 8 的OpenCV匹配版本为 4 1 4 2 我最后安装4 2 0 32版本 如下我记录了 从发现问题 到不断试错 最后解决问题 的
  • 电视制式 NTSC PAL SECAM

    电视制式 电视的制式是指电视信号的标准 目前各国的电视制式不尽相同 如附件1 2 制式的区分主要在于其帧频 场频 的不同 分解率的不同 信号带宽以及载频的不同 色彩空间的转换关系不同等 电视制式现在有模拟和数字信号和数字信号两种 模拟制式一
  • 【Linux】进程篇(补):守护进程

    文章目录 1 补充 1 1 查看 1 2 控制进程组的方式 2 创建守护进程 step1 忽略信号 step2 让自己不是组长 step3 setsid 函数 给调用函数设置新的会话和进程组 ID step4 chdir 函数 可以改变守护
  • Android Service最全面的解析

    本篇文章再次来自 刘明渊 话说刘明渊已经是我公众号的老熟人了 这是第三次发表他投稿的文章 前两篇关于Intent的译文都广受大家好评 而本篇对于Service的译文同样精彩 其实像这种官方文档翻译类文章的投稿我都是非常欢迎的 因为官方文档的
  • 基于内容的图像检索技术(1):从特征到检索

    作者 赵丽丽 链接 https zhuanlan zhihu com p 46735159 来源 知乎 著作权归作者所有 商业转载请联系作者获得授权 非商业转载请注明出处 基于内容的图像检索 CBIR Content Based Image
  • 解决pandas中boxplot函数默认添加大标题“Boxplot grouped by ”

    问题 如何删除下图的 Boxplot grouped by path num 生成上图的代码 fig ax plt subplots figsize 8 6 df df path num 2 boxplot column travel ti

随机推荐

  • 【软件工程

    概念 耦合 coupling 是对两个模块之间联接程度的一种度量 模块间的依赖程度越大 则其耦合程度也就越大 反之 模块间的依赖程度越小 则其耦合程度也就越小 很显然 为了使软件具有较好的可维护性和可修改性 模块间的关联程度即耦合程度应越小
  • 网络安全鹰眼靶场(基础关)

    目录 前言 key在哪里 再加密一次你就得到key啦 猜猜这是经过了多少次加密 据说MD5加密很安全 真的是么 种族歧视 HAHA浏览器 key究竟在哪里呢 key又找不到了 冒充登陆用户 比较数字大小 本地的诱惑 就不让你访问 前言 靶场
  • CodeSmith 使用教程(3): 自动生成Yii Framework ActiveRecord

    上例介绍了使用CodeSmith编写代码模板的基本方法 本例实现一个较为实用的代码模板 通过数据库自动为Yii Framework生成所需要的ActiveRecord 类 本例通过修改Yii Framework 开发教程 26 数据库 Ac
  • numpy明明有高版本却显示版本不够

    不知道和我上篇文章添加Jupyter notebook代码提示是否有关系 大概率 在进行导入numpy库的时候出现了以下问题 真的特别离奇 我之前都用得好好的 怎么突然不行了 接下来就对库版本进行折腾 以下操作均在管理员运行的Anacond
  • 全球计算机出货量排名,2018年全球电脑出货量排名:联想夺冠,惠普戴尔分列二三...

    作者 虎龙吟 美国当地时间2019年1月10日 Gartner发布了2018年全球个人电脑出货量数据报告 根据Gartner发布的数据 2018年第四季度 全球个人电脑出货量达到6860万台 比2017年第四季度下降了4 3 根据Gartn
  • chatglm2-6b在P40上做LORA微调

    背景 目前 大模型的技术应用已经遍地开花 最快的应用方式无非是利用自有垂直领域的数据进行模型微调 chatglm2 6b在国内开源的大模型上 效果比较突出 本文章分享的内容是用chatglm2 6b模型在集团EA的P40机器上进行垂直领域的
  • 萌妖出没服务器维护电视版,萌妖出没-萌妖出没手游官网版预约-9k9k手游网

    萌妖出没是一款根据经典动漫改编而成的策略竞技手游 游戏中还原了众多经典的场景 玩家在玩游戏的过程中还有回味众多经典的动漫情节 众多宠物精灵等你来收集 打造强大的精灵战队 带领它们不断的战斗冒险 体验其中的无穷乐趣 感兴趣的玩家随时可以来下载
  • 牛客网——字符串排序(C++)

    题目描述 编写一个程序 将输入字符串中的字符按如下规则排序 规则 1 英文字母从 A 到 Z 排列 不区分大小写 如 输入 Type 输出 epTy 规则 2 同一个英文字母的大小写同时存在时 按照输入顺序排列 如 输入 BabA 输出 a
  • R语言 第四章 初级绘图(3)核密度图,小提琴图,QQ图,星状图,等高图,固定颜色选择函数,渐变色生成函数,主体调色板,rainbow(),RcolorBrewer包

    关注公众号凡花花的小窝 收获更多的考研计算机专业编程相关的资料 绘制其他图形 核密度图 sm包中sm density compare函数用于绘制核密度图 核密度图是用一条密度曲线而不是通过柱状来展示连续型变量的分布 相比直方图 密度图的一个
  • getUserProfile:fail 调用失败?getUserProfile:fail can o

    一般Fail原因有很多 如果fail函数的参数返回结果有具体的提示错误 比如长度关键字等问题 那么根据提示直接更改就行 还有一种情况就是我们使用测试号 Uni开发时 我们调用getUserProfile函数返回错误 我们首先要考虑AppId
  • 分享三个不同目录双向更新的实用方法

    分享三个不同目录双向更新的实用方法 方法一 rsync 判断脚本 进行双向更新 方法二 使用rsync的 u选项 方法三 使用rsync inotify监控工具 扩展 方法一 rsync 判断脚本 进行双向更新 脚本内容如下 bin bas
  • 一起来!白嫖Amazon DynamoDB!!!

    Amazon DynamoDB简介 Amazon DynamoDB是由Amazon Web Services AWS 提供的一种快速 灵活 全托管的NoSQL数据库服务 支持文档和键 值数据模型 它具有自动扩展 低延迟 高可靠性 高吞吐量等
  • 4.2 配置Mysql与注册登录模块(中)

    目录 学习目标 学习内容 后端 JWT工具类 数据库修改 写具体业务API 根据token获取用户信息 注册API 前端 这节课实现了登录效果 学习目标 jwt验证 后端的API 前端登录注册页面 学习内容 前端和后端会有跨域问题 不用传统
  • Linux 中的 colcrt 命令及示例

    Linux 系统中的colcrt命令用于格式化文本处理器输出 以便可以在阴极射线管显示器上查看 它删除了下划线 删除线和下划线 这些内容无法在 CRT 上显示 因为在 CRT 屏幕上的给定位置只能生成一个字符 它还将所有下划线放在新行上 位
  • 华为服务器怎么升级2016系统,服务器怎么升级

    服务器怎么升级 内容精选 换一换 华为云帮助中心 为用户提供产品简介 价格说明 购买指南 用户指南 API参考 最佳实践 常见问题 视频帮助等技术文档 帮助您快速上手使用华为云服务 当您购买的弹性云服务器规格无法满足业务需要时 您可以随时变
  • Ruby

    1 如何安装ralis 在线安装常常因为公司proxy server的原因产生连接问题 所以可以先到https rubygems org下载然后离线安装 gem install l rails2 3 5 gem
  • 【LeetCode刷题】-岛屿数量

    Task 思路 1 首先判断给定的二维数组是不是空的 2 对二维数组遍历一下 对每个元素进行判断 a 如果这个值等于1 那么就把计数器 1 并且对该元素四周进行深度搜索 3 返回 代码 class Solution public int n
  • 定制 findbugs规则

    come from http www 51testing com html 97 13997 211893 html Findbugs是著名的开源java静态代码分析工具 基于bytecode扫描 具备数据流分析能力 操作很简单 可以下载单
  • vue dialog 弹窗组件

    因为官方组件不是很好用所以自己写了一个弹窗组件 javascript
  • Event Driven Class OSAL 基于事件驱动的模拟操作系统

    EventDrivenClassOSAL详解 Event Driven Class OSAL 基于事件驱动的模拟操作系统 前言 什么是OSAL OSAL为 Operating System Abstraction Layer 即 操作系统抽