讨论背景
不知道初学者有没有遇到这样的疑问,就是我们在FreeRTOS任务调度中使用 vTaskDelay() 延迟函数阻塞任务时,对这个延迟时间一头雾水,为什么 vTaskDelay(100) 就是延迟了100ms?有的时候读者还会碰到 vTaskDelay(pdMS_TO_TICKS(10)); 类似这样的定义。当在开发环境中跳转查看时,最直观的感受就是一堆宏定义。这里,我们还是以 STM32F103ZETx + FreeRTOS API开发为例进行论述。
时间延迟相关宏定义
如果我们要在FreeRTOS上实现精确的任务延时调度,以下两个宏定义是必不可少的。当然了,对所定义的值也是有要求的。
#define configCPU_CLOCK_HZ SystemCoreClock //72MHz
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )
上面的宏定义在配置头文件FreeRTOSConfig.h 中能看到,但并不能解决我们的疑问。比如,(1)为什么这里的 SystemCoreClock 是72MHz,它是在哪里定义的?(2)FreeRTOS是如何引入或者说复用 SysTick 内核滴答定时器的?(3)vTaskDelay是如何在延迟中起作用的?等等。要搞清楚这些问题,有两个文档特别重要:1. 《Cortex-M3权威指南》;2.《stm32f1参考手册》。
针对以上问题,我们来逐一查找。在《stm32f1参考手册》时钟部分,我么可以看到下面这段话:
The RCC feeds the Cortex® System Timer (SysTick) external clock with the AHB clock(HCLK) divided by 8. The SysTick can work either with this clock or with the Cortex® clock(HCLK), configurable in the SysTick Control and Status register.
RCC 向 Cortex® 系统定时器(SysTick)提供外部时钟,该时钟为 AHB 时钟(HCLK)八分频后的信号。SysTick 可选择使用此八分频时钟或 Cortex® 内核时钟(HCLK),具体可通过 SysTick 控制与状态寄存器进行配置。
时间延迟若干问题回答
首先,我们需要明确一点,半导体公司在购买获得ARM授权后,SysTick 已经是Cortex-M3处理器内核自带的一个定时器资源了,是ARM核的一部分,它被捆绑在了 NVIC 中,用于产生 SysTick 异常。 其次,它的时钟来源由自身寄存器配置和具体MCU设计决定。通过寄存器 SysTick -> CLKSOURCE 可以选择使用内部时钟源还是外部时钟源,不同厂商在设计自己的MCU时,可以选择是使用HCLK时钟信号,还是HCLK时钟信号的8分频。
下面我们来回答第一个问题:(1)为什么这里的 SystemCoreClock 是72MHz,它是在哪里定义的?

