中断多任务+状态机 单片机软件结构设计

发布者:TranquilSoul最新更新时间:2015-09-23 来源: eefocus关键字:中断多任务  状态机  单片机  软件结构 手机看文章 扫描二维码
随时随地手机看文章
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

                                (*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 实现:

 

            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 即可。

 [page]

以下是一个键盘扫描的例子,这里假设 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

                                                                (*tasks[i])();

            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;

    }

关键字:中断多任务  状态机  单片机  软件结构 引用地址:中断多任务+状态机 单片机软件结构设计

上一篇:MSP430中断服务函数的定义
下一篇:单片机10种软件滤波程序

推荐阅读最新更新时间:2024-03-16 14:32

基于单片机的新型节能日光灯系统的设计
随着社会的发展和人口的增长,节能已成为一个重要的社会课题。日光灯是目前使用最为广泛的一种灯具,但同大多数灯具一样,一旦开启,无论外界光强多大,它们都只能发出单一光强的光,这造成了能源的浪费。针对这一现象,本文提出了基于单片机的新型日光灯系统,通过采集外界光强信息,采用AT89C51单片机控制日光灯输出光强的方式,使日光灯随外界光强的变化而自动调整照射光强,在满足使用者的用光要求的前提下,达到节约能源的目的。系统具有结构简单、可靠性高、成本低等特点,可广泛用于学校学习和家庭生活。 1 硬件电路组成及工作原理 1.1 系统硬件结构 系统构成如图1所示。系统分为光线采集、单片机控制和日光灯自动调整3部分。光线采集部分主要由光敏电阻
[单片机]
基于<font color='red'>单片机</font>的新型节能日光灯系统的设计
51单片机控制PWM信号实现直流电机转速控制的方法
设计中采用了专门的芯片组成了PWM信号的发生系统并且对PWM信号的原理、产生方法以及如何通过软件编程对PWM信号占空比进行调节从而控制其输入信号波形等均作了详细的阐述。另外本系统中使用了红外对管对直流电机的转速进行测量,经过整形电路后将测量值送到单片机,并且最终作为反馈值输入到单片机进行PID运算从而实现了对直流电机速度的控制。在软件方面,文章中详细介绍了PID运算程序初始化程序等的编写思路和具体的程序实现。 1 单片机最小系统:单片机最小系统由51单片机,晶振电路,复位电路,电源组成。大家都比较熟悉,这里不再赘述。 2 四位数码管显示:在应用系统中,设计要求不同,使用的LED显示器的位数也不同,因此就生产了位数,尺寸,型
[单片机]
51<font color='red'>单片机</font>控制PWM信号实现直流电机转速控制的方法
51单片机学习———2--LED闪烁+流水
LED闪烁实现 原理+代码 #include reg52.h sbit L1=P2^0; typedef unsigned char u8; typedef unsigned int u16; void delay(u16 i)//延时函数 { while(i--); } void main() { while(1) { L1=0; delay(30000);//max=65535 L1=1; delay(30000);//闪烁间隔约为270ms } } LED流水实现 原理+代码 #include reg52.h #include intrins.h #define A P2
[单片机]
两种MCU间的串行通信协议设计
ANLA,Rn;A与Rn中的值按位'与',结果送入A中 ANLA,direct;A与direct中的值按位'与',结果送入A中 ANLA,@Ri;A与间址寻址单元@Ri中的值按位'与',结果送入A中 ANLA,#data;A与立即数data按位'与',结果送入A中 ANLdirect,A;direct中值与A中的值按位'与',结果送入direct中 ANLdirect,#data;direct中的值与立即数data按位'与',结果送入direct中。 这几条指令的关键是知道什么是逻辑与。这里的逻辑与是指按位与
[单片机]
Luminary1美元入门级32位ARM微控制器
Luminary Micro公司近日宣布为其32位的Stellaris家族推出首批微控制器。该MCU的入门级售价仅1美元,同时也是首款在硅芯片中采用了ARM Cortex-M3处理器的MCU产品。此次推出的Stellaris产品包括LM3S101和LM3S102。 基于Stellaris微控制器的应用系统能够实现20倍的指令集兼容性能,跨越了20MHz Stellaris微控制器至千兆级的Cortex-A8处理器解决方案。 除Stellaris MCU外,Luminary还提供了开发套件,包括主板和子卡、外围驱动程序库、文档、原理图以及实例程序、全部电缆和跳线器等配件。开发套件还含有完整的评估软件与ARM硬件开发工具,以及集成
[新品]
国内首家车规级MCU研发团队成立,致力打破发达国家垄断
近日,中国首家主攻车规 MCU 的芯片研发团队 - 蜂驰高芯(天津)科技有限公司(以下简称“蜂驰高芯”)在中新天津生态城正式注册成立。注册资本 1 亿元,致力于高端进口芯片的国产替代。 蜂驰高芯计划通过六年三阶段的持续投入,打破国际车载半导体国产自主研发高性能车载芯片空白。据蜂驰高芯投资人介绍,他们将建设 MCU 研发实验室,从事中国车规级芯片设计研发。目前,实验室已进入设计阶段,如果政策允许,首批员工将从 3 月初陆续进驻,进行实验室搭建,预计 4 月中旬完工。 车规(适用于汽车电子元件的规格标准)芯片是现代汽车工业不可或缺的重要关键零部件之一,目前全球每年 车规芯片 产品销售额约 700 亿美元。中国市场汽车产
[嵌入式]
国内首家车规级<font color='red'>MCU</font>研发团队成立,致力打破发达国家垄断
单片机】调试稳定的三极管一键开关机电路(附详细讲解
先上一个低功耗的一键开关机电路,这个电路的特点在于关机时所有三极管全部截止几乎不耗电。 原理很简单: 利用Q10的输出与输入状态相反(非门)特性和电容的电流积累特性。刚上电时Q6和Q10的发射结均被10K电阻短路所以Q6和Q10均截止,此时实测电路耗电流仅为0.1uA,L_out输出高,H_out输出低。此时C3通过R22缓慢充电最终等于VCC电压,当按下S3后C3通过R26给Q10基极放电,Q10迅速饱和,Q6也因此饱和,H_out变为高电平,当C3放电到Q10be结压降0.7V左右时C3不再放电,此时若按键弹开C3将进一步放电到Q10的饱和压降0.3V左右,当再次按下S3,Q10即截止。 这个电路可以完美解决
[单片机]
【<font color='red'>单片机</font>】调试稳定的三极管一键开关机电路(附详细讲解
基于单片机与旋转编码器的闭环线速度控制系统
O 引言   在电缆生产线上,通常需要检测电缆的走线速度,用来控制收线电机的转速和计算线缆的长度。成缆工艺参数的稳定,直接关系到   该项目中,采用的 旋转编码器的型号是TRDJ1000系列,旋转一周输出1 000个脉冲。因此,根据在一定时间内检测到的脉冲数,就可以计算出电缆的走线速度。实际应用中,将其与一加工精度极高、周长为500 mm的旋转编码器测量主动轮与旋转编码器同轴安装,主动轮与电缆接触。在电缆生产运动过程中,依靠摩擦力拉动测量轮旋转,这样就把电缆的直线位移(长度)转化为旋转编码器的脉冲数字信号输出。   设旋转编码器每旋转一周,其计数脉冲个数为NP(脉冲个数/转),则旋转编码器角分辨率(单位:(°)/个
[单片机]
基于<font color='red'>单片机</font>与旋转编码器的闭环线速度控制系统
小广播
添点儿料...
无论热点新闻、行业分析、技术干货……
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved