BLOG8
跳转至

定时器中断实现精确延时

学习目标

完成本教程后,你将能够:

  • 理解定时器中断的工作原理和应用场景
  • 配置STM32定时器产生周期性中断
  • 编写定时器中断服务函数实现精确延时
  • 使用定时器中断实现周期任务调度
  • 实现软件定时器管理多个定时任务
  • 调试和优化定时器中断应用

前置要求

在开始本教程之前,你需要:

知识要求: - 理解中断系统的基本概念(参考中断系统基础概念) - 了解外部中断的配置方法(参考外部中断配置与使用) - 掌握C语言基础和指针概念 - 了解定时器的基本工作原理

技能要求: - 能够使用STM32CubeIDE或Keil MDK开发环境 - 会使用HAL库进行基本的外设配置 - 能够使用调试器查看变量和寄存器

硬件要求: - STM32开发板(本教程使用STM32F407) - LED指示灯(用于可视化定时效果) - USB转串口模块(用于调试输出)

准备工作

硬件准备

名称 数量 说明 参考型号
STM32开发板 1 STM32F4系列 STM32F407VGT6
LED灯 3 红、绿、蓝各一个 3mm或5mm
电阻 3 限流电阻 220Ω-1kΩ
面包板 1 用于搭建电路 -
杜邦线 若干 连接线 -
USB转串口 1 调试输出 CH340、CP2102

软件准备

  • 开发环境:STM32CubeIDE v1.10+ 或 Keil MDK v5.30+
  • 固件库:STM32CubeF4 HAL库
  • 调试工具:ST-Link驱动、串口调试助手
  • 辅助工具:逻辑分析仪(可选,用于精确测量)

环境配置

  1. 安装STM32CubeIDE开发环境
  2. 安装ST-Link驱动程序
  3. 配置串口调试助手(波特率115200)
  4. 测试开发板连接和下载功能

电路连接

连接图

STM32F407开发板
┌─────────────────┐
│                 │
│  PD12 ────┬─────┼──── LED1 (绿色) ──[220Ω]── GND
│           │     │
│  PD13 ────┼─────┼──── LED2 (橙色) ──[220Ω]── GND
│           │     │
│  PD14 ────┼─────┼──── LED3 (红色)  ──[220Ω]── GND
│           │     │
│  PA9  ────┼─────┼──── USART1_TX (串口输出)
│  PA10 ────┼─────┼──── USART1_RX (串口输入)
│           │     │
│  GND  ────┴─────┼──── 公共地
│                 │
└─────────────────┘

连接说明

开发板引脚 连接到 说明
PD12 LED1正极 通过220Ω电阻连接到GND
PD13 LED2正极 通过220Ω电阻连接到GND
PD14 LED3正极 通过220Ω电阻连接到GND
PA9 USB转串口RX 串口调试输出
PA10 USB转串口TX 串口调试输入
GND 公共地 所有地线连接在一起

注意事项: - LED的长脚为正极,短脚为负极 - 确保电阻连接正确,防止LED烧毁 - 串口连接时注意TX接RX,RX接TX - 连接电路前请断开电源

背景知识

为什么需要定时器中断?

在嵌入式系统中,精确的时间控制至关重要。使用HAL_Delay()等阻塞延时函数存在以下问题:

阻塞延时的缺点: - CPU在延时期间无法执行其他任务 - 无法实现多个并发的定时任务 - 时间精度受系统负载影响 - 不适合实时性要求高的应用

定时器中断的优势: - CPU可以在等待期间执行其他任务 - 支持多个独立的定时任务 - 时间精度高,不受主程序影响 - 适合实时系统和周期性任务

定时器中断工作原理

定时器计数器从0开始递增
    ↓
计数器达到预设值(ARR)
    ↓
触发更新中断
    ↓
计数器清零,重新开始计数
    ↓
执行中断服务函数
    ↓
返回主程序继续执行

关键参数: - 预分频器(Prescaler):降低计数频率 - 自动重装载值(ARR):计数目标值 - 计数模式:向上计数、向下计数、中心对齐

中断周期计算公式:

中断周期 = (Prescaler + 1) × (ARR + 1) / 定时器时钟频率

例如:
定时器时钟 = 84MHz
Prescaler = 8399
ARR = 9999
中断周期 = (8399 + 1) × (9999 + 1) / 84000000 = 1秒

步骤1:创建项目并配置定时器

