A005-软件结构-从前后台到调度器

来源:互联网 发布:阿里云产品 编辑:程序博客网 时间:2024/06/13 02:31

主要内容:
(1). 前后台
(2). 事件管理
(3). 时间触发的调度器(分时复用)
(4). 事件触发的调度器(状态机)
(5). 中断的上下半部机制

-------------------------------------------------------------------------------------------------------------------------------------
开发环境:AVR Studio 4.19 + avr-toolchain-installer-3.4.1.1195-win32.win32.x86
芯片型号:ATmega16
芯片主频:8MHz


-------------------------------------------------------------------------------------------------------------------------------------

本文将一步步地、将软件的结构、从简单前后台过渡到调度器


-------------------------------------------------------------------------------------------------------------------------------------

1、 概述:


      简单的前后台结构如上图所示。
      前台中断为中心,后台CPU为中心。
      这里显示着程序涉及到的3个资源:中断RAMCPU,并隐含第4个资源:时间(CPU消耗多少比例的时间在某个任务上)。

      这种结构下、每个任务产生的数据,都直接作为全局变量放在RAM里面、所有任务都可以直接使用。
      其中:
1、1ms定时任务:每隔1ms更新一次时刻计数
2、红外接收任务:任意时刻(随机)收到红外码、就更新红外接收数据的数值
3、数值运算01任务:计数完毕后、更新计算结果01的数值
4、数码管刷新任务:需要读取计算结果01的数值
5、红外发送任务:需要读取按键码的数值、如果是按键1按下、就启动1次红外发送
6、按键扫描任务:任意时刻(随机)按下按键、就更新按键码的数值

      这样的结构容易出现以下问题:
1、任务数量如果较多、就会有很多任务函数排队在后台CPU的主循环中等待被顺序执行、显得比较拥挤。
      我们不能确定地知道某个任务到底是在哪个时刻被执行的,这使得我们只能粗略的估计出一个任务会在间隔多久后被执行。
      而按键扫描数码管刷新等任务最好在稳定的间隔时刻被周期性地执行,才能保证最终的效果。
2、任务之间可以直接调用其他任务的子函数、这会导致代码结构不够清晰,功能越复杂、互相调用越多,维护代码就越麻烦。
      而任务之间使用全局变量来传递数据和信息的情况、将会加大这种维护的难度。
3、某些数据可能会同时被多个任务使用,这是可能出现冲突:任务1正在使用数据A、此时中断中的任务2打断进来、修改了数据A
      等到程序返回任务1后、被修改的数据A可能导致本次的任务1出错。

     对此、我们可以做如下改进、以应对这些问题:
1、使用某种任务调度方式:分时调度、事件触发调度
2、引入事件管理,将部分共享的数据纳入事件队列统一存储管理
3、对数据访问引入加锁
4、尽量减少可以中断其他任务的抢占式任务的数量
5、中断中只收发数据,具体的数据处理放入后台任务,比如使用中断上下半部方式

-------------------------------------------------------------------------------------------------------------------------------------

2、后台CPU分时调度任务

      这一步将使用分时调度的方式对后台CPU处理的任务队列进行改进,具体结构如下:
      CPU每隔1ms或调度1个任务,直到所有任务都被调用一遍。
      大体结构如下:

      这里设置一个长度为6的任务队列,CPU每隔1ms就去任务队列中调度1个任务,直到完全遍历任务队列的全部6个元素。
      调度周期是6ms,也就是说、每个任务都是每隔6ms被调度1次,或者说是每个时刻调度1个任务,调度周期是6个时刻。
      如果任务数量少于6个,也并不减小任务队列的长度、因为我们需要保持每个任务的调度周期都是固定的。
      这种实现方式相当简洁,任务在何时被调度是很清晰的。

      CPU也可以让每个任务有自己的周期:
      (1). 每隔10个时刻调度1次红外发送任务(通常延迟10ms再启动数据发送并不会有什么副作用)
      (2). 每隔10个时刻调度1次按键扫描任务
      (3). 每隔  2个时刻调度1次数码管刷新任务
      (4). 每隔  1个时刻调度1次数值计算01任务
      这将使用一个时间触发的调度器来实现、大体结构如下(1个任务的调度周期一到、就认为该任务已就绪):


-------------------------------------------------------------------------------------------------------------------------------------

3、前台分时调度任务

      既然是利用1ms定时任务产生的时刻值去调度任务,那么也可以直接在前台里面、每当产生新的时刻值、就去调度1个任务:

      CPU在这里什么都不用做、任务结束后去休眠即可。
      每次中断到来时、CPU被唤醒,将中断函数执行完毕并返回之后,CPU再次进入休眠
      很多应用中、这也是一个很好的方式,整个系统完全由中断事件驱动CPU平时处于静默。

      而对于任务较多、功能较为繁重的情形,一般使用由CPU调度任务的方式、以保持中断轻巧简洁
      以应对较多的中断事件,尤其是随机的中断事件

-------------------------------------------------------------------------------------------------------------------------------------

4、事件/消息管理

(1). 概述

      上面是任务调度上的组织,下面进行RAM数据上的组织,使得任务之间互相隔离、不再互相使用对方的子函数全局变量

事件1ms定时任务 每隔1ms产生一个时刻值,我们可以视为是每隔1ms产生一个事件:1ms时刻到事件、或时刻(时基)更新事件。
           按键扫描任务在按键按下后产生按键码,我们也将其视为是发生了1个事件:按键按下事件、带一个参数(按键号和按键类型)
