先来回顾一下:
  在第2章中介绍了界面函数结构,它的思想是主进程为主体,外部的按键等作为特殊情况单独处理。但是当接触到的程序更复杂时,尤其是当程序里还要进行精确定时时,用单进程结构已经满足不了要求了,这就进一步产生了第3章的结构——定时器分配任务。
  定时器分配任务结构在主进程结构基础上开辟了一个定时器进程,在这个进程里进行按键扫描任务、计时任务等。此时这些任务是不会被中断的,定时并且精确地每隔一段时间执行一次。当时的看法是:这些定时器里的进程完成任务后把结果保存,主进程可以选用这些结果进行处理。
  此外定时器还要进行一个特殊的功能——给主进程下达命令,通知主进程进行某种动作。这个功能的本质就是向主进程提供了时间信息。
  这种结构已经结合了定时器,并且已经把一些简短的代码直接放到了定时器中断处理程序里了,但还有相当一部分代码放在主进程里。不是说不能放在主进程里,而是当时没有明确出定时器中的各个进程是如何形成的,这些定时器中的任务有什么更深刻的特征。
  本章就是专门讨论这些放在定时器里执行的任务。

  这种结构也有产生的背景,它源于一个“温度控制系统”的项目,具体内容大概有:数码管扫描、按键扫描、时间计时、蜂鸣器控制、温度控制,当温度低于某个值时启动一个固定功率的加热器,温度高于某个值时停止加热,温度更高时启动报警。
  由于这是给公司做的项目,所以不公开源代码,不过不需要源代码也完全可以理解这个结构。
  这个项目并不复杂,功能要求很明确,没有多个工作模式和界面。按开始的想法,这里面除了计时任务需要定时器外,其他任务都可以放在主进程里完成。但是这样的话可能就会出现各个任务之间的相互干扰,比如按下按键时进程被阻塞,数码管扫描就无法得到运行。
  所以,在做这个项目时我尝试了另一种方法——把这些任务全部放在定时器中断处理程序里,由定时器驱动每个任务的运行,主循环什么也不做。
  完成之后事实证明这种结构效果很不错,并且体现出了很多操作系统的思想。下面就来分析一下这种结构。

1. 定时器执行任务的程序结构

  【时间分配系统】   这种结构的任务需要在定时器中断里执行,而定时器中断的时间不一定是任务想要调用的时间,并且不同的任务的调用时间可能不同。所以肯定有一个时间分配系统,这个系统在特定的时间调用不同的任务函数。
  比如在这次的工程中,51单片机的定时器8位自动填装模式的定时时间不会太长,定时时间设为250us。而按键扫描、数码管扫描、蜂鸣器控制、温度控制、时间控制,这几个任务的执行时间是不一样的:

   任务名称 调用时间
   —— ——
   按键扫描 5ms一次
   按键处理 5ms一次
   数码管扫描 250us一次
   蜂鸣器控制 5ms一次
   温度检测 1s一次
   温度控制 1s一次
   计时和时间控制 1min一次

  在这里请大家回顾一下,第三章里提到过这样一个问题:当500ms来临时,需要完成的任务有 按键扫描、1602刷新显示、电源管理,(温度采集暂且忽略),定时器中断周期只有200us,现在一下子来了这么多任务,一个中断周期内可以处理完吗?
  当时采用的方法是把一些对时序要求不高的程序放到主进程里完成,而在这里我们还有另一种方法——把任务错开放在不同的定时器中断周期里。虽然这里面的每一个单独的任务都可以在一个定时器中断周期里完成,但是可能会有某个定时器周期里同时来了多个任务,这有可能会导致在一个定时器周期里不能处理完,所以一定要注意时间分配系统要把任务错开放在不同的定时器周期里。
  比如按键扫描和蜂鸣器控制都是5ms执行一次,但是它们却不在同一次定时器中断内执行,因为定时器中断周期是250us,如果在一个中断时处理多个任务可能时间比较长,不能在250us内处理完,所以将它们错开分到不同的时间段内执行,但是周期仍然是5ms。

  时间分配代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void t0() interrupt 1 	//250us一次中断
{
    static unsigned char numto5ms=0;	//用于5ms计时
    static unsigned char numto1min=0;	//用于1min计时
    /***数码管扫描任务***/	//数码管扫描,每次中断都执行,这里省略数码管扫描代码

    numto5ms++;
    if(numto5ms==5)	//5ms一次,温度检测
    {
        /***温度检测任务***/
    }
    else if(numto5ms==10)	//5ms一次,蜂鸣器发生
    {
        /***蜂鸣器发声任务***/
    }
    else if(numto5ms==15)	//5ms一次,按键扫描
    {
        /***按键扫描任务***/
    }
    else if(numto5ms==20)	//5ms到了,进行分钟判断,此时numto5ms要清
    {
        numto5ms=0;
        numto1min++;
        if(numto1min==12000)	//1min到了
        {
            numto1min=0;
            /***时间处理任务***/
        }
    }
}

  【任务执行函数】

  任务执行函数就是在定时器里调用的用于完成某种任务的函数。
  这个函数的具体特点将在下节介绍,因为很重要。

