这里介绍一个基于状态机和时基本的伪多线程。
先铺垫一下定时器调度,
想必做过项目的人都知道,单片机程序里面或多或少需要用到 delay 这个函数,而使用
多了 delay 函数,整个系统就会变得缓慢异常,很多人想,那还不简单,上一个操作系统,
操作系统其实也是一个裸机程序,他是怎么实现高实时性,使得系统运行效率大大增加的呢。
这里先慢慢的从去掉裸机程序里面的 delay 讲起,再讲到操作系统。最终用 51 写一个
时间片轮询的实时内核。
先来看看裸机编程的一般结构:顺序结构。
1.顺序调用程序的一般结构:
#include <reg51.h>
void main()
{
//各种模块初始化
while(1)
{
FUN1();
FUN2();
.........
}
}
顺序结构,我们把程序全部放在一个大的 while 里面不停的循环,里面用到了很多 delay,
delay 这个函数大家都知道是不停的做一些无意义的指令去消耗时间。从而达到延时的目的。
学校学习单片机大部分时间花在了各种模块的驱动上,而忽略了编程框架和思想.
先来看看我们最早在学校学的按键扫描
sbit key=P1^0;
unsigned char keyscan() //返回 0 代表按下,1 代表没按下
{
if(key==0) //说明按键按下
{
delay5(1); //延时 5ms 去抖
if(key==0) //确认按键按下
{
while(key==0);//等待按键释放
return 0;
}
}
return 1;
}
一个按键扫描里面有 5ms 的延时,5ms 延时什么概念,最传统的 51 在 12M 晶
振驱动下,运行了大概 2000-3000 条指令。经历了 5000 个机器周期。而且我的按
钮不释放的话程序会一直停留在 while(key==0);里,如果我还有数码管扫描,那么
数码管扫描就会停掉!怎么去掉这个 delay 和 while 呢?我们先看看用按钮控制流
水灯这个程序。
最开始我们学习了流水灯,然后我们学习用按键控制流水灯方向。但是你们是不是
发现,按键控制流水灯老师一定是让你们用中断来做?
但是能不能用一般的顺序调用来做呢
unsigned char LorR = 0; //0 左移,1 右移动
while(1)
{
if(LorR == 0)
{
//流水灯左移动
}
else
{
//流水灯右移动
}
delay_ms(1000);
Keyscan();
}
很明显,流水灯的 delay_ms 会影响按键扫描,按键扫描里面的 while 会影响流
水灯,所以老师直接让我们用中断方式的按键。所以接下来我们先改造按键扫描程
序。
我们都知道我们单片机有中断,但是中断资源有限啊,我们能不能不用中断达
到类似中断的效果?中断怎么工作的?在上一个机器周期检测到高,在下一个机器
周期监测到低。那么进入中断。那么。我们软件也按这个思路改。
sbit key=P1^0;
unsigned char keyscan() //返回 0 代表按下,1 代表没按下
{
static unsigned char oldkey;
unsigned char returnkey;
//这一次和上一次扫描不一样,并且上一次为高,那么确定有一个按键按下
if(key != oldkey && oldkey == 1)
{
returnkey = 0;
}
else
{
returnkey = 1;
}
olldkey = key;
return returnkey ;
}
在这个程序里,我们再也不需要延时等待,因为我们扫描的是下降沿,而不是电平,
并且我们也不需要等待他的按钮释放。
按键里的延时和 while 去掉了,但是流水灯里的总去不掉把!其实流水灯和按键扫
描就是单片机里面的两种延时,1.可去除延时,2.不可去除的延时。
对于这种必要延时,我们怎么实现效果,又不阻塞其他程序的运行?
生活中,我们怎么等一壶水烧开呢?坐那看着等?我们是不是都是看一下钟,然后
过一会来看一下?怎么在单片机钟实现这种效果?对,我们可以用我们的定时器做一个
钟!很多人会有疑问,直接把流水灯放定时器中断里面就不完了。但是我并不讲这种。
首先,过长的中断服务程序会影响其他中断的响应。而且这样使得一个定时器只能
为一项工作所用,其他任务要用定时器就不方便了。这里引入一个概念,叫任务。把整
个工作分割成一个个的小任务,然后按时间去调度他们。(定时器调度)
定时器调度结构 1
unsigned char task0 = 200,task1=20; //任务 0 流水灯任务,task1,按键扫描任务
TImer0_Init()//1ms 定时器初始化,
while(1)
{
if(task0 == 0)
{
//流水灯,这里 200ms 会进入执行一次流水灯流水操作
task0 = 200;//重新设置倒计时
}
if(task1 == 0)
{
//按键扫描,这里 20ms 会进入执行一次按键扫描操作,
task1 = 20;
}
}
void Timer0_IRQ() interrupt 1 //定时器 0 的中断服务 1ms
{
task0 == 0 ? (task0 = 0) : (task0--);
task1 == 0 ? (task1 = 0) : (task1--);
}
这样就消除了两种延时,但是使用这种结构的话也是有缺点的:
1.任务多了定时器中断服务任务重,
2.每个 task 是共享 cpu,前面的任务不放弃 CPU,后面的就算时间到了,也执行不了
3.移植麻烦,每加一个任务,都需要在定时器里面改
其实很多同学都能想到方法解决缺点 3,这里提示一下,用数组和宏。
定时器调度结构 2
这里再介绍一种定时器调度结构,这种结构更好用,缺点是占用的 RAM 会多一点。
这个方法的原理来源于生活中的时钟,定时器调度结构 1 来源于生活中的倒计时。结构 2
来源于正计时。这里同样是 1ms 的定时器。
unsigned long ntick;
unsigned long task0=0,task1=0;
void millis();
TImer0_Init()//1ms 定时器初始化,
while(1)
{
if((millis()-task0) > 200)
{
//流水灯任务
task0= millis();//这次执行完后,把这次做完的时间记下来
}
if((millis()-task1) > 20)
{
//按键扫描任务
task1= millis();
}
}
void millis() //得到系统从启动到现在所经历的时间单位 ms。50 天后清零
{
return ntick;
}
void Timer0_IRQ() interrupt 1 //定时器 0 的中断服务 1ms
{
//......
ntick++;
}
这种方式,相当于我要求你十分钟做一个俯卧撑,你每次做完记录下我这次做
完的时间,然后过十分钟再做。这个方法比定时器调度 1 容易扩展得多。但是更费
RAM,因为使用了大量的 long
铺垫完毕。
现在我们的单片机有了一个表。现在,我们需要只要给单片机一个计划表,让他到时间按顺序,执行,那岂不是美滋滋。
好,比如我要做的这件事,有3个步骤,每个步骤都要等待片刻,我又不想傻等。我手上有表。
那我们给三个步骤安排成状态1,状态2,状态3把,分别为 S1,S2,S3
OKOK 现在,我们流程大概是,初始化->等待20ms->S1->等待15ms->S2->等待100ms->执行状态3->结束
那么有表我们怎么做呢!!.先运行初始化操作,然后去干别的,过一会,看一下表,时间到了没,到了,去执行S1,如此循环,
前面我们的millis函数相当于看钟。OKOK
那么我们用一个变量表示已经做到的状态把。state.这里先不定义数据类型。你就当作是auto把。
接着,看一个结构。
if((millis() - ntime) >= 100)
{
ntime = millis();
}
这个结构前面出现过哈,就是不停的看表,时间到了。进去执行,执行完记录新时间,如此周期执行,那么我们就要改造他了。使得每次能进入的时间不变
if((millis() - ntime) >= t)
这样写,这样写了,我们只需要改变t的值,就可以控制下一次进入的时间。
if((millis() - ntime) >= t)
{
switch(state)
{
case S1:state = S2;t=20;
case S2:state = S3;t=15;
case S3:state = S1;t=100;
}
}
结构如此。这样,我们不光控制了每次进入的时间,还控制了,每次进入的点,宏观上看,他似乎,在按我们的要求执行,又没柱塞单片机?
对的。那我们每个程序都这样分割是不是太累了。对。我们用宏包起来啊!。
前面公用部分if((millis() - ntime) >= t)
{
switch(state)
{
这部分是不变的。对不对,然后我们每加一个状态,就是一个case ,如果我们直接取行号,作为状态,那是不是就不需要绞尽脑汁去给状态取名字了。
下面就是结束符号。
然后我们要知道一个宏 _lc
下面封装就简单了。
#define OS_SS static uint32_t ddtime = 0,Task_Tick = 0;static uint16_t _lc=0;if((millis()-Task_Tick) >= ddtime){Task_Tick = millis();switch(_lc){default:
剖析一下,就是每次进入,重新配置下次进去的时间,然后switch到相应的断点。
#define OS_EE break;}}
//等待N个时间片 每个时间片初始值是1ms,可以重新对Timer0初始化得到其他时间片长度,建议时间片长度不少于1000个机器周期,否则任务调度花费会大大增加!
#define WaitTick(a) _lc = __LINE__;ddtime = a;return 255;case __LINE__:_lc = 0;ddtime=0;
总体思想就介绍完了。这样我们就可以在一个单片机上,执行出多个单片机的效果。但是。
WaitTick不能运行在另一个switch case里面,原因你懂的,如果是gcc,可以用goto去实现,这样就没有限制了。
伪线程思想,其实就是从任务分割而来。把一个任务分割多个,每个任务定时器调度。今天上传不了代码了。改天上传。
|