news 2026/8/29 9:35:27

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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)的“输出比较”模式产生。

这里有几个关键参数需要彻底理解:

  1. 定时器时钟(TIMx_CLK):这是PWM频率的基石。它来源于系统时钟(SYSCLK)经过预分频器(PSC)后的结果。计算实际计数器时钟频率的公式是:CK_CNT = TIMx_CLK / (PSC + 1)
  2. 自动重装载值(ARR):这个值决定了计数器的周期。计数器从0计数到ARR,然后归零重新开始,这就定义了一个PWM周期。PWM的频率计算公式为:PWM_Freq = CK_CNT / (ARR + 1)
  3. 捕获/比较寄存器(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载波,通过动态调整其占空比来“描绘”出我们想要的低频音频信号波形。

  1. 信号生成:首先,我们需要在代码中创建一个数组,比如pwm_sine_table[100]。这个数组存放的不是电压值,而是对应正弦波每个采样点的PWM占空比值。计算公式为:value = (sin(2*π*i/100) + 1) / 2 * (ARR_MAX)。这里加1除以2是为了将正弦波的[-1,1]范围映射到[0, ARR_MAX]的占空比范围。
  2. 硬件定时器配置
    • 选择一个定时器(如TIM3)的某个通道(如CH2)输出PWM。
    • 设置PWM频率(载波频率)。这里有个关键点:根据奈奎斯特采样定理,为了不失真地还原我们想要的正弦波(假设最高频率1kHz),PWM的载波频率至少需要是信号频率的2倍以上。但实际上,为了获得较好的滤波效果和更平滑的输出,载波频率通常需要是信号最高频率的10倍甚至100倍以上。我们这里可以设置为100kHz。假设系统时钟72MHz,经过预分频PSC=71,得到CK_CNT=1MHz。要得到100kHz的PWM,则ARR = (1MHz / 100kHz) - 1 = 9。这是一个非常小的ARR值,意味着PWM的分辨率很低(只有10个等级),这会导致输出波形阶梯感很强。这就是PWM模拟DAC的一个主要矛盾:载波频率(决定滤波难度)和分辨率(决定输出精度)相互制约。提高ARR可以提高分辨率,但会降低载波频率。为了解决这个问题,我们可以使用定时器的溢出中断,在中断里动态更新CCR寄存器来改变占空比,这样ARR可以设大以保证分辨率,而“波形更新频率”由中断频率控制。
  3. 滤波电路:PWM引脚输出后,必须连接一个低通滤波器(通常是一阶RC滤波),滤除100kHz的开关频率及其谐波,只留下我们想要的低频音频信号。滤波器的截止频率需要精心计算,要低于PWM载波频率,但高于音频信号频率。

3.2 方案二:DAC直接输出音频

这是更直接、性能通常更好的方案。

  1. 信号生成:同样创建一个数组dac_sine_table[100]。这个数组存放的是直接写入DAC数据寄存器的12位数字值。计算公式为:value = (sin(2*π*i/100) + 1) / 2 * 4095
  2. 外设协同配置(核心)
    • 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中断更新、DAC+DMA)的启动控制函数。
    • 可能的话,一个简单的函数用来动态改变频率(通过修改定时器ARR或数据表)。

4. 详细实现步骤与代码剖析

我们以方案二(DAC+DMA)为重点,因为这是更标准、更高效的音频输出方式。

