1. 从PWM到DAC:一个被低估的硬件技巧
如果你手头有一个微控制器项目,需要输出一个模拟电压信号,比如控制LED亮度、驱动一个简单的扬声器或者生成一个缓慢变化的传感器基准电压,但你的MCU偏偏没有内置数模转换器(DAC),你会怎么办?是去外挂一个DAC芯片,增加成本和PCB面积,还是直接换一个带DAC的MCU?其实,绝大多数工程师在职业生涯的某个阶段都会遇到这个问题,而一个经典且高效的解决方案就藏在几乎每个MCU都有的基础外设里:脉冲宽度调制,也就是PWM。
把PWM当DAC用,这听起来像是一个“凑合”的土办法,但事实上,它在精度要求不高、带宽需求低的场景下,是一个非常可靠且成本为零的工程选择。我做过不少消费电子和物联网设备,在很多对成本极其敏感或者IO口资源紧张的项目里,这个技巧帮我省下了宝贵的BOM成本和PCB空间。它的核心思想很简单:通过一个低通滤波器,将PWM输出的数字方波中的高频开关成分滤掉,只留下其直流平均分量,这个平均电压值就正比于PWM的占空比。这样一来,一个只有高低电平的数字引脚,就摇身一变成为了一个可以输出连续可变电压的模拟端口。
当然,这个方法并非没有局限。它输出的“模拟”信号带宽很窄,无法快速变化;它的精度和稳定性受到PWM分辨率、滤波器设计以及电源噪声的显著影响。但这恰恰是它的用武之地——对于那些变化缓慢的控制信号、设定点或者指示灯驱动,它完美契合。本文将彻底拆解“PWM变DAC”这个技巧,从背后的数学原理和电路模型开始,一步步带你设计滤波器、在代码中配置PWM,并深入探讨如何评估其性能、避开常见的坑,最终让你能自信地在下一个项目中应用这个既古老又实用的硬件设计技巧。
2. 核心原理:方波、平均值与低通滤波
要理解PWM如何模拟DAC,我们必须先抛开“模拟”和“数字”的刻板印象,从信号的角度来看待PWM输出。一个PWM信号本质上是一个周期固定、但高电平时间(脉宽)可调的方波。
2.1 直流平均分量的计算
假设PWM信号的周期为T,高电平时间为Ton,占空比D = Ton / T。如果该信号的高电平电压为Vcc(例如3.3V),低电平为0V。那么,在一个周期内,该信号的平均电压值Vavg可以通过积分计算:
Vavg = (1/T) * ∫_0^T V(t) dt = (1/T) * (Vcc * Ton + 0 * (T - Ton)) = Vcc * (Ton / T) = Vcc * D
这个公式揭示了最核心的关系:PWM信号在一个周期内的平均电压,等于电源电压乘以占空比。例如,Vcc=3.3V,50%占空比时,平均电压就是1.65V;75%占空比时,平均电压是2.475V。如果我们能让负载只对这个“平均电压”产生响应,而忽略快速的电压跳变,那么从效果上看,我们就得到了一个DAC。
2.2 低通滤波器的作用与模型
然而,真实的负载(如一个LED、一个运算放大器的输入)并不会自动计算平均值。它们会对电压的瞬时变化做出反应。PWM信号中除了我们想要的直流分量(即平均电压),还包含丰富的高频谐波成分,这些谐波是方波形状所固有的。如果我们直接把PWM引脚连接到负载,负载感受到的将是高速通断的方波,而非平滑的直流电压。
这时,低通滤波器就登场了。它的作用就像一个“频率门卫”:允许低频(这里指变化缓慢的直流或低频信号)通过,而强烈衰减高频成分。一个最简单的一阶RC低通滤波器,由一个电阻和一个电容构成,其截止频率fc由公式决定:
fc = 1 / (2π * R * C)
其中,R是电阻值,C是电容值。截止频率的含义是:频率低于fc的信号大部分能通过,频率高于fc的信号则被大幅衰减。我们的设计目标就是,让fc远低于PWM的基波频率(即1/T),从而将PWM方波中的高频开关噪声滤除得足够干净,最终在电容两端得到一个纹波很小的、近似直流的电压,这个电压值就等于上述计算的平均电压Vavg。
注意:这里说的“基波频率”就是PWM的频率本身。一个占空比50%的方波,其能量主要分布在开关频率及其奇数次谐波上。滤波器必须对这些频率都有足够的衰减。
为了更直观地理解不同滤波器参数下的效果,我们可以看下面这个对比:
| 滤波器类型 | 电路复杂度 | 纹波电压 | 响应速度 | 适用场景 |
|---|---|---|---|---|
| 一阶RC滤波 | 最简单(1电阻+1电容) | 较高,衰减慢(-20dB/十倍频) | 慢,建立时间长 | 对纹波不敏感,信号变化极慢的场景,如LED调光、基准电压 |
| 二阶RC滤波 | 简单(2电阻+2电容) | 较低,衰减更快(-40dB/十倍频) | 更慢 | 需要更低纹波,但仍可接受较慢响应的场景 |
| 有源滤波 | 复杂(需运放) | 很低,可精确设计 | 可设计,通常比无源快 | 需要低阻抗输出、精确截止频率或特定滤波特性的场景 |
对于大多数将PWM用作DAC的应用,一阶RC滤波器因其极简和低成本已成为事实上的标准选择。它的设计就是在纹波、响应速度和电路复杂度之间取得一个平衡。
3. 硬件设计实战:RC滤波器参数计算与选型
理论清晰后,我们来动手设计。整个硬件部分的核心就是这个一阶RC低通滤波器。设计过程就是根据你的性能需求,求解R和C的值。
3.1 明确设计约束条件
在计算之前,必须定义三个关键参数:
- PWM频率:这是你将在微控制器代码中设置的PWM开关频率,记为
f_pwm。 - 允许的电压纹波:你希望滤波后输出电压的波动峰值(V_ripple)有多大?这取决于你的负载对噪声的敏感度。例如,驱动一个LED,100mV纹波可能无关紧要;但作为一个精密传感器的参考电压,可能要求纹波小于10mV。
- 期望的响应速度:当你的PWM占空比改变后,滤波器的输出电压需要多长时间才能稳定到新值的某个百分比(比如90%)?这个时间与滤波器的RC时间常数(τ = R * C)直接相关。τ越大,响应越慢。
这三个参数相互制约。提高PWM频率可以降低纹波,但可能会受限于MCU的性能。减小纹波要求更低的截止频率(更大的RC乘积),但这会降低响应速度。你需要根据具体应用做权衡。
3.2 分步计算与元件选择
假设我们为一个智能灯饰项目设计,MCU的GPIO输出高电平为3.3V,我们期望:
- PWM频率
f_pwm = 1 kHz(周期 T = 1ms) - 可接受的峰峰值纹波
V_ripple_pp < 50mV - 负载是一个高输入阻抗的MOSFET栅极,几乎不吸取电流。
步骤一:估算所需截止频率对于一阶RC滤波器,当输入为方波时,输出纹波电压的近似公式为:V_ripple_pp ≈ Vcc * (T / (R*C)) = Vcc * (T / τ), 其中 τ = R*C。 这个公式在截止频率远低于PWM频率时比较准确。我们可以用它进行初步估算:τ = R*C ≈ Vcc * T / V_ripple_pp = 3.3V * 0.001s / 0.05V = 0.066 秒。 那么,截止频率fc = 1 / (2πτ) ≈ 1 / (2*3.14*0.066) ≈ 2.4 Hz。
这意味着,为了将1kHz PWM的纹波抑制到50mV以下,我们需要一个截止频率大约为2.4Hz的低通滤波器。这是一个非常低的频率。
步骤二:选择具体的R和C值有了 τ = 0.066s,我们就可以选择R和C了。理论上,有无穷多对组合(如 66kΩ + 1μF, 6.6kΩ + 10μF)。选择时需考虑:
- 电阻R:不能太小,否则会从MCU的GPIO吸取过大电流。通常MCU GPIO的拉电流能力在20mA左右,为了安全留有余量,流过R的电流应远小于此值。当输出为高电平(3.3V)时,电流 I = Vcc / R。若R=10kΩ, I=0.33mA,非常安全。也不能太大,否则电路容易受电磁干扰。
- 电容C:不能太小,否则纹波会太大;不能太大,否则体积和成本会增加,且漏电流可能影响精度。电解电容便宜容量大,但精度差、有极性;陶瓷电容(如X7R, X5R)精度高、无极性,是更好的选择,但大容量(>10μF)的陶瓷电容价格较高。
一个折中的方案:选择 R = 10 kΩ, 则 C = τ / R = 0.066s / 10000Ω = 6.6 μF。 我们可以取一个标称值C = 10 μF。此时实际 τ‘ = 10k * 10μF = 0.1s, 截止频率 fc’ = 1.6 Hz, 纹波会更小。
步骤三:电路连接与布局最终的电路极其简单:MCU_PWM_GPIO ---[R]---+---[C]--- GND|Vout
Vout就是从电容C正极引出的“模拟”电压。这里有几个硬件实操要点:
- 电容类型:强烈推荐使用多层陶瓷电容(MLCC),如10μF/10V X5R或X7R材质。它们没有极性,ESR低,性能稳定。如果使用电解电容,务必注意极性,反接会损坏。
- 布局:电阻和电容应尽可能靠近MCU的PWM输出引脚放置,尤其是电容的接地端,回路越短越好,以减少引入噪声的机会。
- 负载考虑:我们的计算假设负载是理想的高阻抗。如果负载会吸取不可忽略的电流(例如直接驱动一个LED),那么负载电阻会与R形成分压,导致输出电压低于计算值。在这种情况下,要么确保负载阻抗远大于R(至少10倍以上),要么在滤波器后增加一个电压跟随器(运放构成)进行缓冲,提供低阻抗输出。
4. 软件配置:最大化PWM性能的关键
硬件滤波器搭建好了,但PWM信号的质量是软件赋予的。不恰当的PWM配置会直接导致最终的“模拟”输出精度差、噪声大。
4.1 PWM频率与分辨率的权衡
这是最重要的一个权衡点。PWM有两个核心参数:
- 频率:如前所述,频率越高,经过相同滤波器后纹波越小,对滤波器要求越低。但频率受限于MCU定时器的时钟和你的分辨率需求。
- 分辨率:指占空比可以调节的精细程度,通常用位数表示。例如,8位分辨率意味着占空比可以有256级(0-255),10位则有1024级。
矛盾在于:在定时器时钟源固定的情况下,提高频率必然会降低分辨率。因为频率 f = 时钟 / (分频系数 * 自动重载值)。自动重载值决定了计数器的最大值,也直接决定了分辨率(最大值+1就是级数)。要提高f,就必须减小自动重载值,从而减少了可用的计数值,分辨率就下降了。
如何选择?这取决于应用:
- 高精度、慢变化信号:优先保证分辨率。例如,要输出0-3.3V,步进小于5mV(约需660级,10位分辨率)。此时可以接受较低的PWM频率(如100Hz-1kHz),然后通过更严格的滤波器(更大的RC)来抑制纹波。
- 快速响应、精度要求一般:可以适当提高频率以加快响应速度或简化滤波器。例如LED调光,人眼对亮度微小变化不敏感,8位256级通常足够,频率可以设在100Hz以上以避免闪烁。
以常见的STM32的通用定时器为例,如果系统时钟是72MHz,我们希望得到10位分辨率(自动重载值ARR=1023),不分频(PSC=0),那么PWM频率f = 72MHz / (1023+1) ≈ 70.3 kHz。这是一个很高的频率,对滤波器非常友好,可以用很小的RC值就能滤出平滑电压。
4.2 代码实现示例与高级技巧
以下是基于STM32 HAL库的一个基础配置示例,目标是生成一个1kHz, 8位分辨率的PWM:
TIM_HandleTypeDef htim2; TIM_OC_InitTypeDef sConfigOC = {0}; // 定时器基础配置 htim2.Instance = TIM2; htim2.Init.Prescaler = 71; // 系统时钟72MHz, 分频后为1MHz (72/(71+1)) htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 999; // 自动重载值ARR, 周期 = (ARR+1)/定时器时钟 = 1000/1MHz = 1ms -> 1kHz htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim2); // PWM通道配置 sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 500; // 初始占空比, CCR值, 500/1000 = 50% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1); // 启动PWM HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1); // 动态改变占空比 __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 750); // 改为75%占空比高级技巧:
- 中心对齐模式:大多数定时器支持边沿对齐(计数器向上计数)和中心对齐(计数器先上后下)。中心对齐模式可以将PWM输出的谐波能量更多地集中在开关频率的倍频上,而不是分散在基波和谐波中,有时更有利于滤波。
- 死区时间:如果用于驱动桥式电路,需要设置死区时间,但作为DAC使用时,通常不需要。
- DMA传输:如果需要输出复杂的模拟波形(如正弦波),可以预先计算好一个周期的占空比数组,然后通过DMA自动、定时地更新PWM的捕获比较寄存器(CCR),从而解放CPU。这是实现“PWM DAC”播放音频等动态波形的基础。
5. 性能评估与常见问题排查
电路搭好了,代码也跑起来了,用万用表一量,电压确实随着占空比变化了。但这就够了吗?作为一个严谨的工程师,我们必须量化它的性能,并知道如何解决可能出现的问题。
5.1 实测指标:纹波、线性度与建立时间
不要只相信万用表的直流电压档,它测的是平均值,会掩盖纹波。你需要一个示波器来真正评估输出质量。
测量纹波:
- 将示波器探头连接到滤波器的输出端(电容两端)。
- 设置示波器为交流耦合,适当调整垂直灵敏度(如20mV/格)和时间基准。
- 你应该能看到一个近似三角波或锯齿波的微小波动,这就是纹波。测量其峰峰值(Vpp)。这个值应该与你理论计算或预期的纹波值在同一数量级。如果纹波过大,首先检查PWM频率是否稳定,然后考虑增大电容C或电阻R(以降低截止频率)。
测试线性度:
- 编写一个循环,让PWM占空比从最小值(如0)线性增加到最大值(如255)。
- 用万用表直流电压档,记录下几个关键占空比点(如0, 64, 128, 192, 255)对应的输出电压。
- 理论上,电压应与占空比成完美的线性关系:
Vout = Vcc * (Duty / Max_Duty)。 - 实际绘制出的Vout-Duty曲线可能会在两端(接近0%和100%)出现非线性。这通常是因为GPIO引脚在输出极低或极高占空比时,其上升/下降时间、驱动能力不对称导致的。如果线性度要求高,应避免使用两端的5%-10%的范围。
测量建立时间:
- 在示波器上,将PWM占空比从一个值(如10%)阶跃变化到另一个值(如90%)。
- 使用示波器的单次触发和上升沿/下降沿触发,捕捉电压变化的完整过程。
- 测量输出电压从变化开始,到达最终值±1%误差带内所需的时间。这就是系统的建立时间。对于一阶RC系统,理论上从10%到90%的上升时间约为2.2τ。实测值应与理论值吻合。如果响应太慢,就需要减小R或C,但这会增大纹波,你需要重新权衡。
5.2 典型问题与解决方案
在实际项目中,我遇到过不少坑,这里总结几个最常见的:
问题一:输出电压达不到理论值(例如,100%占空比时不到Vcc)
- 可能原因1:GPIO负载过重。检查你的电阻R是否太小。如果R=1kΩ,当输出高电平时,GPIO需要提供3.3V/1kΩ=3.3mA的电流。虽然很多MCU可以承受,但如果同时有其他负载,可能导致内部压降,使输出高电平低于Vcc。解决方案是增大R,比如到10kΩ。
- 可能原因2:电容漏电流。特别是使用电解电容时,其漏电流可能达到几十甚至上百微安。这个漏电流在电阻R上会产生压降。解决方案是换用漏电流极小的陶瓷电容,或适当减小R值(但需考虑GPIO驱动能力)。
- 可能原因3:负载电流过大。如果你的负载(如一个LED)直接接在Vout上,它会从滤波器中吸取电流,导致电压下降。解决方案是在滤波器后增加一个电压跟随器进行缓冲。
问题二:输出噪声大,纹波远超计算值
- 可能原因1:电源噪声。PWM输出和模拟电路共用同一个嘈杂的数字电源。用示波器查看Vcc引脚上的噪声。解决方案是给模拟部分(MCU的模拟电源引脚、滤波电路)增加LC滤波或使用低压差线性稳压器(LDO)单独供电。
- 可能原因2:地线噪声。滤波电容的接地端没有接到一个干净、稳定的“模拟地”上,而是和数字地的大电流路径混在一起。解决方案是采用星型接地或单点接地,将滤波电容的地直接接到MCU的模拟地引脚或一个安静的地平面点。
- 可能原因3:PWM频率不稳定。检查代码中定时器配置是否正确,是否有其他中断或任务长时间阻塞影响了PWM的稳定性。可以用示波器直接测量PWM引脚本身的频率和占空比是否精准。
问题三:响应速度太慢,无法跟上控制需求
- 根本原因:RC时间常数τ太大。这是为提高滤波效果(降低纹波)而付出的代价。
- 解决方案:
- 提高PWM频率:这是最有效的方法。频率提高一倍,在保持相同纹波水平的前提下,截止频率可以提高一倍,从而RC乘积可以减半,响应速度翻倍。检查你的MCU能否支持更高的PWM频率。
- 使用高阶滤波器:如前所述,二阶无源或有源滤波器可以在相同的截止频率下提供更陡的衰减,从而允许你使用更高的截止频率(更小的τ)来达到相同的纹波抑制效果,进而提升响应速度。但这增加了电路复杂度。
- 接受更大的纹波:如果负载对纹波不敏感,可以重新评估纹波要求,适当放宽,从而减小RC值。
将PWM用作DAC是一个在成本、性能和复杂度之间寻求优雅平衡的经典工程实践。它绝不仅仅是“凑合能用”,而是在明确边界条件(低带宽、中低精度)下的最优解。理解其背后的信号原理,掌握滤波器设计中的权衡艺术,在软件上精细配置PWM参数,并在调试中善用仪器观察现象、定位问题,你就能把这个简单的技巧变成项目工具箱里一件可靠又灵活的工具。下次当你的设计需要多一路模拟输出而资源紧张时,不妨先问问自己:这里真的需要一个真正的DAC吗?也许,一个PWM加两个元件的方案,正是最合适的那一个。