跳转至

定时器中断实现精确延时

学习目标

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

  • 理解定时器中断的工作原理和应用场景
  • 配置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 - 社区论坛: 讨论区

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