当前位置 : 主页 > 编程语言 > 其它开发 >

时间触发嵌入式系统设计——调度器

来源:互联网 收集:自由互联 发布时间:2022-05-22
一、超级循环 许多简单的嵌入式系统所使用的软件结构都是一种超级循环的形式,如下源程序清单所示: 1 #include " x.h " 2 3 void main ( void ) 4 { 5 X_Init(); // 准备任务X 6 while ( 1 ) // “死循环

一、超级循环

许多简单的嵌入式系统所使用的软件结构都是一种超级循环的形式,如下源程序清单所示:

 1 #include "x.h"
 2 
 3 void main (void)
 4 {
 5     X_Init(); //准备任务X
 6     while(1)  //“死循环,也叫超级循环”
 7     {
 8         x(); //执行任务
 9     }
10 }

 

超级循环结构的主要优点是:

1、简单,因此易于理解;

2、几乎不占用系统存储器或CPU资源。

超级循环占用很少的存储器和处理器资源是因为它们几乎不为开发人员提供什么功能。

尤其是这种结构,它很难在精确的时间间隔执行任务X。这种限制是一个非常大的缺点。

例如:考虑从一系列不同的嵌入式项目汇集的许多要求(没有特别的前后顺序):

  • 必须以0.5s间隔测量汽车的当前速度。
  • 每秒必须刷新显示40次。
  • 计算出来的新的油门位置必须每隔0.5s输出。
  • 必须每秒执行20次时间-频率变换。
  • 如果已经发出警报,则必须在20分钟之后关掉(法律上的要求)。
  • 当前门被打开时,如果在30s内没有输入正确的口令,则必须发出警报。
  • 必须每秒釆样1 000次发动机振动数据。
  • 必须每秒执行20次频域数据分类。
  • 必须每200ms扫描一次键盘。
  • 主机(控制)节点必须每秒与所有其他节点(传感器节点和发出警报节点)通信一次。
  • 必须每0.5s计算一次新的油门位置。
  • 传感器必须每秒釆样一次。

总结这个列表可以发现,许多嵌入式系统必须在某些时刻执行任务。更具体地说,需要执行的任务分为两种:

  • 周期性任务,(比方说)每100ms执行一次
  • 单次任务,(比方说)在50ms的延迟之后执行一次

利用超级源程序清单所示的基本结构很难实现上述任务。例如,假设必须每隔200ms起 动任务X,而完成该任务需要10ms。

如果一定要用超级循环来实现这个要求,可以用以下代买来实现:

1 void main (void)
2 {
3     Init_System(); 
4     while(1)  //“死循环,也叫超级循环”
5     {
6         x(); //执行任务,耗时10ms
7         Delay_190ms(); //延迟190ms
8     }
9 }

 

上述源程序清单中说明的方法通常是难以实际运用的,因为只有当满足以下条件时它才能工作:

1、知道任务X的精确的运行时间

2、这个运行时间永不变化

在实际系统中,很难确定任务的精确运行时间。假设有一个非常简单的任务,不与外界相互作用,而仅仅执行某些内部计算。

即使在这种相当有限的情况下,改变编译程序优化设置, 或者即使是改变一些表面上不相关的程序部分,也可能改变任务运行的速度。

这使得调节定时的过程非常乏味并且易于出错。

第二种条件更是问题多多。在嵌入式系统中,任务往往需要以复杂的方式与外界相互作用。

在这种情况下,任务的运行时间将随着外界行为的变化而变化,而程序员极难控制这种变化。

 

二、更好的解决方案,定时器中断

解决这个问题的更好方案是使用基于定时器的中断,在一定的时刻调用函数。

基于定时器的中断和中断服务程序

中断是一种用来当发生“事件”时通知处理器的硬件机制。这种事件可能是内部事件或者外部事件。8051/8052内核结构共支持7个中断源:

  • 三个定时/计数器中断[分别与定时器0、定时器1和定时器2 ]
  • 两个有关UART的中断(注意:它们共用同一个中断向量,可以看作是一个中断源)
  • 两个外部中断

