第四章:定时器与PWM:让单片机“动”起来

各位同学,欢迎来到第四章。这一章我们聊聊定时器和PWM。说实话,这两个东西是嵌入式开发里最常用的外设之一。你想想看,从最简单的LED闪烁,到复杂的电机控制,都离不开它们。

我个人习惯把定时器比作单片机的“心跳”。没有它,单片机就像个没节奏感的鼓手,干啥都乱套。而PWM呢,就是利用这个心跳,模拟出连续变化的信号。今天我们就从定时器配置讲起,一步步带你玩转PWM,最后用呼吸灯和蜂鸣器来实战一下。

4.1 定时器基础:从“数数”开始

定时器本质上就是个计数器。它内部有个寄存器,每来一个时钟脉冲就加1。当加到某个设定值(比如1000)时,就会产生一个中断,或者让某个引脚翻转。

为什么会这样?因为单片机的主频通常很高(比如72MHz),直接用它来控制外设太浪费了。定时器相当于一个“分频器”,把高频信号降到你需要的频率。

核心概念:

  • 预分频器(PSC): 把时钟频率分频。比如72MHz分频到1MHz,就是除以72。
  • 自动重装载寄存器(ARR): 计数的上限。比如ARR=1000,计数器就从0数到1000,然后归零。
  • 计数模式: 向上计数(0→ARR)、向下计数(ARR→0)、中央对齐(先上后下)。

嗯,这里要注意:不同厂家的单片机,定时器命名可能不同。比如STM32叫TIM1~TIM14,而51单片机叫T0、T1。但原理都一样。

我在项目中遇到过一个问题:有个同事把预分频器设错了,导致定时器中断频率比预期高了10倍。结果LED闪烁快得像频闪灯,差点把测试员的眼睛晃瞎。所以,配置时一定要算清楚。

4.2 PWM生成:模拟世界的“数字画笔”

PWM,全称脉冲宽度调制。说白了,就是通过调整高电平的宽度(占空比),来模拟一个连续变化的电压。

你想想看,一个LED如果一直亮着,那就是100%占空比。如果一半时间亮一半时间灭,那就是50%占空比。只要频率够高(比如1kHz以上),人眼就感觉不到闪烁,只会觉得亮度变暗了。

PWM的两个关键参数:

  • 频率: 由定时器的时钟和ARR决定。频率 = 时钟 / (PSC+1) / (ARR+1)。
  • 占空比: 由比较寄存器(CCR)决定。占空比 = CCR / (ARR+1) × 100%。

我的小技巧: 配置PWM时,先固定频率,再调整占空比。比如呼吸灯,频率设在1kHz左右,占空比从0%慢慢变到100%,再变回来。这样效果最平滑。

我曾经踩过一个坑:在STM32上配置PWM时,忘了使能定时器的预装载功能。结果每次修改占空比,都要等到下一个周期才生效,导致波形有毛刺。后来查了手册才发现,有个叫“预装载使能位”的东西必须置1。

4.3 实战一:呼吸灯

呼吸灯,就是让LED像呼吸一样,由暗到亮,再由亮到暗。实现原理很简单:用定时器产生PWM,然后不断改变占空比。

下面是一个基于STM32的代码示例(使用HAL库):

// 定时器句柄
TIM_HandleTypeDef htim2;

// 初始化定时器2,通道1输出PWM
void PWM_Init(void) {
    TIM_OC_InitTypeDef sConfigOC = {0};
    
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 71;          // 72MHz / 72 = 1MHz
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 999;            // 1MHz / 1000 = 1kHz
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    HAL_TIM_PWM_Init(&htim2);
    
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 0;                // 初始占空比0%
    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 Breathing_LED(void) {
    uint16_t duty = 0;
    uint8_t direction = 1;  // 1: 增加, 0: 减少
    
    while (1) {
        __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty);
        
        if (direction) {
            duty += 5;
            if (duty >= 999) direction = 0;
        } else {
            duty -= 5;
            if (duty <= 0) direction = 1;
        }
        
        HAL_Delay(10);  // 每10ms调整一次
    }
}

