A005-软件结构-前后台结构

Posted Manon_des_sources

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了A005-软件结构-前后台结构相关的知识,希望对你有一定的参考价值。

主要内容:
(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

以上是关于A005-软件结构-前后台结构的主要内容,如果未能解决你的问题,请参考以下文章

Java数据结构及算法实战系列005:渐近记法

Python3-笔记-B-005-数据结构-字符串str

计算机二级Java语言卷005

VSCode自定义代码片段5——HTML元素结构

VSCode自定义代码片段5——HTML元素结构

VSCode自定义代码片段5——HTML元素结构