尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32 PWM与DAC实战:从信号原理到音频输出实现

STM32 PWM与DAC实战:从信号原理到音频输出实现 1. 项目概述从信号到声音的实践之旅在嵌入式开发领域尤其是基于STM32这类主流MCU的项目中PWM脉冲宽度调制和DAC数模转换器是两种极为核心的模拟信号生成技术。很多朋友在初学阶段可能只是照着例程让一个LED灯呼吸闪烁或者输出一个固定的电压但往往知其然不知其所以然。这个练习的目的就是要把这两项技术从“会用”提升到“懂用”的层次通过一个连贯的、有实际意义的项目深入理解它们的工作原理、应用场景以及如何协同工作。简单来说我们将利用STM32的PWM来模拟一个数字音频信号再通过DAC将其转换为真实的模拟音频波形最终驱动一个扬声器或耳机播放出我们“编程”出来的音乐或音效。这个过程就像是在数字世界和模拟世界之间架起了一座桥梁非常有趣且富有成就感。无论你是刚接触STM32的新手还是想巩固模拟外设知识的开发者这个练习都能让你收获颇丰。它不仅涉及硬件定时器、GPIO、DMA等外设的配置更考验你对信号处理、采样率、量化精度等概念的理解。我会以一个具体的工程为例手把手带你走通整个流程并分享我在调试过程中踩过的坑和总结出的技巧。我们使用的硬件平台是常见的STM32F103系列比如“蓝桥杯”竞赛板或正点原子的Mini板软件环境是Keil MDK或STM32CubeIDE这些工具链大家应该都比较熟悉。2. 核心硬件与原理深度解析2.1 PWM不仅仅是控制LED亮度PWM的本质是通过调节一个数字信号在一个周期内高电平所占的时间比例即占空比来等效地获得一个可变的平均电压。对于STM32来说PWM功能通常由高级定时器TIM1, TIM8或通用定时器TIM2-TIM5的“输出比较”模式产生。这里有几个关键参数需要彻底理解定时器时钟TIMx_CLK这是PWM频率的基石。它来源于系统时钟SYSCLK经过预分频器PSC后的结果。计算实际计数器时钟频率的公式是CK_CNT TIMx_CLK / (PSC 1)。自动重装载值ARR这个值决定了计数器的周期。计数器从0计数到ARR然后归零重新开始这就定义了一个PWM周期。PWM的频率计算公式为PWM_Freq CK_CNT / (ARR 1)。捕获/比较寄存器CCRx这是我们控制占空比的核心。当计数器的值小于CCRx时输出一种电平通常为高大于等于CCRx时输出另一种电平通常为低。占空比 CCRx / (ARR 1)。注意在设置ARR和CCRx时务必注意它们的数值范围16位定时器是0-65535。同时STM32的PWM模式有“PWM模式1”和“PWM模式2”主要区别在于输出极性需要根据实际硬件电路比如是共阳极还是共阴极LED来正确选择。2.2 DAC将数字代码变为真实电压STM32内部的DAC模块其核心是一个电阻网络或电容阵列它能够将我们写入DAC数据寄存器的数字值比如12位精度下是0-4095线性地转换为一个模拟电压。对于STM32F103其DAC输出电压范围通常是0到Vref参考电压一般接3.3V。DAC的工作模式值得我们关注软件触发需要代码手动更改DAC数据寄存器来更新输出。响应慢不适合连续信号。定时器触发可以通过定时器更新事件自动触发DAC转换这是生成连续波形如音频的关键。DMA传输这是本练习的精华所在。我们可以预先把一段代表音频波形的数字序列一个数组存放在内存中然后配置DMA让其在定时器的触发下自动、不间断地将数组中的数据搬运到DAC数据寄存器中。这样CPU就被解放出来可以处理其他任务而音频输出则流畅无比。PWM与DAC的关联与区别PWM输出的仍然是数字方波其“模拟”效果是靠外部滤波电路电阻电容将方波平滑成直流电压来实现的。而DAC是真正在芯片内部输出一个连续变化的模拟电压。用PWM来“模拟”DAC实际上是通过生成一个其占空比随时间变化的PWM波再经过低通滤波器滤除高频开关噪声得到近似的模拟信号。这种方式成本低但精度和动态响应通常不如真正的DAC。我们这个练习就是要体验这两种方式并理解它们的优劣。3. 工程设计与软件架构思路我们的目标是生成一段简单的音频比如一个1kHz的正弦波。我们将设计两种实现方案进行对比。3.1 方案一PWM模拟音频输出这个方案的核心思想是用高频率的PWM载波通过动态调整其占空比来“描绘”出我们想要的低频音频信号波形。信号生成首先我们需要在代码中创建一个数组比如pwm_sine_table[100]。这个数组存放的不是电压值而是对应正弦波每个采样点的PWM占空比值。计算公式为value (sin(2*π*i/100) 1) / 2 * (ARR_MAX)。这里加1除以2是为了将正弦波的[-1,1]范围映射到[0, ARR_MAX]的占空比范围。硬件定时器配置选择一个定时器如TIM3的某个通道如CH2输出PWM。设置PWM频率载波频率。这里有个关键点根据奈奎斯特采样定理为了不失真地还原我们想要的正弦波假设最高频率1kHzPWM的载波频率至少需要是信号频率的2倍以上。但实际上为了获得较好的滤波效果和更平滑的输出载波频率通常需要是信号最高频率的10倍甚至100倍以上。我们这里可以设置为100kHz。假设系统时钟72MHz经过预分频PSC71得到CK_CNT1MHz。要得到100kHz的PWM则ARR (1MHz / 100kHz) - 1 9。这是一个非常小的ARR值意味着PWM的分辨率很低只有10个等级这会导致输出波形阶梯感很强。这就是PWM模拟DAC的一个主要矛盾载波频率决定滤波难度和分辨率决定输出精度相互制约。提高ARR可以提高分辨率但会降低载波频率。为了解决这个问题我们可以使用定时器的溢出中断在中断里动态更新CCR寄存器来改变占空比这样ARR可以设大以保证分辨率而“波形更新频率”由中断频率控制。滤波电路PWM引脚输出后必须连接一个低通滤波器通常是一阶RC滤波滤除100kHz的开关频率及其谐波只留下我们想要的低频音频信号。滤波器的截止频率需要精心计算要低于PWM载波频率但高于音频信号频率。3.2 方案二DAC直接输出音频这是更直接、性能通常更好的方案。信号生成同样创建一个数组dac_sine_table[100]。这个数组存放的是直接写入DAC数据寄存器的12位数字值。计算公式为value (sin(2*π*i/100) 1) / 2 * 4095。外设协同配置核心DAC配置使能DAC通道设置为定时器触发、输出缓冲区使能。定时器配置配置一个定时器如TIM6基本定时器作为DAC的触发源。定时器的更新频率就是我们的音频采样率。要生成1kHz的正弦波并且一个周期用100个点来描绘那么采样率就是 1kHz * 100 100kHz。根据这个频率去计算定时器的ARR和PSC。DMA配置这是流畅播放的关键。配置DMA通道将内存中的dac_sine_table作为源地址DAC的数据寄存器如DAC_DHR12R1作为目标地址。设置传输数据宽度为半字16位模式为循环模式。这样每当定时器触发一次DMA就自动搬运一个数据到DAC周而复始CPU零开销。3.3 软件架构规划我们将使用STM32CubeMX进行图形化初始化然后在中途添加业务逻辑代码。软件层次可以这样划分硬件抽象层由CubeMX生成的tim.c,dac.c,dma.c以及对应的gpio.c组成完成所有外设的引脚和基础参数配置。应用层我们创建audio_app.c文件。在这里我们将实现正弦波数据表的生成函数。两种方案PWM中断更新、DACDMA的启动控制函数。可能的话一个简单的函数用来动态改变频率通过修改定时器ARR或数据表。4. 详细实现步骤与代码剖析我们以方案二DACDMA为重点因为这是更标准、更高效的音频输出方式。4.1 使用STM32CubeMX进行初始化时钟树配置确保系统时钟SYSCLK设置为72MHz对于F103。DAC配置在Analog-DAC中使能OUT1 Configuration对应PA4引脚。在Parameter Settings中选择Trigger为Timer 6 Trigger Out event。勾选Output Buffer使能通常建议使能以获得更好的驱动能力。定时器配置配置TIM6基本定时器。在Parameter Settings中设置Prescaler (PSC)和Counter Period (ARR)以产生100kHz的更新事件。计算示例定时器时钟APB1为36MHz。若PSC359则计数器时钟CK_CNT 36MHz / (3591) 100kHz。要产生100kHz的更新率则ARR 0即可因为从0计数到0即溢出。但这样控制不灵活。更常见的做法是设置PSC35CK_CNT1MHz然后设置ARR9这样更新频率就是1MHz/(91)100kHz。关键一步在Trigger Output (TRGO) Parameters中将TRGO Source选择为Update Event。这样定时器每次溢出更新时就会产生一个触发信号给DAC。DMA配置在DAC的配置页找到DMA Settings点击Add。选择通道如DAC1方向为Memory To Peripheral。模式选择Circular循环模式。这样DMA在传输完整个数组后会自动回到开头重新开始实现连续播放。数据宽度都选择Half Word16位因为DAC的12位数据寄存器是16位对齐的。生成代码生成基于MDK或IDE的工程代码。4.2 编写应用层代码在main.c或我们新建的audio_app.c中// 1. 定义音频数据表 #define SINE_TABLE_SIZE 100 uint16_t dac_sine_table[SINE_TABLE_SIZE]; // 2. 生成正弦波表函数 void Generate_SineWave_Table(void) { for (int i 0; i SINE_TABLE_SIZE; i) { // 计算0到2π之间的角度 float angle 2 * 3.1415926f * i / SINE_TABLE_SIZE; // 计算正弦值范围-1到1 float sine_value sinf(angle); // 将-1~1映射到0~409512位DAC // 先映射到0~1: (sine_value 1.0f) / 2.0f // 再映射到0~4095: * 4095.0f dac_sine_table[i] (uint16_t)((sine_value 1.0f) / 2.0f * 4095.0f); } } // 3. 音频播放启动函数 void Audio_Play_Start(void) { // 生成波形数据 Generate_SineWave_Table(); // 使能DAC的DMA请求这一步很关键CubeMX生成的代码可能不会自动开启 // 在stm32f1xx_hal_dac.h中使用 HAL_DAC_Start_DMA 函数 // 参数DAC句柄通道数据数组地址数据长度数据对齐方式 if (HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t*)dac_sine_table, SINE_TABLE_SIZE, DAC_ALIGN_12B_R) ! HAL_OK) { Error_Handler(); } // 启动定时器它将周期性地触发DAC转换 HAL_TIM_Base_Start(htim6); } int main(void) { // HAL初始化时钟配置等由CubeMX生成 ... // 启动音频播放 Audio_Play_Start(); while (1) { // 主循环此时CPU是空闲的音频由DMA自动输出 // 可以在这里添加按键检测用来改变频率或停止播放 ... } }4.3 方案一PWM模拟的关键代码补充对于PWM方案如果采用中断动态更新CCR核心代码如下// 在定时器更新中断中 void TIM3_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim3, TIM_FLAG_UPDATE) ! RESET) { __HAL_TIM_CLEAR_IT(htim3, TIM_IT_UPDATE); static uint8_t index 0; // 更新PWM占空比 __HAL_TIM_SET_COMPARE(htim3, TIM_CHANNEL_2, pwm_sine_table[index]); index; if (index SINE_TABLE_SIZE) { index 0; } } }同时需要将PWM定时器TIM3的溢出中断频率设置为我们的音频采样率如100kHz并在初始化后开启更新中断。5. 调试技巧、常见问题与实测心得5.1 没有输出或输出不正确检查时钟树这是最常见的问题。确保定时器、DAC、DMA的时钟源都已使能并且分频系数计算正确。使用STM32CubeMX的“Clock Configuration”标签页反复核对。检查引脚复用确认PWM或DAC输出引脚已正确配置为复用功能并且没有和其他功能冲突。验证触发链路对于DAC方案确保“定时器更新事件 - 触发DAC - DAC请求DMA”这条链路是通的。一个很好的调试方法是先将DAC配置为软件触发在main循环里手动写一个固定的值如2047到DAC寄存器用万用表测量引脚电压是否约为1.65V3.3V/2。这能先排除DAC本身的问题。DMA传输完成中断可以在DMA传输完成中断里翻转一个LED看看中断是否正常进入以判断DMA是否在工作。5.2 输出波形失真或噪声大PWM方案载波频率过低如果PWM载波频率太低经过RC滤波后残留的开关噪声会落在音频范围内听到的就是刺耳的“吱吱”声。尝试提高PWM频率减小ARR但要注意这会降低分辨率。滤波器设计不当RC滤波器的截止频率f_c 1/(2πRC)。它应该远低于PWM频率同时高于你要输出的最高音频频率。例如PWM频率100kHz音频最高1kHz截止频率可以选在5kHz左右。用示波器观察滤波前后的波形至关重要。中断抖动如果靠定时器中断更新PWM占空比中断响应时间的抖动会导致输出波形周期不稳定听起来有杂音。确保中断优先级够高且中断服务函数执行时间尽可能短。DAC方案采样率不足如果采样率低于信号频率的两倍奈奎斯特频率会产生混叠失真。确保定时器触发频率 2 * 信号最高频率。对于正弦波一个周期至少需要几十个点才能看起来光滑。量化噪声12位DAC本身就有量化误差这是固有的。可以通过提高DAC位数如果MCU支持或使用过采样和噪声整形技术来改善但这属于更高级的话题。电源噪声模拟部分的供电Vref VDDA不干净会直接反映在输出上。确保使用稳定的LDO为模拟部分供电并在电源引脚附近放置足够的去耦电容如10uF钽电容0.1uF陶瓷电容。5.3 实测心得与进阶玩法示波器是你的最佳伙伴无论是看PWM方波、滤波后的波形还是看DAC输出的阶梯波一个示波器能直观地告诉你一切。用它的FFT功能还能分析输出信号的频谱纯度。动态改变音调我们可以通过改变定时器TIM6的ARR值来实时改变采样率从而改变输出音频的音调。在主循环里检测按键按下后调用__HAL_TIM_SET_AUTORELOAD(htim6, new_arr_value)就能实现简单的“弹钢琴”效果。注意改变ARR后可能需要重新启动DMA传输。播放真实音频数据这个框架可以直接用来播放WAV文件需要是未压缩的PCM格式。你需要将WAV文件的数据部分提取出来转换成12位的数组并设置正确的采样率通过定时器ARR实现。这就能让你的STM32开发板变成一个简单的MP3播放器核心。PWM与DAC混合使用有些场景下可能需要用PWM驱动功率部分比如电机同时用DAC提供精密的模拟参考电压。理解两者的特性和配置方法就能灵活地在项目中运用。通过这样一个从原理到实践从配置到调试的完整练习你对STM32的PWM和DAC的理解绝不会再停留在表面。更重要的是你掌握了用DMA来高效管理数据流的方法这在处理ADC采样、通信传输等任务时同样适用。
返回列表