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

http://blog.csdn.net/Akron/archive/2008/08/01/2755643.aspx

mcu由于内部资源的限制,软件设计有其特殊性,程序一般没有复杂的算法以及数据结构,代码量也不大, 通常不会使用 OS (Operating System),  因为对于一个只有 若干K ROM, 一百多byte RAM mcu 来说,一个简单OS  也会吃掉大部分的资源。

 

对于无 os 的系统,流行的设计是主程序(主循环 ) + (定时)中断,这种结构虽然符合自然想法,不过却有很多不利之处,首先是中断可以在主程序的任何地方发生,随意打断主程序。其次主程序与中断之间的耦合性(关联度)较大,这种做法 使得主程序与中断缠绕在一起,必须仔细处理以防不测。

 

那么换一种思路,如果把主程序全部放入(定时)中断中会怎么样?这么做至少可以立即看到几个好处: 系统可以处于低功耗的休眠状态,将由中断唤醒进入主程序; 如果程序跑飞,则中断可以拉回;没有了主从之分(其他中断另计),程序易于模块化。

 

(题外话:这种方法就不会有何处喂狗的说法,也没有中断是否应该尽可能的简短的争论了)

 

为了把主程序全部放入(定时)中断中,必须把程序化分成一个个的模块,即任务,每个任务完成一个特定的功能,例如扫描键盘并检测按键。 设定一个合理的时基 (tick), 例如  5, 10 20 ms,  每次定时中断,把所有任务执行一遍,为减少复杂性,一般不做动态调度(最多使用固定数组以简化设计,做动态调度就接近 os 了),这实际上是一种无优先级时间片轮循的变种。来看看主程序的构成:

Code:
  1. void main()  
  2.   
  3.       {  
  4.   
  5.          ….   // Initialize  
  6.   
  7.          while (true) {  
  8.   
  9.                       IDLE;     //sleep  
  10.   
  11.          }  
  12.   
  13.       }  

这里的 IDLE 是一条sleep 指令,让 mcu 进入低功耗模式。中断程序的构成

Code:
  1. void Timer_Interrupt()  
  2.   
  3. {  
  4.   
  5.                  SetTimer();  
  6.   
  7.                  ResetStack();  
  8.   
  9.                  Enable_Timer_Interrupt;  
  10.   
  11.                  ….  

进入中断后,首先重置Timer, 这主要针对8051, 8051 自动重装分频器只有 8-bit, 难以做到长时间定时;复位 stack ,即把stack 指针赋值为栈顶或栈底(对于 pic TI DSP 等使用循环栈的 mcu 来说,则无此必要),用以表示与过去决裂,而且不准备返回到中断点,保证不会保留程序在跑飞时stack 中的遗体。Enable_Timer_Interrupt 也主要是针对80518051 由于中断控制较弱,只有两级中断优先级,而且使用了如果中断程序不用 reti 返回,则不能响应同级中断这种偷懒方法,所以对于 8051, 必须调用一次 reti 来开放中断:

Code:
  1. _Enable_Timer_Interrupt:  
  2.   
  3.                    acall       _reti  
  4.   
  5.     _reti:        reti            

下面就是任务的执行了,这里有几种方法。第一种是采用固定顺序,由于mcu 程序复杂度不高,多数情况下可以采用这种方法:

Code:
  1. …  
  2.   
  3.                 Enable_Timer_Interrupt;  
  4.   
  5.                 ProcessKey();  
  6.   
  7.                 RunTask2();  
  8.   
  9.                 …  
  10.   
  11.                 RunTaskN();  
  12.   
  13.                 while (1) IDLE;  

可以看到中断把所有任务调用一遍,至于任务是否需要运行,由程序员自己控制。另一种做法是通过函数指针数组:

Code:
  1. define CountOfArray(x) (sizeof(x)/sizeof(x[0]))  
  2.   
  3. typedef void (*FUNCTIONPTR)();  
  4.   
  5.    
  6.   
  7. const FUNCTIONPTR[] tasks = {  
  8.   
  9. ProcessKey,  
  10.   
  11. RunTask2,  
  12.   
  13. …  
  14.   
  15. RunTaskN  
  16.   
  17. };  
  18.   
  19.    
  20.   
  21.                 void Timer_Interrupt()  
  22.   
  23.                 {  
  24.   
  25.                                  SetTimer();  
  26.   
  27.                                  ResetStack();  
  28.   
  29.                                  Enable_Timer_Interrupt;  
  30.   
  31.                      for (i=0; i<CountOfArray (tasks), i++)  
  32.   
  33.                                 (*tasks[i])();  
  34.   
  35.          while (1) IDLE;  
  36.   
  37. }  
  38.   
  39.    

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

 

(题外话:关于函数指针赋值时是否需要取地址操作符 & 的问题,与数组名一样,取决于 compiler. 对于熟悉汇编的人来说,函数名和数组名都是常数地址,无需也不能取地址。对于不熟悉汇编的人来说,用 & 取地址是理所当然的事情。Visual C++ 2005对此两者都支持)

 

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

Code:
  1. mov                A, state  
  2.   
  3.                                       acall                MultiJump  
  4.   
  5.                                       ajmp               state0  
  6.   
  7.                                       ajmp               state1  
  8.   
  9.                              ...  
  10.   
  11.   
  12.   
  13. mp:                  pop                DPH  
  14.   
  15.                           pop                DPL  
  16.   
  17.                           rl                    A  
  18.   
  19.                           jmp                @A+DPTR  

 

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

Code:
  1. FUNCTIONPTR[COUNTOFTASKS] tasks;  
  2.   
  3.    
  4.   
  5.                 tasks[0] = ProcessKey;  
  6.   
  7.                 tasks[0] = RunTaskM;  
  8.   
  9.                 tasks[0] = NULL;  
  10.   
  11.    
  12.   
  13.                              ...  
  14.   
  15.                             FUNCTIONPTR pFunc;  
  16.   
  17.                 for (i=0; i< COUNTOFTASKS; i++)  {  
  18.   
  19.                           pFunc = tasks[i]);  
  20.   
  21.                           if (pFunc != NULL)  
  22.   
  23.                                       (*pFunc)();  
  24.   
  25.                 }  

