定时器中断实验代码1

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 1 Likes: 0 Comments: 0

130.5.2定时器中断实验代码(1)

/*
 * 整体目的:配置 TIM1 每 1 秒产生一次更新中断,并在回调中翻转 LED1 验证中断链路。
 * 参数依据:TIM1CLK=72 MHz,PSC=7199 表示 7200 分频,ARR=9999 表示 10000 个计数节拍,
 *           因此更新周期为 (7199+1)×(9999+1)/72 MHz = 1 s。
 * 调用链:TIM1 更新 → TIM1_UP_IRQHandler() → HAL_TIM_IRQHandler()
 *         → HAL_TIM_PeriodElapsedCallback()。
 * 代码范围:摘自 CubeIDE/06TIM 的 tim.c、stm32f1xx_it.c 和 main.c;
 *           HAL_TIM_Base_Init()、HAL_TIM_IRQHandler() 等库函数内部不逐行展开。
 */

/* ==================== Core/Inc/tim.h:TIM1 共享接口 ==================== */

extern TIM_HandleTypeDef htim1;             /* 声明 TIM1 句柄;存储实体在 tim.c 中定义。 */
void MX_TIM1_Init(void);                   /* 声明 CubeMX 生成的 TIM1 初始化函数。 */

/* ==================== Core/Src/tim.c:TIM1 基本参数 ==================== */

#include "tim.h"                            /* 引入定时器 HAL 类型、宏和本文件对外声明。 */

TIM_HandleTypeDef htim1;                    /* 定义 TIM1 句柄,保存实例地址、初始化参数和运行状态。 */

void MX_TIM1_Init(void)                    /* 配置 TIM1 基本计时、时钟源和主从模式。 */
{                                           /* TIM1 初始化函数体开始。 */
    TIM_ClockConfigTypeDef sClockSourceConfig = {0}; /* 清零时钟源配置结构体。 */
    TIM_MasterConfigTypeDef sMasterConfig = {0}; /* 清零主模式配置结构体。 */

    htim1.Instance = TIM1;                 /* 让句柄指向高级定时器 TIM1 的寄存器块。 */
    htim1.Init.Prescaler = 7199U;          /* 寄存器值加 1 后为 7200 分频:72 MHz 变为 10 kHz。 */
    htim1.Init.CounterMode = TIM_COUNTERMODE_UP; /* 让 CNT 从 0 向 ARR 递增。 */
    htim1.Init.Period = 9999U;             /* ARR=9999,计数器每 10000 个节拍产生一次更新。 */
    htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; /* 不额外分频数字滤波/死区采样时钟。 */
    htim1.Init.RepetitionCounter = 0U;     /* TIM1 每次溢出都产生更新,不额外累计重复周期。 */
    htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; /* ARR 新值在更新边界统一生效。 */

    if (HAL_TIM_Base_Init(&htim1) != HAL_OK) /* 让 HAL 配置基础寄存器并调用 MSP 初始化。 */
    {                                       /* 基础初始化失败分支开始。 */
        Error_Handler();                    /* 停止正常业务,便于暴露配置错误。 */
    }                                       /* 基础初始化结果判断结束。 */

    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; /* 使用芯片内部时钟树驱动计数器。 */
    if (HAL_TIM_ConfigClockSource(          /* 把所选时钟源应用到 TIM1。 */
            &htim1,                         /* 指定要配置的 TIM1 句柄。 */
            &sClockSourceConfig) != HAL_OK) /* 传入内部时钟配置并检查返回状态。 */
    {                                       /* 时钟源配置失败分支开始。 */
        Error_Handler();                    /* 配置失败时进入统一错误处理。 */
    }                                       /* 时钟源配置结果判断结束。 */

    sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; /* 不用 TIM1 的 TRGO 去触发其他外设。 */
    sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; /* 不建立定时器主从同步关系。 */
    if (HAL_TIMEx_MasterConfigSynchronization( /* 应用主模式配置。 */
            &htim1,                         /* 指定 TIM1。 */
            &sMasterConfig) != HAL_OK)      /* 传入主模式结构体并检查执行结果。 */
    {                                       /* 主模式配置失败分支开始。 */
        Error_Handler();                    /* 失败时停止运行。 */
    }                                       /* 主模式配置结果判断结束。 */
}                                           /* TIM1 初始化函数结束;此时尚未开始计数。 */

void HAL_TIM_Base_MspInit(                 /* HAL_TIM_Base_Init() 调用的底层资源初始化回调。 */
    TIM_HandleTypeDef *timBaseHandle)       /* 参数指出当前正在初始化哪个定时器。 */
{                                           /* MSP 初始化函数体开始。 */
    if (timBaseHandle->Instance == TIM1)   /* 只对 TIM1 执行以下配置。 */
    {                                       /* TIM1 底层配置开始。 */
        __HAL_RCC_TIM1_CLK_ENABLE();       /* 开启 APB2 上的 TIM1 外设时钟。 */
        HAL_NVIC_SetPriority(              /* 设置 TIM1 更新中断的 NVIC 优先级。 */
            TIM1_UP_IRQn,                  /* 选择 TIM1 更新中断通道。 */
            0U,                            /* 抢占优先级设为 0。 */
            0U);                           /* 子优先级设为 0。 */
        HAL_NVIC_EnableIRQ(TIM1_UP_IRQn);  /* 允许 NVIC 把 TIM1 更新请求送给 Cortex-M3。 */
    }                                       /* TIM1 底层配置结束。 */
}                                           /* MSP 初始化函数结束。 */

/* ==================== Core/Src/main.c:启动与应用回调 ==================== */

int main(void)                              /* 程序入口,省略未改动的系统时钟函数。 */
{                                           /* main() 函数体开始。 */
    HAL_Init();                             /* 初始化 HAL 和 SysTick。 */
    SystemClock_Config();                   /* 配置 72 MHz 系统时钟与 APB 总线。 */
    MX_GPIO_Init();                         /* 初始化 PB5、PE5 两路低电平有效 LED。 */
    MX_TIM1_Init();                         /* 把上面的 TIM1 参数写入硬件,但暂不启动。 */
    MX_USART1_UART_Init();                  /* 初始化后续观察运行时间所用的 USART1。 */
    HAL_TIM_Base_Start_IT(&htim1);         /* 允许更新中断并置位 CEN,正式开始计数。 */

    while (1)                               /* 主循环暂时没有业务,等待硬件中断发生。 */
    {                                       /* 主循环体开始。 */
    }                                       /* 空循环结束后立即开始下一轮。 */
}                                           /* main() 语法边界。 */

void HAL_TIM_PeriodElapsedCallback(         /* HAL 识别到定时器更新事件后调用的弱回调覆盖实现。 */
    TIM_HandleTypeDef *htim)                /* 参数指出哪一个定时器产生了更新事件。 */
{                                           /* 周期完成回调开始。 */
    if (htim == &htim1)                    /* 只响应 TIM1,防止其他定时器共用回调时误操作。 */
    {                                       /* TIM1 事件分支开始。 */
        HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); /* 每 1 秒翻转 PB5,使 LED1 状态改变。 */
    }                                       /* TIM1 事件分支结束。 */
}                                           /* 回调结束,应尽快返回中断现场。 */

/* ==================== Core/Src/stm32f1xx_it.c:向量入口 ==================== */

extern TIM_HandleTypeDef htim1;             /* 声明 tim.c 中定义的 TIM1 句柄。 */

void TIM1_UP_IRQHandler(void)              /* TIM1 更新中断的向量函数名,必须与启动文件一致。 */
{                                           /* TIM1 更新 IRQ 函数开始。 */
    HAL_TIM_IRQHandler(&htim1);            /* 交给 HAL 检查并清除标志,再调用上面的周期回调。 */
}                                           /* TIM1 更新 IRQ 函数结束。 */