1.1 创建新项目

  1. 打开STM32CubeIDE
  2. 选择 File → New → STM32 Project
  3. 选择目标芯片:STM32F407VGT6
  4. 输入项目名称:timer_interrupt_tutorial
  5. 点击 Finish

1.2 配置系统时钟

在CubeMX配置界面中:

  1. 点击 Clock Configuration 标签
  2. 设置HCLK(系统时钟)为168MHz
  3. 确认APB1时钟为42MHz,APB2时钟为84MHz
  4. TIM2-TIM7使用APB1时钟(84MHz,经过倍频)

时钟树说明:

HSE (8MHz) → PLL → SYSCLK (168MHz)
    ↓
APB1 Prescaler (/4) → APB1 (42MHz) → TIM2-7 Clock (84MHz, ×2)
APB2 Prescaler (/2) → APB2 (84MHz) → TIM1,8-11 Clock (168MHz, ×2)

1.3 配置GPIO引脚

配置LED控制引脚:

  1. 在Pinout视图中找到PD12、PD13、PD14
  2. 右键选择 GPIO_Output
  3. 在GPIO配置中设置:
  4. GPIO output level: Low(初始状态为低电平)
  5. GPIO mode: Output Push Pull
  6. GPIO Pull-up/Pull-down: No pull-up and no pull-down
  7. Maximum output speed: Low

  8. 为引脚设置用户标签:

  9. PD12 → LED_GREEN
  10. PD13 → LED_ORANGE
  11. PD14 → LED_RED

1.4 配置定时器TIM2

配置TIM2产生1ms周期的中断:

  1. 在Pinout视图左侧找到 Timers → TIM2
  2. 勾选 Activated
  3. 在Configuration标签中配置TIM2:

Parameter Settings: - Prescaler (PSC): 8399 - 计算:84MHz / (8399 + 1) = 10kHz - Counter Period (ARR): 9 - 计算:10kHz / (9 + 1) = 1kHz = 1ms周期 - Counter Mode: Up(向上计数) - auto-reload preload: Enable

NVIC Settings: - 勾选 TIM2 global interrupt - Priority: 2(中等优先级)

计算说明:

定时器时钟频率 = 84MHz
预分频后频率 = 84MHz / (8399 + 1) = 10kHz
中断周期 = 1 / [10kHz / (9 + 1)] = 1ms

1.5 配置串口(可选)

配置USART1用于调试输出:

  1. 在Pinout视图中找到 USART1
  2. Mode选择 Asynchronous
  3. Configuration配置:
  4. Baud Rate: 115200
  5. Word Length: 8 Bits
  6. Parity: None
  7. Stop Bits: 1

  8. NVIC Settings:

  9. 不勾选中断(使用轮询方式)

1.6 生成代码

  1. 点击 Project → Generate Code
  2. 等待代码生成完成
  3. 打开生成的项目

预期结果: - 项目结构创建成功 - TIM2初始化代码已生成 - GPIO和USART初始化代码已生成

步骤2:编写基础定时器中断代码

2.1 启动定时器中断

打开 Core/Src/main.c 文件,在主函数中启动定时器:

int main(void)
{
    /* 系统初始化 */
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM2_Init();
    MX_USART1_UART_Init();

    /* USER CODE BEGIN 2 */

    // 启动定时器中断
    HAL_TIM_Base_Start_IT(&htim2);

    printf("Timer Interrupt Tutorial Started\r\n");
    printf("TIM2 configured for 1ms interrupt\r\n");

    /* USER CODE END 2 */

    while (1)
    {
        /* USER CODE BEGIN 3 */

        // 主循环可以执行其他任务
        HAL_Delay(1000);
        printf("Main loop running...\r\n");

        /* USER CODE END 3 */
    }
}

代码说明: - HAL_TIM_Base_Start_IT(&htim2):启动定时器并使能中断 - 定时器开始计数,每1ms触发一次中断 - 主循环可以继续执行其他任务

2.2 实现中断服务函数

在 Core/Src/stm32f4xx_it.c 文件中,找到TIM2的中断服务函数:

/**
  * @brief This function handles TIM2 global interrupt.
  */
void TIM2_IRQHandler(void)
{
    /* USER CODE BEGIN TIM2_IRQn 0 */

    /* USER CODE END TIM2_IRQn 0 */
    HAL_TIM_IRQHandler(&htim2);
    /* USER CODE BEGIN TIM2_IRQn 1 */

    /* USER CODE END TIM2_IRQn 1 */
}

这个函数会自动调用HAL库的中断处理函数,我们需要实现回调函数。

