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

Akron 2008-08-01 11:41:26
加精
http://blog.csdn.net/Akron/archive/2008/08/01/2755643.aspx


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



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


...全文
2986 84 打赏 收藏 转发到动态 举报
写回复
用AI写文章
84 条回复
切换为时间正序
请发表友善的回复…
发表回复
jialong6688 2012-03-10
  • 打赏
  • 举报
回复
顶一下!
wenweima 2012-03-10
  • 打赏
  • 举报
回复

佩服楼主的精神,保留搂主的结论
叮当 2011-09-19
  • 打赏
  • 举报
回复
MSP430不采用这种架构也可以大部分时间处于睡眠状态,我没有看出这种架构的好处,不过可以这样思考还是很不错的!
gzfsaok 2011-09-03
  • 打赏
  • 举报
回复
误入歧途,.............
qq363155763 2011-09-02
  • 打赏
  • 举报
回复
用中断中实现主程序,限制比较大~
ailiufeng6 2011-09-01
  • 打赏
  • 举报
回复
驱动延时外设时就不行了,比如LCD.
guangbiao_w 2011-06-03
  • 打赏
  • 举报
回复
顶一下!
既然存在就有一定的适用范围,只是还没有面面俱到。
革命尚未成功,楼主还需努力。
看好你。。。
宋哥 2011-05-27
  • 打赏
  • 举报
回复
觉得没必要,画蛇添足。按楼主的想法,放在中断外面会更好,也可以实现其所定义的多任务。比如第一个循环干这个,第二个循环干那个,比在中断里安全的多。中断里还是少做点事比较安全。
mark0668 2011-05-26
  • 打赏
  • 举报
回复
高手!学习了
zjh2287 2011-05-26
  • 打赏
  • 举报
回复
46楼、70楼的说法很好,这几天我也要实现一个功能,其中有个3s的延时,而且我在这3s中我还需要做四个按键的检测。按照楼主的方法就没法实现了。但是楼主的想法还是很好的,看得出来楼主对汇编、对实时系统的了解还是很深的。
wrt851001 2011-04-13
  • 打赏
  • 举报
回复
很不错,学习了。
greatriver007 2011-04-11
  • 打赏
  • 举报
回复
像高手学习啊,厉害
liaoshuipinggfkd 2011-04-11
  • 打赏
  • 举报
回复
值得学习一下!
AnYidan 2011-04-10
  • 打赏
  • 举报
回复
佩服楼主的精神,保留搂主的结论
ninesunsz 2011-04-10
  • 打赏
  • 举报
回复
如果不想用操作系统可以在中断里置标志位。在while(1)里用检测标志位的方法来运行各个任务。主要就是切换任务的时候处理比较麻烦,这可能是有些人不愿意用操作系统的原因
ruiyudong 2011-04-09
  • 打赏
  • 举报
回复
好东东,学习,学习,再学习!!!!!
wangweihe68 2011-04-01
  • 打赏
  • 举报
回复
这种想法不错,但是有个致命的问题,负责轮转的中断必须是最高级的中断,这样的话会大大降低系统的实时性
thrawsnow 2011-03-28
  • 打赏
  • 举报
回复
讲得很好,收藏
fzzz123 2011-03-05
  • 打赏
  • 举报
回复
我确实看不懂
hicaru000000 2011-01-24
  • 打赏
  • 举报
回复
终于看完了,说说自己的见解吧。

1. 个人感觉这并非是多任务处理,是响应中断后,一连串的流程控制,只不过是写成了多任务的形式,

也更像是汇编的一些编程方式(似乎LZ对汇编语言的造诣比较深吧,呵呵);

2. 这一长段中断响应之后,整个单片机应该就没有余力去进行其他程序段的运行了,

(没看到LZ中断程序中其他中断响应的处理,应该是屏蔽了吧)。

也就是说程序的可扩展性、可移植性不是很高吧,仅限于完成一些类似于PLC的流程控制。

个人浅见,忘大家品评。
加载更多回复(64)

27,375

社区成员

发帖
与我相关
我的任务
社区描述
硬件/嵌入开发 单片机/工控
社区管理员
  • 单片机/工控社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