使用Protothreads设计嵌入式多任务程序
张 晋
(青岛科技大学 信息科学技术学院,山东 青岛 266061)
摘 要 针对嵌入式多任务程序设计所面对的问题,探讨了该类程序的一般设计方法。分析了使用Protothreads方法设计嵌入式多任务程序的独特优势。通过实例展现了Protothreads的应用,并简要分析了Protothreads的局限性。
关键词 嵌入式系统;多任务;事件驱动;Protothreads
1 引言
目前嵌入式系统的功能日益复杂。受到设计难度、成本等诸多因素的要求,在单片机系统等低成本而又资源紧缺型的嵌入式系统中,设计简洁、稳定且易于调试的多任务程序,已经成为十分重要的软件技术。
Protothreads是一种针对C语言封装后的宏函数库,为C语言模拟了一种无堆栈的轻量线程环境,能够实现模拟线程的条件阻塞、信号量操作等操作系统中特有的机制,从而使程序实现多线程操作。每个Protothreads线程仅增加10行代码和2字节RAM的额外硬件资源消耗[1]。对于资源紧缺而不能移植嵌入式操作系统的嵌入式系统,使用Protothreads能够方便直观地设计多任务程序,能够实现用线性程序结构处理事件驱动型程序和状态机程序,简化了该类程序的设计[2]。
2 嵌入式多任务程序常用的的设计方法
常见的嵌入式多任务程序的设计方法有死循环方法、状态机方法、嵌入式操作系统方法等。这些方法各有各自的优缺点,并且均在不同的场合得到了广泛的应用。
2.1 死循环方法
死循环方法是最常用的多任务处理方法。其程序结构通常如下:
While(1) {
if( condition1 )
task1();
if( condition2 )
task2();
……
}
实际程序中往往是用中断或其他任务来触发各个任务的执行条件。该方法思路简洁,没有多余的硬件资源消耗,程序的设计和调试都十分便捷。但这种的缺点很明显:程序无法保证各个任务的并发执行。如果循环中某个任务需要长时间等待,在这种结构中无法将其暂时阻塞,从而导致其他任务的执行都受到影响。如果程序中有任务有实时性要求,显然这种程序结构是难以满足的[3]。
2.2 状态机方法
有限状态机(FSM)方法是一种重要的程序设计方法,有成熟的理论基础。在事件驱动型程序设计以及硬件描述语言程序(如VHDL,Verilog等)设计中,该方法有广泛的应用,而且有众多软件开发工具的支持。
该方法需要事先建立程序状态转换图,根据状态转换图在程序中通过判断状态标志变量的状态,引导程序在各个任务之间跳转,从而实现多任务程序。在C语言中,该方法往往通过switch-case的程序结构来实现。同时,通过随时向指定的任务跳转,该程序结构也能够实现类似于程序阻塞的操作,让实时性要求更高的程序得以及时执行。状态机程序同样不会引入过多的硬件资源消耗,很适合嵌入式环境。
该方法的缺点主要在于程序状态较多的时候,程序状态图较难建立。此外状态机程序往往是一种网状的程序结构,程序的设计、维护和调试与其他方法相比难度较大[2]。
2.3 嵌入式操作系统方法
嵌入式操作系统为嵌入式环境的多任务设计提供了完善的解决方法。无论是面向低端硬件设备的简单系统,例如uC/OS-II、Contiki、FreeRTOS等;还是面向高端硬件的例如Windows CE、嵌入式Linux等复杂嵌入式操作系统;均提供了完整的进程(线程)环境、消息邮箱、信号量等机制,以及相应的任务调度器。能够十分方便的进行多任务程序设计,同时通过各种调度算法满足各种不同优先级任务的实时性要求。
但该方法也有很多约束性:首先是对硬件资源要求很高,至少是数十KB的RAM以及数百KB的ROM,复杂嵌入式系统甚至要求数MB的RAM和数十MB的ROM,这将带来硬件成本巨大上升,但实际应用中很多场合并不需要这么高端的硬件资源。同时,复杂的软硬件环境使得软硬件的设计过程过于复杂和专业,设计周期也因此变长。此外,嵌入式操作系统多数是商业软件,授权费用昂贵,而且硬件上往往需要一系列与之配套的芯片,高昂的成本使得这种方法对于简单嵌入式设备并不适合。
3 使用Protothreads方法编写嵌入式多任务程序
3.1 Protothreads简介
Protothreads是由瑞典计算机科学研究院的Adam Dunkels编写的基于C语言的宏函数库,通过该宏函数库为C语言模拟了一种轻量级的无堆栈线程环境,从而实现类似多线程的程序。
在Protothreads库中,PT_INIT()、PT_THREAD()操作用来初始化和声明一个Protothreads线程,PT_BEGIN()、PT_END()操作用来开始和结束一个Protothreads线程,PT_WAIT_UNTIL()、PT_WAIT_WHILE()操作用来实现Protothreads线程的条件阻塞或等待,PT_SEM_INIT()、PT_SEM_SIGNAL()、PT_SEM_WAIT()用于实现Protothreads线程信号量的各种操作,此外还有线程的派生、重启等操作[4]。
Protothreads使用ANSI C编写,可以在不同的编译器间方便的移植,在工程中加入Protothreads的源码,并在相应的程序中引用其头文件即可。Protothreads引入的额外硬件资源消耗极小,十分方便在资源紧缺的小型嵌入式系统中使用。尤其对于事件驱动型程序,使用Protothreads能使代码结构更加清晰、线性化。Protothreads能够有效地替代状态机方法,或者辅助减少程序的状态个数,简化状态机程序的编写。
Protothreads是开源软件,鉴于其种种优势,Protothreads在许多著名的软件项目中得到广泛应用,例如嵌入式操作系统Contiki和嵌入式TCP/IP协议栈uIP[2]。
3.2 应用实例
以下使用一个数字钟程序来展示Protothreads的基本用法,在该程序中,时、分、秒的计数分别采用三个Protothreads线程来完成。该程序在PC机上运行检验,直接使用系统函数sleep()来计时一秒钟,在实际的嵌入式系统中,可以使用相应的硬件定时器来实现。主函数如下:
/*包含pt.h头文件后即可使用Protothreads*/
#include <stdio.h>
#include <windows.h>
#include "pt.h"
/*由于Protothreads线程没有各自的堆栈,
所以需要静态变量来保存各个线程的参数*/
static struct pt pt_h,pt_m,pt_s;
/*分别用于存储时、分、秒的计数值*/
static int ihour=0,iminute=0,isecond=0;
/*分钟和秒钟的进位标志位*/
static int c_s=0,c_m=0;
/*用于模拟秒钟的标志位*/
static int clk=0;
int main(void) {
/*分别初始化三个线程*/
PT_INIT(&pt_h);
PT_INIT(&pt_m);
PT_INIT(&pt_s);
while(1) {
/*循环调用三个线程*/
PT_SCHEDULE( second(&pt_s) );
PT_SCHEDULE( minute(&pt_m) );
PT_SCHEDULE( hour(&pt_h) );
/*使用系统函数来计数一秒钟*/
sleep(1);
clk = 1;
/*输出时间*/
printf("Time --- %d:%d:%d\n",ihour,iminute,isecond);
}
return 0;
}
分别实现时、分、秒计数的三个Protothreads线程如下:
/*计数小时的线程*/
PT_THREAD( hour( struct pt * pt ) ) {
/*线程的开始*/
PT_BEGIN(pt);
/*Protothreads的条件阻塞机制,这里用于等待分钟的进位*/
PT_WAIT_UNTIL( pt,c_m==1 );
/*上面的条件满足时,下面的代码才得以运行*/
c_m = 0;
if(ihour == 23)
ihour = 0;
else
ihour++;
/*线程的结束*/
PT_END(pt);
}
/*计数分钟的线程*/
PT_THREAD( minute( struct pt * pt ) ) {
PT_BEGIN(pt);
/*条件阻塞,等待秒钟的进位*/
PT_WAIT_UNTIL( pt,c_s==1 );
/*上面的条件满足时,下面的代码才得以运行*/
c_s = 0;
if(iminute == 59) {
c_m = 1;
iminute = 0;
}
else
iminute ++;
PT_END(pt);
}
/*计数秒钟的线程*/
PT_THREAD( second(struct pt * pt) ) {
PT_BEGIN(pt);
/*条件阻塞,等待秒钟信号的发生*/
PT_WAIT_UNTIL( pt,clk==1 );
/*上面的条件满足时,下面的代码才得以运行*/
clk = 0;
if(isecond == 59) {
isecond = 0;
c_s = 1;
}
else
isecond++;
PT_END(pt);
}
该程序经测试运行正确。由该程序可以看出,使用Protothreads线程可以让不满足运行条件的线程自我阻塞,从而让其他线程得以及时执行。程序的结构清晰化,使得多任务处理、条件阻塞等机制在一般地程序中得以实现。
4 Protothreads的局限性
虽然Protothreads具有占用资源少、方便多任务处理等优点,但由于其本质上仅仅是对一些C程序的封装,并不是真正的线程。所以与真正的操作系统线程相比,Protothreads线程具有一些局限性[1]:
(1)Protothreads线程不具备各自的堆栈,而是采用所有线程共享主程序的堆栈的方式。这虽然能够极大地减少硬件资源的消耗,但也使得每个Protothreads线程无法保存各自的局部变量,而必须通过静态变量来保存必要的数据。
(2)Protothreads虽然提供了在各自线程内的条件阻塞机制,但对于在该线程内调用的其它函数,则无法阻塞其运行。所以,如果要在线程内调用占用时间较多的函数,为保证各个线程的实时性要求,需要将这类函数进一步划分为更小的函数,分步执行。
(3)使用ANSI C实现的Protothreads库采用了封装switch-case语句的方法,因此,按照C语言语法的要求,Protothreads不能与switch-case语句混用。但是,Protothreads库的另一种实现方式是采用了GCC编译器的一些扩展特性,这样的Protothreads库能够克服该缺点,但具有编译器的依赖性。
5 结束语
本文通过与一般嵌入式多任务程序设计方法的对比,分析了Protothreads在设计嵌入式多任务程序方面的优势,通过一个实例展示了Protothreads的基本使用方法,并分析了Protothreads的局限性。Protothreads为嵌入式环境的多任务程序设计和事件驱动程序设计提供了一种有效地处理方法,使得程序的设计、维护和调试更加便捷,对于嵌入式软件开发有较大的参考价值。
参考文献
[1] Adam Dunkels,Oliver Schmidt. Lightweight,Stackless Threads in C[R]. Sweden:Swedish Institute of Computer Science,2005
[2] Adam Dunkels,Oliver Schmidt,Thiemo Voigt. Protothreads:Simplifying Event - Driven Programming of Memory -Constrained Embedded Systems. Proceedings of the Fourth ACM Conference on Embedded Networked Sensor Systems,2006[C]. Boulder,Colorado,USA,November 2006
[3] 罗光平,郭卫锋.利用Protothread实现实时多任务系统[J].单片机与嵌入式系统应用,2008(5):32-35
[4] Adam Dunkels. The Protothreads Library 1.4 Reference Manual[M/OL]. http://www.sics.se/~adam/pt/publications.html
收稿日期:5 月 18 日 修改日期:6 月 24 日
作者简介:张晋(1984-),男,硕士研究生,研究方向:嵌入式系统应用。