跳转到内容
新建笔记

STM32 HAL 与 FreeRTOS 的时基分工

HAL 和 FreeRTOS 都需要时间,但它们的计数和用途不同。HAL 用毫秒时基支持超时和 HAL_Delay();FreeRTOS 用内核 Tick 管理任务延时、超时及调度。CubeMX 常把 HAL 时基移到一个定时器,让默认 FreeRTOS 端口使用 SysTick,这是一种方便集成的方案。

共用 SysTick 也可以实现,前提是只有一个正确的入口,并处理好频率、调度器启动前后和低功耗补偿。不能把“必须把 HAL 从 SysTick 移开”当作所有版本、所有移植的硬件规律。

项目HAL原生 FreeRTOS
读取接口HAL_GetTick()xTaskGetTickCount()
单位毫秒;具体更新步长受 HAL Tick 配置影响TickType_t,每 Tick 为 1 / configTICK_RATE_HZ 秒
常见更新入口HAL_IncTick()本文端口的 xPortSysTickHandler()
普通延时HAL_Delay(ms) 忙等 HAL TickvTaskDelay(ticks) 阻塞当前任务
周期任务需要应用另外设计xTaskDelayUntil() 维持计划释放相位

1000 Hz 时两者数字常常同时每毫秒加 1,仍不能据此认定它们永远是同一计数。100 Hz 内核 Tick 为 10 ms;让每个内核中断只调用一次按 1 ms 配置的 HAL_IncTick(),会使 HAL 时间慢十倍。

为什么 ISR 中的 HAL_Delay 会卡住

跳转到“为什么 ISR 中的 HAL_Delay 会卡住”

假设某 ISR 的逻辑优先级为 3,而推进 HAL Tick 的 SysTick 为 15。ISR 调用 HAL_Delay() 后等待计数增加,但优先级 15 的 SysTick 无法抢占正在运行的优先级 3 ISR,于是等待无法完成。原解释中“低优先级 SysTick 会打断高优先级 ISR”的方向相反。

把 HAL Tick 换成逻辑优先级 5 的 TIM 中断也不能解决所有情况:它仍不能抢占优先级 3 或相同优先级的 ISR,且可能被 PRIMASK、BASEPRI 或临界区屏蔽。应让 ISR 尽快记录事件,由任务或硬件定时逻辑处理后续动作;不应把“提高 HAL Tick 优先级”作为在任意 ISR 中忙等的许可。

任务里调用 HAL_Delay() 通常也会浪费该任务的 CPU 时间,并可能挤压同级或低级任务。能用 RTOS 等待时应使用任务延时、队列、通知或其他阻塞式同步;微秒级波形和硬时序应交给合适的硬件外设。

中断优先级与读取方法

跳转到“中断优先级与读取方法”

本文采用 F103 Cortex-M3 的 4 位优先级、全部作为抢占位:

含义逻辑数值寄存器表示
最低中断优先级150xF0
一个常用内核 API 阈值示例50x50

V11.1.0 GCC ARM_CM3 默认把 PendSV 和 SysTick 设为最低优先级;端口也允许覆盖 vPortSetupTimerInterrupt() 使用其他内核时基。阈值 5 表示逻辑优先级 5–15 的 ISR 才能在满足接口要求时调用 FreeRTOS 的 ISR API;0–4 不能调用。

一个只调用 HAL_IncTick()、完全不使用内核 API 的 HAL Tick ISR,可以根据延迟预算选择阈值以上的紧迫性,例如逻辑优先级 0。它必须保持短小,且仍不能突破全局屏蔽。不能说“所有 ISR 都必须大于等于 FreeRTOS 阈值”。

NVIC_GetPriority(SysTick_IRQn) 已经返回未移位的逻辑值。再右移 4 位会把 15 错误显示成 0。HAL/CMSIS 设置接口也接受未移位值;只有直接检查原始寄存器字节时,才需要解释有效位的位置。