2.3 实现定时器回调函数

在 main.c 文件中添加定时器回调函数:

/* USER CODE BEGIN 0 */

// 全局变量:毫秒计数器
volatile uint32_t millisecond_counter = 0;

/**
  * @brief  定时器周期回调函数
  * @param  htim: 定时器句柄
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    // 检查是否是TIM2触发的中断
    if (htim->Instance == TIM2)
    {
        // 每1ms执行一次
        millisecond_counter++;

        // 每500ms翻转一次LED(实现500ms闪烁)
        if (millisecond_counter % 500 == 0)
        {
            HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);  // 翻转绿色LED
        }
    }
}

/* USER CODE END 0 */

代码说明: - HAL_TIM_PeriodElapsedCallback():定时器更新中断的回调函数 - millisecond_counter:毫秒计数器,每1ms递增 - 使用volatile关键字确保编译器不优化掉这个变量 - 每500ms翻转一次LED,实现闪烁效果

2.4 添加printf重定向(可选)

为了使用printf输出调试信息,需要重定向到串口:

/* USER CODE BEGIN 0 */

// 重定向printf到USART1
int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
    return len;
}

/* USER CODE END 0 */

注意:需要在项目设置中添加 -u _printf_float 链接选项(如果需要打印浮点数)。

步骤3:实现精确延时函数

3.1 基于定时器中断的延时函数

使用定时器中断实现非阻塞的精确延时:

/* USER CODE BEGIN 0 */

/**
  * @brief  精确延时函数(非阻塞)
  * @param  ms: 延时时间(毫秒)
  * @retval None
  */
void Delay_ms(uint32_t ms)
{
    uint32_t start_time = millisecond_counter;

    // 等待指定时间
    while ((millisecond_counter - start_time) < ms)
    {
        // 可以在这里执行其他任务
        // 或者使用__WFI()进入低功耗模式
    }
}

/**
  * @brief  获取当前毫秒计数
  * @retval 当前毫秒数
  */
uint32_t Get_Milliseconds(void)
{
    return millisecond_counter;
}

/**
  * @brief  检查是否超时
  * @param  start_time: 开始时间
  * @param  timeout: 超时时间(毫秒)
  * @retval 1: 超时, 0: 未超时
  */
uint8_t Is_Timeout(uint32_t start_time, uint32_t timeout)
{
    return ((millisecond_counter - start_time) >= timeout);
}

/* USER CODE END 0 */

代码说明: - Delay_ms():非阻塞延时函数,可以在等待期间执行其他任务 - Get_Milliseconds():获取系统运行的毫秒数 - Is_Timeout():检查是否超时,用于超时检测

3.2 使用示例

在主循环中使用精确延时:

int main(void)
{
    /* 初始化代码... */

    HAL_TIM_Base_Start_IT(&htim2);

    uint32_t last_print_time = 0;
    uint32_t led_toggle_time = 0;

    while (1)
    {
        // 每1秒打印一次信息
        if (Is_Timeout(last_print_time, 1000))
        {
            last_print_time = Get_Milliseconds();
            printf("System running: %lu ms\r\n", Get_Milliseconds());
        }

        // 每200ms翻转一次LED
        if (Is_Timeout(led_toggle_time, 200))
        {
            led_toggle_time = Get_Milliseconds();
            HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);  // 橙色LED
        }

        // 可以执行其他任务
    }
}

步骤4:实现软件定时器

4.1 软件定时器结构设计

设计一个软件定时器系统,支持多个独立的定时任务:

/* USER CODE BEGIN 0 */

// 软件定时器结构体
typedef struct {
    uint8_t  enabled;        // 定时器使能标志
    uint8_t  auto_reload;    // 自动重装载标志
    uint32_t period;         // 定时周期(毫秒)
    uint32_t counter;        // 当前计数值
    void (*callback)(void);  // 回调函数指针
} SoftTimer_t;

// 定义最多支持的软件定时器数量
#define MAX_SOFT_TIMERS  8

// 软件定时器数组
SoftTimer_t soft_timers[MAX_SOFT_TIMERS];

/**
  * @brief  初始化软件定时器系统
  * @retval None
  */
void SoftTimer_Init(void)
{
    for (int i = 0; i < MAX_SOFT_TIMERS; i++)
    {
        soft_timers[i].enabled = 0;
        soft_timers[i].auto_reload = 0;
        soft_timers[i].period = 0;
        soft_timers[i].counter = 0;
        soft_timers[i].callback = NULL;
    }
}

