STM32 TIM — 基本定时器定时(1ms 中断)

野火 EBF STM32F103 Mini HAL 库:用 TIM6 基本定时器产生 1ms 定时中断,在回调里累计时间,实现精确、非阻塞的 LED 翻转。

STM32 TIM — 基本定时器定时(1ms 中断)

开发板:野火 EBF STM32F103 Mini
固件库:HAL 库
对应例程TIM—基本定时器定时
参考源码User/main.cUser/tim/bsp_basic_tim.cUser/tim/bsp_basic_tim.h

定时器(TIMER)是 MCU 的"心脏节拍器"。STM32F103 有基本定时器(TIM6/7)、通用定时器(TIM2~5)、高级定时器(TIM1/8)。本文用最基础的 TIM6 产生 1ms 中断,在中断里累计毫秒数,主循环据此每 1 秒翻转一次 LED——这是后续所有"精确计时"应用的地基。


一、功能简介

  • 把 TIM6 配置为 1ms 触发一次更新(溢出)中断
  • 每次中断在 HAL_TIM_PeriodElapsedCallback 回调里把全局 time 加 1;
  • 主循环检测 time == 1000(即 1 秒到),清零并翻转 LED1。

效果:LED1 每 1 秒翻转一次,主循环完全不阻塞,可以并行做别的事。

核心知识点:

  1. 定时器时基:预分频(PSC)与自动重装载(ARR)如何决定溢出周期;
  2. 更新中断 + NVIC 配置;
  3. HAL 的定时器回调机制。

二、硬件准备

项目 说明
开发板 野火 EBF STM32F103 Mini
定时器 TIM6(基本定时器,无外部引脚)
时钟源 内部时钟,挂载在 APB1(36MHz)
指示 LED1 → PC2
调试输出 USART1(115200)

TIM6/7 是纯定时用途的基本定时器,没有输入捕获/输出比较/PWM,只有时基单元和 update 中断,最适合做"系统滴答"。本例也可切换到 TIM7(bsp_basic_tim.h 里改宏即可)。


三、完整代码(带中文注释)

1. bsp_basic_tim.h(参数与宏)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#ifndef __BASIC_TIM_H
#define	__BASIC_TIM_H

#include "stm32f1xx.h"

/* 定时器预分频:实际时钟 = 72MHz / (BASIC_TIMx_PRESCALER + 1) */
#define BASIC_TIMx_PRESCALER   71
/* 自动重装载值:计数到该值产生更新中断 */
#define BASIC_TIMx_PERIOD      1000

#define USE_TIM6

#ifdef  USE_TIM6
#define BASIC_TIMx                  TIM6
#define BASIC_TIM_RCC_CLK_ENABLE()  __HAL_RCC_TIM6_CLK_ENABLE()
#define BASIC_TIM_IRQ               TIM6_IRQn
#else
#define BASIC_TIMx                  TIM7
#define BASIC_TIM_RCC_CLK_ENABLE()  __HAL_RCC_TIM7_CLK_ENABLE()
#define BASIC_TIM_IRQ               TIM7_IRQn
#endif

void BASIC_TIMx_Init(void);

#endif /* __BASIC_TIM_H */

2. bsp_basic_tim.c(定时器初始化与回调)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include "./tim/bsp_basic_tim.h"

extern volatile uint32_t time;     // 主循环与中断共享的毫秒计数
TIM_HandleTypeDef htimx;

/* MSP:开 TIM 时钟 + 配 NVIC 中断优先级 */
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* htim_base)
{
  BASIC_TIM_RCC_CLK_ENABLE();                 // 开 TIM6 时钟
  HAL_NVIC_SetPriority(BASIC_TIM_IRQ, 1, 0); // 抢占优先级 1
  HAL_NVIC_EnableIRQ(BASIC_TIM_IRQ);          // 使能 TIM6 中断
}

/* 时基配置:1ms 溢出一次 */
void BASIC_TIMx_Init(void)
{
  TIM_MasterConfigTypeDef sMasterConfig;

  htimx.Instance               = BASIC_TIMx;
  htimx.Init.Prescaler        = BASIC_TIMx_PRESCALER;  // 71
  htimx.Init.CounterMode       = TIM_COUNTERMODE_UP;   // 向上计数
  htimx.Init.Period            = BASIC_TIMx_PERIOD;    // 1000
  HAL_TIM_Base_Init(&htimx);                         // 触发 HAL_TIM_Base_MspInit

  sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
  sMasterConfig.MasterSlaveMode     = TIM_MASTERSLAVEMODE_DISABLE;
  HAL_TIMEx_MasterConfigSynchronization(&htimx, &sMasterConfig);
}

/* 更新(溢出)中断回调:每进一次 +1ms */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  time++;
}

