打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

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

http://blog.csdn.net/wbwwf8685/article/details/51954083

2016

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

  1. while(1) {    
  2.     LED_ON;    
  3.     Sleep(1000);    
  4.     LED_OFF;  
  5.     Sleep(1000);    
  6. }  

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。

先看代码实现

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

  1. #include "stm32f10x_tim.h"  
  2. #include "stm32f10x.h"  
  3. #include "misc.h"  
  4. #include "..\FreeRtos\inc\mpu_wrappers.h"  
  5.   
  6. static unsigned int delay_cnt;  
  7.   
  8. void Delay_Timer_Init()  
  9. {  
  10.     TIM_TimeBaseInitTypeDef  TIM_TimeBaseStructure;  
  11.     NVIC_InitTypeDef NVIC_InitStructure;  
  12.   
  13.     RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);  //使能TIM3的时钟  
  14.       
  15.     TIM_TimeBaseStructure.TIM_Period = 1;  //定时器自动重装载的周期值  
  16.     TIM_TimeBaseStructure.TIM_Prescaler =359;   //预分频系数  
  17.     TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;  
  18.     TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //向上计数模式  
  19.     TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);  
  20.     TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);  
  21.   
  22.     NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;        //设置TIM3的中断参数  
  23.     NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;  
  24.     NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;  
  25.     NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;  
  26.     NVIC_Init(&NVIC_InitStructure);  
  27.   
  28.     TIM_Cmd(TIM3, DISABLE);   //先将TIM3设为disable的  
  29. }  
  30.   
  31.   
  32. void TIM3_IRQHandler(void)  
  33. {  
  34.     static int cnt = 0;  
  35.     if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)  
  36.     {  
  37.         TIM_ClearITPendingBit(TIM3, TIM_IT_Update);  
  38.         if (delay_cnt)  
  39.         {  
  40.         delay_cnt--;  
  41.         if (!delay_cnt)  
  42.            TIM_Cmd(TIM3, DISABLE);   //如果延迟结束,停止定时器中断  
  43.             }  
  44.     }  
  45. }  
  46.   
  47.   
  48. void u_delay(unsigned int us)  
  49. {  
  50.     if (!us)  
  51.         return;  
  52.       
  53.     if (us < 10)  
  54.     {  
  55.        delay_cnt = 1;  
  56.     }  
  57.     else  
  58.     {  
  59.        delay_cnt = us/10;  
  60.     }  
  61.     vTaskSuspendAll();       //关闭调度器  
  62.     TIM_Cmd(TIM3, ENABLE);   //打开TIM3的定时器中断  
  63.     while(delay_cnt)         //原地打转等定时器中断将delay_cnt减为0  
  64.         ;  
  65.     xTaskResumeAll();        //打开调度器  
  66. }  
  67.   
  68.   
  69. void m_delay(unsigned int ms)  
  70. {  
  71.     u_delay(1000 * ms);  
  72. }  

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的调度器是怎么开关的,首先是关

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

  1. void vTaskSwitchContext( void )  
  2. {  
  3.     if( uxSchedulerSuspended != ( unsigned portBASE_TYPE ) pdFALSE )  
  4.     {  
  5.         xMissedYield = pdTRUE;  
  6.     }  
  7.     else  
  8.     {  
  9.        //do the context switch  
  10.     }  
如果uxSchedulerSuspended不为0的话,就不能做任务上下文的切换,也就是所谓的任务调度切换,而是只讲变量xMissedYield记为TRUE,它是在调度器重新打开的时候,提醒调度器,至少有一次本应执行的调度被错过了。xTaskResumeAll函数的关键代码片段贴在下面
  1. portBASE_TYPE xYieldRequired = pdFALSE;  
  2.   
  3. //遍历Pending的链表,将转为就绪的任务添加到就绪链表里  
  4. while( listLIST_IS_EMPTY( ( xList * ) &xPendingReadyList ) == pdFALSE )  
  5. {  
  6.     pxTCB = ( tskTCB * ) listGET_OWNER_OF_HEAD_ENTRY(  ( ( xList * ) &xPendingReadyList ) );  
  7.     vListRemove( &( pxTCB->xEventListItem ) );  
  8.     vListRemove( &( pxTCB->xGenericListItem ) );  
  9.     prvAddTaskToReadyQueue( pxTCB );  
  10.   
  11.     if( pxTCB->uxPriority >= pxCurrentTCB->uxPriority )  
  12.     {  
  13.     xYieldRequired = pdTRUE;   //如果有更高优先级的task在关闭调度器期间转为就绪态了,需要进行任务切换  
  14.     }  
  15. }  
  16. //调度器关闭期间系统的tick count是不增加的,这里再重新打开调度器后需要对它进行补偿  
  17. if( uxMissedTicks > ( unsigned portBASE_TYPE ) 0U )  
  18. {  
  19.     while( uxMissedTicks > ( unsigned portBASE_TYPE ) 0U )  
  20.     {  
  21.     vTaskIncrementTick();  
  22.     --uxMissedTicks;  
  23.     }  
  24.   
  25.     #if configUSE_PREEMPTION == 1  
  26.     {  
  27.     xYieldRequired = pdTRUE;    //如果用户配置了抢占,那么此时必然做一次任务调度  
  28.     }  
  29.     #endif  
  30. }  
  31.   
  32. if( ( xYieldRequired == pdTRUE ) || ( xMissedYield == pdTRUE ) ) //执行一次调度的条件  
  33. {  
  34.     xAlreadyYielded = pdTRUE;  
  35.     xMissedYield = pdFALSE;  
  36.     portYIELD_WITHIN_API();  //将PendSV中断置位,执行一次任务调度  
  37. }  





本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
stm32_定时器3控制PWM的输出脉冲_步进电机的控制
工程师实战:单片机裸机程序框架是怎样炼成的?
通用定时器库函数
stm32中定时器中断的套路
C语言之“编程超简单系列”:芯片的“手表”——定时器
stm32库函数学习篇
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服