STM32 PWM DAC实战:低成本实现模拟电压输出的原理、设计与性能优化
2026/9/9 3:12:22 网站建设 项目流程

1. 从PWM到模拟电压:一个被低估的“DAC”方案

最近在整理STM32的笔记,翻到PWM DAC这部分,感觉很多朋友对它的理解可能还停留在“用PWM控制LED亮度或者电机速度”的层面。确实,PWM(脉冲宽度调制)最直观的应用就是驱动这些数字执行器,通过改变占空比来等效调节平均电压。但如果你手头的STM32项目恰好需要一个简单的模拟电压输出,而芯片本身又没有足够的DAC(数模转换器)通道,或者你想省下外置DAC芯片的成本和PCB面积,那么把PWM信号经过一个简单的RC低通滤波器,就能摇身一变成为一个相当实用的“DAC”。这个实验不仅仅是PWM功能的简单应用,更是对模拟电路和数字信号处理基础概念的一次绝佳复习。它要求你同时理解定时器如何精确产生PWM、模拟滤波器如何工作,以及如何评估这种方案的性能边界。今天,我就结合STM32F103这个经典型号,把PWM DAC从原理到实现的整个链条,包括那些容易踩坑的细节,系统地梳理一遍。

2. PWM DAC的核心原理:为什么方波能变直流?

在深入代码之前,我们必须先搞清楚PWM DAC到底是怎么一回事。这不仅仅是配置个定时器那么简单,理解其背后的物理和数学原理,才能用好它,并预判它的局限性。

2.1 PWM信号的直流分量

一个理想的PWM波是幅值固定(比如0V和3.3V)、周期固定、但高电平宽度(脉宽)可调的数字方波。当我们说“占空比(Duty Cycle)”为50%时,意味着在一个周期内,高电平持续的时间占整个周期的一半。从信号分析的角度看,这样一个周期性的方波信号,可以分解为一个直流分量和无数个高频交流分量的叠加。这个直流分量的大小,恰好等于信号的幅值乘以占空比。

举个例子,假设STM32的GPIO输出高电平为3.3V,低电平为0V。那么,一个占空比为30%的PWM波,其直流分量就是 3.3V * 30% = 0.99V。这个“直流分量”就是我们想要获取的模拟电压。但问题在于,我们的PWM信号里除了这个想要的0.99V直流,还混杂着频率为PWM基频及其倍频的大量交流谐波。这些高频成分对于需要平滑直流电压的场合(比如作为运放的参考电压、控制VCO的压控端)来说是必须滤除的噪声。

2.2 RC低通滤波器的角色

这时,RC低通滤波器就登场了。它由一个电阻(R)和一个电容(C)组成,其截止频率(f_c)由公式f_c = 1 / (2πRC)决定。这个滤波器的特性是:对于频率远低于f_c的信号,它几乎无衰减地让通过(增益≈1);对于频率远高于f_c的信号,则大幅衰减。

我们的设计目标就是:让PWM信号的直流分量(0Hz)完全通过,同时尽可能大地衰减PWM基频及其所有谐波。因此,滤波器的截止频率f_c必须设置得远低于PWM的频率(f_pwm)。通常,我们会让f_c < f_pwm / 10,甚至更低。这样,高频的方波成分被滤除,电容C两端的电压就会趋于一个稳定的平均值,也就是我们计算出来的直流电压。电容在这里起到了积分和储能的作用,电阻则限制了充放电的电流速度。

注意:这里说的“直流电压”是一个平均效果。实际上,滤波后的电压并非一条绝对平滑的直线,而是在目标电压值上下有一个微小的纹波(Ripple)。纹波的大小直接反映了滤波效果的好坏,也是衡量PWM DAC性能的关键指标之一。

2.3 分辨率与动态性能的权衡

PWM DAC的分辨率取决于定时器产生PWM的精度。例如,如果我们使用一个16位的定时器(如TIM1)的自动重载寄存器(ARR)来设定周期,那么占空比就可以从0调节到ARR的值。此时,PWM DAC的理论分辨率就是16位(65536级)。这意味着我们可以通过改变比较寄存器(CCR)的值,输出65536个不同的平均电压值。