上图的 Cortex System timer就是 SysTick 的全称。可以看到,我们把这个时钟源配置成了72MHz,且无分频。HAL库具体实现函数如下(DriversSTM32G4xx_HAL_DriverSrcstm32g4xx_hal_rcc.c):
/* Update the SystemCoreClock global variable */
SystemCoreClock = HAL_RCC_GetSysClockFreq() >> AHBPrescTable[(RCC->CFGR & RCC_CFGR_HPRE) >> RCC_CFGR_HPRE_Pos];
回答第二个问题:(2)FreeRTOS是如何引入或者说复用 SysTick内核滴答定时器的?
/* Let the user override the default SysTick clock rate. If defined by the
* user, this symbol must equal the SysTick clock rate when the CLK bit is 0 in the
* configuration register. */
#ifndef configSYSTICK_CLOCK_HZ
#define configSYSTICK_CLOCK_HZ ( configCPU_CLOCK_HZ )
/* Ensure the SysTick is clocked at the same frequency as the core. */
#define portNVIC_SYSTICK_CLK_BIT_CONFIG ( portNVIC_SYSTICK_CLK_BIT )
#else
/* Select the option to clock SysTick not at the same frequency as the core. */
#define portNVIC_SYSTICK_CLK_BIT_CONFIG ( 0 )
#endif
路径FreeRTOS-KernelportableGCCARM_CM3port.c源码中,portNVIC_SYSTICK_LOAD_REG 即是 SysTick 的重装载寄存器( 0xe000e014),它的值由 ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) – 1UL 配置。portNVIC_SYSTICK_CTRL_REG 即是 SysTick 的控制及状态寄存器( 0xe000e010),它的值这里是由几个功能位决定。我们可以看到配置configCPU_CLOCK_HZ 的值就是配置configSYSTICK_CLOCK_HZ 的值。所以,为了和 MCU 裸机上的 SysTick 频率保持一致,以获取精确的调度延迟,configCPU_CLOCK_HZ 的值需要等于72MHz。这样,FreeRTOS通过对 SysTick 寄存器的复用,达到了定时1tick == 1ms的目的。读者可以查看《Cortex-M3权威指南》中对 SysTick 寄存器的详细描述。
/* Configure SysTick to interrupt at the requested rate. */
portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL; //72000 - 1
portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT_CONFIG | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT );
回答第三个问题:(3)vTaskDelay是如何在延迟中起作用的?
其实,vTaskDelay的处理逻辑比较复杂,其依赖 FreeRTOS 的内核机制(TCB、链表管理、调度器)和硬件定时器(SysTick),是 FreeRTOS 中最基础、最常用的延时函数。
extern void xPortSysTickHandler( void );
void SysTick_Handler(void)
{
/* USER CODE BEGIN SysTick_IRQn 0 */
if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED)
{
xPortSysTickHandler();
}
/* USER CODE END SysTick_IRQn 0 */
/* USER CODE BEGIN SysTick_IRQn 1 */
/* USER CODE END SysTick_IRQn 1 */
}
系统滴答定时器 SysTick 每中断一次都会进入中断服务函数执行 xPortSysTickHandler() 。而 xPortSysTickHandler() 通过调用 xTaskIncrementTick() 函数完成任务的定时调度。我们可以借用AI工具进行辅助理解:
首先判断延时参数是否有效(若 xTicksToDelay = 0,直接返回,不阻塞),然后将当前任务从「就绪态」切换为「阻塞态」;SysTick 中断服务函数(ISR)会调用 FreeRTOS 的核心时钟处理函数 xTaskIncrementTick(),核心逻辑是:
(1)递增全局滴答计数器 xTickCount;
(2)遍历「阻塞任务链表」(xDelayedTaskList),将每个任务的 xTicksToDelay 减 1;
(3)若某个任务的 xTicksToDelay 减为 0,说明延时到期,将其从阻塞链表移回「就绪链表」;
(4)若就绪链表中有优先级更高的任务,标记「需要上下文切换」,中断退出后触发切换。当延时到期的任务被移回「就绪链表」后,若其优先级高于当前运行的任务(FreeRTOS 默认是抢占式调度),调度器会立即触发上下文切换,让该任务获得 CPU 使用权,从之前的阻塞点继续执行。
知道 configCPU_CLOCK_HZ宏 和 configTICK_RATE_HZ宏 的作用后,我们也就知道了下面两个宏定义的作用了。
/* Converts a time in milliseconds to a time in ticks. This macro can be
* overridden by a macro of the same name defined in FreeRTOSConfig.h in case the
* definition here is not suitable for your application. */
#ifndef pdMS_TO_TICKS
#define pdMS_TO_TICKS( xTimeInMs ) ( ( TickType_t ) ( ( ( uint64_t ) ( xTimeInMs ) * ( uint64_t ) configTICK_RATE_HZ ) / ( uint64_t ) 1000U ) )
#endif
/* Converts a time in ticks to a time in milliseconds. This macro can be
* overridden by a macro of the same name defined in FreeRTOSConfig.h in case the
* definition here is not suitable for your application. */
#ifndef pdTICKS_TO_MS
#define pdTICKS_TO_MS( xTimeInTicks ) ( ( TickType_t ) ( ( ( uint64_t ) ( xTimeInTicks ) * ( uint64_t ) 1000U ) / ( uint64_t ) configTICK_RATE_HZ ) )
#endif
相对延迟和绝对延迟
相对延迟和绝对延迟的概念,涉及到 vTaskDelay() 和 xTaskDelayUntil() 两个延迟函数:
void vTaskDelay( const TickType_t xTicksToDelay )
相对延迟函数vTaskDelay()只有一个参数,它的延迟开始时间是任务执行到vTaskDelay()函数开始计时,比如:vTaskDelay(50),指的是任务执行到vTaskDelay(50)函数那一刻开始延迟计时,阻塞目前任务,执行其它任务,理论上等计时50个tick后,再回来继续执行该任务。但是这50个tick很容易受到其它因素影响,比如,在计时满50个tick后,有一个更高优先级的任务处于就绪状态,那么这时可能就去执行那个更高优先级的任务了。这样导致再执行原来被阻塞的任务的时候,时间可能远超50个tick。所以,相对延迟是不可靠的。只是保证了任务最少的阻塞时间是50个tick,让任务切换,空出CPU资源给其它任务使用。相对延迟函数一般放置在任务循环末尾。
BaseType_t xTaskDelayUntil( TickType_t * const pxPreviousWakeTime,
const TickType_t xTimeIncrement ) PRIVILEGED_FUNCTION;
绝对延迟函数xTaskDelayUntil()有两个参数,第一个参数可以理解为是一个时间基准,其初始化为当时的系统tick时间。之后,这个时间基准保存为上一次任务被唤醒时的tick计数,延迟时间是在这个基准上延迟的。比如:xTaskDelayUntil(&PreviousWakeTime,pdMS_TO_TICKS(1000) ),指的是在上次保存的任务被唤醒时刻tick基准上延迟了1000ms。另外,需要理解的是即使有高优先级任务打断了阻塞任务的唤醒,理论上任务恢复执行的tick时间被延后了,但基准时间数值还是上一次任务被唤醒时保存的tick时间值,不会受影响而改变,而且第二个参数xTimeIncrement是不会变的。绝对延迟函数一般放置在任务循环末尾。
学习参考
FreeRTOS中的时间和tick之间的转换