在支持的 CubeMX/固件包组合中,从 System Core → SYS → Timebase Source 选择空闲定时器,并检查生成的时基源文件、中断、优先级和资源冲突。菜单和模板会随版本变化,以生成文件为准。

常见链路是:

HAL_Init() / RCC 时钟重新配置
↓ 调用被应用强定义覆盖的 HAL_InitTick()
配置并启动 HAL 定时器
↓ 更新中断
TIMx_IRQHandler → HAL_TIM_IRQHandler(&timebase_handle)
↓
HAL_TIM_PeriodElapsedCallback → HAL_IncTick()

HAL 的默认 HAL_InitTick() 是弱定义。链接了强定义的定时器实现后,HAL_Init() 会直接调用它,并非一定先配置 SysTick,再由应用手动调用一次 MX_TIM6_Init()。时钟改变时,该实现还必须按新的 APB/定时器时钟重算周期,避免旧分频继续产生错误的毫秒计数。

F103x8/xB 通常只有 TIM1–TIM4,其中 TIM2 是通用定时器;F103ZET6 等高密度型号才具有 TIM6/TIM7 等附加外设。型号、资源占用与是否支持相应 Cube 模板,决定了实际选择。

若使用 HAL 分派模板,回调还要区分定时器实例。例如下段仅用于已经完整配置的 TIM6 HAL 时基,不可同时再装入本文下一节的 TIM2 直接中断实现:

#include "stm32f1xx_hal.h"
/* 此例要求 STM32F103xE 等实际包含 TIM6 的器件。 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *handle)
{
if (handle->Instance == TIM6) {
HAL_IncTick();
}
/* 其他 TIM 的应用回调在统一分派处继续处理。 */
}

只复制回调不会启动时钟、中断或定时器。一个应用若已有多个 TIM 回调,需在同一个强定义中按实例分派,不能再复制多个同名函数。

F103xB 的最小 TIM2 时基模块

跳转到“F103xB 的最小 TIM2 时基模块”

下面给出另一种完整的直接寄存器分派方式,独占 TIM2,限定 HAL Tick 为默认 1 kHz。它不需要 TIM2 的 HAL 句柄,不再调用 HAL_TIM_IRQHandler(),也不与上面的 TIM6 回调方案叠加。时钟树、全局优先级分组、启动文件和 HAL 库由工程提供。

#include "stm32f1xx_hal.h"
HAL_StatusTypeDef HAL_InitTick(uint32_t priority)
{
const uint32_t pclk1 = HAL_RCC_GetPCLK1Freq();
const uint32_t timer_hz = ((RCC->CFGR & RCC_CFGR_PPRE1) <
RCC_CFGR_PPRE1_DIV2)
? pclk1 : 2U * pclk1;
if (priority >= (1UL << __NVIC_PRIO_BITS) ||
uwTickFreq != HAL_TICK_FREQ_1KHZ ||
timer_hz < 1000000U || timer_hz % 1000000U != 0U ||
timer_hz / 1000000U > 65536U) {
return HAL_ERROR;
}
NVIC_DisableIRQ(TIM2_IRQn);
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_TIM2_FORCE_RESET();
__HAL_RCC_TIM2_RELEASE_RESET();
TIM2->CR1 = TIM_CR1_URS;
TIM2->PSC = timer_hz / 1000000U - 1U;
TIM2->ARR = 1000U - 1U;
TIM2->EGR = TIM_EGR_UG;
TIM2->SR = 0U; /* 初始化期间,清除旧状态。 */
NVIC_ClearPendingIRQ(TIM2_IRQn);
HAL_NVIC_SetPriority(TIM2_IRQn, priority, 0);
uwTickPrio = priority;
TIM2->DIER = TIM_DIER_UIE;
TIM2->CR1 |= TIM_CR1_CEN;
NVIC_EnableIRQ(TIM2_IRQn);
return HAL_OK;
}
void TIM2_IRQHandler(void)
{
if ((TIM2->SR & TIM_SR_UIF) != 0U &&
(TIM2->DIER & TIM_DIER_UIE) != 0U) {
TIM2->SR = (uint32_t)~TIM_SR_UIF;
HAL_IncTick();
}
__DSB();
}
void HAL_SuspendTick(void)
{
TIM2->DIER &= ~TIM_DIER_UIE;
}
void HAL_ResumeTick(void)
{
/* 丢弃暂停期间留下的更新状态,不补算暂停时间。 */
TIM2->SR = (uint32_t)~TIM_SR_UIF;
NVIC_ClearPendingIRQ(TIM2_IRQn);
TIM2->DIER |= TIM_DIER_UIE;
}