2. 定时器里面任务函数的特点

  首先分析一下这种任务函数的特点和要求:

  • 1、这些函数由定时器调用,所以对于它们的调用时间是很精确地每隔固定时间调用一次。
  • 2、由于这些函数是放在定时器里,所以这些函数必须简短,不能占用过长时间,必须可以在一个定时器中断周期内全部处理完。
  • 3、与在主进程连续执行的任务函数相比,这种函数的调用是周期、间断的,这种周期间断性的调用,决定它自己必须具有记忆以前的状态的能力,只有这样才能在本次被调用时决定应该进行什么样的操作。
    在上一章“占用式与非占用式程序结构分析”中已经明确了非占用式程序结构的优势,并且也明确了其内部结构:
    非占用式程序的一般结构
    有了上一章的基础就好理解这些在定时器里的任务函数了,实际上这些任务函数和在主进程连续执行的任务函数相比,就是把它们改为了非占用式程序放在定时器里执行。
  • 4、由于要保证每个任务都要在很短的时间内执行结束,所以就要求每个任务不能阻塞进程,不论这个任务当前处于什么情况,应当执行一次之后立马退出。
    这样,不论某个进程的执行情况如何,其他进程绝对都会继续执行,也就把各个进程独立开了,保证每个进程都会得到及时的执行。
  • 5、各个进程相对独立,但各个进程间也有通信。比如按键扫描进程把按键码传递给其他进程;蜂鸣器进程通过变量接收外部下达的响铃指令。
    要注意一个特点:由于定时器里面的任务函数是被周期性的调用的,所以如果想使用某个进程的功能,必然不可能像以前那样通过调用函数来实现,因为它本身就一直在被调用着,必然是通过这个进程对外设置的接口变量来实现。

3. 过程任务的定时器化

  这里讨论如何把一个过程化的程序改成定时器化的程序。
  没有找到通用的方法,可以确定的是定时器化的任务结构肯定就是像非占用式程序结构那样。
  下面举几个定时器化的程序的例子。

  • 数码管扫描
1
2
3
4
5
6
7
8
9
void smgdisp()
{
	static unsigned char n;
	n++;
	if(n==8)
		n=0;

	smgndisp(n,?);
}

  用静态变量n记忆点亮的数码管序号,这样轮换点亮完成扫描。
  还想举一下上一章里面的扫描全彩点阵的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void refresh7()
{
	static unsigned char r=0;
	static unsigned char flagrgb=0;	//当前需要点亮的颜色,0-R,1-G,2-B
	static unsigned char num=0;
	num++;
	if(num==32)
	{
		num=0;
		flagrgb++;
		if(flagrgb==3)	//说明三种颜色都扫描完了
		{
			flagrgb=0;	//从红色开始扫描
			r++;		//开始扫描下一行
			if(r==8)	//如果发现行都扫描结束则从第行开始扫描
				r=0;
		}
	}

	if(num<light7)	//说明需要点亮
	{
		switch(flagrgb)
		{
		case 0:	//扫描红色
			DPw = ~(0x01<<r);
			DPr = ~vm7r[r];//送入R灯IO接口显示
			break;
		case 1:	//扫描绿色	
			DPw = ~(0x01<<r);
			DPg = ~vm7g[r];
			break;
		case 2:	//扫描蓝色
			DPw = ~(0x01<<r);
			DPb = ~vm7b[r];
			break;
		}
	}
	else	//说明不需要点亮
	{
		DPw=0xff;
		DPr=0xff;
		DPg=0xff;
		DPb=0xff;
	}
}

  也是通过静态变量记忆,完成某行某个颜色的亮度判断。

  • 按键扫描

  简单的一个按键扫描程序在上一章也例举过:

1
2
3
4
5
6
static unsigned char keylast;	//保存上次的按键值
if(key==0 && keylast==1)	//检测到一个下降沿
{
	/*do something*/
}
keylast=key;

  下面例举一个增强型的按键扫描程序,它可以识别多个按键按下、释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void keyscan()	//5ms调用一次 

