在嵌入式开发领域,定时器就像系统的心跳节拍器。作为STM32单片机最基础的外设模块之一,定时器承担着时间基准生成、PWM输出、输入捕获等关键功能。我接触过的实际项目中,90%以上的STM32应用都会用到定时器功能,从简单的LED闪烁到复杂的电机控制都离不开它。
基础定时器(Basic Timer)是STM32定时器家族中最精简的成员,典型代表是TIM6和TIM7(型号不同可能有差异)。与高级定时器相比,它没有PWM生成和输入捕获这些花哨功能,但正因结构简单,反而成为学习定时器原理的最佳切入点。就像学开车先练手动挡一样,掌握基础定时器后,再接触其他复杂定时器会轻松很多。
基础定时器的核心是一个16位向上计数器,其时钟来源通常是APB1总线。这里有个关键细节:当APB1预分频系数不为1时,定时器时钟会倍频。例如APB1时钟为42MHz,预分频系数为2时,定时器实际时钟是84MHz。这个特性手册里不会特别强调,但直接影响定时精度计算。
计数器的运作可以想象成水桶接水滴:时钟信号是水滴,预分频器控制水滴间隔,自动重装载寄存器(ARR)决定水桶容量。当水滴装满水桶(计数器值=ARR),就会产生溢出事件,同时计数器归零重新计数。这个简单的机制就是所有定时功能的基础。
以TIM6为例,开发中最常操作的寄存器有:
这里有个实用技巧:修改ARR和PSC时,建议先关闭定时器或设置UG位手动触发更新事件,避免寄存器值不同步导致的计时异常。我在早期项目中就遇到过因为ARR修改时机不当导致定时周期紊乱的问题。
使用STM32CubeMX配置基础定时器时,这几个参数需要特别注意:
一个典型1ms定时配置示例(APB1时钟84MHz):
对于追求极致效率的场景,可以直接操作寄存器:
c复制void TIM6_Init(void)
{
// 1. 使能TIM6时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM6EN;
// 2. 配置预分频器(产生10kHz时钟)
TIM6->PSC = 8399; // 84MHz / 8400 = 10kHz
// 3. 设置自动重装载值(1ms中断)
TIM6->ARR = 9; // 10kHz / 10 = 1ms
// 4. 使能更新中断
TIM6->DIER |= TIM_DIER_UIE;
// 5. 启动定时器
TIM6->CR1 |= TIM_CR1_CEN;
// 6. 配置NVIC
NVIC_EnableIRQ(TIM6_DAC_IRQn);
NVIC_SetPriority(TIM6_DAC_IRQn, 1);
}
关键提示:直接操作寄存器时,务必查阅参考手册确认寄存器地址和位域。我曾遇到过因为错用保留位导致定时器无法启动的坑。
一个完整的中断服务程序应该包含:
c复制void TIM6_DAC_IRQHandler(void)
{
if(TIM6->SR & TIM_SR_UIF)
{
TIM6->SR &= ~TIM_SR_UIF; // 清除中断标志
// 用户代码区
GPIOA->ODR ^= GPIO_ODR_OD13; // 翻转PA13(LED)
}
}
在要求严格的实时系统中,中断响应时间很关键。通过以下方法可以优化:
__attribute__((section(".ramfunc"))))实测数据显示,优化后的中断响应时间可以从约1.2μs缩短到0.7μs(Cortex-M4内核@168MHz)。对于需要精确时间戳的应用,这个优化非常有必要。
不使用中断的微秒级延时函数:
c复制void delay_us(uint16_t us)
{
TIM6->CNT = 0;
while(TIM6->CNT < us);
}
使用时需确保:
虽然基础定时器没有硬件PWM输出,但可以通过中断模拟:
c复制volatile uint32_t pwm_ticks = 0;
volatile uint32_t pwm_duty = 30; // 占空比30%
void TIM6_DAC_IRQHandler(void)
{
static uint32_t counter = 0;
if(TIM6->SR & TIM_SR_UIF)
{
TIM6->SR &= ~TIM_SR_UIF;
counter++;
if(counter >= 100) counter = 0;
if(counter < pwm_duty) {
GPIOA->BSRR = GPIO_BSRR_BS13; // 输出高
} else {
GPIOA->BSRR = GPIO_BSRR_BR13; // 输出低
}
}
}
这种方法的PWM频率=定时器中断频率/100。虽然不如硬件PWM精确,但在IO口紧张时很实用。
时钟检查:
配置检查:
中断检查:
遇到定时不准时,重点检查:
一个快速验证方法:用定时器翻转IO,用逻辑分析仪测量实际周期。我在调试中发现,当APB1分频系数为2时,如果不考虑时钟倍频,定时误差会达到100%。
将多个定时器级联可以扩展计数范围。例如TIM6作为主定时器,溢出信号触发TIM7:
c复制// TIM6配置(1MHz,ARR=999)
TIM6->PSC = 83;
TIM6->ARR = 999;
TIM6->CR2 |= TIM_CR2_MMS_1; // 主模式:更新事件作为触发输出
// TIM7配置(从模式)
TIM7->SMCR |= TIM_SMCR_SMS_2; // 触发从模式
TIM7->ARR = 0xFFFF; // 最大计数值
这样组合可实现32位定时器功能,累计计数可达(1000×65535)个时钟周期。
在STOP模式下,定时器通常停止工作。但通过以下方式可以保持定时:
实测数据:普通定时器在STOP模式下的唤醒重启时间约20μs,而LPTIM仅需3μs。对电池供电设备,这个差异直接影响续航。