/**
  * @brief  创建软件定时器
  * @param  period: 定时周期(毫秒)
  * @param  auto_reload: 是否自动重装载(1:是, 0:否)
  * @param  callback: 回调函数指针
  * @retval 定时器ID(0-7),失败返回-1
  */
int SoftTimer_Create(uint32_t period, uint8_t auto_reload, void (*callback)(void))
{
    // 查找空闲的定时器
    for (int i = 0; i < MAX_SOFT_TIMERS; i++)
    {
        if (soft_timers[i].enabled == 0)
        {
            soft_timers[i].period = period;
            soft_timers[i].auto_reload = auto_reload;
            soft_timers[i].counter = 0;
            soft_timers[i].callback = callback;
            soft_timers[i].enabled = 1;
            return i;
        }
    }
    return -1;  // 没有空闲定时器
}

/**
  * @brief  删除软件定时器
  * @param  timer_id: 定时器ID
  * @retval None
  */
void SoftTimer_Delete(int timer_id)
{
    if (timer_id >= 0 && timer_id < MAX_SOFT_TIMERS)
    {
        soft_timers[timer_id].enabled = 0;
    }
}

/* USER CODE END 0 */

4.2 软件定时器处理函数

在定时器中断回调中处理所有软件定时器:

/**
  * @brief  软件定时器处理函数(在定时器中断中调用)
  * @retval None
  */
void SoftTimer_Process(void)
{
    for (int i = 0; i < MAX_SOFT_TIMERS; i++)
    {
        if (soft_timers[i].enabled)
        {
            soft_timers[i].counter++;

            // 检查是否到达周期
            if (soft_timers[i].counter >= soft_timers[i].period)
            {
                // 执行回调函数
                if (soft_timers[i].callback != NULL)
                {
                    soft_timers[i].callback();
                }

                // 重装载或停止
                if (soft_timers[i].auto_reload)
                {
                    soft_timers[i].counter = 0;  // 重新开始计数
                }
                else
                {
                    soft_timers[i].enabled = 0;  // 停止定时器
                }
            }
        }
    }
}

/**
  * @brief  定时器周期回调函数(修改版)
  * @param  htim: 定时器句柄
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        millisecond_counter++;

        // 处理所有软件定时器
        SoftTimer_Process();
    }
}

代码说明: - SoftTimer_Process():遍历所有软件定时器,检查是否到期 - 到期后执行回调函数 - 根据auto_reload标志决定是否重新开始计数 - 在定时器中断中调用,确保精确的时间控制

4.3 软件定时器使用示例

创建多个定时任务:

/* USER CODE BEGIN 0 */

// 定时任务1:每100ms翻转绿色LED
void Task_LED_Green(void)
{
    HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}

// 定时任务2:每300ms翻转橙色LED
void Task_LED_Orange(void)
{
    HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
}

// 定时任务3:每1000ms打印信息
void Task_Print_Info(void)
{
    printf("System uptime: %lu ms\r\n", Get_Milliseconds());
}

// 定时任务4:5秒后执行一次(单次定时器)
void Task_OneShot(void)
{
    printf("One-shot timer triggered!\r\n");
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_14, GPIO_PIN_SET);  // 点亮红色LED
}

/* USER CODE END 0 */

int main(void)
{
    /* 初始化代码... */

    HAL_TIM_Base_Start_IT(&htim2);

    // 初始化软件定时器系统
    SoftTimer_Init();

    // 创建周期性定时器
    SoftTimer_Create(100, 1, Task_LED_Green);    // 100ms周期,自动重装载
    SoftTimer_Create(300, 1, Task_LED_Orange);   // 300ms周期,自动重装载
    SoftTimer_Create(1000, 1, Task_Print_Info);  // 1000ms周期,自动重装载

    // 创建单次定时器
    SoftTimer_Create(5000, 0, Task_OneShot);     // 5000ms后执行一次

    printf("Software Timer System Started\r\n");
    printf("Task1: LED Green toggle every 100ms\r\n");
    printf("Task2: LED Orange toggle every 300ms\r\n");
    printf("Task3: Print info every 1000ms\r\n");
    printf("Task4: One-shot timer after 5000ms\r\n");

    while (1)
    {
        // 主循环可以执行其他任务
        // 所有定时任务由软件定时器自动管理
    }
}