3. main.c(启动定时器 + 主循环判断)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "main.h"
#include "stm32f1xx.h"
#include "./usart/bsp_debug_usart.h"
#include "./led/bsp_led.h"
#include "./tim/bsp_basic_tim.h"

volatile uint32_t time = 0;   // ms 计时变量(全局,被中断修改)
extern TIM_HandleTypeDef htimx;

int main(void)
{
  SystemClock_Config();       // 72MHz
  DEBUG_USART_Config();
  LED_GPIO_Config();

  BASIC_TIMx_Init();          // 初始化 TIM6(1ms)
  HAL_TIM_Base_Start_IT(&htimx);   // 以中断方式启动定时器

  printf("\r\n 这是TIM—基本定时器定时 \r\n");

  while(1)
  {
    if (time == 1000)        // 累计 1000 次 = 1 秒
    {
      time = 0;
      LED1_TOGGLE;           // 翻转 LED1
    }
  }
}

四、代码分段讲解

1. 溢出周期怎么算?

定时器时钟来自 APB1,经倍频后 TIM6 实际时钟为 72MHz(F103 中 APB1 定时器时钟 = PCLK1×2 = 72M)。溢出时间公式:

1
2
3
溢出周期 T = (PSC + 1) × (ARR + 1) / TIM_CLK
           = (71 + 1) × (1000 + 1) / 72,000,000
           ≈ 1.000 ms
  • Prescaler = 71:72M 先 ÷72 → 计数频率 1MHz(每计数 1 次 = 1µs);
  • Period = 1000:计满 1000 次 → 1ms 到,产生更新中断。 (注:ARR=1000 实际计 1001 个边沿,误差极小,工程上直接用 1000 即可。)

2. HAL_TIM_Base_Start_IT 做了什么?

它启动计数器,并使能更新中断(UIE)。当计数器从 ARR 溢出归零时,硬件置更新标志、触发中断,跳进 TIM6_IRQHandler

3. 中断怎么进到回调的?

stm32f1xx_it.c 里的 TIM6_IRQHandler 调用 HAL_TIM_IRQHandler(&htimx),后者清标志并调用 HAL_TIM_PeriodElapsedCallback。我们把累加逻辑写在这个弱函数里,就实现了"每 1ms 自增"。这就是 HAL 的回调式中断编程,无需自己写清标志等寄存器操作。

4. 为什么 time 要加 volatile

time 在中断里被修改、在主循环里被读取。加 volatile 告诉编译器"它可能随时被意外修改",禁止把它的读取优化进寄存器缓存,否则主循环可能永远读不到最新值。

5. 与 HAL_Delay 的区别

HAL_Delay 也基于 SysTick 1ms 滴答,但它是阻塞的;本例用 TIM 中断做出非阻塞计时框架,主循环随时能响应别的任务,是更专业的写法。


五、实验现象

  1. 下载程序,串口打印提示语;
  2. 板载 LED1 每 1 秒翻转一次(亮 1 秒、灭 1 秒);
  3. 与主循环 while(1) 轮询延时不同,此时若在主循环里加别的任务,LED 翻转节奏依然精准。

六、常见问题

Q1:LED 不闪或闪烁频率不对?

  • 确认 HAL_TIM_Base_Start_IT 已调用(只 InitStart 不会计数);
  • 检查 Prescaler/Period 计算是否与系统时钟匹配(系统须为 72MHz);
  • 确认 NVIC 中断已使能(HAL_NVIC_EnableIRQ)。

Q2:进入中断后卡死/反复进中断?

  • 回调里不要做耗时操作;长时间占用中断会影响系统;
  • 若用轮询清标志方式,记得在中断里清 UPDATE 标志,否则会连续触发。

Q3:想做更长时间定时(如 1 分钟)?

  • 不要在中断里设超大 ARR,而是让 time 累计——每 1000 次(1 秒)在主循环判断换算,或再加一层 second 计数。

七、小结

  • 基本定时器 = 预分频 + 自动重装载,决定溢出周期;
  • 定时流程:开时钟 → 配时基 → 配 NVIC → Start_IT → 在回调里处理逻辑
  • volatile 全局变量 + 中断回调,是构建非阻塞"时间片"系统的标准范式。

八、拓展应用

  1. 多路软定时:用 time 派生出 secondminute,或做一个 SoftTimer 数组,实现多个独立周期任务(如 100ms 采样、500ms 刷屏)。
  2. 精准延时函数:基于 time 实现 delay_ms_nonblock() 替代阻塞延时。
  3. 通用定时器进阶:TIM2~5 支持输入捕获(测频率/脉宽)和 PWM 输出(调光/电机),见 TIM—通用定时器-输入捕获 与 PWM 章节。
  4. RTOS 时基:在 TIM 中断里调用 xTaskIncrementTick(FreeRTOS),把定时器变成操作系统的心跳。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计