mcu由于内部资源的限制,软件设计有其特殊性,程序一般没有复杂的算法以及数据结构,代码量也不大, 通常不会使用 OS (Operating System),  因为对于一个只有 若干K ROM, 一百多byte RAM mcu 来说,一个简单OS  也会吃掉大部分的资源。

 

对于无 os 的系统,流行的设计是主程序(主循环 ) + (定时)中断,这种结构虽然符合自然想法,不过却有很多不利之处,首先是中断可以在主程序的任何地方发生,随意打断主程序。其次主程序与中断之间的耦合性(关联度)较大,这种做法 使得主程序与中断缠绕在一起,必须仔细处理以防不测。

 

那么换一种思路,如果把主程序全部放入(定时)中断中会怎么样?这么做至少可以立即看到几个好处: 系统可以处于低功耗的休眠状态,将由中断唤醒进入主程序; 如果程序跑飞,则中断可以拉回;没有了主从之分(其他中断另计),程序易于模块化。

 

(题外话:这种方法就不会有何处喂狗的说法,也没有中断是否应该尽可能的简短的争论了)

 

为了把主程序全部放入(定时)中断中,必须把程序化分成一个个的模块,即任务,每个任务完成一个特定的功能,例如扫描键盘并检测按键。 设定一个合理的时基 (tick), 例如  5, 10 20 ms,  每次定时中断,把所有任务执行一遍,为减少复杂性,一般不做动态调度(最多使用固定数组以简化设计,做动态调度就接近 os 了),这实际上是一种无优先级时间片轮循的变种。来看看主程序的构成:

     

                void main()

                {

                   ….   // Initialize

                   while (true) {

                                IDLE     //sleep

                   }

                }

 

