4. 定时器基础:SysTick系统滴答定时器、基本定时器、通用定时器、PWM输出

定时器,说白了就是单片机的“心跳”。没有它,你连个精准的延时都做不了,更别提什么PWM调速、捕获编码器信号了。我刚开始学STM32那会儿,总觉得定时器就是用来计数的,后来才发现——这玩意儿的水深着呢。

今天咱们就把STM32的定时器家族捋一遍。从最简单的SysTick,到基本定时器,再到功能最全的通用定时器,最后用PWM输出做个实战。嗯,跟着我的节奏来,保证你学完就能上手。

本章核心脉络:SysTick(系统心跳)→ 基本定时器(纯计时)→ 通用定时器(多功能)→ PWM输出(实战应用)。层层递进,每个模块都配有代码示例。

STM32 定时器知识体系 STM32 定时器家族 SysTick 基本定时器 通用定时器 高级定时器 系统滴答 RTOS心跳 精确延时 仅向上计数 无外部引脚 时基发生器 向上/下/中央对齐 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%的嵌入式应用场景。