定时器实验演示

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

129.5.1定时器实验演示

/*
 * 整体目的:通过三个已经完成的工程预览 STM32 定时器的典型用途:
 *           06TIM 用更新中断计时,07PWM 用比较通道生成 PWM,08IC 用输入捕获测量高电平宽度。
 * 课程上下文:本节重点是观察现象和建立定时器功能地图,不是从头编写三个完整工程。
 * 代码范围:以下仅摘录三个配套工程中直接体现实验行为的业务代码;
 *           CubeMX 初始化模板及 HAL/CMSIS 库内部实现不在本节逐行展开。
 */

/* ==================== CubeIDE/06TIM:周期中断与延后打印 ==================== */

volatile uint32_t G_TIM_Count = 0U;         /* 保存 TIM1 更新次数;本工程每次更新为 1 秒。 */
volatile uint8_t G_UART_Flag = 0U;          /* ISR 置位、主循环清零,用于把慢速串口输出移出中断。 */

HAL_TIM_Base_Start_IT(&htim1);             /* 开启 TIM1 计数并允许更新中断;仅初始化不会自动开始计数。 */

void HAL_TIM_PeriodElapsedCallback(         /* HAL 确认更新中断后调用的周期完成回调。 */
    TIM_HandleTypeDef *htim)                /* 参数指出产生事件的定时器。 */
{                                           /* 定时器回调开始。 */
    if (htim == &htim1)                    /* 只处理 TIM1,避免以后增加其他定时器时串扰。 */
    {                                       /* TIM1 分支开始。 */
        HAL_GPIO_TogglePin(LED1_GPIO_Port, LED1_Pin); /* 每秒翻转一次 LED1;完整亮灭周期为 2 秒。 */
        G_TIM_Count++;                      /* 更新上电秒计数。 */
        if ((G_TIM_Count % 5U) == 1U)      /* 在第 1、6、11……秒产生一次打印事件。 */
        {                                   /* 五秒间隔事件分支开始。 */
            G_UART_Flag = 1U;              /* 只通知主循环,不在 ISR 中执行阻塞式串口发送。 */
        }                                   /* 五秒间隔事件分支结束。 */
        if ((G_TIM_Count % 2U) == 0U)      /* 每逢偶数秒触发一次 LED2 翻转。 */
        {                                   /* 两秒间隔事件分支开始。 */
            HAL_GPIO_TogglePin(LED2_GPIO_Port, LED2_Pin); /* 每 2 秒翻转 LED2,完整周期为 4 秒。 */
        }                                   /* 两秒间隔事件分支结束。 */
    }                                       /* TIM1 分支结束。 */
}                                           /* 回调结束并返回中断处理流程。 */

if (G_UART_Flag == 1U)                     /* 主循环轮询 ISR 产生的打印事件。 */
{                                           /* 打印事件处理开始。 */
    printf("MCU stm32 run time is %lus \n", /* 输出当前累计秒数;printf 已重定向到 USART1。 */
           G_TIM_Count);                    /* 把 32 位无符号秒计数代入 %lu。 */
    G_UART_Flag = 0U;                       /* 消费事件后清零,防止主循环重复打印同一事件。 */
}                                           /* 打印事件处理结束。 */

/* ==================== CubeIDE/07PWM:呼吸灯与舵机 PWM ==================== */

uint8_t PWM_Value = 0U;                    /* 保存 0~199 的三角波相位。 */
uint8_t LED_PWM_Value = 0U;                /* 保存写入 TIM3 比较寄存器的 LED 占空比值。 */
uint8_t SG_PWM_Value = 5U;                 /* 保存舵机 PWM 比较值,在 5~25 之间步进。 */

HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);  /* 启动 TIM3 通道 1 的硬件 PWM 输出。 */
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);  /* 启动 TIM3 通道 2,用相同比较值驱动另一负载。 */
HAL_TIM_PWM_Start(&htim4, TIM_CHANNEL_1);  /* 启动 TIM4 通道 1 的舵机控制 PWM。 */

PWM_Value++;                               /* 主循环每轮推进一次三角波相位。 */
PWM_Value = PWM_Value % 200U;              /* 把相位限制在 0~199,随后重新回到 0。 */
if (PWM_Value > 99U)                       /* 后半程需要让亮度值从 100 逐渐下降。 */
{                                           /* 三角波下降段开始。 */
    LED_PWM_Value = 200U - PWM_Value;       /* 将 100~199 映射为 100~1。 */
}                                           /* 三角波下降段结束。 */
else                                        /* 前半程让亮度值随相位逐渐上升。 */
{                                           /* 三角波上升段开始。 */
    LED_PWM_Value = PWM_Value;              /* 将 0~99 直接作为比较值。 */
}                                           /* 三角波上升段结束。 */
HAL_Delay(25U);                             /* 每 25 ms 更新一次占空比,使亮度变化可见且较平滑。 */
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, LED_PWM_Value); /* 更新 TIM3_CH1 占空比。 */
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_2, LED_PWM_Value); /* 同步更新 TIM3_CH2 占空比。 */

