从单片机到操作系统⑦——深入了解FreeRTOS的延时机制
>没研究过操作系统的源码都不算学过操作系统
# FreeRTOS 时间管理
时间管理包括两个方面:系统节拍以及任务延时管理。
## 系统节拍:
在前面的文章也讲得很多,想要系统正常运行,那么时钟节拍是必不可少的,`FreeRTOS`的时钟节拍通常由`SysTick`提供,它周期性的产生定时中断,所谓的时钟节拍管理的核心就是这个定时中断的服务程序。`FreeRTOS`的时钟节拍isr中核心的工作就是调用`vTaskIncrementTick()`函数。具体见上之前的文章。
## 延时管理
FreeRTOS提供了两个系统延时函数:
- 相对延时函数`vTaskDelay() `
- 绝对延时函数`vTaskDelayUntil()`。
这些延时函数可不像我们以前用裸机写代码的延时函数操作系统不允许CPU在死等消耗着时间,因为这样效率太低了。
同时,要告诫学操作系统的同学,千万别用裸机的思想去学操作系统。
## 任务延时
任务可能需要延时,两种情况,一种是任务被`vTaskDelay`或者`vTaskDelayUntil`延时,另外一种情况就是任务等待事件(比如等待某个信号量、或者某个消息队列)时候指定了`timeout`(即最多等待timeout时间,如果等待的事件还没发生,则不再继续等待),在每个任务的循环中都必须要有阻塞的情况出现,否则比该任务优先级低的任务就永远无法运行。
## 相对延时与绝对延时的区别
**相对延时:vTaskDelay():**
相对延时是指每次延时都是从任务执行函数`vTaskDelay()`开始,延时指定的时间结束
**绝对延时:vTaskDelayUntil():**
绝对延时是指调用`vTaskDelayUntil()`的任务每隔x时间运行一次。也就是任务周期运行。
**相对延时:vTaskDelay()**
相对延时`vTaskDelay()`是从调用`vTaskDelay()`这个函数的时候开始延时,但是任务执行的时候,可能发生了中断,导致任务执行时间变长了,但是整个任务的延时时间还是1000个tick,这就不是周期性了,简单看看下面代码:
```js
void vTaskA( void * pvParameters )
{
while(1)
{
// ...
// 这里为任务主体代码
// ...
/* 调用相对延时函数,阻塞1000个tick */
vTaskDelay( 1000 );
}
}
```
可能说的不够明确,可以看看图解。

当任务运行的时候,假设被某个高级任务或者是中断打断了,那么任务的执行时间就更长了,然而延时还是延时`1000`个`tick`这样子,整个系统的时间就混乱了。
如果还不够明确,看看vTaskDelay()的源码
```js
void vTaskDelay( const TickType_t xTicksToDelay )
{
BaseType_t xAlreadyYielded = pdFALSE;
/* 延迟时间为零只会强制切换任务。 */
if( xTicksToDelay > ( TickType_t ) 0U ) (1)
{
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll(); (2)
{
traceTASK_DELAY();
/*将当前任务从就绪列表中移除,并根据当前系统节拍
计数器值计算唤醒时间,然后将任务加入延时列表 */
prvAddCurrentTaskToDelayedList( xTicksToDelay, pdFALSE );
}
xAlreadyYielded = xTaskResumeAll();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
/* 强制执行一次上下文切换 */
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
```
- (1):如果传递进来的延时时间是`0`,只能进行强制切换任务了,调用的是`portYIELD_WITHIN_API()`,它其实是一个宏,真正起作用的是`portYIELD()`,下面是它的源码:
```js
#define portYIELD() \
{ \
/* 设置PendSV以请求上下文切换。 */ \
portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \
__dsb( portSY_FULL_READ_WRITE ); \
__isb( portSY_FULL_READ_WRITE ); \
}
```
- (2):挂起当前任务
然后将当前任务从就绪列表删除,然后加入到延时列表。是调用函数`prvAddCurrentTaskToDelayedList()`完成这一过程的。由于这个函数篇幅过长,就不讲解了,有兴趣可以看看,我就简单说说过程。在`FreeRTOS`中有这么一个变量,是用来记录`systick`的值的。
```js
PRIVILEGED_DATA static volatile TickType_t xTickCount = ( TickType_t ) 0U;
```
在每次`tick`中断时`xTickCount`加一,它的值表示了系统节拍中断的次数,那么啥时候唤醒被加入延时列表的任务呢?其实很简单,FreeRTOS的做法将`xTickCount`(当前系统时间) + `xTicksToDelay`(要延时的时间)即可。当这个相对的延时时间到了之后就唤醒了,这个`(xTickCount+ xTicksToDelay)`时间会被记录在该任务的任务控制块中。
看到这肯定有人问,这个变量是`TickType_t`类型(32位)的,那肯定会溢出啊,没错,是变量都会有溢出的一天,可是`FreeRTOS`乃是世界第一的操作系统啊,`FreeRTOS`使用了两个延时列表:
`xDelayedTaskList1 和 xDelayedTaskList2`
并使用两个列表指针类型变量`pxDelayedTaskList`和`pxOverflowDelayedTaskList`分别指向上面的延时列表1和延时列表2(在创建任务时将延时列表指针指向延时列表)如果内核判断出`xTickCount+xTicksToDelay`溢出,就将当前任务挂接到列表指针 `pxOverflowDelayedTaskList`指向的列表中,否则就挂接到列表指针`pxDelayedTaskList`指向的列表中。当时间到了,就会将延时的任务从延时列表中删除,加入就绪列表中,当然这时候就是由调度器觉得任务能不能运行了,如果任务的优先级大于当前运行的任务,那么调度器才会进行任务的调度。
**绝对延时:vTaskDelayUntil()**
`vTaskDelayUntil()`的参数指定了确切的滴答计数值
调用`vTaskDelayUntil()`是希望任务以固定频率定期执行,而不受外部的影响,任务从上一次运行开始到下一次运行开始的时间间隔是绝对的,而不是相对的。假设主体任务被打断`0.3s`,但是下次唤醒的时间是固定的,所以还是会周期运行。

下面看看`vTaskDelayUntil()`的使用方法,注意了,这`vTaskDelayUntil()`的使用方法与`vTaskDelay()`不一样:
```js
void vTaskA( void * pvParameters )
{
/* 用于保存上次时间。调用后系统自动更新 */
static portTickType PreviousWakeTime;
/* 设置延时时间,将时间转为节拍数 */
const portTickType TimeIncrement = pdMS_TO_TICKS(1000);
/* 获取当前系统时间 */
PreviousWakeTime = xTaskGetTickCount();
while(1)
{
/* 调用绝对延时函数,任务时间间隔为1000个tick */
vTaskDelayUntil( &PreviousWakeTime,TimeIncrement );
// ...
// 这里为任务主体代码
// ...
}
}
```
在使用的时候要将延时时间转化为系统节拍,在任务主体之前要调用延时函数。
任务会先调用`vTaskDelayUntil()`使任务进入阻塞态,等到时间到了就从阻塞中解除,然后执行主体代码,任务主体代码执行完毕。会继续调用`vTaskDelayUntil()`使任务进入阻塞态,然后就是循环这样子执行。即使任务在执行过程中发生中断,那么也不会影响这个任务的运行周期,仅仅是缩短了阻塞的时间而已。
下面来看看`vTaskDelayUntil()`的源码:
```js
void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
{
TickType_t xTimeToWake;
BaseType_t xAlreadyYielded, xShouldDelay = pdFALSE;
configASSERT( pxPreviousWakeTime );
configASSERT( ( xTimeIncrement > 0U ) );
configASSERT( uxSchedulerSuspended == 0 );
vTaskSuspendAll(); // (1)
{
/* 保存系统节拍中断次数计数器 */
const TickType_t xConstTickCount = xTickCount;
/* 生成任务要唤醒的滴答时间。*/
xTimeToWake = *pxPreviousWakeTime + xTimeIncrement;
/* pxPreviousWakeTime中保存的是上次唤醒时间,唤醒后需要一定时间执行任务主体代码,
如果上次唤醒时间大于当前时间,说明节拍计数器溢出了 具体见图片 */
if( xConstTickCount xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
else
{
/* 滴答时间没有溢出。 在这种情况下,如果唤醒时间溢出,
或滴答时间小于唤醒时间,我们将延迟。*/
if( ( xTimeToWake xConstTickCount ) )
{
xShouldDelay = pdTRUE;
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
/* 更新唤醒时间,为下一次调用本函数做准备. */
*pxPreviousWakeTime = xTimeToWake;
if( xShouldDelay != pdFALSE )
{
traceTASK_DELAY_UNTIL( xTimeToWake );
/* prvAddCurrentTaskToDelayedList()需要块时间,而不是唤醒时间,因此减去当前的滴答计数。 */
prvAddCurrentTaskToDelayedList( xTimeToWake - xConstTickCount, pdFALSE );
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
xAlreadyYielded = xTaskResumeAll();
/* 如果xTaskResumeAll尚未执行重新安排,我们可能会让自己入睡。*/
if( xAlreadyYielded == pdFALSE )
{
portYIELD_WITHIN_API();
}
else
{
mtCOVERAGE_TEST_MARKER();
}
}
```
与相对延时函数`vTaskDelay`不同,本函数增加了一个参数`pxPreviousWakeTime`用于指向一个变量,变量保存上次任务解除阻塞的时间,此后函数`vTaskDelayUntil()`在内部自动更新这个变量。由于变量`xTickCount`可能会溢出,所以程序必须检测各种溢出情况,并且要保证延时周期不得小于任务主体代码执行时间。
就会有以下3种情况,才能将任务加入延时链表中。
请记住这几个单词的含义:
- `xTimeIncrement`:任务周期时间
- `pxPreviousWakeTime`:上一次唤醒的时间点
- `xTimeToWake`:下一次唤醒的系统时间点
- `xConstTickCount`:进入延时的时间点
3. 第三种情况:常规无溢出的情况。
以时间为横轴,上一次唤醒的时间点小于下一次唤醒的时间点,这是很正常的情况。

2. 第二种情况:唤醒时间计数器(`xTimeToWake`)溢出情况。
也就是代码中`if( ( xTimeToWake xConstTickCount ) )`

1. 第一种情况:唤醒时间(`xTimeToWake`)与进入延时的时间点(`xConstTickCount`)都溢出情况。
也就是代码中`if( ( xTimeToWake xConstTickCount ) )`

从图中可以看出不管是溢出还是无溢出,都要求在下次唤醒任务之前,当前任务主体代码必须被执行完。也就是说任务执行的时间不允许大于延时的时间,总不能存在每`10ms`就要执行一次`20ms`时间的任务吧。计算的唤醒时间合法后,就将当前任务加入延时列表,同样延时列表也有两个。每次系统节拍中断,中断服务函数都会检查这两个延时列表,查看延时的任务是否到期,如果时间到期,则将任务从延时列表中删除,重新加入就绪列表。如果新加入就绪列表的任务优先级大于当前任务,则会触发一次上下文切换。
## 总结
如果任务调用相对延时,其运行周期完全是不可测的,如果任务的优先级不是最高的话,其误差更大,就好比一个必须要在`5ms`内相应的任务,假如使用了相对延时1ms,那么很有可能在该任务执行的时候被更高优先级的任务打断,从而错过`5ms`内的相应,但是调用绝对延时,则任务会周期性将该任务在阻塞列表中解除,但是,任务能不能运行,还得取决于任务的优先级,如果优先级最高的话,任务周期还是比较精确的(相对`vTaskDelay`来说),如果想要更加想精确周期性执行某个任务,可以使用系统节拍钩子函数`vApplicationTickHook()`,它在`tick`中断服务函数中被调用,因此这个函数中的代码必须简洁,并且不允许出现阻塞的情况。
# 关注我

更多资料欢迎关注“物联网IoT开发”公众号!
从单片机到操作系统⑦——深入了解FreeRTOS的延时机制的更多相关文章
- 简单的51单片机多任务操作系统(C51)
在网上看到这段代码,所以自己尝试了,可以跑起来,但是没有精确的定时功能,仅仅是任务的调度而已. 数组中是11,而不是12.这里写错了... /* 简单的多任务操作系统 其实只有个任务调度切换,把说它是 ...
- 在中断服务函数中使用FreeRTOS系统延时函数vTaskDelay导致看门狗复位的情况
@2019-04-09 [问题] 控制程序工作一段时间异常重启 [分析] 经定位分析重启原因为看门狗复位导致 [解决] 经排查发现在中断服务函数中使用了FreeRTOS的系统时延函数vTaskDela ...
- 操作系统:x86下内存分页机制 (1)
前置知识: 分段的概念(当然手写过肯定是坠吼的 为什么要分页 当我们写程序的时候,总是倾向于把一个完整的程序分成最基本的数据段,代码段,栈段.并且普通的分段机制就是在进程所属的LDT中把每一个段给标识 ...
- 操作系统开发系列—13.h.延时操作
计数器的工作原理是这样的:它有一个输入频率,在PC上是1193180HZ.在每一个时钟周期(CLK cycle),计数器值会减1,当减到0时,就会触发一个输出.由于计数器是16位的,所以最大值是655 ...
- 51单片机C51毫秒级(ms)精确延时
如下程序能实现ms毫秒级的比较精确的延时 void Delayms(unsigned int n) { unsigned int i,j; ;j--) ;i>;i--); } 用keil可以看出 ...
- coursera 《现代操作系统》 -- 第五周 同步机制(2)
分清紧急等待队列与条件等待队列(c 链) 条件等待队列:但是进入管程的这个进程可能由于对资源的操作的过程中发现条件不成熟, 那么它就不能够继续对资源进行相应的操作. 我们以生产者. 消费者为例. 如果 ...
- coursera 《现代操作系统》 -- 第五周 同步机制(1)
临界区块(Critical section)指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源有无法同时被多个线程访问的特性.(不是字面意思的一个区域,是程序片段的集合) ...
- FreeRTOS操作系统最全面使用指南
FreeRTOS操作系统最全面使用指南 1 FreeRTOS操作系统功能 作为一个轻量级的操作系统,FreeRTOS提供的功能包括:任务管理.时间管理.信号量.消息队列.内存管理.记录功能等,可基本满 ...
- 一种基于C51单片机的非抢占式的操作系统架构
摘 要:从Keil C51的内存空间管理方式入手,着重讨论实时操作系统在任务调度时的重入问题,分析一些解决重入的基本方式与方法:分析实时操作系统任务调度的占先性,提出非占先的任务调度是能更适合于Kei ...
随机推荐
- python中列表,数字,字符串函数总结
列表list: arr = [] 1.可以定义空列表 2.可以定义只有一个元素的列表 3.元素可以是任意类型 arr.append('abc')末尾添加 arr.insert(index,objec) ...
- 3、pytest中文文档--编写断言
目录 编写断言 使用assert编写断言 编写触发期望异常的断言 特殊数据结构比较时的优化 为失败断言添加自定义的说明 关于断言自省的细节 复写缓存文件 去使能断言自省 编写断言 使用assert编写 ...
- Spring Boot2 系列教程(五)Spring Boot中的 yaml 配置
搞 Spring Boot 的小伙伴都知道,Spring Boot 中的配置文件有两种格式,properties 或者 yaml,一般情况下,两者可以随意使用,选择自己顺手的就行了,那么这两者完全一样 ...
- fremark遍历出的li怎么加事件(cforeach循环遍历也同样适用)
遇见这个问题烦了好久好久,网上也没找到答案,琢磨了半天终于找到了方法 1.下面是一个分页信息遍历添加事件,页面跳转 <!-- 分页条信息 --> <div class="c ...
- Guava的RateLimiter实现接口限流
最近开发需求中有需要对后台接口进行限流处理,整理了一下基本使用方法. 首先添加guava依赖: <dependency> <groupId>com.google.guava&l ...
- Python网络爬虫实战(四)模拟登录
对于一个网站的首页来说,它可能需要你进行登录,比如知乎,同一个URL下,你登录与未登录当然在右上角个人信息那里是不一样的. (登录过) (未登录) 那么你在用爬虫爬取的时候获得的页面究竟是哪个呢? 肯 ...
- 为什么Java进程使用的RAM比Heap Size大?
Java进程使用的虚拟内存确实比Java Heap要大很多.JVM包括很多子系统:垃圾收集器.类加载系统.JIT编译器等等,这些子系统各自都需要一定数量的RAM才能正常工作. 当一个Java进程运行时 ...
- Kubernetes 从懵圈到熟练:集群服务的三个要点和一种实现
作者 | 声东 阿里云售后技术专家 文章来源:Docker,点击查看原文. 以我的经验来讲,理解 Kubernetes 集群服务的概念,是比较不容易的一件事情.尤其是当我们基于似是而非的理解,去排查服 ...
- JAVA数据处理的常用技术
背景 在实际开发中,数据的处理有五种:获取.传输.存储.分析.转换.每种各对应一些常用的技术. 序列化和反序列化 序列化是将对象的信息转换为可传输或可存储形式的过程.反序列化就是反过来让这些可传输的. ...
- 神奇的互换身体术--java的类型擦除
故事背景 <互换身体>是由环球影业发行的喜剧电影,于2011年8月5日在美国上映.该片由大卫·道金执导,瑞安·雷诺兹.杰森·贝特曼.奥利维亚·王尔德等主演.该片讲述了一位居家好男人和一位蜂 ...