4. STM32 定时器与中断:SysTick定时器、通用定时器配置、外部中断与优先级管理

定时器和中断,这俩东西在嵌入式里有多重要?我这么说吧,没有它们,你的单片机基本就是个“睁眼瞎”,只能傻乎乎地轮询,啥也干不了。这一章,咱们就把STM32的定时器和中断系统彻底捋一遍。我会结合自己做项目时踩过的坑,把SysTick、通用定时器、外部中断和优先级管理这些核心知识点,用最实在的方式讲给你听。

本章核心脉络:从“心跳”SysTick开始,到灵活可配的通用定时器,再到响应外部事件的NVIC中断系统。这三者构成了STM32时间管理与事件响应的基石。

STM32 定时器与中断知识体系 定时器与中断 SysTick 定时器 系统心跳 / HAL_Delay 通用定时器 PWM / 输入捕获 / 计时 外部中断 (EXTI) GPIO触发 / 按键响应 1ms 中断 时基基准 PWM 输出 输入捕获 上升沿/下降沿触发 NVIC 中断控制器统一管理所有中断的优先级与嵌套

4.1 SysTick定时器:系统的“心跳”

SysTick,说白了就是Cortex-M内核自带的一个24位递减计数器。它就像一个节拍器,给整个系统提供统一的时间基准。HAL库里的HAL_Delay(),还有各种超时判断,底层都是靠它。

我个人习惯,在项目一开始就把SysTick配置好。它的配置其实很简单,就是设置一个重装载值,然后让它每1ms中断一次。

// SysTick 配置示例(HAL库通常自动完成,但你要理解)
// 假设系统时钟 HCLK = 72MHz
// 重装载值 = 72000000 / 1000 = 72000
// 这样每计数 72000 次,就是 1ms

SysTick_Config(72000);  // 配置并启动SysTick,使能中断

我的经验:SysTick的中断优先级,我通常设置为最低(15)。因为它是系统时基,不应该打断其他更紧急的中断。如果你把SysTick优先级设高了,万一它卡住了,整个系统的延时和超时都会乱套。

4.2 通用定时器:不只是计时

通用定时器(TIMx)比SysTick灵活得多。它有多种工作模式:定时、PWM输出、输入捕获、编码器接口等等。咱们做LoRa通信和GPS驱动,主要用它的PWM和计时功能。

配置一个通用定时器,核心就是三步:时钟源、预分频器、自动重装载寄存器。

参数 说明 示例值
时钟源 通常选择内部时钟(CK_INT) 72 MHz
预分频器 (PSC) 将时钟分频,降低计数频率 71(分频后为1 MHz)
自动重装载 (ARR) 计数值达到此值后溢出/更新 999(产生1 kHz的更新事件)

举个例子,我想让TIM2产生一个1kHz的定时中断,配置如下:

// 定时器配置代码片段
TIM_HandleTypeDef htim2;

htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;         // 72MHz / (71+1) = 1MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999;           // 1MHz / (999+1) = 1kHz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start_IT(&htim2);     // 启动并使能中断

注意:预分频器和自动重装载的值都是“实际值-1”。比如你想分频72倍,PSC就写71。这个坑我刚开始学的时候踩过好几次,计数器从0开始计,所以最大值是ARR,而不是ARR+1。

4.3 外部中断 (EXTI):响应外部事件

外部中断,就是让单片机能够“感知”到外部引脚电平的变化。比如按键按下、GPS模块的PPS脉冲、LoRa模块的DIO中断引脚,都需要用到EXTI。

配置EXTI的步骤,我总结为四步:

  1. 配置GPIO:设置为输入模式,并选择上下拉。
  2. 连接EXTI线:将GPIO引脚连接到对应的EXTI线(比如PA0连接到EXTI0)。
  3. 配置触发方式:上升沿、下降沿,或者双边沿触发。
  4. 使能中断并编写服务函数:在NVIC中使能对应的中断通道,然后编写中断处理函数。
// 外部中断配置示例(以PA0为例,下降沿触发)
GPIO_InitTypeDef GPIO_InitStruct = {0};

__HAL_RCC_GPIOA_CLK_ENABLE();      // 使能GPIOA时钟
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发中断
GPIO_InitStruct.Pull = GPIO_PULLUP;          // 内部上拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// 使能中断线
HAL_NVIC_SetPriority(EXTI0_IRQn, 1, 0);      // 设置抢占优先级1,子优先级0
HAL_NVIC_EnableIRQ(EXTI0_IRQn);              // 使能中断

中断服务函数怎么写?HAL库已经帮我们封装好了。你只需要在对应的回调函数里添加你的逻辑:

// 中断服务函数(在stm32f1xx_it.c中)
void EXTI0_IRQHandler(void)
{
    HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
}

// 回调函数(在你自己的代码中)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if(GPIO_Pin == GPIO_PIN_0)
    {
        // 按键被按下,执行你的操作
        // 比如:切换LED状态,或者唤醒LoRa模块
    }
}

避坑指南:我曾经在一个项目里,把外部中断的回调函数写得特别长,里面还调用了HAL_Delay()。结果一按按键,系统就卡死。后来才意识到,中断服务函数里要“快进快出”,千万别做延时、打印这种耗时操作。正确的做法是设置一个标志位,然后在主循环里处理。

4.4 中断优先级管理:NVIC的规则

STM32的中断优先级管理,用的是NVIC(嵌套向量中断控制器)。它支持最多16级抢占优先级和16级子优先级(具体看芯片型号)。

优先级分组,说白了就是决定用多少位来表示抢占优先级,多少位来表示子优先级。HAL库默认使用4位抢占优先级,0位子优先级(即分组4)。

优先级分组 抢占优先级位数 子优先级位数 说明
NVIC_PRIORITYGROUP_0 0 4 只有子优先级,无法嵌套
NVIC_PRIORITYGROUP_1 1 3 2级抢占,8级子优先级
NVIC_PRIORITYGROUP_2 2 2 4级抢占,4级子优先级
NVIC_PRIORITYGROUP_3 3 1 8级抢占,2级子优先级
NVIC_PRIORITYGROUP_4 4 0 16级抢占,无子优先级(HAL默认)

优先级数值越小,优先级越高。所以0是最高优先级,15是最低。

重要:整个系统中,优先级分组只能设置一次!一般在系统初始化时调用HAL_NVIC_SetPriorityGrouping()设置。中途更改会导致优先级混乱,我见过有人因为这个bug查了一整天。

我的优先级分配原则很简单:

  • 时间关键的中断(如通信超时、电机控制)→ 高优先级(0-3)
  • 数据接收中断(如UART、SPI)→ 中等优先级(4-7)
  • 外部按键中断 → 较低优先级(8-11)
  • SysTick和通用定时器 → 最低优先级(12-15)

你想想看,如果按键中断的优先级比通信超时还高,那在传输关键数据时,按一下键就可能把数据包搞丢。这在实际产品中是不能接受的。

嗯,这一章的内容就这些。定时器和中断是STM32开发的“基本功”,练好了,后面的LoRa和GPS驱动才能写得顺手。记住,多动手配置,多想想为什么这么配,慢慢就有感觉了。