4. 定时器中断实现精确延时

任务说明

在MCU裸机开发中,HAL_Delay()依赖SysTick且为阻塞式,无法在延时期间响应其他中断或执行任务。本案例利用STM32 HAL库的定时器中断,实现一个非阻塞的毫秒级延时函数:调用延时后立即返回,CPU可继续执行其他代码,延时到达后通过回调通知。

硬件连接与资源分配

  • 使用TIM3,时钟源为内部APB1定时器时钟(假设84MHz,若实际不同需调整预分频值)
  • 通道无需映射到引脚,仅使用更新中断
  • 延时范围:1ms ~ 65535ms(受16位自动重载寄存器限制,可扩展)

实现代码


// timer_delay.h
#ifndef TIMER_DELAY_H
#define TIMER_DELAY_H

#include "main.h"

typedef void (*DelayCallback)(void);

void TimerDelay_Init(TIM_HandleTypeDef *htim);
void TimerDelay_Start(uint16_t ms, DelayCallback cb);
void TimerDelay_Stop(void);
uint8_t TimerDelay_IsRunning(void);

#endif

// timer_delay.c
#include "timer_delay.h"

static TIM_HandleTypeDef *delay_tim;
static DelayCallback delay_cb;
static volatile uint8_t delay_running = 0;

void TimerDelay_Init(TIM_HandleTypeDef *htim) {
    delay_tim = htim;
    delay_cb = NULL;
    delay_running = 0;
    HAL_TIM_Base_Start_IT(delay_tim);  // 启动定时器中断
}

void TimerDelay_Start(uint16_t ms, DelayCallback cb) {
    if (delay_running) return;  // 已有延时进行中,拒绝新请求
    
    // 计算重载值:定时器时钟84MHz,预分频84-1 → 1MHz计数频率,即1us/tick
    // 需要ms级:重载值 = ms * 1000 - 1
    __HAL_TIM_SET_PRESCALER(delay_tim, 84 - 1);
    __HAL_TIM_SET_AUTORELOAD(delay_tim, (uint32_t)ms * 1000 - 1);
    __HAL_TIM_SET_COUNTER(delay_tim, 0);
    
    delay_cb = cb;
    delay_running = 1;
    HAL_TIM_Base_Start_IT(delay_tim);
}

void TimerDelay_Stop(void) {
    if (delay_running) {
        HAL_TIM_Base_Stop_IT(delay_tim);
        delay_running = 0;
        delay_cb = NULL;
    }
}

uint8_t TimerDelay_IsRunning(void) {
    return delay_running;
}

// 在stm32f1xx_it.c或stm32f4xx_it.c的中断回调中调用
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim == delay_tim && delay_running) {
        delay_running = 0;
        HAL_TIM_Base_Stop_IT(delay_tim);
        if (delay_cb) {
            DelayCallback cb = delay_cb;
            delay_cb = NULL;
            cb();  // 执行延时完成回调
        }
    }
}

// 使用示例(main.c片段)
#include "timer_delay.h"

TIM_HandleTypeDef htim3;
volatile uint8_t led_state = 0;

void DelayDone_Callback(void) {
    // 延时完成,翻转LED
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
    led_state = !led_state;
}

int main(void) {
    HAL_Init();
    SystemClock_Config();  // 配置系统时钟84MHz
    MX_GPIO_Init();
    MX_TIM3_Init();       // 基础初始化,不启动中断
    
    TimerDelay_Init(&htim3);
    
    while (1) {
        // 非阻塞延时500ms,期间可执行其他任务
        TimerDelay_Start(500, DelayDone_Callback);
        
        // 延时期间CPU可处理其他事务
        while (TimerDelay_IsRunning()) {
            // 执行其他低优先级任务,如按键扫描、通信处理等
            ProcessOtherTasks();
        }
        // 延时完成后继续主循环
    }
}

关键配置说明

  • 预分频器设为83(即84-1),将84MHz时钟分频为1MHz,计数周期1μs
  • 自动重载值 = 延时毫秒数 × 1000 - 1,实现1ms精度
  • 若系统时钟不是84MHz,需按实际时钟调整预分频值,保证计数频率为1MHz
  • 中断回调函数HAL_TIM_PeriodElapsedCallback需在工程的中断管理文件中实现

验证方法

  1. 编译烧录后,观察LED以约500ms周期翻转(亮500ms灭500ms)
  2. 在ProcessOtherTasks()中设置断点或翻转另一个GPIO,确认延时期间主循环仍在执行——若LED翻转的同时另一个测试引脚也在翻转,说明非阻塞生效
  3. 用逻辑分析仪或示波器测量LED引脚波形,高/低电平持续时间应为500ms±1ms
  4. 连续调用多次TimerDelay_Start,确认第二次调用在第一次完成后才生效(delay_running保护)

判定标准:LED周期翻转且主循环在延时期间不被阻塞,即判定实现成功。

定时器中断非阻塞延时调用时序 主循环 TIM3中断 回调函数 t0 t0+500ms t0+1000ms 调用Start(500) 执行其他任务(非阻塞) 调用Start(500) 启动计数 计数中(不占CPU) 启动计数 延时完成回调 延时完成回调 CPU在延时期间可执行其他任务 主循环操作 定时器中断 回调执行

代码集成说明

将timer_delay.c/h加入工程,在CubeMX中完成TIM3的基础时钟配置(不使能中断启动),然后在main.c中调用TimerDelay_Init()。中断回调函数HAL_TIM_PeriodElapsedCallback需要放在工程已有的中断回调区域(如stm32f1xx_it.c),确保定时器更新中断能正确路由到该函数。若使用其他STM32系列,仅需调整预分频值以匹配实际定时器时钟频率。