实时操作系统的忙等延迟实现
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中断置位,执行一次任务调度 }
相关文章推荐
- 应用领航:盘点那些年我们一起追过的OS
- 无奇不有!盘点各国自己开发的操作系统
- Mootools 1.2教程 定时器和哈希简介
- C#定时器和随机数
- 可自定义oem的萝卜家园 Ghost XP 新春装机版 V200801 下载
- C#中timer定时器用法实例
- C#实现判断操作系统是否为Win8以上版本
- JS中自定义定时器让它在某一时刻执行
- JS定时器使用,定时定点,固定时刻,循环执行详解
- js获取本机操作系统类型的两种方法
- 理解javascript定时器中的setTimeout与setInterval
- JavaScript定时器详解及实例
- 基于jQuery实现的扇形定时器附源码下载
- jQuery基础的工厂函数以及定时器的经典实例分析
- 跟我学习javascript的定时器
- Linux操作系统添加新硬盘方法
- Javascript中获取浏览器类型和操作系统版本等客户端信息常用代码
- Qt定时器和随机数详解
- asp.net中Timer无刷新定时器的实现方法
- java如何获取本地操作系统进程列表