一、定时器实验

1.1 芯片手册

基本、通用和高级定时器的差异来自内部硬件通路。无论框图多复杂,都可以先找共同的计数主干:

时钟源选择 → 预分频器 PSC → 计数器 CNT ← 自动重装值 ARR
                                  │
                                  ├─ 更新事件/更新中断
                                  └─ 与 CCRx 比较或被捕获到 CCRx

基本定时器只保留计数、更新事件、DMA/中断等主干;通用定时器增加输入捕获、输出比较、PWM 和编码器接口;高级定时器再增加互补输出、死区、刹车和重复计数等功率控制功能。

框图中的连线不是程序执行流程,而是芯片内部真实的信号选择和硬件数据通路。初学时不必一次记住全部开关,只需按当前任务追踪“时钟从哪里来、CNT 如何计数、什么事件触发中断”。本实验选用 TIM1。TIM1 虽然属于高级定时器,但完全可以只使用其基本定时与更新中断功能。

1.2 实验演示

实验目标如下:

  • TIM1 每 1 秒产生一次更新中断,LED1 每次中断翻转。
  • 用 G_TIM_Count 记录上电后的秒数。
  • LED2 每隔 2 秒翻转一次。
  • 主循环按标志位通过 USART1 输出运行时间。

注意“每隔 1 秒翻转一次”不是“完整闪烁周期为 1 秒”:一次翻转只完成亮到灭或灭到亮,亮灭一整轮需要 2 秒。按复位键后,RAM 中的计数变量重新初始化,运行时间也从头统计。

1.3 新建工程

工程选择 STM32F103ZET6,并让 CubeMX 分别生成外设的 .c/.h 文件。生成代码后,用户逻辑放在 USER CODE BEGIN/END 区域,避免重新生成时被覆盖。

1.3.1 配置系统时钟

开发板使用外部高速晶振 HSE。时钟配置中启用 HSE,经 PLL 得到 72 MHz 的系统时钟 SYSCLK,并确认 AHB、APB1、APB2 没有超过芯片允许的最高频率。

flowchart LR
    A[板载HSE晶振] --> B[PLL锁相环]
    B --> C[SYSCLK 72MHz]
    C --> D[AHB/HCLK 72MHz]
    D --> E[APB2/PCLK2 72MHz]
    D --> F[APB1/PCLK1 36MHz]
    E --> G[TIM1/TIM8 时钟]
    F --> H[TIM2至TIM7 时钟经条件倍频]

HSE 是 PLL 的参考时钟;“定时器选择内部时钟”则是说 TIM1 的计数源来自芯片内部时钟树,而不是 TIM1 的 ETR 外部计数引脚。这两个“内部/外部”的语境不能混为一谈。

1.3.2 配置定时器
1.3.2.1 定时器选择与基本配置

本实验使用 TIM1,时钟源选择 Internal Clock,计数模式选择向上计数。虽然 TIM6/TIM7 这种基本定时器也能完成周期中断,但具体芯片、封装和工程资源可能已有占用,因此应按实际可用资源选择。

TIM1 的高级功能在本实验中不启用。配置结果本质上是 CubeMX 帮助程序填写 TIM_HandleTypeDef,随后 HAL_TIM_Base_Init(&htim1) 把这些参数写入硬件寄存器。

1.3.2.2 定时器时钟源的选择

定时器可由内部时钟、ETR 外部触发输入或其他触发源驱动。本实验的“定时”依赖稳定的片内时钟树,因而使用内部时钟。ETR 模式更像对外部脉冲计数,例如统计传感器输出的脉冲个数。

只做更新中断不需要启用任何 CH1~CH4 引脚。PWM 输出或输入捕获才需要选择通道,并按复用功能表使用与该通道相连的固定 GPIO。

1.3.2.3 定时器通道与引脚选择

