||
摘要:介绍将windows系统中消息的概念推广到单片机编程中,通过实例说明这种方法可以解决多中断的实时响应和耗时的数据处理的矛盾,并避免了代码重入的危险。同时,代码的可重用性也得到了极大的提高。
主题词:单片机,面向对象,消息,软件,中断
在单片机开发运用中,通常要使用各种中断,编制中断服务程序,当中断要处理的东西很多时,就会导致中断服务程序执行时间过长。这时如果有另外的中断发生,单片机就不能及时地进行处理,特别是对MCS-51这样的只有两级中断优先级的单片机,在这种情况下可能导致不能及时响应一些实时性要求高的中断。
对大多数要求实时性强的中断来说,最重要的是能不能及时将中断时刻的数据采集到,而处理数据的时间一般情况下是比较宽裕的;因此,采用只在中断服务程序中进行数据的采集和标志位的设置,而将数据的处理放到中断之外,同时,借鉴windows 编程的原理,在单片机编程中引入消息的概念,将中断产生的标志作为消息,而数据处理则放在消息循环中进行,这样一方面减少了中断服务程序的长度,提高了响应中断的速度;另一方面又不影响数据的处理。很好的解决了快速获取数据和数据处理耗时之间的矛盾。此外,本方法还有效的防止了代码重入带来的问题。
1.基于消息的编程方法
1.1消息驱动机制
在面向对象编程方法中,消息是各个对象之间进行通讯的手段。程序是通过消息来请求对象进行动作的,对象间的联系或相互作用也是通过消息来完成的,消息中包含了消息发送者的要求,消息接受者在收到消息后,根据需要来处理消息。对象发出消息后,进入消息队列(或不进入队列而直接发给消息处理对象),主程序负责组织消息队列,将消息发送给相应的消息处理对象,这个过程即称为消息循环。消息驱动机制如图1所示。
1.2消息驱动程序设计
当在程序中采取消息驱动的程序结构时,一般是主控程序中只处理消息循环,判断并为消息处理对象发送所需处理的消息。接受消息的对象则以消息响应函数的方式出现,当接收到主程序发来的消息时,消息响应函数获得控制权,做完相应的处理后将控制权交还给主程序继续消息循环的处理。用户的输入及对象的请求仅是向消息队列中添加相应的消息。采用消息驱动程序设计,可使程序结构更加清晰,各功能模块界限分明,便于修改,扩充性好,对新的功能只要添加新的消息和响应函数,这对于输入输出复杂的系统程序设计来说,优点更加明显。
2.基于消息的单片机编程方法。
和windows 多任务操作系统相比,单片机是一个单任务的系统,不能同时运行多个程序,资源也十分有限。因此,单片机系统不可能象windows 那样建立庞大的消息循环机制,将消息分发给各个程序并行处理。在基于消息的单片机编程中,采取一种简化的方式,消息可以这样来定义:当某个事件(例如中断)发生时,事件处理程序(例如中断服务程序)设置相应的标志,不同的标志即代表不同的消息;而主程序所进行的消息循环就是主程序不断的判断这些标志,以决定启动哪一个处理函数(即将消息发送给特定的消息处理函数)。采用这种结构,可以合理的利用有限的系统资源,将数据的实时采集和耗时的数据处理有机的结合起来;采用这种结构,还可以实现对外部中断的分时复用③。下面,以一个笔者实际实现的液体冰点测试仪为例,探讨一下基于消息的编程方法在单片机编程中的具体运用。
2.1测试仪的系统结构。
该测试仪由温度变换器,冰点传感器,LCD显示器,打印机及89C52构成。系统结构方框图如图2 。
该测试仪用于测定液体的冰点。当在菜单中选取“测试”时,89C52先控制加热器加温至一设定值,接着关闭加热器,启动制冷器制冷,直到冰点传感器发出一低电平通过外部中断0通知89C52液体已经结冰,89C52控制关闭制冷器并打印数据。整个过程中,每0.5秒实时采集一次温度并在LCD上显示,同时每秒对时钟显示进行一次刷新。开始测试前,还可通过键盘对系统进行设置和自检。T0工作在方式2,T1定时器用做波特率发生器。T0的两个定时器都设置为记到一个外部脉冲就溢出的状态,扩展的键盘占用T0中断,T1中断作为外部实时时钟的刷新中断请求信号输入端。T2设置成自动重装载方式,每0.5秒刷新一次温度值。
2.2软件需求分析。
根据测试仪的系统结构,可以看出对该单片机的软件设计来说,最重要的是协调好各个中断的处理。在本系统中共有4个中断,分别为:
a、冰点传感器的外部中断
b、键盘中断
c、定时采集温度的中断
d、时钟更新中断
因为系统最终所测试的数据是冰点传感器发出中断请求时刻的温度值,所以将该中断置为高优先级,而其余中断都设置为低优先级。
2.3传统的编程方法。
通常情况下,我们将中断发生后所需进行处理的工作全部放在中断服务程序中完成,而主控程序需要做的只是初始化系统并等待中断。主控流程如图三。
以本设计的键盘中断服务程序为例,中断服务程序流程如图四。分析上面的中断服务程序流程不难发现这种设计存在几个弊端。
因此,笔者在系统软件设计中想到采用一种基于消息的编程方法,即通过在中断服务程序中,向主程序的消息队列添加相应的消息,来达到将中断服务程序中的数据处理部分分离出来的目的。中断服务程序返回主程序后,再根据消息队列中的消息转向相应的消息处理函数。这样,中断服务程序只需添加消息,而由主控程序给各个消息处理函数分发消息。由于消息处理函数不会被相互调用,所以不会产生代码重入的现象。以下仍以主控程序和键盘中断为例,说明基于消息的软件设计思想。
2.4 基于消息的编程方法
在基于消息的软件设计中,主控程序不仅仅是等待中断,还需要查询消息队列,并根据队列中的不同消息,调用相应的消息处理函数(在面向对象编程中,可理解为将消息分发给各个消息对象),在本系统中,产生消息的是各个中断,数量不多,因此在系统设计中,消息队列表现为一个字节的变量,通过置位该字节的不同位来反映不同中断的消息,消息队列数据结构图五,主控程序流程如图六,键盘中断服务程序流程如图七,键盘消息处理函数流程如图八。
由于消息处理函数已经脱离了中断处理过程,所以系统对消息处理不再象中断服务程序那样,在时间上有十分苛刻的要求。同时,在消息处理过程中,用户仍可通过键盘中断来与系统进行交互。基于消息的单片机编程思想是从windows 系统中进行借鉴的一个有益尝试。
参考文献:
1.MCS-51 单片机应用设计 张毅刚 哈尔滨工业大学出版社
2.VC++ online help Microsoft Corp.
3.实现MCS-51系列单片机多重中断的一种简易方法 高敏麟,丁杰 《电子技术应用》 1997.6
附: 部分C51源代码
//主控程序 main()
void main()
{
//初始化各硬件设备
Initial_Devices();
//初始LCD
Current_screen_no=1; //设置屏幕编号
Current_function_no=0; //设置功能键编号
LCD_Write(Current_screen_no,Current_function_no);//处理消息队列
while(1) {
if((Interrupt_id&0x01)==0x01)_Int_W0(); //外部中断0处理模块
if((Interrupt_id&0x02)==0x02)_Int_T1(); //定时器0 TF1中断处理模块
if((Interrupt_id&0x04)==0x04)_Int_T2(); //定时器2定时中断处理模块
if((Interrupt_id&0x08)==0x08)_Int_T0_left(); //定时器0 TF0中断处理模块
//左键被按下
if((Interrupt_id&0x10)==0x10)_Int_T0_right(); //定时器0 TF0中断处理模块
//右键被按下
if((Interrupt_id&0x20)==0x20)_Int_T0_top(); //定时器0 TF0中断处理模块
//上键被按下
if((Interrupt_id&0x40)==0x40)_Int_T0_bottom(); //定时器0 TF0中断处理模块
//下键被按下
if((Interrupt_id&0x80)==0x80)_Int_T0_ok(); //定时器0 TF0中断处理模块
//确认键被按下
}
}
//定时器0 TF0中断服务程序(键盘中断服务程序) _timer0()
void _timer0(void) interrupt 1 using 1
{
unsigned char i;
for(i=0;i<10;k++); //软件消抖
//读取键值,设置消息队列
if(isLeft)Interrupt_id+=0x08;
if(isRight)Interrupt_id+=0x10;
if(isTop)Interrupt_id+=0x20;
if(isBottom)Interrupt_id+=0x40;
if(isOk)Interrupt_id+=0x80;
//启动定时器
TL0=-1; //重装初值
TR0=1;
}
//左键被按下处理程序 _Int_T0_left()
void _Int_T0_left()
{
//清除消息队列
Interrupt_id=Interrupt_id&0xf7;
//功能键编号减1,即功能键左移一个单位
if(Current_function_no!=0)Current_function_no--;
//刷新LCD
LCD_Write(Current_screen_no,Current_function_no);
}
//右键被按下处理程序 _Int_T0_right()
void _Int_T0_right()
{
//清除消息队列
Interrupt_id=Interrupt_id&0xef;
//功能键编号加1,即功能键右移一个单位
Current_function_no++;
//刷新LCD
LCD_Write(Current_screen_no,Current_function_no);
}