嵌入式领域实现顺序流控制的方法常采用多任务或事件驱动来完成。相对于多任务系统,事件驱动模型不需要为每个进程或线程设立堆栈来保存它们的状态,对系统资源的需求较小,因而在系统资源紧张的嵌入式领域中,基于事件驱动的编程模型是一种非常常用的顺序流控制实现方式。在实际开发当中,由于事件驱动模型没有阻塞机制,因此需要由程序员构造一个有限状态机来实现顺序流控制。从以往经验来看,使用有限状态机的方法实现顺序流控制,在程序的编写、维护以及调试方面都具有不小的难度。
1 Protothread简介
Protothread是专为资源有限系统设计的一种耗费资源少,且不使用堆栈的线程模型,不使用复杂的状态机机制或多任务方式来实现顺序流的控制。它为开发者提供了一种以顺序执行的C语言代码来实现基于事件驱动系统的方法。其特点如下:
◇ 以纯C语言实现,无硬件依赖性;
◇ 极少的资源需求,每个Protothread仅需要2个额外的字节;
◇ 可以用于有操作系统或无操作系统的场合;
◇ 支持阻塞操作且没有栈的切换。
图1 简单无线通信的状态切换示意图
下面举例说明Protothread在构造事件驱动模型方面的优越性。图1是非常简单的无线通信的状态切换示意图。其文字描述的流程如下:
① 打开无线通道;
② 等待tawake毫秒;
③ 当所有通信结束后关闭无线通道;
④ 如果通信尚未结束,则等待其结束;
⑤ 关闭无线通道,如果在tsleep毫秒前由于通信未结束而未关闭通道,则不再关闭;
⑥ 重复第①步。
为实现这一简单的过程控制,在状态机方式下的事件驱动模型,首先需要定义一套状态集:ON(状态开),OFF(状态关),WAITING(等待通信结束)。然后还需要一个变量state来存放当前的状态,并且使用C语言中的switchcase结构来控制不同状态下的动作。这些代码被放入一个事件处理函数中,这个函数在每一个时间结束或通信结束事件到来时被调用一次:
enum {
ON,
WAITING,
OFF
} state;
void radio_wake_eventhandler() {
switch(state) {
case OFF:
if(timer_expired(&timer)) {
radio_on();
state = ON;
timer_set(&timer, T_AWAKE);
}
break;
case ON:
if(timer_expired(&timer)) {
timer_set(&timer, T_SLEEP);
if(!communication_complete()) {
state = WAITING;
} else {
radio_off();
state = OFF;
}
}
break;
case WAITING:
if(communication_complete() || timer_expired(&timer)) {
state = ON;
timer_set(&timer, T_AWAKE);
} else {
radio_off();
state = OFF;
}
break;
}
}
如此简单的一个机制竞需要编写如此多的代码,并且这个顺序流描述的6个步骤也不能从程序中体现出来。
而用Protothread实现同样的机制时,使用PT_WAIT_UNTIL()来等待事件的发生:
PT_THREAD(radio_wake_thread(struct pt *pt)) {
PT_BEGIN(pt);
while(1) {
radio_on();
timer_set(&timer, T_AWAKE);
PT_WAIT_UNTIL(pt, timer_expired(&timer));
timer_set(&timer, T_SLEEP);
if(!communication_complete()) {
PT_WAIT_UNTIL(pt, communication_complete()
|| timer_expired(&timer));
}
if(!timer_expired(&timer)) {
radio_off();
PT_WAIT_UNTIL(pt, timer_expired(&timer));
}
}
PT_END(pt);
}
可以看到,编写的代码比基于状态机的代码少了许多;而且从程序的结构来看,也更接近于这个无线收发顺序流机制的描述。由于使用的是线性结构的C语言代码,因此整个处理逻辑能直接从C代码中体现出来。同时,在基于Protothread的代码中,还可以使用普通的C语言顺序控制语句,如if、while等。
2 用Protothread控制顺序流
通过对大量现存的事件驱动程序进行分析得知,大多数的顺序流控制不外乎3种情况:顺序、重复和分支,如图2所示。
图2 顺序流控制的3种情况
可用Protothread实现这3种结构代码。
顺序结构:
PT_BEGIN
(* ... *)
PT_WAIT_UNTIL(condition)
(* ... *)
PT_END
重复结构:
PT_BEGIN
(* ... *)
while (cond1)
PT_WAIT_UNTIL(
cond1 or cond2
)
(* ... *)
PT_END
分支结构:
PT_BEGIN
(* ... *)
if (condition)
PT_WAIT_UNTIL(cond2a)
else
PT_WAIT_UNTIL(cond2b)
(* ... *)
PT_END
3 实例——Protothread实现密码锁
本实例演示了怎样去实现一个简单的密码锁,通过按顺序输入4个正确的数字来开锁。密码锁要求两次按键的时间间隔不得超过1 s,并且在全部正确的键被按下后的0.5 s内不得再有键按下,否则不能开锁。源程序如下:
#include "pt.h"
struct timer { int start, interval; };
static int timer_expired(struct timer *t);
static void timer_set(struct timer *t, int usecs);
static struct timer codelock_timer, input_timer;
static const char code[4] = {'1', '4', '2', '3'};
static struct pt codelock_pt, input_pt;
static char key, key_pressed_flag;
static void press_key(char k) {
printf(" Key '%c' pressed\\n", k);
key = k;
key_pressed_flag = 1;
}
static int key_pressed(void) {
if(key_pressed_flag != 0) {
key_pressed_flag = 0;
return 1;
}
return 0;
}
static PT_THREAD(codelock_thread(struct pt *pt)) {
static int keys;/*用于保存按下的键值,使用static是为了确保变量不保存在堆栈里*/
PT_BEGIN(pt);
while(1) {
for(keys = 0; keys < sizeof(code); ++keys) {
/*读入N个按键*/
if(keys == 0) { /*如果还没有键按下过*/
PT_WAIT_UNTIL(pt, key_pressed());
/*等待键按下事件发生*/
}
else {
timer_set(&codelock_timer, 1000);
/*设置超时时间*/
PT_WAIT_UNTIL(pt, key_pressed() || timer_expired(&codelock_timer));
/*等待键按下事件或超时事件发生*/
if(timer_expired(&codelock_timer)) {
/*如果是超时事件,则重新开始*/
printf("Code lock timer expired.\\n");
break;
}
}
if(key != code[keys]) { /*检查按键是否正确,不正确则重新开始*/
printf("Incorrect key '%c' found\\n", key);
break;
}
else {
printf("Correct key '%c' found\\n", key);
}
}
if(keys == sizeof(code)) { /*检查是否全部按键通过*/
printf("Correct code entered, waiting for 500 ms before unlocking.\\n");
timer_set(&codelock_timer, 500);
/*设置500 ms超时时间*/
PT_WAIT_UNTIL(pt, key_pressed() || timer_expired(&codelock_timer));
/*等待键按下事件或超时事件发生*/
if(!timer_expired(&codelock_timer)) { /*500 ms内又有键按下*/
printf("Key pressed during final wait, code lock locked again.\\n");
}
else {
printf("Code lock unlocked.\\n");
PT_EXIT(pt);
}
}
}
PT_END(pt);
}
/*下面是一个模拟按键的Protothread线程*/
static PT_THREAD(input_thread(struct pt *pt)) {
PT_BEGIN(pt);
printf("Waiting 1 second before entering first key.\\n");
timer_set(&input_timer, 1000);
PT_WAIT_UNTIL(pt, timer_expired(&input_timer));
press_key('3');
/*模拟按键可自行添加*/
timer_set(&input_timer, 100);
PT_WAIT_UNTIL(pt, timer_expired(&input_timer));
press_key('1');
timer_set(&input_timer, 100);
PT_WAIT_UNTIL(pt, timer_expired(&input_timer));
press_key('4');
timer_set(&input_timer, 400);
PT_WAIT_UNTIL(pt, timer_expired(&input_timer));
press_key('2');
timer_set(&input_timer, 500);
PT_WAIT_UNTIL(pt, timer_expired(&input_timer));
press_key('3');
timer_set(&input_timer, 2000);
PT_WAIT_UNTIL(pt, timer_expired(&input_timer));
PT_END(pt);
}
int main(void) {
PT_INIT(&input_pt);/*初始化Protothread*/
PT_INIT(&codelock_pt);/*初始化Protothread*/
while(PT_SCHEDULE(codelock_thread(&codelock_pt))) {
PT_SCHEDULE(input_thread(&input_pt));
}
return 0;
}
结语
在嵌入式领域中有许多系统都构造在基于事件驱动的编程模型上,而且这种实现又往往通过构造状态机的方式,这给开发人员在编写代码、调试代码以及维护代码上造成诸多不便和困难。通过引入Protothread来实现事件驱动的系统极大地降低了代码的编写难度,并且代码的可读性、可维护性也得到极大的提升。笔者在实际工作当中将Protothread应用于电能表按键处理模块和通信模块中,效果不错。