消息:本文将事件(event)所带的参数称为消息(message),以区分事件本身和事件的参数
          事件/消息管理简称事件管理

      在简单的前后台结构里面、事件消息都是作为数据、直接使用全局变量来存放的。
      下面要将这些数据统一放在一个事件队列里面进行管理,不再分散到每个任务单独管理。
      每个任务产生的事件、都统一交给事件队列存储管理,它们不再是任务私有的数据

      事件管理下的后台CPU分时调度任务方式

      比如、数值计算01任务在执行后将向事件队列发出2个事件计算结果01事件(参数=计算结果),数码管刷新事件(参数=计算结果)。
      虽然这些数据需要显示在数码管上,但产生这些数据数值计算01任务不去调用数码管显示函数
      它并不负责这个、也不关心这些数据是否被数码管正确地使用。
      数码管刷新任务自己会到事件队列里面去查询数码管刷新事件的是否有效。
      如果该事件的有效,它就将数码管刷新事件对应的参数取出来、送到数码管显示,至于这个数据由哪个任务产生,它并不关心。
      也就是说、任务之间相互独立。

      关于事件管理在前后台中的应用、可以参考这篇文章《消息机制在软件设计中的应用》。

(2). 基本结构

事件队列的结构如下:

      图中事件队列的结构中包含了事件的三个信息:事件类型(告诉我们这是什么事件)、事件的参数、事件的锁定状态。
      事件(event)放入type部分,事件的消息(message)放入data部分。
      对应如下结构:
// 事件队列的结构(type[7bit],lock[1bit],data[32bit])typedef struct {    uint8_t  type :7 ;  // 事件类型、如数码管数据有更新:EVENT_SEG_UPDATE    uint8_t  lock :1 ;  // 加锁标志    uint32_t data;      // 事件参数、如数码管的数据:1265214}T_EVENT_LIST, *pT_EVENT_LIST;
      至于消息是否需要加锁
      1、如果约定所有中断中都不访问事件队列,就不需要加锁
           此时,中断做得比较小巧、只接收或发送数据,数据处理都在后台的某个任务中完成。
           在某个任务访问事件队列期间,打断它的中断都不会访问事件队列,因为不用担心数据会被修改。
      2、如果允许中断访问事件队列,就需要加锁
      3、如果后台CPU调度方式里面、包含软件中断,那么可能需要加锁

(3). 事件队列的代码实现

sys_event.h
// ==========================================================================================================// Copyright (c) 2016 Manon.C <codingmanon@163.com>// // Permission is hereby granted, free of charge, to any person obtaining a copy of this software and // associated documentation files (the "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or // sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject // to the following conditions:// // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software.// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING // BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.// // ---------------------------// 本文定义了事件管理模块// // 说明:// (1).本文将事件(event)所带的参数称为消息(message),以区分事件本身和事件的参数// // ==========================================================================================================#ifndef __SYS_EVENT_H__#define __SYS_EVENT_H__#include <avr/interrupt.h>#include "sys_timer.h"#include "config.h"// 事件(事件的类型tpye,为8bit)(事件的参数data,为32bit)typedef enum {    EVENT_SYS,    EVENT_KEY,    EVENT_IR_RECIEVE,    EVENT_IR_SEND,    EVENT_RTC,    EVENT_DIGITAL_FORMAT,// 数据进制格式、范围:[2,16]进制    EVENT_SEG_UPDATE,    // 参数为32bit的事件(必须至少有一个、避免数组sys_event_int32[]的元素个数为0)    EVENT_MAX}EVENT;// 事件队列的结构(type[7bit],lock[1bit],data[32bit])typedef struct {    uint8_t  type :7 ;  // 事件类型、如数码管数据有更新:EVENT_SEG_UPDATE    uint8_t  lock :1 ;  // 加锁标志    uint32_t data;      // 事件参数、如数码管的数据:1265214}T_EVENT_LIST, *pT_EVENT_LIST;// 事件管理器的结构(任务独占的事件缓存应是这种结构:T_EVENT_INT32 task_event_buffer[])typedef struct {          uint8_t number;   // 缓存中的事件数量    pT_EVENT_LIST pBuffer;  // 事件缓存的地址}T_TASK_EVENT_BOX;void sys_event_lock(uint8_t type);void sys_event_unlock(uint8_t type);void sys_event_unlock_all(void);uint8_t sys_event_any_lock(void);void sys_event_init(void);void sys_event_buffer_set(const p_void_funtion_void task, const pT_EVENT_LIST buffer);void sys_event_buffer_post(const p_void_funtion_void task, const uint8_t event_number);bool sys_event_push(void);bool sys_event_post(uint8_t type, uint32_t data);bool sys_event_get(pT_EVENT_LIST event);bool sys_event_peek(uint8_t type, uint32_t data);bool sys_event_data(uint8_t type, uint32_t *data);#endif// #ifndef __SYS_EVENT_H__
sys_event.c
#include "sys_event.h"// 事件队列static T_EVENT_LIST sys_event_list[EVENT_MAX];// 事件管理器// (保存着每个任务独占的事件缓存的首地址,数组下标和任务队列的下标保持一致)T_TASK_EVENT_BOX task_event_box[SYS_TASK_MAX];// ==========================================================================================================//      锁定事件队列中的元素// // ==========================================================================================================void sys_event_lock(uint8_t type){    if(type < EVENT_MAX)    {        sys_event_list[type].lock = LOCKED;    }}// ==========================================================================================================//      解锁事件队列中的元素// // ==========================================================================================================void sys_event_unlock(uint8_t type){    if(type < EVENT_MAX)    {        sys_event_list[type].lock = UNLOCKED;    }}// ==========================================================================================================//      检查是否有事件被锁定// // 返回值:index  被锁定事件的事件号// // 说明:// (1). 从头开始查找,直到找到第一个被锁定的事件为止// (2). 调度器在每次调度新任务前,都会检查所有事件,确保没有任何锁的存在//      因为,每个任务退出后、必须解锁所有的锁// // ==========================================================================================================uint8_t sys_event_any_lock(void){    uint8_t index;    for(index = 0; index < EVENT_MAX; index++)    {        if(LOCKED == sys_event_list[index].lock)        {            break;        }    }    return index;}// ==========================================================================================================//      解锁所有事件// // ==========================================================================================================void sys_event_unlock_all(void){    uint8_t index;    for(index = 0; index < EVENT_MAX; index++)    {        sys_event_list[index].lock = UNLOCKED;    }}// ==========================================================================================================//      事件队列初始化、事件缓存管理器初始化// // ==========================================================================================================void sys_event_init(void){    uint8_t index;    for(index = 0; index < EVENT_MAX; index++)    {        sys_event_list[index].type = EVENT_MAX;        sys_event_list[index].lock = UNLOCKED;        sys_event_list[index].data = 0;    }    for(index = 0; index < SYS_TASK_MAX; index++)    {        task_event_box[index].number  = 0;        task_event_box[index].pBuffer = NULL;    }}// ==========================================================================================================//      直接将每个任务产生的事件写入到事件队列// // 返回值:Fin  TURE  = 写入成功//              FALSE = 写入失败(事件被锁定、或事件是无效的事件)// // ==========================================================================================================bool sys_event_post(uint8_t type, uint32_t data){    bool Fin = FALSE;    if(type < EVENT_MAX)    {        if(UNLOCKED == sys_event_list[type].lock)        {            sys_event_list[type].lock = LOCKED;            sys_event_list[type].type = type;            sys_event_list[type].data = data;            sys_event_list[type].lock = UNLOCKED;            Fin = TRUE;        }    }    return Fin;}
写入事件
使用上面这几个函数来将事件写入事件队列,并设置事件的锁定状态。
比如、任务A可以使用函数bool sys_event_post(uint8_t type, uint32_t data);将1个事件及其参数直接写入事件队列。