但是,高分辨率和高动态性能(快速响应)是一对矛盾。为了提高分辨率,我们希望ARR尽可能大,这样电压细分更精细。但ARR增大,在定时器时钟不变的情况下,PWM的周期(T_pwm = (ARR+1)/f_clk)就会变长,频率f_pwm就会降低。如前所述,为了有效滤波,我们需要更低的截止频率f_c,这通常意味着需要增大R或C的值。更大的RC时间常数会导致滤波器响应变慢——当你改变PWM占空比后,需要更长的时间,输出电压才能稳定到新的目标值。这对于需要快速变化的模拟输出场景是不可接受的。

因此,在设计PWM DAC时,你必须在分辨率输出纹波响应速度之间做一个权衡。一个常见的折衷是:选择一个足够高的PWM频率(例如10kHz - 100kHz),使得滤波器可以做得比较小(R、C值小,响应快),同时通过使用高位数的定时器(如16位)来保证分辨率。即使最终由于纹波等因素,有效位数(ENOB)可能达不到16位,但12位以上的精度对于许多应用(如LED调光、简单音频提示音生成)已经绰绰有余。

3. STM32定时器PWM生成机制深度复盘

理解了原理,我们来看在STM32上如何精准地产生所需的PWM波。这里以STM32F103的通用定时器TIM3的通道1(PA6)为例,但原理适用于大多数定时器。

3.1 定时器时钟树与频率计算

一切始于时钟。STM32的定时器时钟(CK_INT)通常来源于APB1或APB2总线。以TIM3为例,它挂在APB1上。你需要清楚APB1的预分频系数。假设系统主频72MHz,APB1预分频器为2,则APB1时钟为36MHz。但STM32有一个设计:当APB预分频系数不为1时,定时器的时钟会是APB时钟的2倍。因此,此时TIM3的时钟CK_INT = 36MHz * 2 = 72MHz。这是计算PWM频率的基准。

PWM频率公式为:Fpwm = CK_INT / ((ARR + 1) * (PSC + 1))其中:

  • CK_INT:定时器内部时钟频率。
  • PSC(预分频器寄存器):对CK_INT进行分频,用于调整计数器的计数时钟(CK_CNT)。
  • ARR(自动重载寄存器):决定了计数器的周期,计数器从0计数到ARR。

例如,我们希望产生一个36kHz的PWM(这个频率相对较高,有利于后续滤波)。设定PSC=0(即不分频),则CK_CNT = CK_INT = 72MHz。那么ARR = (72MHz / 36kHz) - 1 = 2000 - 1 = 1999。这样,计数器就在0~1999之间循环,产生36kHz的方波。

3.2 PWM模式与通道配置详解

STM32的定时器支持多种PWM模式,最常用的是PWM模式1PWM模式2。它们的区别在于输出电平极性在计数比较时的逻辑。

  • PWM模式1:在向上计数时,当计数器值小于比较寄存器(CCR)值时,通道输出有效电平(由CCxP位定义);大于或等于CCR时,输出无效电平。
  • PWM模式2:逻辑与模式1相反。

通常,我们设置“有效电平”为高电平。那么,在PWM模式1下,占空比 = CCR / (ARR + 1)。例如,ARR=1999,若CCR=600,则占空比 = 600 / 2000 = 30%。

配置步骤的关键点:

  1. 时基单元初始化:设置PSC和ARR,决定PWM频率。
  2. 输出比较初始化:配置通道为PWM模式(例如PWM模式1),选择输出极性(通常先高后低,即有效电平为高),并启用预装载寄存器(这样CCR的更新可以在下一个更新事件生效,避免周期中产生毛刺)。
  3. GPIO复用功能:将对应的GPIO引脚(如PA6)配置为复用推挽输出模式,并映射到定时器通道上(对于PA6是TIM3_CH1)。
  4. 使能:先使能定时器通道的输出(CCxE),最后再使能定时器计数器(CEN)。

一个容易忽略的细节是重装载时机。务必确保在修改ARR或CCR时,如果应用正在运行,最好使用定时器的“预装载”功能,并通过生成更新事件(UG位)或在合适的时机(如一个PWM周期结束时)来让新值生效,以避免输出异常的脉冲。

3.3 动态调节DAC输出电压

PWM DAC的“DAC”功能就体现在动态调节CCR值上。输出电压Vout = Vhigh * (CCR / (ARR + 1)),其中Vhigh是GPIO高电平电压(约3.3V)。

