HAL 和 FreeRTOS 都需要时间,但它们的计数和用途不同。HAL 用毫秒时基支持超时和 HAL_Delay();FreeRTOS 用内核 Tick 管理任务延时、超时及调度。CubeMX 常把 HAL 时基移到一个定时器,让默认 FreeRTOS 端口使用 SysTick,这是一种方便集成的方案。
共用 SysTick 也可以实现,前提是只有一个正确的入口,并处理好频率、调度器启动前后和低功耗补偿。不能把“必须把 HAL 从 SysTick 移开”当作所有版本、所有移植的硬件规律。
两种 Tick 的单位
跳转到“两种 Tick 的单位”| 项目 | HAL | 原生 FreeRTOS |
|---|---|---|
| 读取接口 | HAL_GetTick() | xTaskGetTickCount() |
| 单位 | 毫秒;具体更新步长受 HAL Tick 配置影响 | TickType_t,每 Tick 为 1 / configTICK_RATE_HZ 秒 |
| 常见更新入口 | HAL_IncTick() | 本文端口的 xPortSysTickHandler() |
| 普通延时 | HAL_Delay(ms) 忙等 HAL Tick | vTaskDelay(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 位优先级、全部作为抢占位:
| 含义 | 逻辑数值 | 寄存器表示 |
|---|---|---|
| 最低中断优先级 | 15 | 0xF0 |
| 一个常用内核 API 阈值示例 | 5 | 0x50 |
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 的独立 HAL 时基
跳转到“CubeMX 的独立 HAL 时基”在支持的 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、原生端口与不同固件包的入口不同,不能把该函数名当成万能模板。
共用前至少明确:
- 频率:HAL 毫秒与内核 Tick 是否同速,若不同如何换算和累计。
- 生命周期:调度器尚未启动时,HAL 可能已需要超时;内核 Tick 处理不能过早进入未准备好的状态。
- 暂停和 tickless:暂停 HAL Tick 是否也意外停止调度器;内核抑制 SysTick 后如何维护 HAL 的经过时间。
- 入口归属:只保留一个向量实现,不重复递增内核或 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 使能、向量归属及优先级,能区分“定时器不走”“中断没到”“回调没有递增”和“单位比较错了”。