基于 RTOS 信号量优先级翻转的硬件定时器补偿策略:以 FreeRTOS 和 STM32 为例

1. 问题背景:优先级翻转如何破坏定时器精度

在 FreeRTOS 中,信号量用于任务间同步,但经典优先级翻转(Priority Inversion)会导致高优先级任务被低优先级任务阻塞,而中优先级任务又抢占低优先级任务,形成不可预测的延迟。当硬件定时器中断(如 PWM 或周期采样)与低优先级任务共享信号量时,这种延迟会直接反映为定时器周期的抖动,甚至丢失中断,破坏实时性。

例如,一个高优先级控制任务(优先级 3)等待信号量,而低优先级任务(优先级 1)持有该信号量,同时一个中优先级任务(优先级 2)持续运行,导致高优先级任务等待时间不可控。若定时器中断在此时触发,中断服务程序(ISR)可能因信号量被占用而延迟处理,造成定时误差。

2. 补偿策略核心思想

补偿策略的核心是:在检测到优先级翻转发生时,动态调整硬件定时器的重装载值(ARR),以抵消因阻塞造成的额外时间。具体实现分为两步:

  • 测量阻塞时间:在任务获取信号量前记录时间戳,获取后计算差值,得到实际阻塞时长。
  • 调整定时器周期:将阻塞时长折算为定时器时钟周期数,并修改 ARR 值,使下一次中断提前触发,从而补偿丢失的时间。

此方法适用于周期性中断场景,如电机控制中的 PWM 同步采样,或传感器数据采集。

3. 硬件与软件环境

  • 硬件:STM32F407 开发板(Cortex-M4,168MHz),使用 TIM2 作为硬件定时器,产生 1kHz 周期中断。
  • 软件:STM32CubeIDE + FreeRTOS(CMSIS-OS v2 封装),HAL 库。

4. 配置步骤

4.1 定时器基础配置

在 CubeMX 中启用 TIM2,时钟源为内部时钟(APB1 定时器时钟 84MHz),预分频器(PSC)设为 83,自动重装载值(ARR)设为 999,产生 1kHz 中断(84MHz/(83+1)/(999+1)=1kHz)。使能中断,并在 FreeRTOS 中创建两个任务:高优先级任务(HighTask)和低优先级任务(LowTask),以及一个二值信号量。

4.2 信号量与时间戳机制

使用 osSemaphoreNew 创建信号量,并利用 HAL_GetTick() 或 DWT 计数器获取高精度时间戳(建议使用 DWT,精度为 CPU 周期)。

5. 完整代码示例

以下代码展示了补偿策略的核心实现。

/* Includes */
#include "cmsis_os.h"
#include "stm32f4xx_hal.h"
#include "dwt_delay.h"  // 假设有DWT延迟库

/* 全局变量 */
osSemaphoreId_t semHandle;
volatile uint32_t last_overflow_time;  // 上次中断时间戳

/* 定时器中断回调 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2) {
        BaseType_t xHigherPriorityTaskWoken = pdFALSE;
        // 记录当前时间戳(DWT计数器)
        last_overflow_time = DWT_GetCycle();
        // 发送信号量给高优先级任务(从ISR)
        osSemaphoreReleaseFromISR(semHandle, &xHigherPriorityTaskWoken);
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

/* 高优先级任务 */
void HighTask(void *argument)
{
    uint32_t start_time, end_time, blocked_cycles;
    uint32_t current_arr, new_arr;
    TIM_TypeDef *tim = TIM2;

    for (;;) {
        // 等待信号量(可能发生优先级翻转)
        start_time = DWT_GetCycle();
        osSemaphoreAcquire(semHandle, osWaitForever);
        end_time = DWT_GetCycle();

        // 计算阻塞时间(周期数)
        blocked_cycles = end_time - start_time;
        // 假设正常获取信号量应小于某个阈值(如1000周期),超过则视为翻转
        if (blocked_cycles > 1000) {
            // 补偿:将阻塞时间折算为定时器周期数(84MHz,1周期=1/84us)
            // 定时器频率 = 84MHz/(PSC+1) = 1MHz,所以1周期=1us
            uint32_t blocked_us = blocked_cycles / 84;  // 粗略转换
            // 调整ARR,使下一次中断提前blocked_us微秒
            current_arr = tim->ARR;
            new_arr = (current_arr > blocked_us) ? (current_arr - blocked_us) : 0;
            __HAL_TIM_SET_AUTORELOAD(&htim2, new_arr);
            // 重新启动定时器(如果ARR为0则需特殊处理)
            if (new_arr == 0) {
                __HAL_TIM_SET_COUNTER(&htim2, 0);
                __HAL_TIM_ENABLE(&htim2);
            }
        }

        // 处理业务(例如读取ADC、控制输出)
        // ...

        // 恢复ARR为默认值(可选,根据需求)
        __HAL_TIM_SET_AUTORELOAD(&htim2, 999);
    }
}

/* 低优先级任务(持有信号量) */
void LowTask(void *argument)
{
    for (;;) {
        osSemaphoreAcquire(semHandle, osWaitForever);
        // 模拟长时间占用信号量
        HAL_Delay(5);
        osSemaphoreRelease(semHandle);
        osDelay(10);
    }
}

/* 主函数中初始化 */
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM2_Init();
    DWT_Init();

    // 创建信号量
    semHandle = osSemaphoreNew(1, 0, NULL);

    // 创建任务
    osThreadNew(HighTask, NULL, 3);  // 高优先级
    osThreadNew(LowTask, NULL, 1);   // 低优先级

    // 启动定时器
    HAL_TIM_Base_Start_IT(&htim2);

    osKernelStart();
    while (1) {}
}

6. 注意事项

  • 时间戳精度:使用 DWT 计数器获取 CPU 周期,精度高,但需注意溢出(32位计数器在168MHz下约25秒溢出一次),建议使用64位扩展或定期复位。
  • 补偿范围:如果阻塞时间过长,ARR 可能减到 0,导致定时器立即溢出,需特殊处理(如设置最小 ARR 值或跳过补偿)。
  • 中断安全:在 ISR 中调用 osSemaphoreReleaseFromISR 是安全的,但修改 ARR 寄存器时需确保操作原子性(STM32 的 ARR 写入是原子的,但建议在临界区或关闭中断)。
  • 任务优先级设计:补偿策略不能根治优先级翻转,应优先使用互斥量(Mutex)并启用优先级继承,或使用 FreeRTOS 的 vTaskPrioritySet 动态调整优先级。本方法仅作为补充。
  • 实时性验证:建议使用逻辑分析仪或示波器测量实际中断周期,验证补偿效果。

7. 总结

本文提出的硬件定时器补偿策略,通过动态调整 ARR 值,有效缓解了因优先级翻转导致的定时器周期抖动。该方法实现简单,适用于周期性中断场景,但需注意补偿边界和原子操作。在实际工程中,应结合互斥量优先级继承等机制,从根源上减少优先级翻转,实现更可靠的实时系统。