此外,另有一个程序员极少控制的中断源:

  • “上电复位”(POR)中断

当中断产生时,处理器“跳转"到程序存储器底部的某个地址。这些地址必须包含微控制器对中断做出响应的相应代码。

通常,这里将包含另一个“跳转”指令,跳到位于程序存储器其他地方相应的“中断服务程序”地址。

虽然处理中断的这个过程看起来有点复杂,然而使用高级语言来创建中断服务程序(ISR) 的过程是简单的,如以下代码所示:

 1 #include <AT89x52.h>
 2 #define INTERRUPT_Timer_2_Overflow 5
 3 
 4 void Timer_2_Init(void)
 5 
 6 void main(void)
 7 {
 8     Timer_2_Init(); //设置定时器2
 9     EA = 1; //允许所有中断
10     while(1); //一个空的超级循环
11 }
12 
13 
14 void Timer_2_Init(void)
15 {
16     T2CON     = 0x04;
17     T2MOD     = 0x00;
18     TH2       = 0xFC;
19     RCAP2H    = 0xFC;
20     TL2       = 0x18;
21     RCAP2H  = 0x18;
22     ET2     = 1;
23     TR2     = 1; //启动定时器2运行
24 }
25 
26 void x(void) interrupt INTERRUPT_Timer_2_Overflow
27 {
28     //每隔1ms调用这个中断服务程序
29     //所需的代码放在这里
30 }

 

三、在不同的时间间隔执行多个任务

虽然绝大多数嵌入式系统只要求运行一个程序,但是确实有必要支持多个任务的运行。这些任务必须以周期性或单次的方式运行,

一般具有不同的运行时间并以不同的时间间隔运行。例如,可能需要每隔1ms从模数转换器 读取输入,每隔200ms读取一个或多个开关状态,以及每隔3ms刷新一次LCD显示。

通过多个定时器中断,就可以运行多个任务。例如,假设微控制器有三个定时器可用。通过使用独立的中断服务程序来执行每个任务,可以使用这些定时器来控制三个任务的运行。

 1 #include <AT89x52.h>
 2 #define TNTERRUPT_Timer_0_Overf1ow 1
 3 #define INTERRUPT_Timer_l_Overflow 3
 4 #define INTERRUPT_Timer_2_Overflow 5
 5 
 6 //函数原型
 7 //注意,中断服务程序不被直接调用,因此不需要原型
 8 void Timer_O_Init(void);
 9 void Timer_l_Init(void);
10 void Timer_2_Init(void);
11 
12 void main(void)
13 {
14     Timer_O_Init () ;    // 设置定时器 0
15     Timer_l_Init () ;    // 设置定时器 1
16     Timer_2_Init () ;    // 设置定时器 2
17     EA = 1//允许所有中断
18     while(1);
19 }
20 
21 void Timer_O_Init(void)
22 {
23     //详略
24 }
25 
26 void Timer_l_Init (void)
27 {
28     //详略
29 }
30 
31 void Timer_2_Init(void)
32 {
33     //详略
34 }
35 
36 void X(void) interrupt INTERRUPT_Timer_0_Overflow
37 {
38     //每隔1ms调用这个中断服务程序一次
39     //详细代码在此省略
40 }
41 
42 void Y(void) interrupt INTERRUPT_Timer_l_Overflow
43 { 
44     //每隔2ms调用这个中断服务程序一次
45     //详细代码在此省略
46 )
47 
48 void Z(void) interrupt INTERRUPT_Timer_2_0verflow
49 { 
50     //每隔5ms调用这个中断服务程序一次
51     //所需的代码放在这里...
52 } 

 

通常只要有足够的定时器可用,这种方法就将有效。然而,这种方法违反了基本的软件设计准则。

有三个不同的定时器需要管理(而如果有100个任务,将需要100个定时器)。这就使系统的维护变得非常困难,

例如,如果改变振荡器频率,将需要在100处做相应改动而且也很难扩展,例如,如果没有更多的硬件定时器可用,如何再添加一个任务?