如果你想实现一个12位精度的DAC(0~4095对应0~3.3V),而你的ARR固定为1999(约2000级),那么你的实际分辨率约为2000级,略低于12位。你可以通过软件进行线性映射。更常见的做法是,为了获得更好的分辨率,直接将ARR设置为最大值65535(16位),然后通过提高PSC来降低频率以满足滤波要求。此时,16位的CCR值直接就是你的DAC输入码值。

在代码中,改变输出电压就是一条语句的事:TIM3->CCR1 = target_code;。但背后,硬件定时器会精确地在每个周期内控制高电平的持续时间,从而改变方波的平均电压。

4. 硬件滤波电路设计与参数计算实战

软件产生了干净的PWM方波,接下来就要靠硬件电路来提取其中的直流分量了。一个无源RC低通滤波器是最简单、最经济的选择,但设计时需要考虑多个因素。

4.1 一阶RC滤波器设计

电路非常简单:PWM输出引脚接一个电阻R,然后连接到电容C到地,从电容两端取输出电压Vout。

关键参数计算:

  1. 确定PWM频率(f_pwm):如前所述,假设我们选择f_pwm = 36kHz。
  2. 确定滤波器截止频率(f_c):为了充分滤除基频,通常取f_c = f_pwm / 10f_c = f_pwm / 50。我们取f_c = f_pwm / 20 = 1.8kHz。更低的f_c滤波效果更好,但响应更慢。
  3. 选择电容C:电容值不宜过小(易受寄生参数影响)也不宜过大(体积大、成本高、漏电流可能影响精度)。通常选择0.1uF(104)、1uF、10uF等常见值。我们先选定C = 0.1uF。
  4. 计算电阻R:根据公式f_c = 1 / (2πRC),可得R = 1 / (2π * f_c * C)
    • 代入f_c = 1800Hz,C = 0.1e-6 F
    • R = 1 / (2 * 3.1416 * 1800 * 0.1e-6) ≈ 884 Ω
    • 我们取一个接近的标准值R = 1kΩ

此时,实际的截止频率f_c' = 1 / (2π * 1000 * 0.1e-6) ≈ 1592 Hz,仍然满足远小于36kHz的要求。

4.2 纹波电压估算与负载影响

即使经过滤波,输出电压上仍然存在纹波。这个纹波主要来源于PWM方波的高频成分没有被完全滤除。对于一阶RC滤波器,纹波电压(峰峰值,V_pp)可以近似估算为:

V_ripple_pp ≈ Vhigh * (T_pwm / (RC)) = Vhigh * (1 / (f_pwm * RC))

代入我们的参数:Vhigh=3.3V, f_pwm=36kHz, R=1kΩ, C=0.1uF。RC = 1000 * 0.1e-6 = 0.0001 sV_ripple_pp ≈ 3.3V * (1 / (36000 * 0.0001)) = 3.3V * (1 / 3.6) ≈ 0.92V

这个估算值看起来很大,但这是基于最粗略的线性充放电模型。实际上,由于滤波器的衰减,真实纹波远小于此。更准确的方法是通过仿真或实际测量。但公式告诉我们,降低纹波的有效方法是增大RC时间常数(增大R或C)或提高PWM频率

负载影响:如果你的DAC输出需要驱动一个负载(例如,接到一个运放的同相输入端,或一个轻负载电路),这个负载电阻(R_load)会与滤波电阻R并联,从而改变滤波器的实际时间常数和分压比。

  • 时间常数变化:等效电阻R_eq = R // R_load。如果R_load较小(负载较重),R_eq会显著减小,导致截止频率f_c升高,滤波效果变差,纹波增大。
  • 分压影响:输出电压不再是单纯的Vhigh * Duty,而是Vhigh * Duty * (R_eq / (R + R_eq))(忽略电容)。这会导致输出电压的绝对精度下降。

实操心得:因此,PWM DAC最好用于高阻抗负载的场合,比如运放的输入端。如果必须驱动一定负载,可以在RC滤波器后增加一个电压跟随器(同相放大器,增益为1)进行缓冲。这个运放能提供低的输出阻抗,隔离后级负载对滤波器的影响,是提升PWM DAC实用性的关键一步。

4.3 进阶:二阶滤波与有源滤波

对于要求输出更干净、纹波更小的应用,一阶RC滤波器可能不够用。可以考虑二阶无源RC滤波器(两个RC级联),但其设计更复杂,且存在级间负载效应。更优秀的方案是使用有源低通滤波器,例如Sallen-Key或多重反馈(MFB)结构。它们利用运放提供增益和隔离,可以设计出更陡峭的滚降特性(如巴特沃斯、切比雪夫响应),在相同的截止频率下能更有效地抑制PWM频率成分。