但是,可能会有事件写入失败,比如:
1、任务B正在访问这个事件、并已经将其锁定。
2、接着任务A打断任务B,并且要去更新(写入)这个事件,这个写入操作会因为该事件被锁定而导致写入失败。
     如果写入失败,任务A就只能将这个事件保存起来,以便下次再次尝试写入。

鉴于任务A会写入失败,我们给出了另一种方法:
将事件写入操作独立出来,让事件管理模块自己负责去写入这个事件。
任务A不负责写入操作,只是将事件保存起来,并通知事件管理模块、这里有个事件需要写入。

为了让事件管理模块自己去负责事件的写入,需要:
1、任务A需要建立一个缓存来保存自己产生的所有事件、并且任务A独占这个缓存,其他任务不会访问这个缓存。
2、建立一个事件管理器task_event_box[SYS_TASK_MAX],将任务A及其他所有任务独占的缓存们都注册到里面。
3、任务管理模块将遍历检查事件管理器,如果发现任务A有事件需要写入,就将其写入事件队列。
     如果写入失败,可能是任务B正在访问这个事件并将其锁定了,但这个事件仍然保存在任务A的缓存中。
     在任务B退出后,任务管理模块再次遍历事件管理器时,就可以将该事件正常地写入了。

事件管理器和任务独占的缓存之间的关系如下:

具体代码:
// ==========================================================================================================//      将任务独占的事件缓存注册到事件管理器// // 参数:task       任务函数//       buffer     该任务独占的事件缓存的首地址(每个任务独占1个buffer,不和其他任务共享、无访问冲突)// // 说明:// (1). 一般在任务初始化函数里面、去注册该任务独占的事件缓存//      也就是说、这个函数一般在任务的初始化函数里面被调用// // ==========================================================================================================void sys_event_buffer_set(const p_void_funtion_void task, const pT_EVENT_LIST buffer){    uint8_t task_index;    task_index = sys_task_index(task);    if(task_index < SYS_TASK_MAX)    {        task_event_box[task_index].pBuffer = buffer;    }}// ==========================================================================================================//      设置任务需要发送的事件的数量// // 参数:task       任务函数//       number     该任务独占的事件缓存里面需要发送的事件的数量// // 说明:// (1). 每个任务根据需要建立事件缓存,并在任务退出时将缓存地址发给事件管理模块即可//      如果事件管理模块发现该任务需要发送的事件数量>0,它就会将这些事件送到事件队列,任务本身不需要自己去发送事件// (2). 被delete的任务的缓存还在,所以依然可以在被delete后得以发送// (3). 假如任务A的事件缓存(.buffer)有4个元素,但本次只发送2个事件、并设置.number = 2//      那么就需要将这2个事件放在.buffer[0]和.buffer[1],否则不能保证事件可以被发送//      因为我们在sys_event_push()里面只查询.buffer的前2个元素(前.number个元素),后面的不再查询//      所以一般将.number设为_countof(event_buffer)、如果这个值不是很大的话//      当然、如果已经确认将这2个事件放在了buffer[0]和buffer[1],那么只需.number = 2即可避免额外的任务消耗// // ==========================================================================================================void sys_event_buffer_post(const p_void_funtion_void task, const uint8_t event_number){    uint8_t task_index;    task_index = sys_task_index(task);    if(task_index < SYS_TASK_MAX)    {        task_event_box[task_index].number = event_number;    }}// ==========================================================================================================//      将所有任务的事件缓存中的事件保存到事件队列// // 返回值:Fin  FALSE = 至少有1个事件还未写入事件队列//              TRUE  = 所有事件都已成功地写入事件队列// // 说明:// (1). 每个任务独占1个buffer,不和其他任务共享、无访问冲突// (2). 假如有>=2个事件必须同时成功地写入事件队列、才能保证用户任务执行正常//      那么就必须在用户任务里面判断这>=2个事件同时有效// // ==========================================================================================================bool sys_event_push(void){    bool Fin = TRUE;    uint8_t post;       // 某个事件发送是否成功    uint8_t task_index; // 任务号    uint8_t msg_number; // 事件数量    uint8_t msg_index;  // 事件序号    T_EVENT_LIST event; // 事件及其参数    for(task_index = 0; task_index < SYS_TASK_MAX; task_index++)    {        msg_number = task_event_box[task_index].number;        if(msg_number > 0)        {            // --------            // 发送事件            for(msg_index = 0; msg_index < msg_number; msg_index++)            {                event.type = (task_event_box[task_index].pBuffer)[msg_index].type;                if(EVENT_MAX != event.type)                {                    event.data = (task_event_box[task_index].pBuffer)[msg_index].data;                    post = sys_event_post(event.type, event.data);                    if(TRUE == post)  // 事件发送成功,则清除该事件                    {                        (task_event_box[task_index].pBuffer)[msg_index].type = EVENT_MAX;                    }                }            }            // ----------------------------------------------------------------------------------            // 检查是否将全部事件都发送完毕,没发完就将未发送的部分移到前面,等待下次进来再次发送            post = TRUE;            for(msg_index = 0; msg_index < msg_number; msg_index++)            {                if(EVENT_MAX != (task_event_box[task_index].pBuffer)[msg_index].type)                {                    post = FALSE;                    Fin  = FALSE;  // 至少有1个事件还未写入事件队列                    break;                }            }            if(TRUE == post)            {                task_event_box[task_index].number = 0;            }        }    }    return Fin;}
任务管理模块使用函数bool sys_event_push(void)去遍历查询任务管理器,并将其中的事件写入事件队列。
其中使用到的函数uint8_t sys_task_index(const p_void_funtion_void task);在下面的调度器部分会给出,它用来读取一个任务的任务号。