除了违反最基本的软件设计准则外,源程序清单的程序有一个更具体的问题。这个问题出现在当多个中断同时产生的情况下。

系统有多于一个有效的中断将可能导致不可预知的运行结果,由此将造成性能上的不可靠。

再回头看源程序清单,同时产生多个中断的情况是不可避免的。处理这种情况是有可能的,但是将极大地增加系统的复杂性,

总的说来,正如将在下一节看到的,使用调度器将提供一个非常完美的解决方案。

 

一、什么是调度器

可以从两种角度来看调度器:

  1、调度器可以看作是一个简单的操作系统,允许以周期性或(更少见)单次方式来调用任务。

  2、从底层的角度来看,调度器可以看作是一个由许多不同任务共享的定时器中断服务程序。

因此,只需要初始化一个定时器,而且改变定时的时候通常只需要改变一个函数。此外,无论需要运行1个、10个还是100个不同的任务,

通常都可以使用同一个调度器完成。注意,这种“共用中断服务程序”与桌面操作系统提供的共用打印功能非常类似。

以下源程序清单展示了如何使用调度器来调度程序中的3个任务。

 1 void main(void)
 2 {
 3     //设置调度器1次
 4     SCH_Init();
 5     
 6     //增加任务(1ms时标间隔)
 7     SCH_Add_Task(Function_A, 0, 2); //Function_A将每隔2ms运行一次
 8     SCH_Add_Task(Function_B, 1, 10); //Function_B将每隔10ms运行一次
 9     SCH_Add_Task(Function_C, 3, 15);//Function_C将每隔15ms运行一次
10     SCH_Start();
11     while(1)
12     {
13         SCH_Dispatch_Tasks();
14     }
15 }

 

合作式调度器

合作式调度器提供了一种单任务的系统结构

  操作:

    任务在特定的时刻被调度运行(以周期性或单次方式)

    当任务需要运行时,被添加到等待队列

    当CPU空闲时,运行等待任务中的下一个(如果有的话)

    任务运行直到完成,然后由调度器来控制

  实现:

    这种调度器很简单,用少量代码即可实现

    该调度器必须一次只为一个任务分配存储器

    该调度器通常完全由高级语言(比如“C” )实现

    该调度器不是一种独立的系统,它是开发人员的代码的一部分

  性能:

    设计阶段需要小心以快速响应外部事件

  可靠性和安全性:

    合作式调度简单、可预测、可靠并且安全

 

合作式调度器不但可靠而且可预测的主要原因是在任一时刻只有一个任务是活动的。

这个任务运行直到完成,然后由调度器来控制。与此相对,在完全的抢占式系统的情况下,有多个活动任务。

在这样的系统中,假设有一个任务正在从端口读取时,调度器执行了“上下文切换”, 使另一个任务访问同一个端口。

在这种情况下,如果不釆取措施阻止这种操作,数据将可能丢失或被破坏。

 

合作式调度器提供了一种简单而可预测性非常高的平台。该调度器全部用“C”编写而且 成为系统的一部分。

这将使整个系统的运行更加清晰且易于开发、维护,以及向不同的平台上 移植。存储器的开销为每个任务7个字节,对CPU的要求(随时标间隔而变)很低。

 

函数指针

许多C程序员不熟悉函数指针。相对来说,函数指针很少用于桌面程序,然而它却是创建调度器的关键。因此将在这里提供一个介绍性的简短例子。

需注意的要点是:例如,正如能够确定一组数据在存储器中的起始地址,也可以在存储器 中找到特定函数的可执行程序代码的起始地址。

这个地址用于“指向”该函数,最重要的是, 它可用于调用该函数。只要小心使用,函数指针能够使复杂的程序更易于设计和实现。

例如,假设正在开发一个 大规模的、安全至上的系统来控制一个工厂。一旦检测到紧急的情况,将希望尽可能快速地关 闭系统。

然而,关闭系统的合理方式随系统的状态而变化。因此,将建立多种恢复函数和一个 函数指针。每当系统状态改变时,就改变函数指针使它总是指向最合理的恢复函数。

