主要内容:
(1). 前后台
(2). 事件管理
(3). 时间触发的调度器(分时复用)
(4). 事件触发的调度器(状态机)
(5). 中断的上下半部机制
-------------------------------------------------------------------------------------------------------------------------------------
开发环境:AVR Studio 4.19 + avr-toolchain-installer-3.4.1.1195-win32.win32.x86
芯片型号:ATmega16
芯片主频:8MHz
简单的前后台结构如上图所示。
前台以 中断为中心, 后台以 CPU为中心。
这里显示着程序涉及到的 3个资源: 中断、 RAM、 CPU,并隐含第 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、 中断中只收发数据,具体的数据处理放入后台任务,比如使用中断上下半部方式
-------------------------------------------------------------------------------------------------------------------------------------
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个任务的调度周期一到、就认为该任务已就绪):
-------------------------------------------------------------------------------------------------------------------------------------
CPU在这里什么都不用做、任务结束后去 休眠即可。
每次 中断到来时、 CPU被唤醒,将 中断函数执行完毕并返回之后, CPU再次进入 休眠。
很多应用中、这也是一个很好的方式,整个系统完全由 中断事件驱动, CPU平时处于静默。
而对于任务较多、功能较为繁重的情形,一般使用由 CPU调度任务的方式、以保持 中断的 轻巧简洁,
以应对较多的 中断事件,尤其是随机的 中断事件。
-------------------------------------------------------------------------------------------------------------------------------------
事件: 1ms定时任务 每隔 1ms产生一个 时刻值,我们可以视为是每隔 1ms产生一个事件: 1ms时刻到事件、或 时刻(时基)更新事件。
按键扫描任务在按键按下后产生 按键码,我们也将其视为是发生了1个事件: 按键按下事件、带一个 参数(按键号和按键类型)。
消息:本文将 事件(event)所带的 参数称为 消息(message),以区分 事件本身和事件的 参数。
事件/消息管理简称 事件管理。
在 简单的前后台结构里面、 事件和 消息都是作为 数据、直接使用 全局变量来存放的。
下面要将这些 数据统一放在一个 事件队列里面进行管理,不再分散到每个任务单独管理。
每个任务产生的 事件、都统一交给 事件队列存储管理,它们不再是任务私有的 数据。
事件管理下的 后台CPU分时调度任务方式:
比如、 数值计算01任务在执行后将向 事件队列发出2个 事件: 计算结果01事件( 参数=计算结果), 数码管刷新事件( 参数=计算结果)。
虽然这些 数据需要显示在 数码管上,但产生这些 数据的 数值计算01任务不去调用 数码管的 显示函数。
它并不负责这个、也不关心这些数据是否被 数码管正确地使用。
数码管刷新任务自己会到 事件队列里面去查询 数码管刷新事件的是否有效。
如果该事件的有效,它就将 数码管刷新事件对应的 参数取出来、送到 数码管显示,至于这个 数据由哪个任务产生,它并不关心。
也就是说、任务之间相互独立。
关于 事件管理在前后台中的应用、可以参考这篇文章 《消息机制在软件设计中的应用》。
图中事件队列的结构中包含了事件的三个信息:事件 类型(告诉我们这是什么事件)、事件的 参数、事件的 锁定状态。
事件(event)放入 type部分,事件的 消息(message)放入 data部分。
对应如下结构:
1、如果 约定所有 中断中都不访问 事件队列,就不需要 加锁。
此时, 中断做得比较小巧、只接收或发送数据,数据处理都在后台的某个任务中完成。
在某个任务访问 事件队列期间,打断它的 中断都不会访问 事件队列,因为不用担心数据会被修改。
2、如果允许 中断访问 事件队列,就需要 加锁。
3、如果 后台CPU的 调度方式里面、包含 软件中断,那么可能需要 加锁。
(1). 前后台
(2). 事件管理
(3). 时间触发的调度器(分时复用)
(4). 事件触发的调度器(状态机)
(5). 中断的上下半部机制
-------------------------------------------------------------------------------------------------------------------------------------
开发环境:AVR Studio 4.19 + avr-toolchain-installer-3.4.1.1195-win32.win32.x86
芯片型号:ATmega16
芯片主频:8MHz
-------------------------------------------------------------------------------------------------------------------------------------
本文将一步步地、将软件的结构、从简单前后台过渡到调度器。
-------------------------------------------------------------------------------------------------------------------------------------
1、 概述:
简单的前后台结构如上图所示。
前台以 中断为中心, 后台以 CPU为中心。
这里显示着程序涉及到的 3个资源: 中断、 RAM、 CPU,并隐含第 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];
// 事件管理器
// (保存着每个任务独占的事件缓存的首地址,数组下标和任务