先来看一下这个东西吧:http://v.youku.com/v_show/id_XNTk2NzExMjg4.html

  看完之后应该会觉得这个东西的结构非常复杂,这是笔者做过的最复杂的项目之一。由于是给公司开发的,所以和上一章一样不能公开源代码,但是会举一些简单的例子说明。
  刚接到这个项目了解了大体功能后,第一反应是用“界面函数”的结构。确实,这个东西是非常适合用界面函数完成的,但是由于当时笔者正在自学C++,于是用C++的思路分析了一下这个项目:
  1、总共有4个界面,而且有两个和列表框好像啊。
  2、几乎所有的动作都是由旋转编码器触发的。
  经过一番思考之后,笔者决定做一次尝试,用面向对象的思想加上事件驱动的机制完成它。

  下面就介绍一下这种结构,也希望能借此说明“对象”和“事件”的概念。

1. 对象和事件

  基本上每个面向对象语言的书都会把对象的概念说一下。在这个项目里有4个界面,把这四个界面看作四个对象,这四个对象的所有动作都由事件驱动。
  什么又是“事件”呢?简单来说“一个对象发生了某个事情”就是这个对象的某种事件。事件一定是基于某个特定的对象而言的,不能简单地说“发生了某个事情”,应该说“某个对象发生了某个事情”。而我们要做的就是确定“每个对象有哪些事情会发生”,并完成“某个对象在发生某个事情时要做的事”。

  所以对于一个对象而言,它应该有:

  • 1、与它对应的事件函数,用于执行“某个事件发生时要做的事”,一个对象所拥有的这些函数的个数和它可能发生的事件数是相等的。
  • 2、完成上述函数所需要的辅助函数。
    这些函数有些可能是公共的,是由系统提供的API或者其他工具函数;
    也可能是这个对象特有的,是对这个对象做的某种更改。
  • 3、每个对象都有自己的属性,这些属性在程序中的体现就是变量。

  这里面,1和2的函数本质上都是一样的,都是这个对象所包含的函数。不同的是:
  与事件相对应的函数称它为这个对象的“事件”,这些函数数量与这个对象的事件数是相等的;
  完成某种特定操作的函数称它为这个对象的“方法”,也就是“这个对象可以做的事”。并且这些方法中,有些是允许被外界调用的,有些只允许在本对象内使用,所以又分为“公共方法”和“私有方法”。

  下面就以一张表来说明一个对象内部的组成,以及各个成员的含义:

对象的组成

  有了这些了解之后我们就可以开始构建整个系统了。

2. C语言对一个对象的封装

  在C++ 中有专门的对象结构,它可以把对象里的函数和变量分为公共、私有等类型。

  而在C语言中没有这样的结构,我们通过使用上的约定也可以达到同样的效果。比如一个内部的函数,我们约定有些函数外部可以调用,它就是公共函数;约定有些函数外部不要调用,它就是私有函数。

  下面是对象的各个部分在C语言中的形式及意义:

对象的各个部分在C语言中的形式及意义

  公共方法和事件本质上都是对外公开的函数。公共方法完成对本对象的某一主动的操作,而事件是完成对外部被动的响应。
  甚至可以把事件从对象的组成中取出来,把事件归到系统范畴。


  下面就以一个简单的列表框的例子说明如何封装一个对象,假设这个列表框(对象)的名字叫做List1,一共只有3个列表项,每个列表项都有一个自己的名称和内容(0~255),有一个光标指示当前选中列表项,分配的事件有“列表项+1”“列表项-1”“列表项内容+1”“列表项内容-1”,并且这些改变是可以循环的(255+1=0,0-1=255)。

  假设屏幕显示的网格是4行16列,外部提供的绘图函数有:
  Disp(r,c,unsigned char *) //往r行c列写入一个字符串
  Clear() //清屏

  有了这些条件后,对这个对象的封装如下:

  • List1.c
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
96
97
98
99
100
101
102
103
/* List1 */
void Clear();	//清屏函数
void Disp(unsigned char r,unsigned char c,unsigned char *p);	//在r行c列显示字符串
/**************公共变量*****************/
#define List1_ListCount 3	//列表长度为固定
unsigned char List1_ListIndex=0;	//当前选中的列表项,从0开始数
#define List1_StartIndex 0	/*当前屏幕显示的第一个列表项序号,从开始数,
				由于屏幕能一次性把所有的3个列表项都显示出来,所以这里是固定的值。*/