这样就可 以保证一旦出现紧急情况,便能够通过函数指针快速地调用最合理的函数。

解决方案

调度器有以下主要组成部分:

•   调度器数据结构。

•   初始化函数。

•   中断服务程序(ISR),用来以一定的间隔刷新调度器。

•   向调度器增加任务的函数。

•   使任务在应当运行的时候被执行的调度函数。

•.   从调度器删除任务的函数(并不是所有系统都需要)。

在本节中将讨论这些所需的模块。

概述

在讨论调度器的模块之前,先讨论一下在用户看来什么是调度器。用一个简单的例子来说明:一个用来重复闪烁LED的调度器,一秒亮,一秒灭,如此循环。

 

 1 void main (void)
 2 {
 3     //设置调度器
 4     SCH_Init_T2 ();
 5     
 6     //为”Flash_LED"任务作准备
 7     LED_Flash_Init ();
 8     
 9     // 增加"Flash LED”任务(1000ms 亮,1000ms 灭)
10     //定时单位为时标(1ms时标间隔)
11     // (最大的间隔/延迟是65535个时标)
12     SCH_Add_Task(LED_Flash_Update, 0, 1000);
13     
14     //开始调度器
15     SCH_Start ( ) ; //刷新任务队列
16     while(1)
17     {
18         SCH_Disptch_Tasks();
19     }
20 }
21 
22 void SCH_Update(void) interrupt INTERRUPT_Timer_2_Overflow
23 { 
24     //刷新任务队列
25 }

 

 

源程序如下运行:

1、假定LED将通过LED_Flash_Update()任务被点亮和熄灭。这样,如果LED最初是熄灭的,则调用LED_Flash_Update()两次,LED将被点亮然后再次熄灭。

因此,为了获得需要的闪烁频率,要求调度器每秒调用LED_Flash_Update()一次,且无限循环。

2、使用函数SCH_Init_T2()来准备调度器。

3、调度器准备好后,使用函数SCH_Add_Task()将函数LED_Flash_Update()添加到调度任务队列中。同时,以如下方式指定LED以需要的频率闪烁:

    // 增加"Flash LED”任务(1000ms 亮,1000ms 灭)
    //定时单位为时标(1ms时标间隔)
    // (最大的间隔/延迟是65535个时标)
    SCH_Add_Task(LED_Flash_Update, 0, 1000);

(随后将讨论SCH_Add_Task()的所有参数,并研究它的内部结构)。

4、函数 LED_Flash_Update()的定时将由函数 SCH_Update()控制,SCH_Update()是一个由定时器2溢出触发的中断服务程序:

1 22 void SCH_Update(void) interrupt INTERRUPT_Timer_2_Overflow
2 23 { 
3 24     //刷新任务队列
4 25 }

 

5、“刷新”中断服务程序不运行任务,而是计算任务应该在什么时候运行并设置标志。

运行LED_Flash_Update()的任务由调度函数SCH_Dispatch_Tasks()完成,这个函数在主(超级)循环中运行。

 

1     while(1)
2     {
3         SCH_Disptch_Tasks();
4     }    

 

 

在详细讨论这些模块之前,应该承认对于闪烁LED来说,这是一种复杂的实现方式。

如果目标是开发一个需要最少存储器以及最短代码长度的闪烁LED应用,那么这并不是一种好的解决方案。

然而,我们的主要目的是在后面所有的例子中都将使用同样的调度器结构。

这些系统将包括许多有实际价值的、复杂的系统。为理解这种平台的运行方式所付出的努力将很快得到回报。

还需要强调的是调度器是一种“低成本的”方案,它占用很小比例的CPU资源(将随后介绍精确的百分比)。

此外,就调度器本身而言,每个任务只要求不超过7个字节的存储器。

因为在一个典型的系统中不会超过4〜6个任务,即使运行在8位微控制器上,所需的任务预算(大约40个字节)也是不多的。

调度器数据结构以及任务队列