/* ==================== CubeIDE/08IC:输入捕获测量脉宽 ==================== */

volatile uint32_t ic_state = 0U;            /* 0 等待上升沿,1 等待下降沿,2 表示一次测量完成。 */
volatile uint32_t period_count = 0U;        /* 统计测量期间 TIM2 发生的溢出次数。 */
volatile uint32_t ic_count = 0U;            /* 保存下降沿到来时 CCR2 捕获的计数值。 */
uint32_t us_count = 0U;                    /* 主循环计算出的高电平持续时间,单位约为微秒。 */

HAL_TIM_Base_Start_IT(&htim2);             /* 启动 TIM2 更新中断,用于统计测量期间的计数器溢出。 */
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); /* 启动 PA1/TIM2_CH2 的输入捕获中断。 */

void HAL_TIM_IC_CaptureCallback(            /* 捕获通道检测到设定边沿后由 HAL 调用。 */
    TIM_HandleTypeDef *htim)                /* 参数指出发生捕获的定时器。 */
{                                           /* 输入捕获回调开始。 */
    if (htim->Instance == TIM2)            /* 仅处理 TIM2 的捕获事件。 */
    {                                       /* TIM2 捕获分支开始。 */
        if (ic_state == 0U)                /* 第一个边沿应是高脉冲的上升沿。 */
        {                                   /* 上升沿处理开始。 */
            __HAL_TIM_SET_COUNTER(htim, 0U); /* 从上升沿开始把 CNT 清零,建立测量起点。 */
            __HAL_TIM_SET_CAPTUREPOLARITY(  /* 把下一次捕获边沿切换为下降沿。 */
                &htim2, TIM_CHANNEL_2,      /* 修改 TIM2 通道 2 的捕获极性。 */
                TIM_INPUTCHANNELPOLARITY_FALLING); /* 下一次捕获高电平结束时刻。 */
            ic_state = 1U;                 /* 标记当前正在等待下降沿。 */
            period_count = 0U;             /* 清除上一次测量遗留的溢出次数。 */
            ic_count = 0U;                 /* 清除上一次捕获值。 */
        }                                   /* 上升沿处理结束。 */
        else                                /* 已有起点,本次边沿视为下降沿。 */
        {                                   /* 下降沿处理开始。 */
            ic_count = HAL_TIM_ReadCapturedValue(&htim2, TIM_CHANNEL_2); /* 读取锁存在 CCR2 的 CNT。 */
            __HAL_TIM_SET_CAPTUREPOLARITY(  /* 为下一帧恢复上升沿捕获。 */
                &htim2, TIM_CHANNEL_2,      /* 仍配置 TIM2 通道 2。 */
                TIM_INPUTCHANNELPOLARITY_RISING); /* 下一次先寻找新脉冲的起点。 */
            ic_state = 2U;                 /* 通知主循环一次脉宽测量已经完成。 */
        }                                   /* 下降沿处理结束。 */
    }                                       /* TIM2 捕获分支结束。 */
}                                           /* 输入捕获回调结束。 */

if (ic_state == 2U)                        /* 主循环发现测量完成后再计算并打印。 */
{                                           /* 测量结果处理开始。 */
    us_count = ic_count + period_count * 65536U; /* 每次 16 位计数器溢出跨过 65536 个节拍。 */
    printf("High level duration: %luus \n", us_count); /* 输出高电平宽度;1 MHz 计数时一拍约 1 μs。 */
    ic_state = 0U;                         /* 清除完成状态,等待下一帧上升沿。 */
}                                           /* 测量结果处理结束。 */

一、定时器

1.1 定时器基本使用

定时器可以理解为“由稳定时钟推动的自动计数器”。它不需要 CPU 在软件中不停加一,而是由片内硬件按固定节拍更新计数值;达到设定条件后,还可以置位状态标志、请求中断、翻转输出或锁存外部信号出现时的计数值。

flowchart LR
    A[定时器时钟 f_TIM] --> B[预分频器 PSC]
    B -->|得到计数节拍| C[计数器 CNT]
    D[自动重装寄存器 ARR] --> C
    C -->|计满并更新| E[更新事件 UEV]
    E --> F[置位 SR.UIF]
    F -->|DIER.UIE=1 且 NVIC 已使能| G[进入定时器中断]
    E --> H[重新开始下一周期]

最基本的向上计数模式中,计数器从 0 数到 ARR,一共经历 ARR+1 个计数节拍。预分频器也按“寄存器值加 1”分频,因此更新周期为:

计数频率:f_CNT = f_TIM / (PSC + 1)
更新频率:f_UPDATE = f_TIM / [(PSC + 1) × (ARR + 1)]
更新周期:T_UPDATE = (PSC + 1) × (ARR + 1) / f_TIM

定时器中断与串口中断、ADC 中断的共同点是:外设先产生事件并置位标志,外设中断使能位和 NVIC 通道都打开后,CPU 才跳转到中断服务函数。掌握这条链路后,学习其他外设中断会容易很多。

1.1.1 定时器配置

以 51 单片机的软件 PWM 为例,可以让定时器每 10 ms 进入一次中断,在中断中维护 count 和 level:当 count < level 时输出有效电平,否则输出无效电平。一个 PWM 周期包含若干次定时中断,占空比由有效电平所占的计数次数决定。

定时中断节拍: |--10 ms--|--10 ms--|--10 ms--|...
软件计数 count:    0        1        2       ...
输出判定:          count < level ? 有效 : 无效

这种方法原理直观,但每个节拍都占用 CPU。STM32 的通用和高级定时器带有输出比较/PWM 硬件,配置 PSC、ARR、CCR 和通道模式后,计数器与比较器可以自行产生波形,CPU 只需在需要改变频率或占空比时更新寄存器。

1.2 定时器的分类

STM32F103ZE 的常用定时器分为三类:

类别本型号中的定时器基本计时输入捕获/输出比较/PWM高级电机控制
基本定时器TIM6、TIM7支持不支持不支持
通用定时器TIM2、TIM3、TIM4、TIM5支持支持不支持互补输出、死区和刹车
高级定时器TIM1、TIM8支持支持支持互补输出、死区、刹车和重复计数

“高级”是在通用功能上增加电机控制能力,不代表 TIM1 只能做复杂任务。TIM1 也可以像本章实验一样,只使用内部时钟和更新中断完成普通的 1 秒定时。

不同 STM32F103 型号封装和资源规模不同,定时器数量也不完全相同。选型时应查对应型号的数据手册或在 CubeMX/CubeIDE 的外设列表中确认,不能把 ZET6 的资源数量直接套到 C8T6。

1.2.1 基本定时器

基本定时器的核心数据通路最短,适合产生周期性更新事件,也能为片内 DAC 等外设提供触发:

内部时钟
   │
   ▼
[PSC 预分频] → [CNT 16位向上计数] ← [ARR 周期上限]
                         │
                         └──计满──> 更新事件 / DMA请求 / 中断请求

主要寄存器及作用如下:

寄存器关键内容初学阶段的理解
PSC预分频值决定计数器每隔多久加一
CNT当前计数值定时器此刻数到了哪里
ARR自动重装值决定一个周期包含多少个计数节拍
DIERUIE是否允许更新事件提出中断请求
SRUIF是否已经发生更新事件
CR1CEN、ARPE是否开始计数、ARR 写入是否使用缓冲
EGRUG软件产生一次更新事件,使预装载值立即生效
1.2.2 通用定时器

通用定时器在基本计数单元外增加了多个捕获/比较通道。通道前端可以对外部引脚进行选择、滤波和边沿检测;通道后端可以比较 CNT 与 CCR1~CCR4,生成输出比较或 PWM。

flowchart LR
    A[TIx 外部引脚] --> B[输入选择与数字滤波]
    B --> C[边沿检测]
    C --> D[捕获 CNT 到 CCRx]
    E[CNT] --> F[比较器]
    G[CCRx] --> F
    F --> H[OC/PWM 输出控制]
    H --> I[CHx 引脚]

通用定时器通常支持向上计数、向下计数和中心对齐计数。输入捕获用于测量频率、周期或脉宽;输出比较用于在指定计数点改变输出;PWM 模式则把周期寄存器 ARR 与比较寄存器 CCR 配合起来生成重复脉冲。

1.2.3 高级定时器

高级定时器在通用定时器的基础上增加了互补输出、可编程死区、刹车输入、主输出使能和重复计数器。这些功能主要面向半桥、全桥、三相逆变器等功率电路。

以同一桥臂的上下两个 MOSFET 为例,如果二者在切换瞬间同时导通,会把电源直接短路。死区电路会在关闭一个管子后等待极短时间,再打开另一个管子;刹车输入则能在过流等故障发生时快速关断 PWM。普通 LED 调光不需要这些功能,但电机逆变控制经常需要。

