定时器(Timer)¶
定时器是 STM32 中功能最强大、应用最广泛的外设之一。从简单的延时计数,到 PWM 波形输出驱动电机,再到输入捕获测量脉冲宽度,定时器无处不在。
一、定时器分类¶
STM32F103 共有 8 个定时器,分为三类:
| 类型 | 定时器 | 位数 | 总线 | 主要功能 |
|---|---|---|---|---|
| 基本定时器 | TIM6, TIM7 | 16 位 | APB1 | 定时、触发 DAC |
| 通用定时器 | TIM2, TIM3, TIM4, TIM5 | 16 位 | APB1 | 定时、PWM 输出、输入捕获、编码器 |
| 高级定时器 | TIM1, TIM8 | 16 位 | APB2 | 通用定时器全部功能 + 互补输出、死区控制、刹车 |
graph TB
TIM[STM32 定时器] --> BASIC[基本定时器<br>TIM6/7]
TIM --> GENERAL[通用定时器<br>TIM2/3/4/5]
TIM --> ADVANCED[高级定时器<br>TIM1/8]
BASIC --> B1[定时中断]
BASIC --> B2[触发 DAC]
GENERAL --> G1[定时中断]
GENERAL --> G2[PWM 输出]
GENERAL --> G3[输入捕获]
GENERAL --> G4[编码器接口]
ADVANCED --> A1[通用定时器全部功能]
ADVANCED --> A2[互补 PWM + 死区]
ADVANCED --> A3[刹车输入]
二、定时器的核心概念¶
计数器基本原理¶
定时器的本质就是一个 自动计数的计数器:
- 每来一个时钟脉冲,计数器 +1(向上计数时)
- 当计数到设定值(自动重装载值 ARR)时,产生一个 更新事件
- 计数器清零,重新开始计数
三个关键参数¶
| 参数 | 寄存器 | 含义 |
|---|---|---|
| 预分频器 | PSC(Prescaler) | 对输入时钟进行分频,决定计数频率 |
| 自动重装载值 | ARR(Auto-Reload Register) | 计数器的上限值,到达后产生更新事件 |
| 计数器 | CNT(Counter) | 当前计数值 |
定时时间计算¶
其中 \(F_{\text{CLK}}\) 是定时器的输入时钟频率。
计算举例
STM32F103 的 TIM2 挂在 APB1 总线上,经过倍频后时钟为 72MHz。
如果我们想让定时器每 1 秒 触发一次中断:
取 \(PSC = 7199\), \(ARR = 9999\):
或者 \(PSC = 71\), \(ARR = 999999\)... 但 ARR 是 16 位寄存器(最大 65535),所以这个组合不可行!
PSC 和 ARR 的取值技巧
- 先用 PSC 把时钟降到一个"好算"的频率(如 10kHz),再用 ARR 设定计数次数
- 例如 72MHz / (7199+1) = 10kHz,然后 ARR = 9999 → 每 1 秒溢出一次
- PSC 和 ARR 的值都要在 0~65535 范围内
计数模式¶
| 模式 | 计数方向 | 溢出条件 | 典型应用 |
|---|---|---|---|
| 向上计数 | 0 → ARR | CNT == ARR 时溢出 | 最常用 |
| 向下计数 | ARR → 0 | CNT == 0 时溢出 | 较少用 |
| 中央对齐 | 0 → ARR → 0 | 到达 ARR 和 0 时都溢出 | 产生对称 PWM |
三、定时中断(CubeMX + HAL)¶
最基本的定时器应用——每隔固定时间触发一次中断。
CubeMX 配置步骤¶
- Timers → TIM2
- Clock Source: Internal Clock
- Prescaler: 7200 - 1(即 7199)
- Counter Period: 5000 - 1(即 4999)
- Counter Mode: Up
- auto-reload preload: Enable
- NVIC Settings 标签页中勾选 TIM2 global interrupt,设置优先级
- 生成代码
代码示例:每 500ms 翻转 LED¶
CubeMX 会在 tim.c 中生成 MX_TIM2_Init() 函数。你需要做两件事:
1. 在 main.c 中启动定时器中断
2. 实现回调函数
/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM2) // 确认是 TIM2
{
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); // 翻转 LED
}
}
/* USER CODE END 4 */
HAL 定时器中断流程
graph LR
A["TIM2_IRQHandler()<br>(stm32f1xx_it.c)"] --> B["HAL_TIM_IRQHandler()"]
B --> C["检查 & 清除标志"]
C --> D["HAL_TIM_PeriodElapsedCallback()<br>(你来实现)"]
你不需要手动清除中断标志位,HAL 已经处理了。
四、PWM 输出¶
什么是 PWM?¶
PWM(Pulse Width Modulation,脉冲宽度调制)是一种用数字信号模拟模拟量的技术:
- 通过高速切换高低电平,控制平均电压
- 占空比越大,平均电压越高
其中 \(D\) 是占空比(Duty Cycle),范围 0% ~ 100%。
PWM 的实际效果
3.3V 的 GPIO 输出 50% 占空比的 PWM → 等效 1.65V → LED 变暗一半
3.3V 的 GPIO 输出 10% 占空比的 PWM → 等效 0.33V → LED 很暗
PWM 模式¶
定时器通过比较 CNT 和 CCR(Capture/Compare Register,捕获比较值)来产生 PWM:
| PWM 模式 | CNT < CCR 时 | CNT ≥ CCR 时 |
|---|---|---|
| PWM 模式 1 | 有效电平(高) | 无效电平(低) |
| PWM 模式 2 | 无效电平(低) | 有效电平(高) |
PWM 模式 1(向上计数):
┌──────┐ ┌──────┐
高 │ │ │ │
│ │ │ │
低 ──┘ └──────────┘ └──────
0 CCR ARR 0 CCR ARR
占空比 = CCR / (ARR + 1)
PWM 关键公式¶
CubeMX 配置 PWM¶
以 TIM3 CH1(PA6)输出 1kHz PWM 为例:
- Timers → TIM3
- Clock Source: Internal Clock
- Channel1: PWM Generation CH1
- Parameter Settings:
- Prescaler: 72 - 1(72MHz / 72 = 1MHz 计数频率)
- Counter Period: 1000 - 1(1MHz / 1000 = 1kHz PWM)
- Pulse(CCR): 0(初始占空比 0%)
- Mode: PWM mode 1
- CH Polarity: High
- PA6 会自动映射为 TIM3_CH1
- 生成代码
PWM 代码示例:呼吸灯¶
/* main.c */
/* USER CODE BEGIN 2 */
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 启动 PWM 输出
/* USER CODE END 2 */
uint16_t duty = 0;
uint8_t direction = 1;
/* USER CODE BEGIN WHILE */
while (1)
{
if (direction)
{
duty += 5;
if (duty >= 1000) direction = 0;
}
else
{
duty -= 5;
if (duty == 0) direction = 1;
}
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, duty); // 修改占空比
HAL_Delay(5);
/* USER CODE END WHILE */
}
PWM 驱动舵机¶
舵机通常需要 50Hz 的 PWM 信号,通过脉宽控制角度:
| 脉宽 | 角度 | CCR(ARR=19999 时) |
|---|---|---|
| 0.5ms | 0° | 500 |
| 1.0ms | 45° | 1000 |
| 1.5ms | 90° | 1500 |
| 2.0ms | 135° | 2000 |
| 2.5ms | 180° | 2500 |
CubeMX 配置:PSC = 72-1,ARR = 20000-1 → 50Hz
// 启动 PWM
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
// 设置舵机角度
__HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 1500); // 90°
五、输入捕获¶
什么是输入捕获?¶
输入捕获用于测量外部信号的参数:
- 脉冲频率:两个上升沿之间的时间
- 脉冲宽度:一个上升沿到下一个下降沿之间的时间
- 占空比:高电平时间 / 周期
工作原理¶
- 配置定时器通道为输入捕获模式
- 当检测到指定边沿(上升/下降)时,硬件自动将当前 CNT 值锁存到 CCR
- 通过两次捕获的 CCR 差值计算时间
信号: ┌──────┐ ┌──────┐
高 │ │ │ │
│ │ │ │
低 ──┘ └──────────┘ └──────
↑ ↑ ↑
捕获1 捕获2 捕获3
CCR1 CCR2 CCR3
频率 = F_CNT / (CCR3 - CCR1)
脉宽 = (CCR2 - CCR1) / F_CNT
CubeMX 配置输入捕获¶
以 TIM3 CH1(PA6)捕获上升沿为例:
- Timers → TIM3
- Clock Source: Internal Clock
- Channel1: Input Capture direct mode
- Parameter Settings:
- Prescaler: 72 - 1(计数频率 1MHz → 1μs 精度)
- Counter Period: 65535(最大计数范围)
- Polarity Selection: Rising Edge
- IC Filter: 15(滤波)
- NVIC 中勾选 TIM3 global interrupt
- 生成代码
代码示例:测量信号频率¶
/* main.c */
volatile uint32_t capture1 = 0, capture2 = 0;
volatile uint8_t capture_done = 0;
/* USER CODE BEGIN 2 */
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1); // 启动输入捕获中断
/* USER CODE END 2 */
/* USER CODE BEGIN 4 */
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3 && htim->Channel == HAL_TIM_ACTIVE_CHANNEL_1)
{
if (capture_done == 0)
{
capture1 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
capture_done = 1;
}
else
{
capture2 = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
capture_done = 2; // 两次捕获完成
// 可选:停止捕获
HAL_TIM_IC_Stop_IT(&htim3, TIM_CHANNEL_1);
}
}
}
/* USER CODE END 4 */
/* 在主循环中计算频率 */
/* USER CODE BEGIN WHILE */
while (1)
{
if (capture_done == 2)
{
uint32_t diff;
if (capture2 >= capture1)
diff = capture2 - capture1;
else
diff = (65536 + capture2) - capture1; // 处理溢出
float freq = 1000000.0f / diff; // 计数频率 1MHz → 单位是 μs
capture_done = 0;
// 重新启动捕获
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);
}
/* USER CODE END WHILE */
}
六、HAL 定时器常用函数速查¶
| 函数 | 功能 |
|---|---|
HAL_TIM_Base_Start() |
启动定时器(无中断) |
HAL_TIM_Base_Start_IT() |
启动定时器并使能更新中断 |
HAL_TIM_Base_Stop_IT() |
停止定时器并禁止更新中断 |
HAL_TIM_PWM_Start() |
启动 PWM 输出 |
HAL_TIM_PWM_Stop() |
停止 PWM 输出 |
HAL_TIM_IC_Start_IT() |
启动输入捕获(中断模式) |
HAL_TIM_IC_Stop_IT() |
停止输入捕获 |
__HAL_TIM_SET_COMPARE() |
修改 CCR 值(改变占空比) |
__HAL_TIM_SET_AUTORELOAD() |
修改 ARR 值 |
__HAL_TIM_SET_PRESCALER() |
修改 PSC 值 |
HAL_TIM_ReadCapturedValue() |
获取捕获值 |
常用回调函数¶
| 回调函数 | 触发时机 |
|---|---|
HAL_TIM_PeriodElapsedCallback() |
更新事件(定时中断) |
HAL_TIM_IC_CaptureCallback() |
输入捕获完成 |
HAL_TIM_PWM_PulseFinishedCallback() |
PWM 脉冲结束 |
HAL_TIM_OC_DelayElapsedCallback() |
输出比较匹配 |
七、常见问题¶
定时器的时钟频率怎么确定?
- TIM2/3/4/5 挂在 APB1 上。APB1 预分频 ≠ 1 时,定时器时钟 = APB1 × 2 = 36MHz × 2 = 72MHz
- TIM1/8 挂在 APB2 上,时钟同样为 72MHz
- 结论:在默认时钟配置下(CubeMX 默认配好),所有定时器的时钟都是 72MHz
PWM 频率和分辨率如何权衡?
PWM 频率 = 72MHz / (PSC+1) / (ARR+1)。ARR 越大,占空比的调节精度(分辨率)越高,但 PWM 频率越低。需要根据应用场景权衡:
- LED 调光:1kHz 就够,ARR 可以设大些
- 电机驱动:通常 10~20kHz(避免听到电机的啸叫声)
- 高速通信:可能需要 MHz 级别
为什么在 CubeMX 中 Preload 建议 Enable?
不使能预装载时,修改 ARR 或 CCR 会立即生效,可能导致在一个 PWM 周期中出现异常波形。使能预装载后,新值会在下一个更新事件时才加载,保证波形完整。
HAL_TIM_Base_Start vs HAL_TIM_Base_Start_IT?
HAL_TIM_Base_Start():只启动计数器,不产生中断。用于 PWM 输出、编码器等不需要中断的场景HAL_TIM_Base_Start_IT():启动计数器 + 使能更新中断。用于需要定时中断的场景