调度器的核心是调度器数据结构。这是一种用户自定义的数据类型,集中了每个任务所需的信息。

 1 typedef data struct
 2 {    
 3     void (code * pTask)(void); //指向任务的指针,必须是一个void(void)函数
 4     
 5     tWord  Delay;//延迟直到函数将(下一次)运行
 6     
 7     tWord  Period; //连续运行之间的间隔
 8     
 9     tByte  RunMe; //当任务需要运行时(由调度器)+1
10     
11 } sTask;

 

在文件Sch51.C中,数据类型sTask和常数SCH_MAX_TASKS 一起用来创建任务队列, 并一直被调度器所引用:

//任务队列

sTask SCH_tasks_G[SCH_MAX_TASKS];

任务队列的大小

必须通过调整SCH_MAX_TASKS的值来保证足够长的任务队列,以保存系统所需的 任务。例如,如果需要调度如下的三个任务:

SCH_Add_Task(Function_A, 0, 2);

SCH_Add_Task(Function_B, 1, 10);

SCH_Add_Task(Function__C, 3, 15);

那么SCH_MAX_TASKS必须为3 (或更大),以保证调度器的正常运行。 同时注意,如果不满足这个条件,调度器将产生一个错误代码。

 

初始化函数

  如同大多数需要被调度的任务一样,调度器本身也需要一个初始化函数。

  虽然该函数执行各种重要的操作,诸如准备调度器队列(在前面讨论的)以及准备错误代码变量(将在后面讨论),然而这个函数的主要用途是设置定时器,用来产生驱动调度器的定期“时标”。

  大多数8051芯片都有三个定时器(定时器0、定时器1,以及定 时器2),它们中的任何一个都能用来驱动调度器。然而,只有定时器2可以用作自动重装的16位精度定时器。

  因此,如果可能的话,使用该定时器是合理的。

 

使用定时器2的一个初始化函数的例子在源程序清单14.4中给出:

 

SCH_Init_T2()

调度器初始化函数。准备调度器数据结构并且设置定时器以所需的频率中断。

必须在使用调度器之前调用这个函数

 

void SCH_Init_T2(void)
{
    tByte i;
    for(i=0; i< SCH_MAX_TASKS; i++)
    {
        SCH_Delete_Task(i);
    }
    
    //复位全局错误变量
    //SCH_Delete_Task()将产生一个错误代码,因为任务队列是空的
    Error_code_G = 0;
    
    //现在设置定时器2
    //自动重装、16位定时器功能
    //晶振假定为12MHz
    //定时器2的精度是1us
    //要求的定时器2溢出为1ms
    //需要1000个定时器时标
    //重装值为65536-1000 = 64536 = 0xFC18
    T2CON = 0x04; //加载定时器2的控制寄存器
    T2MOD = 0x00; //加载定时器2的模式寄存器
    TH2   = 0xFC; //加载定时器2的高位字节
    RCAP2H= 0XFC; //加载定时器2的重装捕捉寄存器的高位字节
    TL2   = 0x18; //加载定时器2的地位字节
    RCAP2L= 0x18; //加载定时器2的重装捕捉寄存器的低位字节
    ET2   = 1//使能定时器2中断
    TR2   = 1//启动定时器2
    
}

 

 

  当使用本书中的任何一个调度器时,通常必须修改初始化代码来满足需要。尤其必须保证:

    1、始化函数中假定的振荡器/谐振器频率与硬件相符。

    2、调度器的时标间隔满足需要。在上述源程序清单中,时标间隔为1ms。

  下面的“可靠性和安全性”中将提供有关选择时标间隔的指导。

  “每个微控制器一个中断”的原则

  调度器的初始化函数将使能和微控制器某个定时器溢出有关的中断。

  因为在第1章中讨论的理由,本书始终假定只有“时标”中断源是活动的。具体地说, 假定没有别的中断被使能。

  如果在允许有其他的中断时试图使用调度器代码,那么系统根本不能保证运行正常。 通常,充其量也只不过是得到完全不可预知的而且很不可靠的系统行为。

 