读取事件
有两类读取方式:
1、遍历事件队列,看看有没有事件,这是无目的读取。
2、精确地读取某个事件,看看该事件是否存在,如果存在、可以读出它的参数。

代码:
// ==========================================================================================================//      查询事件队列// // 参数:  event    用于读出事件的type和data// // 返回值:Fin      TURE  = 读到1个事件及其消息参数//                  FALSE = 没有任何事件存在// // 说明:// (1). 由于总是从头开始查找,直到找到第一个有效的事件为止//      所以在typedef enum { }EVENT中越靠前的事件、越会被优先查询到// // ==========================================================================================================bool sys_event_get(pT_EVENT_LIST event){    bool Fin = FALSE;    uint8_t index;    event->type = EVENT_MAX;    event->data = 0;    for(index = 0; index < EVENT_MAX; index++)    {        if(UNLOCKED == sys_event_list[index].lock)        {            sys_event_list[index].lock = LOCKED;            if(EVENT_MAX != sys_event_list[index].type)            {                Fin = TRUE;                event->type = sys_event_list[index].type;                event->data = sys_event_list[index].data;                sys_event_list[index].type = EVENT_MAX;                sys_event_list[index].data = 0;            }            sys_event_list[index].lock = UNLOCKED;        }        if(TRUE == Fin)        {            break;        }    }    return Fin;}// ==========================================================================================================//      查看某个事件是否已经存在、要求参数也对应// // 参数:  type     事件//         data     事件的参数// // 返回值:FALSE    该事件没有发生、或被锁定//         TRUE     该事件已经发生,返回后将该事件从事件队列中清除// // ==========================================================================================================bool sys_event_peek(uint8_t type, uint32_t data){    bool Fin = FALSE;    if(type < EVENT_MAX)    {        if(UNLOCKED == sys_event_list[type].lock)        {            sys_event_list[type].lock = LOCKED;            if(sys_event_list[type].type == type)            {                if(sys_event_list[type].data == data)                {                    Fin = TRUE;                    sys_event_list[type].type = EVENT_MAX;                    sys_event_list[type].data = 0;                }            }            sys_event_list[type].lock = UNLOCKED;        }    }    return Fin;}// ==========================================================================================================//      查看某个事件是否已经存在、并取出事件的参数// // 参数:  type     事件号//        *data     取出该事件的参数// // 返回值:FALSE    该事件没有发生、或被锁定//         TRUE     该事件已经发生,返回后将该事件从事件队列中清除// // ==========================================================================================================bool sys_event_data(uint8_t type, uint32_t *data){    bool Fin = FALSE;    if(type < EVENT_MAX)    {        if(UNLOCKED == sys_event_list[type].lock)        {            sys_event_list[type].lock = LOCKED;            if(sys_event_list[type].type == type)            {                Fin = TRUE;                *data = sys_event_list[type].data;                sys_event_list[type].type = EVENT_MAX;                sys_event_list[type].data = 0;            }            sys_event_list[type].lock = UNLOCKED;        }    }    return Fin;}

(4). 事件管理下的任务函数

有了事件管理,一个任务将包含以下几个特征:
1、独占1个事件缓存
2、任务函数需要查询事件
3、任务函数需要将事件写入事件缓存、并通知事件管理模块

例如,数码管刷新任务的任务函数,它只需要查询消息:
// ==========================================================================================================// LED数码管刷新任务// // 查询消息:EVENT_SEG_UPDATE//           EVENT_DIGITAL_FORMAT// 消息参数:32位数值// 发送消息:无// // 说明:// (1). 在系统定时器或任务调度器中定时刷新(被作为1个任务去调度)// // ==========================================================================================================void task_Mod_LED_display(void){    uint32_t temp = 0;    // ------------------------------------------------    // 查询事件    if(TRUE == sys_event_data(EVENT_SEG_UPDATE, &temp))    {        p_LED_display_ctrl->set_data   = TRUE;  // 如果得到更新的数据、就启动数据拆分        p_LED_display_ctrl->data_index = 0;     // 如果正在拆分过程中、又一次需要拆分,就需要重新设置.data_index为0        p_LED_display_ctrl->data       = temp;        p_LED_display_ctrl->data_copy  = temp;    }    if(TRUE == sys_event_data(EVENT_DIGITAL_FORMAT, &temp))    {        p_LED_display_ctrl->set_format = TRUE;        p_LED_display_ctrl->format     = temp;    }    // -------------------------------------    // 任务正文    if(TRUE == p_LED_display_ctrl->set_data)    {        p_LED_display_ctrl->set_data = Mod_LED_display_set_data(p_LED_display_ctrl->format);    }    if(TRUE == p_LED_display_ctrl->set_format)    {        p_LED_display_ctrl->set_format = FALSE;        p_LED_display_ctrl->set_data   = TRUE;        p_LED_display_ctrl->data_index = 0;        p_LED_display_ctrl->data = p_LED_display_ctrl->data_copy;    }    // --------    // 刷新显示    Mod_LED_display_update();    // ----------------------    // 发送事件}

数码管模块的完整代码详见《B001-Atmega16-数码管》的最后一步。

计时任务需要建立事件缓存,并通知事件管理模块:
volatile uint32_t cout = 0;volatile uint32_t second = 10000000;  // 测试用变量// 事件缓存T_EVENT_LIST event_buffer_task_count_time[2];// ==========================================================================================================// 任务事件缓存初始化// // ==========================================================================================================void task_count_time_event_buffer_init(void){    uint8_t index;    for(index = 0; index < _countof(event_buffer_task_count_time); index++)    {        event_buffer_task_count_time[index].lock = UNLOCKED;        event_buffer_task_count_time[index].type = EVENT_MAX;        event_buffer_task_count_time[index].data = 0;    }    // 将自己的事件缓存注册到事件管理器    sys_event_buffer_set(task_count_time, event_buffer_task_count_time);}void task_count_time_init(void){    // ----------    // 硬件初始化    Drv_IO_mode_bit(DDRD, DDD0, IO_OUTPUT);    Drv_IO_clr_bit(PORTD, PD0);    // --------------    // 事件缓存初始化    task_count_time_event_buffer_init();}void task_count_time(void){    // 运行时刻标记、用来标记任务何时被调度    Drv_IO_toggle_bit(PORTD, PD0);    // ------------------------------------------    // 消息查询    // ------------------------------------------    // 任务正文    if(++cout >= 250)    {        cout = 0;        second++;        // --------------------------------------        // 组织事件、并通知事件管理模块        event_buffer_task_count_time[0].type = EVENT_IR_SEND;        event_buffer_task_count_time[0].data = second;        event_buffer_task_count_time[1].type = EVENT_SEG_UPDATE;        event_buffer_task_count_time[1].data = second;        sys_event_buffer_post(task_count_time, _countof(event_buffer_task_count_time));    }}

-------------------------------------------------------------------------------------------------------------------------------------

(5). 组织事件的参数(消息)

事件队列的结构:

1、在这个结构中,消息(message)是一个32bit的数,但事件(event)只有1个,这会带来一个问题。
     比如按键事件,事件是EVENT_KEY,那如何区分按键的状态呢(按下、松手、短按、长按、... )。
     显然这需要在32bit的消息里面去进一步组织。
     也就是说、 这些详细划分消息的工作都需要产生这些事件的任务自己去完成。
     因为每个任务如何划分消息、消息的意义,都需要任务自己定义。

