您的位置:首页 > 其它

实时操作系统的忙等延迟实现

2016-07-19 11:10 281 查看

摘要

在嵌入式系统开发里,经常会遇到要将一个操作“延迟”一段时间执行的情况,下面的一段led blink就是一段最简单而标准的案例(据说是嵌入式领域的hello world):

while(1) {
LED_ON;
Sleep(1000);
LED_OFF;
Sleep(1000);
}

LED第一秒点亮,接下来第二秒灭掉,第三秒点亮,第四秒再灭掉,如此周而复始。这里的sleep函数一般都是将Task暂时设为睡眠或者挂起的状态,同时在后台有一个计数的机制,待时间超时,再将任务重新唤醒。也就是说,这是一个“闲等”的实现,在sleep的时间里会将CPU让出去给其它Task以运行的机会。虽然说主动和别人share CPU是一种好的习惯,但有的时候,我们也需要另外一种“忙等”的机制,考虑以下几个场景:

1 在做外设驱动时,有时候某些寄存器配置下去不会立即生效,而可能需要一小段时间等待,外设硬件的状态才会正确

2 键盘驱动程序,在判断到一次按键按下之后,需要做去抖,防止一次按下触发多次重复操作,一般需要加一个几毫秒的忙等机制

3 做过单片机的同学都知道,经常要用IO接口输出高低电平来模拟I2C、SPI等接口协议,这些硬件时序的高低电平需要维持稳定一段时间,而且一般都比较短,几十到几百微秒级别,这时候是不能用sleep的,原因是sleep的计时一般都由systick来累加,而systick的频率一般都在毫秒级

在上面的几种场景里,就需要用忙等的延迟,用大白话来说,就是“我需要休息一下再往前走,因为时间不长,我不想把CPU让出去”。有单片机开发经验的同学看到这个需求,很自然的想到一个方法是根据CPU主频计算每秒可执行的指令数量,再实现一个函数,让CPU在里面循环想要的次数,从而达到延迟的目的。例如CPU每秒可执行100万条指令,如果我想延迟1ms,就可以让CPU循环执行1000条无意义的空指令。这种方法在经典的“前后台系统模型”(main函数就只有一个无限循环,以中断作为驱动)里是适用的,但是在运行RTOS的系统里,就可能会有比较大的误差。请看下面这张示意图:



当前有3个相同优先级的任务在轮转运行(关于时间片轮转请参考实时操作系统的任务调度示例之时间片),Task1在它的10ms时间片里先正常运行了5ms,然后调用一个delay函数,本意是延迟10ms,但是在delay函数运行了5ms之后,Task1的时间片耗尽,开始运行Task2,紧接着又运行Task3,各运行了10ms之后,才回到Task1,它又继续刚才未完成的循环,运行5ms之后,退出delay函数,这整个delay的时间就达到了5+10+10+5=30ms。这种场景下,比较好的实现应该是下面这样:



小结一下:针对前面描述的几个问题,我们需要一个好用的delay函数,它至少需要具备两个特点

1 精度要够高,可以到10us级别,如果硬件允许的话,当然还要更高

2 不能被调度器打断

本文基于以上两个需求,实现了一种忙等delay的方法,硬件平台是STM32F103VET6,操作系统是FreeRTOS V7.2.0。

先看代码实现

先把代码贴上,后面再来分析

#include "stm32f10x_tim.h"
#include "stm32f10x.h"
#include "misc.h"
#include "..\FreeRtos\inc\mpu_wrappers.h"

static unsigned int delay_cnt;

void Delay_Timer_Init()
{
TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);  //使能TIM3的时钟

TIM_TimeBaseStructure.TIM_Period = 1;  //定时器自动重装载的周期值
TIM_TimeBaseStructure.TIM_Prescaler =359;   //预分频系数
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);

NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;        //设置TIM3的中断参数
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

TIM_Cmd(TIM3, DISABLE);   //先将TIM3设为disable的
}

