拓冰建站拓冰建站
首页 / 资讯中心 / 正文

STM32 PWM与DAC实战:从原理到示波器验证的嵌入式信号输出指南

1. 项目概述从信号到声音的STM32实战如果你正在学习STM32或者已经用它做过一些简单的GPIO控制、串口通信那么“PWM和DAC”这两个词对你来说可能既熟悉又有点模糊。熟悉是因为它们频繁出现在电机控制、音频输出、电源管理等场景模糊则是因为如果不亲手用示波器抓一下波形不亲自写代码让蜂鸣器唱出个调调你很难真正理解数字世界是如何“模拟”出连续变化的物理量的。这个练习的核心就是打通这个认知闭环。它不是简单地配置几个寄存器而是让你理解在嵌入式系统中我们如何用定时器的精准节拍PWM和数模转换器的“翻译”能力DAC去驱动真实世界中的设备并用示波器这个“眼睛”去验证你的代码是否如你所想。简单来说这个项目就是利用STM32的片上资源分别产生PWM波和DAC模拟电压信号并通过示波器观察其波形从而深入掌握这两种关键输出方式的原理、配置方法和应用场景。对于初学者这是从“点灯”到“控制”的关键一步对于有经验的开发者这是夯实基础、排查复杂问题的必备技能。接下来我会以一个STM32F103系列芯片为例手把手带你走通整个流程并分享那些数据手册和教程里不会写的“坑”和技巧。2. 核心硬件与原理深度解析在动手写代码之前我们必须把PWM和DAC的“家底”和“工作方式”摸清楚。很多配置错误和调试困境根源都在于对原理的一知半解。2.1 PWM用数字开关“模拟”出平均值PWM脉宽调制听起来高大上其实原理很直观。想象一下你用一个非常快的速度比如每秒1万次去反复开关水龙头。如果开的时间长关的时间短那么平均下来水流就大反之平均水流就小。PWM做的就是这件事只不过它控制的是电压。在STM32中PWM功能通常由高级定时器TIM1, TIM8或通用定时器TIM2-TIM5等的“输出比较”模式产生。其核心是三个寄存器自动重装载寄存器ARR、捕获/比较寄存器CCR和计数器CNT。工作流程如下计数器CNT从0开始每个时钟周期加1。当CNT的值小于CCR时对应的输出通道比如CH1被设置为有效电平高或低可配置。当CNT的值大于等于CCR但小于ARR时输出通道被设置为无效电平。当CNT的值达到ARR时产生一个更新事件UEVCNT被硬件清零重新开始计数如此循环。这里就引出了两个关键概念频率周期由ARR和定时器时钟频率共同决定。PWM频率 定时器时钟频率 / (ARR 1)。ARR决定了计数器的“天花板”。占空比由CCR和ARR的比值决定。占空比 CCR / (ARR 1)。CCR决定了有效电平的持续时间。例如定时器时钟为72MHzARR设置为7199则PWM频率为72MHz / (71991) 10kHz。若CCR设置为3600则占空比为3600 / (7200) 50%。注意这里“ARR1”是因为计数器从0计数到ARR总共是ARR1个计数周期。这是新手最容易算错频率和占空比的地方。2.2 DAC数字代码的“模拟化身”DAC数模转换器它的工作更像一个“翻译官”。它接收一个数字代码比如8位、12位然后输出一个与之成比例的模拟电压。STM32的DAC通常是12位分辨率这意味着它可以将0到40952^12 - 1的数字值线性地映射到一个参考电压范围内通常是0V到VDDA比如3.3V。其输出电压公式很简单Vout (DOR / 4095) * Vref。其中DOR是数据输出寄存器中的值Vref是参考电压通常接VDDA。STM32的DAC使用起来比PWM更“直接”因为它输出的是真正的、平滑在微观上仍是阶梯的直流电压。但它也有局限输出能力弱驱动电流小需要外接运放才能驱动大多数负载并且转换速度相比PWM的切换速度要慢。2.3 示波器不可或缺的“第三只眼”无论你的代码逻辑多么清晰最终都要用示波器来验证。对于这个练习一台带宽100MHz、双通道的数字示波器就完全够用了。关键是要学会用它测量PWM使用示波器的频率/周期测量功能以及占空比测量功能直接读取数值与你的代码计算值对比。测量DAC将示波器调到直流耦合DC观察输出的电压电平是否稳定是否符合VDD * (数值/4095)的计算结果。当你让DAC输出一个变化的波形如正弦波时用示波器的自动设置或手动调整时基和幅值以观察到完整的波形。很多人代码写完后烧录进去发现设备没反应就懵了。第一步永远应该是用示波器探头点一下对应的引脚看有没有信号出来。没有信号就检查硬件连接、时钟配置、GPIO模式有信号但不对再检查代码计算。3. 开发环境搭建与基础工程创建工欲善其事必先利其器。这里我推荐使用STM32CubeIDE它集成了STM32CubeMX图形化配置工具和基于Eclipse的IDE对新手非常友好也能满足老手的需求。3.1 工程创建与时钟树配置新建工程打开STM32CubeIDE选择对应的芯片型号如STM32F103C8T6。配置时钟最关键的一步进入Clock Configuration标签页。对于STM32F103我们通常使用外部8MHz晶振HSE通过PLL倍频到72MHz作为系统时钟SYSCLK。在图形界面上将HSE选为Crystal/Ceramic Resonator。将PLL Source Mux选择为HSE。设置PLL Mul为9倍频因为8MHz * 9 72MHz。将System Clock Mux选择为PLLCLK。此时HCLK、PCLK1、PCLK2应该会自动计算出来。注意APB1总线时钟PCLK1最高36MHzAPB2总线时钟PCLK2最高72MHz。定时器如果挂在APB1上其时钟可能还会被倍频如果APB1预分频系数不为1这个CubeMX会帮你处理好。实操心得务必点击“OK”或“Apply”让配置生效。然后回到Pinout Configuration视图你会发现之前灰色的外设如定时器现在可以配置了因为它们的时钟源已经就绪。3.2 GPIO与外围设备引脚分配在Pinout Configuration的芯片图形上或者左侧的Categories列表里找到我们需要的外设PWM我们选择TIM3的通道1CH1来产生PWM。找到TIM3将Channel1的模式设置为PWM Generation CH1。软件会自动将对应的引脚如PA6配置为复用推挽输出Alternate Function Push Pull。你可以右键点击引脚给它起个别名比如“PWM_OUT”。DAC找到DAC将OUT1或OUT2使能。对应的引脚如PA4对应DAC_OUT1PA5对应DAC_OUT2会自动配置为模拟输入Analog模式这是DAC输出所必需的。调试接口务必使能SYS-Debug为Serial Wire这样才能使用ST-LINK进行下载和调试。配置完成后点击右上角的“GENERATE CODE”生成初始化代码。4. PWM输出配置与波形生成实战代码生成后我们进入main.c的用户代码区位于/* USER CODE BEGIN */和/* USER CODE END */之间添加功能。4.1 定时器PWM模式详解与初始化在main()函数的初始化部分/* USER CODE BEGIN 2 */之后我们需要启动PWM输出。/* 启动TIM3的PWM通道1输出 */ HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1);但在这之前理解htim3这个结构体是如何被CubeMX初始化的至关重要。我们打开tim.c文件找到MX_TIM3_Init函数htim3.Instance TIM3; htim3.Init.Prescaler 71; // 预分频系数 htim3.Init.CounterMode TIM_COUNTERMODE_UP; // 向上计数模式 htim3.Init.Period 1999; // 自动重载值ARR htim3.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; // 使能ARR预装载Prescaler预分频器定时器时钟源如72MHz先经过Prescaler1分频才作为计数器的计数时钟。这里设置为71则计数器时钟 72MHz / (711) 1MHz。Period周期即ARR的值设置为1999。因此PWM频率 计数器时钟 / (ARR1) 1MHz / (19991) 500Hz。占空比则由通道的捕获/比较寄存器CCR决定在HAL_TIM_PWM_ConfigChannel函数中配置。例如如果Pulse即CCR值设置为1000则初始占空比 1000 / (19991) 50%。注意事项AutoReloadPreload设置为ENABLE是个好习惯。这意味着你对ARR或CCR的修改会先写入预装载寄存器等到下一个更新事件时才生效。这可以防止在修改参数的过程中产生毛刺或错误的PWM脉冲。如果你需要实时、立即改变占空比比如电机控制中有时会禁用此功能但需要更精细的时序控制。4.2 动态调整PWM频率与占空比生成固定波形只是第一步动态控制才是应用的核心。我们可以通过修改ARR和CCR来实时改变频率和占空比。/* 动态修改占空比为75% */ __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 1500); // CCR 1500, 占空比1500/200075% /* 动态修改PWM频率为1kHz (ARR 999) */ __HAL_TIM_SET_AUTORELOAD(htim3, 999); // 注意修改ARR后如果占空比需要保持不变CCR也需要按比例调整。 // 例如原占空比50%原ARR1999CCR1000。新ARR999则新CCR应设为500以保持50%占空比。 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, 500);重要提示直接使用__HAL_TIM_SET_COMPARE和__HAL_TIM_SET_AUTORELOAD这些宏是立即生效的在预装载使能时是下一个周期生效。在复杂的应用中特别是修改ARR时为了保持波形连续无毛刺最好在计数器为0或一个安全点时进行修改。HAL库提供了__HAL_TIM_GET_COUNTER来获取当前计数值你可以判断后再修改。4.3 多通道PWM与同步输出一个定时器往往有多个通道CH1-CH4可以独立配置CCR但共享同一个ARR即频率相同。这使得它可以轻松驱动多个需要同步控制的设备比如RGB LED的三色灯珠或者四轴飞行器的四个电调。// 假设TIM3的CH1, CH2, CH3都已配置为PWM输出 HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_2); HAL_TIM_PWM_Start(htim3, TIM_CHANNEL_3); // 分别设置不同的占空比实现RGB混色 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_1, red_value); // 红色亮度 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, green_value); // 绿色亮度 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_3, blue_value); // 蓝色亮度5. DAC输出配置与波形合成实战DAC的配置相对PWM更简单但用它来生成复杂波形则需要一些算法和技巧。5.1 DAC基础配置与直流电压输出在CubeMX中使能DAC通道后在main.c中启动DAC。/* 启动DAC通道1并启用其输出缓冲区 */ HAL_DAC_Start(hdac, DAC_CHANNEL_1);输出一个固定的直流电压非常简单/* 输出一个1.65V的电压 (假设Vref3.3V) */ // 计算数字值 1.65V / 3.3V * 4095 2047.5 ≈ 2048 uint32_t dac_value 2048; HAL_DAC_SetValue(hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_value);DAC_ALIGN_12B_R表示数据是12位右对齐。你也可以使用DAC_ALIGN_12B_L左对齐或DAC_ALIGN_8B_R8位右对齐但需要注意传入的数据格式。5.2 使用DMA生成任意波形以正弦波为例让DAC输出变化的波形如果靠CPU频繁调用HAL_DAC_SetValue会消耗大量资源且定时不准。最佳实践是使用DMA直接存储器访问来自动搬运波形数据到DAC数据寄存器。步骤分解生成波形表在内存中预先计算好一个周期波形如正弦波的采样点数组。#define SINE_WAVE_SAMPLES 100 // 一个周期的采样点数 uint16_t sine_wave[SINE_WAVE_SAMPLES]; // 生成正弦波表值范围在0-4095之间12位DAC for(int i 0; i SINE_WAVE_SAMPLES; i) { // 正弦函数值从-1到1我们将其映射到0-4095 sine_wave[i] (uint16_t)((sin(2 * M_PI * i / SINE_WAVE_SAMPLES) 1.0) * 4095 / 2.0); }CubeMX配置DAC DMA在DAC配置界面找到DAC OUT1 Settings或OUT2。将Mode设置为Normal。在DMA Settings标签页点击Add选择DAC1Channel1方向为Memory To Peripheral。模式选择Circular循环模式这样DMA会周而复始地搬运数据形成连续波形。数据宽度选择Half Word半字16位因为我们的数据是uint16_t。代码中启动DAC DMA/* 启动DAC通道1并使用DMA传输数据 */ HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t*)sine_wave, SINE_WAVE_SAMPLES, DAC_ALIGN_12B_R);执行这行代码后DMA会自动、不间断地将sine_wave数组中的数据搬运到DAC的DHR12R1寄存器从而在引脚上输出连续的正弦波。计算与调整输出频率输出频率DAC触发频率/SINE_WAVE_SAMPLES。DAC的触发源可以配置为定时器TIM。例如我们配置一个定时器TIM6每100us产生一次更新事件UEV去触发DAC转换。那么DAC触发频率 1 / 100us 10kHz。如果采样点SINE_WAVE_SAMPLES为100则最终输出的正弦波频率 10kHz / 100 100Hz。通过改变定时器的触发频率或波形表的采样点数可以灵活调整输出波形的频率。实操心得使用DMA输出波形时务必确保波形数据数组位于内存中通常是全局变量并且其生命周期覆盖整个输出过程。如果使用局部变量函数退出后数组内存被释放DMA访问将导致不可预知的错误通常是硬件错误HardFault。6. 示波器测量与波形分析技巧代码写完了烧录进去真正的调试才刚刚开始。示波器是你的终极裁判。6.1 PWM波形测量要点探头校准与连接使用×10档位探头并先进行补偿校准。将探头地线夹在板子的GND上探针接触PWM输出引脚如PA6。触发设置将触发模式设为“边沿触发”触发源选择你正在测量的通道触发电平设置为PWM幅值的一半左右如1.6V。这能稳定波形。关键参数测量频率/周期使用示波器的自动测量功能直接读取“Freq”和“Period”。与代码计算的1MHz/(ARR1)进行对比。占空比测量“Duty Cycle”或“正脉宽”。与CCR/(ARR1)的计算值对比。幅值测量“Vpp”峰峰值和“Vmax”。PWM的幅值应该是你单片机的IO口电压如3.3V。如果幅值不对检查GPIO配置必须是复用推挽输出和外部负载是否过重。观察动态调整在代码中插入延时循环改变CCR值在示波器上你应该能看到占空比平滑变化。使用示波器的“单次”触发或“滚动”模式可以捕捉到这种变化。6.2 DAC波形测量要点耦合方式务必选择“DC”耦合。AC耦合会滤掉直流分量使你无法准确测量直流电压值。观察直流输出设置DAC输出一个固定值如2048示波器应显示一条稳定的水平线电压值约为1.65V。使用示波器的“GND”基准校正零位然后使用“测量”功能里的“Mean”平均值或“DC RMS”来读取电压。观察动态波形正弦波首先使用“Auto Set”让示波器自动捕捉波形。调整时基Time/Div使屏幕上显示2-3个完整的波形周期为宜。调整幅值Volts/Div使波形占据屏幕垂直方向的约2/3。使用“测量”功能查看波形的“Freq”频率、“Vpp”峰峰值、“Max”、“Min”。峰峰值应该接近参考电压Vref3.3V频率应与你通过触发频率/采样点数计算的结果一致。检查波形失真阶梯状这是正常的因为DAC输出是离散的采样点。提高采样点数SINE_WAVE_SAMPLES可以使波形更平滑但会降低最大输出频率。毛刺或非对称可能是DMA传输被中断打断或者定时器触发不稳定。尝试提高DMA和定时器中断的优先级。幅值不足或削顶检查DAC的输出缓冲区是否使能建议使能以增强驱动能力检查外部电路是否有负载过重或短路。DAC直接驱动能力很弱通常需要接一个电压跟随器运放构成。7. 进阶应用与故障排查实录掌握了基础操作后我们可以尝试一些更有挑战性的应用并系统梳理常见问题。7.1 应用场景延伸用PWM和DAC驱动外围设备PWM驱动LED调光这是最经典的应用。将LED串联一个限流电阻后接到PWM引脚和GND之间。通过改变PWM占空比可以无级调节LED亮度。人眼对光强有非线性感知如PWM频率低于100Hz会有闪烁感通常需要将PWM频率设置在1kHz以上并使用Gamma校正表来使亮度变化更符合人眼感受。PWM经滤波电路产生模拟电压在PWM输出引脚后连接一个低通滤波器一个电阻加一个电容到地可以将PWM的方波“平均”成一个直流电压。滤波器的截止频率需要远低于PWM频率。这样可以用一个GPIO引脚实现简单的DAC功能成本低但精度和响应速度较差。DAC生成音频信号结合DMA和高质量波形表如WAV文件解码后的PCM数据DAC可以直接驱动耳机需加耦合电容和简单放大电路或音频功放播放音乐或提示音。关键在于音频采样率如44.1kHz和DAC触发定时器的精准匹配。PWM控制舵机舵机控制信号是一个周期为20ms50Hz脉宽在0.5ms到2.5ms之间的PWM波。用STM32的定时器可以非常精准地产生这个信号。计算ARR和CCR时要确保定时器时钟分频后一个计数周期的时间足够精细例如1us这样才能精确控制0.5ms到2.5ms的脉宽。7.2 常见问题、原因与解决方案速查表下表总结了在STM32 PWM和DAC练习中从零开始最容易遇到的典型问题及其解决方法。现象可能原因排查步骤与解决方案PWM无输出1. 定时器时钟未使能。2. GPIO模式配置错误不是AF_PP。3. 未调用HAL_TIM_PWM_Start()。4. 引脚被其他外设占用。1. 检查CubeMX中定时器的时钟源是否已开启如APB1/APB2总线时钟。2. 在CubeMX或代码中确认引脚模式为Alternate Function Push Pull。3. 确保在初始化后调用了启动函数。4. 检查CubeMX引脚分配图确认无冲突红色警告。PWM频率/占空比不对1. ARR或CCR计算错误注意1。2. 定时器预分频器PSC设置错误。3. 计数器时钟源频率不对。1. 重新核对公式频率 时钟 / ((PSC1)*(ARR1))。2. 使用示波器测量实际频率反向推算时钟。3. 检查System Core-RCC和Clock Configuration确认系统时钟和总线时钟配置正确。DAC无输出或电压不对1. DAC通道未使能或未启动。2. 引脚模式不是模拟输入Analog。3. 参考电压VDDA未连接或异常。4. 输出缓冲区未使能驱动能力太弱。1. 检查HAL_DAC_Start()是否调用。2. DAC输出引脚必须配置为Analog模式。3. 测量VDDA引脚电压确保为3.3V左右并稳定。4. 在CubeMX的DAC配置中将Output Buffer设置为Enable。DAC DMA输出波形混乱1. DMA或波形数据数组配置错误。2. 数据对齐方式DAC_ALIGN不匹配。3. 定时器触发频率设置过高DAC转换跟不上。4. 数组是局部变量已被释放。1. 检查DMA配置外设地址是DAC数据寄存器内存地址是数组长度正确循环模式。2. 确保HAL_DAC_Start_DMA中的对齐方式与数组数据类型匹配12位右对齐对应uint16_t数据。3. 降低触发定时器的频率。4. 将波形数据数组定义为全局变量或静态变量。示波器测量噪声大1. 探头地线环路过长。2. 板子电源噪声大。3. 数字信号对模拟信号的干扰。1. 使用探头附带的弹簧接地针而不是长长的鳄鱼夹地线。2. 在板子的电源入口和芯片电源引脚处增加滤波电容如10uF钽电容并联0.1uF陶瓷电容。3. 布局上让模拟部分远离数字部分或使用磁珠隔离。对于DAC输出可以跟随一个运放做缓冲和滤波。修改参数后波形异常1. 修改ARR或CCR的时机不对导致产生残缺脉冲。2. 预装载寄存器未使能修改后立即生效产生冲突。1. 在计数器为0或一个较低值时更新参数。可以使用TIM_EGR寄存器的UG位软件产生更新事件让参数立即生效并重置计数器。2. 确保AutoReloadPreload和CCXPreload通道预装载在CubeMX中已使能。7.3 性能优化与高级技巧当基本功能实现后为了追求更精准、更稳定的性能可以考虑以下优化定时器时钟源选择高级定时器TIM1, TIM8的时钟可以来自系统时钟也可以来自外部时钟。对于极高精度的PWM应用如数字电源可以考虑使用外部高精度晶振作为时钟源并通过定时器的“外部时钟模式”输入来减少系统时钟抖动带来的影响。互补输出与死区插入在电机驱动如H桥中需要一对互补的PWM信号来控制上下桥臂并且必须插入“死区时间”防止上下桥臂直通短路。STM32的高级定时器TIM1, TIM8硬件支持带死区插入的互补PWM输出只需在CubeMX中配置Dead Time参数即可这是软件模拟无法比拟的精准和可靠。DAC输出缓冲与运放选型STM32片内DAC的输出阻抗较高直接驱动负载会导致电压跌落。使能片内输出缓冲区可以改善但其性能有限带宽、压摆率。对于音频或精密测量外接一个高性能的运放作为电压跟随器是标准做法。选择运放时需关注其压摆率Slew Rate、增益带宽积GBW和输入失调电压Vos。DMA双缓冲模式对于需要实时更新波形数据的应用如音频播放可以使用DMA的双缓冲模式Double Buffer。当DMA在传输其中一个缓冲区Buffer0的数据时CPU可以准备下一个缓冲区Buffer1的数据。当Buffer0传输完成DMA会自动切换到Buffer1并产生一个半传输完成或传输完成中断通知CPU去填充已传输完的Buffer0。这样可以实现无缝的数据流避免音频播放中的“咔嗒”声或波形断裂。经过这一整套从原理到配置、从代码到测量、从基础到进阶的练习你应该对STM32的PWM和DAC有了肌肉记忆般的理解。记住嵌入式开发是软硬件结合的藝術示波器是你连接数字思维和模拟世界的桥梁。多动手多测量多思考“为什么”每一个波形异常的背后都藏着一个让你功力增长的知识点。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门