定时器中断实现精确延时¶
学习目标¶
完成本教程后,你将能够:
- 理解定时器中断的工作原理和应用场景
- 配置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驱动、串口调试助手
- 辅助工具:逻辑分析仪(可选,用于精确测量)
环境配置¶
- 安装STM32CubeIDE开发环境
- 安装ST-Link驱动程序
- 配置串口调试助手(波特率115200)
- 测试开发板连接和下载功能
电路连接¶
连接图¶
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可以在等待期间执行其他任务 - 支持多个独立的定时任务 - 时间精度高,不受主程序影响 - 适合实时系统和周期性任务
定时器中断工作原理¶
关键参数: - 预分频器(Prescaler):降低计数频率 - 自动重装载值(ARR):计数目标值 - 计数模式:向上计数、向下计数、中心对齐
中断周期计算公式:
中断周期 = (Prescaler + 1) × (ARR + 1) / 定时器时钟频率
例如:
定时器时钟 = 84MHz
Prescaler = 8399
ARR = 9999
中断周期 = (8399 + 1) × (9999 + 1) / 84000000 = 1秒
步骤1:创建项目并配置定时器¶
1.1 创建新项目¶
- 打开STM32CubeIDE
- 选择
File → New → STM32 Project - 选择目标芯片:
STM32F407VGT6 - 输入项目名称:
timer_interrupt_tutorial - 点击
Finish
1.2 配置系统时钟¶
在CubeMX配置界面中:
- 点击
Clock Configuration标签 - 设置HCLK(系统时钟)为168MHz
- 确认APB1时钟为42MHz,APB2时钟为84MHz
- 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控制引脚:
- 在Pinout视图中找到PD12、PD13、PD14
- 右键选择
GPIO_Output - 在GPIO配置中设置:
- GPIO output level: Low(初始状态为低电平)
- GPIO mode: Output Push Pull
- GPIO Pull-up/Pull-down: No pull-up and no pull-down
-
Maximum output speed: Low
-
为引脚设置用户标签:
- PD12 →
LED_GREEN - PD13 →
LED_ORANGE - PD14 →
LED_RED
1.4 配置定时器TIM2¶
配置TIM2产生1ms周期的中断:
- 在Pinout视图左侧找到
Timers → TIM2 - 勾选
Activated - 在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(中等优先级)
计算说明:
1.5 配置串口(可选)¶
配置USART1用于调试输出:
- 在Pinout视图中找到
USART1 - Mode选择
Asynchronous - Configuration配置:
- Baud Rate:
115200 - Word Length:
8 Bits - Parity:
None -
Stop Bits:
1 -
NVIC Settings:
- 不勾选中断(使用轮询方式)
1.6 生成代码¶
- 点击
Project → Generate Code - 等待代码生成完成
- 打开生成的项目
预期结果: - 项目结构创建成功 - 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 编译项目¶
- 点击工具栏的 🔨 Build 按钮
- 查看控制台输出
- 确认编译成功(0 errors, 0 warnings)
可能的错误:
- 如果出现 undefined reference to _write,检查printf重定向代码
- 如果出现 TIM2_IRQHandler multiple definition,检查是否重复定义
5.2 下载程序¶
- 连接ST-Link调试器到开发板
- 点击 ▶️ Run 按钮
- 等待下载完成
预期结果: - 程序下载成功 - 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翻转周期:
- 将探头连接到PD12(绿色LED)
- 测量高电平和低电平的持续时间
- 验证周期是否为200ms(100ms高+100ms低)
精度要求: - 误差应小于±1% - 长时间运行不应有累积误差
步骤6:调试和优化¶
6.1 使用调试器验证¶
设置断点验证定时器中断:
- 在
HAL_TIM_PeriodElapsedCallback()函数中设置断点 - 启动调试模式(🐛 Debug)
- 观察程序是否每1ms进入一次中断
- 查看
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中断未使能 - 时钟配置错误
解决方法:
-
检查定时器是否启动:
-
检查NVIC配置:
-
检查时钟配置:
问题2:定时不准确¶
可能原因: - 预分频器或ARR值计算错误 - 系统时钟配置错误 - 中断优先级过低被其他中断抢占
解决方法:
-
重新计算定时器参数:
-
验证系统时钟:
-
提高中断优先级:
问题3:系统运行一段时间后崩溃¶
可能原因: - 栈溢出(ISR嵌套过深) - 在ISR中使用了不安全的函数 - 变量溢出(millisecond_counter)
解决方法:
-
增加栈大小:
-
避免在ISR中使用危险函数:
-
处理计数器溢出:
问题4:软件定时器不工作¶
可能原因: - 未初始化软件定时器系统 - 回调函数指针为NULL - 定时器ID超出范围
解决方法:
-
确保初始化:
-
检查创建返回值:
-
验证回调函数:
问题5:LED闪烁频率不对¶
可能原因: - 定时器周期配置错误 - LED翻转逻辑错误 - 硬件连接问题
解决方法:
- 使用示波器或逻辑分析仪测量实际频率
-
检查LED翻转代码:
-
检查硬件连接:
- 确认LED极性正确
- 确认限流电阻值合适
- 使用万用表测试引脚电压
总结¶
通过本教程,你学习了:
- ✅ 定时器中断的工作原理和配置方法
- ✅ 如何计算定时器的预分频器和重装载值
- ✅ 实现基于定时器中断的精确延时函数
- ✅ 设计和实现软件定时器系统
- ✅ 管理多个并发的定时任务
- ✅ 调试和优化定时器中断应用
核心要点:
- 定时器配置:
- 中断周期 = (PSC + 1) × (ARR + 1) / 定时器时钟
- 选择合适的预分频器和重装载值
-
使能定时器更新中断
-
中断服务函数:
- ISR应该尽可能短
- 避免在ISR中使用阻塞函数
-
使用回调函数处理定时事件
-
软件定时器:
- 支持多个独立的定时任务
- 支持周期性和单次定时
-
在硬件定时器中断中统一管理
-
最佳实践:
- 使用volatile关键字修饰共享变量
- 合理设置中断优先级
- 监控中断执行时间和CPU占用率
进阶挑战¶
尝试以下挑战来巩固学习:
- 挑战1:实现微秒级定时器
- 配置定时器产生10μs周期的中断
- 实现微秒级的精确延时函数
-
测量实际精度
-
挑战2:实现定时器级联
- 使用TIM2和TIM3级联
- 实现更长周期的定时(如1小时)
-
避免计数器溢出问题
-
挑战3:实现任务调度器
- 基于软件定时器实现简单的任务调度器
- 支持任务优先级
-
支持任务挂起和恢复
-
挑战4:低功耗定时器
- 在定时器中断中使用低功耗模式
- 测量功耗降低效果
-
实现唤醒机制
-
挑战5:定时器性能测试
- 测试最大支持的软件定时器数量
- 测试中断响应延迟
- 优化软件定时器算法
完整代码¶
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配置文件和源代码
下一步¶
建议继续学习以下内容:
- 中断调试技巧与常见问题 - 学习如何调试中断相关问题
- 中断优先级配置与抢占机制 - 深入理解中断优先级
- 定时器驱动基础与应用 - 学习定时器的更多功能
- PWM驱动开发 - 使用定时器实现PWM输出
参考资料¶
- 官方文档:
- STM32F4 Reference Manual - 第18章:通用定时器
- STM32F4 HAL库用户手册 - TIM HAL驱动
-
技术文章:
- "Understanding STM32 Timer Interrupts" - 定时器中断详解
- "Software Timer Implementation" - 软件定时器实现方法
-
"Real-Time Task Scheduling with Timers" - 基于定时器的任务调度
-
视频教程:
- STM32定时器中断配置 - B站视频教程
-
软件定时器设计与实现 - 实战演示
-
开源项目:
- FreeRTOS - 专业的实时操作系统,参考其定时器实现
- 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 - 社区论坛: 讨论区
贡献:欢迎贡献代码示例、改进建议或错误修正!