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个资源: 中断、 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];
// 事件管理器
// (保存着每个任务独占的事件缓存的首地址,数组下标和任务队列的下标保持一致)
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-软件结构-前后台结构的主要内容,如果未能解决你的问题,请参考以下文章