-
pt-thread 是一种名称为protothreads的新型抽象编程模型的简称。主要设计用于内存受限严重的嵌入式系统,实际上在MCU裸机编程中,我们经常使用状态机模型去编写这种运行在MCU上的事件驱动程序。而 pt-thread 的最大优点是允许我们使用一种不消耗堆栈并且类似线程的风格代替状态机风格的代码。
-
pt-thread 是作者Adam Dunkels在开源BSD协议许可下发布,允许用于商业、非商业用途,商用时可能需要申请相关license。
-
相关特性:
- 纯C语言编写,不依赖硬件
- 每个抽象的线程结构体只占2byte
- 支持阻塞操作,并且没有堆栈的切换
- 支持裸机 OS
- 通过链接http://dunkels.com/adam/pt/download.html进行下载相关版本的ptthread库
- 以 pt-1.4 版本的使用为例,只需要下载后将下面几个关键文件导入工程中,include "pt.h"之后就能直接使用相关接口
下面例程主要使用两个任务的调度轮转来展示 PT-Thread 的一般用法
任务A:通过按键中断触发,优先级高可阻塞任务B
任务B:长时间运行,但是优先级低,会被任务A阻塞
#include "gpio.h"
#include "irq.h"
#include "pt.h"
unsigned char taskA_flag, taskB_flag = 0;
void irq_handler (void)
{
unsigned int irq = irq_type_get();
if (irq == GPIO_IRQ)
{
if (0 == gpio_read(KEY_PIN))
{
delay_ms(10); //消抖
if (0 == gpio_read(KEY_PIN))
{
while (0 == gpio_read(KEY_PIN)) // 等待释放
taskA_flag = 1;
taskB_flag = 0; // 阻塞任务B
}
}
}
}
static
PT_THREAD(taskA(struct pt *pt))
{
PT_BEGIN(pt);
PT_WAIT_UNTIL(pt, taskA_flag != 0);
printf("taskA is running");
taskA_flag = 0;
taskB_flag = 1; // 执行完taskA,取消taskB的阻塞
PT_END(pt);
}
static
PT_THREAD(taskB(struct pt *pt))
{
taskB_flag = 1;
PT_BEGIN(pt);
delay_ms(10);
printf("taskB-1 is running");
taskB_flag = 0;
PT_WAIT_UNTIL(pt, taskB_flag != 0);
delay_ms(10);
printf("taskB-2 is running");
taskB_flag = 0;
PT_WAIT_UNTIL(pt, taskB_flag != 0);
delay_ms(10);
printf("taskB-3 is running");
taskB_flag = 0;
PT_WAIT_UNTIL(pt, taskB_flag != 0);
PT_END(pt);
}
int main(void)
{
gpio_init();
...
while (1)
{
PT_SCHEDULE(taskA(&ptA));
PT_SCHEDULE(taskB(&ptB));
}
return 0;
}
- 运行结果:当没有按下按键时,任务B中的每一部分被PT_WAIT_UNTIL分割的子任务将依次在while(1)中执行;当按键按下后,任务A将被执行,并且阻塞任务B,松开按键,任务B继续跳转到上次执行的位置执行剩余的子任务;
本文只介绍了简单的任务调度切换的一些接口的基础用法,pt-thread还提供一套信号量的接口,具体的实现和用法可以参考pt.c
简单介绍例程中使用到的相关接口的实现:实际上pt-thread支持2种实现方式,分别是swich 、 goto,分别对应源码中的 lc-switch.h lc-addrlabels.h,本例程中使用的是lc-swich.h的实现方式;
// 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_THREAD(name_args) char name_args
#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0;
PT_INIT(pt); return PT_ENDED; }
// 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) }
-
无论是switch还是goto的实现方式,都是通过保存PTHREAD创建的任务在切换之前执行的代码行数,当任务被恢复时,又从这一行开始往下执行,这是对C语言switch语句的灵活应用,用case来记录上次执行的任务位置,也因为使用的是switch,在task中定义的变量的作用域也仅限于switch代码段中,因此使用局部变量要非常小心。
-
进一步把宏替换,有助于我们理解它的实现方式,下面为任务B把PT中使用的宏替换后的代码:
static
char taskB(struct pt *pt)
{
taskB_flag = 1;
//PT_BEGIN(pt); ↓↓↓↓↓↓↓↓
char PT_YIELD_FLAG = 1;
switch (pt->lc)
{
case 0:
delay_ms(10);
printf("taskB-1 is running");
taskB_flag = 0;
//PT_WAIT_UNTIL(pt, taskB_flag != 0);↓↓↓↓↓↓↓↓
pt->lc = __LINE__;
case __LINE__:
if(!(taskB_flag != 0))
return PT_WAITING;
delay_ms(10);
printf("taskB-2 is running");
taskB_flag = 0;
//PT_WAIT_UNTIL(pt, taskB_flag != 0);↓↓↓↓↓↓↓↓
pt->lc = __LINE__;
case __LINE__:
if(!(taskB_flag != 0))
return PT_WAITING;
delay_ms(10);
printf("taskB-3 is running");
taskB_flag = 0;
//PT_WAIT_UNTIL(pt, taskB_flag != 0);↓↓↓↓↓↓↓↓
pt->lc = __LINE__;
case __LINE__:
if(!(taskB_flag != 0))
return PT_WAITING;
//PT_END(pt);
PT_YIELD_FLAG = 0;
pt->lc = 0;
return PT_ENDED;
}
- 至此,关于PT-Thread的分享完毕