定时器通道不是任意 GPIO 软件映射,而是芯片内部预先布线到某些引脚。例如某个 TIMx_CHy 能出现在哪些引脚上,由芯片型号和重映射配置共同决定。

基础定时中断:内部时钟 → PSC → CNT/ARR → 更新中断
              不经过通道,不需要定时器 GPIO

PWM:          CNT 与 CCRx 比较 → 通道输出级 → TIMx_CHy 引脚
输入捕获:     TIMx_CHy 引脚 → 滤波/边沿检测 → 把 CNT 锁存到 CCRx

因此当前页面只需配置 TIM1 基本参数;后续 PWM 与输入捕获实验再配置具体通道。

1.3.2.4 定时器参数设置: 计数值与分频值

两个核心参数是预分频寄存器 PSC 和自动重装寄存器 ARR。向上计数时,CNT 从 0 数到 ARR,共计数 ARR+1 次;预分频器的实际分频系数是 PSC+1。

f_CNT = f_TIM / (PSC + 1)
T_UPDATE = (ARR + 1) / f_CNT
         = (PSC + 1) × (ARR + 1) / f_TIM

不能只看到界面中的“计数值 9999”就说计数 9999 次,也不能把 PSC 填写值直接当分频系数。两个加一是定时计算中最常见的易错点。

1.3.2.5 定时器时钟源与总线架构

STM32F103ZE 中,TIM1、TIM8 挂在 APB2;TIM2~TIM7 挂在 APB1。定时器时钟不总是等于相应的 PCLK:

若 APBx 预分频系数 = 1:f_TIMx = PCLKx
若 APBx 预分频系数 > 1:f_TIMx = 2 × PCLKx

当前时钟树通常令 APB2=72 MHz、预分频为 1,所以 TIM1 时钟为 72 MHz;APB1=36 MHz、预分频为 2,而 TIM2~TIM7 的定时器时钟经过倍频同样为 72 MHz。

这个倍频是时钟控制电路的规则,不是看到 36 MHz 后无条件乘二。更改 APB 分频时,必须重新按时钟树确认 f_TIM。

1.3.2.6 定时器分频值计算与设置

若把 72 MHz 分频为 1 MHz,可填写 PSC=71:

f_CNT = 72 MHz / (71 + 1) = 1 MHz
每个计数节拍 = 1 μs

此时 ARR=65535 的更新周期为:

T_UPDATE = (65535 + 1) × 1 μs = 65.536 ms

所以 PSC=71、ARR=65535 并不能直接得到 1 秒。16 位 ARR 最大为 65535,要得到 1 秒,必须进一步降低计数频率,或使用软件累计多个较短周期。

1.3.2.7 定时器中断周期设置

配套 06TIM 工程采用:

TIM1CLK = 72 MHz
PSC = 7199  → 实际 7200 分频 → f_CNT = 10 kHz
ARR = 9999  → 一个更新周期 10000 个节拍
T_UPDATE = 7200 × 10000 / 72,000,000 = 1 s

对应初始化参数为:

htim1.Init.Prescaler = 7199;
htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
htim1.Init.Period = 9999;
htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim1.Init.RepetitionCounter = 0;
htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;

ClockDivision 主要影响定时器数字滤波/死区所用的采样时钟,不是这里用于产生 1 秒周期的 PSC。RepetitionCounter 是高级定时器特有的重复计数设置,本实验置 0,表示每次计数器溢出都产生更新事件。

1.3.2.8 定时器计数方式与自动重装

CounterMode=UP 表示向上计数。连续模式下,CNT 到达 ARR 并发生更新后会从 0 开始下一轮,这是定时器的基本计数行为。

CubeMX 中的 Auto-reload preload 控制的是“写 ARR 时是否先进入影子/缓冲机制”:

  • DISABLE:新的 ARR 通常立即作用于计数周期,可能使当前周期突然变长或缩短。
  • ENABLE:新的 ARR 先保存在预装载寄存器,在下一次更新事件时统一转入有效寄存器,周期边界更整齐。

