在单片机开发中很多时候都是无操作系统环境,这时候如果要实现异步操作,并且流程逻辑比较复杂时处理起来会稍稍麻烦。这时候可以试试 Protothread 这个协程库。
官网: https://dunkels.com/adam/pt/
Protothreads are extremely lightweight stackless threads designed for severely memory constrained systems, such as small embedded systems or wireless sensor network nodes. Protothreads provide linear code execution for event-driven systems implemented in C. Protothreads can be used with or without an underlying operating system to provide blocking event-handlers. Protothreads provide sequential flow of control without complex state machines or full multi-threading.
Protothreads是为内存严重受限的系统(如小型嵌入式系统或无线传感器网络节点)设计的极为轻量级的无堆栈线程。协议线程为在C中实现的事件驱动系统提供线性代码执行。协议线程可以与底层操作系统一起使用,也可以不与底层操作体系一起使用,以提供阻塞事件处理程序。原线程提供顺序控制流,而无需复杂的状态机或完整的多线程。
这篇文章主要是我自己使用入门记录。具体是实现原理细节等可以参考官网的文档或是下面文章:
《一个“蝇量级” C 语言协程库》https://coolshell.cn/articles/10975.html
从官网下载Protothread库解压后里面就包含了源码、例程和文档:
整个库总共就五个头文件:
pt.h
协程库用户接口;lc.h
用来选择具体协程的实现方式(默认为 lc-switch.h
,可以手动在这里更改);lc-switch.h
使用 C语言 switch/case
语法实现的协程(使用该方式时协程函数中不能使用 switch/case
,可能会冲突);lc-addrlabels.h
使用 gcc label
特性实现的协程(这个依赖GCC编译器);pt-sem.h
信号量实现;pt.h
中几个数据接口和接口如下:
// 协程控制数据结构
struct pt {
lc_t lc;
};
// lc-switch.h中lc_t原型为typedef unsigned short lc_t;
// lc-addrlabels.h中lc_t原型为typedef void * lc_t;
// 以下是协程调度过程中的一些返回状态
#define PT_WAITING 0
#define PT_YIELDED 1
#define PT_EXITED 2
#define PT_ENDED 3
PT_INIT(pt) // 初始化控制数据结构(设置lc=0)
PT_THREAD(name_args) // 声明一个协程的函数(这个用不用无所谓,官方的例程有时候也没用)
PT_BEGIN(pt) // 协程入口
PT_END(pt) // 协程出口
PT_WAIT_UNTIL(pt, condition) // 等待condition为真向下运行,否则跳出当前协程
PT_WAIT_WHILE(pt, cond) // 和PT_WAIT_UNTIL相反,当cond为假向下运行,否则跳出当前协程
PT_WAIT_THREAD(pt, thread) // 等待子协程thread调度完成
PT_SPAWN(pt, child, thread) // 启动子协程thread,并等待其完成。child是子协程的pt
PT_RESTART(pt) // 重置协程
PT_EXIT(pt) // 退出协程
PT_SCHEDULE(f) // 调度一个协程,如果协程还在运行则返回值非0,如果协程退出则返回值为0
PT_YIELD(pt) // 主动出让协程
PT_YIELD_UNTIL(pt, cond) // 等待cond为真向下运行,否则出让当前协程
pt-sem.h
中几个数据接口和接口如下:
// 信号量数据结构
struct pt_sem {
unsigned int count;
};
PT_SEM_INIT(s, c) // 初始化信号量值等于c
PT_SEM_WAIT(pt, s) // 等待信号量可用(>0),向下运行并消耗一个信号量
PT_SEM_SIGNAL(pt, s) // 给出一个信号量
下面是个最简单的演示:
用上信号量的话上面代码可以改写成下面这样:
#include <stdio.h>
#include "pt-sem.h"
static time_t pretime = 0, nowstamp;
// 以下为信号量
static struct pt_sem sem1;
// 以下为协程控制数据
static struct pt pt1;
// 以下为协程函数
static PT_THREAD(protothread1(struct pt *pt)) {
PT_BEGIN(pt); // 协程入口
printf("Protothread1 begin\n\n");
while(1) {
PT_SEM_WAIT(pt, &sem1); // 等待信号量可用,并消耗信号量
time(&nowstamp);
printf("Protothread1 running, current time is %s\n", ctime(&nowstamp));
}
PT_END(pt); // 协程出口
}
int main(void) {
time(&pretime);
PT_INIT(&pt1); // 初始化协程控制数据结构
PT_SEM_INIT(&sem1, 0); // 初始化信号量
while(1) {
protothread1(&pt1); // 运行协程
// 以下代码每2s给出一个sem
time(&nowstamp);
if((nowstamp - pretime) >= 2) {
pretime = nowstamp;
PT_SEM_SIGNAL(&pt1, &sem1); // 给出信号量
}
}
}
下面是一个协程间调用的演示:
#include <stdio.h>
#include "pt.h"
static PT_THREAD(childpt(struct pt *pt)) {
static int counter = 4; // 使用函数内部静态变量保存状态
PT_BEGIN(pt); // 协程入口
printf("childpt begin\n\n");
while(counter--) {
printf("childpt running, counter = %d\n\n", counter);
PT_YIELD(pt); // 主动出让CPU
printf("childpt resume run\n\n");
}
printf("childpt end\n\n");
PT_END(pt); // 协程出口
}
static PT_THREAD(parentpt(struct pt *pt)) {
static struct pt child;
PT_BEGIN(pt); // 协程入口
printf("parentpt begin\n\n");
PT_SPAWN(pt, &child, childpt(&child)); // 调度子协程直至运行结束
printf("parentpt end\n\n");
PT_END(pt); // 协程出口
}
int main(void) {
static struct pt parant;
PT_INIT(¶nt); // 初始化协程控制数据结构
while(PT_SCHEDULE(parentpt(¶nt))); // 调度父协程直至运行结束
while(1);
}
Protothread使用起来比较简单,当然功能也比较简单。另外使用时还有一定的限制,比如使用默认实现时不能在协程中使用 switch/case
,需要在协程中使用静态变量来保存相关数据等。
如果上了操作系统的话,Protothread这种协程相对来说意义一般,但是对于没有操作系统的单片机开发这些来说Protothread就非常好用了。