中断驱动多任务单片机MCU下的一种软件设计结构.docx

上传人:b****8 文档编号:10286790 上传时间:2023-02-09 格式:DOCX 页数:11 大小:20.93KB
下载 相关 举报
中断驱动多任务单片机MCU下的一种软件设计结构.docx_第1页
第1页 / 共11页
中断驱动多任务单片机MCU下的一种软件设计结构.docx_第2页
第2页 / 共11页
中断驱动多任务单片机MCU下的一种软件设计结构.docx_第3页
第3页 / 共11页
中断驱动多任务单片机MCU下的一种软件设计结构.docx_第4页
第4页 / 共11页
中断驱动多任务单片机MCU下的一种软件设计结构.docx_第5页
第5页 / 共11页
点击查看更多>>
下载资源
资源描述

中断驱动多任务单片机MCU下的一种软件设计结构.docx

《中断驱动多任务单片机MCU下的一种软件设计结构.docx》由会员分享,可在线阅读,更多相关《中断驱动多任务单片机MCU下的一种软件设计结构.docx(11页珍藏版)》请在冰豆网上搜索。

中断驱动多任务单片机MCU下的一种软件设计结构.docx

中断驱动多任务单片机MCU下的一种软件设计结构

中断驱动多任务---单片机(MCU)下的一种软件设计结构

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

                                (*tasks[i])();

         while

(1)IDLE;

}使用const是让数组内容位于codesegment(ROM)而非datasegment(RAM)中,8051中使用code作为const的替代品。

(题外话:

关于函数指针赋值时是否需要取地址操作符&的问题,与数组名一样,取决于compiler.对于熟悉汇编的人来说,函数名和数组名都是常数地址,无需也不能取地址。

对于不熟悉汇编的人来说,用&取地址是理所当然的事情。

VisualC++2005对此两者都支持)

这种方法在汇编下表现为散转,一个小技巧是利用stack获取跳转表入口:

                                    mov                A,state

                                             acall                MultiJump

                                             ajmp               state0

                                             ajmp               state1

                                    ...

 

MultiJump:

                  pop                DPH

                                 pop                DPL

                                 rl                    A

                                 jmp                @A+DPTR

还有一种方法是把函数指针数组(动态数组,链表更好,不过在mcu中不适用)放在datasegment中,便于修改函数指针以运行不同的任务,这已经接近于动态调度了:

FUNCTIONPTR[COUNTOFTASKS]tasks;

                tasks[0]=ProcessKey;

                tasks[0]=RunTaskM;

                tasks[0]=NULL;

                             ...

                            FUNCTIONPTRpFunc;

                for(i=0;i

                          pFunc=tasks[i]);

                          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{

                                int                keyValue;

                                bool                keyPressed;

    };

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

                                                                (*tasks[i])();

            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);        

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 求职职场 > 简历

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1