文章目录
一、快照模式 / 值捕获模式
以下为systick中断服务函数中的一段代码
static volatile TickType_t xTickCount = ( TickType_t ) 0U;
void xTaskIncrementTick( void )
{
TCB_t * pxTCB;
TickType_t xItemValue;
const TickType_t xConstTickCount = xTickCount + 1;
xTickCount = xConstTickCount;
}
为啥要这样赋值xTickCount ,不直接xTickCount = xTickCount + 1?
在这段FreeRTOS代码中,不直接使用xTickCount = xTickCount + 1,而是通过中间变量xConstTickCount来赋值,主要有以下几个原因:
-
保证操作的原子性语义
xTickCount是一个全局的、volatile修饰的变量,通常在中断服务程序和任务中都会被访问。使用xConstTickCount作为中间变量,可以确保"读取旧值→计算新值→写入新值"的逻辑在语义上更清晰,避免因编译器优化或多线程/中断并发访问导致的歧义。 -
便于后续代码复用
上面提供的代码是xTaskIncrementTick()函数的简化版本,在完整实现中,xConstTickCount(递增后的节拍值)会被多次使用,例如:- 检查哪些任务的阻塞时间已到期(需要与任务的唤醒时间比较)
- 参与任务调度决策
- 作为调试信息输出等
提前计算并保存递增后的值,可以避免多次计算
xTickCount + 1,既提高效率,也保证了后续所有操作使用的是"同一时刻"的节拍值。 -
防御编译器优化可能带来的问题
虽然volatile关键字会阻止编译器过度优化,但在某些复杂场景下(尤其是多核心或带缓存的系统中),通过中间变量显式保存计算结果,可以确保读写操作的顺序性,避免出现"读取到未更新的值"或"重复计算"等意外情况。
简单说,这种写法是FreeRTOS代码风格中"清晰优先、安全优先"的体现,虽然单从这几行看似乎多此一举,但在完整的系统逻辑中,这种设计能有效提升代码的可靠性和可维护性。
- 这种写法主要是为了确保在当前函数的整个逻辑中,使用的是“同一时刻”的节拍值,而非“释放使用频率”。
具体来说,xTickCount是全局变量,且被volatile修饰,意味着它可能被中断服务程序(ISR)或其他任务随时修改(比如在多核心系统中)。如果不通过中间变量,直接多次使用xTickCount,可能出现以下问题:
假设完整函数中需要多次用到“递增后的值”,比如:
// 不使用中间变量的情况
xTickCount = xTickCount + 1; // 步骤1:递增
if (某任务的唤醒时间 == xTickCount) { ... } // 步骤2:判断
// (中间可能被其他代码/中断修改xTickCount的值)
if (另一任务的唤醒时间 == xTickCount) { ... } // 步骤3:再次判断
此时步骤2和步骤3中读取的xTickCount可能已经被其他操作修改(不再是步骤1中递增后的值),导致逻辑混乱。
而通过xConstTickCount保存“递增后的值”:
const TickType_t xConstTickCount = xTickCount + 1; // 保存当前递增后的值
xTickCount = xConstTickCount; // 更新全局变量
// 后续所有判断都使用xConstTickCount,确保是同一时刻的节拍值
if (某任务的唤醒时间 == xConstTickCount) { ... }
if (另一任务的唤醒时间 == xConstTickCount) { ... }
这样即使后续xTickCount被其他操作修改,当前函数中涉及“本次节拍递增”的所有逻辑,都能基于同一个确定的值(xConstTickCount)进行,保证了逻辑的一致性。
这本质上是一种“快照”思想——在多线程/中断并发场景下,通过局部变量保存全局变量的某一时刻状态,避免因全局变量被异步修改而导致的逻辑错误。
- 这种通过局部变量保存全局变量(或共享变量)的特定时刻值,以确保在当前操作序列中使用一致状态的编程方式,通常被称为**“快照模式”(Snapshot Pattern)** 或**“值捕获”(Value Capture)**。
☆☆☆☆☆
在并发编程(尤其是实时操作系统如FreeRTOS的场景中),这种模式的核心思想是:
- 对可能被多线程/中断异步修改的共享变量(如
xTickCount),在操作开始时“捕获”其当前值 - 后续所有逻辑都基于这个“快照值”进行,而非反复读取可能已被修改的共享变量
- 以此避免因并发修改导致的逻辑不一致(如多次读取同一变量却得到不同值)
这种写法在需要保证“操作原子性”或“逻辑一致性”的场景中非常常见,尤其适合:
- 多线程/多任务环境下的共享变量访问
- 中断与主程序可能同时操作的变量
- 需要基于“同一时刻状态”进行一系列判断的逻辑
本质上,这是一种通过局部变量“固化”瞬时状态的策略,是并发编程中保证数据一致性的简单而有效的手段。
二、xItemValue - 唤醒时间戳
/* 节点结构体定义 */
struct xLIST_ITEM
{
TickType_t xItemValue; /* 辅助值,用于帮助节点做顺序排列 */
struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */
struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */
void * pvOwner; /* 指向拥有该节点的内核对象,通常是TCB */
void * pvContainer; /* 指向该节点所在的链表 */
};
typedef struct xLIST_ITEM ListItem_t; /* 节点数据类型重定义 */
在FreeRTOS中,xItemValue 是阻塞任务的“唤醒时间戳”,代表该任务从阻塞(阻塞)状态转为ready(就绪)状态的系统滴答计数**,其核心作用是:
-
标记任务唤醒的时间点
当任务调用vTaskDelay()、xSemaphoreTake()等阻塞性API时,系统会根据阻塞时长计算出一个未来的系统滴答值(当前滴答数 + 阻塞时长),并将这个值存入任务控制块(TCB)的列表项中,即xItemValue。
例如:任务调用vTaskDelay(100)时,xItemValue会被设置为当前滴答数 + 100,表示该任务将在100个滴答后被唤醒。 -
作为阻塞列表的排序依据
所有处于阻塞状态的任务会被放入“阻塞列表”(blocked list),而列表正是以xItemValue为依据进行升序排序的(最早唤醒的任务排在最前)。
这样系统在遍历阻塞列表时,能快速通过xItemValue找到“下一个即将唤醒的任务”。 -
用于判断任务是否到唤醒时间
在SysTick中断(系统滴答定时器)中,系统会用当前滴答数(xConstTickCount)与xItemValue比较:- 若
xConstTickCount >= xItemValue:说明任务阻塞时间已到,需要将其从阻塞列表移至就绪列表; - 若
xConstTickCount < xItemValue:说明任务还未到唤醒时间,此时xItemValue会被暂存为xNextTaskUnblockTime(下一次需要唤醒任务的时间),用于优化SysTick定时器的触发时机(避免无效中断)。
- 若
简单说,xItemValue 就是阻塞任务的“闹钟时间”,系统通过这个值来管理任务的唤醒节奏,是FreeRTOS时间管理和任务调度的核心变量之一。**
三、移植
3.1 AMR_CM4F 和 ARM_CM4_MPU区别
在FreeRTOS v9.0版本中,ARM_CM4_MPU和ARM_CM4F文件夹均针对ARM Cortex-M4架构设计,但适用场景和功能有明确区别,主要体现在对硬件功能的支持上:
一、核心区别:是否支持MPU和FPU
-
ARM_CM4F文件夹
用于带有FPU(浮点运算单元)的Cortex-M4芯片。- “F”代表FPU(Floating Point Unit),该文件夹下的移植代码包含了对硬件浮点单元的支持,能更高效地处理浮点运算。
- 适用于绝大多数Cortex-M4芯片(如STM32F4系列、GD32F4系列等),这些芯片普遍集成FPU,适合需要浮点计算的场景(如电机控制、传感器数据处理)。
-
ARM_CM4_MPU文件夹
用于带有MPU(内存保护单元)的Cortex-M4芯片。- “MPU”代表Memory Protection Unit(内存保护单元),该文件夹下的代码额外实现了对MPU的支持,可用于隔离不同任务的内存访问权限,增强系统安全性和稳定性(例如防止任务越权访问内存)。
- 并非所有Cortex-M4芯片都带MPU,因此该文件夹仅适用于明确集成了MPU的型号。
二、MPU(内存保护单元)的作用
MPU是Cortex-M4内核中可选的硬件模块,主要功能是:
- 内存区域划分:将芯片的内存(SRAM、Flash等)划分为多个区域,并为每个区域设置访问权限(如只读、只写、读写、执行禁止等)。
- 任务隔离:在多任务系统中,通过MPU限制不同任务只能访问自身权限范围内的内存,防止某一任务出错(如数组越界、非法指针)时破坏其他任务或内核的内存数据,提高系统容错能力。
- 安全防护:可用于保护关键代码或数据(如加密密钥)不被未授权任务访问或修改。
三、如何选择?
- 如果你的Cortex-M4芯片有FPU但无MPU(多数情况),使用
ARM_CM4F文件夹下的移植代码。 - 如果你的Cortex-M4芯片同时有MPU和FPU,且需要内存保护功能,使用
ARM_CM4_MPU文件夹下的代码。 - 若芯片既无FPU也无MPU(极少数Cortex-M4简化型号),则可能需要使用更基础的
ARM_CM4文件夹(部分FreeRTOS版本可能合并或省略此文件夹)。
实际开发中,需根据具体芯片的数据手册确认是否包含FPU和MPU,再选择对应的移植文件。
四 freeRTOS的系统节拍如何计算
在FreeRTOS中,系统节拍(RTOS Tick) 是任务调度、延时(vTaskDelay())、阻塞等时间相关操作的基准时间单位,由系统定时器(通常是硬件定时器,如SysTick) 周期性触发中断产生。两者的计算逻辑紧密关联,核心是通过定时器配置确保系统节拍的周期稳定。
一、核心概念
-
系统节拍(RTOS Tick):
FreeRTOS的最小时间单位,用TickType_t表示,单位为“节拍数”。例如,若1个节拍代表1ms,则任务延时10个节拍即表示延时10ms。 -
系统定时器:
用于产生周期性中断的硬件定时器(如Cortex-M内核的SysTick、GD32/STM32的通用定时器TIMx),其中断服务程序(ISR)会调用FreeRTOS的xTaskIncrementTick()函数,每触发一次中断,系统节拍计数+1。
你可以把它想象成一个精准的、周期性的定时器中断。
定义:系统节拍是 FreeRTOS 内核定期产生的一个固定频率的时钟信号。
来源:通常由一个硬件定时器(如 MCU 的 SysTick 定时器)来产生。
频率:它的频率由 configTICK_RATE_HZ 这个宏定义在 FreeRTOSConfig.h 配置文件中设定。例如:
#define configTICK_RATE_HZ ( ( TickType_t ) 1000 ) // 表示每秒 1000 个节拍
这意味着系统节拍每 1 毫秒 发生一次。
二、计算逻辑(关键公式)
系统定时器的配置目标是:让定时器每隔T时间产生一次中断,其中T = 1 / configTICK_RATE_HZ(单位:秒)。
具体计算步骤:
-
确定系统定时器的时钟频率(
f_timer):
定时器的时钟源由硬件决定(如SysTick可选择HCLK或HCLK/8,通用定时器可能由APB总线时钟分频得到)。例如:- 若SysTick使用
HCLK/8作为时钟源,且HCLK = 72MHz,则f_timer = 72MHz / 8 = 9MHz。
- 若SysTick使用
-
确定系统节拍周期(
T_tick):
由FreeRTOSConfig.h中的configTICK_RATE_HZ配置,代表每秒的节拍数。例如:- 若
configTICK_RATE_HZ = 1000,则T_tick = 1/1000 = 1ms(即每1ms产生1个节拍)。
- 若
-
计算定时器的重载值(
reload):
定时器需要在T_tick时间内计数到溢出,产生一次中断。重载值计算公式为:reload = (f_timer × T_tick) - 1(减1是因为定时器通常从
reload值开始向下计数,计数到0时产生中断)
三、实例说明
以GD32C113芯片为例,假设:
- 系统时钟
HCLK = 48MHz(通过时钟配置函数设置); - 使用SysTick作为系统定时器,时钟源选择默认的
HCLK/8(未调用systick_clksource_set修改); configTICK_RATE_HZ = 1000(即1ms一个节拍)。
计算过程:
- 定时器时钟频率
f_timer = HCLK / 8 = 48MHz / 8 = 6MHz(6,000,000 Hz); - 节拍周期
T_tick = 1 / 1000 = 0.001s; - 重载值
reload = (6,000,000 × 0.001) - 1 = 6000 - 1 = 5999。
此时,SysTick定时器会从5999开始向下计数,每计数6000个时钟周期(耗时1ms)产生一次中断,触发系统节拍计数+1,实现1ms的节拍基准。
计数一次的时间为 t = 1/6MHz
重载值设置为6000,那个系统定时器每计数6000会产生一次周期中断,即周期为 T = 6000 * t = 6000 * (1/6MHz) = 1 ms
此外如果不配置系统时钟源 那么
void systick_config(void)
{
/* systick clock source is from HCLK/8 */
// systick_clksource_set(SYSTICK_CLKSOURCE_HCLK_DIV8);
// systick_clksource_set(SYSTICK_CLKSOURCE_HCLK);
count_1us = SystemCoreClock/8000000;
count_1ms = count_1us * 1000;
}
在 GD32C113 芯片中,SysTick 定时器的时钟源(systick_clksource)默认配置为 “外部时钟源”,其频率由系统时钟(SYSCLK)经过分频得到。
具体频率计算如下:
外部时钟源是系统时钟(SYSCLK)的 1/8。
GD32C113 芯片复位后的默认系统时钟(SYSCLK)为内部高速振荡器(HSI)的频率,即8MHz。
因此,若未调用systick_clksource_set函数修改时钟源,SysTick 定时器的默认时钟频率为:8MHz ÷ 8 = 1MHz。
四、FreeRTOS中的配置与关联
-
configTICK_RATE_HZ:
定义在FreeRTOSConfig.h中,直接决定节拍周期。例如:#define configTICK_RATE_HZ (1000) // 1ms/节拍数值越大,节拍周期越小(时间精度越高),但定时器中断频率越高(消耗更多CPU资源)。
-
定时器中断服务程序(ISR):
系统定时器的ISR需调用xTaskIncrementTick()(FreeRTOS内核函数),示例(以SysTick为例):void SysTick_Handler(void) { if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { xTaskIncrementTick(); // 系统节拍计数+1 } }该函数会检查是否有任务因节拍到达而解除阻塞,并在必要时触发任务调度。
-
定时器优先级:
系统定时器的中断优先级必须低于或等于configMAX_SYSCALL_INTERRUPT_PRIORITY(定义在FreeRTOSConfig.h),否则xTaskIncrementTick()等内核函数可能无法安全执行。
五、常见问题与注意事项
-
节拍周期与定时器时钟不匹配:
若reload计算错误(如时钟频率取值错误),会导致实际节拍周期与预期不符(如配置1ms实际为2ms),进而影响任务延时、调度周期的准确性。 -
定时器选择:
除了SysTick,也可使用通用定时器(如TIM2)作为系统定时器,只需确保其能产生周期性中断,且ISR中正确调用xTaskIncrementTick()。 -
低功耗场景:
若需进入低功耗模式,可在定时器中断中唤醒芯片,此时需确保定时器在低功耗下仍能正常计数(如使用低速时钟源)。
总结
FreeRTOS的系统节拍与系统定时器的核心关系是:通过配置定时器的时钟频率和重载值,使其周期性产生中断,每次中断对应1个系统节拍。计算的关键是根据configTICK_RATE_HZ确定节拍周期,再结合定时器时钟频率算出重载值,最终实现稳定的时间基准。
1648

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