例如,一个简单的单位增益Sallen-Key二阶低通滤波器,使用两个电阻和两个电容,配合一个运放,就能实现40dB/dec的衰减(一阶是20dB/dec)。这意味着对于36kHz的PWM,其抑制能力更强,输出纹波可以做得非常小。当然,电路成本和复杂度也相应增加。

5. 软件实现、校准与性能测试

硬件搭好后,我们需要用软件驱动它,并评估其实际性能。

5.1 初始化与输出电压控制函数

以标准外设库(虽然已停产,但理解原理重要)或HAL库为例,初始化流程清晰。以下是基于HAL库的核心代码逻辑:

// 1. 定时器PWM初始化 TIM_HandleTypeDef htim3; TIM_OC_InitTypeDef sConfigOC; htim3.Instance = TIM3; htim3.Init.Prescaler = 0; // PSC htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 1999; // ARR,对应36kHz @72MHz htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_PWM_Init(&htim3); sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 0; // 初始CCR值,占空比0% sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; HAL_TIM_PWM_ConfigChannel(&htim3, &sConfigOC, TIM_CHANNEL_1); // 2. 启动PWM HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); // 3. 设置DAC输出电压函数 void PWM_DAC_SetVoltage(float voltage) { if (voltage < 0.0f) voltage = 0.0f; if (voltage > 3.3f) voltage = 3.3f; // 假设Vhigh=3.3V // 计算CCR值: Duty = Vout / Vhigh uint32_t ccr_value = (uint32_t)((voltage / 3.3f) * (htim3.Init.Period + 1)); // 更新CCR寄存器 __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, ccr_value); }

5.2 非线性校准与精度提升

理想情况下,Vout = Vhigh * (CCR / (ARR+1))。但现实中存在误差:

  1. GPIO输出电平误差:Vhigh并非精确的3.3V,会随电源和负载轻微波动。
  2. 滤波器非线性:由于二极管效应、电容漏电、运放偏移等,实际传递函数可能不是完美的线性。
  3. 量化误差:数字CCR值到模拟电压的离散化。

为了获得更高精度,可以进行简单的两点校准:

  • 设置CCR为0,用高精度万用表测量实际输出电压V_zero(理想为0V)。
  • 设置CCR为最大值(ARR),测量实际输出电压V_full(理想为3.3V)。
  • 在软件中建立线性修正公式:Vout_actual = V_zero + (CCR / ARR) * (V_full - V_zero)
  • 你可以反转这个公式,根据目标电压V_target反推出需要的CCR值:CCR = (V_target - V_zero) * ARR / (V_full - V_zero)

通过这种校准,可以显著消除系统的增益误差和偏移误差。

5.3 实测性能评估方法

搭建好电路并写好驱动后,如何评估这个PWM DAC的性能?

  1. 静态精度测试:用代码循环输出一系列递增的CCR值(例如,每隔100个CCR点输出一次),用精度较高的数字万用表测量对应的输出电压。绘制“CCR-电压”曲线,观察线性度。计算INL(积分非线性)和DNL(微分非线性)来量化精度。
  2. 纹波观测:使用示波器,将探头设置为交流耦合(AC coupling),垂直刻度调到mV级别,观察输出电压上的纹波。测量其峰峰值。这是衡量滤波器效果最直观的方法。
  3. 建立时间测试:编写一个让输出电压在0V和满量程电压之间阶跃跳变的程序。用示波器(直流耦合)观察输出电压波形,测量从跳变开始到电压稳定在目标值±1%误差带内所需的时间。这反映了DAC的响应速度。
  4. 带负载能力测试:在输出端接入一个可变的电阻负载(例如从100kΩ到1kΩ),观察在不同负载下,输出电压值的变化和纹波的增大情况。这能确定该PWM DAC的驱动能力边界。

从我实际测试的经验来看,一个设计良好的、采用一阶RC滤波并加电压跟随器缓冲的STM32 PWM DAC,在10kHz PWM频率、16位分辨率下,实现12位有效精度(ENOB)、纹波小于10mV、建立时间在几十毫秒量级是完全可行的。这对于许多非关键性的模拟信号生成需求,例如可调基准源、慢速波形生成、电机电流给定等,已经是一个性价比极高的解决方案。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询