运行效果: - 绿色LED以100ms周期闪烁(5Hz) - 橙色LED以300ms周期闪烁(1.67Hz) - 每1秒打印一次系统运行时间 - 5秒后红色LED点亮(单次触发)

步骤5:编译、下载和测试

5.1 编译项目

  1. 点击工具栏的 🔨 Build 按钮
  2. 查看控制台输出
  3. 确认编译成功(0 errors, 0 warnings)

可能的错误: - 如果出现 undefined reference to _write,检查printf重定向代码 - 如果出现 TIM2_IRQHandler multiple definition,检查是否重复定义

5.2 下载程序

  1. 连接ST-Link调试器到开发板
  2. 点击 ▶️ Run 按钮
  3. 等待下载完成

预期结果: - 程序下载成功 - LED开始按照设定的周期闪烁

5.3 功能测试

测试1:基本定时功能

观察LED的闪烁情况:

  • 绿色LED以100ms周期闪烁
  • 橙色LED以300ms周期闪烁
  • 红色LED在5秒后点亮

测试2:串口输出

打开串口调试助手(115200波特率):

  • 看到启动信息
  • 每1秒输出一次系统运行时间
  • 5秒后看到单次定时器触发信息

预期输出:

Software Timer System Started
Task1: LED Green toggle every 100ms
Task2: LED Orange toggle every 300ms
Task3: Print info every 1000ms
Task4: One-shot timer after 5000ms
System uptime: 1000 ms
System uptime: 2000 ms
System uptime: 3000 ms
System uptime: 4000 ms
System uptime: 5000 ms
One-shot timer triggered!
System uptime: 6000 ms
...

测试3:时间精度测试

使用逻辑分析仪或示波器测量LED翻转周期:

  1. 将探头连接到PD12(绿色LED)
  2. 测量高电平和低电平的持续时间
  3. 验证周期是否为200ms(100ms高+100ms低)

精度要求: - 误差应小于±1% - 长时间运行不应有累积误差

步骤6:调试和优化

6.1 使用调试器验证

设置断点验证定时器中断:

  1. 在 HAL_TIM_PeriodElapsedCallback() 函数中设置断点
  2. 启动调试模式(🐛 Debug)
  3. 观察程序是否每1ms进入一次中断
  4. 查看 millisecond_counter 变量的值

调试技巧: - 使用 Expressions 窗口监视变量 - 使用 SWV ITM Data Console 查看printf输出 - 使用 Live Expressions 实时监视变量变化

6.2 性能优化

优化中断服务函数的执行时间:

/**
  * @brief  优化后的定时器回调函数
  * @param  htim: 定时器句柄
  * @retval None
  */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        // 快速递增计数器
        millisecond_counter++;

        // 只处理软件定时器,不在ISR中执行复杂操作
        SoftTimer_Process();

        // 不要在ISR中使用printf、HAL_Delay等耗时函数
    }
}

优化原则: - ISR应该尽可能短(建议<10μs) - 避免在ISR中使用阻塞函数 - 避免在ISR中使用浮点运算 - 使用标志位延迟处理复杂任务

6.3 中断负载分析

计算中断占用的CPU时间:

/* USER CODE BEGIN 0 */

volatile uint32_t isr_enter_time = 0;
volatile uint32_t isr_total_time = 0;
volatile uint32_t isr_max_time = 0;

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        // 记录进入时间(使用DWT计数器)
        uint32_t start = DWT->CYCCNT;

        millisecond_counter++;
        SoftTimer_Process();

        // 计算执行时间
        uint32_t elapsed = DWT->CYCCNT - start;
        isr_total_time += elapsed;

        if (elapsed > isr_max_time)
        {
            isr_max_time = elapsed;
        }
    }
}

/* USER CODE END 0 */

分析结果: - 平均ISR执行时间 = isr_total_time / millisecond_counter - 最大ISR执行时间 = isr_max_time - CPU占用率 = (平均ISR时间 × 1000) / 系统时钟周期

故障排除

问题1:定时器中断不触发

可能原因: - 定时器未启动 - NVIC中断未使能 - 时钟配置错误

解决方法:

  1. 检查定时器是否启动:

    // 确保调用了启动函数
    HAL_TIM_Base_Start_IT(&htim2);
    

  2. 检查NVIC配置:

    // 在CubeMX中确认勾选了TIM2 global interrupt
    // 或手动使能:
    HAL_NVIC_SetPriority(TIM2_IRQn, 2, 0);
    HAL_NVIC_EnableIRQ(TIM2_IRQn);
    

  3. 检查时钟配置:

    // 在调试器中查看TIM2->PSC和TIM2->ARR寄存器
    // 确认值是否正确
    

