发布时间:2026/8/29 9:42:08
STM32 PWM DAC实战:低成本实现模拟电压输出的原理、设计与性能优化 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 (ARR1)/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的通道1PA6为例但原理适用于大多数定时器。3.1 定时器时钟树与频率计算一切始于时钟。STM32的定时器时钟CK_INT通常来源于APB1或APB2总线。以TIM3为例它挂在APB1上。你需要清楚APB1的预分频系数。假设系统主频72MHzAPB1预分频器为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这个频率相对较高有利于后续滤波。设定PSC0即不分频则CK_CNT CK_INT 72MHz。那么ARR (72MHz / 36kHz) - 1 2000 - 1 1999。这样计数器就在0~1999之间循环产生36kHz的方波。3.2 PWM模式与通道配置详解STM32的定时器支持多种PWM模式最常用的是PWM模式1和PWM模式2。它们的区别在于输出电平极性在计数比较时的逻辑。PWM模式1在向上计数时当计数器值小于比较寄存器CCR值时通道输出有效电平由CCxP位定义大于或等于CCR时输出无效电平。PWM模式2逻辑与模式1相反。通常我们设置“有效电平”为高电平。那么在PWM模式1下占空比 CCR / (ARR 1)。例如ARR1999若CCR600则占空比 600 / 2000 30%。配置步骤的关键点时基单元初始化设置PSC和ARR决定PWM频率。输出比较初始化配置通道为PWM模式例如PWM模式1选择输出极性通常先高后低即有效电平为高并启用预装载寄存器这样CCR的更新可以在下一个更新事件生效避免周期中产生毛刺。GPIO复用功能将对应的GPIO引脚如PA6配置为复用推挽输出模式并映射到定时器通道上对于PA6是TIM3_CH1。使能先使能定时器通道的输出CCxE最后再使能定时器计数器CEN。一个容易忽略的细节是重装载时机。务必确保在修改ARR或CCR时如果应用正在运行最好使用定时器的“预装载”功能并通过生成更新事件UG位或在合适的时机如一个PWM周期结束时来让新值生效以避免输出异常的脉冲。3.3 动态调节DAC输出电压PWM DAC的“DAC”功能就体现在动态调节CCR值上。输出电压Vout Vhigh * (CCR / (ARR 1))其中Vhigh是GPIO高电平电压约3.3V。如果你想实现一个12位精度的DAC0~4095对应0~3.3V而你的ARR固定为1999约2000级那么你的实际分辨率约为2000级略低于12位。你可以通过软件进行线性映射。更常见的做法是为了获得更好的分辨率直接将ARR设置为最大值6553516位然后通过提高PSC来降低频率以满足滤波要求。此时16位的CCR值直接就是你的DAC输入码值。在代码中改变输出电压就是一条语句的事TIM3-CCR1 target_code;。但背后硬件定时器会精确地在每个周期内控制高电平的持续时间从而改变方波的平均电压。4. 硬件滤波电路设计与参数计算实战软件产生了干净的PWM方波接下来就要靠硬件电路来提取其中的直流分量了。一个无源RC低通滤波器是最简单、最经济的选择但设计时需要考虑多个因素。4.1 一阶RC滤波器设计电路非常简单PWM输出引脚接一个电阻R然后连接到电容C到地从电容两端取输出电压Vout。关键参数计算确定PWM频率f_pwm如前所述假设我们选择f_pwm 36kHz。确定滤波器截止频率f_c为了充分滤除基频通常取f_c f_pwm / 10到f_c f_pwm / 50。我们取f_c f_pwm / 20 1.8kHz。更低的f_c滤波效果更好但响应更慢。选择电容C电容值不宜过小易受寄生参数影响也不宜过大体积大、成本高、漏电流可能影响精度。通常选择0.1uF104、1uF、10uF等常见值。我们先选定C 0.1uF。计算电阻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))代入我们的参数Vhigh3.3V f_pwm36kHz R1kΩ C0.1uF。RC 1000 * 0.1e-6 0.0001 s。V_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; // 假设Vhigh3.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 / (ARR1))。但现实中存在误差GPIO输出电平误差Vhigh并非精确的3.3V会随电源和负载轻微波动。滤波器非线性由于二极管效应、电容漏电、运放偏移等实际传递函数可能不是完美的线性。量化误差数字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的性能静态精度测试用代码循环输出一系列递增的CCR值例如每隔100个CCR点输出一次用精度较高的数字万用表测量对应的输出电压。绘制“CCR-电压”曲线观察线性度。计算INL积分非线性和DNL微分非线性来量化精度。纹波观测使用示波器将探头设置为交流耦合AC coupling垂直刻度调到mV级别观察输出电压上的纹波。测量其峰峰值。这是衡量滤波器效果最直观的方法。建立时间测试编写一个让输出电压在0V和满量程电压之间阶跃跳变的程序。用示波器直流耦合观察输出电压波形测量从跳变开始到电压稳定在目标值±1%误差带内所需的时间。这反映了DAC的响应速度。带负载能力测试在输出端接入一个可变的电阻负载例如从100kΩ到1kΩ观察在不同负载下输出电压值的变化和纹波的增大情况。这能确定该PWM DAC的驱动能力边界。从我实际测试的经验来看一个设计良好的、采用一阶RC滤波并加电压跟随器缓冲的STM32 PWM DAC在10kHz PWM频率、16位分辨率下实现12位有效精度ENOB、纹波小于10mV、建立时间在几十毫秒量级是完全可行的。这对于许多非关键性的模拟信号生成需求例如可调基准源、慢速波形生成、电机电流给定等已经是一个性价比极高的解决方案。

相关新闻

2026/8/29 9:37:08

