一、以下转自http://www.arduino.cn/thread-5833-1-1.html
1楼、背景——
想象一个这样的情况,请不要在意这样奇怪的情景——
一个四位的数码管,由于要“同时”显示,因此每5ms刷新一次。(求别说MAX7219之类的IC……)
同时要处理一个矩阵键盘,设计是每10ms扫描一行,同时还有去抖处理,需要在检测到按键后再延时40ms检查一次。
检测到有效按键,在数码管上显示某个值,比如1234。
同时还能从串口接收数据,如果有数据收到,马上在数码管上显示某个值,比如5678,停留1s,期间按矩阵键盘不会有任何反应。
程序怎么写?晕了没?
比如说,去抖的时候,如果直接用delay(40)的话,那数码管的5ms刷新怎么办?串口收到的数据办?
基于这种超烦的(划掉)多任务处理,为了编程方便,让我们祭出嵌入式操作系统这一个神器!!!
哎!别走!!!妹子等我说完,我不打算讲高深理论哎!!(旁白:反正你也不是这个专业的,也讲不出)
嵌入式操作系统是用来处理这类超烦的(划掉)多任务处理的情况,常见的有uCos、RT-Thread等等,有兴趣的可以去看看。
但是Arduino,编译一个文件出来,如果你有留意的话,体积很大,而arduino本身的内存就不多,再移植一个就Orz了。所以,上面说的,不能用!!!哎!别走啊喂!!!
鉴于大家做些小作品,不需要用到如此高深的操作系统,只要简单地处理一下这些多任务的问题,所以,让我们祭出Adam Dunkels大神的ProtoThreads!
2、ProtoThreads与嵌入式操作系统简介
ProtoThreads是一个通过宏(#define)写出来的神奇的模拟多线程(理解成多任务先)的库,里面全是头文件,找不到.cpp等程序文件。它的核心利用了C语言switch语句的特性。说是嵌入式系统,但这其实还只算是一个调度器,所以,并不能说是一个完整的操作系统。
操作系统最核心的功能是:在等待某个事件发生的时候,比如说定时一段时间、有无按键、串口上有无数据等等,操作系统帮你将单片机从当前的任务中临时切换到另一个任务运行,直到指定事件发生了再回来接着运行,这样就是变相实现了多任务处理,节省了CPU时间,还极大地减少开发难度(我会说我学过嵌入式系统后就再也不想做流水线式的设计了吗?)。
ProtoThreads在较大程度上实现出操作系统的核心功能,而且,每个新建一个任务,只需额外增加16bit即两个字节的空间(引入我的定时器宏则为6个字节)。除了核心功能外,还增加了信号量、延时这两个功能(仅限于我提供的库),我大概想到消息队列、标志怎么写了,但是没空写(旁白:其实是懒吧?)。
但是!!有缺点!!我说过了,它利用了switch语句的特性,所以,我非常不建议在任务中使用switch这个语句,除非你能保证在你的switch语句内不会切换任务。其次,请慎用内部变量,尤其是循环变量,在切换任务时有一定的可能性发生不可预料的错误,要用,请一定加上static修饰。
讲完,下面讲讲怎么用。
3、任务准备工作
首先,每个任务都必须要有一个记录变量,记录任务的状态,便于返回。语句:
static struct pt xxx;
这个xxx你们自己取好了。下面的全部都是xxx。请一定要加上前面的static struct。
好,然后要初始化一个任务。在setup()函数里面用这个语句:
PT_INIT(&xxx);
这样就初始化成功啦~记得要加个&符号。(一定要哦~)
4、编写任务
每个任务在程序里面,就算是一个独立函数,在里面处理你要做的东西就可以啦。
函数格式如下:
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,条件,毫秒数);
如果条件成立了,或者超时了,就继续运行,否则切换任务。
又说完啦~就这些~
5、信号量(Semaphore)
妹子求别走T_T
我不得不解释一个专业术语,因为这二货很有用(信号量:你才二)……
我不上定义了,直接用例子说,不是我发明的例子。
停车场。停车。停车场里面的车位是固定的,假设没有一辆车占多个车位的情况。在这种情况下,剩余车位数就是一个“信号量”了。进一量车,剩余车位数就减一;出一量车,剩余车位数就加一。如果剩余车位数为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()就好了。
其实信号量这货解决的问题中,比较出名的是生产者与消费者问题。简单地说,消费者要买,必须要生产者生产才能买到,没生产出来,消费者只能等。
6、例子
大家翻了那么久都累了伐……给个例子呗……
要求:板载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);
}
7、后记
版权问题:ProtoThreads的基本代码是由Adam Dunkels编写了,详情请看Readme.md,我个人只扩展了pt-timer.h这一个库。转载及使用ProtoThreads的基本代码请遵循Adam Dunkels的声明。转载及使用我写的pt-timer.h请署名“逍遥猪葛亮”。
欢迎转载、使用、修改等等,提一提“逍遥猪葛亮”我会很高兴的。百度Arduino吧里面的是我写的,所以不存在非法复制粘贴的问题吧?
二、以下转自http://www.geek-workshop.com/forum.php?mod=viewthread&tid=610&extra=page%3D1
先上一段简单的代码look look
#include <pt.h> static int counter1,counter2,state1=0,state2=0; static int protothread1(struct pt *pt) { PT_BEGIN(pt); while(1) { PT_WAIT_UNTIL(pt, counter1==1); digitalWrite(12,state1); state1=!state1; counter1=0; } PT_END(pt); } static int protothread2(struct pt *pt) { PT_BEGIN(pt); while(1) { PT_WAIT_UNTIL(pt, counter2==5); counter2=0; digitalWrite(13,state2); state2=!state2; } PT_END(pt); } static struct pt pt1, pt2; void setup() { pinMode(12,OUTPUT); pinMode(13,OUTPUT); PT_INIT(&pt1); PT_INIT(&pt2); } void loop () { protothread1(&pt1); protothread2(&pt2); delay(1000); counter1++; counter2++; }
#include <pt.h>//ProtoThreads必须包含的头文件 static int counter1,counter2,state1=0,state2=0; //counter为定时计数器,state为每个灯的状态 static int protothread1(struct pt *pt) //线程1,控制灯1 { PT_BEGIN(pt); //线程开始 while(1) //每个线程都不会死 { PT_WAIT_UNTIL(pt, counter1==1); //如果时间满了1秒,则继续执行,否则记录运行点,退出线程1 digitalWrite(12,state1); state1=!state1;//灯状态反转 counter1=0; //计数器置零 } PT_END(pt); //线程结束 } static int protothread2(struct pt *pt) //线程2,控制灯2 { PT_BEGIN(pt); //线程开始 while(1) { //每个线程都不会死 PT_WAIT_UNTIL(pt, counter2==5); //如果时间满了5秒,则继续执行,否则记录运行点,退出线程2 counter2=0; //计数清零 digitalWrite(13,state2); state2=!state2; //灯状态反转 } PT_END(pt); //线程结束 } static struct pt pt1, pt2; void setup() { pinMode(12,OUTPUT); pinMode(13,OUTPUT); PT_INIT(&pt1); //线程1初始化 PT_INIT(&pt2); //线程2初始化 } void loop () //这就是进行线程调度的地方 { protothread1(&pt1); //执行线程1 protothread2(&pt2); //执行线程2 delay(1000); //时间片,每片1秒,可根据具体应用设置大小 counter1++; counter2++; }看上面的代码,你会发现很多大写的函数,其实那些都是些宏定义(宏定义用大写是约定俗成的..),如果把这些宏都展开你就能更好的理解他的原理了:
#include <pt.h>//ProtoThreads必须包含的头文件 static int counter1,counter2,state1=0,state2=0; //counter为定时计数器,state为每个灯的状态 static int protothread1(struct pt *pt) //线程1,控制灯1 { { char PT_YIELD_FLAG = 1; switch((pt)->lc) {//用switch来选择运行点 case 0: //此乃初始运行点,线程正常退出或刚开始都从这开始运行 while(1) //每个线程都不会死 { do { (pt)->lc=12;//记录运行点 case 12: if(!(counter1==1)) { return PT_WAITING; //return 0 } } while(0) digitalWrite(12,state1); state1=!state1;//灯状态反转 counter1=0; //计数器置零 } } PT_YIELD_FLAG = 0; pt->lc=0; return PT_ENDED; // return 1 } } static int protothread2(struct pt *pt) //线程2,控制灯2 { { char PT_YIELD_FLAG = 1; switch((pt)->lc) {//用switch来选择运行点 case 0: //线程开始 while(1) //每个线程都不会死 { do { (pt)->lc=39; case 39://记录运行点 if(!(counter2==5)) { return PT_WAITING; //return 0 } } while(0) counter2=0; //计数清零 digitalWrite(13,state2); state2=!state2; //灯状态反转 } } PT_YIELD_FLAG = 0; pt->lc=0; return PT_ENDED; // return 1 } } static struct pt pt1, pt2; void setup() { pinMode(12,OUTPUT); pinMode(13,OUTPUT); pt1->lc=0; //线程1初始化 pt2->lc=0; //线程2初始化 } void loop () //这就是进行线程调度的地方 { protothread1(&pt1); //执行线程1 protothread2(&pt2); //执行线程2 delay(1000); //时间片,每片1秒,可根据具体应用设置大小 counter1++; counter2++; }
三、MALC还做了个带定时器的库http://www.geek-workshop.com/thread-666-1-1.html
#include <PT_timer.h> #include <pt.h> int servopin=8;//定义舵机接口数字接口7 int myangle=100;//定义角度变量 int val=0; static struct pt pt1,pt2; PT_timer servotimer; static int servoMove(struct pt *pt) { PT_BEGIN(pt); while(1) { servotimer.setTimer(25); PT_WAIT_UNTIL(pt,servotimer.Expired()); int pulsewidth;//定义脉宽变量 myangle%=156;//视舵机而定,防止越界 pulsewidth=myangle*11+500;//将角度转化为500-2205的脉宽值 digitalWrite(servopin,HIGH);//将舵机接口电平至高 delayMicroseconds(pulsewidth);//延时脉宽值的微秒数 digitalWrite(servopin,LOW);//将舵机接口电平至低 } PT_END(pt); } String inString = ""; static int angleRead(struct pt *pt) { PT_BEGIN(pt); while(1) { PT_WAIT_UNTIL(pt, Serial.available()>0); int inChar = Serial.read(); if (isDigit(inChar)) inString += (char)inChar; if (inChar == ' '||inChar=='\n') { myangle=inString.toInt(); Serial.print("myangle="); Serial.println(myangle); inString = ""; } } PT_END(pt); } void setup() { pinMode(servopin,OUTPUT);//设定舵机接口为输出接口 PT_INIT(&pt1); PT_INIT(&pt2); Serial.begin(9600); } void loop()// { servoMove(&pt1); angleRead(&pt2); }