## 一、定时器中断原理概述 STM32定时器本质是一个16位/32位的向上/向下计数器,其时钟源来自内部时钟(如APB1 Timer Clock)或外部信号。当计数器计数达到设定的自动重装载值(ARR)时,会产生更新事件(Update Event)。若使能了更新中断,CPU将暂停当前任务,跳转到中断服务函数执行用户逻辑,待处理完毕后再返回。 HAL库将这一过程封装成了三个关键环节: - 定时器句柄初始化(`HAL_TIM_Base_Init`) - 时基单元配置(`TIM_MasterConfigTypeDef`) - 中断使能与回调函数(`HAL_TIM_IRQHandler` / `HAL_TIM_PeriodElapsedCallback`) 理解这三个环节,就能轻松驾驭任何STM32型号的定时器中断。 ## 二、HAL库定时器中断配置步骤 ### 1. 使能定时器时钟 使用CubeMX或直接操作RCC寄存器,使能对应定时器的时钟。例如TIM2挂在APB1总线上: ```c __HAL_RCC_TIM2_CLK_ENABLE(); ``` 若使用CubeMX,勾选TIM2后系统自动生成此句,无需手动添加。 ### 2. 初始化时基参数 时基单元决定定时器计数频率和溢出周期。常用参数如下: - `Prescaler`(预分频):将定时器时钟分频,例如APB1 Timer Clock为84MHz,设Prescaler为8399则计数频率为10kHz。 - `Period`(自动重装载值):计数达到该值产生更新事件,例如设Period为4999则定时周期为500ms。 - `CounterMode`:通常用向上计数模式(`TIM_COUNTERMODE_UP`)。 - `ClockDivision`:一般设为`TIM_CLOCKDIVISION_DIV1`。 使用HAL库编写: ```c TIM_HandleTypeDef htim2; void MX_TIM2_Init(void) { htim2.Instance = TIM2; htim2.Init.Prescaler = 8399; // 84MHz / 8400 = 10kHz htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 4999; // 10kHz / 5000 = 2Hz,即500ms中断一次 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(&htim2) != HAL_OK) { Error_Handler(); } } ``` ### 3. 使能更新中断并配置NVIC 初始化完成后,需要使能定时器更新中断,并在NVIC中设置中断优先级。注意优先级分组需在主函数中提前配置(`HAL_NVIC_SetPriorityGrouping`)。 ```c void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) { if (htim_base->Instance == TIM2) { __HAL_RCC_TIM2_CLK_ENABLE(); // 若已在外部使能可省略 // 设置中断优先级,抢占优先级1,子优先级0 HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); // 使能TIM2全局中断 HAL_NVIC_EnableIRQ(TIM2_IRQn); } } ``` 在CubeMX生成的代码中,`HAL_TIM_Base_MspInit`会自动被调用,无需手动调用。但需确保其内部已包含NVIC配置。 ### 4. 编写中断服务函数(IRQHandler) 每个定时器中断源都有固定的中断函数名,如TIM2对应`TIM2_IRQHandler`。在中断服务函数中必须调用HAL库的`HAL_TIM_IRQHandler`,它会自动判断中断类型并调用对应的回调函数。 ```c void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(&htim2); } ``` ### 5. 重写回调函数 HAL库使用弱定义的`HAL_TIM_PeriodElapsedCallback`,用户需要在自己的代码中重新实现该函数。注意此函数对所有使用HAL_TIM_IRQHandler的定时器共用,因此需要通过句柄判断具体是哪个定时器。 ```c void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { // 用户代码:翻转LED或执行周期任务 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); } } ``` ### 6. 启动定时器中断 在主函数初始化完成后,调用以下函数启动定时器: ```c HAL_TIM_Base_Start_IT(&htim2); ``` `_IT`后缀表示中断模式启动,区别于阻塞模式`HAL_TIM_Base_Start`。此后定时器开始计数,每次溢出进入中断。 ## 三、完整代码示例(TIM2 产生500ms周期中断) ```c /* main.c 精简示例 */ TIM_HandleTypeDef htim2; void Error_Handler(void) { while(1) {} } void MX_TIM2_Init(void) { htim2.Instance = TIM2; htim2.Init.Prescaler = 8399; htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 4999; htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(&htim2) != HAL_OK) { Error_Handler(); } } void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base) { if (htim_base->Instance == TIM2) { __HAL_RCC_TIM2_CLK_ENABLE(); HAL_NVIC_SetPriority(TIM2_IRQn, 1, 0); HAL_NVIC_EnableIRQ(TIM2_IRQn); } } void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(&htim2); } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 翻转板载LED } } int main(void) { HAL_Init(); // 初始化系统时钟(如72MHz或84MHz) SystemClock_Config(); // 初始化LED引脚(略) MX_GPIO_Init(); // 初始化定时器 MX_TIM2_Init(); // 启动定时器中断 HAL_TIM_Base_Start_IT(&htim2); while(1) { // 主循环可以执行其他低优先级任务 } } ``` ## 四、注意事项与常见问题 1. **时钟源频率**:不同STM32系列APB1定时器时钟可能不同,务必根据实际主频计算Prescaler和Period。例如F103默认72MHz,F407若APB1分频为2则定时器时钟仍为84MHz。 2. **中断服务函数命名**:一定不要随意修改`TIMx_IRQHandler`函数名,否则中断无法正确链接。 3. **回调函数中避免长时间操作**:中断回调函数应尽量精简,不要使用`HAL_Delay`或阻塞式串口打印,否则会拖慢系统响应。需要大工作量时可置标志位,在主循环中处理。 4. **AutoReloadPreload**:建议使能预装载,这样修改Period时不会立即生效,而是等本次溢出后才更新,避免周期跳动。 5. **MPU与Cache(Cortex-M7)**:若使用F7/H7系列,需确保定时器相关寄存器访问未受MPU限制,并注意D-Cache对DMA等的作用,但普通中断不受影响。 6. **多定时器共用回调**:`HAL_TIM_PeriodElapsedCallback`是所有定时器更新中断的统一回调,务必通过`htim->Instance`区分,否则多个定时器会相互干扰。 7. **NVIC优先级设置**:若系统使用FreeRTOS,建议将定时器中断优先级设置为低于内核最高优级(如5~15),避免破坏临界区。 ## 五、总结 使用HAL库配置STM32定时器中断的流程可以浓缩为:时钟使能→时基初始化→NVIC配置→IRQHandler内调用HAL_TIM_IRQHandler→重写PeriodElapsedCallback→启动`_IT`模式。掌握这套流程,就能灵活生成精确的毫秒级、微秒级定时中断,为按键扫描、传感器采样、通信超时处理等场景提供可靠的时基基础。实践时多注意时钟树和中断优先级这两个细节,就能避免大部分低级错误。