当前位置: 首页 > 工具软件 > Protothreads > 使用案例 >

ProtoThreads在Arduino中的应用#多任务处理#

谭卜鹰
2023-12-01
想象一个情景——

一个四位的数码管,由于要“同时”显示,因此每5ms刷新一次。
同时要处理一个矩阵键盘,设计是每10ms扫描一行,同时还有去抖处理,需要在检测到按键后再延时40ms检查一次。

检测到有效按键,在数码管上显示某个值,比如1234。
同时还能从串口接收数据,如果有数据收到,马上在数码管上显示某个值,比如5678,停留1s,期间按矩阵键盘不会有任何反应。

程序怎么写?

比如说,去抖的时候,如果直接用delay(40)的话,那数码管的5ms刷新怎么办?串口收到的数据办?

基于这种多任务处理,为了编程方便,让我们祭出嵌入式操作系统这一个神器!!!

嵌入式操作系统是用来处理这类超烦的(划掉)多任务处理的情况,常见的有uCos、RT-Thread等等,有兴趣的可以去看看。

但是Arduino,编译一个文件出来,如果你有留意的话,体积很大,而arduino本身的内存就不多,再移植一个就Orz了。

鉴于大家做些小作品,不需要用到如此高深的操作系统,只要简单地处理一下这些多任务的问题,所以,让我们祭出Adam Dunkels大神的ProtoThreads

二、ProtoThreads与嵌入式操作系统简介

