在嵌入式系统和微控制器应用中,定时器(Timer)堪称最基础却又最核心的外设模块之一。我从业十余年来,从8位单片机到32位ARM Cortex-M系列,几乎没有一个项目能完全避开定时器的使用。简单来说,定时器就是芯片内部的"秒表"——它通过计数时钟脉冲来实现精确的时间测量和控制。但与普通秒表不同的是,现代微控制器的定时器模块往往集成了中断触发、PWM生成、输入捕获等高级功能。
以STM32的TIM模块为例,一个定时器通常包含以下核心组件:
关键提示:预分频器和自动重装载寄存器的配合使用,是精确控制定时周期的核心所在。前者决定"计数步长",后者设定"计数上限"。
在实际项目中,定时器最常见的两大应用场景正是标题中提到的:
以STM32F4系列的标准定时器为例,配置定时中断需要关注以下寄存器:
c复制// 典型初始化代码片段
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 8399; // 84MHz/(8399+1)=10kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999; // (999+1)/10kHz=100ms
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
这里有几个关键计算点:
避坑指南:STM32的预分频器和周期寄存器实际生效值都是写入值+1,这是新手最容易忽略的细节。比如写入8399实际分频系数是8400。
完成基础配置后,还需启用定时器中断并编写中断服务例程:
c复制// 中断使能
HAL_TIM_Base_Start_IT(&htim2);
// 中断服务函数
void TIM2_IRQHandler(void) {
HAL_TIM_IRQHandler(&htim2);
}
// 回调函数重写
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
if(htim->Instance == TIM2) {
// 用户处理代码
LED_Toggle();
}
}
实测中发现的几个关键点:
在要求严格的定时应用中,我们需要考虑以下因素来提升精度:
时钟树配置:
中断延迟补偿:
c复制void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
uint32_t latency = TIM2->CNT; // 读取中断发生后的计数值
// 根据latency值进行补偿计算
}
定时器级联:
当需要测量外部信号频率或计数外部脉冲时,需要将定时器配置为外部时钟模式。STM32支持多种外部时钟源输入方式:
外部触发输入(ETR):
从模式选择:
c复制// 外部时钟模式1配置示例
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_ETRMODE2;
sClockSourceConfig.ClockPolarity = TIM_CLOCKPOLARITY_NONINVERTED;
sClockSourceConfig.ClockPrescaler = TIM_CLOCKPRESCALER_DIV1;
sClockSourceConfig.ClockFilter = 0;
HAL_TIM_ConfigClockSource(&htim2, &sClockSourceConfig);
// 配置GPIO为外部触发输入
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
利用外部时钟模式实现频率测量的两种经典方法:
输入捕获法:
测周法:
c复制// 输入捕获模式配置示例
TIM_IC_InitTypeDef sConfigIC = {0};
sConfigIC.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 0;
HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);
实测经验:
PWM输入是外部时钟模式的特例,可以同时测量PWM信号的周期和占空比:
c复制// PWM输入模式配置
TIM_SlaveConfigTypeDef sSlaveConfig = {0};
sSlaveConfig.SlaveMode = TIM_SLAVEMODE_RESET;
sSlaveConfig.InputTrigger = TIM_TS_TI1FP1;
HAL_TIM_SlaveConfigSynchronization(&htim3, &sSlaveConfig);
根据多年调试经验,整理出定时器应用的"故障树":
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 无中断触发 | 中断未使能/NVIC未配置 | 检查__HAL_TIM_ENABLE_IT()和HAL_NVIC_EnableIRQ() |
| 定时不准 | 时钟源配置错误 | 检查RCC时钟树配置 |
| 外部时钟不计数 | GPIO复用功能未启用 | 检查GPIO_InitStruct.Alternate设置 |
| 捕获值异常 | 输入信号抖动 | 调整ICFilter参数增加滤波 |
在电池供电应用中,定时器的低功耗配置尤为关键:
c复制// 进入STOP模式并等待定时器唤醒
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 唤醒后需要重新初始化时钟
SystemClock_Config();
经过多个项目的验证,我总结出以下定时器使用的最佳实践:
资源规划:
代码架构:
调试技巧:
跨平台考量:
在最近的一个工业传感器项目中,我们使用TIM3产生1ms基准中断用于数据采集,TIM4配置为外部时钟模式测量转速信号,TIM2用于生成PWM驱动电机。这种多定时器协同工作的架构,经过精心设计的中断优先级管理,最终实现了精确的时间控制,采样周期抖动小于10μs。