# eOSAL-GD32E230-GD32F310 **Repository Path**: cl_jon/e-osal-GD32E230 ## Basic Information - **Project Name**: eOSAL-GD32E230-GD32F310 - **Description**: 基于事件驱动的模拟操作系统 - **Primary Language**: C - **License**: MIT - **Default Branch**: master - **Homepage**: https://blog.csdn.net/yuleying/article/details/128042546?spm=1001.2014.3001.5501 - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 3 - **Created**: 2025-04-21 - **Last Updated**: 2025-04-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 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,在共享文件中获得。 # 软件整体架构 ![在这里插入图片描述](https://img-blog.csdnimg.cn/9a687976ee1744238ea9c916801a19aa.png) OASL采用3层架构:硬件层,OASL+HAL层,应用层 ## 第一层 LIB层:为芯片原创提供的库文件 ## 第二层 - OSAL:为系统层,为用户提供编程框架. - HAL:为驱动抽象层,抽象标准驱动库,让应用与硬件隔离. - HAL不依赖于OSAL,是一个独立的框架,用户在不需要OSAL的时候也可单独使用HAL - OSAL不依赖于HAL,用户可以跳过HAL支持操作LIB层,实现一些特定的功能. ## 第三层 应用层,用户自己应用程序. OSAL详解 概要 OSAL框架 ![在这里插入图片描述](https://img-blog.csdnimg.cn/351f52798776416b8d1c166808c88802.png) # 系统硬件层(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就越大, ```c /** 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中定义了任务函数的原型 ```c /* 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) 注意被删除后,应用层无法对应用是否删除进行判断,此时用户要避免还要此任务发送消息. ## 任务初始化: 任务创建时系统会自动发一个初始化消息给任务,任务接是到此消息时可以初始化各变量,或者硬件资源. ```c /* 给当前任务发送初始化消息 */ sendMsgToTask(gtTask.taskIdMax, TASK_INIT, NULL, 0); /* 系统初始化 */ case TASK_INIT: { adcInit(>CfgAdcVol); createTimer(1000,secTimer); }break; ``` ## 任务调度: 当有消息发给任务时,任务调度器(osScheduler)会从任务消息取出消息,并回调任务函数。 ## 任务消息发送: 调用sendMsgToTask可以发送消息给指定的任务. ```c /** * 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获取数据长度,任务根据消息内存做出相应的处理,处理完成后返回,系统再次调度其它任务. 当任务在处理消息发生新的消息,任务可以向自己发送另外一个消息. ```c 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函数; ```c /* ** 系统延时并发数 ** 加大此时,在任务中用到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”文件定义了任务最大数 ```c #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可以创建一个事件, ```c /** * 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就处理不了其它事情,如以下代码 ```c led_on(); delay_ms(100); led_off(); ``` - 常见的第二个是开一个定时器,创建一个全局变量,将变量放在定时器中自减,开灯后设置变量,再一直判断这个变量,当变量到达预期的值,去关闭这个灯. ```c led_on(); led_off_time = 100; void timer(void) { if(led_off_time) { if(--led_off_time == 0) led_off(); } } ``` - 以上两个方法虽然能实现但过程比较麻烦,而采用Event后,代码变成如下方法: - 写一个关闭灯的回调函数,打开灯->createEvent注册事件,然后就不管了;时间到了,灯就会自动关闭; ```c led_on(); osCreatEvent(ledOff) void ledOff(uint32_t eventId, uint32_t param) { led_off(); } ``` - 这样处理逻辑简单明了,解决了第一个方法死等,CPU资源浪费;简捷了第二个方法变量漫天飞的情况. ## osTimer软件定时器(以下简称定时器): ## 定时器配置: 在”osConfig.h”文件定义了任务最大数 #define MAX_TIMER_NUM 3u, 需要开发者根据情况需求调整大小,此值越大所占内成RAM就越大. 定时器回调函数原型 ```c /* 定义定时器函数指针数组类型*/ 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可以创建一个定时器. ```c /** * 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的基础,任务的通讯和调度可以说就是基于消息队列来实现的,用户根据需要可自由创建各种类型的队列。 队列的底层是链表,研究队列可适合初始化学习链表。 ## 消息队列创建方法: 定义消息结构 根据用户根据需求创建一个队列缓冲区,队列可以是单个字节的数据类型,可以是一个复杂的结构体类型,可参考任务部分创建的消息类型. 示例: ```cpp /* 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; ## 创建队列(使用消息缓冲区和已创建的消息队列对象) ```c /** * 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); ## 发送消息 ```c /*** ** 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:消息的数量,注意:这里并非数据长度,而是装入消息的数量,每个消息的大小是固定的。 返回值:发送成功与否 ## 使用宏定义来简化消息的发送。 ```c #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) ``` 消息队列采用先进先出策略,有时有希望部分消息能各得到优先处理,这里就可以采用插入的方式处理。 ```c bool pushQueuePrior(tsQueue *psQueue,void *pbuf,uint8_t num) 第一个参数psQueue:消息队列对象指针,此对象是在创业消息队列前已经被创建。 第二个参数pBuf:是发送数据的指针, 第三个参数num:消息的数量,注意:这里并非数据长度,而是装入消息的数量,每个消息的大小是固定的。 返回值:发送成功与否 ``` ## 接收消息 ```c 判断是否有消息 uint8_t getQueueDataNum(tsQueue *psQueue)可以判断队列中是否有消息和消息的条数. /* * 获取当前队列数据个数 */ uint8_t getQueueDataNum(tsQueue *psQueue) 宏定义#define getQuDataNum(S_QUEUE) getQueueDataNum(&S_QUEUE)简化操作 #define getQuDataNum(S_QUEUE) getQueueDataNum(&S_QUEUE) ``` ## 取消息 ```c /* ** 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 #include 这两个库,需要自己定义,定义文件见”osTypedef.h”. - 部分MCU在初始化TICK有特殊要求,建议移植时一定要看芯片用户手册. # 任务延时和多任务 OSAL非抢占式,不带任务切换,故在任务中不能写死循环,任务执行某事件完成后必需立即返回,以便其它使用使用CPU,这样就大大提高了系统实时性,感觉系统像一个多任务在运行. - 当某任务实际情况确实需要延时,可调用系统提供的bOsDelayMs,在当前任务延时的时候,系统会调度其它任务,达到多任务的目的,但由于bOsDelayMs可能会再次调度被延时的任务,造成递归调用,而使栈空间资源过多的消耗,故不建议bOsDelayMs同时被多个任务使用.在万不得以的情况下需要使用,需要适当增加栈区大小,并设置bOsDelayMs的调用限制. # HAL 为了更好的实现模块化设计,彻底解除应用与硬件的耦合,OSAL设计了HAL层. - 引入HAL库同时还可以快速的完成程序的开发. # bspGPIO 驱动做为HAL的基础驱动库,大多外设均需要依赖于bspGPIO来实现。 采用HAL的bspGPIO最大的好处就是不需要调用复杂的函数去初始化GPIO,而是采用简单的配置即可初始化GPIO,极的方便地程序的开发,也便于在同一个MCU上开发不同的程序,大大提高了工作效率。 ```c 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 ```c #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的 ```c /* 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,而不需要花太多的精力去修改;您需要做的只是移植驱动程序。