它不表示“中断后是否继续自动重装”。即使预装载关闭,只要 CR1.CEN 仍为 1、单脉冲模式未启用,计数器依然会连续运行。若需要让预装载值立即生效,可以通过 EGR.UG 产生更新事件;HAL 初始化过程会处理相关装载时序。

1.3.3 配置灯

PB5 与 PE5 配置为推挽输出,并将初始输出电平设为高,使两个 LED 上电后先保持熄灭。板上 LED 为低电平有效:

3.3 V ── LED ── 1 kΩ ── PB5/PE5

引脚输出低:电流流入 GPIO,LED 亮
引脚输出高:LED 两端近似等电位,LED 灭

1 kΩ 电阻用于限制 LED 电流。GPIO 的“高/低”描述的是引脚电位,而“灯亮/灭”由完整电路中的电流决定,所以低电平也可以表示逻辑上的“点亮”。

1.3.4 配置串口

USART1 选择异步模式,常用配置为 115200 bit/s、8 数据位、无校验、1 停止位、收发使能。开发板板载 CH340C 与 USART1 的 PA9/PA10 相连,用于在电脑串口终端查看运行时间。

串口打印不是定时器工作所必需,但它能帮助观察内部秒计数。若终端乱码,应依次检查波特率、数据格式、地线、串口号以及 printf 重定向所使用的 UART 句柄。

1.3.5 初始化代码

生成后的主函数按顺序完成 HAL、系统时钟和外设初始化:

HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_TIM1_Init();
MX_USART1_UART_Init();

MX_TIM1_Init() 只负责配置定时器,并不等于启动计数。程序还需要显式调用 HAL_TIM_Base_Start_IT(&htim1)。

定时器更新中断能进入用户回调,必须同时满足以下条件:

定时器正在计数:CR1.CEN = 1
更新中断允许:  DIER.UIE = 1
NVIC通道允许:  TIM1_UP_IRQn 已使能
事件已经发生:  SR.UIF = 1
1.3.6 找到中断函数
1.3.6.1 中断函数与中断向量表的基本原理

Cortex-M3 发生 TIM1 更新中断后,根据启动文件中的中断向量表取得 TIM1_UP_IRQHandler 地址。调用链为:

flowchart TD
    A[TIM1 CNT计满] --> B[产生更新事件并置位 SR.UIF]
    B --> C{DIER.UIE与NVIC均允许?}
    C -- 否 --> D[只保留/处理状态,不进入应用回调]
    C -- 是 --> E[CPU根据向量表进入 TIM1_UP_IRQHandler]
    E --> F[HAL_TIM_IRQHandler &htim1]
    F --> G[识别更新中断并清除UIF]
    G --> H[HAL_TIM_PeriodElapsedCallback &htim1]

向量表负责找到入口,stm32f1xx_it.c 中的 IRQHandler 负责把具体句柄交给 HAL,HAL 再统一判定标志与中断源,最后调用可由用户实现的回调。

1.3.6.2 定时器中断的配置与中断线路管理器设置

CubeMX 的 NVIC Settings 中需要勾选 TIM1 update interrupt。生成代码会在 HAL_TIM_Base_MspInit() 中设置优先级并调用:

HAL_NVIC_SetPriority(TIM1_UP_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM1_UP_IRQn);

NVIC 使能只打开 CPU 侧的中断通道;HAL_TIM_Base_Start_IT() 还会设置定时器侧的 DIER.UIE 并启动 CR1.CEN。缺少任意一侧,回调都不会按预期执行。

1.3.6.3 中断优先级与子优先级的设置

优先级数值越小,抢占优先级越高。抢占优先级决定一个中断能否打断另一个中断;子优先级只在抢占优先级相同时决定等待执行的先后顺序,其有效位数受 NVIC 优先级分组影响。

本实验只有少量中断,设置为 0 可以运行,但工程扩大后应统一规划。高频且耗时的高优先级中断可能长期阻塞串口、SysTick 等其他中断。优先级的核心原则不是“全部设最高”,而是保证实时性同时缩短 ISR。