餐饮外卖系统Java毕设全解析:Spring Boot+小程序+MySQL

简介:在Java后端开发与微信小程序应用日益普及的背景下,如何实现一个功能完整、逻辑清晰的多角色外卖系统,成为很多开发者和毕业设计学生的关注焦点。文章从系统整体架构与角色划分出发,深入解析Spring Boot、微信小程序与MySQL三…

2026/8/29 9:37:08

梯级水电站调度优化建模:从程序包解压到算法实现完整指南

简介:水库调度是水利水电运行管理的核心环节,其目标在于协调发电、防洪、生态等多方需求。梯级水电站因上下游水库存在水力与电力双重耦合,使得调度优化建模比单库问题复杂得多,通常需要以发电量最大或保证出力最大为目标&#xf…

2026/8/29 9:52:09

自动驾驶生意的技术底盘:自动驾驶运营系统与单均成本拆解

美团外卖的商业模式并不复杂:平台连接用户和配送人群,用订单规模摊薄固定成本,靠履约效率和佣金体系形成利润。现在,自动驾驶正在尝试复制同一套逻辑——把车变成可调度的移动服务单元,用 Robotaxi、无人配送车、无人零…

2026/8/29 9:52:09

从 0 到第一个界面:Dear ImGui C++ UI 库上手笔记

从 0 到第一个界面:Dear ImGui C UI 库上手笔记 【免费下载链接】imgui Dear ImGui: Bloat-free Graphical User interface for C with minimal dependencies 项目地址: https://gitcode.com/GitHub_Trending/im/imgui 做引擎、仿真器或实时可视化程序时&…

2026/8/29 9:52:09

一台2011年的i3也能聊大模型:GPT4All本地LLM上手记录

一台2011年的i3也能聊大模型:GPT4All本地LLM上手记录 【免费下载链接】gpt4all GPT4All: Run Local LLMs on Any Device. Open-source and available for commercial use. 项目地址: https://gitcode.com/GitHub_Trending/gp/gpt4all GPT4All 是 Nomic 开源的…

2026/8/29 9:47:08

Hoppscotch API 测试工具快速上手:从在线版到自建部署

Hoppscotch API 测试工具快速上手:从在线版到自建部署 【免费下载链接】hoppscotch Open-Source API Development Ecosystem • https://hoppscotch.io • Offline, On-Prem & Cloud • Web, Desktop & CLI • Open-Source Alternative to Postman, Insomni…

2026/8/28 16:16:17

[光学原理与应用-521]:对光的错误理解与纠偏

首先光是一种能量的载体和形态,宏观上观察到的光是由无数个微观的光量子组成的,每个光子在产生的瞬间,其在真空的空间中以确定不变的速度沿着一个初始的方向一直向前,在微观层面,每个光量子的运动轨迹是以波函数所展现…

2026/8/28 16:16:21

SIP通话转接原理与REFER方法实战解析

1. 通话转接不是“挂断再拨号”,而是SIP会话的动态重定向你有没有遇到过这样的场景:客服坐席A正在和客户通电话,突然需要把这通对话无缝转给专家坐席B,客户完全感知不到中间的断连——既没听到忙音,也没被要求重新拨号…

2026/8/28 16:16:22

Kolla-ansible单节点OpenStack部署实战:从环境准备到排坑指南

1. 为什么选择Kolla-ansible来部署单节点OpenStack?如果你正在寻找一种能把OpenStack从“概念”快速变成“可用的实验环境”的方法,那么Kolla-ansible几乎是当前最主流、最省心的选择。我见过太多人卡在手动编译依赖、配置服务、处理版本冲突的泥潭里&am…

2026/8/29 0:01:10

etc目录下的profile.d文件目录设置环境变量和全局脚本shell

一、设置环境变量etc目录下的profile.d文件目录 /etc/profile.d1、编写 vi test.sh文件内容# jdk变量 export ZHK_HOME/root export PATH$PATH:$ZHK_HOME/test # 可以取出来ZHK_HOME变量给ZZZ_HOME赋值 export ZZZ_HOME${ZHK_HOME}/test2、刷新 执行source /etc/profile 命令使…

2026/8/29 0:01:10

【JavaScript】内存管理-垃圾回收机制-内存泄露

内存管理 C 语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()。 而 JavaScript 是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。释放的过程称为垃圾回收。 整…

2026/8/29 0:01:10

Labgrid-MCP:为嵌入式硬件实验室接入AI Agent操控能力

Labgrid-MCP 的目标是把 MCP(Model Context Protocol)能力延伸到真实嵌入式硬件实验室:AI Agent 通过一个标准化的 MCP Server,就能查看目标板状态、控制上电断电、复位开发板、读取串口日志,甚至执行镜像刷写。对于经…

2026/8/28 16:16:48

实测才敢推 AI论文网站 2026最新测评与推荐

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。一、综…

2026/8/28 16:16:50

2026必备!AI论文网站测评:最新推荐与深度对比

2026年真正好用的AI论文网站,核心看生成的论文质量、低AI味、格式正确、学术适配四大指标。综合实测,千笔AI、ThouPen、豆包、DeepSeek、Grammarly 是当前最值得推荐的梯队,覆盖从免费到付费、从中文到英文、从文科到理工的全场景需求。 一、…

2026/8/28 11:06:45

摆脱论文困扰!盘点2026年全网爆红的的AI论文写作工具

一天写完毕业论文在2026年已不再是天方夜谭。2026年最炸裂、实测能大幅提速的AI论文写作工具,覆盖选题构思、文献整理、内容生成、格式排版等核心场景,真正帮你高效搞定论文难题。 一、全流程王者:一站式搞定论文全链路(一天定稿首…