{
	static unsigned int key;	//本次扫描结果
	static unsigned int keylast=0xffff;	//上次扫描结果
	unsigned char i,j;

	//开始扫描
	for(i=0;i<=3;i++)
	{
		DPkey=~pow2[i];    //pow[]是一个数组,代表2^i
		for(j=4;j<=7;j++)
		{
			if(DPkey&pow2[j])	//是1
			{
				key|=pow2[4*i+7-j];
			}
			else	//是0
			{
				key&=~pow2[4*i+7-j];
			}
		}
	}
	//开始判断上升沿和下降沿
	if(key^keylast)	//说明按键状态有变化
	{
		for(i=0;i<=15;i++)
		{
			if((key&pow2[i])==0 && (keylast&pow2[i]))	//下降沿,按键按下
			{
				/****在此添加i号按键按下时要做的事****/
			}
			else if((key&pow2[i]) && (keylast&pow2[i])==0)	//上升沿,按键释放
			{
				/****在此添加i号按键释放时要做的事****/
			}
		}
	}

	keylast=key;
}

  在这里我想说明的是,任何复杂的功能都可以写成这种“定时器化”的形式。
  如果希望更多的功能则需要添加其他结构,比如想要识别按键长按,则需要添加静态变量记忆按键状态并进行计时。
  不管需要的功能如何,都是可以用这种结构实现的。

  • 蜂鸣器控制

  这个相比较于上面两个比较特别,因为上面两个结构比较简单,目的明确,要干什么很清楚。而这个蜂鸣器控制任务平时不工作,当外部下达指令时才会发声。
  所以想以此为例,再次说明任意功能都可以写成定时器内部任务的结构。虽然没有证明它,但看起来是这样的。
  对于这个蜂鸣器控制进程来说,需要有一下几种功能:短响1声、短响2声、短响3声、长响1s。
  这些功能在定时器任务里完成,通过一个变量通知这个进程执行哪种任务。
  具体完成代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/*****************************以下是蜂鸣器任务相关*********************************
蜂鸣器的任务函数会由定时器5ms调用一次,由一个标志变量标志完成什么样的声音,使用时只要修改一下变量就行了。
总共有这么及几种声音:
	短响1声、短响2声、短响3声、长响1s
/**********************************************************************************/
unsigned char music=0;	/*对外接口,外部想要发声直接修改这个变量就可以了。
			0-不响
			1-短响声
			2-短响声
			3-短响声
			4-长响1s		*/
void beep()	//发声任务,由定时器每5ms调用一次
{
	static unsigned char flagDoing=0;	/*标志当前是否正在执行某个任务,0-不在,1-在
						当检测到music变量不为时说明有任务,此时将此变量置,标志正在执行;
						具体执行哪个任务根据music变量指示;
						当任务执行完毕时清此变量,标志没有任务,是空闲状态,清零music变量,标志没有任务将要执行。	*/

	if(music==0)	//没有任务
	{
		buzzer=1;	//关闭蜂鸣器,	buzzer是蜂鸣器控制引脚,0-响,1-不响
	}
	else	//说明有任务
	{
		if(flagDoing==0)	//如果当前没有任务正在执行,则给当前任务赋值为想要执行的任务
			flagDoing=music;

		if(flagDoing==1)	//短响声
		{
			static unsigned char count=0;	//用来计时
			buzzer=0;	//打开蜂鸣器
			count++;
			if(count==20)	//任务结束判断条件
			{
				count=0;	//为下次任务做准备
				buzzer=1;	//关闭蜂鸣器
				flagDoing=0;
				music=0;
			}
		}
		else if(flagDoing==2)	//短响声
		{
			static unsigned char count=0;
			count++;
			if(count<=20)
				buzzer=0;	//打开蜂鸣器
			else if(count>20 && count<=30)
				buzzer=1;	//关闭
			else if(count>30 && count<=50)
				buzzer=0;	//打开
			else	//执行结束
			{
				count=0;
				buzzer=1;
				flagDoing=0;
				music=0;
			}
		}
		else if(flagDoing==3)	//短响声
		{
			static unsigned char count=0;
			count++;
			if(count<=20)
				buzzer=0;	//打开蜂鸣器
			else if(count>20 && count<=30)
				buzzer=1;	//关闭
			else if(count>30 && count<=50)
				buzzer=0;	//打开
			else if(count>50 && count<=60)
				buzzer=1;	//关闭
			else if(count>60 && count<=80)
				buzzer=0;
			else	//执行结束
			{
				count=0;
				buzzer=1;
				flagDoing=0;
				music=0;
			}
		}
		else if(flagDoing==4)	//长响s
		{
			static unsigned char count=0;
			buzzer=0;	//打开蜂鸣器
			count++;
			if(count==140)	//结束条件
			{
				count=0;
				buzzer=1;
				flagDoing=0;
				music=0;
			}
		}
	}
}

  用一个变量music来作为对外的API,通过它通知本进程执行哪个任务;
  music和具体任务之间有一个中间变量flagDoing,这个变量是用来缓冲外部对此进程发送的指令的,只有当进程内部的某个任务执行结束后才会响应下一个任务请求;
  本进程的各个分支是一个小任务,每个小任务中都各自有自己的静态变量,用于完成各自特定的功能;

  在每个小任务执行结束后,都会做一些处理,让本进程准备好接收下一个任务。
  总之,不管什么复杂的控制结构,都是可以写成这种被间断调用的“定时器化”的形式。