注意: 呼吸灯的频率不能太低。如果PWM频率低于50Hz,人眼就能看到闪烁。我建议至少用100Hz以上。另外,调整占空比的步长要适中,步长太大呼吸会“卡顿”,步长太小又太慢。

4.4 实战二:蜂鸣器驱动

蜂鸣器分两种:有源蜂鸣器和无源蜂鸣器。有源蜂鸣器内部自带振荡电路,通电就响。无源蜂鸣器需要外部提供一定频率的方波才能发声。

这里我们讲无源蜂鸣器。它本质上就是个电磁线圈,通过PWM驱动,改变频率就能发出不同音调。

我记得有一次做项目,客户要求蜂鸣器播放一段简单的旋律。我直接用定时器PWM输出不同频率,配合延时函数控制节拍。效果还不错,就是音质有点“电子味”。

下面是一个播放“哆来咪”的示例:

// 音符频率表(单位:Hz)
#define NOTE_DO  523
#define NOTE_RE  587
#define NOTE_MI  659

// 设置蜂鸣器频率
void Buzzer_Set_Freq(uint16_t freq) {
    uint16_t period = 1000000 / freq;  // 假设定时器时钟1MHz
    __HAL_TIM_SET_AUTORELOAD(&htim3, period - 1);
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, period / 2);  // 50%占空比
}

// 播放一段旋律
void Play_Melody(void) {
    Buzzer_Set_Freq(NOTE_DO);
    HAL_Delay(500);
    Buzzer_Set_Freq(NOTE_RE);
    HAL_Delay(500);
    Buzzer_Set_Freq(NOTE_MI);
    HAL_Delay(500);
    HAL_TIM_PWM_Stop(&htim3, TIM_CHANNEL_1);  // 停止
}

避坑指南: 我曾经把蜂鸣器的PWM频率设成了1Hz,结果蜂鸣器“咔咔咔”地响,像老式电报机。后来才意识到,人耳能听到的频率范围是20Hz~20kHz。低于20Hz就是次声波,听不到;高于20kHz是超声波,人也听不到。所以,蜂鸣器频率一般设在1kHz~5kHz之间最合适。

4.5 定时器与PWM的进阶技巧

掌握了基础,我们聊聊进阶用法。我个人觉得,定时器和PWM的潜力远不止呼吸灯和蜂鸣器。

应用场景 定时器模式 PWM要求 注意事项
LED调光 向上计数 频率>100Hz,占空比可调 避免低频闪烁
舵机控制 向上计数 频率50Hz,脉宽0.5~2.5ms 占空比精度要够
直流电机调速 向上计数 频率1kHz~20kHz 频率太低会抖动
音频播放 向上计数 频率可变,占空比50% 需要查表生成音符

嗯,这里有个关键点:当PWM用于电机驱动时,频率不能太低。否则电机会发出“嗡嗡”声,而且扭矩不稳定。我见过有人用100Hz驱动直流电机,结果电机像得了帕金森一样抖个不停。

另外,如果你用的是高级定时器(比如STM32的TIM1、TIM8),还可以实现互补PWM输出,专门用来驱动H桥电路。这个在无刷电机控制中很常见。

4.6 总结与思考

这一章我们讲了定时器的配置、PWM的生成,以及两个实战案例。说白了,定时器就是个“节拍器”,PWM就是在这个节拍下画出的“波形图”。

我建议你动手试试:先点亮一个呼吸灯,再让蜂鸣器唱首歌。你会发现,原来单片机也可以这么“有温度”。

最后留个思考题:如果我想用PWM控制一个RGB LED,实现七彩渐变,该怎么做?提示:需要三个PWM通道,分别控制红、绿、蓝三色。

下一章我们会讲中断系统,那是单片机的“紧急通道”。到时候见!