单片机用定时器分配任务的程序结构总结

转载请注明本文地址: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等,所有其它进程可以有选择地对这些标识变量进行响应。类似于进程间通信。
附:定时器直接调用的任务的特点:
定时器中断是间断产生的,所以由它直接调用的函数可能很简单,在一次中断里处理完了就什么也不用考虑了(比如时间累加)。
也可能比较复杂,需要多次中断时调用任务的执行结果相比较(比如按键扫描需要结合上次扫描结果比较)。
总之,定时器中断调用的程序要求尽快执行结束,一般不会是连续的等待,而是多次调用结果的结合判断。

时间: 2024-12-21 12:40:07

单片机用定时器分配任务的程序结构总结的相关文章

时间片轮调的单片机程序结构

时间片轮调的单片机程序结构 作者:佚名 来源:本站原创 我们使用单片机去做一些任务的时候,通常把程序写成顺序结构,基本可以解决大部分的设计要求了.而且这种结构便于理解,而且程序易构成模块化,在各个模块中调用实现更复杂的任务. 然而顺序结构的写法,有时候避免不了沉重冗长的时间等待.例如键盘扫描,你就给我弄了一个delay_20ms()函数,而在这延时的过程,其实 MCU可以做很多事情的,这不白白的浪费掉这段时间吗?其实,delay的这段时间用数码管显示代替,也就是在等待的过程,我们可以做一下显示.

第五章 程序结构

返回值 系统自动生成返回值的副本,该副本可以在程序中的返回点获得 函数原型声明 除非函数的定义在相同源文件的前面,否则必须使用函数原型声明(通常在#include和using之后): #include <iostream> using namespace std; double power(double x,int y);  //函数原型声明,形参可以和函数实现不同甚至只写double,int int main() { x=power(y,z); } double power(double x

《C程序设计语言(第2版&#183;新版)》第4章 函数与程序结构

函数功能:隐藏操作细节,结构更加清晰,降低修改难度: 4.1 函数基本知识 返回值类型 函数名(参数声明表) { 声明和语句 } 函数在源文件中出现的次序可以任意: 返回值类型省略则默认int:return可不带表达式,执行到最后右花括号也会返回:都是没有返回值的,合法,但未成功返回的“值”肯定是无用的: 程序可看做变量定义与函数定义的集合:函数通过参数.返回值和外部变量通信: 4.2 返回非整型的函数 函数与调用它的主函数在同一源文件中,并且类型不一致时,编译就会发现该错误: 隐式声明:如果未

JavaWeb-06(Dom4j技术及Schema 约束、Tomcat与Web程序结构)

JavaWeb-06 JavaWeb-Dom4j技术及Schema 约束.Tomcat与Web程序结构 DOM4J及Schema 一.Dom4j解析(掌握) DOM4J解析XML文档 a. Dom4j是一个简单.灵活的开放源代码的库.Dom4j是由早期开发JDOM的人分离出来而后独立开发的.与JDOM不同的是,dom4j使用接口和抽象基类,虽然Dom4j的API相对要复杂一些,但它提供了比JDOM更好的灵活性. b. Dom4j是一个非常优秀的Java XML API,具有性能优异.功能强大和极

C语言函数与程序结构

title : C语言函数与程序结构 tags : C语言作用域规则 , 外部变量 ,静态变量 ,寄存器变量,宏定义 grammar_cjkRuby: true --- 外部变量 变量声明用于说明变量的属性(类型),而变量定义还会引起存储器分配 int sp; double s[MAX]; 声明地方:函数外 如果上面的变量定义在所有函数之外,即为外部变量,并为这两个外部变量sp.s[MAX],分配储存单元以及数组的长度,在其源文件中的所有函数都可以使用这两个外部变量. extern int sp

C#学习笔记二:C#程序结构

从最简单的HelloWorld开始入手,这是一个最低限度的C#程序结构. C# Hello World 示例 一个C#程序主要由以下几部分组成: 命名空间声明 一个类 类方法 类属性 一个Main方法 语句和表达式 注释 先看看下面的示例,将打印字的简单的代码 "Hello World": using System; namespace HelloWorldApplication { class HelloWorld { static void Main(string[] args)

黑马程序员---C基础3【变量的易错】【程序结构】【if语句】【Switch语句】

------Java培训.Android培训.iOS培训..Net培训.期待与您交流! ------- [变量的易错] 1.变量为什么要初始化为0 int  sum,a=3: sum = sum+a 如果未初始化则会成为一个不确定的变量,结果也会不确定,容易出错. 2.不同类型的变量之间的转换 切记int  a=1,b=0:b=1-1.5:其中b为一个整型所有结果是保留整数部分的0,而不是-0.5,又因为0没有正负之分,所有保存结果为b=0: 3.关于Xcode的一个快速注释的插件 快捷键://

计算机病毒的定义、特征、程序结构、命名、传播与生命周期

一.定义:凡是人为编制的,干扰计算机正常运行并造成计算机软硬件故障, 甚至破坏计算机数据的可以自我复制的计算机程序或者指令集合 都是计算机病毒. 二.特征:非法性.隐藏性.潜伏性.可触发性.表现性.破坏性.传染性. 针对性.变异性.不可预见性. 隐藏性:缩小体积.潜入系统目录.标记坏簇.系统漏洞. 潜伏性:依附宿主程序伺机扩散. 破坏性:良性病毒.恶性病毒. 不可预见性:病毒超前于反病毒产品. 三.计算机病毒的程序结构 1> 引导部分:将病毒主题加载到内存,为传染部分做准备. 2> 传染部分:

C# 程序结构

C# 程序结构 在我们学习 C# 编程语言的基础构件块之前,让我们先看一下 C# 的最小的程序结构,以便作为接下来章节的参考. C# Hello World 实例 一个 C# 程序主要包括以下部分: 命名空间声明(Namespace declaration) 一个 class Class 方法 Class 属性 一个 Main 方法 语句(Statements)& 表达式(Expressions) 注释 让我们看一下上面程序的各个部分: 程序的第一行 using System; - using