FreeRTOS学习笔记(七):官方函数解析笔记

一、快照模式 / 值捕获模式

以下为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来赋值,主要有以下几个原因:

  1. 保证操作的原子性语义
    xTickCount是一个全局的、volatile修饰的变量,通常在中断服务程序和任务中都会被访问。使用xConstTickCount作为中间变量,可以确保"读取旧值→计算新值→写入新值"的逻辑在语义上更清晰,避免因编译器优化或多线程/中断并发访问导致的歧义。

  2. 便于后续代码复用
    上面提供的代码是xTaskIncrementTick()函数的简化版本,在完整实现中,xConstTickCount(递增后的节拍值)会被多次使用,例如:

    • 检查哪些任务的阻塞时间已到期(需要与任务的唤醒时间比较)
    • 参与任务调度决策
    • 作为调试信息输出等

    提前计算并保存递增后的值,可以避免多次计算xTickCount + 1,既提高效率,也保证了后续所有操作使用的是"同一时刻"的节拍值。

  3. 防御编译器优化可能带来的问题
    虽然volatile关键字会阻止编译器过度优化,但在某些复杂场景下(尤其是多核心或带缓存的系统中),通过中间变量显式保存计算结果,可以确保读写操作的顺序性,避免出现"读取到未更新的值"或"重复计算"等意外情况。

简单说,这种写法是FreeRTOS代码风格中"清晰优先、安全优先"的体现,虽然单从这几行看似乎多此一举,但在完整的系统逻辑中,这种设计能有效提升代码的可靠性和可维护性。

  1. 这种写法主要是为了确保在当前函数的整个逻辑中,使用的是“同一时刻”的节拍值,而非“释放使用频率”。

具体来说,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)进行,保证了逻辑的一致性。

这本质上是一种“快照”思想——在多线程/中断并发场景下,通过局部变量保存全局变量的某一时刻状态,避免因全局变量被异步修改而导致的逻辑错误。

  1. 这种通过局部变量保存全局变量(或共享变量)的特定时刻值,以确保在当前操作序列中使用一致状态的编程方式,通常被称为**“快照模式”(Snapshot Pattern)** 或**“值捕获”(Value Capture)**。
    ☆☆☆☆☆
    在并发编程(尤其是实时操作系统如FreeRTOS的场景中),这种模式的核心思想是:
  • 对可能被多线程/中断异步修改的共享变量(如xTickCount),在操作开始时“捕获”其当前值
  • 后续所有逻辑都基于这个“快照值”进行,而非反复读取可能已被修改的共享变量
  • 以此避免因并发修改导致的逻辑不一致(如多次读取同一变量却得到不同值)

这种写法在需要保证“操作原子性”或“逻辑一致性”的场景中非常常见,尤其适合:

  1. 多线程/多任务环境下的共享变量访问
  2. 中断与主程序可能同时操作的变量
  3. 需要基于“同一时刻状态”进行一系列判断的逻辑

本质上,这是一种通过局部变量“固化”瞬时状态的策略,是并发编程中保证数据一致性的简单而有效的手段。

二、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(就绪)状态的系统滴答计数**,其核心作用是:

  1. 标记任务唤醒的时间点
    当任务调用 vTaskDelay()xSemaphoreTake() 等阻塞性API时,系统会根据阻塞时长计算出一个未来的系统滴答值(当前滴答数 + 阻塞时长),并将这个值存入任务控制块(TCB)的列表项中,即 xItemValue
    例如:任务调用 vTaskDelay(100) 时,xItemValue 会被设置为 当前滴答数 + 100,表示该任务将在100个滴答后被唤醒。

  2. 作为阻塞列表的排序依据
    所有处于阻塞状态的任务会被放入“阻塞列表”(blocked list),而列表正是以 xItemValue 为依据进行升序排序的(最早唤醒的任务排在最前)。
    这样系统在遍历阻塞列表时,能快速通过 xItemValue 找到“下一个即将唤醒的任务”。

  3. 用于判断任务是否到唤醒时间
    在SysTick中断(系统滴答定时器)中,系统会用当前滴答数(xConstTickCount)与 xItemValue 比较:

    • xConstTickCount >= xItemValue:说明任务阻塞时间已到,需要将其从阻塞列表移至就绪列表;
    • xConstTickCount < xItemValue:说明任务还未到唤醒时间,此时 xItemValue 会被暂存为 xNextTaskUnblockTime(下一次需要唤醒任务的时间),用于优化SysTick定时器的触发时机(避免无效中断)。

