4. STM32定时器详解:从SysTick到PWM的完整指南
定时器这东西,说简单也简单,说复杂能写一本书。我在做嵌入式开发的头两年,其实没太当回事——不就是计个数、产生个中断嘛。直到有一次做一个电机控制项目,PWM频率稍微偏了一点,电机就开始嗡嗡叫,客户直接投诉了。嗯,从那以后,我老老实实把STM32的定时器体系啃了个透。
今天咱们就把这玩意儿彻底讲明白。你想想看,STM32里定时器家族其实分三拨:系统滴答定时器SysTick、基本定时器、通用定时器。它们各有各的脾气,但核心逻辑是相通的。
核心认知:所有定时器的本质就是一个不断累加的计数器,配合预分频器和自动重装载寄存器,实现各种时间相关的功能。
4.1 SysTick:系统的心脏起搏器
SysTick是Cortex-M内核自带的24位向下计数定时器。说白了,它就是给操作系统提供心跳的。就算你不跑RTOS,它也能当个精准的延时工具用。
我个人习惯在裸机项目里也用SysTick做时间基准。为什么?因为它不占用通用定时器资源,而且所有STM32芯片都有,移植性极好。
// SysTick配置示例:产生1ms中断
void SysTick_Init(void)
{
// 系统时钟72MHz,重装载值72000-1 = 1ms
if (SysTick_Config(SystemCoreClock / 1000))
{
while(1); // 配置失败,死循环
}
}
// 中断服务函数
void SysTick_Handler(void)
{
static uint32_t tick = 0;
tick++;
// 每1ms执行一次
}
我的经验:SysTick_Config()函数里会自动使能中断。如果你只想做轮询延时,记得手动关中断,否则会频繁进中断影响效率。
4.2 基本定时器:简单但够用
基本定时器(TIM6/TIM7)只有向上计数模式,没有外部引脚,不能输出PWM,也不能做输入捕获。那它有什么用?
我在一个温控项目里用过它——只需要每100ms触发一次ADC采样,基本定时器正好合适。资源占用少,配置也简单。
// 基本定时器TIM6配置,定时100ms
void TIM6_Init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM6EN; // 使能时钟
TIM6->PSC = 7199; // 预分频:72MHz/(7199+1) = 10KHz
TIM6->ARR = 999; // 自动重装载:10KHz/1000 = 10Hz = 100ms
TIM6->DIER |= TIM_DIER_UIE; // 使能更新中断
TIM6->CR1 |= TIM_CR1_CEN; // 启动定时器
NVIC_EnableIRQ(TIM6_IRQn);
}
注意:基本定时器的预分频器和计数器都是16位的。PSC最大65535,ARR最大65535。如果你要定时很长时间,得用中断里累加的方式。
4.3 通用定时器:功能最丰富的选手
通用定时器(TIM2~TIM5等)才是咱们用得最多的。它支持向上、向下、中央对齐三种计数模式。而且每个定时器有4个独立通道,可以配置成PWM输出、输入捕获、输出比较等。
我记得第一次用输入捕获测PWM脉宽时,折腾了一下午。后来发现是极性配置反了——上升沿捕获和下降沿捕获搞混了。这种坑,踩过一次就记住了。
4.4 PWM输出:从原理到实战
PWM的本质就是让定时器的计数器与比较寄存器的值不断比较。计数器小于比较值时输出高电平,大于时输出低电平。就这么简单。
但实际配置时有个关键点:极性。你想想看,如果有效电平是低电平,那占空比的计算方式就反过来了。
// 通用定时器TIM3,通道1输出PWM,频率1KHz,占空比50%
void TIM3_PWM_Init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN; // PB4为TIM3_CH1
GPIOB->CRL &= ~(0x0F << 16); // PB4配置为复用推挽输出
GPIOB->CRL |= (0x0B << 16);
TIM3->PSC = 71; // 72MHz/72 = 1MHz
TIM3->ARR = 999; // 1MHz/1000 = 1KHz
TIM3->CCR1 = 500; // 占空比50%
TIM3->CCMR1 |= (6 << 4); // PWM模式1
TIM3->CCER |= TIM_CCER_CC1E; // 使能输出
TIM3->CR1 |= TIM_CR1_CEN; // 启动
}
关键公式:
- PWM频率 = 定时器时钟 / (PSC+1) / (ARR+1)
- 占空比 = CCR / (ARR+1) × 100%
- 分辨率 = 1 / (ARR+1) × 100%
4.5 输入捕获:测量外部信号的脉宽
输入捕获的原理是:当检测到指定边沿(上升沿或下降沿)时,把当前计数器的值锁存到捕获寄存器里。两次捕获的值一减,就是脉宽。
我曾经用这个功能测量一个旋转编码器的转速。编码器每转一圈输出一个脉冲,捕获两个脉冲之间的时间间隔,就能算出转速。精度比单纯用中断高得多。
// 输入捕获配置:捕获上升沿
void TIM2_InputCapture_Init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 71; // 1MHz计数频率
TIM2->ARR = 0xFFFF; // 最大计数值
TIM2->CCMR1 |= (0x01 << 0); // 通道1配置为输入,映射到TI1
TIM2->CCER |= TIM_CCER_CC1E; // 使能捕获
TIM2->CCER &= ~TIM_CCER_CC1P; // 上升沿捕获
TIM2->DIER |= TIM_DIER_CC1IE; // 使能捕获中断
TIM2->CR1 |= TIM_CR1_CEN;
NVIC_EnableIRQ(TIM2_IRQn);
}
// 中断中读取捕获值
void TIM2_IRQHandler(void)
{
if(TIM2->SR & TIM_SR_CC1IF)
{
uint16_t capture = TIM2->CCR1;
// 处理捕获值
TIM2->SR = ~TIM_SR_CC1IF; // 清除标志
}
}
避坑指南:我曾经在捕获高频信号时发现数据总是不对。后来查手册才发现,定时器的输入频率不能超过APB时钟的一半。这是个硬件限制,软件怎么调都没用。
4.6 定时器中断应用:不止是延时
定时器中断最常见的用法就是做周期性任务调度。但我想说的是,别把所有事情都塞进中断里。中断服务函数应该短小精悍,只做标志位设置或数据搬运。
我见过一个同事,在定时器中断里做浮点运算、调用printf、甚至操作Flash。结果系统动不动就卡死。中断里只该做三件事:读寄存器、写寄存器、置标志位。
| 定时器类型 | 位数 | 计数模式 | PWM | 输入捕获 | 典型应用 |
|---|---|---|---|---|---|
| SysTick | 24 | 向下 | ✗ | ✗ | OS心跳、延时 |
| 基本定时器 | 16 | 向上 | ✗ | ✗ | 基础定时 |
| 通用定时器 | 16 | 上/下/中央 | ✓ | ✓ | PWM、捕获、编码器 |
| 高级定时器 | 16 | 上/下/中央 | ✓ | ✓ | 电机控制、死区插入 |
嗯,定时器这部分内容确实不少。但只要你理解了计数器的核心逻辑——预分频器降频、计数器累加、比较器触发动作——剩下的都是围绕这个核心的变种。我在实际项目中,80%的定时器需求用通用定时器就能搞定。剩下的20%,要么是SysTick做延时,要么是高级定时器做电机控制。
最后说一句:多看芯片参考手册的定时器章节。网上很多教程都是抄来抄去,寄存器配置经常写错。手册虽然厚,但寄存器描述是最准确的。
本章要点回顾:
- SysTick是内核定时器,24位向下计数,适合做时间基准
- 基本定时器功能简单,适合纯定时场景
- 通用定时器功能最全,PWM和输入捕获是核心应用
- PWM频率由PSC和ARR决定,占空比由CCR决定
- 输入捕获测量脉宽,注意极性配置和信号频率限制
- 中断服务函数要短小,只做必要操作
公众号:蓝海资料掘金营,微信deep3321