这一章将介绍的是主循环调用任务函数的一种非常常用的结构。到目前为止,在主进程的构建方面,除了顺序调用我只用到过这一种构建方式,并且用得非常多。在2011年的电子设计大赛上,笔者就用了这种程序结构获得了安徽省第一的成绩,可见这种结构的威力。之所以叫它“界面函数”结构,是因为它各个函数就像一个个界面一样,在每个界面(函数)中完成某些特定的功能,我没找到这种结构的通用叫法。

  在第1章中介绍到由主循环顺序调用其他函数的结构,这种顺序执行的方式是最简单的情况了。当情况复杂时,各个函数之间存在着某种逻辑关系,一个函数执行完毕后可能要根据具体情况决定下一个要执行的是哪个函数,同时也可能由于外部原因需要立马切换到另一个函数里。

  为了让大家更好地理解,举这样一个例子:

  一个电子钟,上下左右4个方向键,一个 设置/确定 键,一个返回键,一个背光灯键。
  在正常模式下,显示时间、日期等信息,此时忽略上下左右方向键和返回键,当按下设置键时进入设置模式;
  在设置模式下,通过上下左右方向键来设置时间、日期,按下 设置/确定 键保存设置的信息并返回正常模式,按下返回键取消本次设置并返回正常模式;
  在以上两个模式中的任意模式按下背光灯键就亮灯6秒。

  例子中有两个基本模式,每个模式中对外部资源(如按键)的响应方式不同。如果把两种模式代码混合在一起编程的话会造成程序混乱,所以很自然地就想到把两种模式分开控制。这就产生了“界面函数”这种结构,之所以叫它界面函数,是因为它们就像是一个个界面一样,每个界面完成不同的功能。下面先介绍一下这种程序结构的一般形式:

1. 界面函数一般结构

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
unsigned char FlagPage=0;

void main()
{
	/*初始化*/

	while(1)
	{
		if(FlagPage==0)
			Task0();
		else if(FlagPage==1)
			Task1();
		else if(FlagPage==2)
			Task2();
		……
	}
}

void Task0()
{
	/*Do Something*/
	while(1)
	{
		/*Do Something*/
		if(FlagPage!=0)
			return;
	}
}
void Task1()
{
	/*Do Something*/
	while(1)
	{
		/*Do Something*/
		if(FlagPage!=1)
			return;
	}
}
void Task2()
{
	/*Do Something*/
	while(1)
	{
		/*Do Something*/
		if(FlagPage!=2)
			return;
	}
}

  可以看到,主循环其实不进行任何实际功能的处理,它完成的只是调用各个任务函数。对于比较大型复杂的系统,main函数的主循环里根本不放要实际处理的代码,而是把所有任务函数归到一起,根据选择进入相应的任务函数,当处理完该任务之后又会回到主循环,由主循环再次分配任务。
  此时主循环的作用就是调配任务(当然用来调配任务的主循环本身也是一个最基本的任务),而在被调配的任务里面可能还会再次被该任务调配的子任务。
  再来看看被调用的任务函数,这些函数已经不只是完成一些简单功能了,它并不是执行一些固定操作后返回,每个任务函数都有自己的一套控制逻辑,并且“不那么容易返回”。
  这些任务函数同属于一个进程,但是同一时刻只有一个可以运行。当进入某个函数时,可以说进程被这个函数阻塞,其他函数得不到运行。但这也就是我们需要的效果,因为每个函数都有自己的一套控制逻辑,不需要考虑其他界面函数。
  而在函数退出时,可以由该函数本身通过FlagPage指定下一个要进入的函数,或者本来就是由于外部(中断)修改了FlagPage变量才导致该函数退出的。
  这种结构是非常常用的,并且尤其适合那些有多种界面(或者说多种工作模式)的场合。
  比如电子钟里可能有时钟界面、设置界面,再复杂一点有秒表、定时器等界面,从一个界面进入到另一个界面都是由按键控制的。如:在时间界面按下设置键进入到设置界面,按下返回键就进入到logo界面。这一个个界面也就是任务函数,只不过这个任务函数不会自动跳出,而是根据按键情况决定是否跳出、并通知主循环要跳到哪。每个界面里也会有选择地对其他进程提供的信息进行处理,比如时间界面就会对时间累加进程所提供的时间信息进行显示,同时也会对按键扫描进程提供的按键序号进行处理;而logo界面只会对按键信息进行响应,忽略时间进程提供的时间,但是时间进程仍然在运行。这些进程都是由定时器进行的,在后面会说。
  再如电子设计大赛的小车程序就是,它分为领跑模式、跟随模式、超车模式等,每种模式就是一个界面函数,只不过在运行的过程中不允许有外部操作,完全由每个界面函数本身根据采集到的当前状态信息决定是否退出,并结合一些其他全局变量判断下一个要进入的模式。
  以上把这个典型结构做了一个简单介绍。

2. 更高的角度分析这种结构

  在2.1节分析了一下界面函数这种典型结构,在此我想借这种结构分析一下整个系统的构成。把上述系统用下面这个图表示: 程序结构示意图

  这里面所有函数都是由主函数调用的,属于主进程,并且这些函数也都体现出了系统结构。
  例如在函数1里面想进入函数2,不是直接调用函数2,而是先返回函数1,再由主循环分配到函数2。
  正如前面所说,这种程序结构特别适合于多种“界面”的功能。一般情况下,主进程不会停留在主循环里,而是偶尔退出到主循环重新分配下一个将要进入的函数,大部分时间会停留在某个界面函数里。

  此外,这些函数之间有一些公共变量,这些变量的作用就是被各个函数使用,甚至用于函数间通信,辅助完成这些函数之间的逻辑结构的构建。比如1.1节中提到重要的FlagPage变量,这个标志变量就指明了当前工作于哪种工作模式下,任何函数(包括中断进程中的函数)都可以通过改变此变量来切换工作模式。
  也有一些与函数对应的用于完成特定功能的变量。比如用于数码管或者显示屏显示的现存,这些显存是有特定用处的,一般其他函数不会使用(但确实是公共变量,是可以被使用的)。

  上面是把变量进行了分类,与之对应地我们也把函数进行分类。
  图中的函数都是所谓的“界面函数”,是用于完成某一特定任务的函数,一般进入这个函数后主进程就会停在里面,当达到特殊目的后返回。而这些“界面函数”也会不断地调用其他函数完成功能,比如延时等。
  这些被界面函数调用的函数把它们称作“工具函数”。这些功能函数中有一些是公用的,比如延时函数,很多地方都会用到。而也有一些是某一个界面函数才会用到的,用于完成这个特殊功能的函数,比如用于完成屏幕显示用到的屏幕驱动函数、字符显示函数等,这些函数在其他地方几乎不会被调用。
  这些函数、变量结构是在编程中自然产生的,在此将它们明确分类一下,整个系统的构成有:

  • 1、整体的程序框架是由各个界面函数和少数关键的全局变量构建起来的。这是构成系统的主体框架。
  • 2、每个界面函数在完成特定功能时,会携带一些为自己服务的“私有的”变量和函数。
  • 3、为整个框架服务的还有一些常用的变量和函数,它们完成的是一些通用功能,可以把它们理解为“库函数”。

  需要提一下,到目前为止讨论的程序结构都没有考虑中断,或者说到目前为止都是把中断当作程序运行过程中的特殊情况处理,并没有融入到整个系统构建的问题内讨论,但是中断确实是存在的。请大家思考一下这种情况会发生什么事情:主进程所构建的程序框架会有一些服务函数,这些服务函数在中断处理程序里也可能会被用到。比如主进程在调用延时函数时被中断,而中断处理程序也调用了该延时函数,这会怎么样呢?
  这就是所谓“函数重入”问题,在此不想过多地讨论该问题,要知道51单片机的编译器keil默认是不支持函数重入的(事实上,如果你这样做了编译器会发出警告的)。
  所以,至少对于51单片机编程来说,在遇到多个进程编程时要注意这样一个原则:中断处理程序不要调用到可能被中断的函数。必要时可以为中断进程单独写一个服务函数,函数内容可能跟主进程中的某个函数一模一样,但这样可以避免上述问题。

  以上就是对“界面函数”这个典型结构的介绍,也分析了整个系统的构成。但是,正如上面说过的,这些都是对一个进程的结构的讨论,并没有涉及到中断。后面的内容会引入中断,甚至到最后把所有的任务都放到中断处理函数里,你会发现把任务放到中断处理函数里有着巨大的优势。

Comments