http://blog.csdn.net/wbwwf8685/article/details/51954083
2016
在嵌入式系统开发里,经常会遇到要将一个操作“延迟”一段时间执行的情况,下面的一段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中断置位,执行一次任务调度
- }
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请
点击举报。