查看: 4286|回复: 3

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

[复制链接]
  • TA的每日心情
    慵懒
    2018-3-28 17:24
  • 签到天数: 276 天

    连续签到: 1 天

    [LV.8]以坛为家I

    发表于 2015-8-6 14:09:55 | 显示全部楼层 |阅读模式
    分享到:
    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;

        }

    点评

    赞一个  发表于 2015-8-10 02:07

    评分

    参与人数 1与非币 +5 收起 理由
    loveeeboard + 5 三周年铜板双倍!

    查看全部评分

    回复

    使用道具 举报

  • TA的每日心情
    慵懒
    2018-3-28 17:24
  • 签到天数: 276 天

    连续签到: 1 天

    [LV.8]以坛为家I

    发表于 2015-8-6 14:12:49 | 显示全部楼层
    都是干货         
    回复 支持 反对

    使用道具 举报

  • TA的每日心情

    2023-7-25 22:49
  • 签到天数: 385 天

    连续签到: 1 天

    [LV.9]以坛为家II

    发表于 2015-8-10 16:27:41 | 显示全部楼层
    感谢分享。。。
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    发表于 2015-12-11 13:45:42 | 显示全部楼层
    不错,经验之谈
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /3 下一条



    手机版|小黑屋|与非网

    GMT+8, 2025-1-12 13:38 , Processed in 0.144483 second(s), 22 queries , MemCache On.

    ICP经营许可证 苏B2-20140176  苏ICP备14012660号-2   苏州灵动帧格网络科技有限公司 版权所有.

    苏公网安备 32059002001037号

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.