问题2:定时不准确

可能原因: - 预分频器或ARR值计算错误 - 系统时钟配置错误 - 中断优先级过低被其他中断抢占

解决方法:

  1. 重新计算定时器参数:

    // 公式:中断周期 = (PSC + 1) × (ARR + 1) / 定时器时钟
    // 例如:1ms = (8399 + 1) × (9 + 1) / 84000000
    

  2. 验证系统时钟:

    // 在main函数中打印系统时钟
    printf("SYSCLK: %lu Hz\r\n", HAL_RCC_GetSysClockFreq());
    printf("HCLK: %lu Hz\r\n", HAL_RCC_GetHCLKFreq());
    printf("PCLK1: %lu Hz\r\n", HAL_RCC_GetPCLK1Freq());
    

  3. 提高中断优先级:

    // 设置更高的优先级
    HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);  // 最高优先级
    

问题3:系统运行一段时间后崩溃

可能原因: - 栈溢出(ISR嵌套过深) - 在ISR中使用了不安全的函数 - 变量溢出(millisecond_counter)

解决方法:

  1. 增加栈大小:

    // 在启动文件中修改栈大小
    Stack_Size      EQU     0x1000  ; 增加到4KB
    

  2. 避免在ISR中使用危险函数:

    // 不要在ISR中使用:
    // - printf()(除非使用DMA)
    // - malloc()/free()
    // - HAL_Delay()
    // - 浮点运算
    

  3. 处理计数器溢出:

    // uint32_t最大值约为49天
    // 如果需要更长时间,使用64位计数器
    volatile uint64_t millisecond_counter = 0;
    

问题4:软件定时器不工作

可能原因: - 未初始化软件定时器系统 - 回调函数指针为NULL - 定时器ID超出范围

解决方法:

  1. 确保初始化:

    // 在main函数开始时调用
    SoftTimer_Init();
    

  2. 检查创建返回值:

    int timer_id = SoftTimer_Create(1000, 1, Task_Print_Info);
    if (timer_id < 0)
    {
        printf("Failed to create timer!\r\n");
    }
    

  3. 验证回调函数:

    // 确保回调函数不为NULL且正确定义
    void Task_Print_Info(void)
    {
        printf("Timer callback executed\r\n");
    }
    

问题5:LED闪烁频率不对

可能原因: - 定时器周期配置错误 - LED翻转逻辑错误 - 硬件连接问题

解决方法:

  1. 使用示波器或逻辑分析仪测量实际频率
  2. 检查LED翻转代码:

    // 正确的翻转方式
    HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
    
    // 或者
    if (HAL_GPIO_ReadPin(GPIOD, GPIO_PIN_12) == GPIO_PIN_SET)
    {
        HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_RESET);
    }
    else
    {
        HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET);
    }
    

  3. 检查硬件连接:

  4. 确认LED极性正确
  5. 确认限流电阻值合适
  6. 使用万用表测试引脚电压

总结

通过本教程,你学习了:

  • ✅ 定时器中断的工作原理和配置方法
  • ✅ 如何计算定时器的预分频器和重装载值
  • ✅ 实现基于定时器中断的精确延时函数
  • ✅ 设计和实现软件定时器系统
  • ✅ 管理多个并发的定时任务
  • ✅ 调试和优化定时器中断应用

核心要点:

  1. 定时器配置:
  2. 中断周期 = (PSC + 1) × (ARR + 1) / 定时器时钟
  3. 选择合适的预分频器和重装载值
  4. 使能定时器更新中断

  5. 中断服务函数:

  6. ISR应该尽可能短
  7. 避免在ISR中使用阻塞函数
  8. 使用回调函数处理定时事件

  9. 软件定时器:

  10. 支持多个独立的定时任务
  11. 支持周期性和单次定时
  12. 在硬件定时器中断中统一管理

  13. 最佳实践:

  14. 使用volatile关键字修饰共享变量
  15. 合理设置中断优先级
  16. 监控中断执行时间和CPU占用率

进阶挑战

