基于单片机的小系统设置,无需使用OS系统,课本上讲的设计是主程序(主循环 ) + 中断,这种结构虽然符合自然想法。但是也存在某些弊端,这里换一种思路,把所有的程序全部放入中断,程序不处理任何事件,这么做至少看到几个好处: 系统可以处于低功耗的休眠状态,将由中断唤醒进入主程序, 模块化管理程序。
为了把程序全部放入中断中,必须把事件化分成一个个模块程序,每个模块完成一个指定的功能,模块之间通过消息传递(参数)。系统需要设定一个合理的时基(tick), 例如 1-20 ms, 每次定时中断,把所有任务执行一遍,为减少复杂性,不做动态调度(最多使用固定数组以简化设计,做动态调度就接近 os 了),这实际上是一种无优先级时间片轮循的变种。
主程序的构成:
void main()// 程序复位
{
Initial();//初始化程序
while (true)
{
IDLE();//sleep程序,让 mcu 进入低功耗模式
}
}
void Timer_Interrupt()//定时中断
{
SetTimer();// 定时设置,根据需求设置
ResetStack();// 堆栈设置
Enable_Interrupt;// 设置开启中断嵌套,根据需求设置
// 下面把所有的任务都执行一遍
KeyTask1();
RunTask2();
...
RunTaskN();
//
while(1) IDLE();
}
中断把所有任务调用一遍,至于任务是否需要运行,由程序员自己控制;另一种做法是通过函数指针数组:
#define CountOfArray(x) (sizeof(x))// #define CountOfArray(x) (sizeof(x[0]))
typedef void (*PTR)();
const PTR[] tasks = {
KeyTask1();
RunTask2();
...
RunTaskN();
};
void Timer_Interrupt()
{
SetTimer();
ResetStack();
Enable_Timer_Interrupt;
for (i=0; i<CountOfArray (tasks), i++)
(*tasks)();
while(1) IDLE;
}
使用const让数组内容位于程序段,保存在ROM中,而非RAM中。
注意:51中使用code代替const。
关于函数指针赋值时是否需要取地址操作符&的问题, 与数组名一样, 取决于编译器. 一般来讲, 函数名和数组名都是常数地址, 但对于不熟悉汇编的人, 用 & 取地址,无需这样取地址
在汇编下表现为散转, 一个小技巧是利用 stack 获取跳转表入口:
movA, state
acallMultiJump
ajmpstate0
ajmpstate1
...
MultiJump:
popDPH
popDPL
rlA
jmp@A+DPTR
还有一种方法是把函数指针数组(动态数组,链表更好,不过在小系统的mcu 中不适用)放在data段中,便于修改函数指针以运行不同的任务,这已经接近于动态调度了:
PTR[COUNTOFTASKS] tasks;
tasks[0] = KeyTask1;
tasks[1] = RunTaskM;
tasks[2] = NULL;
...
PTR pFunc;
for (i=0; i< COUNTOFTASKS; i++)
{
pFunc = tasks);
if (pFunc != NULL)
(*pFunc)();
}
通过这些手段, 一个中断驱动的框架形成了, 但必须保证每个时基内所有任务的运行时间总和不能超过一个时基的时间。如果做不到,可以把每个任务切分成一个个的时间片,在每个时基内只运行一个任务。
状态机的程序实现相当简单,第一种方法是用 swich-case 实现:
void RunTaskN()
{
switch (state)
{
case 0: state0(); break;
case 1: state1(); break;
...
case M: stateM(); break;
default: state=0;
break;
}
}
另一种方法,简洁的函数指针数组:
const PTR[] states = { state0, state1, …, stateM };
void RunTaskN()
{
(*states[state])();
}
void state0() { }// 建议将 state0 设置为空状态, state =0可以让整个task 停止运行,如果需要投入运行,简单的让 state = 1 即可
...
void stateM() { state=0; }
下面是一个键盘扫描的例子,这里假设 tick = 5 ms, ScanKeyboard() 函数控制口线的输出扫描,并检测输入转换为键码,利用每个state 之间 5 ms 的间隔去抖动。
enum EnumKey {//使用 enum 对于常数类型,分类组织,避免使用大量 #define 定义常数
EnumKey_NoKey = 0,
...
};
struct StructKey {
intkeyValue;
boolkeyPressed;
};
struct StructKeyProcess key;
void KeyTask1()
{
(*states[state])();
}
void state0() { }// 空任务
void state1()//
{
key.keyPressed = false; state++;
}
void state2()
{
if (ScanKey() != EnumKey_NoKey) state++;
}
void state3()
{
key.keyValue = ScanKey();
if (key.keyValue == EnumKey_NoKey) state--;
else
{
key.keyPressed = true;
state++;
}
}
void state4()
{
if (ScanKey() == EnumKey_NoKey) state++;
}
void state5()
{
ScanKey() == EnumKey_NoKey? state = 1 : state--;
}
键盘处理过程显然比通常使用标志去抖的程序简洁清晰,而且没有软件延时去抖的困扰。以此类推,各个任务都可以划分成一个个的state, 每个state 实际上占用不多的处理时间。某些任务可以划分成若干个子任务,每个子任务再划分成若干个状态。
对于一些完全不能分割,必须独占的任务来说,这时只能牺牲其他的任务了。
两种做法:一种是关闭中断,完全的独占;
void RunTaskN()
{
Disable_Interrupt;
...
Enable_Interrupt;
}
另一种,允许定时中断发生,保证某些时基得以更新;
void Timer_Interrupt()
{
SetTimer();
Enable_Timer_Interrupt;
UpdateTimingRegisters();
if (MeCounter = 0)
{
ResetStack();
for (i=0; i<CountOfArray (tasks), i++) (*tasks)();
while (1) IDLE;
}
else MeCounter--;
}
只要MeCounter不为 0,那么中断正常返回到中断点,继续执行先前被中断的任务,否则 复位 stack, 重新进行任务循环. 这种状况下, 中断处理过程极短, 对独占任务的影响也有限。