文章目录
- 目的
- 源码说明
- 使用演示
- 总结
目的
在单片机开发中很多时候都是无操作系统环境,这时候如果要实现异步操作,并且流程逻辑比较复杂时处理起来会稍稍麻烦。这时候可以试试 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 3PT_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是子协程的ptPT_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给出一个semtime(&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); // 主动出让CPUprintf("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就非常好用了。