ProtoThreads是一个通过宏(#define)写出来的神奇的模拟多线程(理解成多任务先)的库,里面全是头文件,找不到.cpp等程序文件。它的核心利用了C语言switch语句的特性。说是嵌入式系统,但这其实还只算是一个调度器,所以,并不能说是一个完整的操作系统。

操作系统最核心的功能是:在等待某个事件发生的时候,比如说定时一段时间、有无按键、串口上有无数据等等,操作系统帮你将单片机从当前的任务中临时切换到另一个任务运行,直到指定事件发生了再回来接着运行,这样就是变相实现了多任务处理,节省了CPU时间,还极大地减少开发难度(我会说我学过嵌入式系统后就再也不想做流水线式的设计了吗?)。

ProtoThreads在较大程度上实现出操作系统的核心功能,而且,每个新建一个任务,只需额外增加16bit即两个字节的空间(引入我的定时器宏则为6个字节)。除了核心功能外,还增加了信号量、延时这两个功能(仅限于我提供的库),我大概想到消息队列、标志怎么写了,但是没空写。

但是!!有缺点!!我说过了,它利用了switch语句的特性,所以,我非常不建议在任务中使用switch这个语句,除非你能保证在你的switch语句内不会切换任务。其次,请慎用内部变量,尤其是循环变量,在切换任务时有一定的可能性发生不可预料的错误,要用,请一定加上static修饰。

讲完,下面讲讲怎么用。

三、任务准备工作

首先,每个任务都必须要有一个记录变量,记录任务的状态,便于返回。语句:
static struct pt xxx;

这个xxx你们自己取好了。下面的全部都是xxx。请一定要加上前面的static struct。

好,然后要初始化一个任务。在setup()函数里面用这个语句:
PT_INIT(&xxx);

这样就初始化成功啦~记得要加个&符号。

四、编写任务

每个任务在程序里面,就算是一个独立函数,在里面处理你要做的东西就可以啦。

函数格式如下:
static int 任务名(struct pt *pt)
{
PT_BEGIN(pt);
//你的处理过程
PT_END(pt);
}

如果不太懂ProtoThreads内部结构,就只改任务名好了,然后就这样写。PT_BEGIN(pt);一定要在开头,PT_END(pt);一定要在结尾,别漏,否则编译错误,运行到这里的时候这个任务就彻底结束了。

由于处理过程一般是循环处理的,所以处理过程一般是while(1){处理内容},作用就像loop(){}函数啦。

下面是一些等待某种信号所用到的宏:(部分,我没有全部讲,只挑了一些常用的,有兴趣的可以自己看源代码)

PT_WAIT_UNTIL(pt,条件);
这个语句的功能是,如果条件不成立,那么暂时退出当前任务,先处理别的任务,再回来看看。如果条件成立了,那么继续往下执行。第一个变量pt我个人建议别改啦。

PT_WAIT_WHILE(pt,条件);
作用和上面的相反,条件成立则切换任务,条件不成立则继续执行。

PT_WAIT_THREAD(pt,任务x名);
等到任务x完成了(任务x运行到PT_END了)才继续执行。x应为一次性任务而不是循环任务。

PT_RESTART(pt);
重启当前任务

PT_EXIT(pt);
退出并注销当前任务

PT_YIELD(pt);
把CPU让给别的任务用一下下,用完了我再继续用。

(下面的是定时器,该宏是我自己写的,用之前请在#include “pt.h” 的前面,前面啊!加上一句#define PT_USE_TIMER)

先说明一下,下面的定时不一定完全准确的,可能会有点点的误差,可能偏后。如果遇上了很烦的任务,有可能会使延时延后。但是正常情况下,直接用就好了。

如果要很精确的延时,请用delay语句或者计时器,但是,绝大多数情况下,绝大多数情况!绝大多数情况!请用下面的语句代替delay延时!这样才能把CPU让给别的任务使用。

PT_TIMER_DELAY(pt,延时毫秒数);
字面上的意思,不用多说了吧?最大值约为49.7天,估计没人会延时那么久

PT_TIMER_MICRODELAY(pt,延时微秒数);
字面上的意思,不用多说了吧?注意,最小精度与arduino的版本有关,与micros()有精度一致。

PT_TIMER_WAIT_TIMEOUT(pt,条件,毫秒数);
如果条件成立了,或者超时了,就继续运行,否则切换任务。

五、信号量(Semaphore)

停车场。停车。停车场里面的车位是固定的,假设没有一辆车占多个车位的情况。在这种情况下,剩余车位数就是一个“信号量”了。进一量车,剩余车位数就减一;出一量车,剩余车位数就加一。如果剩余车位数为0,那么想进来的车就只能在外面淋雨了
对,信号量也是这样用。得到了一个信号量,任务继续运行,得不到,一边呆着去。

具体有什么用呢?比如说,一楼写着的,监控串口的任务读到数据了,要占用数码管。那么我们命令一个信号量为土豪,土豪只有一个。每次矩阵键盘要显示数据,先申请一个土豪,写数据,然后释放土豪,如果申请不到就在墙角不断画圈圈。监控串口的任务一旦申请到土豪就劫持1s,不让矩阵键盘用。这样就可以达到要求啦~

下面是用法:
要用的话,请在#include "pt.h"前面加上一句 #define PT_USE_SEM

首先要创建一个信号量,这个一定是全局变量:
static struct pt_sem 信号量名;

接着请在setup()函数里面给它初始化:
PT_SEM_INIT(&信号量名,数量);
信号量名前面有个&,别忘了。数量就相当于停车场的总车位数。

然后要用啦。任务要停一辆车进去:
PT_SEM_WAIT(pt,&信号量名);
信号量名前面有个&,别忘了。一个语句只能停一辆车,土豪好多车就用多次。

任务要开一辆车出来:
PT_SEM_SIGNAL(pt,&信号量名);
信号量名前面有个&,别忘了。用一次出一辆。

当然,对于一个任务来说,信号量没上限,就是说,你可以在停车场内再开辟新的车位,不断用PT_SEM_SIGNAL()就好了。

其实信号量这货解决的问题中,比较出名的是生产者与消费者问题。简单地说,消费者要买,必须要生产者生产才能买到,没生产出来,消费者只能等。

六、例子

大家翻了那么久都累了伐……给个例子呗……
要求:板载LED以4秒一周期的速率闪烁。一旦收到串口发来的信息,不管信息量多少,快闪5次。用ProtoThreads写。

//首先启用定时器库和信号量库,下面会用到
#define PT_USE_TIMER
#define PT_USE_SEM
//引用库
#include "pt.h"
static struct pt thread1,thread2; //创建两个任务
static struct pt_sem sem_LED; //来个LED的信号量,同一时间只能一个任务占用
unsigned char i; //循环变量,写在这里其实不合适

void setup() {
//初始化13口和串口
pinMode(13,OUTPUT);
Serial.begin(115200);


PT_SEM_INIT(&sem_LED,1); //初始化信号量为1,即没人用
//初始化任务记录变量
PT_INIT(&thread1);
PT_INIT(&thread2);
}
//这是LED慢速闪烁的任务
static int thread1_entry(struct pt *pt)
{
PT_BEGIN(pt);
while (1)
{
PT_SEM_WAIT(pt,&sem_LED); //LED有在用吗?
//没有
digitalWrite(13,!digitalRead(13));
PT_TIMER_DELAY(pt,1000);//留一秒
PT_SEM_SIGNAL(pt,&sem_LED);//用完了。
PT_YIELD(pt); //看看别人要用么?
}
PT_END(pt);
}

//这是LED快速闪烁的任务,如果有串口消息,快速闪5次
static int thread2_entry(struct pt *pt)
{
PT_BEGIN(pt);
while (1)
{
PT_WAIT_UNTIL(pt, Serial.available()); //等到有串口消息再继续
PT_SEM_WAIT(pt,&sem_LED);//我要用LED啊!
//抢到使用权了,虐5次
for (i=0;i<5;i++)
{
digitalWrite(13,HIGH);
PT_TIMER_DELAY(pt,200);
digitalWrite(13,LOW);
PT_TIMER_DELAY(pt,200);
}
while (Serial.available())
Serial.read();
//清空串口数据,防止又来
PT_SEM_SIGNAL(pt,&sem_LED); //归还LED使用权了
}
PT_END(pt);
}
void loop() {
//依次调用即可
thread1_entry(&thread1);
thread2_entry(&thread2);
}
七、后记

版权问题:ProtoThreads的基本代码是由Adam Dunkels编写了,详情请看Readme.md,我个人只扩展了pt-timer.h这一个库。转载及使用ProtoThreads的基本代码请遵循Adam Dunkels的声明。转载及使用我写的pt-timer.h请署名“逍遥猪葛亮”。

转载地址:
https://www.arduino.cn/thread-5833-1-1.html

 类似资料: