前面我们用了FreeRTOS中的延时函数,本篇博客就来探讨FreeRTOS中的延时函数,看看他们是如何发挥作用的。当我们在裸机开发中调用delay_ms()函数时,我们的处理器将不处理任何事,造成处理器资源的浪费。 为此,为了提高CPU的利用率,在FreeRTOS中提供了两个延时API函数。
目录
6.2 主函数调用入口函数,操作系统开始进行任务的切换和调度
一、相对延时函数
1.1 函数介绍
FreeRTOS 的相对延时函数 vTaskDelay 是一个常用的任务延时函数。相对延时:指每次延时都是从执行函数vTaskDelay()开始,直到延时指定的时间结束。它使当前任务进入阻塞状态一段指定的时间,以节拍(ticks)为单位。
函数原型:void vTaskDelay( const TickType_t xTicksToDelay );
参数:xTicksToDelay(将调⽤任务转换到就绪状态前保持在阻塞状态的滴答中断次数)
参数
xTicksToDelay: 指定要延迟的时钟节拍数。一个时钟节拍的时间长度取决于系统节拍率(通常通过configTICK_RATE_HZ定义), 我们通常使用的是1ms作为一个时钟节拍。
1.2 函数实现过程
1.2.1. 任务进入阻塞状态
当调用 vTaskDelay 时,当前任务会立即进入阻塞状态。该函数首先计算出当前系统的时钟节拍计数(xTickCount)和需要延迟的节拍数(xTicksToDelay)。
1.2.2. 计算延迟结束时间
任务延迟结束的时间点(即节拍计数)由当前节拍计数加上需要延迟的节拍数计算得出:
TickType_t xTimeToWake;
xTimeToWake = xTickCount + xTicksToDelay;
这里,xTickCount 是当前系统的节拍计数,xTicksToDelay 是要延迟的节拍数。
1.2.3. 检查延迟数是否为0
如果 xTicksToDelay 为 0,函数直接返回,因为不需要延迟。
1.2.4. 任务进入延迟列表
FreeRTOS 使用延迟列表(delay list)来管理处于延迟状态的任务。任务被添加到延迟列表中,并记录其唤醒时间(xTimeToWake)。
vListInsert( &xDelayedTaskList, &( pxCurrentTCB->xStateListItem ) );
1.2.5. 调度任务
任务进入阻塞状态后,调度器会选择下一个任务运行。任务切换是通过 taskYIELD 实现的:
taskYIELD();
1.2.6. 时钟节拍中断处理
系统时钟节拍中断(tick interrupt)发生时,节拍计数器(xTickCount)递增。中断处理函数 vTaskIncrementTick 负责更新系统节拍计数器,并检查是否有任务需要从延迟列表中移到就绪列表。
void vTaskIncrementTick( void )
{
if( uxSchedulerSuspended == ( UBaseType_t ) pdFALSE )
{
const TickType_t xConstTickCount = xTickCount + ( TickType_t ) 1;
xTickCount = xConstTickCount;
if( xConstTickCount == ( TickType_t ) 0U )
{
// 检查溢出情况
}
// 检查是否有任务需要唤醒
if( xConstTickCount >= xNextTaskUnblockTime )
{
prvCheckDelayedTasks();
}
}
}
1.2.7. 任务唤醒
当当前的节拍计数达到或超过任务的唤醒时间(xTimeToWake)时,延迟任务将从延迟列表中移除,并加入就绪列表。这个过程在 prvCheckDelayedTasks 函数中完成:
void prvCheckDelayedTasks( void )
{
TCB_t *pxTCB;
while( listLIST_IS_EMPTY( pxDelayedTaskList ) == pdFALSE )
{
pxTCB = ( TCB_t * ) listGET_OWNER_OF_HEAD_ENTRY( pxDelayedTaskList );
if( xTickCount < listGET_LIST_ITEM_VALUE( &( pxTCB->xStateListItem ) ) )
{
break;
}
( void ) uxListRemove( &( pxTCB->xStateListItem ) );
vTaskSwitchContext();
}
}
1.3 总结
vTaskDelay 函数通过将任务放入延迟列表,计算任务的唤醒时间,并在时钟节拍中断时检查任务是否需要被唤醒,从而实现任务的延时功能。这个过程涉及到任务调度、时钟节拍计数的管理以及列表操作等机制,确保任务能够在指定的时间点后重新进入就绪状态,继续执行。
二、绝对延时
2.1 函数介绍
xTaskDelayUntil() 是 FreeRTOS 中用于创建相对精确的周期性任务的函数。它可以确保任务以固定的时间间隔运行,而不受其它任务的影响。与 vTaskDelay() 不同,xTaskDelayUntil() 通过一个时间基准点来确保周期的准确性。绝对延时:指将整个任务的运行周期看成一个整体,适用于需要按照一定频率运行的任务。

函数原型:
BaseType_t xTaskDelayUntil( TickType_t *pxPreviousWakeTime, const TickType_t xTimeIncrement );
参数
pxPreviousWakeTime: 指向一个变量,这个变量保存上一次任务被唤醒的时间。该变量在第一次调用前需要被初始化为当前时间,通常使用
xTaskGetTickCount()获取。xTimeIncrement: 任务之间的时间间隔,即任务周期,单位是 tick 数。
返回值
返回
pdPASS表示成功。理论上,它总是返回pdPASS,因此可以忽略返回值。
2.2 函数实现过程
2.2.1 获取当前时间
函数首先获取当前的 tick 计数,即系统运行到目前为止的时间。
TickType_t xTimeNow;
xTimeNow = xTaskGetTickCount();
2.2.2 计算下一个唤醒时间
计算任务下一个唤醒时间 xNextWakeTime,这个时间是基于上一次唤醒时间 *pxPreviousWakeTime 加上时间增量 xTimeIncrement。
TickType_t xNextWakeTime;
xNextWakeTime = *pxPreviousWakeTime + xTimeIncrement;
2.2.3 检查是否需要延时
比较当前时间 xTimeNow 和下一个唤醒时间 xNextWakeTime,决定是否需要延时。如果当前时间已经超过下一个唤醒时间,说明任务已经错过了预定的周期,此时不会延时;否则,任务需要延时到下一个唤醒时间。
if( xNextWakeTime > xTimeNow ) {
// 延时到下一个唤醒时间
vTaskDelay( xNextWakeTime - xTimeNow );
}
2.2.4 更新上一次唤醒时间
更新 *pxPreviousWakeTime 为 xNextWakeTime,为下一次调用 xTaskDelayUntil 做准备。
*pxPreviousWakeTime = xNextWakeTime;
2.2.5 返回 pdPASS
函数总是返回 pdPASS 表示成功,理论上可以忽略返回值。
以下是 xTaskDelayUntil() 的完整实现代码:
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement )
{
TickType_t xTimeNow;
BaseType_t xShouldDelay = pdFALSE;
configASSERT( pxPreviousWakeTime );
vTaskSuspendAll();
{
xTimeNow = xTaskGetTickCount();
// 下一次唤醒时间
const TickType_t xNextWakeTime = *pxPreviousWakeTime + xTimeIncrement;
// 检查是否需要延时
if( xNextWakeTime > xTimeNow ) {
xShouldDelay = pdTRUE;
}
}
( void ) xTaskResumeAll();
// 如果需要延时,则调用 vTaskDelay 进行延时
if( xShouldDelay != pdFALSE ) {
vTaskDelay( xNextWakeTime - xTimeNow );
}
// 更新上一次唤醒时间
*pxPreviousWakeTime = xNextWakeTime;
return pdPASS;
}
4. 解释
vTaskSuspendAll 和 xTaskResumeAll: 这两个函数用于确保在获取当前时间和计算延时时间的过程中,任务不会被调度器中断。这样可以避免在多任务环境中出现时间不一致的情况。
configASSERT: 这是一个断言,用于检查指针
pxPreviousWakeTime是否为空,确保函数调用的正确性。时间计算和比较: 通过
xTimeNow和xNextWakeTime的比较来决定是否需要延时,如果当前时间小于下一个唤醒时间,则需要延时到下一个周期。
2.3 总结
xTaskDelayUntil() 函数通过维护一个上次唤醒时间和一个时间增量,确保任务可以在精确的周期时间内执行。它的实现通过获取当前时间、计算下一个唤醒时间、决定是否需要延时、更新上一次唤醒时间等步骤,确保任务在 FreeRTOS 中能够以固定的时间间隔运行,提高了系统的实时性和任务调度的精确性。
2.4 举例理解
例子 1: 定时闹钟
想象你是一个上班族,每天早上需要在 7:00 起床,你设置了一个闹钟来确保你准时起床。
1. 第一次设定闹钟:你第一次设定闹钟在 7:00。
2. 起床:第二天早上闹钟响了,你在 7:00 起床。
3. 重新设定闹钟:你再次设定闹钟在第二天的 7:00,确保你每天都在同一时间起床。
这里,7:00 就是你的基准时间(`pxPreviousWakeTime`),每天 24 小时的间隔就是时间增量(`xTimeIncrement`)。不管你前一天晚上几点睡觉,闹钟都会确保你第二天早上 7:00 起床。
例子 2: 公交车的时间表
公交车按照固定的时间表运行,比如每隔 15 分钟发一班车。
1. 公交车发车:假设第一班车在早上 8:00 发车。
2. 下一班车时间:每隔 15 分钟一班,下一班车在 8:15,接着是 8:30,以此类推。
在这个例子中,早上 8:00 是基准时间(`pxPreviousWakeTime`),15 分钟是时间增量(`xTimeIncrement`)。公交车司机会按照这个固定的时间表发车,不受前一班车是否晚点的影响。
例子 3: 厨房定时器
你在厨房烘焙蛋糕,需要每隔 30 分钟检查一次烤箱。
1. 第一次检查:你在蛋糕入烤箱后的 30 分钟时进行第一次检查。
2. 设定定时器:检查完毕后,你再次设定定时器在接下来的 30 分钟。
3. 后续检查:每隔 30 分钟你都进行一次检查,确保蛋糕不会烤焦。
这里,第一次检查时间就是你的基准时间(`pxPreviousWakeTime`),30 分钟是时间增量(`xTimeIncrement`)。不管你在检查时花了多长时间,定时器会确保你每隔 30 分钟进行一次检查。
在 FreeRTOS 中的类比
在 FreeRTOS 中,`xTaskDelayUntil()` 类似于上述例子中的定时机制,它保证了任务以固定的时间间隔运行,而不是简单地延迟一段时间(这可能会因为任务执行的时间不同而导致整体时间漂移)。
1. 基准时间:任务上一次唤醒的时间。
2. 时间增量:任务周期,即任务之间的固定时间间隔。每次任务运行完后,都会计算下次唤醒的时间点(基准时间 + 时间增量),然后延时到这个时间点。如果任务执行时间过长导致当前时间已经超过下次唤醒时间,任务会立即执行而不再延时。
通过 `xTaskDelayUntil()`,你可以确保任务以固定的周期运行,类似于每天固定时间起床、公交车按时间表发车以及厨房定时器检查烤箱。
三、相对延时和绝对延时的对比

适用场景举例
相对延时 适合场景:
- 简单的 LED 闪烁程序。
- 需要延时一段时间再执行某些操作,而不关心时间的绝对精度。
绝对延时 适合场景:
- 定期采集传感器数据,每隔固定时间采集一次。
- 实时控制系统中,需要以固定时间间隔执行控制算法。
四、延时函数解析


五、使用延时函数
5.1 相对延时函数的使用
使用方式:vTaskDelay(pdMS_TO_TICKS(100));或者直接 vTaskDelay(毫秒数)
pdMS_TO_TICKS() 是 FreeRTOS 提供的一个宏,用于将毫秒数转换为 tick 数。它的定义如下:
#define pdMS_TO_TICKS( xTimeInMs ) ( ( TickType_t ) ( ( ( TickType_t ) ( xTimeInMs ) * configTICK_RATE_HZ ) / 1000 ) )
其中 configTICK_RATE_HZ 是 FreeRTOS 的 tick 频率配置项,表示每秒的 tick 数。通过这个宏,可以方便地将时间转换为 tick 数,以便使用 vTaskDelay() 进行延时。
vTaskDelay()的延时精度取决于系统的 tick 频率(configTICK_RATE_HZ)。例如,如果 tick 频率为 1000 Hz(即 1 ms 一个 tick),那么延时的精度就是 1 毫秒。如果 tick 频率较低,延时的精度也会降低。
5.2 绝对延时函数的使用
1、初始化基准时间
在任务函数中,使用
xTaskGetTickCount()获取当前的 tick 计数,并将其赋值给xLastWakeTime,作为初始的基准时间。TickType_t xLastWakeTime = xTaskGetTickCount(); //获取当前的tick数 TickType_t xFrequency = pdMS_TO_TICKS(1000); //延时时间为1秒周期2、进行绝对延时
vTaskDelayUntil(&xLastWakeTime, xFrequency);
六、实验验证
定义三个任务,任务1和任务2使用vTaskDelay()函数。任务3使用vTaskDelayUntil()函数。 任务3将以固定的频率进行执行。
6.1 创建任务及实现任务函数
#include "stm32f4xx.h" // Device header
#include "stdio.h"
#include "FreeRTOS.h"
#include "task.h"
#include "dynamic.h"
/**********************START_TASK任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define START_TASK_STACK_SIZE 128 //定义堆栈大小为128字(1字等于4字节)
#define START_TASK_PRIO 1 //定义任务优先级,0-31根据任务需求
TaskHandle_t start_task_handler; //定义任务句柄(结构体指针)
void start_task(void* args);
/**********************TASK1任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define TASK1_STACK_SIZE 128 //定义堆栈大小为128字(1字等于4字节)
#define TASK1_PRIO 1 //定义任务优先级,0-31根据任务需求
TaskHandle_t task1_handler; //定义任务句柄(结构体指针)
void task1(void* args);
/**********************TASK2任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define TASK2_STACK_SIZE 128 //定义堆栈大小为128字(1字等于4字节)
#define TASK2_PRIO 1 //定义任务优先级,0-31根据任务需求
TaskHandle_t task2_handler; //定义任务句柄(结构体指针)
void task2(void* args);
/**********************TASK3任务配置******************************/
/***********包括任务堆栈大小、任务优先级、任务句柄、创建任务***********/
#define TASK3_STACK_SIZE 128 //定义堆栈大小为128字(1字等于4字节)
#define TASK3_PRIO 2 //定义任务优先级,0-31根据任务需求
TaskHandle_t task3_handler; //定义任务句柄(结构体指针)
void task3(void* args);
/*********开始任务用来创建其他三个任务,只创建一次,不能是死循环,同时创建完3个任务后删除任务1本身***********/
void start_task(void* args)
{
taskENTER_CRITICAL(); /*进入临界区*/
BaseType_t xReturn;
xTaskCreate( (TaskFunction_t) task1,
(char *) "task1",
( configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE,
(void *) NULL,
(UBaseType_t) TASK1_PRIO ,
(TaskHandle_t *) &task1_handler );
/*对任务1创建是否成功进行判断*/
if(xReturn == pdPASS)
{
printf("create task1 Success\n");
}
else
{
printf("create task1 error\n");
}
xTaskCreate( (TaskFunction_t) task2,
(char *) "task2",
( configSTACK_DEPTH_TYPE) TASK2_STACK_SIZE,
(void *) NULL,
(UBaseType_t) TASK2_PRIO ,
(TaskHandle_t *) &task2_handler );
/*对任务2创建是否成功进行判断*/
if(xReturn == pdPASS)
{
printf("create task2 Success\n");
}
else
{
printf("create task2 error\n");
}
xTaskCreate( (TaskFunction_t) task3,
(char *) "task3",
( configSTACK_DEPTH_TYPE) TASK3_STACK_SIZE,
(void *) NULL,
(UBaseType_t) TASK3_PRIO ,
(TaskHandle_t *) &task3_handler );
/*对任务3创建是否成功进行判断*/
if(xReturn == pdPASS)
{
printf("create task3 Success\n");
}
else
{
printf("create task3 error\n");
}
vTaskDelete(NULL); //删除开始任务自身,传参NULL
taskEXIT_CRITICAL(); /*退出临界区*/
//临界区内不会进行任务的调度切换,出了临界区才会进行任务调度,抢占式
}
/********其余三个任务的任务函数,无返回值且是死循环***********/
/***任务1:打印字符串验证*******/
void task1(void* args)
{
while(1)
{
printf("任务1运行!\n");
vTaskDelay(300); //FreeRTOS自带的延时函数
}
}
/***任务2:打印字符串验证*******/
void task2(void* args)
{
while(1)
{
printf("任务2运行!\n");
vTaskDelay(300); //FreeRTOS自带的延时函数
}
}
/***任务3:实现绝对延时*******/
void task3(void* args)
{
TickType_t xLastWakeTime = xTaskGetTickCount(); //获取当前的tick数
TickType_t xFrequency = pdMS_TO_TICKS(2000); //延时时间为2秒周期
while(1)
{
printf("任务3运行!\n");
vTaskDelayUntil(&xLastWakeTime, xFrequency);
}
}
//FreeRTO入口例程函数,无参数,无返回值
void freertos_demo(void)
{
xTaskCreate( (TaskFunction_t) start_task,
(char *) "start_task",
( configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE,
(void *) NULL,
(UBaseType_t) START_TASK_PRIO ,
(TaskHandle_t *) &start_task_handler );
vTaskStartScheduler(); //开启任务调度器
}
6.2 主函数调用入口函数,操作系统开始进行任务的切换和调度
#include "stm32f4xx.h" // Device header
#include "stdio.h"
#include "myusart.h"
#include "FreeRTOS.h"
#include "task.h"
#include "dynamic.h"
int main(void)
{
//1、硬件初始化
My_UsartInit();
//2、进入入口函数
freertos_demo();
}
6.3 实验结果

至此,已经讲解完毕!初次学习,循序渐进,一步步掌握即可!以上就是全部内容!请务必掌握,创作不易,欢迎大家点赞加关注评论,您的支持是我前进最大的动力!下期再见!
6560

被折叠的 条评论
为什么被折叠?



