0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

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

GReq_mcu168 来源:互联网 作者:佚名 2017-11-20 18:27 次阅读

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

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

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

}

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 中断系统
    +关注

    关注

    1

    文章

    96

    浏览量

    60881
  • 单片机系统
    +关注

    关注

    1

    文章

    73

    浏览量

    103668

原文标题:把主程序放入中断如何?单片机的一种软件设计新思路

文章出处:【微信号:mcu168,微信公众号:硬件攻城狮】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    关于单片机C语言编程中,中断函数对主程序的影响

    具体问题的描述:倘若我使用ADC0809作为模数转换的芯片,我们知道这种AD芯片是要提供时钟信号的,倘若我通过单片机计时中断的方式提供时钟,那么在主程序执行的时候或者AD芯片转换的时候,这个提供时钟信号的
    发表于 10-13 09:28

    状态机思路单片机程序设计中的应用

    状态机思路单片机程序设计中的应用 状态机的概念状态机是软件编程中的一个重要概念。比这个概念更重要的是对它的灵活应用。在一个思路清晰而且高效的程序
    发表于 02-09 11:25 1w次阅读
    状态机<b class='flag-5'>思路</b>在<b class='flag-5'>单片机</b><b class='flag-5'>程序</b>设计中的应用

    使用硬件中断的方法实现单片机设计的0到60秒的计时器电路图和程序

    本文档的主要内容详细介绍的是使用硬件中断的方法实现单片机设计的0到60秒的计时器电路图和程序资料免费下载。
    发表于 09-05 17:27 12次下载
    使用硬件<b class='flag-5'>中断</b>的方法实现<b class='flag-5'>单片机设</b>计的0到60秒的计时器电路图和<b class='flag-5'>程序</b>

    不利用中断使用单片机设计数字时钟的程序免费下载

    本文档的主要内容详细介绍的是不利用中断使用单片机设计数字时钟的程序免费下载。
    发表于 07-26 17:36 1次下载

    单片机中断程序如何运行

    单片机中断就是类似的一个过程,发生中断时,就会打断正在执行的主程序,先处理完中断任务,返回主程序
    的头像 发表于 01-27 17:11 1w次阅读
    <b class='flag-5'>单片机</b>的<b class='flag-5'>中断</b><b class='flag-5'>程序</b>如何运行

    单片机主程序中断程序是怎么样运行的

    的一个过程,发生中断时,就会打断正在执行的主程序,先处理完中断任务,返回主程序继续运行,当然在执行中断函数之前,
    的头像 发表于 01-04 10:11 1.3w次阅读
    <b class='flag-5'>单片机</b>的<b class='flag-5'>主程序</b>和<b class='flag-5'>中断</b><b class='flag-5'>程序</b>是怎么样运行的

    单片机主程序是如何执行的

    我们从单片机的工作原理可以看出单片机是执行程序来完成我们所要求的任务的,在单片机中有很多子程序单片机
    的头像 发表于 10-30 17:28 9655次阅读
    <b class='flag-5'>单片机</b>的<b class='flag-5'>主程序</b>是如何执行的

    单片机设程序30例资料

    单片机设程序30例资料免费下载。
    发表于 05-30 09:38 11次下载

    单片机-中断法实现数码管每秒加一

    单片机-外部中断与计数定时器外部中断计数定时器外部中断中断的概念:CPU在执行主程序的时候,
    发表于 11-15 13:06 28次下载
    <b class='flag-5'>单片机</b>-<b class='flag-5'>中断</b>法实现数码管每秒加一

    【51单片机】有关单片机执行中断无法恢复主程序探讨

    Author: Manba Cople专业:IOT说明:记录和输出学习内容文章目录问题思考修改进阶声明问题  最近在给小伙伴培训单片机中断时,小伙伴写了一段中断的代码(代码如下),出现每次执行完
    发表于 11-22 12:06 12次下载
    【51<b class='flag-5'>单片机</b>】有关<b class='flag-5'>单片机</b>执行<b class='flag-5'>中断</b>无法恢复<b class='flag-5'>主程序</b>探讨

    MCS-51单片机中断系统

    ,CPU暂时中断当前程序而转去执行相应的处理程序,待处理程序执行完毕后,CPU再继续执行原来被中断程序
    发表于 11-23 16:20 7次下载
    MCS-51<b class='flag-5'>单片机</b>的<b class='flag-5'>中断</b>系统

    单片机课设-中断程序

    proteus单片机中断程序利用单片机的P0口做输出接8只发光二极管,P3.2引脚接独立按键产生外部中断信号。编写
    发表于 01-13 15:02 2次下载
    <b class='flag-5'>单片机</b>课设-<b class='flag-5'>中断</b><b class='flag-5'>程序</b>

    谨慎处理单片机中断中断等价于比主程序优先级更高的线程

     有些小伙伴喜欢在单片机中断里做任务,殊不知可能会因此遇到棘手的bug,然后查半天查不出个所以然。本文为了纠正这个不良习惯,对单片机中断进行阐述。 无
    发表于 01-14 14:54 2次下载
    谨慎处理<b class='flag-5'>单片机</b><b class='flag-5'>中断</b>,<b class='flag-5'>中断</b>等价于比<b class='flag-5'>主程序</b>优先级更高的线程

    51单片机中断程序示例

    51单片机中断程序示例
    发表于 05-17 18:03 0次下载

    基于单片机的外部中断实验 中断系统知识介绍

    单片机中有两个重要的概念分别叫做中断中断系统,那么他们分别又代表什么意义呢?当单片机CPU正在运行主程序时外界发生了紧急事件请求,要求
    的头像 发表于 07-26 17:23 1708次阅读
    基于<b class='flag-5'>单片机</b>的外部<b class='flag-5'>中断</b>实验 <b class='flag-5'>中断</b>系统知识介绍