“刷新”函数

  “刷新”函数是调度器的中断服务程序。它由定时器的溢出激活(正如在前面讨论的,使用“初始化”函数来设置),和大多数调度器类似,刷新函数并不复杂。

  当刷新函数确定某个任务需要运行时,将这个任务的RunMe标志加1 ,然后该任务将由调度程序执行,正如在后面讨论的。

 

 1 void SCH_Update() interrupt INTERRUPT_Timer_2_0verflow
 2 {
 3     tByte Index;
 4     TF2 = 0; //必须手工清零
 5     for(Index = 0; Index < SCH_MAX_TASKS; Index++)
 6     {
 7         //检查这里是否有任务
 8         if(SCH_tasks_G[Index].pTask)
 9         {
10             if(SCH_tasks_G[Index].Delay == 0)
11             {
12                 //任务需要运行
13                 SCH_tasks_G[Index].RunMe +=1;//RunMe标志加1
14                 if(SCH_tasks_G[Index].Period)
15                 {
16                     //调度周期性的任务再次运行
17                     SCH_tasks_G[Index].Delay = SCH_tasks_G[Index].Period;
18                 }
19             } else{
20                 //还没准备好运行,延迟-1
21                 SCH_tasks_G[Index].Delay -= 1;
22             }
23         }
24     }
25 } 

 

 

 

“添加任务”函数

 

  正如其名称所暗示的,“添加任务”函数用来添加任务到任务队列上,以保证它们在需要的时候被调用。

 

  “添加任务”函数的参数在图14.2中进行了说明。

 

  这里是一些例子。

 

  这组参数使函数Do_X()在1000个调度器时标后运行一次:

 

    SCH_Add_Task(Do_X,1000,0);

 

  这组参数的作用相同,但是将任务标识符(在任务队列中的位置)保存以便以后在必要时删除该任务(关于从任务队列删除任务的更详尽的资料参见SCH_Delete_Task()):

 

    Task_ID = SCH_Add_Task(Do_X,1000, 0);

 

  这组参数使函数Do_X()每隔1000个调度器时标周期性地运行一次。一旦调度开始,该任务就开始运行:

 

    SCH_Add_Task(Do_X, 0, 1000);

 

  这组参数使函数Do_X()每隔1000个调度器时标周期性地运行一次。任务将首;先在T = 300 个时标时执行,然后在1300个时标、2300个时标等等执行:

    SCH_Add_Task(Do_X, 300, 1000);

 

SCH_Add_Task(Task_Name, Initial_Delay, Period);

Task_Name:需要调度的函数(任务)的名称。

Initial_Delay:任务第一次执行前的延迟,如果设置为0,任务将立即执行。

Period:任务重复运行的(时标)间隔。如果设置为0,则任务将只执行一次。

 

 1 tByte SCH_Add_Task(void (code * pFunction)(),
 2                    const tWord DELAY,
 3                    const tWord PERIOD)
 4 {
 5     tByte Index = 0;
 6     
 7     //首先在队列中找到一个空隙
 8     while((SCH_tasks_G[Index].pTask !=0)&& (Index < SCH_MAX_TASKS))
 9     {
10         Index++;
11     }
12     
13     //是否已经到达队列的结尾
14     if(Index == SCH_MAX_TASKS)
15     {
16         //任务队列已满
17         //设置全局错误变量
18         Error_code_G = ERROR_SCH_TOO_MANY_TASKS;
19         
20         //同时返回错误代码
21         return Error_code_G;
22     }
23     
24     //如果能运行到这里,则说明任务队列中有空间
25     SCH_tasks_G[Index].pTask = pFunction;
26     SCH_tasks_G[Index].Delay = DELAY;
27     SCH_tasks_G[Index].pTask = PERIOD;
28     
29     SCH_tasks_G[Index].RunMe = 0;
30     
31     return Index; //返回任务的位置(以便以后删除)
32 }

 

 

 

 

“调度”程序函数

 

