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];

// 事件管理器
// (保存着每个任务独占的事件缓存的首地址,数组下标和任务
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值