搜索
bottom↓
回复: 26

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

[复制链接]

出0入0汤圆

发表于 2008-8-6 13:22:39 | 显示全部楼层 |阅读模式
原帖地址: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 了),这实际上是一种无优先级时间片轮循的变种。来看看主程序的构成:      

                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 也主要是针对8051。8051 由于中断控制较弱,只有两级中断优先级,而且使用了如果中断程序不用 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)();
                             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);
                          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 实现:

            void RunTaskN()
                {
                switch (state) {
                                case 0: state0(); break;
                                case 1: state1(); break;
                                …
                                case M: stateM(); break;
                                default:
                                        state = 0;
                               }
                }



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

const FUNCTIONPTR[] states = { state0, state1, …, stateM };
void RunTaskN()
{
(*states[state])();
}

下面是 state machine 控制的例子:

void state0() { }            
void state1() { state++; }   //  next state;
void state2() { state+=2; }   //  go to state 4;
void state3() { state--; }      //  go to previous state;
void state4() { delay = 100; state++; }
void state5() { delay--; if (delay <= 0) state++; }   //delay 100*tick
void state6() { state=0; }      //  go to the first state

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

                void state0() { }

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

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

                enum EnumKey {
                              EnumKey_NoKey =  0,
                              …
                             };

                struct StructKey {
                                  int                keyValue;
                                  bool                keyPressed;
                                 } ;

                struct StructKeyProcess key;

                void ProcessKey() { (*states[state])(); }   

                void state0() { }   
                void state1() { key.keyPressed = false; state++; }
                void state2() { if (ScanKey() != EnumKey_NoKey) state++; }  //next state if a key pressed
                void state3() {                                             //debouncing state
                               key.keyValue = ScanKey();
                               if (key.keyValue == EnumKey_NoKey)
                                   state--;
                               else {
                                   key.keyPressed = true;      
                                   state++;
                                   }               
                              }   
                void state4() {  if (ScanKey() == EnumKey_NoKey) state++; }  //next state if the key released
                void state5() {  ScanKey() == EnumKey_NoKey? state = 1 : state--; }

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

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

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

    void RunTaskN()
    {
                Disable_Interrupt;
                …
                Enable_Interrupt;
    }            

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

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

                state1() { InitializeMotor(); state++; }
                state2() {  
                          if (innerSwitch != ON) {
                             SendCommand(EnumCommand_SlidingMotorBackward);
                             timeout = MILLISECOND(10000);  
                             state++;                // 滑板电机向内运动, 直至触及最内开关。
                             }
                          else
                             state += 2;
                         }           
                state3() {
                          if ((--timeout) == 0) {   //note: some C compliers do not support (--timeout) ==
                             SendCommand(EnumCommand_SlidingMotorStop)
                             systemErrorCode = EnumErrorCode_InnerSwitch;
                             state = 0;    // 10 s 超时错误,
                             }
                          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++;
                         }                // 滑板电机向外运动,脱离inner switch
                state6() {
                          if ((--timeout) == 0) {     
                             SendCommand(EnumCommand_SlidingMotorStop)
                             systemErrorCode = EnumErrorCode_InnerSwitch;
                             state = 0;              // 2 s 超时错误,
                             }
                          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 任务
                             }
                          else if ((--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;
                          }

阿莫论坛20周年了!感谢大家的支持与爱护!!

一只鸟敢站在脆弱的枝条上歇脚,它依仗的不是枝条不会断,而是自己有翅膀,会飞。

出0入0汤圆

发表于 2008-8-6 17:03:15 | 显示全部楼层
有時間再仔細看下

出0入0汤圆

发表于 2008-8-6 21:04:20 | 显示全部楼层
帖子的内容和《时间触发嵌入式系统设计模式》书上写的完全一致,而且没有书上写的专业,强烈建议没有看过的朋友仔细看看,收获一定不小的。

出0入0汤圆

发表于 2008-8-22 07:25:17 | 显示全部楼层
呵呵,自从能现场写OS后,就没再用过状态机了.偶尔为了用以前写的代码才搞一下状态机.

出0入0汤圆

发表于 2008-8-22 07:53:30 | 显示全部楼层
在21上看过了

出0入0汤圆

发表于 2008-8-22 09:23:45 | 显示全部楼层
作记号....

出0入0汤圆

发表于 2008-8-22 13:31:37 | 显示全部楼层
2楼似乎很强,不敢小看

出0入0汤圆

发表于 2008-8-25 17:21:33 | 显示全部楼层
最后一段程序看不懂,帮顶下

出0入0汤圆

发表于 2008-8-26 22:16:57 | 显示全部楼层
和我的疯狂思路不谋而合:http://www.ouravr.com/bbs/bbs_content.jsp?bbs_sn=1390602&bbs_page_no=1&search_mode=3&search_text=thomasdu&bbs_id=9999 (见30楼)

呵呵,想起办法来都是殊途同归呀,不同的是,我没有做下去,眼前也没有需要,只是想想而已,sigh~

出0入0汤圆

发表于 2008-9-6 20:36:36 | 显示全部楼层
mark

出0入0汤圆

发表于 2008-9-12 09:35:24 | 显示全部楼层
嗯,看过之后还是有点意思

出75入4汤圆

发表于 2009-1-12 23:09:22 | 显示全部楼层
时间触发思路还好。

出0入0汤圆

发表于 2009-1-12 23:33:30 | 显示全部楼层
我就是用这种思路写程序的,不过最大的问题就是分时间片,我的一个时间片是200us,可以完成一次AD转换(TLC2551),但是算一次除法就比较费事了,一个除法就要占用一个时间片,总的来说这种方法在实时性上还是比较好的,但调试时最好有一台示波器配合观察时间片分配情况。

出0入0汤圆

发表于 2009-1-20 14:50:53 | 显示全部楼层
还是OS爽&nbsp;呵呵

不过我要在Mega64才能爽一把

M8&nbsp;M16&nbsp;我只能时间片的方式&nbsp;

出0入0汤圆

发表于 2009-2-23 21:25:10 | 显示全部楼层
不错,任务之间的通讯比较麻烦

出0入0汤圆

发表于 2009-2-23 21:51:21 | 显示全部楼层
没仔细看,有时间再看

出0入0汤圆

发表于 2009-2-23 22:12:05 | 显示全部楼层
mark

出0入0汤圆

发表于 2009-3-5 15:51:54 | 显示全部楼层
mark

出0入0汤圆

发表于 2009-3-30 10:03:45 | 显示全部楼层
mark先 慢慢看

出0入0汤圆

发表于 2009-4-10 01:31:07 | 显示全部楼层
感谢楼主分享

出0入0汤圆

发表于 2009-6-4 18:07:29 | 显示全部楼层
实际上,如果对 RTOS 的架构及其系统的运行方式有了真正的理解!
经过裁剪后也是可以在资源有限的 MCU 上实现 RTOS !

   
   包括任务管理,调度;
   邮箱;
   信号量;
   时间任务的管理,实现;

出0入0汤圆

发表于 2009-6-5 01:01:54 | 显示全部楼层
标记下

出0入0汤圆

发表于 2009-6-6 11:55:26 | 显示全部楼层
中断驱动多任务 在实时性要求严格的情况下,会造成致命的错误!

出0入0汤圆

发表于 2009-6-6 12:33:40 | 显示全部楼层
mark。

出0入0汤圆

发表于 2009-6-6 13:10:50 | 显示全部楼层
【12楼】 igoal  ”我就是用这种思路写程序的,不过最大的问题就是分时间片,我的一个时间片是200us“

     你很牛x 【200us 的时间片】 你想把 MCU 累死呀!

    按照你的方法,MCU 的主要任务就是 处理时间片! 这样反而让 MCU 处理其它任务的响应时间变长,得不偿失!

出0入0汤圆

发表于 2009-8-26 09:28:38 | 显示全部楼层
mark

出0入0汤圆

发表于 2009-12-4 12:37:00 | 显示全部楼层
看看
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-5-20 20:40

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表