mcu由于内部资源的限制,软件设计有其特殊性,程序一般没有复杂的算法以及数据结构,代码量也不大,通常不会使用OS(OperatingSystem),因为对于一个只有若干KROM,一百多byteRAM的mcu来说,一个简单OS也会吃掉大部分的资源。
对于无os的系统,流行的设计是主程序(主循环)+(定时)中断,这种结构虽然符合自然想法,不过却有很多不利之处,首先是中断可以在主程序的任何地方发生,随意打断主程序。其次主程序与中断之间的耦合性(关联度)较大,这种做法使得主程序与中断缠绕在一起,必须仔细处理以防不测。
那么换一种思路,如果把主程序全部放入(定时)中断中会怎么样?这么做至少可以立即看到几个好处:系统可以处于低功耗的休眠状态,将由中断唤醒进入主程序;如果程序跑飞,则中断可以拉回;没有了主从之分(其他中断另计),程序易于模块化。
(题外话:这种方法就不会有何处喂狗的说法,也没有中断是否应该尽可能的简短的争论了)
为了把主程序全部放入(定时)中断中,必须把程序化分成一个个的模块,即任务,每个任务完成一个特定的功能,例如扫描键盘并检测按键。设定一个合理的时基(tick),例如5,10或20ms,每次定时中断,把所有任务执行一遍,为减少复杂性,一般不做动态调度(最多使用固定数组以简化设计,做动态调度就接近os了),这实际上是一种无优先级时间片轮循的变种。来看看主程序的构成:
voidmain()
{
….//Initialize
while(true){
IDLE;//sleep
}
}
这里的IDLE是一条sleep指令,让mcu进入低功耗模式。中断程序的构成
voidTimer_Interrupt()
{
SetTimer();
ResetStack();
Enable_Timer_Interrupt;
….
进入中断后,首先重置Timer,这主要针对8051,8051自动重装分频器只有8-bit,难以做到长时间定时;复位stack,即把stack指针赋值为栈顶或栈底(对于pic,TIDSP等使用循环栈的mcu来说,则无此必要),用以表示与过去决裂,而且不准备返回到中断点,保证不会保留程序在跑飞时stack中的遗体。Enable_Timer_Interrupt也主要是针对8051。8051由于中断控制较弱,只有两级中断优先级,而且使用了如果中断程序不用reti返回,则不能响应同级中断这种偷懒方法,所以对于8051,必须调用一次reti来开放中断:
_Enable_Timer_Interrupt:
acall_reti
_reti:reti
下面就是任务的执行了,这里有几种方法。第一种是采用固定顺序,由于mcu程序复杂度不高,多数情况下可以采用这种方法:
…
Enable_Timer_Interrupt;
ProcessKey();
RunTask2();
…
RunTaskN();
while(1)IDLE;
可以看到中断把所有任务调用一遍,至于任务是否需要运行,由程序员自己控制。另一种做法是通过函数指针数组:
#defineCountOfArray(x)(sizeof(x)/sizeof(x[0]))
typedefvoid(*FUNCTIONPTR)();
constFUNCTIONPTR[]tasks={
ProcessKey,
RunTask2,
…
RunTaskN
};
voidTimer_Interrupt()
{
SetTimer();
ResetStack();
Enable_Timer_Interrupt;
for(i=0;i<CountOfArray(tasks),i++)
(*tasks
)();
while(1)IDLE;
}
使用const是让数组内容位于codesegment(ROM)而非datasegment(RAM)中,8051中使用code作为const的替代品。
(题外话:关于函数指针赋值时是否需要取地址操作符&的问题,与数组名一样,取决于compiler.对于熟悉汇编的人来说,函数名和数组名都是常数地址,无需也不能取地址。对于不熟悉汇编的人来说,用&取地址是理所当然的事情。VisualC++2005对此两者都支持)
这种方法在汇编下表现为散转,一个小技巧是利用stack获取跳转表入口:
movA,state
acallMultiJump
ajmpstate0
ajmpstate1
...
MultiJump:popDPH
popDPL
rlA
jmp@A+DPTR
还有一种方法是把函数指针数组(动态数组,链表更好,不过在mcu中不适用)放在datasegment中,便于修改函数指针以运行不同的任务,这已经接近于动态调度了:
FUNCTIONPTR[COUNTOFTASKS]tasks;
tasks[0]=ProcessKey;
tasks[0]=RunTaskM;
tasks[0]=NULL;
...
FUNCTIONPTRpFunc;
for(i=0;i<COUNTOFTASKS;i++){
pFunc=tasks);
if(pFunc!=NULL)
(*pFunc)();
}
通过上面的手段,一个中断驱动的框架形成了,下面的事情就是保证每个tick内所有任务的运行时间总和不能超过一个tick的时间。为了做到这一点,必须把每个任务切分成一个个的时间片,每个tick内运行一片。这里引入了状态机(statemachine)来实现切分。关于statemachine,很多书中都有介绍,这里就不多说了。
(题外话:实践升华出理论,理论再作用于实践。我很长时间不知道我一直沿用的方法就是statemachine,直到学习UML/C++,书中介绍tachniquesforidentifyingdynamicbehvior,方才豁然开朗。功夫在诗外,掌握C++,甚至C#JAVA,对理解嵌入式程序设计,会有莫大的帮助)
状态机的程序实现相当简单,第一种方法是用swich-case实现:
voidRunTaskN()
{
switch(state){
case0:state0();break;
case1:state1();break;
…
caseM:stateM();break;
default:
state=0;
}
}
另一种方法还是用更通用简洁的函数指针数组:
constFUNCTIONPTR[]states={state0,state1,…,stateM};
voidRunTaskN()
{
(*states[state])();
}
下面是statemachine控制的例子:
voidstate0(){}
voidstate1(){state++;}//nextstate;
voidstate2(){state+=2;}//gotostate4;
voidstate3(){state--;}//gotopreviousstate;
voidstate4(){delay=100;state++;}
voidstate5(){delay--;if(delay<=0)state++;}//delay100*tick
voidstate6(){state=0;}//gotothefirststate
一个小技巧是把第一个状态state0设置为空状态,即:
voidstate0(){}
这样,state=0可以让整个task停止运行,如果需要投入运行,简单的让state=1即可。
以下是一个键盘扫描的例子,这里假设tick=20ms,ScanKeyboard()函数控制口线的输出扫描,并检测输入转换为键码,利用每个state之间20ms的间隔去抖动。
enumEnumKey{
EnumKey_NoKey=0,
…
};
structStructKey{
intkeyValue;
boolkeyPressed;
};
structStructKeyProcesskey;
voidProcessKey(){(*states[state])();}
voidstate0(){}
voidstate1(){key.keyPressed=false;state++;}
voidstate2(){if(ScanKey()!=EnumKey_NoKey)state++;}//nextstateifakeypressed
voidstate3()
{//debouncingstate
key.keyValue=ScanKey();
if(key.keyValue==EnumKey_NoKey)
state--;
else{
key.keyPressed=true;
state++;
}
}
voidstate4(){if(ScanKey()==EnumKey_NoKey)state++;}//nextstateifthekeyreleased
voidstate5(){ScanKey()==EnumKey_NoKey?state=1:state--;}
上面的键盘处理过程显然比通常使用标志去抖的程序简洁清晰,而且没有软件延时去抖的困扰。以此类推,各个任务都可以划分成一个个的state,每个state实际上占用不多的处理时间。某些任务可以划分成若干个子任务,每个子任务再划分成若干个状态。
(题外话:对于常数类型,建议使用enum分类组织,避免使用大量#define定义常数)
对于一些完全不能分割,必须独占的任务来说,比如我以前一个低成本应用中红外遥控器的软件解码任务,这时只能牺牲其他的任务了。两种做法:一种是关闭中断,完全的独占;
voidRunTaskN()
{
Disable_Interrupt;
…
Enable_Interrupt;
}
第二种,允许定时中断发生,保证某些时基register得以更新;
voidTimer_Interrupt()
{
SetTimer();
Enable_Timer_Interrupt;
UpdateTimingRegisters();
if(watchDogCounter=0){
ResetStack();
for(i=0;i<CountOfArray(tasks),i++)
(*tasks)();
while(1)IDLE;
}
else
watchDogCounter--;
}
只要watchDogCounter不为0,那么中断正常返回到中断点,继续执行先前被中断的任务,否则,复位stack,重新进行任务循环。这种状况下,中断处理过程极短,对独占任务的影响也有限。
中断驱动多任务配合状态机的使用,我相信这是mcu下无os系统较好的设计结构。对于绝大多数mcu程序设计来说,可以极大的减轻程序结构的安排,无需过多的考虑各个任务之间的时间安排,而且可以让程序简洁易懂。缺点是,程序员必须花费一定的时间考虑如何切分任务。
下面是一段用C改写的CDPlayer中检测disc是否存在的伪代码,用以展示这种结构的设计技巧,原源代码为Z8mcu汇编,基于Sony的DSP,ServoandRF处理芯片,通过送出命令字来控制主轴/滑板/聚焦/寻迹电机,并读取状态以及CD的subQ码。这个处理任务只是一个大任务下用statemachine切开的一个二级子任务,tick=20ms。
state1(){InitializeMotor();state++;}
state2(){
if(innerSwitch!=ON){
SendCommand(EnumCommand_SlidingMotorBackward);
timeout=MILLISECOND(10000);
state++;//滑板电机向内运动,直至触及最内开关。
}
else
state+=2;
}
state3(){
if((--timeout)==0){//note:someCcompliersdonotsupport(--timeout)==
SendCommand(EnumCommand_SlidingMotorStop)
systemErrorCode=EnumErrorCode_InnerSwitch;
state=0;//10s超时错误,
}
else{
if(innerSwitch==ON){
SendCommand(EnumCommand_SlidingMotorStop)
timeout=MILLISECOND(200);//200ms电机停止时间
state++;
}
}
}
state4(){if((--timeout)==0)state++;}//等待电机完全停止
state5(){
SendCommand(EnumCommand_SlidingMotorForward);
timeout=MILLISECOND(2000);
state++;
}//滑板电机向外运动,脱离innerswitch
state6(){
if((--timeout)==0){
SendCommand(EnumCommand_SlidingMotorStop)
systemErrorCode=EnumErrorCode_InnerSwitch;
state=0;//2s超时错误,
}
else{
if(innerSwitch==OFF){
SendCommand(EnumCommand_SlidingMotorStop)
timeout=MILLISECOND(200);//200ms电机停止时间
state++;
}
}
}
state7(){state4();}
state8(){LaserOn();state++;retryCounter=3;}//打开激光器
state9(){
SendCommand(FocusUp);
state++;
timeout=MILLISECOND(2000);
}//光头上举,检测聚焦过零3次,判断cd是否存在
state10(){
if(FocusCrossZero){
systemStatus.Disc=EnumStatus_DiscExist;
SendCommand(EnumCommand_AutoFocusOn);//有cd,打开自动聚焦。
state=0;//本任务结束。
playProcess.state=1;//启动play任务
}
elseif((--timeout)==0){
SendCommand(EnumCommand_FocusClose);//光头聚焦复位
if((--retryCounter)==0){
systemStatus.Disc=EnumStatus_Nodisc;//无盘
displayProcess.state=EnumDisplayState_NoDisc;//显示闪烁的无盘
LaserOff();
state=0;//任务停止
}
else
state--;//再试
}
}
stateStop(){
SendCommand(EnumCommand_SlidingMotorStop);
SendCommand(EnumCommand_FocusClose);
state=0;
}
网友评论:^_^
网友评论:如果象LZ这样的高手被你一句话气跑了,岂不是让很多新手失去请教的机会?!!!
网友评论:为什么害死的是一只猫而不是一只老鼠或者别的???
网友评论:感谢楼主分享
网友评论:虽然很欣赏这种模式,可以降低功耗,但还是不愿意尝试
貌似中断服务程序还是简单点好
网友评论:不看好!
网友评论:处理一个跑马灯程序可能这样做可以。但是真的需要处理一些实时度要求高一点的程序就不可行了。
网友评论:这是典型的协作式,但是这样做不安全,容易丢失时间片。建议中断里面设置标志,把那一大套移到外面,这样更安全。
网友评论:不错!不错!!!!!!!!
网友评论:1.不是什么单片机都支持中断嵌套,咱的意思,能放到外面的就不要放到中断中,尽可能快的释放中断是中断机制的本意
2.低功耗啥的和处理时间有关,在终端中处理同样不低功耗
网友评论:学习学习!!!!!!!!!!!!!
网友评论:非常欢迎来自五湖四海的朋友,电子技术交流群:29303696
网友评论:还没有认真地看,不过,粗略地看一下有点像RTX51的办法。
RTX51就是用T0作时间中断计时器,主程序就可以是一个maintask,然后给每个任务分配一定的运行时间片,到时就中断执行别的任务,
不知道我说的对不对,我现在就新接手一个这样的项目,遇到的问题是每个任务的运行时间有比较严格的要求,不可以太长,想请问一下楼主你怎么预算一下每个task的执行时间?
网友评论:收藏
网友评论:中断复杂化与简约化并不矛盾,应用不同,灵活选择了
网友评论:没法细细看完,楼主的意思是不是这样的?(我不懂什么OS,操作系统)
假设每5MS中断一次,
中断一次,做第1件事,要在5MS内做完.做完空循环.
中断第2次,做第2件事,要在5MS内做完.做完,做完空循环.
中断第3次,做第3件事,要在5MS内做完.做完,做完空循环.
...
中断第N次,做第N件事,要在5MS内做完.做完,清除中断记数标记.做完空循环.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
我在没中断的系统中这样做:
计数器如果等于1,做第1件事,完成刚好200US,然后记数器加1.
计数器如果等于2,做第2件事,完成刚好200US,然后记数器加1.
计数器如果等于1,做第3件事,完成刚好200US,然后记数器加1.
...
计数器如果等于N,做第N件事,完成刚好200US,然后记数器清0.然后记数器加1.
网友评论:虽然我的观点和楼主不同,我支持能在主程序做的绝不用中断。例如按键去抖就用主程序周期,反正去抖有50%甚至更大的偏差都没关系。
不过还是支持一下楼主原创。
网友评论:IC库存管理软件第一品牌,您的最佳选择
要用就用“易管理”
“易管理”库存管理系统,轻松实现采购、销售、询价、客户关系管理,销售订单合同等信息一体化管理,轻松实现库存与贸易统一运行。这是在国内首次提出进销存贸易一体化的平台,这是领先全球的专享网络平台。
在激烈的市场竞争中,如何提升企业市场竞争力和自身形象,适应社会各方面发展的需求,以及如何应对新形势、新挑战?通过合理的企业信息化建设就可以让你的企业插上腾飞的翅膀,它不但可以让你的企业变得更高效,还可以让您的企业具有更高的竞争力!
随着信息技术的发展,决战IC市场,信息在IC行业起着举足轻重的作用,我们致力于专为IC行业的公司提供专业的全方位信息技术服务。
“易管理”,使您发现弊端,果断解决
我们分析您的管理烦恼,为您寻求解决方案
想实时掌握公司的运作情况吗?
想节约时间将更多精力用于提升业务竞争力吗?
想随时掌握客户的销售情况吗?
想更有效的掌握库存情况吗?
想掌握复杂工作流程,从容知道处理方案吗?
想排除加班处理繁杂财务工作的烦恼吗??
企业在管理中遇到的问题
1、供应商库存管理混乱!
2、商业秘密的供应商库存信息给员工轻易带走!
3、从前使用的库存管理系统,麻烦又不好用!
4、分公司数据无法共享!
5、询报价记录没有存底,潜在客户无法抓住!
。。。。。。。。。
我们拥有全国几千客户的优异口碑见证,帮助你实时准确的掌握公司库存、客户、财务,业务的状况。
联系人:陈明亮
电子邮件:cml@eguanli.com
电 话:+86-755-83910101839102028306712183067044
移动电话:13510964861
地 址:深圳市福田区深南中路6002号人民大厦5楼
网 址:http://www.iclook.cn
QQ:860736028
网友评论:想法不错.
网友评论:还是比较赞成所长的简约中断
网友评论:板凳
网友评论:好像有处笔误constFUNCTIONPTR[]tasks={
constFUNCTIONPTRtasks[]={
网友评论:不好理解,个人感觉:
时序的主次将完全颠倒;
而且系统可能变得很复杂;
是否放在中断中CPU运行的时间基本一样,低功耗从何而来?
主程序往往耗时都很长,时序岂不是要大乱?
中断中多安排一些例行性的动作,如显示,读健,发声等,这个是提倡的,但整个主程序搬过来,实在看不出什么好处。
网友评论:技术资料中心
汇集全球各大厂商的产品资料,从技术参数到应用笔记、解决方案、应用笔记等一应俱全,搜索方式灵活,各种技术资料相互关联,资源集成度极高;同时,为了帮助工程师获得更多新技术,ICBuy还将联合各技术代理商开展在线技术培训。
亿芯网,简称ICBuy,立足于通过互联网为电子行业的用户提供元器件相关信息服务,内容包括技术资料中心、工程样片零售商城、市场供货信息查询三个方面,为客户提供从技术开发到小批量采购以及量产供应商的选择等一系列产品和信息服务。
相关链接:http://www.icbuy.com/
网友评论:楼主实在是把一件简单的事情复杂化了。如果能合理应用“状态机”的概念,所谓的多任务处理就能轻松实现。
网友评论:不错,努力学习中
网友评论:这样能保证时钟的准确?你能保证中断里的任务能在这么短的时间内完成?假如你用了液晶,有刷写液晶任务,他所耗用的时间早已经够定时器中段多次了。用这种方式,只能是任务少,且耗时短。
假如使用了ps2接口,而此时正在定时器中断中处理任务,当ps2设备有请求时,无法响应,应此会错过这个响应。此类对响应要求苛刻的设备在这种系统中会很不可靠。
网友评论:虽然做过多年的单片机程序,还没有想过原来可以这样做,呵呵。。。
就像习惯了右手,却忽视了左手也可以做一些事情,建议大家学习LZ变相思考的做法,没准能想出更好的办法
与诸君共勉
网友评论:记号
网友评论:只是很多具体的情况是任务不多不复杂,这时所有任务都可以在中断中完成。。。
网友评论:感觉不是个很好的一个构架
当然了,比没有构架好
网友评论:偶也是这么做的,就是有的观点不敢苟同,个人认为,一个时间片已经足够程序执行了,想想12M晶振,平均每条语句算他2us,10ms一个时间片,可以执行5000条语句,在状态机的情况下并不是每条语句都执行到的,一般的项目一个状态执行不可能5000条语句,好!执行完跳出我可以让它休眠,降低耗电,更重要的是增加的抗干扰性!偶觉得开门狗拉回来没用,如果开门狗溢出,重启,需要利用状态机来返回,返回到原来执行的状态
网友评论:应用的场合要求实时性高,用个状态机机制好点。事件驱动,我不认为中断内做太多的动作,一般在中断做个时间标志或者处理一些响应程度高的动作。没有必要复杂化
网友评论:中断程序应该做尽可能少的事情。
前后台系统中,我喜欢这样。
voidmain(void)
{
while(1)
{
Sleep();
SystemCounter++;
switch(status)
{
//各个任务......
}
}
}
网友评论:学习了。多谢LS
网友评论:不过用中断要小心!
网友评论:超宏达科技是一家专业的电子元器件代理商(www.super-grand.net),主要是NXP、ATMEL、ST、FAIRCHILD、NS、MICROCHIP、Winbond、Rohm、JRC、SAMSUNG、IR、UTC的代理,公司强大的技术支持和电子商务平台,可以解决电子元器件BOM表一站式打样和在线询价,详细资料请登陆www.super-grand.net,电话:83219636传真:83261186
网友评论:
voidmain(void)
{
ucharbuffer=SmartOption[0];//SmartOption
system_init();
receiver_init();
while(1)
{
keyscan_thread();
key_handle_therad();
minder_thread();
RF_encode_thread();
idle();
};
}
网友评论:有点操作系统的味道了