尝试以下挑战来巩固学习:

  1. 挑战1:实现微秒级定时器
  2. 配置定时器产生10μs周期的中断
  3. 实现微秒级的精确延时函数
  4. 测量实际精度

  5. 挑战2:实现定时器级联

  6. 使用TIM2和TIM3级联
  7. 实现更长周期的定时(如1小时)
  8. 避免计数器溢出问题

  9. 挑战3:实现任务调度器

  10. 基于软件定时器实现简单的任务调度器
  11. 支持任务优先级
  12. 支持任务挂起和恢复

  13. 挑战4:低功耗定时器

  14. 在定时器中断中使用低功耗模式
  15. 测量功耗降低效果
  16. 实现唤醒机制

  17. 挑战5:定时器性能测试

  18. 测试最大支持的软件定时器数量
  19. 测试中断响应延迟
  20. 优化软件定时器算法

完整代码

main.c 完整代码

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : 定时器中断实现精确延时教程
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include <stdio.h>

/* Private variables ---------------------------------------------------------*/
TIM_HandleTypeDef htim2;
UART_HandleTypeDef huart1;

/* USER CODE BEGIN PV */

// 全局变量:毫秒计数器
volatile uint32_t millisecond_counter = 0;

// 软件定时器结构体
typedef struct {
    uint8_t  enabled;
    uint8_t  auto_reload;
    uint32_t period;
    uint32_t counter;
    void (*callback)(void);
} SoftTimer_t;

#define MAX_SOFT_TIMERS  8
SoftTimer_t soft_timers[MAX_SOFT_TIMERS];

/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_TIM2_Init(void);
static void MX_USART1_UART_Init(void);

/* USER CODE BEGIN PFP */

// 软件定时器函数声明
void SoftTimer_Init(void);
int SoftTimer_Create(uint32_t period, uint8_t auto_reload, void (*callback)(void));
void SoftTimer_Delete(int timer_id);
void SoftTimer_Process(void);

// 定时任务函数声明
void Task_LED_Green(void);
void Task_LED_Orange(void);
void Task_Print_Info(void);
void Task_OneShot(void);

// 工具函数声明
uint32_t Get_Milliseconds(void);
uint8_t Is_Timeout(uint32_t start_time, uint32_t timeout);

/* USER CODE END PFP */

/* USER CODE BEGIN 0 */

// printf重定向到USART1
int _write(int file, char *ptr, int len)
{
    HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
    return len;
}

// 软件定时器实现
void SoftTimer_Init(void)
{
    for (int i = 0; i < MAX_SOFT_TIMERS; i++)
    {
        soft_timers[i].enabled = 0;
        soft_timers[i].auto_reload = 0;
        soft_timers[i].period = 0;
        soft_timers[i].counter = 0;
        soft_timers[i].callback = NULL;
    }
}

int SoftTimer_Create(uint32_t period, uint8_t auto_reload, void (*callback)(void))
{
    for (int i = 0; i < MAX_SOFT_TIMERS; i++)
    {
        if (soft_timers[i].enabled == 0)
        {
            soft_timers[i].period = period;
            soft_timers[i].auto_reload = auto_reload;
            soft_timers[i].counter = 0;
            soft_timers[i].callback = callback;
            soft_timers[i].enabled = 1;
            return i;
        }
    }
    return -1;
}

void SoftTimer_Delete(int timer_id)
{
    if (timer_id >= 0 && timer_id < MAX_SOFT_TIMERS)
    {
        soft_timers[timer_id].enabled = 0;
    }
}

void SoftTimer_Process(void)
{
    for (int i = 0; i < MAX_SOFT_TIMERS; i++)
    {
        if (soft_timers[i].enabled)
        {
            soft_timers[i].counter++;

            if (soft_timers[i].counter >= soft_timers[i].period)
            {
                if (soft_timers[i].callback != NULL)
                {
                    soft_timers[i].callback();
                }

                if (soft_timers[i].auto_reload)
                {
                    soft_timers[i].counter = 0;
                }
                else
                {
                    soft_timers[i].enabled = 0;
                }
            }
        }
    }
}

// 定时任务实现
void Task_LED_Green(void)
{
    HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_12);
}

void Task_LED_Orange(void)
{
    HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_13);
}

void Task_Print_Info(void)
{
    printf("System uptime: %lu ms\r\n", Get_Milliseconds());
}

void Task_OneShot(void)
{
    printf("One-shot timer triggered!\r\n");
    HAL_GPIO_WritePin(GPIOD, GPIO_PIN_14, GPIO_PIN_SET);
}

// 工具函数实现
uint32_t Get_Milliseconds(void)
{
    return millisecond_counter;
}

uint8_t Is_Timeout(uint32_t start_time, uint32_t timeout)
{
    return ((millisecond_counter - start_time) >= timeout);
}

