4、定时器基础与应用:SysTick定时器、基本定时器、PWM输出、输入捕获测量频率

定时器这玩意儿,可以说是嵌入式开发里最常用的外设之一了。我刚开始做项目那会儿,总觉得定时器不就是计个数嘛,有啥好研究的?后来踩了几个坑才明白——定时器用得好不好,直接决定了你产品的实时性和稳定性。

这一章,咱们把STM32的定时器体系捋一遍。从最简单的SysTick,到基本定时器,再到PWM和输入捕获。每个知识点我都会结合项目经验来讲,尤其是那些容易翻车的地方。

4.1 SysTick定时器:FreeRTOS的心跳

SysTick是Cortex-M内核自带的24位递减计数器。说白了,它就是给操作系统提供“心跳”的。FreeRTOS的任务调度、时间片轮转、vTaskDelay这些功能,全都依赖SysTick。

核心要点:SysTick的时钟源通常选择处理器时钟(HCLK)的8分频。比如HCLK=72MHz,那么SysTick的计数频率就是9MHz。每计数一次,耗时1/9MHz ≈ 0.111μs。

配置SysTick其实很简单。HAL库已经帮我们封装好了:

// SysTick中断优先级配置
HAL_NVIC_SetPriority(SysTick_IRQn, 15, 0);
// 使能SysTick中断
HAL_NVIC_EnableIRQ(SysTick_IRQn);
// 配置SysTick重装载值,产生1ms中断
// 假设HCLK=72MHz,8分频后=9MHz
// 重装载值 = 9000000 / 1000 - 1 = 8999
SysTick_Config(8999);

嗯,这里要注意——SysTick的中断优先级一定要设成最低(15)。为什么?因为FreeRTOS的临界区保护依赖这个优先级。如果你把SysTick优先级设高了,在临界区里它照样能打断,那临界区就形同虚设了。

我曾经踩过的坑:有个项目,任务调度偶尔会莫名其妙卡死。查了两天才发现,是某次代码升级时不小心把SysTick优先级改成了5。结果在操作队列时,SysTick中断进来触发了任务切换,队列数据还没写完就被读走了……从那以后,我每次移植FreeRTOS第一件事就是检查SysTick优先级。

4.2 基本定时器:最纯粹的计时单元

基本定时器(TIM6、TIM7)是STM32定时器家族里最“单纯”的成员。它只有向上计数功能,没有外部引脚,不能输出PWM,也不能做输入捕获。那它有什么用?

  • 产生精准的时间基准——比如每隔100ms采集一次传感器数据
  • 触发DMA传输——定时器更新事件可以触发DMA,实现自动数据搬运
  • 作为其他定时器的从模式——级联使用,扩展定时范围

基本定时器的配置流程,我习惯这么写:

// 假设TIM6,时钟源为APB1,通常=36MHz
// 目标:产生1ms中断
// 预分频器:36000-1,得到1MHz计数频率
// 自动重装载值:1000-1,得到1ms周期

TIM_HandleTypeDef htim6;
htim6.Instance = TIM6;
htim6.Init.Prescaler = 36000 - 1;   // 预分频
htim6.Init.Period = 1000 - 1;       // 自动重装载
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_Base_Init(&htim6);
HAL_TIM_Base_Start_IT(&htim6);      // 启动并使能中断

个人习惯:我一般把基本定时器留给“周期性任务”用。比如一个温湿度采集任务,每2秒读一次传感器,用TIM6的更新中断触发一个标志位,主循环里检测到标志位就去采集。这样比在任务里硬等要优雅得多。

4.3 PWM输出:让硬件帮你干活

PWM(脉冲宽度调制)说白了就是输出一个频率固定、占空比可调的方波。你想想看,控制电机转速、调节LED亮度、驱动舵机角度——这些场景全都能用PWM搞定。

STM32的高级定时器和通用定时器都支持PWM输出。以通用定时器TIM2为例,配置PWM的步骤:

  1. 配置定时器时基——决定PWM频率
  2. 配置输出比较通道——决定PWM占空比
  3. 配置GPIO为复用推挽输出——让引脚输出PWM波形
  4. 启动PWM输出
// 配置TIM2通道1输出PWM,频率=1kHz
// 预分频器=72-1,得到1MHz计数频率
// 自动重装载值=1000-1,得到1kHz
// 占空比=50%,即比较值=500

TIM_OC_InitTypeDef sConfigOC = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 72 - 1;
htim2.Init.Period = 1000 - 1;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_PWM_Init(&htim2);

sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500;              // 占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

关键点:PWM频率 = 定时器时钟 / (预分频器+1) / (自动重装载值+1)。占空比 = 比较值 / (自动重装载值+1) × 100%。

