朱工

早年从事单片机、实时控制系统产品设计及编程。目前耄耋之年开始学习AI技术。

Protothreads的简化

1
阅读(5331)

Protothreads的简化

-- 微控制器中基于协程的实时协作多任务方法 (1)

  以小型微控制器,例如MCS51、MSP430、MICROCHIP等单片机、以至于ARM Cortex-M0甚至M3,为核心的嵌入式系统中,由于资源的限制,实时操作系统是无法施展其优势的。虽然资源丰富的芯片不断出现,但是,在单机控制领域,以及M2M或者说物联网的出现,小型微控制器还是具有相当的生命力。所以我们需要一个与之相适应的实时多任务编程方法。

  特别是,需要高速轮转的多任务,比如任务的轮转周期为1毫秒,以至于亚毫秒(三、五百微秒),通常的实时操作系统是无能为力的。因为这点时间还不够实时操作系统倒腾。

  针对这种情况,目前看来,如果用C来编程,Protothreads就是最好的选择了。自2005-2006年Adam Dunkels等推出Protothreads后[1,2],2007-2008年以来,国内已有杂志上的文章对它做了介绍和推荐[3,4,5]。此外,Lych(边缘独行者) 的两篇博文,文字不多,却很深刻[6,7]。

  大家都强调使用它极少内存和无堆栈。其实,我觉得更应该注重它在任务切换时的高效。这是通常的实时操作系统无法与之相比的。当需要高速切换任务时,切换效率就显得特别重要。

  尽管Protothreads已被认为是精简到了极致,但是我还是想对它做一点简化,以推进Protothreads的使用。

  强调一下,我不想从理论方面,譬如“实时操作系统”等方面讨论Protothreads。而是以工程应用的观点,从解决问题的方法方面进行讨论,并且主要针对小型微控制器的应用。这里我所提出的一些措施,对Protothreads进行简化,主要是为了更加提高它的切换效率。

  好在语句不多,我们把主要的简化前后都列出来,以便对比。


Protothread 1.4

lc-switch.h :

typedef unsigned short lc_t;

#define LC_INIT(s) s = 0;

#define LC_RESUME(s) switch(s) { case 0:

#define LC_SET(s) s = __LINE__; case __LINE__:

#define LC_END(s) }

pt.h :

#define PT_INIT(pt) LC_INIT((pt)->lc)

#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc)

#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0; \

    PT_INIT(pt); return PT_ENDED; }

#define PT_WAIT_UNTIL(pt, condition) \

do { \

    LC_SET((pt)->lc); \

    if(!(condition)) { \

    return PT_WAITING; \

} \

} while(0)

#define PT_YIELD(pt) \

do { \

    PT_YIELD_FLAG = 0; \

    LC_SET((pt)->lc); \

    if(PT_YIELD_FLAG == 0) { \

    return PT_YIELDED; \

} \

} while(0)


本文(by fy_zhu)

lc-sw.h :

typedef unsigned short lc_t;

#define LC_INIT(s) s = 0;

#define LC_RESUME(s) switch(s) { case 0:

#define LC_YIELD(s,ret_code) s = __LINE__; return ret_code ; case __LINE__:

#define LC_END(s) }

pt.h :

#define PT_INIT(pt) LC_INIT((pt)->lc)

#define PT_BEGIN(pt) { LC_RESUME((pt)->lc)

#define PT_END(pt) LC_END((pt)->lc); \

    PT_INIT(pt); return PT_ENDED; }

#define PT_WAIT_UNTIL(pt, condition) \

do { \

    while(!(condition)) { \

    LC_YIELD((pt)->lc,PT_WAITING); \

} \

} while(0)

#define PT_YIELD(pt) LC_YIELD((pt)->lc,PT_YIELDED);


  文中的粗体红字,是主要的差别。简化的要点就是把“case __LINE__:”放在return之后,因而省去了与标志“char PT_YIELD_FLAG”有关的一些语句,进而使宏PT_WAIT_UNTIL和宏PT_YIELD得到简化。这两个宏的指令多少,影响到任务切换效率。

  事实上,“case __LINE__:”就是return之后要重入的后续点LC(Local Continuation)。为了LC_SET的书写方便写在return之前,实在不合逻辑。而重入后,再借PT_YIELD_FLAG绕过return,就显得不简练了。

  Protothreads的switch方法在各种C编译器都能通行,简化后的Protothreads的switch方法,用KEIL、IAR、CooCox+GCC编译器,在ARM Cortex-M0的实验板上调试通过。另外用KEIL、IAR、TASKING、RAISONANCE、SDCC的编译器,在C8051F的实验板上调试通过。

 

Protothreads的addrlabels方法也同样简化,我们只把有差别的部分写出:

lc-addrlabels.h :

#define LC_SET(s) \

do { \

    LC_CONCAT(LC_LABEL, __LINE__): \

    (s) = &&LC_CONCAT(LC_LABEL, __LINE__); \

} while(0)

 

本文(by fy_zhu) :

#define LC_YIELD(s,ret_code) \

do { \

    (s) = &&LC_CONCAT(LC_LABEL, __LINE__); \

    return ret_code ; \

    LC_CONCAT(LC_LABEL, __LINE__): \

} while(0)

 

  遗憾的是,小型微控制器常用的主流的C编译器KEIL、IAR等都不支持“Labels as Values”的扩展,因而简化的Protothreads的addrlabels方法也无法用这些编译器实施。在支持“Labels as Values”扩展的CooCox+GCC编译器,简化方法在ARM Cortex-M0的实验板上调试通过。

  GCC不支持8051,因此简化的Protothreads的addrlabels方法也无法在8051上实施。

  正如大家所知,C语言的switch语句的目标代码的执行效率比较低。尽管各家C编译器以各种方法改善switch语句的目标代码的执行效率,未尽人意。真正能使Protothreads发挥效率的,还是其addrlabels方法。

  addrlabels方法的高执行效率和C编译器的不支持,是一大矛盾。如何解决这个矛盾,后面我们将逐步展开。

  顺便说一句,在FreeRTOS的coroutine中,也是把“case __LINE__:”放在return之后。FreeRTOS的coroutine只使用switch-case方法,没有提供对addrlabels方法的支持。

  所以,这里“Protothreads的简化”只不过一个引子,在了解这种编程思想的真谛后,我们就不难理解如何用各种C编译器实现比Protothreads的addrlabels方法目标代码的执行效率更高的实时协作多任务编程方法。

 

[1] Protothreads - Lightweight, Stackless Threads in C , SICS Technical Report T2005:05

[2] Protothreads: Simplifying Event-Driven Programming of Memory-Constrained Embedded Systems

  Proc. ACM SenSys, Nov 2006.

[3,4] 罗光平等

  使用Protothread简化嵌入式系统中的顺序流控制,单片机与嵌入式系统应用, 2007年11期

  利用Protothread实现实时多任务系统 单片机与嵌入式系统应用  2008年5期

[5] 闫石等 时间触发模式下的Protothreads设计应用 单片机与嵌入式系统应用  2009年1期

[6,7] 最轻量级的C协程库:Protothreads ,2008-05-05

  传说中不可能的任务:在C语言中实现协程 ,2009-01-15

===

fy_zhu

2013-02-12 SV_CA