在进行设备端硬件模块开发之前,先来分享一个单片机通用的软件调度器框架 --- 基于任务和事件的OSAL调度器。
OSAL的全称是Operating System Abstraction Layer,即操作系统抽象层。OSAL最初的概念是由德州仪器TI在ZigBee的协议栈Z-Stack上引入的,严格意义上来说,它并不是一个传统意义的操作系统,但可以实现部分类似操作系统的功能。
基于任务和事件的调度器,可以为MCU单片机编程提供一种通用意义上的框架,事件event是这个框架的最小单位,多个事件组成一个任务task。当一个事件发生时,OSAL负责将该事件分配给能够处理这个事件的任务,然后任务判断该事件的类型,再调用相应的事件处理函数进行处理。
本次开源项目设备端的MCU代码均采用OSAL调度器,OSAL调度器的框架如下图所示:
这个OSAL调度器主要实现了以下两个功能:任务调度和时间管理。
任务调度:采用任务数组tasks_events来管理各层对应的任务事件,tasks_arr是任务事件的处理数组,使用osal_set_event()函数,可以为对应的任务设置事件,使用osal_clear_event()函数,可以清除对应的任务事件。
时间管理:OSAL使用STM32的滴答时钟systick作为时间基准,可以虚拟出多个软件定时器,使用这些软件定时器,可以设置延时任务事件,当延时时间到达后,会为对应的任务事件设置触发标志位,然后执行延时事件。
OSAL调度器主要涉及以下源文件:
osal.c和osal.h:主要是任务的注册和调度实现,以及提供任务事件函数设置函数和任务事件清除函数。
osal_clock.c和osal_clock.h:主要是OSAL调度器使用STM32的滴答时钟systick作为时基,更新系统的软件定时器计数值,以及查找定时任务是否到达定时时间。。
osal_timers.c和osal_timers.h:主要是通过链表的方式管理系统的软件定时器,对外提供软件定时任务的启动函数,软件定时任务的停止函数。
如何使用OSAL调度器?
先来看看OSAL调度器的调度流程图,如下图所示:
以上流程图,板级初始化和OSAL任务初始化,在main函数中完成,具体的调用方式可以查看GitHub里面的源代码文件。run_system()函数主要在while(1)大循环中调用,不断更新软件定时器并查找是否有定时任务,如果有立即执行的任务或定时任务,则执行任务事件函数,事件处理函数执行完成后,清除事件标志位。
run_system()函数主要在osal.c文件中实现,函数的实现方式如下所示:
void run_system(void)
{
unsigned char idx = 0;
osal_time_update(); //更新系统时间,查找定时器任务
do {
if (tasks_events[idx])break; //查找当前就绪的任务事件
}while (++idx < tasks_cnt);
if (idx < tasks_cnt) { //当前有任务就绪
unsigned short events;
__disable_irq();
events = tasks_events[idx]; //在任务列表取出任务
tasks_events[idx] = 0;
__enable_irq();
events = (tasks_arr[idx])( idx, events ); //调用事件处理函数
__disable_irq();
tasks_events[idx] |= events; //保存未处理的事件
__enable_irq();
}
}
举个例子:例如我们需要添加一个看门狗的任务来监测系统是否正在运行,并定时进行喂狗操作。那么可以新建任务源文件iwdg_task.c和头文件iwdg_task.h,然后在osal.h文件中定义一个任务ID:IWDG_TASK_ID,这个ID就代表看门狗任务,看门狗任务的一系列事件,可以在头文件iwdg_task.h中进行定义。
源文件iwdg_task.c提供看门狗任务的初始化函数和任务执行函数,每个任务都需要提供任务的初始化函数和任务执行函数,格式如下图所示。
/***********************************************************
** 函 数 名: uint16 iwdg_task(uint8 task_id, uint16 events)
** 输 入: task_id-任务id,event-事件id
** 输 出: events-未处理的事件
** 功能描述: 看门狗任务处理
** 全局变量: 无
** 调用模块: 无
** 作 者:
** 日 期:
***********************************************************/
static uint16 iwdg_task(uint8 task_id, uint16 events)
{
(void)task_id;
if( events & IWDG_FEED_EVENT )
{
iwdg_feed(); //定时喂狗
return ( events ^ IWDG_FEED_EVENT );
}
if( events & IWDG_RESET_EVENT)
{
while(1){delay_ms(10);}
}
return 0;
}
/***********************************************************
** 函 数 名: void iwdg_task_init(void)
** 输 入: 无
** 输 出: 无
** 功能描述: 看门狗任务初始化
** 全局变量: 无
** 调用模块: 无
** 作 者:
** 日 期:
***********************************************************/
void iwdg_task_init(void)
{
register_task_array(iwdg_task,IWDG_TASK_ID);
iwdg_init(IWDG_Prescaler_64,625);
osal_start_timer( IWDG_TASK_ID , IWDG_FEED_EVENT , IWDG_FEED_PERIOD , IWDG_FEED_PERIOD );
}
在函数iwdg_task_init()中,先通过register_task_array()函数,绑定IWDG_TASK_ID任务ID和iwdg_task()任务处理函数,然后再初始化STM32的看门狗外设,最后通过osal_start_timer()函数启动一个定时器,不断定时进行喂狗操作。
喂狗事件IWDG_FEED_EVENT和喂狗周期IWDG_FEED_PERIOD是在头文件iwdg_task.h中进行定义的,这里需要注意的是,每个任务事件都是按位操作的,目前每个任务下面最多定义16个事件如下图所示。
#ifndef __IWDG_TASK_H_
#define __IWDG_TASK_H_
#define IWDG_FEED_EVENT 0x0001 //定时喂狗事件
#define IWDG_RESET_EVENT 0x0002 //看门狗复位事件
#define IWDG_FEED_PERIOD 500 //定时喂狗周期
#define IWDG_RESET_PERIOD 1000 //看门狗复位时间
extern void iwdg_task_init(void);
#endif
对于OSAL调度器,以下函数接口使用得比较多:
/***********************************************************
** 函 数 名: unsigned char osal_set_event(unsigned char task_id, unsigned short event_flag)
** 输 入: task_id-任务id,event_flag-事件标志
** 输 出: 无
** 功能描述: 立即事件设置函数
** 全局变量: 无
** 调用模块: 无
** 作 者:
** 日 期:
***********************************************************/
extern unsigned char osal_set_event(unsigned char task_id, unsigned short event_flag);
/***********************************************************
** 函 数 名: unsigned char osal_clr_event(unsigned char task_id, unsigned short event_flag)
** 输 入: task_id-任务id,event_flag-事件标志
** 输 出: 无
** 功能描述: 立即事件清除函数
** 全局变量: 无
** 调用模块: 无
** 作 者:
** 日 期:
***********************************************************/
extern unsigned char osal_clear_event(unsigned char task_id, unsigned short event_flag);
/***********************************************************
** 函 数 名: unsigned char osal_stop_timer( unsigned char task_id, unsigned short event_id )
** 输 入: task_id-任务id,event_id-事件id
** 输 出: 无
** 功能描述: 停止定时器事件
** 全局变量: 无
** 调用模块: 无
** 作 者:
** 日 期:
***********************************************************/
extern unsigned char osal_stop_timer( unsigned char task_id, unsigned short event_id );
/***********************************************************
** 函 数 名: unsigned char osal_start_timer(unsigned char task_id, unsigned short event_id,\
unsigned int timeout_value ,unsigned int reload_timeout_value)
** 输 入: task_id-任务id,event_id-事件id,timeout_value-超时值,reload_timeout_value-重装值
** 输 出: 1-添加定时器事件成功,FALSE-添加定时器事件失败
** 功能描述: 添加定时器事件
** 全局变量: 无
** 调用模块: 无
** 作 者:
** 日 期:
***********************************************************/
extern unsigned char osal_start_timer(unsigned char task_id, unsigned short event_id, \
unsigned int timeout_value ,unsigned int reload_timeout_value);
函数osal_set_event()和函数osal_clear_event()主要用来设置和清除立即执行的任务事件,当任务事件设置后,在run_system()函数中,该事件会立即执行。
函数osal_start_timer()和osal_stop_timer()主要用来设置和停止定时器任务事件,当定时时间到达后事件才会执行。osal_start_timer()的第三个参数timeout_value表示首次执行的超时时间,第四个参数reload_timeout_value表示下一次执行的重装载时间。如果要定时任务事件只触发一次,只需要把第四个参数reload_timeout_value置为0即可。
对于德州仪器TI提供的原生OSAL调度器,还有消息通信机制,不同的任务之间通过消息队列来进行数据传输,以实现任务间的数据访问。但这里为了使调度器简单易用,并没有把消息通信机制一并整合进来,使用这个调度器,多个任务间的数据仍然以共享内存的方式进行访问。
项目的开源地址:
https://github.com/embediot/Embedded-IoT-Project
https://gitee.com/embediot/Embedded-IoT-Project