第四章 定时器与PWM输出:用硬件定时器产生精确延时,输出PWM波控制屏幕背光或舵机

说到定时器,我脑子里第一个蹦出来的词就是「心脏」。没有定时器,单片机就是个瘫子,啥也干不了。你想想看,不管是延时、计数、产生波形,还是做任务调度,哪样离得开它?

这一章咱们就聊聊定时器的两个核心应用:精确延时PWM输出。这两个东西,说白了就是嵌入式开发里最常用的「时间管理术」。屏幕背光要调亮度?舵机要转角度?电机要调速?统统离不开PWM。

4.1 硬件定时器 vs 软件延时

很多初学者喜欢用 delay() 或者 HAL_Delay() 来做延时。嗯,我刚开始做项目时也这么干,直到有一次踩了个大坑。

我曾经在一个四轴飞行器项目里,用软件延时控制电调信号。结果飞行器一上天,电机就开始抽搐——因为软件延时被中断打断了,时序全乱套。从那以后,我再也不敢在关键时序里用软件延时了。

硬件定时器和软件延时有什么区别?我列个表你就明白了:

特性 软件延时 硬件定时器
精度 受中断影响,不稳定 硬件计数,精确到时钟周期
CPU占用 死循环,CPU被占死 后台运行,不阻塞CPU
可移植性 依赖主频,换芯片要重算 配置寄存器,通用性强
多任务能力 只能做一件事 可同时管理多个定时器

说白了,软件延时就是「傻等」,硬件定时器是「让硬件帮你等」。你想想看,CPU 几百兆的主频,让它在那空转,多浪费啊。

4.2 定时器的基本配置

以 STM32 为例,我习惯用 TIM2 来做通用定时器。配置其实不复杂,核心就三步:

  1. 时钟源选择——内部时钟还是外部触发
  2. 预分频器设置——把高频时钟降到你需要的频率
  3. 自动重装载值——决定定时周期

举个例子,我想产生一个 1ms 的定时中断。系统时钟是 72MHz,预分频器设成 72-1,这样定时器时钟就是 1MHz。自动重装载值设成 1000-1,那么每计数 1000 次就是 1ms。

// 定时器配置示例(STM32 HAL库)
void MX_TIM2_Init(void)
{
    TIM_ClockConfigTypeDef sClockSourceConfig = {0};
    TIM_MasterConfigTypeDef sMasterConfig = {0};

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 71;          // 72MHz / (71+1) = 1MHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 999;            // 1MHz / (999+1) = 1kHz = 1ms
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;

    HAL_TIM_Base_Init(&htim2);
    HAL_TIM_Base_Start_IT(&htim2);      // 启动定时器并开启中断
}
我个人习惯把预分频器和重装载值分开计算。先算预分频器让定时器时钟变成整数,再算重装载值得到目标周期。这样调试时脑子不容易乱。

4.3 PWM 输出原理

PWM,脉冲宽度调制。说白了就是让一个方波的高电平时间占比(占空比)可调。你想想看,如果这个方波频率足够高,人眼就看不出来闪烁,LED 的亮度就由占空比决定了。

PWM 的核心参数就两个:

  • 频率——方波每秒重复的次数。控制舵机用 50Hz,控制背光用 1kHz 以上
  • 占空比——高电平时间占整个周期的比例。0% 全灭,100% 全亮

硬件产生 PWM 的原理其实很简单:定时器从 0 开始计数,到达「比较值」时翻转电平,到达「重装载值」时归零。这个比较值,就是占空比的控制点。

// PWM 输出配置示例
void MX_TIM2_PWM_Init(void)
{
    TIM_OC_InitTypeDef sConfigOC = {0};

    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 71;          // 1MHz
    htim2.Init.Period = 999;            // 1kHz PWM频率

    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);
}

// 动态调整占空比
void Set_PWM_Duty(uint16_t duty)
{
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty);
}

4.4 实战:屏幕背光控制

我在做车载仪表盘时,屏幕背光控制就是个典型应用。白天要亮,晚上要暗,还得支持无级调光。