这里的 IDLE 是一条sleep 指令,让 mcu 进入低功耗模式。中断程序的构成

 

                void Timer_Interrupt()

                {

                                 SetTimer();

                                 ResetStack();

                                 Enable_Timer_Interrupt;

                                 ….

 

 

进入中断后,首先重置Timer, 这主要针对8051, 8051 自动重装分频器只有 8-bit, 难以做到长时间定时;复位 stack ,即把stack 指针赋值为栈顶或栈底(对于 pic TI DSP 等使用循环栈的 mcu 来说,则无此必要),用以表示与过去决裂,而且不准备返回到中断点,保证不会保留程序在跑飞时stack 中的遗体。Enable_Timer_Interrupt 也主要是针对80518051 由于中断控制较弱,只有两级中断优先级,而且使用了如果中断程序不用 reti 返回,则不能响应同级中断这种偷懒方法,所以对于 8051, 必须调用一次 reti 来开放中断:

 

                 _Enable_Timer_Interrupt:

                                acall       _reti

                 _reti:        reti         

       

下面就是任务的执行了,这里有几种方法。第一种是采用固定顺序,由于mcu 程序复杂度不高,多数情况下可以采用这种方法:

 

                Enable_Timer_Interrupt;

                ProcessKey();

                RunTask2();

               

                RunTaskN();

                while (1) IDLE

 

可以看到中断把所有任务调用一遍,至于任务是否需要运行,由程序员自己控制。另一种做法是通过函数指针数组:

 

                #define CountOfArray(x) (sizeof(x)/sizeof(x[0]))

typedef void (*FUNCTIONPTR)();

 

const FUNCTIONPTR[] tasks = {

ProcessKey,

RunTask2

RunTaskN

};

 

                void Timer_Interrupt()

                {

                                 SetTimer();

                                 ResetStack();

                                 Enable_Timer_Interrupt;

                     for (i=0; i<CountOfArray (tasks), i++)

                                (*tasks[i])();

         while (1) IDLE

}

 

 

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

 

(题外话:关于函数指针赋值时是否需要取地址操作符 & 的问题,与数组名一样,取决于 compiler. 对于熟悉汇编的人来说,函数名和数组名都是常数地址,无需也不能取地址。对于不熟悉汇编的人来说,用 & 取地址是理所当然的事情。Visual C++ 2005对此两者都支持)

 

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

           

                                    mov                A, state

                                             acall                MultiJump

                                             ajmp               state0

                                             ajmp               state1

                                    ...

 

MultiJump:                  pop                DPH

                                 pop                DPL

                                 rl                    A

                                 jmp                @A+DPTR

 

 

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

 

FUNCTIONPTR[COUNTOFTASKS] tasks;

 

                tasks[0] = ProcessKey;

                tasks[0] = RunTaskM;

                tasks[0] = NULL;

 

                             ...

                            FUNCTIONPTR pFunc;

                for (i=0; i< COUNTOFTASKS; i++)  {

                          pFunc = tasks[i]);

                          if (pFunc != NULL)

                                      (*pFunc)();

                }

 

 

通过上面的手段,一个中断驱动的框架形成了,下面的事情就是保证每个 tick 内所有任务的运行时间总和不能超过一个 tick 的时间。为了做到这一点,必须把每个任务切分成一个个的时间片,每个 tick 内运行一片。这里引入了状态机 (state machine) 来实现切分。关于 state machine,  很多书中都有介绍, 这里就不多说了。

 

