4. 定时器基础与应用:SysTick、PWM与输入捕获
定时器这东西,可以说是STM32里最常用的外设之一了。我刚开始做嵌入式那会儿,总觉得定时器不就是个计数器嘛,有啥好研究的?后来做项目多了才发现,用得好不好,直接决定了系统的实时性和稳定性。这一章咱们就把三个最核心的定时器应用讲透:SysTick系统滴答、通用定时器的PWM输出、还有输入捕获测频率。
- SysTick的工作原理与配置方法
- 通用定时器产生PWM信号的完整流程
- 输入捕获测量外部信号频率
- 实际项目中的避坑经验
4.1 SysTick系统滴答定时器
SysTick是Cortex-M内核自带的24位递减计数器。说白了,它就是系统的心跳。你想想看,如果没有这个心跳,操作系统怎么调度任务?延时函数怎么实现?
我个人习惯把SysTick配置成1ms中断一次。为什么是1ms?因为大多数RTOS的时间片轮转都是以ms为单位的,而且1ms的精度对大部分应用场景来说足够了。
// SysTick配置示例 - 1ms中断
void SysTick_Init(void)
{
// 重装载值 = 系统时钟 / 1000 - 1
// 假设系统时钟72MHz
SysTick->LOAD = 72000 - 1;
// 选择内核时钟源,使能中断,启动定时器
SysTick->CTRL |= SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
}
// SysTick中断服务函数
void SysTick_Handler(void)
{
// 每1ms执行一次
// 可以在这里做系统节拍计数
static uint32_t tick = 0;
tick++;
}
我曾经在一个项目中遇到过一个坑:SysTick中断里做了太多事情,导致中断执行时间超过了1ms。结果就是系统节拍丢失,延时函数不准,整个系统时序全乱了。嗯,这里要注意:SysTick中断里只做最轻量的事情,比如计数、置标志位,千万别在里面做复杂运算或者调用printf。
4.2 通用定时器PWM输出
PWM输出是通用定时器最常用的功能之一。控制电机转速、调节LED亮度、驱动舵机,都离不开它。STM32的通用定时器可以同时输出多路PWM,而且每路的占空比可以独立调节。
我画了一张图,帮你理清PWM输出的核心逻辑:
配置PWM输出其实就几个步骤:
- 使能定时器时钟 - 别漏了GPIO的时钟也要使能
- 配置GPIO为复用推挽输出 - 注意要选对复用功能号
- 设置预分频器和自动重装载值 - 决定PWM频率
- 配置输出比较模式 - 常用PWM模式1或模式2
- 使能定时器 - 开始输出PWM信号
// TIM2 PWM输出配置示例 - 1kHz, 50%占空比
void TIM2_PWM_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 1. 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 2. 配置PA0为TIM2_CH1复用输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 配置定时器时基
TIM_TimeBaseStructure.TIM_Period = 999; // ARR
TIM_TimeBaseStructure.TIM_Prescaler = 71; // PSC
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 4. 配置PWM模式1
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 500; // CCR = 500 → 50%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 5. 使能定时器
TIM_Cmd(TIM2, ENABLE);
}
- 高级定时器(TIM1/TIM8)需要额外使能刹车和死区寄存器
- 修改占空比时,直接写CCR寄存器即可,不用重新初始化
- PWM频率不要超过GPIO的翻转速度,一般50MHz的GPIO能输出几MHz的PWM
4.3 输入捕获测量频率
输入捕获是定时器的另一个重要功能。它可以测量外部信号的频率、周期、脉宽。原理其实很简单:当检测到指定的边沿(上升沿或下降沿)时,定时器会把当前的计数值锁存到捕获寄存器里。
我做过一个项目,需要测量一个传感器的输出频率。当时用了输入捕获,配合DMA,实现了连续采样。效果还不错,精度能达到0.1Hz。
测量频率有两种常用方法:
| 方法 | 原理 | 适用场景 | 精度 |
|---|---|---|---|
| 周期测量法 | 测量两个上升沿之间的计数值差 | 低频信号(<100kHz) | 高(低频时) |
| 频率测量法 | 固定时间内计数脉冲个数 | 高频信号(>100kHz) | 高(高频时) |
周期测量法的实现步骤:
- 配置定时器的输入捕获通道,选择上升沿触发
- 使能捕获中断
- 在中断里读取CCR值,计算两次捕获的差值
- 频率 = 定时器时钟频率 / (差值 × 预分频系数)
// 输入捕获配置 - 测量信号频率
uint32_t g_cap1 = 0, g_cap2 = 0;
uint8_t g_cap_flag = 0;
void TIM3_Cap_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM_ICInitStructure;
// 使能时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 配置PA6为输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 定时器时基配置
TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 1MHz计数频率
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// 输入捕获配置 - 通道1,上升沿
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0;
TIM_ICInit(TIM3, &TIM_ICInitStructure);
// 使能捕获中断
TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE);
NVIC_EnableIRQ(TIM3_IRQn);
TIM_Cmd(TIM3, ENABLE);
}
// 中断服务函数
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3, TIM_IT_CC1) != RESET)
{
TIM_ClearITPendingBit(TIM3, TIM_IT_CC1);
if(g_cap_flag == 0)
{
g_cap1 = TIM_GetCapture1(TIM3);
g_cap_flag = 1;
}
else
{
g_cap2 = TIM_GetCapture1(TIM3);
g_cap_flag = 0;
// 计算频率
uint32_t diff;
if(g_cap2 > g_cap1)
diff = g_cap2 - g_cap1;
else
diff = (0xFFFF - g_cap1) + g_cap2;
// 频率 = 1MHz / diff
uint32_t freq = 1000000 / diff;
// 这里可以处理freq值
}
}
}
- 输入捕获的滤波器可以滤除毛刺,但会增加延迟。我一般设成4-8个采样点
- 如果信号频率很低(比如几Hz),定时器可能会溢出。这时候要配合溢出中断来处理
- 测量高频信号时,预分频器要设小一点,不然计数精度不够
4.4 实战中的避坑指南
做定时器相关的项目,有几个坑我踩过,分享给你:
- 定时器时钟源搞错 - APB1和APB2的时钟频率可能不同。APB1上的定时器最高36MHz,APB2上的可以到72MHz。我刚开始就吃过这个亏,算出来的频率总是不对。
- PWM输出极性搞反 - 高电平有效还是低电平有效?这取决于你的外设。驱动LED的话,高电平有效没问题。但驱动某些电机驱动器,可能需要低电平有效。
- 输入捕获的边沿选择 - 测量脉宽需要同时捕获上升沿和下降沿。可以用两个通道分别捕获,也可以用同一个通道切换边沿。
- 中断优先级 - 如果系统里同时有多个中断,定时器中断的优先级要设好。我一般把SysTick设成最高优先级,其他定时器中断根据实时性要求来排。