STM32F1 的 APB1 不分频时,定时器时钟等于 PCLK1;分频时通常为 2 × PCLK1。72 MHz 系统、APB1 二分频时,TIM2 仍为 72 MHz,所以 PSC = 71 产生 1 MHz 计数,ARR = 999 产生 1 ms 更新。此规则不能原样替代 STM32H7 的 TIMPRE 和内核时钟选择。

此模块在初始化时重置独占的 TIM2,不适合与输入捕获/PWM 共用。它只接受默认 1 kHz HAL Tick,调用方须检查 HAL_Init()、时钟配置和 HAL_SetTickFreq() 的返回值。暂停、长中断屏蔽和低功耗造成的时间不会自动补算;单个 UIF 标志也不会保存多次溢出的计数。因此这个示例解决时基来源与配置归属,不能代替低功耗时间维护或长临界区审计。

共用 SysTick 时必须处理什么

跳转到“共用 SysTick 时必须处理什么”

某些 CMSIS-RTOS v1 生成工程会在唯一的 SysTick_Handler() 中同时执行 HAL_IncTick() 和 osSystickHandler();这说明共用在特定配置下可行。CMSIS-RTOS2、原生端口与不同固件包的入口不同,不能把该函数名当成万能模板。

共用前至少明确:

  1. 频率:HAL 毫秒与内核 Tick 是否同速,若不同如何换算和累计。
  2. 生命周期:调度器尚未启动时,HAL 可能已需要超时;内核 Tick 处理不能过早进入未准备好的状态。
  3. 暂停和 tickless:暂停 HAL Tick 是否也意外停止调度器;内核抑制 SysTick 后如何维护 HAL 的经过时间。
  4. 入口归属:只保留一个向量实现,不重复递增内核或 HAL 计数;端口中直接映射宏与包装函数不可产生冲突。

对普通 Cube 工程,独立 HAL 定时器更容易分开管理这些责任,但仍需正确的初始化、频率和中断延迟预算。

可在任务中运行的诊断快照

跳转到“可在任务中运行的诊断快照”
#include <stdint.h>
#include "stm32f1xx_hal.h"
#include "FreeRTOS.h"
#include "task.h"
struct timebase_snapshot {
uint32_t hal_ms;
TickType_t rtos_ticks;
uint32_t systick_priority;
uint32_t tim2_priority;
};
struct timebase_snapshot read_timebases_from_task(void)
{
struct timebase_snapshot result;
result.hal_ms = HAL_GetTick();
result.rtos_ticks = xTaskGetTickCount();
result.systick_priority = NVIC_GetPriority(SysTick_IRQn);
result.tim2_priority = NVIC_GetPriority(TIM2_IRQn);
return result;
}

这不是同时采样,适合比较一段时间后的增量。打印时在任务中使用与字段宽度匹配的格式,或显式转换到对应类型;不要在 Tick ISR 内反复打印。分别检查 HAL 和内核计数是否推进,再看 PSC、ARR、CEN、UIE/UIF、NVIC 使能、向量归属及优先级,能区分“定时器不走”“中断没到”“回调没有递增”和“单位比较错了”。