单片机多任务调度
2016-04-25 09:14
211 查看
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 定义常数)
对于一些完全不能分割,必须独占的任务来说,比如我以前一个低成本应用中红外遥控器的软件解码任务,这时只能牺牲其他的任务了。两种做法:一种是关闭中断,完全的独占;
void
RunTaskN()
{
Disable_Interrupt;
…
Enable_Interrupt;
}
第二种,允许定时中断发生,保证某些时基 register 得以更新;
void
Timer_Interrupt()
{
SetTimer();
Enable_Timer_Interrupt;
UpdateTimingRegisters();
if
(watchDogCounter = 0) {
ResetStack();
for
(i=0; i<CountOfArray (tasks), 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;
}
(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 定义常数)
对于一些完全不能分割,必须独占的任务来说,比如我以前一个低成本应用中红外遥控器的软件解码任务,这时只能牺牲其他的任务了。两种做法:一种是关闭中断,完全的独占;
void
RunTaskN()
{
Disable_Interrupt;
…
Enable_Interrupt;
}
第二种,允许定时中断发生,保证某些时基 register 得以更新;
void
Timer_Interrupt()
{
SetTimer();
Enable_Timer_Interrupt;
UpdateTimingRegisters();
if
(watchDogCounter = 0) {
ResetStack();
for
(i=0; i<CountOfArray (tasks), 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;
}
相关文章推荐
- 【代码笔记】iOS-iCarouselDemo
- hdu2522 A simple problem(循环节)
- 代码生成UIButton控件
- 我司参加2016中国国际电子商务博览会
- 设计模式读书笔记之备忘录模式(Memento)
- 【代码笔记】iCarouselDemo
- Laravel 学习笔记之语言包 IDE IDE提示工具 IDE插件笔记
- 容器技术适合你的企业吗
- 为什么在Spring的配置里,最好不要配置xsd文件的版本号
- 设计模式读书笔记之单例模式(Singleton)
- 如何实现拼音与汉字的互相转换
- 当PHP随机数遇到最小值是58以后
- 机房水题欢乐赛 2016-04-26 上午
- QVariant
- Bootstrap每天必学之响应式导航、轮播图
- redis的基本类型及操作命令
- 服务器上传,时间超时 Apache 如何设置保持连接
- 设计模式读书笔记之组合模式(Composite)
- 《巴黎圣母院》读后感
- 2Gb_DDR3_SDRAM——16 Meg x 16 x 8 Banks