3. 定时器与PWM关系:从原理到实战
做嵌入式这些年,我见过不少新手一上来就对着寄存器猛啃,结果调了三天PWM就是不出波形。其实啊,搞懂定时器和PWM的关系,比背寄存器重要得多。说白了,PWM就是定时器的一个“副业”——定时器本来是用来计时的,顺便把计数的结果映射到引脚上,就成了PWM。
3.1 定时器工作原理:心脏是怎么跳的
定时器的核心,就是一个不断递增(或递减)的计数器。它靠时钟源驱动,每来一个时钟脉冲,计数器就加1。当计数值达到某个目标时,就会触发事件——比如更新中断、DMA请求,或者翻转引脚电平。
我习惯把定时器想象成一个“电子秒表”:
- 时钟源:秒表的“滴答”频率,决定了计时精度
- 计数器:秒表上跳动的数字
- 自动重装载寄存器(ARR):秒表的最大刻度,到了就归零
- 预分频器(PSC):把“滴答”频率降下来,免得计数器跑太快
举个例子,STM32的TIM2挂载在APB1总线上,典型时钟频率是72MHz。如果不做任何处理,计数器每秒钟要数7200万次——太快了,根本没法用。这时候就需要预分频器出场。
核心公式:
定时器溢出频率 = 定时器时钟 / ( (PSC + 1) × (ARR + 1) )
PWM频率 = 定时器溢出频率(在PWM模式下)
3.2 预分频器(PSC)的作用:把节奏降下来
预分频器就是一个分频器。它的值写在PSC寄存器里,实际分频系数是 PSC + 1。为什么加1?因为PSC=0时表示不分频,也就是除以1。
我在项目中遇到过一个问题:客户要求PWM频率精确到1Hz,但我的定时器时钟是72MHz。算了一下,如果PSC设成7199,ARR设成999,那么:
PWM频率 = 72,000,000 / (7200 × 1000) = 10 Hz
嗯,刚好满足。但如果你想要1kHz呢?把ARR改成100,PSC保持7199:
PWM频率 = 72,000,000 / (7200 × 101) ≈ 99.01 Hz
你看,精度就不够了。这时候需要调整PSC和ARR的搭配。我个人习惯是先定PSC,让计数器工作在合理的范围(比如几百到几千),再用ARR微调频率。
小技巧:PSC和ARR的值不要设得太极端。PSC太大(比如超过65535)会导致低频抖动,ARR太小(比如小于10)则占空比调节精度不够。我一般让ARR在100~10000之间。
3.3 自动重装载寄存器(ARR)的作用:决定周期
ARR决定了定时器数到多少就“归零”。在PWM模式下,ARR直接决定了PWM的周期(也就是频率的倒数)。同时,ARR还影响PWM的分辨率——ARR越大,你能调节的占空比档位就越多。
举个例子:
- ARR = 99:占空比可以从0%到100%,步进1%(共100档)
- ARR = 999:占空比步进0.1%(共1000档)
- ARR = 65535:占空比步进约0.0015%(共65536档)
但分辨率越高,PWM频率就越低。这是个trade-off。我曾经在一个LED调光项目里,为了追求平滑的呼吸效果,把ARR设成了65535,结果PWM频率掉到了约1kHz,人眼倒是看不出闪烁,但摄像头拍出来有条纹。后来我妥协到ARR=999,频率提到约72kHz,问题解决。
注意:ARR的值不能超过定时器的位宽。16位定时器的ARR最大是65535,32位定时器可以到4294967295。别问我怎么知道的——我曾经在STM32F103上把ARR写成70000,结果定时器直接“罢工”了。
3.4 PWM模式:边沿对齐 vs 中心对齐
这是PWM的两个“流派”。说白了,就是计数器计数的方向不同。
| 特性 | 边沿对齐模式 | 中心对齐模式 |
|---|---|---|
| 计数方向 | 从0递增到ARR,然后跳回0 | 从0递增到ARR,再递减回0 |
| PWM频率 | 定时器溢出频率 | 定时器溢出频率的一半 |
| 波形对称性 | 不对称(上升沿和下降沿位置不同) | 对称(波形关于中心对称) |
| 适用场景 | 普通电机控制、LED调光 | H桥控制、三相逆变、音频DAC |
| 更新事件时机 | 计数器归零时 | 计数器归零和到达ARR时各一次 |
边沿对齐模式:计数器从0开始,每来一个时钟加1,加到ARR后立刻跳回0。PWM的输出在计数值小于比较值(CCR)时输出有效电平,大于时输出无效电平。这种模式简单粗暴,适合大多数场景。
中心对齐模式:计数器先递增到ARR,再递减回0,形成一个三角波。PWM的输出在计数值小于CCR时有效,但因为是双向计数,所以波形天然对称。这种模式在电机控制中特别有用——可以减少谐波,降低电磁干扰。
我记得有一次做无刷电机驱动,用边沿对齐模式,电机低速时噪音特别大。换成中心对齐模式后,噪音明显下降。为什么?因为对称的PWM波形产生的电流纹波更小,电机运行更平滑。
避坑指南:中心对齐模式下,更新中断会触发两次(一次在顶点,一次在谷底)。如果你在中断里更新CCR值,要注意时序——我曾经在这里踩过坑,导致占空比更新不同步,电机突然抖动了一下。解决方案是用预装载寄存器,让CCR在下一个周期开始时统一更新。
3.5 一张图看懂定时器与PWM的关系
下面这张图是我自己画的,把定时器、预分频器、ARR、CCR和PWM输出的关系串起来了。你仔细看一遍,应该就能理解整个链路。
3.6 实战中的配置思路
说了这么多理论,咱们来点实际的。假设你要用STM32的TIM3输出一个1kHz、占空比50%的PWM,时钟是72MHz。你会怎么配?
我的习惯是分三步走:
- 确定PSC:让计数器工作在1MHz左右。72MHz ÷ (71+1) = 1MHz,所以PSC=71。
- 确定ARR:1kHz的周期是1ms,计数器跑1MHz就是1000个时钟周期。所以ARR=999。
- 确定CCR:50%占空比,CCR = ARR × 50% = 500。
代码大概长这样:
// TIM3 配置为 PWM 模式1,边沿对齐
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
// 1. 时基配置
TIM_TimeBaseStructure.TIM_Prescaler = 71; // PSC = 71
TIM_TimeBaseStructure.TIM_Period = 999; // ARR = 999
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 边沿对齐
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// 2. PWM 通道配置
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 500; // CCR = 500
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM3, &TIM_OCInitStructure);
// 3. 启动定时器
TIM_Cmd(TIM3, ENABLE);
嗯,就这么简单。但实际项目中,你可能会遇到各种“意外”。比如:
- 换了芯片型号,定时器时钟源变了,忘了改PSC
- 用了中心对齐模式,但CCR更新时机没处理好,波形不对称
- ARR设得太小,占空比调节不够细腻
这些坑我都踩过。所以我的建议是:先画图,再写代码。把定时器的时钟树、分频关系、计数范围在纸上过一遍,确认无误后再动手。磨刀不误砍柴工嘛。
个人经验:调试PWM时,先用示波器看波形,别急着调参数。我一般会先看频率对不对,再看占空比能不能线性调节。如果波形有毛刺,检查一下GPIO的推挽配置和定时器的互补输出设置。