单片机工程经验 - 时间片
时间片
时间片很好理解,就是把一个任务分成若干个片段,每过一个时间节点就执行一个片段。在无操作系统的情况下,时间片可以非常有效的完成多个任务。它的本质是定时器的复用。
顺序执行
我们来考虑最简单的情况,一盏灯不断闪烁。最简单的方法就是先点亮一盏灯,等待500ms后熄灭,再等待500ms后点亮,如此往复不断循环,大多数的单片机教程里也是这么写的,因为这样很容易理解。
void mian()
{
while(1)
{
open_led();
delay(500);
close_led();
delay(500);
}
}
这里的delay 通常使用的是for 循环的方法,因为单片机每次执行语句都需要一个时钟周期的时间,只要算好delay 的时间需要多少个时间周期就可以准确的进行延时。
但是这里有个问题,因为在delay 中需要一直执行空语句,它会一直霸占MCU,其他代码无法执行,多任务就无法进行。
多任务
如果此时我们需要一盏灯亮灭500ms,另一盏灯亮灭300ms该怎么实现?
其实很简单,我们先找到500和300的最大公约数,也就是100,然后只要delay 一次就将计数值加一,这样只要知道计数值是不是就能知道过去了多少时间呢。
void mian()
{
static unsigned int tick = 0;
while(1)
{
delay(100);
tick++;
if(tick%10 == 0)
{
open_led();
}
else if(tick % 5 == 0)
{
close_led();
}
if(tick%6 == 0)
{
open_led_2();
}
else if(tick % 3 == 0)
{
close_led_2();
}
}
}
tick*100 就是当前程序已经走过了多少ms,对于第一盏灯,只要tick能被10整除,就开灯,如果不能被10整除但能被5整除就灭灯,这样这盏灯就能在500ms关、1000ms开、1500ms关、2000ms开,第二盏灯也同理。
时间片
经过上述变种我们发现,只要将需要做的所有任务的间隔时间取最大公约数,然后通过计数就能实现多任务工作。
但是通常为了简单,我们会直接取一个非常小的时间来计数,比如1ms,这既符合人类的数字使用习惯,也能很好的被几乎所有整数整除。但是这里我们就要思考一个问题,延时1ms就真的是1ms执行一次嘛?
显然不是,因为即使你写的delay 再怎么精准,其他的代码执行也同样需要时间,也许今天的代码执行需要0.5ms,那么你的时间片就是1.5ms,明天的代码执行需要0.7ms,那么你的时间片就是1.7ms。而你如果还是当1ms来计算时间,这种误差会不断的堆叠,最后造成不可预知的后果。
幸好我们的单片机都至少有个硬件定时器,它的时间完全独立于程序,如果将程序放进它的定时器中断中执行就完全不需要考虑由程序执行引起的时间偏移问题。
但这样就真的没有问题了嘛?
我们考虑以下情况,假设我们定时器中断1ms进入一次,而中断中的程序完全执行需要1.3ms,那么会发生什么后果?
上一次中断程序还没执行完全下一次中断已经到来,这样程序会完全陷入到中断中去。这样一来程序的其他中断可能由于优先级问题而完全不会进入,非中断代码则会完全不执行,因此时间片的选择需要大于你的程序执行时间。
大部分时候,我们的中断优先级是需要高于时间片的,因此将程序放在定时器中断中似乎并不是很好的选择。
简易框架
显然存在一种更好的方法,那就是把时间片的计数放到中断中,而将程序放到主循环里,这样既能保证时间片的准确,又能保证其他中断不被时间片的程序影响。这里简单介绍一个时间片框架
static unsigned char flag_1ms;
static unsigned char flag_10ms;
static unsigned char flag_100ms;
static unsigned char flag_1000ms;
void timer_irq()
{
static unsigned char tick_1ms;
static unsigned char tick_10ms;
static unsigned char tick_100ms;
tick_1ms++;
if(tick_1ms >= 10)
{
tick_1ms = 0;
flag_10ms = 1;
tick_10ms++;
if(tick_10ms >= 10)
{
tick_10ms = 0;
flag_100ms = 1;
tick_100ms++;
if(tick_100ms >= 10)
{
tick_100ms = 0;
flag_1000ms = 1;
}
}
}
}
void timer_loop()
{
if(flag_1ms == 1)
{
flag_1ms = 0;
}
if(flag_10ms == 1)
{
flag_10ms = 0;
}
if(flag_100ms == 1)
{
flag_100ms = 0;
}
if(flag_1000ms == 1)
{
flag_1000ms = 0;
}
}
在定时器中断调用timer_irq() 在while(1)大循环中调用timer_loop() ,然后将所有的时间片程序写在timer_loop() 中,这样就能很好的实现时间片轮询系统了。
当然这个系统还很简陋,但它基本能适用所有单片机,了解基本原理,你也可以实现自己的时间片系统。
|