unsigned char List1_ListData[3];	//三个列表项的数据
/**************私有变量***************/
unsigned char code List1_Name0[]="Power";	//第0号列表项的名称
unsigned char code List1_Name1[]="Mode ";	//第1号列表项的名称
unsigned char code List1_Name2[]="K    ";	//第2号列表项的名称
/**************私有方法***************/
void List1_DispName()	//在固定位置显示个列表项的名称
{
	Disp(0,1,ListName0);	//显示号列表项名称
	Disp(1,1,ListName1);	//显示号列表项名称
	Disp(2,1,ListName2);	//显示号列表项名称
}
void List1_DispCursor()	//在当前选中列表项前显示“>”,没选中的显示空格
{
	unsigned char i;
	for(i=0;i<List1_ListCount;i++)
	{
		if(i==List1_ListIndex)
			Disp(i,0,">");
		else
			Disp(i,0," ");
	}
}
void List1_DispData(unsigned char n)	//显示n号列表项的数据
{
	unsigned char vm[4];	//现存,以十进制显示,总共三位数
	//先计算现存
	vm[0]=List1_ListData[n]/100+0x30;	//计算百位的现存
	vm[1]=(List1_ListData[n]%100)/10+0x30;	//计算十位的现存
	vm[2]=List1_ListData[n]%10+0x30;	//计算个位的现存
	vm[3]='\0';	//字符串结尾
	//下面开始显示
	Disp(n,10,vm);	//从n行列开始写入数据
}
/***************公共方法************************/
void List1_Show()
{
	//先清屏
	Clear();
	//再显示所有列表项的名称
	List1_DispName();
	//再显示所有列表项的数据
	{
		unsigned char i;
		for(i=0;i<List1_ListCount;i++)
			List1_DispData(i);
	}
	//再显示光标
	List1_DiapCursor();
}
void List1_Hide()
{
	Clear();
}
void List1_SelectedListP1()	//当前选中项内容+1
{
	List1_ListData[List1_ListIndex]++;
	List1_DispData(List1_ListIndex);
}
void List1_SelectedListM1()	//当前选中项内容-1
{
	List1_ListData[List1_ListIndex]--;
	List1_DispData(List1_ListIndex);
}
void List1_ListP1()	//选中项序号+1
{
	List1_ListIndex++;
	if(List1_ListIndex==3)	//实现循环
		List1_ListIndex=0;
	List1_DispCursor();	//刷新光标
}
void List1_ListM1()	//选中项序号-1
{
	List_ListIndex--;
	if(List_ListIndex==0xff)	//实现循环
		List1_ListIndex=2;
	List1_DispCursor();	//刷新光标
}
/**************事件******************/
void List1_Key0Down()	//0号键按下,让选中项序号-1
{
	List1_ListM1();
}
void List1_Key1Down()	//1号键按下,让选中项序号+1
{
	List1_ListP1();
}
void List1_Key2Down()	//2号键按下,让选中项内容-1
{
	List1_SelectedListM1();
}
void List1_Key3Down()	//3号键按下,让选中项内容+1
{
	List1_SelectedListP1();
}

  在其他地方只要包含这个List1.c文件就可以调用该对象里的各种方法和事件函数了,里面的私有方法和私有变量虽然约定外部是不要使用的,但是外部确实是可以调用的。所以也可以为这个对象写一个List1.h文件,只把公开的部分进行声明:

  • List1.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* List1 */
#ifndef LIST1_H
#define LIST1_H

#define List1_ListCount 3	//列表长度为固定
unsigned char List1_ListIndex=0;	//当前选中的列表项,从开始数
#define List1_StartIndex 0	/*当前屏幕显示的第一个列表项序号,从开始数,
					由于屏幕能一次性把个列表项都显示出来,所以这里是固定。*/
unsigned char List1_ListData[3];	//三个列表项的数据
/***************公共方法************************/
void List1_Show();
void List1_Hide();
void List1_SelectedListP1();	//当前选中项内容+1
void List1_SelectedListM1();	//当前选中项内容-1
void List1_ListP1();//选中项序号+1
void List1_ListM1();//选中项序号-1
/**************事件******************/
void List1_Key0Down();	//0号键按下,让选中项序号-1
void List1_Key1Down();	//1号键按下,让选中项序号+1
void List1_Key2Down();	//2号键按下,让选中项内容-1
void List1_Key3Down();	//3号键按下,让选中项内容+1

#endif

  如果采用了List1.h的话,要把List1.c文件里重复定义的部分给去掉。
  每个对象有每个对象的特点,它们差别很大,构建方法也是大不相同的。但是每个对象构建好之后就是一个模板,它是非常独立的,在其他地方只要把代码直接复制过去做少量更改就可以使用了。

3. 事件分配机制

  一个工程中会有多个对象,每个对象都有一些可能会发生的事件,这些事件函数是由系统调用的,由系统来判断什么对象发生了什么事件。
  比如在这个项目中,基本的事件有:左编码器按下、右编码器按下、左编码器左旋、左编码器右旋、右编码器左旋、右编码器右旋、左编码器按下左编码器左旋、左编码器按下左编码器右旋、右编码器按下右编码器左旋、右编码器按下右编码器右旋、串口接收数据事件、IIC接收数据事件。
  由于旋转编码器的驱动本身用的就是外部中断,外部中断对单片机来说就是一种意外事件。所以只要在中断里判断当前进行的是什么操作,并记录当前哪个界面正在被使用,就可以调用相应对象的相应事件了。
  然而,并不是所有的对象都一定具有所有的这些事件,根据该对象功能的需要,选用一部分有用的事件进行响应。比如在IIC界面里不考虑串口,这个界面就只响应IIC接收数据事件,而忽略串口接收数据事件。
  事件并不一定非要由外部中断产生,也可能是系统虚拟的。总之事件分配有系统完成,系统利用各个资源抽象出事件概念,然后分配到相应对象上。

  下面是一个完成事件分配的例子:

中断电路

  有两个按键,任意一个按键按下都会触发int0中断。假设共有2个对象,下面是事件分配的代码:

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
unsigned char FocusNum=0;	//标志当前获焦的对象序号,在此只有两个对象,范围是0~1
sbit kint0=P3^2;
sbit k0=P0^0;
sbit k1=P0^1;

……

void int0() interrupt 0
{
    EA=0;	//关中断

    if(k0==0)	//说明是k0按下
    {
        switch(FocusNum)
        {
        case 0:	//对象0
            Form0_k0Down();
            break;
        case 1:	//对象1
            Form1_k0Down();
            break;
        }
    }
    else if(k1==0)	//说明是k1按下
    {
        switch(FocusNum)
        {
        case 0:	//对象0
            Form0_k1Down();
            break;
        case 1:	//对象1
            Form1_k1Down();
            break;
        }
    }

    while(kint0==0);	//等待两个按键都释放     IE0=0;	//清除中断标志,防止在中断处理程序执行过程中再次触发了中断     EA=1;	//开中断
}

  采用FocusNum来记录当前激活的对象。

  在此想说明的是,k0和k1两个操作都是放在一个中断里的,具体产生什么事件则是再次通过代码判断的,这些代码属于系统层,是根据实际需要对事件的抽象。

  比如如果需要产生“两个按键同时按下”事件,则可按如下方式分配:

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
void int0() interrupt 0
{
    unsigned char 
    EA=0;	//关中断
    mdelay(200);    //这个延时是为了等待两个按键的状态都稳定,虽然我们是把两个按键同时按下,
                    //但肯定因为某个先按下触发中断,此时检测另一个按键不一定是按下的状态
    if(k0==0 && k1!=0)	//说明是k0按下
    {
        switch(FocusNum)
        {
        case 0:	//对象0
            Form0_k0Down();
            break;
        case 1:	//对象1
            Form1_k0Down();
            break;
        }
    }
    else if(k1==0 && k0!=0)	//说明是k1按下
    {
        switch(FocusNum)
        {
        case 0:	//对象0
            Form0_k1Down();
            break;
        case 1:	//对象1
            Form1_k1Down();
            break;
        }
    }
    else if(k0==0 && k0==0)	//两个按键都按下
    {
        switch(FocusNum)
        {
        case 0:	//对象0
            Form0_BothDown();
            break;
        case 1:	//对象1
            Form1_BothDown();
            break;
        }
    }

    while(kint0==0);	//等待两个按键都释放
    IE0=0;	//清除中断标志,防止在中断处理程序执行过程中再次触发了中断
    EA=1;	//开中断
}

  甚至可以继续改,实现“k0按下时k1按下”“k1按下时k0按下”“k0长按”“k0连按”等事件,这里就不一个个实现了。
  总之只要系统层能够识别的动作都可以抽象成事件。

4. 系统层构建

  在以前的编程中从来没把程序这么明确地分层,可能是因为这次项目比较复杂,并且用来面向对象的方法,所以这种结构自然就产生了。在各个对象中有一些公共方法,这些函数完成特定的功能,而他们都依赖于底层的支持。
  当然可以让对象直接操作驱动函数,从最底层开始。但是这样的话,一方面用起来会很麻烦,另一方面可能不是所有的底层功能都会用到。
  所以让系统层根据上层对象的需要把这些功能封装,并向上层提供使用接口。这是系统层做的事情之一。
  另外系统层也会构建一些控制逻辑,这些功能并不在底层有实体的驱动函数,它是系统在软件层面抽象出来的。比如当前系统的数据显示进制、背光灯时间等,它们不对应底层的操作,仅在系统层抽象。对于上层的对象来说,根本不需要考虑这些,只要调用系统层提供的API就行了。
  此外,系统层要做的当然还有事件分配,上一节介绍的事件分配系统是系统层核心的一部分,而系统层还有其他很多功能。事件分配系统是利用各个资源抽象出事件概念,并分配给各个对象。而本节讨论的系统层是利用各个资源,在底层驱动的支持下,根据需要构建出一些控制逻辑,并封装成系统API,供上层软件使用。

  综上,系统层做的事情有:
  1、构建事件分配系统。
  2、对底层驱动封装,并向上层提供操作接口。
  3、根据需要再构建一些其他控制结构,并向上提供接口。

5. 库函数

  库函数也属于对系统对底层的封装。某些功能可能比较复杂,可以构成一整套体系,那么就可以把这些函数归位一类,作为完成某个功能的库函数。
  这点和类似于操作系统的GTK库或者QT库,它们提供了大量的绘图函数。
  在本项目中,使用了绘图库,它就是在底层对液晶屏操作的基础上建立的文字和图片显示函数,提供了在指定位置写入字符、汉字,以及反色写入、垂直镜像写入等功能。上层的对象调用这些函数会非常方便。
  总结一下库的作用:利用系统层的API,或者跳过系统层直接调用驱动函数,构建出自己的一套控制逻辑,并对外提供基于这个控制逻辑的函数库。

Comments