void TIM3_IRQHandler(void)
{
static int cnt = 0;
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
if (delay_cnt)
{
delay_cnt--;
if (!delay_cnt)
TIM_Cmd(TIM3, DISABLE);   //如果延迟结束,停止定时器中断
}
}
}

void u_delay(unsigned int us)
{
if (!us)
return;

if (us < 10)
{
delay_cnt = 1;
}
else
{
delay_cnt = us/10;
}
vTaskSuspendAll();       //关闭调度器
TIM_Cmd(TIM3, ENABLE);   //打开TIM3的定时器中断
while(delay_cnt)         //原地打转等定时器中断将delay_cnt减为0
;
xTaskResumeAll();        //打开调度器
}

void m_delay(unsigned int ms)
{
u_delay(1000 * ms);
}


Delay_Timer_Init函数是对计数定时器的初始化,应该在使用delay函数之前调用。u_delay的参数单位是us,m_delay的参数单位是ms

高精度定时器

前面已经提到,系统tick一般都是毫秒级的,为达到微秒级的精度,需要用STM32的硬件定时器来实现计数功能,这里用的是普通定时器TIM3,它的定时器周期计算公式为:

              (1 + TIM_Period) * ((1 + TIM_Prescaler)/72000000)

将代码里的TIM_Period(1)和TIM_Prescaler(359)代入公式,可以得出定时器周期为了10us,理论上还能再高,但实测结果精度就会变得较差。另外为了降低CPU的开销,在延迟结束时,总是将定时器暂时disable掉,有延迟请求了再次打开。

开关调度器到底干了什么?

这节我们来看FreeRTOS的调度器是怎么开关的,首先是关

void vTaskSuspendAll( void )
{
++uxSchedulerSuspended;
}
居然就只有这么简单一句话,将uxSchedulerSuspended变量加1,那么uxSchedulerSuspended加1了以后会影响什么?答案在任务上下文的切换过程中,

void vTaskSwitchContext( void )
{
if( uxSchedulerSuspended != ( unsigned portBASE_TYPE ) pdFALSE )
{
xMissedYield = pdTRUE;
}
else
{
//do the context switch
}
如果uxSchedulerSuspended不为0的话,就不能做任务上下文的切换,也就是所谓的任务调度切换,而是只讲变量xMissedYield记为TRUE,它是在调度器重新打开的时候,提醒调度器,至少有一次本应执行的调度被错过了。xTaskResumeAll函数的关键代码片段贴在下面
portBASE_TYPE xYieldRequired = pdFALSE;

//遍历Pending的链表,将转为就绪的任务添加到就绪链表里
while( listLIST_IS_EMPTY( ( xList * ) &xPendingReadyList ) == pdFALSE )
{
pxTCB = ( tskTCB * ) listGET_OWNER_OF_HEAD_ENTRY(  ( ( xList * ) &xPendingReadyList ) );
vListRemove( &( pxTCB->xEventListItem ) );
vListRemove( &( pxTCB->xGenericListItem ) );
prvAddTaskToReadyQueue( pxTCB );

if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )
{
xYieldRequired = pdTRUE;   //如果有更高优先级的task在关闭调度器期间转为就绪态了,需要进行任务切换
}
}
//调度器关闭期间系统的tick count是不增加的,这里再重新打开调度器后需要对它进行补偿
if( uxMissedTicks > ( unsigned portBASE_TYPE ) 0U )
{
while( uxMissedTicks > ( unsigned portBASE_TYPE ) 0U )
{
vTaskIncrementTick();
--uxMissedTicks;
}

#if configUSE_PREEMPTION == 1
{
xYieldRequired = pdTRUE;    //如果用户配置了抢占,那么此时必然做一次任务调度
}
#endif
}

if( ( xYieldRequired == pdTRUE ) || ( xMissedYield == pdTRUE ) ) //执行一次调度的条件
{
xAlreadyYielded = pdTRUE;
xMissedYield = pdFALSE;
portYIELD_WITHIN_API();  //将PendSV中断置位,执行一次任务调度
}


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息