第四章:定时器与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通道,分别控制红、绿、蓝三色。
下一章我们会讲中断系统,那是单片机的“紧急通道”。到时候见!