首页 > 代码库 > 单片机用定时器分配任务的程序结构总结
单片机用定时器分配任务的程序结构总结
转载请注明本文地址:http://blog.sina.cn/dpool/blog/s/blog_6f2b6ba80101bwka.html?vt=4
http://blog.sina.cn/dpool/blog/s/blog_6f2b6ba80101bwka.html?vt=4
本文是2013年写的,后来整理成了系统文章,请访问 http://nicekwell.net/ 查看单片机编程系列文章。
以下是2013年原文:
经过这几天做的程序,和以前做电子钟时的感悟,现在对单片机的整个程序结构做一下总结。相信这个总结是很有必要的,在组织大型复杂程序结构时需要有一个正确的理论指导。
感觉这种程序结构的很多想法和操作系统非常像,但是毕竟没学过操作系统,有些表述可能不准确,欢迎批评指正。
首先介绍一下任务的概念,main函数最后是一个死循环,它就是一个任务,是整个系统最基础的任务。除main函数的主循环无条件执行外,其实每一个进程和任务都是一个有条件跳出的循环,当这个任务没有完成时就会一直在这个循环里,当任务完成了就会从循环跳出,结束这个任务。当然也有一些简单的任务没有循环,就是一些顺序代码执行完了就结束,也可以认为它是一个简单的任务。
其实所谓的任务在单片机里本质上也就是一个函数,只不过这个函数可能“没那么容易结束”,需要达到一定的目的才会结束,把这样的函数称作“任务”。
如果要完成的是一个很简单的功能,我们可以把这个功能的一小段代码直接放到主循环里,单片机所有的精力都会放在这段代码上(暂不考虑中断)。
但是如果程序结构比较复杂,有多种任务可能会处理,主循环可以调用其它的任务函数而转到其它任务,但是一旦跳转到其它任务里,主循环和其它任务都不会得到执行。
所以有这么一个特点:由主循环调用的任务都只能单独地运行,进入一个任务,就不能处理其它任务。
对于比较大型复杂的系统,main函数的主循环里根本不放要实际处理的代码,而是把所有任务函数归到一起,根据选择进入相应的任务函数,当处理完该任务之后又会回到主循环,由主循环再次分配任务。
此时主循环的作用就是调配任务(当然用来调配任务的主循环本身也是一个 最基本的任务),而在被调配的任务里面可能还会再次被该任务调配的子任务。
既然主循环用来调配任务,那什么时候该进入什么任务呢?
1、 顺序调用。这是最简单的了,几个任务按顺序进行。
像飞思卡尔智能车的程序就是这种结构,先进行传感器扫描 - 再进行转向角计算 - 再进行最大速度计算 - 再进行速度控制,就这几个任务不断重复。虽然在主循环里只是简单调用一下函数,而被调用的任务函数可能很复杂,比如转向角计算这个函数,对它支持函数专门写出一个c文件,而这个文件的代码量甚至比main函数所在文件的代码量都要大。
2、 可能各个任务之间存在逻辑关系,在各个任务里面直接指定下一个要进入的任务。
比如电子钟里面的模式切换,1602从一个界面进入到另一个界面都是由按键控制的。如:在时间界面按下设置键进入到设置界面,按下返回键就进入到logo界面。这一个个界面也就是任务函数,只不过这个任务函数不会自动跳出,而是根据按键情况决定是否跳出、并通知主循环要跳到哪。(每个界面里也会有选择地对其它进程提供的信息进行处理,比如时间界面就会对时间累加进程所提供的时间信息进行显示,同时也会对按键扫描进程提供的按键序号进行处理;而logo界面只会对按键信息进行响应,忽略时间进程提供的时间,但是时间进程仍然在运行,不然时间岂不是不准了。 这些进程都是由定时器进行的,在后面会说。)
再如红外遥控的小车,在执行完一个动作之后都会回到ActionNum=0(不执行任何动作)的状态,把主进程释放,可以执行其它类的任务或者进入空闲状态。
3、 由定时器分配,整个系统按定时器的节拍运行。
电子钟就是这种结构非常典型的例子。由于没用时钟芯片,采用的是8位自动填装定时器每隔200us一次中断来计时的(很准哦),除计时和日期计算以外,要处理的任务还有:1602显示、按键扫描、温度采集和后来添加的电源管理(控制电池充放电)。
下面就来讨论一下在时间显示界面里需要做的这么几个任务(时间显示界面本身就是由main函数的主循环调配的一个子任务):
1602需要500ms刷新一次(小时和分钟之间有一个冒号“:”需要500ms闪动一次,因为不显示秒,所以如果分钟发生了变化整屏也要刷新一次);
按键扫描5ms扫描一次(如果放在时间界面的主循环里进行按键扫描的话就不用考虑这么多,但是那样有很多缺点,我这里是把按键扫描当做一个固定的进程,其它所有任务都能用利用这个扫描结果);
电源管理500ms检测一次;
温度采集500ms一次,但比较复杂,涉及到“任务分割”问题(自己起的名字哈),后面单独讨论。
很自然地想到利用定时器计时来进行,那么定时器会以什么样的工作方式来调配整个系统呢?
首先,定时器200us一次中断,肯定要有一个变量累加,当累加到5ms时,进行一次按键扫描;
然后继续累加,每累加到一个5ms都要进行一次按键扫描,当累加到500ms时进行一次按键扫描、1602刷新显示、电源管理,(温度采集暂且忽略)。
那么,我们可以直接把这些代码写入中断处理程序吗?
比如按键扫描我可以把这段代码写入中断处理程序吗:
for(i=0;i<=6;i++) //总共7个独立按键
{
if(P1&pow2[i]==0) //pow2[i]就是2的i次方
{
delay5(1); //延时5ms,以确认是否真的按下
if(P1&pow2[i]==0)
{
keynum=i;
break;
}
}
}
这是当然不行的,执行完这段代码所需的时间就超过5ms了,而定时器是200us一次中断。如果把这段代码放到时间界面主循环里是可以的,但是这样的话在其它界面就不能使用(除非也加入相同的代码),正如我上面所说:那样有很多缺点,我这里是把按键扫描当做一个固定的进程,其它所有任务都能用利用这个扫描结果。
所以这里要提出一个定时器分配任务的程序结构原则一:定时器中断里的代码执行长度一定不能超过定时器中断时间。
这不是废话吗,肯定的啊。所以我们要想想办法,把按键扫描程序改成了如下:
static unsigned char reslast; //保存上次扫描结果,0-没按下,1-按下
unsigned char res;
unsigned char i;
numforkey=0;
res=P1;
for(i=0;i<=6;i++)
{
if(((res&pow2[i])==0)&&((reslast&pow2[i])!=0)) //这次按下,上次断开
break;
}
//从这里出来,如果i==7则表示没有按键按下,i<=6的任意一个值表示那个键被按下了
keynum=i;
reslast=res;
这段代码里面没有延时,执行一次是很快的,而且也可以很好地完成按键扫描,比上面的那种延时扫描更有优势(不占用资源,而且稍加改造可以识别同时按下多个按键)。
所以,把原则一加上一句,定时器分配任务的程序结构原则一:定时器中断里的代码执行长度一定不能超过定时器中断时间,要想办法把任务改成不占用定时器时间的结构,给主进程让出更多的时间。
按键扫描在中断处理程序里算是可以完成了,但是500ms时的那么多任务在200us以内可以完成吗?好像有可能是可以的,就算这里可以,以后其它地方肯定会遇到处理量很大,在一个中断里完成不了的任务。而且我在电子钟里面把1602刷新就没有放在定时器中断里。
此时,就是定时器分配任务的程序结构原则二:当节拍时间到来时,要处理的任务真的很多,可以通过标志变量通知主进程执行。但通知让主进程做的事对实时性要求不能太高。
比如我在程序里设置了一个flag500ms标志变量,当此变量为1时标志到了500ms,时间界面的主循环检测这个变量,当发现这个变量为1时就执行1602刷新。(按键扫描和电源管理由于在任何界面都会用到,所以把它们独立出来,放在定时器中断里进行)。
这里的1602刷新对实时性要求不高,所以可以用定时器通知主进程执行。
下面要说的就是比较复杂的温度采集了,上面为什么没讨论它,就是因为它比较复杂。它既不能满足原则一(在一个定时器中断时间内完成)也不能满足原则二(对实时性要求不高)。
温度传感器用的是18b20,由单总线协议决定了对它进行一次读写大约需要18ms,而且读写它对实时性要求也很高。
这里隆重推出一个很高深的自己起名的概念——“任务分割”!!!所谓任务分割就是把不能在一个定时器中断时间里完成的任务分割成多个可以在一个定时器中断时间里完成的任务。顺便引出定时器分配任务的程序结构原则三:当既不满足原则一又不满足原则二,即既不能在一个定时器中断时间里完成又对实时性要求很高的任务,对它进行任务分割。
这是最不想做的办法了,因为很麻烦,需要彻底地了解这个任务的过程(而不是简单调用一下以前写好的驱动程序),并找到合适的办法进行分割。
这种方法我只在电子钟的温度采集里用过,其它地方从没用过,下面是我程序里的原文:
void reftemp() //读取并刷新温度。 //技术:原本的温度扫描是一个连续完整的函数,而这个函数完成的时间大约是18ms。但是由于定时器绝对不能停止工作,而定时器的中断时间是200us,在一个中断周期内不能完成所有的工作。定时器会对温度扫描造成影响。
//这里的解决方案是把原来的温度扫描函数中的延时全部用定时器及时处理,也就是把整个温度读取函数分割成许多可以在一个定时器中断周期内完成的程序片段,分成多个定时器周期完成。
//这种方法以前是没有用到过的,这是第一次用。测试完成,帅气!这个方法竟然成功了!!
{
float temperature; //保存温度信息
unsigned int temp;
temp=gettemperature();
if((temp&0x8000)!=0) //是负的
{
sign=1;
temp=~temp;
temp+=1;
}
else
sign=0;
temperature=temp*0.0625; //获取温度的浮点数
temp1=((int)temperature)/10; //获取温度的十位
temp0=((int)temperature); //获取温度的个位
temperature*=10;
tempdp=((int)temperature); //获取温度的一位小数
wcom(0x80+0x40+8);
if(sign==1) //负的
wdat(‘-‘);
else //正的
wdat(‘+‘);
wdat(0x30+temp1);
wdat(0x30+temp0);
wdat(‘.‘);
wdat(0x30+tempdp);
wdat(0xdf); //写入℃的圆圈
wdat(‘C‘);
}
分割方法是这样的:
这个函数是定时器通过500ms标识变量通知时间界面函数的主循环而调用的,随后这个系统的主进程会进入这个函数里。这个函数调用了温度采集的驱动函数gettemperature(),这个采集函数原本是用延时的方法来控制时间的,分割的方法就是把所有的函数延时改为定时器延时。在温度采集完之前,主进程还是被这个函数占用着,但是不会影响到定时器中断,所有定时器调用的任务都正常运行。
最后,总结一下整个单片机编程系统的结构:
1、 整个系统有一个主进程:main函数的主循环及其调用的所用任务函数,以及所有任务函数调用的子任务函数。
这个主进程的特点是一条线,精力只能放在一处;优先级低,任何中断所调用的任务都会使其停止工作。
2、定时器也可开辟一道进程,所有由定时器直接调用的任务都属于这个进程。
定时器进程可以通过一些标志变量通知主进程进行某种动作,最常用的控制方法是用定时器产生节拍信号,通知主进程进行相应动作;
同时,定时器也可以直接调用一些函数,在定时器中断处理程序里完成任务。所有由定时器直接调用的程序都属于定时器进程,优先级高于主进程;
用定时器分配任务有一下三点原则:
定时器分配任务的程序结构原则一:定时器中断里的代码执行长度一定不能超过定时器中断时间,要想办法把任务改成不占用定时器时间的结构,给主进程让出更多的时间。
定时器分配任务的程序结构原则二:当节拍时间到来时,要处理的任务真的很多,可以通过标志变量通知主进程执行。但通知让主进程做的事对实时性要求不能太高。
定时器分配任务的程序结构原则三:当既不满足原则一又不满足原则二,即既不能在一个定时器中断时间里完成又对实时性要求很高的任务,对它进行任务分割。
3、整个系统来看有两个并行的进程——主进程和定时器进程。主进程一次只能执行一个任务,而定时器进程由于任务一般比较小(如按键扫描、计时、数码管扫描等),所以认为定时器进程的任务也一并完成了。
看上去就像是多个进程在同时运行,这些进程之间可以通过公共变量进行通信,比如节拍时间的标识变量、计时产生的时间、按键扫描结果变量keynum等,所有其它进程可以有选择地对这些标识变量进行响应。类似于进程间通信。
附:定时器直接调用的任务的特点:
定时器中断是间断产生的,所以由它直接调用的函数可能很简单,在一次中断里处理完了就什么也不用考虑了(比如时间累加)。
也可能比较复杂,需要多次中断时调用任务的执行结果相比较(比如按键扫描需要结合上次扫描结果比较)。
总之,定时器中断调用的程序要求尽快执行结束,一般不会是连续的等待,而是多次调用结果的结合判断。
单片机用定时器分配任务的程序结构总结