4. 定时器执行任务程序结构总结

  1、每个任务函数不会因外部状态不同而阻塞,不管外部状态如何,这个任务的本次执行都能够顺利通畅地执行完。
  2、每个任务函数执行时间都很短,有时间限制,都有自己的时间段。所以不管一个进程的状态如何,它绝对不会影响到其他进程的执行,整个系统不会因为一个进程而停下来,仍然随着定时器的节拍不断地运行。
  3、在这种结构中,所有被执行的代码都是高效的,因为没有延时等函数。
  4、可以用一个定时器完成多个精确的时间控制任务,事实上整个系统都在精确的时间控制下运行。

5. 我们追求的是什么

  从第3章开始,程序的主体逐渐从主进程转移到了定时器中断(从后台转移到前台),也对任务函数进行了一系列改造。

  首先,我想强调这些都是在大量编程时自然产生的,是整个系统越来越复杂的必然结果。

  另外,我们进行了这么多改变到底是在追求什么?我们渴望的系统结构是什么样的?

  在此我想引用《底层工作者手册之嵌入式操作系统内核》中的一段话,也算是为操作系统做个铺垫:
  “在没有操作系统的情况下,C语言是以函数为单位实现功能的,一个函数一个函数串行地执行,一个完整的功能会由多个函数共同完成。然而当软件系统的功能变得多而庞大的时候,这种方法几乎无法使用,因为此时各个功能之间必然会有千丝万缕的联系,不可能依次串行地完成每个功能,各个功能必然需要交替执行。以函数为功能单元的程序很难在执行一个函数的时候转而去执行另外一个不相关的函数,即使是使用一些技巧实现了,也会使整个软件的结构变得混乱不堪,不利于软件的维护和扩展。函数的工作方式就决定了并不适合以它为功能单元运行复杂的程序,在这种情况下就要使用操作系统了。操作系统是对函数运行管理的系统,它可以在一个函数还没有运行完就转而去执行另一个函数,并且还可以恢复到原来的函数继续执行,这样就可以根据需要及时调整到需要运行的函数来满足各种要求。”

  这段话的大概意思是:
  1、传统的过程式程序是以函数为单位执行的。
  2、以函数为单位的程序,在一个函数的执行过程中不能立刻跳转到另一个函数,也就是说可能会耽误另一个函数的响应。
  3、就算在一个函数中嵌入了另一个函数的代码使得另一个函数也能及时得到响应,那也会是整个程序结构混乱,不利于维护和扩展。
  而相比之下,操作系统具有很大的好处:
  1、操作系统中的编程是以功能为单位的。在实现一个任务时根本不需要考虑其他的任务函数,更不需要在一个任务里嵌入另一个任务的代码。
  2、操作系统可以在一个函数没有运行完之前直接跳转到另一个地方运行,并且以后可以恢复到原来地方继续运行。这样就可以及时对重要的函数进行响应。


  再来回头看看我们做的更改,我们把占用式程序改为了非占用式程序,实际上就是细化了各个任务,让每个任务函数的单次执行时间很短,也就可以及时地响应其他任务。对比第1章所说的主函数顺序调用的结构,如果把里面的函数全都换成非占用式结构,就能大幅缩短循环时间。当时有么一句话:“在最坏情况下的任务级响应时间取决于整个循环的执行时间”,为什么这里把这句话搬过来,你懂的。

  另外一点,在改为定时器执行任务后,保证了各个任务都有自己固定的时间段执行,每个任务都绝对不会阻塞进程、绝对不会影响到其他任务的运行。这一点和操作系统相比甚至更有优势,因为操作系统的高优先级任务是可以阻塞低优先级任务的,而定时器执行任务结构中的所有任务都一定会及时得到执行。只要能够把这个任务以“定时器化”的形式写入到定时器中断处理程序里,它就绝对不会因为意外而被阻塞(这也是对任务函数的要求之一)。


  我们的追求和操作系统是一样的,我们希望各个任务功能独立实现,不要相互影响。同时各个任务都能够及时得到响应。操作系统采用将寄存器入栈的方式保存状态信息,而在定时器执行任务的结构中是用静态变量的方式保存任务信息。

  从第3章到这里介绍的都是定时器中断相关的结构,下一章将介绍一种以外部中断为核心的编程结构。

Comments