简单说,xItemValue 就是阻塞任务的“闹钟时间”,系统通过这个值来管理任务的唤醒节奏,是FreeRTOS时间管理和任务调度的核心变量之一。**

三、移植

3.1 AMR_CM4F 和 ARM_CM4_MPU区别

在FreeRTOS v9.0版本中,ARM_CM4_MPUARM_CM4F文件夹均针对ARM Cortex-M4架构设计,但适用场景和功能有明确区别,主要体现在对硬件功能的支持上:

一、核心区别:是否支持MPU和FPU

  1. ARM_CM4F文件夹
    用于带有FPU(浮点运算单元)的Cortex-M4芯片

    • “F”代表FPU(Floating Point Unit),该文件夹下的移植代码包含了对硬件浮点单元的支持,能更高效地处理浮点运算。
    • 适用于绝大多数Cortex-M4芯片(如STM32F4系列、GD32F4系列等),这些芯片普遍集成FPU,适合需要浮点计算的场景(如电机控制、传感器数据处理)。
  2. 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) 周期性触发中断产生。两者的计算逻辑紧密关联,核心是通过定时器配置确保系统节拍的周期稳定。

一、核心概念

  1. 系统节拍(RTOS Tick)
    FreeRTOS的最小时间单位,用TickType_t表示,单位为“节拍数”。例如,若1个节拍代表1ms,则任务延时10个节拍即表示延时10ms。

  2. 系统定时器
    用于产生周期性中断的硬件定时器(如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(单位:秒)。

具体计算步骤:

  1. 确定系统定时器的时钟频率(f_timer
    定时器的时钟源由硬件决定(如SysTick可选择HCLKHCLK/8,通用定时器可能由APB总线时钟分频得到)。例如:

    • 若SysTick使用HCLK/8作为时钟源,且HCLK = 72MHz,则f_timer = 72MHz / 8 = 9MHz
  2. 确定系统节拍周期(T_tick
    FreeRTOSConfig.h中的configTICK_RATE_HZ配置,代表每秒的节拍数。例如:

    • configTICK_RATE_HZ = 1000,则T_tick = 1/1000 = 1ms(即每1ms产生1个节拍)。
  3. 计算定时器的重载值(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一个节拍)。

计算过程:

  1. 定时器时钟频率f_timer = HCLK / 8 = 48MHz / 8 = 6MHz(6,000,000 Hz);
  2. 节拍周期T_tick = 1 / 1000 = 0.001s
  3. 重载值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中的配置与关联

  1. configTICK_RATE_HZ
    定义在FreeRTOSConfig.h中,直接决定节拍周期。例如:

    #define configTICK_RATE_HZ  (1000)  // 1ms/节拍
    

    数值越大,节拍周期越小(时间精度越高),但定时器中断频率越高(消耗更多CPU资源)。

  2. 定时器中断服务程序(ISR)
    系统定时器的ISR需调用xTaskIncrementTick()(FreeRTOS内核函数),示例(以SysTick为例):

    void SysTick_Handler(void) {
        if (xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) {
            xTaskIncrementTick();  // 系统节拍计数+1
        }
    }
    

    该函数会检查是否有任务因节拍到达而解除阻塞,并在必要时触发任务调度。

  3. 定时器优先级
    系统定时器的中断优先级必须低于或等于configMAX_SYSCALL_INTERRUPT_PRIORITY(定义在FreeRTOSConfig.h),否则xTaskIncrementTick()等内核函数可能无法安全执行。

五、常见问题与注意事项

  1. 节拍周期与定时器时钟不匹配
    reload计算错误(如时钟频率取值错误),会导致实际节拍周期与预期不符(如配置1ms实际为2ms),进而影响任务延时、调度周期的准确性。

  2. 定时器选择
    除了SysTick,也可使用通用定时器(如TIM2)作为系统定时器,只需确保其能产生周期性中断,且ISR中正确调用xTaskIncrementTick()

  3. 低功耗场景
    若需进入低功耗模式,可在定时器中断中唤醒芯片,此时需确保定时器在低功耗下仍能正常计数(如使用低速时钟源)。

总结

FreeRTOS的系统节拍与系统定时器的核心关系是:通过配置定时器的时钟频率和重载值,使其周期性产生中断,每次中断对应1个系统节拍。计算的关键是根据configTICK_RATE_HZ确定节拍周期,再结合定时器时钟频率算出重载值,最终实现稳定的时间基准。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值