正如已经看到的,“刷新”函数不执行任何函数任务,需要运行的任务由“调度程序”函数 激活。

 

 

 1 void SCH_Disptch_Tasks(void)
 2 {
 3     tByte Index;
 4     
 5     //调度(运行)下一个任务(如果有任务就绪)
 6     for(Index = 0; Index < SCH_MAX_TASKS; Index++)
 7     {
 8         if(SCH_tasks_G[Index].RunMe > 0)
 9         {
10             (*SCH_tasks_G[Index].pTask); //执行任务
11             
12             //复位或者减少RunMe标志
13             //周期性的任务将自动地再次执行
14             SCH_tasks_G[Index].RunMe -= 1; 
15             
16             if(SCH_tasks_G[Index].Period == 0)
17             {
18                 SCH_Delete_Task(Index);
19             }
20         }
21     }
22     
23     //报告系统状况
24     SCH_Report_Status();
25     
26     //这里调度器进入空闲模式
27     SCH_Go_To_Sleep();
28 }

 

 

调度程序是超级循环中的唯一模块

1 while(1)
2 {
3     SCH_Disptch_Tasks();
4 }

 

是否需要一个调度函数?

  乍看起来,即使用“刷新”函数又使用“调度”函数似乎是一种相当复杂的任务执行方式。

  具体地说,看起来调度函数也许是不必要的,而刷新函数能够直接激活任务。

  然而,为了在长任务的情况下使调度器的可靠性最大化,分离刷新和调度操作是必要的。

  假设有一个1ms时标间隔的调度器,无论出于什么原因,有时待调度的任务具有3ms的运行时间。

  如果刷新函数直接运行函数,长任务将一直运行,时标中断将被禁止。

  具体地说,将漏掉 两个“时标”。这意味着所有的系统定时都将受到严重影响,并且可能有两个(或更多个)任务不能被调度执行。

  如果将刷新和调度函数分开,则当长任务运行的时候系统时标仍然能够被处理。这意味着虽然发生任务“抖动”(漏掉的任务不能在正确的时间运行),但这些任务最终将运行。

 

 

“删除任务”函数

当任务被添加到任务队列时,SCH_Add_Task()返回该任务在任务队列中的位置:

Task_ID = SCH_Add_Task(Do_X, 1000, 0);

有时需要从队列中删除任务,可以如下使用SCH_Delete_Task()来实现:

 1 bit SCH_Delete_Task(const tByte TASK_INDEX)
 2 {
 3     bit Return_code;
 4     if(SCH_tasks_G[TASK_INDEX].pTask == 0)
 5     {
 6         //这里没有任务...
 7         //
 8         //设置全局错误变量
 9         Error_COde_G = ERROR_SCH_C2\NOT_DELETE_TASK;
10         //同时返回宿误代码
11         Return_code = RETURN_ERROR;
12     }else{
13         Return_code = RETURN_NORMAL;
14     }
15     SCH_tasks_G[TASK_INDEX].pTask = 0x0000;
16     SCH_tasks_G[TASK_INDEX].Delay = 0;
17     SCH_tasks_G[TASK_INDEX].Period = 0;
18     SCH_tasks_G[TASK_INDEX].RunMe = 0; 
19     
20     return Return_code; // 返回状态 
21 
22 }

 

降低功耗

  调度应用程序的一个重要特性是能够支持低功耗运行。

  这是有可能的,因为当前所有的 8051系列芯片都提供“空闲”模式来暂停CPU的活动,同时保持处理器的状态。

  在这种模式 下,运行处理器所要求的功率一般减少大约50%。

  在调度应用程序中,这种空闲模式尤其有用。

  因为可以由软件控制进入空闲模式,而当微控制器收到任何中断时返回正常运行方式。

  因为调度器会产生定期的时钟中断,所以可以在每次调用调度程序的结尾将系统置为“睡眠”,并将在下一个定时 器时标产生时醒来。

1 void SCH_Go_To_Sleep()
2 {
3     PCON |= 0x01; //进入空闲模式
4     //在80C515/80C505上为了避免意外的触发,进入空闲模式需要两个连续的指令
5     //PCON |= 0x01; //进入空闲模式(#1)
6     //PCON |= 0x20; //进入空闲模式(#2)
7 }

 

网友评论