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

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/wbwwf8685/article/details/51954083

摘要

在嵌入式系统开发里,经常会遇到要将一个操作“延迟”一段时间执行的情况,下面的一段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中断置位,执行一次任务调度
}






展开阅读全文

没有更多推荐了,返回首页