// 定时器回调函数
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        millisecond_counter++;
        SoftTimer_Process();
    }
}

/* USER CODE END 0 */

int main(void)
{
    /* MCU Configuration--------------------------------------------------------*/
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM2_Init();
    MX_USART1_UART_Init();

    /* USER CODE BEGIN 2 */

    // 启动定时器中断
    HAL_TIM_Base_Start_IT(&htim2);

    // 初始化软件定时器系统
    SoftTimer_Init();

    // 创建定时任务
    SoftTimer_Create(100, 1, Task_LED_Green);
    SoftTimer_Create(300, 1, Task_LED_Orange);
    SoftTimer_Create(1000, 1, Task_Print_Info);
    SoftTimer_Create(5000, 0, Task_OneShot);

    printf("\r\n=== Timer Interrupt Tutorial ===\r\n");
    printf("Software Timer System Started\r\n");
    printf("Task1: LED Green toggle every 100ms\r\n");
    printf("Task2: LED Orange toggle every 300ms\r\n");
    printf("Task3: Print info every 1000ms\r\n");
    printf("Task4: One-shot timer after 5000ms\r\n");
    printf("================================\r\n\r\n");

    /* USER CODE END 2 */

    /* Infinite loop */
    while (1)
    {
        /* USER CODE BEGIN 3 */

        // 主循环可以执行其他任务
        // 所有定时任务由软件定时器自动管理

        /* USER CODE END 3 */
    }
}

// 其他初始化函数省略...

完整的项目代码可以从以下链接下载: - GitHub仓库:timer-interrupt-tutorial - 包含完整的CubeMX配置文件和源代码

下一步

建议继续学习以下内容:

参考资料

  1. 官方文档:
  2. STM32F4 Reference Manual - 第18章:通用定时器
  3. STM32F4 HAL库用户手册 - TIM HAL驱动
  4. AN4013: STM32定时器应用笔记

  5. 技术文章:

  6. "Understanding STM32 Timer Interrupts" - 定时器中断详解
  7. "Software Timer Implementation" - 软件定时器实现方法
  8. "Real-Time Task Scheduling with Timers" - 基于定时器的任务调度

  9. 视频教程:

  10. STM32定时器中断配置 - B站视频教程
  11. 软件定时器设计与实现 - 实战演示

  12. 开源项目:

  13. FreeRTOS - 专业的实时操作系统,参考其定时器实现
  14. RT-Thread - 国产实时操作系统,学习软件定时器设计

常见问题FAQ

Q1: 定时器中断和SysTick中断有什么区别?

A: - SysTick是ARM Cortex-M内核的系统定时器,主要用于操作系统的时间片调度 - 通用定时器(TIM2-TIM7)是STM32外设,功能更丰富,可以配置不同的中断周期 - SysTick通常用于系统时基,通用定时器用于应用层定时任务 - 一个系统可以同时使用SysTick和多个通用定时器

Q2: 为什么要使用软件定时器而不是多个硬件定时器?

A: - 硬件定时器数量有限(STM32F407有14个定时器) - 软件定时器可以创建任意数量的定时任务 - 软件定时器更灵活,易于管理和调试 - 节省硬件资源,硬件定时器可用于PWM、输入捕获等其他功能

Q3: 定时器中断的优先级应该设置多高?

A: - 取决于应用的实时性要求 - 一般设置为中等优先级(2-3) - 如果需要精确的时间控制,可以设置为高优先级(0-1) - 避免设置为最高优先级,以免影响其他紧急中断

Q4: 如何处理定时器计数器溢出问题?

A: - uint32_t类型的计数器,最大值为4,294,967,295ms(约49.7天) - 如果需要更长时间,使用uint64_t类型 - 或者使用两个uint32_t变量,一个记录溢出次数 - 在实际应用中,49天通常足够,系统会定期重启

Q5: 定时器中断会影响系统性能吗?

A: - 会占用一定的CPU时间,但通常很小(<1%) - 1ms中断周期,ISR执行10μs,占用率 = 10μs / 1000μs = 1% - 如果ISR执行时间过长,会影响系统响应性 - 优化ISR代码,保持执行时间在10μs以内


反馈:如果你在学习过程中遇到问题或有改进建议,欢迎通过以下方式反馈: - GitHub Issues: 提交问题 - 邮箱: feedback@embedded-knowledge.com - 社区论坛: 讨论区

贡献:欢迎贡献代码示例、改进建议或错误修正!