1.3 定时器的应用

1.3.1 实验一

基础定时器实验使用 TIM1 每秒产生一次更新中断。回调函数完成三件短小的工作:

  1. 每次中断翻转 LED1。
  2. 全局秒计数 G_TIM_Count++。
  3. 到达指定秒数时设置串口打印标志,并按 2 秒条件翻转 LED2。

LED1 和 LED2 分别连接 PB5、PE5,板上电路为低电平点亮:

3.3 V
  │
 LED(阳极在上)
  │
 1 kΩ
  │
PB5 / PE5(GPIO 灌电流)

GPIO=0:形成 3.3 V → LED → 电阻 → GPIO → GND 的电流,灯亮
GPIO=1:LED 两端压差接近 0,灯灭

这种“低有效”接法解释了为什么初始化输出高电平时灯默认熄灭。每 1 秒“翻转一次”表示电平每秒改变一次,完整的亮—灭周期是 2 秒;同理,LED2 每 2 秒翻转一次,完整闪烁周期是 4 秒。

串口发送比 GPIO 翻转慢得多,不应直接放在中断回调中。更合适的结构是中断只设置标志,主循环看到标志后再打印。

1.3.2 实验二

硬件 PWM 实验包含呼吸灯与直流电机调速。边沿对齐 PWM 的基本关系为:

f_PWM = f_TIM / [(PSC + 1) × (ARR + 1)]
占空比约为 CCRx / (ARR + 1)

呼吸灯通过缓慢改变 CCRx,让 LED 的平均电流逐渐变化。肉眼看不到快速开关,只会感到亮度渐变。

直流电机的工作电流和启停冲击电流通常远大于 GPIO 能承受的电流,不能把电机直接接到 STM32 引脚。常用的低边 MOSFET 驱动电路如下:

                    +Vmotor
                       │
                    [直流电机]
                       │───────|<|───────┐
                       │      续流二极管 │
                       │                 │
                       D                 │
STM32 PWM ──Rg── G  N-MOSFET             │
                │      S                 │
              Rpd      │                 │
                │      └──── GND ────────┘
               GND

STM32 GND 与电机电源 GND 必须共地。
二极管正常工作时反向截止;MOSFET 关断时为电机电感电流提供回路。

PWM 高电平使 MOSFET 导通,低电平使其关断。改变占空比会改变电机获得的平均电压和转矩。选管时还要确认逻辑电平栅极、电流能力、导通电阻和散热,而不是只看耐压。

1.3.3 实验三

输入捕获实验用 STM32 测量 51 单片机输出的舵机控制脉冲。配套 08IC 工程使用 TIM2 通道 2,对应 PA1;预分频值 71 把 72 MHz 定时器时钟变为 1 MHz,因此 CNT 每增加 1 代表 1 μs。

sequenceDiagram
    participant S as 51单片机舵机信号
    participant P as PA1/TIM2_CH2
    participant T as TIM2 CNT/CCR2
    participant C as 程序
    S->>P: 上升沿
    P->>T: 捕获当前CNT并切换为下降沿
    C->>T: 将CNT清零
    S->>P: 下降沿
    P->>T: 把CNT锁存到CCR2
    T->>C: 捕获中断,读取高电平宽度

舵机信号常以约 20 ms 为一帧,高电平宽度常见为 0.5 ms、1.0 ms、1.5 ms、2.0 ms、2.5 ms,对应不同目标角度。1.5 ms 是 1500 μs,不是 11500 μs。

硬件连接时,信号线接 PA1,两个控制板必须共地。STM32 只测量控制脉冲,不应把舵机电机电流引入 PA1。若计时期间 CNT 发生溢出,只读取单次 CCR 差值会得到错误结果;可靠实现需要统计更新溢出次数,或选择足够长的计数周期。08IC 使用 ARR=65535、1 μs 计数节拍,一个周期为 65.536 ms,足以覆盖正常的 0.5~2.5 ms 舵机高电平。

1.4 思维导图

mindmap
  root((STM32定时器))
    基本计时
      时钟源
      PSC预分频
      CNT计数
      ARR周期
      更新中断
    通用通道
      输入捕获
      输出比较
      PWM输出
      编码器接口
    高级控制
      互补输出
      死区
      刹车
      重复计数
    本章实验
      1秒定时与运行时间
      呼吸灯和电机PWM
      舵机脉宽测量

理解定时器时可以始终抓住两条主线:一条是“时钟经过 PSC 推动 CNT,CNT 与 ARR 决定周期”;另一条是“通道把 CNT 与引脚信号或 CCR 联系起来”。中断、PWM 和输入捕获,都是在这两条主线上增加不同的事件与硬件通路。