4. 定时器基础:SysTick系统滴答定时器、基本定时器、通用定时器、PWM输出
定时器,说白了就是单片机的“心跳”。没有它,你连个精准的延时都做不了,更别提什么PWM调速、捕获编码器信号了。我刚开始学STM32那会儿,总觉得定时器就是用来计数的,后来才发现——这玩意儿的水深着呢。
今天咱们就把STM32的定时器家族捋一遍。从最简单的SysTick,到基本定时器,再到功能最全的通用定时器,最后用PWM输出做个实战。嗯,跟着我的节奏来,保证你学完就能上手。
本章核心脉络:SysTick(系统心跳)→ 基本定时器(纯计时)→ 通用定时器(多功能)→ PWM输出(实战应用)。层层递进,每个模块都配有代码示例。
4.1 SysTick:系统的心跳
SysTick,全称System Tick Timer,是Cortex-M内核自带的24位递减计数器。说白了,它就是给操作系统用的——FreeRTOS、uC/OS这些RTOS,全靠它来产生时间片。
就算你不跑系统,SysTick也能当个精准延时器用。我习惯在裸机项目里用它做“软定时器”,比如按键消抖、LED闪烁,比HAL_Delay灵活多了。
个人习惯:SysTick的优先级我一般设成最低(15),这样不会被其他中断频繁打断。但如果你跑RTOS,记得把SysTick优先级设成最高——否则系统调度会出问题。
4.1.1 SysTick寄存器
就三个寄存器,简单得很:
| 寄存器 | 地址 | 功能 |
|---|---|---|
| STK_CTRL | 0xE000E010 | 控制寄存器:使能、时钟源、中断使能 |
| STK_LOAD | 0xE000E014 | 重装载值(24位,最大16,777,215) |
| STK_VAL | 0xE000E018 | 当前计数值(读清零,写清零) |
4.1.2 实战:1ms SysTick中断
假设系统时钟72MHz,我们让SysTick每1ms中断一次:
// SysTick配置函数
void SysTick_Init(void)
{
// 1. 设置时钟源为系统时钟(72MHz)
// 2. 计算重装载值:72MHz / 1000 = 72000
// 3. 使能中断,使能计数器
SysTick->CTRL = 0; // 先关闭
SysTick->LOAD = 72000 - 1; // 重装载值
SysTick->VAL = 0; // 清空计数器
SysTick->CTRL = 0x07; // 使能+中断+系统时钟
}
// 中断服务函数(在startup_stm32f10x_md.s中声明)
void SysTick_Handler(void)
{
static uint32_t tick = 0;
tick++;
// 每1000ms翻转一次LED
if(tick % 1000 == 0)
{
GPIO_ToggleBits(GPIOB, GPIO_Pin_0);
}
}
我曾经踩过的坑:SysTick的LOAD寄存器是24位的,最大只能装16,777,215。如果你系统时钟是168MHz,想产生1s中断——168,000,000 > 16,777,215,直接溢出!解决办法:要么用HSE分频,要么级联另一个定时器。
4.2 基本定时器:最简单的时基
基本定时器(TIM6、TIM7),功能少得可怜——只能向上计数,没有外部引脚,不能输出PWM。但你别小看它,它可是DAC的触发源,也是其他定时器的级联基础。
我有个项目需要产生一个精确的1kHz时基,用SysTick太浪费,用通用定时器又杀鸡用牛刀——基本定时器刚刚好。
4.2.1 基本定时器结构
- 时钟源:来自APB1总线(通常36MHz或72MHz)
- 预分频器(PSC):16位,分频系数1~65536
- 自动重装载寄存器(ARR):16位,最大65535
- 计数器(CNT):向上计数,从0到ARR
计算公式:定时时间 = (PSC + 1) × (ARR + 1) / 定时器时钟频率
4.2.2 配置示例:1s定时中断
void TIM6_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6, ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
// 72MHz / (7200-1+1) = 10kHz → 0.1ms
TIM_TimeBaseStructure.TIM_Prescaler = 7200 - 1;
// 10kHz / 10000 = 1Hz → 1s
TIM_TimeBaseStructure.TIM_Period = 10000 - 1;
TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);
TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);
TIM_Cmd(TIM6, ENABLE);
// 别忘了在NVIC里使能中断
NVIC_EnableIRQ(TIM6_IRQn);
}
void TIM6_IRQHandler(void)
{
if(TIM_GetITStatus(TIM6, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM6, TIM_IT_Update);
// 1s到了,做你想做的事
}
}
关键点:基本定时器没有影子寄存器(预装载寄存器),修改ARR或PSC会立即生效。如果你在运行时动态改变周期,可能会产生不完整的计数周期——我建议在更新事件发生后修改。
4.3 通用定时器:瑞士军刀
通用定时器(TIM2~TIM5,部分型号还有TIM9~TIM14),功能就丰富多了。它除了能做时基,还能输出PWM、输入捕获、编码器接口、霍尔传感器接口……
你想想看,一个外设能干这么多活,是不是很划算?我做过一个四轴飞行器项目,一个TIM2同时干三件事:PWM驱动电机、捕获遥控器信号、做系统时基——省了三个外设。
4.3.1 通用定时器特性
| 特性 | 说明 |
|---|---|
| 计数模式 | 向上、向下、中央对齐(向上/向下) |
| 通道数 | 4个独立通道(CH1~CH4) |
| PWM输出 | 每个通道独立配置占空比 |
| 输入捕获 | 测量脉冲宽度、频率、相位 |
| 编码器接口 | 支持增量式编码器(正交解码) |
| DMA支持 | 更新事件、捕获事件均可触发DMA |
4.3.2 计数模式对比
三种计数模式,用哪个?我一般这么选:
- 向上计数:普通PWM、定时中断——90%的场景用这个
- 向下计数:特殊波形需求,用得少
- 中央对齐:电机控制、三相PWM——需要对称波形时用
我的经验:中央对齐模式会产生两倍于向上计数的更新事件频率。比如ARR=999,向上计数1kHz更新一次,中央对齐就是2kHz。做电机FOC控制时,这个特性特别有用。
4.4 PWM输出:让LED呼吸起来
PWM,脉冲宽度调制。说白了就是通过调节高电平占空比,来模拟一个模拟电压。LED调光、电机调速、舵机控制——全得靠它。
我记得第一次用STM32做呼吸灯,调了半天占空比就是不平滑。后来发现是ARR值设得太小,分辨率不够。嗯,这里要注意:PWM的分辨率 = ARR + 1,ARR越大,调光越细腻。
4.4.1 PWM模式
通用定时器支持两种PWM模式:
| 模式 | 输出电平 | 应用场景 |
|---|---|---|
| PWM模式1 | CNT < CCR时有效,CNT ≥ CCR时无效 | 正逻辑PWM(常用) |
| PWM模式2 | CNT < CCR时无效,CNT ≥ CCR时有效 | 反逻辑PWM |
4.4.2 实战:呼吸灯
用TIM2的CH1(PA0)输出PWM,驱动LED呼吸:
void 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为复用推挽输出
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_Prescaler = 72 - 1; // 72MHz/72 = 1MHz
TIM_TimeBaseStructure.TIM_Period = 1000 - 1; // 1MHz/1000 = 1kHz PWM
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 4. 配置PWM通道
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 0; // 初始占空比0%
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
// 5. 使能定时器
TIM_Cmd(TIM2, ENABLE);
}
// 呼吸灯主循环
void Breathing_LED(void)
{
uint16_t duty = 0;
uint8_t direction = 1; // 1:增加 0:减少
while(1)
{
TIM_SetCompare1(TIM2, duty);
if(direction)
{
duty++;
if(duty >= 999) direction = 0;
}
else
{
duty--;
if(duty <= 1) direction = 1;
}
// 延时约2ms,让呼吸平滑
Delay_ms(2);
}
}
注意:TIM_SetCompare1()设置的CCR值不能超过ARR。如果你设CCR=1000而ARR=999,输出会一直为高电平——因为CNT永远达不到CCR。我曾经在这个问题上浪费了半天,排查到最后发现是边界条件没处理好。
4.5 定时器中断优先级与NVIC配置
多个定时器同时工作时,中断优先级怎么设?我有个原则:
- SysTick:RTOS下最高优先级,裸机下最低优先级
- PWM中断:中等优先级(电机控制需要快速响应)
- 基本定时器:低优先级(纯计时,不着急)
// NVIC配置示例
NVIC_InitTypeDef NVIC_InitStructure;
// TIM2中断(PWM)优先级:抢占1,子优先级0
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// TIM6中断(基本定时器)优先级:抢占2,子优先级0
NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
总结一下:定时器是STM32最基础也最强大的外设之一。SysTick管系统心跳,基本定时器管纯计时,通用定时器管多功能,PWM输出管模拟量控制。把这四个吃透了,你就能搞定80%的嵌入式应用场景。