4.1 使用STM32CubeMX进行初始化

  1. 时钟树配置:确保系统时钟(SYSCLK)设置为72MHz(对于F103)。
  2. DAC配置
    • Analog->DAC中,使能OUT1 Configuration(对应PA4引脚)。
    • Parameter Settings中,选择TriggerTimer 6 Trigger Out event。勾选Output Buffer使能(通常建议使能以获得更好的驱动能力)。
  3. 定时器配置
    • 配置TIM6(基本定时器)。在Parameter Settings中,设置Prescaler (PSC)Counter Period (ARR)以产生100kHz的更新事件。
    • 计算示例:定时器时钟APB1为36MHz。若PSC=359,则计数器时钟CK_CNT = 36MHz / (359+1) = 100kHz。要产生100kHz的更新率,则ARR = 0即可(因为从0计数到0即溢出)。但这样控制不灵活。更常见的做法是,设置PSC=35,CK_CNT=1MHz,然后设置ARR=9,这样更新频率就是1MHz/(9+1)=100kHz。
    • 关键一步:在Trigger Output (TRGO) Parameters中,将TRGO Source选择为Update Event。这样定时器每次溢出更新时,就会产生一个触发信号给DAC。
  4. DMA配置
    • DAC的配置页,找到DMA Settings,点击Add
    • 选择通道(如DAC1),方向为Memory To Peripheral
    • 模式选择Circular(循环模式)。这样DMA在传输完整个数组后会自动回到开头重新开始,实现连续播放。
    • 数据宽度都选择Half Word(16位),因为DAC的12位数据寄存器是16位对齐的。
  5. 生成代码:生成基于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~4095(12位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.65V(3.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 实测心得与进阶玩法

  1. 示波器是你的最佳伙伴:无论是看PWM方波、滤波后的波形,还是看DAC输出的阶梯波,一个示波器能直观地告诉你一切。用它的FFT功能还能分析输出信号的频谱纯度。
  2. 动态改变音调:我们可以通过改变定时器TIM6的ARR值来实时改变采样率,从而改变输出音频的音调。在主循环里检测按键,按下后调用__HAL_TIM_SET_AUTORELOAD(&htim6, new_arr_value),就能实现简单的“弹钢琴”效果。注意,改变ARR后,可能需要重新启动DMA传输。
  3. 播放真实音频数据:这个框架可以直接用来播放WAV文件(需要是未压缩的PCM格式)。你需要将WAV文件的数据部分提取出来,转换成12位的数组,并设置正确的采样率(通过定时器ARR实现)。这就能让你的STM32开发板变成一个简单的MP3播放器核心。
  4. PWM与DAC混合使用:有些场景下,可能需要用PWM驱动功率部分(比如电机),同时用DAC提供精密的模拟参考电压。理解两者的特性和配置方法,就能灵活地在项目中运用。

通过这样一个从原理到实践,从配置到调试的完整练习,你对STM32的PWM和DAC的理解绝不会再停留在表面。更重要的是,你掌握了用DMA来高效管理数据流的方法,这在处理ADC采样、通信传输等任务时同样适用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/29 9:34:19

开题报告AI工具怎么选?我折腾一圈给你捋清楚

又到开题季&#xff0c;先问几个扎心的问题&#xff1a; 题目换了三四个还定不下来的&#xff0c;举个手&#x1f64b;&#xff1b;文献下了几十篇&#xff0c;打开PDF就犯困的&#xff0c;举个手&#x1f64b;&#xff1b;好不容易用AI憋出几千字&#xff0c;导师一句"逻…

作者头像 李华
网站建设 2026/8/29 9:31:01

电脑供电真相:+12V才是现代PC的电压主干

1. 电脑主机电源的“电压真相”&#xff1a;不是5V和3.3V说了算&#xff0c;而是它在背后统一调度 你拆开过台式机机箱吗&#xff1f;如果留意过主板上那些密密麻麻的供电接口、CPU插槽旁的多相供电模块、显卡插槽附近的电容阵列&#xff0c;甚至仔细看过ATX电源背面的线缆标签…

作者头像 李华
网站建设 2026/8/29 9:29:43

不锈钢焊管自动切割程序

切割机控制板 代码修改 单选框和双态按钮联动 beep 50 r1.val0 bt0.val1 bt0.txt"时间方式" printh C0 00 00 00 00 00 FA beep 50 r0.val0 bt0.val0 bt0.txt"位置方式" printh C0 01 00 00 00 00 FA if(bt0.val1) { //此时按键是按下的 bt0.txt"…

作者头像 李华
网站建设 2026/8/29 9:26:36

3 步装好 Hoppscotch:API 测试工具快速安装与配置教程

3 步装好 Hoppscotch&#xff1a;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/29 9:26:29

OBS Studio 视频采集入门:屏幕捕获与摄像头 10 分钟搞定

OBS Studio 视频采集入门&#xff1a;屏幕捕获与摄像头 10 分钟搞定 【免费下载链接】obs-studio OBS Studio - Free and open source software for live streaming and screen recording 项目地址: https://gitcode.com/GitHub_Trending/ob/obs-studio 刚接触 OBS Stud…

作者头像 李华
网站建设 2026/8/29 9:23:41

VSCode 32位便携版zip:从绿色部署到AI插件接入的完整指南

简介&#xff1a;在软件工具快速迭代的今天&#xff0c;兼容性与轻量化部署依旧是开发环境搭建中的核心诉求。Visual Studio Code 作为主流代码编辑器&#xff0c;其 zip 免安装形态为受限环境提供了灵活方案&#xff0c;尤其是 ia32 架构的 32 位版本&#xff0c;常用于旧电脑…

作者头像 李华