(题外话:实践升华出理论,理论再作用于实践。我很长时间不知道我一直沿用的方法就是state machine,直到学习UML/C++,书中介绍 tachniques for identifying dynamic behvior,方才豁然开朗。功夫在诗外,掌握 C++, 甚至C# JAVA, 对理解嵌入式程序设计,会有莫大的帮助)

 

状态机的程序实现相当简单,第一种方法是用 swich-case 实现:

Code:
  1.   void RunTaskN()  
  2.   
  3.                 {  
  4.   
  5.                 switch (state) {  
  6.   
  7.                                 case 0: state0(); break;  
  8.   
  9.                                 case 1: state1(); break;  
  10.   
  11.                                 …  
  12.   
  13.                                 case M: stateM(); break;  
  14.   
  15.                                 default:  
  16.   
  17.                                                 state = 0;  
  18.   
  19.                 }  
  20.   
  21. }  
  22.   
  23.    

 

另一种方法还是用更通用简洁的函数指针数组:

Code:
  1. const FUNCTIONPTR[] states = { state0, state1, …, stateM };  
  2.   
  3.    
  4.   
  5. void RunTaskN()  
  6.   
  7. {  
  8.   
  9. (*states[state])();  
  10.   
  11. }  

下面是 state machine 控制的例子:

Code:
  1. void state0() { }              
  2.   
  3. void state1() { state++; }   //  next state;  
  4.   
  5. void state2() { state+=2; }   //  go to state 4;  
  6.   
  7. void state3() { state--; }      //  go to previous state;  
  8.   
  9. void state4() { delay = 100; state++; }  
  10.   
  11. void state5() { delay--; if (delay <= 0) state++; }   //delay 100*tick  
  12.   
  13. void state6() { state=0; }      //  go to the first state  

一个小技巧是把第一个状态 state0 设置为空状态,即:

Code:
  1. void state0() { }  

这样,state =0可以让整个task 停止运行,如果需要投入运行,简单的让 state = 1 即可。

 

以下是一个键盘扫描的例子,这里假设 tick = 20 ms, ScanKeyboard() 函数控制口线的输出扫描,并检测输入转换为键码,利用每个state 之间 20 ms 的间隔去抖动。

Code:
  1.     enum EnumKey {  
  2.   
  3. EnumKey_NoKey =  0,  
  4.   
  5. …  
  6.   
  7.     };  
  8.   
  9.                 struct StructKey {  
  10.   
  11.                                 int                keyValue;  
  12.   
  13.                                 bool                keyPressed;  
  14.   
  15.     } ;  
  16.   
  17.    
  18.   
  19. struct StructKeyProcess key;  
  20.   
  21.    
  22.   
  23. void ProcessKey() { (*states[state])(); }                
  24.   
  25.    
  26.   
  27.                 void state0() { }              
  28.   
  29.                 void state1() { key.keyPressed = false; state++; }  
  30.   
  31.                 void state2() { if (ScanKey() != EnumKey_NoKey) state++; }  //next state if a key pressed  
  32.   
  33.                 void state3()  
  34.   
  35.     {                                                               //debouncing state  
  36.   
  37.                                 key.keyValue = ScanKey();  
  38.   
  39.                                 if (key.keyValue == EnumKey_NoKey)  
  40.   
  41.                                                 state--;  
  42.   
  43.                                 else {  
  44.   
  45.                                                 key.keyPressed = true;        
  46.   
  47.                                                 state++;  
  48.   
  49.                                 }                  
  50.   
  51.     }    
  52.   
  53.     void state4() {  if (ScanKey() == EnumKey_NoKey) state++; }  //next state if the key released  
  54.   
  55.                 void state5() {  ScanKey() == EnumKey_NoKey? state = 1 : state--; }  
  56.   
  57.    

 

上面的键盘处理过程显然比通常使用标志去抖的程序简洁清晰,而且没有软件延时去抖的困扰。以此类推,各个任务都可以划分成一个个的state, 每个state 实际上占用不多的处理时间。某些任务可以划分成若干个子任务,每个子任务再划分成若干个状态。

 

(题外话:对于常数类型,建议使用 enum 分类组织,避免使用大量 #define 定义常数)

 

对于一些完全不能分割,必须独占的任务来说,比如我以前一个低成本应用中红外遥控器的软件解码任务,这时只能牺牲其他的任务了。两种做法:一种是关闭中断,完全的独占;

Code:
  1. void RunTaskN()  
  2.   
  3.    {  
  4.   
  5.                Disable_Interrupt;  
  6.   
  7.                …  
  8.   
  9.                Enable_Interrupt;  
  10.   
  11.    }             

 

第二种,允许定时中断发生,保证某些时基 register 得以更新;

Code:
  1. void Timer_Interrupt()  
  2.   
  3.                 {  
  4.   
  5.                                 SetTimer();  
  6.   
  7.                                 Enable_Timer_Interrupt;  
  8.   
  9.                                 UpdateTimingRegisters();  
  10.   
  11.                                 if (watchDogCounter = 0) {  
  12.   
  13.                                                ResetStack();  
  14.   
  15.                                                 for (i=0; i<CountOfArray (tasks), i++)  
  16.   
  17.                                                                 (*tasks[i])();  
  18.   
  19.             while (1) IDLE;  
  20.   
  21.         }  
  22.   
  23.         else  
  24.   
  25.                 watchDogCounter--;            
  26.   
  27.     }  

只要watchDogCounter 不为 0,那么中断正常返回到中断点,继续执行先前被中断的任务,否则,复位 stack, 重新进行任务循环。这种状况下,中断处理过程极短,对独占任务的影响也有限。

 

中断驱动多任务配合状态机的使用,我相信这是mcu 下无os 系统较好的设计结构。对于绝大多数 mcu 程序设计来说,可以极大的减轻程序结构的安排,无需过多的考虑各个任务之间的时间安排,而且可以让程序简洁易懂。缺点是,程序员必须花费一定的时间考虑如何切分任务。

 

下面是一段用 C 改写的CD Player 中检测 disc 是否存在的伪代码,用以展示这种结构的设计技巧,原源代码为Z8 mcu 汇编, 基于 Sony DSP, Servo and RF 处理芯片, 通过送出命令字来控制主轴/滑板/聚焦/寻迹电机,并读取状态以及 CD sub Q 码。这个处理任务只是一个大任务下用state machine切开的一个二级子任务,tick = 20 ms

Code:
  1.   state1() { InitializeMotor(); state++; }  
  2.   
  3.                 state2() {   
  4.   
  5. if (innerSwitch != ON) {  
  6.   
  7. SendCommand(EnumCommand_SlidingMotorBackward);  
  8.   
  9. timeout = MILLISECOND(10000);   
  10.   
  11. state++;                // 滑板电机向内运动, 直至触及最内开关。  
  12.   
  13. }  
  14.   
  15. else  
  16.   
  17.             state +=                2;  
  18.   
  19.     }                
  20.   
  21.                 state3() {  
  22.   
  23.                                 if ((--timeout) == 0) {   //note: some C compliers do not support (--timeout) ==  
  24.   
  25.                                                 SendCommand(EnumCommand_SlidingMotorStop)  
  26.   
  27.                                                 systemErrorCode = EnumErrorCode_InnerSwitch;  
  28.   
  29.                                                 state = 0;    // 10 s 超时错误,  
  30.   
  31.         }  
  32.   
  33.         else {  
  34.   
  35.                 if (innerSwitch == ON) {  
  36.   
  37.                                                         SendCommand(EnumCommand _SlidingMotorStop)  
  38.   
  39.                                 timeout = MILLISECOND(200);                  // 200ms电机停止时间   
  40.   
  41.                                 state++;  
  42.   
  43.                 }  
  44.   
  45.    
  46.   
  47. }  
  48.   
  49.     }  
  50.   
  51.                 state4() { if ((--timeout) == 0) state++; }                  //等待电机完全停止  
  52.   
  53.                 state5() {   
  54.   
  55. SendCommand(EnumCommand_SlidingMotorForward);  
  56.   
  57. timeout = MILLISECOND(2000);   
  58.   
  59. state++;  
  60.   
  61. }                // 滑板电机向外运动,脱离inner switch  
  62.   
  63.    
  64.   
  65.                 state6() {  
  66.   
  67.                                 if ((--timeout) == 0) {      
  68.   
  69.                                                 SendCommand(EnumCommand_SlidingMotorStop)  
  70.   
  71.                                                 systemErrorCode = EnumErrorCode_InnerSwitch;  
  72.   
  73.                                                 state = 0;              // 2 s 超时错误,  
  74.   
  75. }  
  76.   
  77. else {  
  78.   
  79.                 if (innerSwitch == OFF) {  
  80.   
  81.                                                         SendCommand(EnumCommand_SlidingMotorStop)  
  82.   
  83.                                 timeout = MILLISECOND(200);                  // 200ms电机停止时间   
  84.   
  85.                                 state++;  
  86.   
  87.                 }  
  88.   
  89. }  
  90.   
  91.                 }  
  92.   
  93.                 state7() { state4(); }   
  94.   
  95.                 state8() { LaserOn(); state++; retryCounter = 3;}                 //打开激光器  
  96.   
  97.                 state9() {  
  98.   
  99. SendCommand(FocusUp);  
  100.   
  101. state++;   
  102.   
  103. timeout = MILLISECOND(2000);  
  104.   
  105.     }                  //光头上举,检测聚焦过零 3 次,判断cd 是否存在  
  106.   
  107.                  
  108.   
  109.                 state10() {  
  110.   
  111.                                 if (FocusCrossZero)  {  
  112.   
  113.                                                 systemStatus.Disc = EnumStatus_DiscExist;     
  114.   
  115.                                                 SendCommand(EnumCommand_AutoFocusOn);    //有cd, 打开自动聚焦。  
  116.   
  117.                                     state = 0;                             //本任务结束。  
  118.   
  119.                                     playProcess.state = 1;                //启动 play 任务  
  120.   
  121.                                 }  
  122.   
  123.                                 else if ((--timeout) == 0) {  
  124.   
  125.                                                 SendCommand(EnumCommand_ FocusClose);                  //光头聚焦复位  
  126.   
  127.                                                 if ((--retryCounter) == 0) {  
  128.   
  129.                                                                 systemStatus.Disc = EnumStatus_Nodisc;       //无盘  
  130.   
  131.                                                                 displayProcess.state = EnumDisplayState_NoDisc;  //显示闪烁的无盘    
  132.   
  133.                                                                 LaserOff();  
  134.   
  135.                                                                 state = 0;                //任务停止  
  136.   
  137.             }  
  138.   
  139.             else  
  140.   
  141.                             state--;                                 //再试                 
  142.   
  143.         }  
  144.   
  145.                 }  
  146.   
  147.    
  148.   
  149.     stateStop() {  
  150.   
  151.                 SendCommand(EnumCommand_SlidingMotorStop);  
  152.   
  153.     SendCommand(EnumCommand_FocusClose);   
  154.   
  155.     state = 0;  
  156.   
  157.     }  
  158.   
  159.    
  160.   
  161.                           

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值