测·控领域专业互动媒体平台
推动测试测量,检测诊断,传感物联,遥测自控智能化发展
  1. 电子测量网 电气测控网
  2. 无损检测网 实验分析网
  3. 遥测导航网 数据采集网
  4. 检测诊断网 虚拟仪器网
当前位置:CK365鸿运国际 > 技术中心 > 相关知识 >

单片机设计新思路:把主程序放入中断如何?

玩转单片机 2017-11-21 10:58:41 [导读] mcu由于内部资源的限制,软件设计有其特殊性,程序一般没有复杂的算法以及数据结构,代码量也不大, 通常不会使用 OS (Operating System), 因为对于一个只有 若干K ROM, 一百多byte RAM 的 mcu 来说,一个简单OS 也会吃掉大部分的资源。
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获取跳转表入口:   movA, state acallMultiJump ajmpstate0 ajmpstate1 ... MultiJump: popDPH popDPL rlA [email protected]+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即可。 以下是一个键盘扫描的例子,这里假设tick = 20 ms, ScanKeyboard()函数控制口线的输出扫描,并检测输入转换为键码,利用每个state之间20 ms的间隔去抖动。 enum EnumKey { EnumKey_NoKey =0, }; struct StructKey { intkeyValue; boolkeyPressed; } ; 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; }
[整理编辑:鸿运国际]
标签:  中断系统[0]
 
[ 技术中心搜索 ]  [ ]  [ 好友分享 ]  [ 打印本文 ]  [ 关闭窗口 ]  [ 返回顶部 ]

版权与免责声明:

①凡本网注明"来源:鸿运国际"的所有作品,版权均属于鸿运国际,转载请必须注明鸿运国际 www.ck365.cn。违反者本网将追究相关法律责任。
②本网转载并注明自其它来源的作品,目的在于传递更多信息,并不代表本网赞同其观点或证实其内容的真实性,不承担此类作品侵权行为的直接责任及连带责任。如其他媒体、网站或个人从本网下载使用,必须保留本网注明的"稿件来源",并自负版权等法律责任。
③如涉及作品内容、版权等问题,请在作品发表之日起一周内与本网联系,否则视为放弃相关权利。
注册成为鸿运国际会员 可以无需任何费用浏览专业技术文章  
 
注册鸿运国际会员以便浏览全文
 
  1. 应用技术
  2. 设计技术
  3. 解决方案
  4. 专业论文
  5. 典型应用
  6. 相关知识
  7.  
  1. 标签:
  2.   中断系统[0]
  3.  
 
鸿运国际娱乐欢迎你