我在项目中遇到过一个问题:用PWM驱动一个直流电机,电机在低速时抖动得很厉害。后来发现是PWM频率太低了(只有50Hz)。电机线圈在PWM关断期间电流掉得太快,导致转矩不连续。把频率提高到1kHz后,问题就解决了。

避坑指南:PWM频率不是越高越好。频率太高,MOS管的开关损耗会急剧增加,驱动芯片可能会过热。我一般遵循这个原则:电机驱动用1kHz~20kHz,LED调光用100Hz~1kHz,舵机控制用50Hz(标准周期20ms)。

4.4 输入捕获:测量外部信号的频率

输入捕获是定时器的“反向操作”——它不输出波形,而是测量外部信号的频率或脉宽。原理很简单:当检测到指定边沿(上升沿或下降沿)时,定时器会把当前的计数值“捕获”到寄存器里。两次捕获的差值,就是信号的周期。

测量频率的典型步骤:

  1. 配置定时器输入捕获通道,选择上升沿触发
  2. 使能捕获中断
  3. 在中断服务函数里读取捕获值
  4. 计算两次捕获的差值,得到周期
  5. 频率 = 1 / 周期
// 配置TIM3通道1输入捕获
// 预分频器=72-1,计数频率=1MHz,精度=1μs

TIM_IC_InitTypeDef sConfigIC = {0};
htim3.Instance = TIM3;
htim3.Init.Prescaler = 72 - 1;
htim3.Init.Period = 0xFFFF;         // 最大计数值
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
HAL_TIM_IC_Init(&htim3);

sConfigIC.ICPolarity = TIM_ICPOLARITY_RISING;
sConfigIC.ICSelection = TIM_ICSELECTION_DIRECTTI;
sConfigIC.ICPrescaler = TIM_ICPSC_DIV1;
sConfigIC.ICFilter = 0;
HAL_TIM_IC_ConfigChannel(&htim3, &sConfigIC, TIM_CHANNEL_1);
HAL_TIM_IC_Start_IT(&htim3, TIM_CHANNEL_1);

中断服务函数里处理捕获值:

void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim)
{
    static uint16_t lastCapture = 0;
    static uint8_t firstCapture = 1;
    uint16_t currentCapture;
    uint16_t period;
    uint32_t frequency;

    if(htim->Instance == TIM3)
    {
        currentCapture = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1);
        if(!firstCapture)
        {
            // 处理计数器溢出
            if(currentCapture >= lastCapture)
                period = currentCapture - lastCapture;
            else
                period = (0xFFFF - lastCapture) + currentCapture + 1;

            // 频率 = 1MHz / period
            frequency = 1000000 / period;
            // 这里可以保存frequency供主循环使用
        }
        else
        {
            firstCapture = 0;
        }
        lastCapture = currentCapture;
    }
}

个人经验:输入捕获测量低频信号(比如10Hz以下)时,要注意定时器溢出问题。如果信号周期超过了定时器的最大计数值,捕获值会“绕圈”。我一般会配合定时器的更新中断,记录溢出次数,这样就能测量很宽的频率范围了。

4.5 知识体系总览

为了让你更直观地理解这四种定时器应用的关系,我画了一张结构图:

STM32定时器应用知识体系 STM32 定时器 SysTick 定时器 基本定时器 PWM 输出 输入捕获 功能 FreeRTOS心跳 任务调度时间基准 vTaskDelay依赖 功能 精准时间基准 触发DMA传输 定时器级联扩展 功能 电机转速控制 LED亮度调节 舵机角度驱动 功能 外部信号频率测量 脉宽测量 编码器接口

4.6 四种定时器应用对比

我把这四种应用的关键参数整理成了表格,方便你对照选择:

类型 硬件资源 典型用途 精度 中断频率
SysTick 内核内置 RTOS心跳、软件延时 1ms(可调) 固定(通常1kHz)
基本定时器 TIM6/TIM7 周期性任务、DMA触发 1μs~1s 可配置
PWM输出 通用/高级定时器 电机、LED、舵机 占空比分辨率1~65535级 无中断(硬件自动输出)
输入捕获 通用/高级定时器 频率测量、脉宽测量 取决于计数频率 每次捕获边沿触发

选型建议:如果你只是需要一个简单的周期性中断,用基本定时器就够了,别浪费通用定时器。PWM和输入捕获必须用通用或高级定时器,因为它们需要硬件通道支持。SysTick是操作系统的“专属资源”,裸机开发时也可以用它做延时,但一旦上了RTOS,就别再动SysTick的配置了。

好了,这一章的内容就到这里。定时器这块东西,光看代码是不够的,我建议你拿一块开发板,把每个例程都跑一遍。尤其是PWM和输入捕获,用示波器看看波形,比看一百遍代码都管用。