Protothreads用途
协程库听起来很高大尚
其实他就是类似于一种调度器,他能模拟多线程
如果传统你要实现灯在闪烁的时候同时再实现其他功能的时候
那就要靠定时去实现,比如谁时间到了就执行谁的代码
当然如果不想用定时器,就可以用到Protothreads协程库开发
他就是可以协调程序的运行的先后,相当于是让程序可以同时运行
实现伪多线程
缺点:
所有线程必须是非阻塞的,延时使用定时器 + 条件判断(如 timer_elapsed())
严禁在 PT 线程内调用 delay_ms() 等忙等或阻塞函数。

我们单片机模拟的是图中的第二种情况
Protothreads原理
传统 RTOS 线程切换需要保存全部寄存器、切换堆栈指针(SP),代价数百字节 RAM 和几十个 CPU 周期。
Protothreads 的思路是:既然函数调用返回后再进来会从头执行,那我能不能记住上次执行到了函数的哪一行,下次直接跳过去?
C 语言提供了一个天然的支持——switch 配合 case 标签。
每个 case 都是一个明确的跳转目标,只要我们把函数的当前执行位置记录为一个整数(行号),下次进入时先用 switch 跳到那个整数对应的 case,然后继续往下执行,就实现了“暂停并恢复”。
所以Protothreads它不需要任何汇编,完全由标准 C 宏实现。
案例
以下是最简单的一个线程:
1 | static struct pt pt; |
经过预处理器展开后等价于:
1 | void example(struct pt *pt) |
执行过程:
第一次调用:pt->lc初始为0,switch 跳到 case 0,执行到 pt->lc = 10; case 10:,此时如果按键没按下,则 return,函数结束,pt->lc 停留在 10。
第二次调用:switch(pt->lc) 因为 lc==10 直接跳到 case 10,接着判断 button_pressed()。若按下,跳出 do-while,执行 led_on(),然后线程结束。
宏观上,程序员编写的是线性顺序代码,而运行起来却是事件驱动的状态机。代码写法是线程,执行本质是状态机。
| 宏 | 作用 | 原理 |
|---|---|---|
| PT_BEGIN(pt) | 定义恢复点 | 展开为 switch((pt)->lc) { case 0:,把函数入口变成跳转表 |
| PT_END(pt) | 结束线程 | 闭合 switch 的花括号,并重置 lc |
| PT_WAIT_UNTIL(pt, cond) | 等待条件为真,否则暂停 | 记录当前行号到 lc,添加 case __LINE__:,如果条件为假则 return |
| PT_WAIT_WHILE(pt, cond) | 等待条件为假 | 等价于 PT_WAIT_UNTIL(pt, !(cond)) |
| PT_INIT(pt) | 初始化线程 | 将 lc 置为 0 |
注意
不能在 PT_WAIT_ 之外嵌套使用 switch
因为宏内部生成了 case __LINE__:,如果用户代码也有 switch,会意外地把 case 插入到用户的 switch 里面,导致跳转混乱。
解决办法:如果必须在 PT 线程里写 switch,可以在 switch 之前先 PT_WAIT,或者将 switch 放在单独的函数中调用。
跨等待保留的局部变量必须声明为 static
每次等待时函数会 return,普通局部变量的生命周期结束。下一次调用时,函数栈重新创建,变量会丢失值。static 变量保存在全局区,不会随函数返回丢失。
Protothreads使用
Protothreads协程库有三种实现模式
switch/case 模式
基于switch/case和行号实现, 每个协程由pt_begin开始, 先放置一个switch语句, 然后再协程控制点添加case标签。
协程执行的时候, 根据记录的行号跳转到对应的case分支。
限制:
1.每行不允许超过一个标签
2.不能保存本地变量
头文件
我们只需要在main.c里面头文件加上这个就可以
1 | #include "pt.h" |
线程控制块
然后我们用static struct pt xxx去声明线程控制块
1 | static struct pt pt_led; |
编写线程函数
按照static PT_THREAD(xxxx (struct pt *pt)) 这样写函数
xxxx是自己新定义的线程函数
1 | static PT_THREAD(task_led(struct pt *pt)) |
PT_BEGIN(pt) 标记线程函数的开始。
PT_END(pt) 标记线程函数的结束,线程结束并返回 PT_ENDED。
PT_WAIT_UNTIL(pt, 条件) 阻塞线程,直到 条件 为真时才继续往下执行。可以替代delay
添加进主函数
PT_INIT(pt) 初始化一个 struct pt 变量。
PT_INIT(&xxx); xxx是线程控制块
xxxx(&xxx); xxxx是上面的线程函数
1 | int main(void) |
其他的api:
PT_YIELD(pt) 主动让出 CPU,下次被调用时从下一句继续执行。其实就是PT_WAIT_UNTIL(pt, 0)
PT_WAIT_WHILE(pt, thread) 条件循环等待(等待条件不成立)等于 PT_WAIT_UNTIL(pt, !(条件))
PT_SPAWN(父pt, 子pt结构体, 子协程函数(子pt)); 作用:阻塞父协程,直到子协程执行结束才继续往下跑。
补充:
变量的话一定要是static int a或者是全局变量,千万不能是int a因为这样的话退出的话就会被注销掉变量
goto 模式
基于goto和标签引用实现, 每个协程由pt_begin开始, 通过记录的标签地址, 进行跳转。
限制:
1.需要GCC或Clang编译器(要求编译器支持 Labels as Values)
2.不能保存本地变量
头文件
1 | #define PT_USE_GOTO |
setjmp/longjmp 模式
基于系统函数setmp/longjmp实现, 每个协程由pt_begin开始,通过setjmp恢复寄存器数据,继续执行;
然后再协程的控制点保存寄存器数据, 让出控制权限。
其他和上面的一样
限制:
1.慢
2.可能会擦除全局寄存器变量
头文件
1 | #define PT_USE_SETJMP |
主函数
1 | jmp_buf scheduler_env; // 必须定义全局跳转环境 |
其他和上面的一样