2、下面以EVENT_SYS为例来进行组织。
EVENT_SYS的参数(消息)如下:
typedef enum {    MSG_SYS_TASK_DELAYED, // 有任务被延迟    MSG_SYS_EVENT_LOCKED, // 有事件被锁定    MSG_SYS_SLEEP,    MSG_SYS_WAKEUP,    MSG_SYS_IDLE,    MSG_SYS_START}MSG_EVENT_SYS;
使用如下结构来进一步组织消息(message)、以区分出EVENT_SYS的众多参数(消息):
// MSG_EVENT_SYS的结构(32bit)typedef struct {                               // 数据放在一个或半个字节里面、调试的时候以十六进制格式查看会更方便    uint8_t  event_index  : 8;  // bit[07:00]被锁定的事件号    uint8_t  task_index   : 8;  // bit[15:08]被延迟的任务号    uint8_t  event_locked : 1;  // bit[  :16]有事件被锁定    uint8_t  task_delayed : 1;  // bit[  :17]有任务被延迟    uint16_t reserved     : 10; // bit[27:18]用于将来扩展消息    uint8_t  sys_sleep    : 1;  // bit[  :28]系统休眠    uint8_t  sys_wakeup   : 1;  // bit[  :29]系统唤醒    uint8_t  sys_ldle     : 1;  // bit[  :30]系统空闲    uint8_t  sys_start    : 1;  // bit[  :31]系统开机}T_MSG_EVENT_SYS, *pT_MSG_EVENT_SYS;// MSG_EVENT_SYS的联合体结构、更适合在函数中进行操作typedef union{    T_MSG_EVENT_SYS msg;    uint32_t data;}U_MSG_EVENT_SYS;
这里使用位域将32bit的消息拆分成很多段,每一段对应MSG_EVENT_SYS里面的一个消息。

下面的函数将EVENT_SYS的众多参数(消息)写入事件队列:
// ==========================================================================================================// 更新系统产生的消息和警告// // 参数:type    需要更新的事件//       msg     需要更新的消息//       index   任务号、或事件号// // 说明:// (1). 使用读-修改-写的方式更新事件// (2). 使用了sys_event_post()来直接写入,并在读写之前强制解锁该事件// // ==========================================================================================================void sys_update_event(const uint8_t type, const uint32_t msg, const uint8_t index){    bool peek;    U_MSG_EVENT_SYS sys;    sys_event_unlock(type);    peek = sys_event_data(type, &sys.data);    if(TRUE == peek)    {        switch(msg)        {            // 有事件被锁定 ----------            case MSG_SYS_EVENT_LOCKED : sys.msg.event_locked = 1;                                        sys.msg.event_index  = index;                                        break;            // 有任务被延迟 ----------            case MSG_SYS_TASK_DELAYED : sys.msg.task_delayed = 1;                                        sys.msg.task_index   = index;                                        break;            // 系统状态 --------            case MSG_SYS_SLEEP  : sys.msg.sys_sleep  = 1;                                  break;            case MSG_SYS_WAKEUP : sys.msg.sys_wakeup = 1;                                  break;            case MSG_SYS_IDLE   : sys.msg.sys_ldle   = 1;                                  break;            case MSG_SYS_START  : sys.msg.sys_start  = 1;                                  break;            default : break;        }        sys_event_post(type, sys.data);    }}
这里使用sys_event_post(type, sys.data);来将消息直接写入事件队列,而没有建立事件缓存,
是因为EVENT_SYS不与其他任务共享,不存在访问冲突。

如果是按键事件EVENT_KEY,就需要建立事件缓存,同时使用相同的方法将事件写入缓存、并通知事件管理模块。
EVENT_KEY的参数(消息)组织方法类似:
// EVENT_KEY的参数(按键状态)typedef enum {    MSG_KEY_DOWN,    MSG_KEY_UP,    MSG_KEY_SHORT,    MSG_KEY_LONG,    MSG_KEY_HOLD,    MSG_KEY_DOUBLE,    MSG_KEY_TWO     // 两个按键同时按下、得到一个新的键值(同时按下的两个按键的键值也许需要清0)}MSG_EVENT_KEY;// MSG_EVENT_KEY的结构(32bit)(初值=0)typedef struct {                             // 数据放在一个或半个字节里面、调试的时候以十六进制格式查看会更方便    uint32_t key_index : 24;  // bit[23:00]具体键值(支持2^24个键值)    uint8_t  reserved   : 1;  // bit[  :24]将来作为第8种按键状态    uint8_t  key_two    : 1;  // bit[  :25]两个按键同时按下、得到一个新的键值(同时按下的两个按键的键值也许需要清0)    uint8_t  key_double : 1;  // bit[  :26]双击    uint8_t  key_hold   : 1;  // bit[  :27]保持    uint8_t  key_long   : 1;  // bit[  :28]长按    uint8_t  key_short  : 1;  // bit[  :29]短按    uint8_t  key_down   : 1;  // bit[  :30]按下    uint8_t  key_up     : 1;  // bit[  :31]松手}T_MSG_EVENT_KEY, *pT_MSG_EVENT_KEY;// MSG_EVENT_KEY的联合体结构、更适合在函数中进行操作typedef union{    T_MSG_EVENT_SYS msg;    uint32_t data;}U_MSG_EVENT_KEY;
使用位域可以避免大量定义MASK_EVENT_KEY_DOWN等常量,更方便编程、容易阅读。



-------------------------------------------------------------------------------------------------------------------------------------

(6). 事件队列和任务之间互相独立


      事件队列对于任务函数来说、就是一个公共的资源地,或者说是替任务管理数据、管理任务共享出来的那部分数据:



      事件队列类似一个水池,每个任务都可以从中取得消息,也可以将消息放入其中:


-------------------------------------------------------------------------------------------------------------------------------------

5、时间触发的任务调度

(1). 基本结构

      这个调度器的结构如下:
 
它和上面给出的后台CPU分时调度任务方式(编号4)类似,但又会循环遍历任务队列。

任务的特点:
1、我们为每个任务设置独立的调度周期
      (1). 每隔10个时刻调度1次红外发送任务(通常延迟10ms再启动数据发送并不会有什么副作用)
      (2). 每隔10个时刻调度1次按键扫描任务
      (3). 每隔  2个时刻调度1次数码管刷新任务
      (4). 每隔  1个时刻调度1次数值计算01任务
2、一个任务的调度周期到来、就表示任务处于就绪状态

调度器:
1、调度器将建立一个任务队列,将所有任务都注册进去。
2、调度模块处于后台CPU处、它将遍历任务队列,并执行队列中所有处于就绪状态的任务
3、事件管理模块将被嵌入到调度模块
0
任务队列的结构:
typedef struct {    uint8_t number;         // 任务号:该任务在任务队列中的位置    uint8_t co_op;          // 任务类型:1=合作式任务,0=抢占式任务    uint8_t run;            // 任务状态:(0)=准备中、(>0)=就绪、(>1)=任务曾经被延迟    uint8_t delay;          // 任务延时计数    uint8_t period;         // 任务运行间隔    p_void_funtion_void task;  // 任务函数}T_sys_task;T_sys_task sys_task_ctrl[SYS_TASK_MAX];  // 任务队列
1、在1ms定时中断中、任务延时delay会每隔1ms减1、减到0时表示调度周期到来
2、调度周期到来、就会设置状态标识run,表示任务处于就绪状态
3、然后重置delay = period,再次进入每隔1ms减1的循环
4、抢占式任务的调度周期到来时、将直接在1ms定时中断中执行、而不去等待调度

(2). 代码实现

1、这里使用《时间触发的嵌入式系统设计模式》里面提供的调度器来实现、并做部分改动。
sys_timer.h
#ifndef __SYS_TIMER_H__#define __SYS_TIMER_H__#include <stdint.h>#include <avr/interrupt.h>#include "Drv_IO_Port.h"#include "Drv_Sys.h"#include "Drv_Timer.h"#include "sys_event.h"#include "sys_warning.h"#include "config.h"#define SYS_TASK_MAX         4  // 最多支持10个任务#define SYS_TASK_RUN_MAX    10  // 1个任务的就绪状态的最大值typedef enum {    SYS_TASK_TYPE_PRE_EM = 0,   // 抢占式任务    SYS_TASK_TYPE_CO_OP  = 1    // 合作式任务}SYS_TASK_TYPE;void sys_task_init(void);void sys_task_start(void);void sys_task_dispatch(void);void sys_task_delete(const uint8_t index);uint8_t sys_task_add(const uint8_t delay, const uint8_t period, const p_void_funtion_void task,                      const uint8_t co_op, const p_void_funtion_void init);uint8_t sys_task_index(const p_void_funtion_void task);void delay_ms(const uint16_t count);#endif // #ifndef __SYS_TIMER_H__

sys_timer.c
#include "sys_timer.h"typedef struct {    uint8_t number;         // 任务号:该任务在任务队列中的位置    uint8_t co_op;          // 任务类型:1=合作式任务,0=抢占式任务    uint8_t run;            // 任务状态:(0)=准备中、(>0)=就绪、(>1)=任务曾经被延迟    uint8_t delay;          // 任务延时计数    uint8_t period;         // 任务运行间隔    p_void_funtion_void task;  // 任务函数}T_sys_task;T_sys_task sys_task_ctrl[SYS_TASK_MAX];// ==========================================================================================================//      系统任务调度定时器启动// // (1). 使用Timer0产生1ms的时标//      定时周期 T = ((1.0/8000000)*1000000)*64*(124+1) = 1000us = 1ms//      使用较小的OCR0=122、可以从PA1得到更精确的1ms,因为进入中断也是需要十几us//      这可以让从中断产生到进入中断函数为止的时间更精确为1.0ms// // (2). t = ((1.0 / 8000000)) * div * (N + 1)(单位:秒)//      f = 8000000 / (div * (N + 1))//      N = 8000000 / freq / div - 1// // ==========================================================================================================void sys_task_start(void){    uint16_t div;          // 16bit宽度的分频系数    uint16_t freq = 1000;  // 16bit宽度的频率:1000Hz    uint8_t  ocr0;    uint8_t  DIV  = T0_CLK_SOURCE_DIV_64;    // --------    // 计算参数    switch(DIV)    {        case T0_CLK_SOURCE_DIV_1:    div = 1;    break;        case T0_CLK_SOURCE_DIV_8:    div = 8;    break;        case T0_CLK_SOURCE_DIV_64:   div = 64;   break;        case T0_CLK_SOURCE_DIV_256:  div = 256;  break;        case T0_CLK_SOURCE_DIV_1024: div = 1024; break;        default: return;    }    ocr0 = SYS_OSC_FREQUENCE / freq / div - 1;  // 运算结果是8位的、但分步的中间结果可能是16位或32位的    ocr0 = ocr0 - 2;                            // 稍微减少一点OCR0、可以从PA1得到更精确的1ms,因为进入中断也需要十几us    // --------    // 设置参数    Drv_Timer0_init(T0_WGM_CTC, COM_MODE_NONE, DIV);    Drv_Timer0_set_TCNT0_OCR0(0, ocr0);    Drv_Timer0_INT_Enable(INT_MODE_OCF, ENABLE);}// ==========================================================================================================// 初始化系统任务调度模块// // ==========================================================================================================void sys_task_init(void){    uint8_t index = 0;    for(index = 0; index < SYS_TASK_MAX; index++)    {        sys_task_delete(index);    }}// ==========================================================================================================//      添加任务到任务队列// // 参数://      delay   任务延时计数//      period  任务运行间隔//      task    任务函数名//      co_op   任务类型:1=合作式任务,0=抢占式任务-(SYS_TASK_TYPE_CO_OP, SYS_TASK_TYPE_PRE_EM)//      init    任务的初始化函数-(任务不再放到sys_init()里面去初始化)// // 返回值://      index   任务号,>=SYS_TASK_MAX==SYS_TASK_MAX的任务号是无效的任务号// // ==========================================================================================================uint8_t sys_task_add(const uint8_t delay, const uint8_t period, const p_void_funtion_void task,                      const uint8_t co_op, const p_void_funtion_void init){    uint8_t index = 0;    for(index = 0; index < SYS_TASK_MAX; index++)    {        if(NULL == sys_task_ctrl[index].task) { break; }    }    if(index < SYS_TASK_MAX)    {        sys_task_ctrl[index].number = index;        sys_task_ctrl[index].co_op  = co_op;        sys_task_ctrl[index].run    = (delay == 0) ? 1      : 0;        sys_task_ctrl[index].delay  = (delay == 0) ? period : delay;        sys_task_ctrl[index].period = period;        sys_task_ctrl[index].task   = task;        // 必须在任务设置完毕后才初始化        // 因为初始化里面需要用到sys_task_index(),这涉及到sys_task_ctrl[index].task、sys_task_ctrl[index].number        init();    }    return index;}// ==========================================================================================================// 删除任务队列中的任务// // ==========================================================================================================void sys_task_delete(const uint8_t index){    if(index < SYS_TASK_MAX)    {        if(NULL != sys_task_ctrl[index].task)        {            sys_task_ctrl[index].number = SYS_TASK_MAX;            sys_task_ctrl[index].co_op  = SYS_TASK_TYPE_CO_OP;            sys_task_ctrl[index].run    = 0;            sys_task_ctrl[index].delay  = 0;            sys_task_ctrl[index].period = 0;            sys_task_ctrl[index].task   = NULL;        }    }}// ==========================================================================================================// 获取一个任务的任务号// // 参数://      task    任务函数名// // 返回值://      index   任务号,>=SYS_TASK_MAX的任务号是无效的任务号// // ==========================================================================================================uint8_t sys_task_index(const p_void_funtion_void task){    uint8_t index = SYS_TASK_MAX;    if(NULL != task)    {        for(index = 0; index < SYS_TASK_MAX; index++)        {            if(sys_task_ctrl[index].task == task)   // 指针可能需要转换为(uint16_t)来进行比较            {                index = sys_task_ctrl[index].number;                break;            }        }    }    return index;}// ==========================================================================================================//      任务调度函数// // 说明:// (1). 运行任务队列中 所有 已经就绪的任务// (2). .run > 1表示该任务之前有被延迟 >1个调度时刻// // (3). 新时刻到来会立即打断调度函数://      在运行过程中会被刷新任务状态的ISR(TIMER0_COMP_vect)打断,但由于任务是在每次时刻到来时被执行//      所以每次打断的时刻、都是刚才任务被执行的时刻结束、而新时刻到来的时刻//      此时、只要刚才的任务没超过或接近1ms,那么在这个时刻到来前、该任务早就执行完毕了//      所以、此时打断并不影响刚才的任务,也不会出现刚才的任务执行完毕后、被加载在其后面执行的情况// // (2). 单次任务://      将.period初始化为0,该函数就只会被执行1次、执行完毕后会立即被删除//      sys_task_add(0, n, X, Xi)在初始化结束X后,X会立即被执行、或者在下一个时刻被执行://      1、如果任务队列中、当前任务前面还有空余位置,任务X就会被放在当前任务之前,那么任务X会在下一个时刻被执行//      2、如果任务队列中、当前任务前面没有空余位置,任务X就会被放在当前任务之后,那么任务X会在当前任务所在时刻被执行//      由于任务X及其初始化函数Xi都会增加执行时间,所以必须考虑新任务不会导致当前任务超时//      一般://      -->只执行一次的任务、尽量放到启动任务队列之前去执行,不放到任务队列中//      -->只执行一次的任务、尽量做得短小,其初始化一般为空//      可以不用支持单次运行的任务,这里保留// // (3). 每个任务都需要在退出任务前、解锁自己锁定的事件等资源// // ==========================================================================================================void sys_task_dispatch(void){    uint8_t  index = 0;    uint8_t  lock;    bool event = FALSE;    for(index = 0; index < SYS_TASK_MAX; index++)    {        // --------        // 任务调度        if(sys_task_ctrl[index].run > 0)        {            sys_task_ctrl[index].task();            if(sys_task_ctrl[index].run > 1)            {                // 该任务曾经被延迟                sys_update_event(EVENT_SYS, MSG_SYS_TASK_DELAYED, index);            }            sys_task_ctrl[index].run = 0;            if(0 == sys_task_ctrl[index].period)            {                sys_task_delete(index);            }        }        // --------        // 事件管理        lock = sys_event_any_lock();        if(lock < EVENT_MAX)        {            // 有事件被锁定了            sys_update_event(EVENT_SYS, MSG_SYS_EVENT_LOCKED, lock);        }        event = sys_event_push();    }    // --------------------------------------------------------    // 所有任务都执行完毕、且所有事件都已写入事件队列后,进入休眠    if(TRUE == event)    {        task_sys_enter_sleep();    }}// ==========================================================================================================//      系统定时器(Timer0)中断(中断周期=1ms)// // (1). 刷新任务状态// (2). sys_task_add中将.delay初始化为0,意味着第1次进入 任务调度函数、就会执行这个任务// (3). 抢占式任务将直接在中断中执行,所以要求尽量短小// // ==========================================================================================================ISR(TIMER0_COMP_vect){    uint8_t index = 0;    for(index = 0; index < SYS_TASK_MAX; index++)    {        temp2016 = index;        if(NULL != sys_task_ctrl[index].task)        {            if(0 < sys_task_ctrl[index].delay)  // 任务延时计数            {                sys_task_ctrl[index].delay--;            }            if(0 == sys_task_ctrl[index].delay) // 任务就绪检查            {                if(SYS_TASK_TYPE_CO_OP == sys_task_ctrl[index].co_op)                {                    if(sys_task_ctrl[index].run < SYS_TASK_RUN_MAX)                    {                        sys_task_ctrl[index].run++;                    }                }                else                {                    sys_task_ctrl[index].task();                    if(0 == sys_task_ctrl[index].period)                    {                        sys_task_ctrl[index].co_op = SYS_TASK_TYPE_CO_OP;                        sys_task_ctrl[index].task  = NULL;                    }                }                // 单次运行的任务将会被删除、在被删除前不再重新计数                if(sys_task_ctrl[index].period > 0)                {                    sys_task_ctrl[index].delay = sys_task_ctrl[index].period;                }            }        }    }}


-------------------------------------------------------------------------------------------------------------------------------------

6、事件触发的任务调度

      引入事件管理之后、CPU可以根据事件来调度任务,而不是时间:

      由此可以设计一个事件触发的调度器
      消息CPU统一查询,任务不再查询消息、只发出自己产生的消息
      根据事件来调度任务的方式很适合以输出接口为主要内容、或以输入输出为主要内容的应用场合。

7、中断的上下半部机制










0
0 0
原创粉丝点击