背光 PWM 的几个要点:

  • 频率选 1kHz 左右——太低会闪烁,太高驱动芯片跟不上
  • 占空比范围 0%~100%——但实际使用时建议留 5%~95%,避免完全熄灭或全亮
  • 用指数曲线调光——人眼对亮度的感知是非线性的,线性调光会感觉「前面没变化,后面突然变亮」
// 指数调光曲线
uint16_t Brightness_Curve[100];
void Init_Brightness_Curve(void)
{
    for (int i = 0; i < 100; i++)
    {
        // 指数映射:输入 0~99,输出 0~999
        float normalized = i / 99.0f;
        Brightness_Curve[i] = (uint16_t)(999.0f * normalized * normalized);
    }
}

void Set_Brightness(int percent)
{
    if (percent < 0) percent = 0;
    if (percent > 99) percent = 99;
    Set_PWM_Duty(Brightness_Curve[percent]);
}
避坑指南:我曾经在背光 PWM 上吃过亏。一开始用 100Hz,结果用手机摄像头一拍,屏幕全是条纹。后来改成 1.2kHz,问题就解决了。记住,人眼不闪不代表摄像头不闪。

4.5 实战:舵机控制

舵机控制是另一个经典应用。标准舵机的控制信号是 50Hz 的 PWM,占空比 5%~10% 对应 0°~180°。

具体来说:

  • 周期 20ms(50Hz)
  • 0.5ms 高电平 → 0°
  • 1.0ms 高电平 → 45°
  • 1.5ms 高电平 → 90°
  • 2.0ms 高电平 → 135°
  • 2.5ms 高电平 → 180°
// 舵机角度控制
#define SERVO_PERIOD   19999   // 20ms @ 1MHz定时器
#define SERVO_MIN      500     // 0.5ms
#define SERVO_MAX      2500    // 2.5ms

void Servo_SetAngle(uint8_t angle)
{
    if (angle > 180) angle = 180;
    
    // 线性映射:0°→500, 180°→2500
    uint16_t pulse = SERVO_MIN + (uint16_t)((float)(SERVO_MAX - SERVO_MIN) * angle / 180.0f);
    
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, pulse);
}

// 使用示例:让舵机从 0° 转到 180°
for (uint8_t i = 0; i <= 180; i++)
{
    Servo_SetAngle(i);
    HAL_Delay(10);  // 每步延时 10ms,让舵机跟上
}
注意:舵机的 PWM 频率必须是 50Hz,不能随便改。频率不对,舵机会嗡嗡响或者根本不转。我见过有人用 100Hz 去驱动舵机,结果舵机直接烧了——因为内部电路一直在复位。

4.6 多通道 PWM 管理

一个定时器通常有 4 个通道,可以同时输出 4 路 PWM。但要注意,所有通道共享同一个频率,只有占空比可以独立设置。

我在项目中常用这样的分配方案:

定时器 通道 用途 频率
TIM2 CH1 屏幕背光 1kHz
TIM2 CH2 舵机 50Hz
TIM3 CH1 电机调速 20kHz
TIM3 CH2 蜂鸣器 4kHz

你想想看,如果背光和舵机用同一个定时器,频率冲突怎么办?我的做法是:不同频率的需求,用不同的定时器。TIM2 做低频(50Hz~1kHz),TIM3 做高频(10kHz 以上),各管各的,互不干扰。

4.7 调试技巧与常见问题

做 PWM 调试时,我建议你准备一个逻辑分析仪。别光靠眼睛看,眼睛看不出来 1% 的占空比误差。

几个我踩过的坑:

  • 预分频器算错——72MHz 分频到 1MHz,预分频器要设 71,不是 72。这个 +1 和 -1 的坑,我至少栽过三次
  • 重装载值溢出——16位定时器最大 65535,如果周期太长会溢出。这时候要用 32位定时器或者级联
  • 占空比突变——在中断里直接改比较值,可能导致当前周期异常。建议用影子寄存器或者等周期结束再更新
我的调试习惯:先用示波器看波形,确认频率和占空比都对。然后接负载测试,看实际效果。最后才写业务逻辑。这样出了问题,能快速定位是硬件问题还是软件问题。

嗯,这一章的内容就到这里。定时器和 PWM 说难不难,说简单也不简单。关键是要理解硬件的工作原理,然后多动手试。下一章咱们聊聊中断系统,那可是嵌入式开发的另一个核心技能。