微波EDA网,见证研发工程师的成长!
首页 > 硬件设计 > 嵌入式设计 > 中断多任务+状态机 单片机软件结构设计

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

时间:11-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,直到学

Copyright © 2017-2020 微波EDA网 版权所有

网站地图

Top