前后台结构
-------------------------------------------------------------------------------------------------------------------------------------
开发环境:AVR Studio 4.19 + avr-toolchain-installer-3.4.1.1195-win32.win32.x86
芯片型号:ATmega16
芯片主频:8MHz
-------------------------------------------------------------------------------------------------------------------------------------
一、 概述
1、前后台
前台:用来处理输入输出,一般在中断中进行
后台:用来处理逻辑判断、任务计算等,一般交由CPU处理
事件:前后台之间使用事件互相通信,告知事件的发生和结束,这也称消息管理
事件/消息管理是前后台的核心。
关于事件/消息管理,参考这篇文章(消息机制在软件设计中的应用):http://www.xuebuyuan.com/1777265.html
-------------------------------------------------------------------------------------------------------------------------------------
2、前后台的运行过程
下面这个示例包含三部分:[按键输入,红外输入,数码管输出] + [事件管理] + [数值计算与存储]
在这个示例中、将使用定时器0的OCF0中断定时检测按键和刷新数码管、使用定时器1的ICP1中断接收红外信号。
如果中断中监测到按键1被按下、就将“事件_按键1按下”设置为1,表示这个事件发生,否则为0。
如果中断中收到红外码2、就将“事件_接收红外2”设置为1,表示这个事件发生,否则为0。
其余事件也是以同样的方式产生、所有事件都保存在事件队列中,供CPU实时查询。
这些事件并不直接调用对应的任务API,而只是设置消息标志。
到此、前台中断的任务结束,不再参与后续的操作。
在后台、CPU会循环监测消息队列,如果发现“事件_按键1按下”标志为1,就调度API"任务_显示001"去执行。
同时、如果后台CPU处理数值计算完毕,也不直接将数值送给前台的数码管显示,而是设置“事件_计算结束”标志为1,然后就此结束。
CPU监测到“事件_计算结束”标志为1时 ,才调度数码管的API"任务_更新显示"去执行,修改数码管显示的数据。
在中断中、数码管实时刷新后,就可以看到数值计算的结果了。
也就是说、前台的消息通过消息队列,将信息传递给CPU。
而CPU通过与前台任务关联的API来控制和传送数据给前台任务。
前后台之间互相隔离,当然也可以使用消息队列来联系前后台。
-------------------------------------------------------------------------------------------------------------------------------------
3、事件/消息机制的好处
这样的设计、将各个任务模块之间互相隔离,形成松散的联系,软件构成很清晰。
降低了系统的总体复杂度,也就是将系统拆分成了很多小模块,他们互相独立。
而互相独立的模块很容易被单独修改和替换、而又不会影响到其他模块。
-------------------------------------------------------------------------------------------------------------------------------------
二、 代码实现
说明:
1、下面将分几个步骤实现上面的示例:
第一步:前台定时中断调度模块,使用定时器0分时调度每个定时任务,包括数码管和按键扫描等,并给出操作任务的API。
第二步:事件/消息管理模块,作为前后台之间的通信模块。
第三步:前台实时任务,包括红外接收等,并给出操作任务的API。
第四步:后台数值计算任务。
第一步: 前台定时任务调度模块
说明:
1、这里将建立一个分时调度模块,用来分时调度3个定时任务
代码:
config.h中用到的一些定义:
#ifndef _countof #define _countof(_Array) (sizeof(_Array) / sizeof(_Array[0])) #endif #ifndef NULL #define NULL 0 #endif
sys.timer.c中的调度模块:
#include <avr/interrupt.h> #include "Drv_Timer.h" #include "sys_timer.h" typedef void(*p_task_funtion)(void); typedef struct { uint8_t delay; // 任务延时计数 uint8_t period; // 任务运行间隔 p_task_funtion task; // 任务函数 }T_sys_task, *pT_sys_task; // 这个数组用来注册和管理任务队列 static T_sys_task sys_task_ctrl[10]; static pT_sys_task p_sys_task_ctrl = sys_task_ctrl; // ========================================================================================================== // 初始化系统任务队列 // // ========================================================================================================== void sys_task_init(void) { uint8_t index = 0; for(index = 0; index < _countof(sys_task_ctrl); index++) { (p_sys_task_ctrl + index)->delay = 0; (p_sys_task_ctrl + index)->period = 0; (p_sys_task_ctrl + index)->task = NULL; } } // ========================================================================================================== // 添加任务到任务队列 // // ========================================================================================================== void sys_task_add(uint8_t delay, uint8_t period, p_task_funtion task) { uint8_t index = 0; for(index = 0; index < _countof(sys_task_ctrl); index++) { if(NULL == (p_sys_task_ctrl + index)->task) { break; } } if(index >= _countof(sys_task_ctrl)) { return; } (p_sys_task_ctrl + index)->delay = delay; (p_sys_task_ctrl + index)->period = period - 1; (p_sys_task_ctrl + index)->task = task; } // ========================================================================================================== // 系统任务定时器 // // (1). 使用Timer0的OCF0中断产生1ms的时标 // // ========================================================================================================== void sys_timer_init(void) { // 定时器0初始化:CTC模式、OC0引脚不连接、64预分频 Drv_Timer0_init(T0_WGM_CTC, COM_MODE_NONE, T0_CLK_SOURCE_CLK_64); // 设置初值:TCNT0=0、OCR0=122 Drv_Timer0_set_TCNT0_OCR0(0, 122); // 使能OCF0中断 Drv_Timer0_INT_Enable(INT_MODE_OCF, ENABLE); } // ========================================================================================================== // 系统定时器中断(中断周期=1ms) // // (1). 使用Timer0的CTC中断调度各个任务 // (2). sys_task_add中将delay初始化为0,意味着第1次进入这个中断、就会执行这个任务 // // ========================================================================================================== volatile uint8_t temp2016; // 调试用 ISR(TIMER0_COMP_vect) { uint8_t index = 0; for(index = 0; index < _countof(sys_task_ctrl); index++) { temp2016 = index; if(NULL != (p_sys_task_ctrl + index)->task) { if(0 == (p_sys_task_ctrl + index)->delay) { (p_sys_task_ctrl + index)->delay = (p_sys_task_ctrl + index)->period; (p_sys_task_ctrl + index)->task(); } else { (p_sys_task_ctrl + index)->delay--; } } } }
Mod_LED_display.c中的数码管任务:
// ========================================================================================================== // LED数码管显示数据的刷新 // // (1). 在系统定时器中定时刷新 // // ========================================================================================================== void Mod_LED_display_update(void) { PORTD ^= (1 << PD0); // 运行时刻标记 // 熄灭当前数码管、并保持3个时钟周期的熄灭,用来避免余晖 *p_LED_display_ctrl->seg_code = segment_code[_countof(segment_code) - 1]; // 切换到下1个数码管 p_LED_display_ctrl->index++; if(p_LED_display_ctrl->index > (_countof(segment_index) - 1)) { p_LED_display_ctrl->index = 0; } // 修改位选、修改显示 *p_LED_display_ctrl->seg_index |= segment_index[_countof(segment_index) - 1]; *p_LED_display_ctrl->seg_index &= segment_index[p_LED_display_ctrl->index]; *p_LED_display_ctrl->seg_code = segment_code[p_LED_display_ctrl->data[segment_index[p_LED_display_ctrl->index]]]; }
main.c中初始化3个任务:
// ========================================================================================================== // 主函数 // ========================================================================================================== #include <avr/io.h> #include "Mod_LED_Displayer.h" #include "sys_timer.h" #include "system.h" #include "config.h" void Mod_test01(void) { PORTD ^= (1 << PD1); // 运行时刻标记 } void Mod_test02(void) { PORTD ^= (1 << PD2); // 运行时刻标记 } // ========================================================================================================== // main函数 // ========================================================================================================== int main(void) { // ------------------------------------------------------------------------------------------------------ // 关全局中断 cli(); // 系统初始化 sys_init(); // 开全局中断 sei(); // PD[2:0]初始化为输出0 DDRD |= (1 << DDD0) | (1 << DDD1) | (1 << DDD2); PORTD &= ~((1 << PD0 ) | (1 << PD1 ) | (1 << PD2 )); // 注册3个任务 sys_task_add(0, 3, Mod_LED_display_update); // 从时刻0开始运行,以后每隔3个时刻运行一次(1个时刻是1ms) sys_task_add(1, 3, Mod_test01); // 从时刻1开始运行,... sys_task_add(2, 3, Mod_test02); // 从时刻2开始运行,... // CPU数值计算 Mod_LED_display(123456789); // ------------------------------------------------------------------------------------------------------ while(1) { } return 0; }
运行结果:
1、任务1:数码管显示23456789、同时PD0输出脉冲,周期为3ms
任务2:PD1引脚每隔3ms翻转一次电平
任务3:PD2引脚每隔3ms翻转一次电平
2、3个任务都是每隔3ms被调度一次,但他们之间互相间隔1ms被调度:
任务1的运行时刻:0, 3, 6, 9, ...
任务2的运行时刻:1, 4, 7, 10, ...
任务3的运行时刻:2, 5, 8, 11, ...
3个任务的调度时刻如下图所示:
也就是说、同一个ms内,只有1个任务被调度运行,这可以保证1ms只有很少的时间在运行前台任务,剩余的时间交给CPU去做后台任务。
示波器输出如下:
CH1是PD0的输出,CH2是PD1的输出,PD1滞后PD0 1ms。
这表示任务2滞后任务1 1ms后被调度,周期为3ms,说明2个任务都是每隔3ms被调度一次。
CH1是PD0的输出,CH2是PD2的输出,PD2滞后PD0 2ms。
这表示任务3滞后任务1 2ms后被调度,也就是说、任务3滞后任务2 1ms后被调度。
任务调度的周期和预期的一致、为3ms。
这里实现了定时调度任务,支持10个任务,可以设定每个任务被调度运行的时刻。
两个或多个任务可以在同一时刻被调度,也可以相隔几个时刻被调度。
对于比较耗时的任务,应该单独放在1个时刻内去调度。
测试任务耗时
下面使用PD0粗略测量了数码管刷新任务消耗的时间
代码:
Mod_LED_display.c中的数码管任务中修改PD0如下:
// ========================================================================================================== // LED数码管显示数据的刷新 // // (1). 在系统定时器中定时刷新 // // ========================================================================================================== void Mod_LED_display_update(void) { PORTD |= (1 << PD0); // 运行时刻标记 // 熄灭当前数码管、并保持3个时钟周期的熄灭,用来避免余晖 *p_LED_display_ctrl->seg_code = segment_code[_countof(segment_code) - 1]; // 切换到下1个数码管 p_LED_display_ctrl->index++; if(p_LED_display_ctrl->index > (_countof(segment_index) - 1)) { p_LED_display_ctrl->index = 0; } // 修改位选、修改显示 *p_LED_display_ctrl->seg_index |= segment_index[_countof(segment_index) - 1]; *p_LED_display_ctrl->seg_index &= segment_index[p_LED_display_ctrl->index]; *p_LED_display_ctrl->seg_code = segment_code[p_LED_display_ctrl->data[segment_index[p_LED_display_ctrl->index]]]; PORTD &= ~(1 << PD0); // 运行时刻标记 }
任务1开始时PD0为高电平,任务1结束后PD0为低电平,也就是说、PD0的高电平时间就是任务1消耗的时间。
这个时间不包含进入和退出任务函数的时间,所以真实时间会稍长一点,但不会差太多。
示波器输出如下:
高电平时间为8.12us,在8MHz下每条指令是0.125us,所以这里大概是执行了66条指令。
同时、8.12us相对于1ms来说,是极其短暂的,只占到8.2%的时间,所以还有91.8%的空闲时间可以交给CPU去处理后台任务。
-------------------------------------------------------------------------------------------------------------------------------------
第二步: 消息管理模块
-------------------------------------------------------------------------------------------------------------------------------------
第三步: 前台实时任务 (红外接收)
-------------------------------------------------------------------------------------------------------------------------------------
第四步: 后台数值计算任务
0
0
000