1.3.6.4 中断服务函数的查找与定位

CubeMX 在 Core/Src/stm32f1xx_it.c 中生成:

void TIM1_UP_IRQHandler(void)
{
    HAL_TIM_IRQHandler(&htim1);
}

htim1 实体定义在 tim.c,中断文件通过 extern TIM_HandleTypeDef htim1; 引用它。函数名必须与启动文件向量表中的弱符号完全一致,否则硬件会进入默认处理函数而不是预期代码。

1.3.6.5 HAL TIM IRQHandler函数的作用

HAL_TIM_IRQHandler() 是 HAL 的通用定时器中断分发器。一个定时器可能同时产生更新、捕获比较、触发、刹车等多种中断,所以它会分别检查“状态标志是否置位”和“对应中断使能位是否打开”。

对于本实验的更新事件,关键逻辑可以概括为:

检查 SR.UIF
  └─ 再检查 DIER.UIE
       ├─ 清除 UIF
       └─ 调用 HAL_TIM_PeriodElapsedCallback(htim)

先清状态标志再进入回调,可防止同一事件退出 ISR 后立刻重复触发。不要在自己的回调中随意清除整个 SR,否则可能误删同一定时器的其他待处理事件。

1.3.6.6 查找对应的回调函数

更新事件对应 HAL_TIM_PeriodElapsedCallback(),名称中的 PeriodElapsed 表示一个计数周期已经结束。HAL 源码提供弱定义,应用程序写出同名、同参数函数后,链接器会使用应用实现。

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM1)
    {
        /* TIM1 的周期任务 */
    }
}

参数 htim 不能忽略。多个定时器共用同一个回调接口时,要用 htim->Instance 或与 &htim1 比较来区分中断来源。

1.3.6.7 例题1: 回调函数在定时器中断中的应用

让 LED1 每秒翻转一次的最小回调为:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM1)
    {
        HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
    }
}

这里的 GPIO 操作很短,适合放在回调中。串口阻塞发送、长延时、复杂格式化或大量计算则会拉长中断占用时间,应改为设置标志,交给主循环处理。

1.3.7 修改定时器中断回调函数

在 main.c 的用户代码区域实现回调,保留定时器来源判断:

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM1)
    {
        HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin);
    }
}

HAL_GPIO_TogglePin() 会根据当前输出状态翻转对应 GPIO。因为 LED 低电平有效,第一次翻转可能是高变低并点亮,下一次低变高并熄灭。

回调应写在 USER CODE BEGIN 4 一类受保护区域内。不要改 HAL 驱动里的弱回调,也不要把应用逻辑直接写进自动生成的 IRQHandler,否则代码生成和后续复用都会更困难。

1.3.8 编译并运行

初始化完成后启动带中断的基础定时器:

if (HAL_TIM_Base_Start_IT(&htim1) != HAL_OK)
{
    Error_Handler();
}

Start_IT 与 Start 的区别是前者同时打开更新中断请求。建议检查返回值,避免句柄状态异常时仍假定定时器已经工作。

如果 LED 不闪,可按信号链逐项排查:

  1. 时钟树是否使 TIM1CLK 为预期频率。
  2. PSC=7199、ARR=9999 是否写入。
  3. HAL_TIM_Base_Start_IT() 是否执行且返回 HAL_OK。
  4. DIER.UIE、CR1.CEN 和 NVIC 的 TIM1_UP 通道是否打开。
  5. TIM1_UP_IRQHandler() 是否调用 HAL_TIM_IRQHandler(&htim1)。
  6. 回调签名是否完全正确,是否判断到了 TIM1。
  7. PB5 的 GPIO 模式与低有效 LED 电路是否正确。

完成上述链路后,TIM1 每秒产生一次更新事件,CPU 经 IRQHandler 和 HAL 分发进入回调,LED1 的输出电平随之翻转。