ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32低成本音频输出实战:PWM模拟DAC与滤波电路设计

STM32低成本音频输出实战:PWM模拟DAC与滤波电路设计 STM32做音频输出很多人第一反应是不是得外挂一个DAC芯片或者至少用上芯片内部的自带DAC。但实际上一个最普通的定时器PWM引脚配合几颗电阻电容就能把音频信号“造”出来。这个方案在成本敏感型产品里非常常见像一些智能小家电的提示音、玩具的语音播放、工控设备的蜂鸣报警升级版基本都是这么干的。这篇文章就带你完整走一遍“STM32 PWM 滤波电路”的音频输出方案从原理到滤波设计再到CubeMX配置和DMA播放代码最后附上一份实际问题排查清单。适合想低成本给产品加声音功能的人或者刚入门STM32想搞点有意思实战项目的朋友。本质上说这个方案就是用PWM去模拟一个DAC再用模拟滤波器把PWM的高频载波滤掉留下平滑的音频波形。思路不复杂但里面的坑不少滤波截止频率怎么定、PWM频率和采样率怎么配合、音频数据怎么送到定时器里、为什么播放出来声音发闷。下面一条条说清楚。1. 为什么用PWM来“冒充”DAC原理与选型思路1.1 PWM变成一个DAC靠的是低通滤波的平均效应先想想PWM输出的是什么波形一个频率固定、占空比可变的方波。这个方波经过低通滤波器之后高频分量被衰减输出就变成了一个相对平滑的直流电平。这个直流电平的大小正好等于占空比乘以PWM的高电平电压。比如STM32的引脚输出高电平是3.3V占空比50%那滤波后的平均电压就是1.65V占空比100%就是3.3V0%就是0V。这个特性其实就是PWM DAC的核心思想占空比就是“数字量”滤波后的平均电压就是“模拟量”。你给PWM设置256级占空比就能得到256级电压输出相当于一个8位的DAC。如果把定时器的自动重载值ARR设为255那么比较寄存器CCR的值从0到255变化时占空比就从0%到100%线性变化对应输出电压从0V到3.3V每级约12.9mV。如果你播放一段音频采样点是8位的每个采样点对应一个占空比输出波形经过滤波后就还原出了原始音频的包络。这就是“PWM DAC播放音频”的最简模型。要注意的是这里每个采样点至少占一个完整的PWM周期所以PWM频率决定了你能输出的采样率上限。1.2 STM32自带DAC不好用聊聊PWM方案的优势很多STM32型号其实自带了DAC外设比如STM32F103系列就有2个12位DAC通道。那为什么还要折腾PWM有几个很现实的原因。第一引脚限制。自带DAC的引脚通常是固定的比如F103的PA4和PA5。这在PCB布局上很被动你可能为了把一个DAC引脚引到音频输出位置绕了一大圈走线。而且如果这几个引脚被其他功能占用了DAC就没法用。PWM方案就没这问题几乎所有定时器通道都能输出PWM哪个引脚方便就用哪个。第二输出驱动能力。DAC的输出是电压信号带载能力很弱直接接耳机或者喇叭基本带不动后面照样得加运放或者功放。而PWM引脚好歹是推挽输出的GPIO灌电流拉电流能力更强驱动一个简单的RC负载没问题。第三成本角度。不是所有项目都用F103这种带DAC的型号很多便宜的MCU根本没有DAC外设。用PWM方案意味着哪怕你换一颗1块钱的芯片只要它有定时器这套代码和电路就能复用。这对做产品的人来说是一个非常有吸引力的可迁移方案。还有一点PWM的位数可以通过定时器自由扩展。16位定时器可以把ARR设到65535得到16位的PWM分辨率远超大多数内部DAC的12位。当然代价是PWM频率会跟着降下来这里就涉及一个“分辨率”和“频率”的取舍后面参数计算部分再细说。1.3 什么场景适合用PWM DAC什么场景别硬上PWM DAC不是万能的它更适合以下场景语音提示、人声播报采样率8kHz到16kHz这种电话音质级别的要求完全没有问题简单的音效比如按键音、开机音、报警提示音需要输出一个可变的模拟电压比如控制某些模拟电路的偏置电压低成本产品不想为音频功能增加一颗DAC芯片或codec芯片。但如果你要做HiFi音乐播放、需要对音频信号进行高保真还原那PWM DAC方案就不合适了。它最大的短板是音质上限由PWM分辨率和采样率共同决定即使你把滤波做得很好量化噪声的底子也摆在那跟专业的音频DAC还是差几个数量级。另外如果你要求输出真正意义上的双极性音频信号正负半周都有那就得在滤波输出后面加隔直电容或者偏置电路这部分我会在滤波设计里专门讲。2. 滤波电路设计RC、LC还是有源滤波电路是整个PWM DAC方案里最关键的部分也是很多人忽略的地方。PWM信号经过滤波后如果滤波不干净你听到的就是“嘶嘶”的高频噪声叠加在音频上如果滤波太狠音频的高频细节又会被滤掉声音变得闷闷的。所以这个“度”必须把握好。2.1 一阶RC滤波最省料的起手式最简单的滤波电路就是一颗电阻加一颗电容构成一阶低通滤波器。截止频率公式是fc 1 / (2π × R × C)实际设计滤波器时首先要确定PWM的载波频率。比如你的PWM频率是100kHz那么滤波器的截止频率至少要远低于100kHz才能把载波衰减到足够小。常见做法是把截止频率设置在载波频率的1/10到1/20。举个例子PWM频率100kHz截止频率设10kHz取R1kΩ那么C 1/(2π × 1000 × 10000) ≈ 15.9nF取标称值15nF或10nF都可以。如果PWM频率只有20kHz截止频率还想设5kHz那么C 1/(2π × 1000 × 5000) ≈ 31.8nF取33nF也能凑合。但一阶RC的衰减斜率只有20dB/dec这意味着截止频率之后频率每升高10倍信号衰减20dB。如果PWM频率是截止频率的10倍那载波只衰减了20dB大概相当于信号幅度缩减到原来的1/10听起来还是会有明显的“齿音”或者“嘶嘶声”。所以一阶RC适合要求不高的场景想效果好一点就得用二阶或更高阶的滤波器。2.2 二阶RC与LC滤波把载波压得更干净二阶RC低通滤波器有两种接法。一种是无源的两级RC直接串联但要注意两级RC直接级联时后一级的输入阻抗会成为前一级的负载实际截止频率会偏离理论值计算时不能简单地把两级公式相乘。如果两级之间加一个运放电压跟随器做隔离那每一级的截止频率就可以独立计算。这种方法在成本允许的情况下是首选。另一种更常用的思路是用π型RC滤波也就是在输入端串一颗电阻然后分成两个并联支路每个支路各有一颗对地的电容。这种结构对高频的衰减比一阶好而且元件数量也不多。实际搭电路的时候我比较推荐用“R1kΩC1C210nF”这种组合在STM32的PWM输出上表现很不错。LC滤波在PWM输出电路里也有应用特别是在D类功放的输出端用来滤除高频开关频率。它比RC滤波的衰减特性更陡峭但电感有两个问题一是成本高、体积大二是有可能产生谐振峰如果没有加阻尼电阻在某些频率点反而会放大信号。对PWM DAC这种小信号场景除非你对体积和成本不敏感否则RC已经足够不推荐一开始就去动LC。2.3 有源滤波和电平偏置想要正常音质的必经之路如果你直接用RC滤波后的信号去驱动耳机或者喇叭大概率会失望。一方面RC滤波输出的是高阻信号带不动低阻抗负载音质会明显变差另一方面STM32的PWM输出电压范围是0V到3.3V经过滤波后得到的是一个纯正的单极性信号也就是只有正半周没有负半周。真正的音频信号是交流的有正有负你直接把单极性信号怼到耳机里声音会严重失真而且如果信号平均值不为零还会导致耳机振膜一直偏向一侧长时间工作还可能损坏耳机。解决这个问题有几个办法。第一个办法是串联一个隔直电容。在滤波电路输出端接一个电容比如10μF到100μF的电解电容再串联一个电阻到耳机。这个电容把直流分量隔断只让交流音频信号通过。这个办法最简单但音质也就那样低频信号会被电容衰减特别是电容容量太小的时候低音基本没了。第二个办法是加法偏置电路。用两个电阻把信号偏置到1.65V然后用运放做电压跟随再做隔直输出。STM32很多芯片内部有VREFINT或者你可以直接用3.3V电源分压得到一个1.65V的参考电压。这样音频信号的中间点就在1.65V附近交流信号可以在0V到3.3V之间摆动再串隔直电容输出就能还原出比较完整的双极性音频。第三个办法更专业一点用运放做电平移位和放大典型电路是用反相放大器把0V到3.3V的单极性信号转换成以0V为中点的双极性信号。这个方案需要正负双电源或者用轨到轨运放配合虚拟地电路复杂度上去了但音质上限也更高。我个人在产品里最常用的方案是PWM引脚 → π型RC滤波R1kC10nF×2→ 运放电压跟随器 → 隔直电容10μF→ 耳机或功放。这个组合成本低、性能稳定而且对新手友好。3. STM32软件实现定时器PWM DMA播放音频硬件方案定下来以后软件部分分两大块音频数据怎么来以及怎么把数据以正确的节奏送到PWM输出上。3.1 整体流程与音频数据准备软件的整体流程可以总结为准备音频数据 → 将数据映射为占空比数值 → 定时器PWM生成载波 → DMA把占空比数值逐个搬运到定时器的比较寄存器 → 占空比实时变化 → 输出端得到音频波形。音频数据有几种常见来源。第一种是直接生成“简易音调”比如正弦波查表。如果你只是想播放一个“叮咚”的提示音根本不需要外部音频文件在代码里生成一个正弦波表用DMA循环播放同一个表再配合定时器的频率就能得到持续的正弦波音调。这个方法做报警器、门铃、键盘提示音非常合适。第二种是播放WAV文件。把音频文件转成C语言数组常见工具是“WAV to C array”或者Python脚本。转换时需要注意音频格式单声道、8位PCM是比较方便直接播放的因为每个采样点正好一个字节可以直接作为PWM的占空比数据。如果原来的音频是16位PCM可以取高8位作为输出数据不过这样相当于做了取整会引入一定的量化噪声实际听感会稍微有点“粗糙”。想减少这种粗糙感可以加简单的噪声整形但新手阶段不建议一上来就搞这么复杂。第三种是使用语音合成芯片比如SYN6288这类它输出的本来就是文本转语音的音频信号可以接到STM32的ADC里再重采样播放。但这个偏题了这里不展开。3.2 关键参数计算采样率、PWM频率和ARR这一节直接关系到你能不能正确播放先说结论采样率是多少PWM频率就必须是采样率的整数倍最直接的做法是让PWM频率等于采样率。为什么因为每个采样点需要保持一个完整的PWM周期这样在每个PWM周期内占空比不变滤波后电压稳定。如果PWM频率是采样率的2倍那么一个采样点要占2个PWM周期但这2个PWM周期的占空比应该相同这就得靠DMA或中断来精确控制更新时间实现起来更复杂。所以新手入门PWM频率 采样率是最好理解也最容易实现的。举例你想以8kHz采样率播放一段8位PCM音频那么定时器的更新频率要设成8kHz。STM32F103的时钟是72MHz定时器预分频PSC设为0那么自动重载值ARR 72000000 / 8000 - 1 8999。因为PWM占空比寄存器是16位的而音频数据是8位的所以我们要把8位音频数据左移或者映射到16位范围让占空比和PWM分辨率匹配。这里有个细节ARR设成8999意味着PWM的分辨率是9000级但你用8位数据去驱动它实际只用了256级。这没问题等价于8位PWM。但如果你的采样率是44.1kHzARR 72000000 / 44100 - 1 ≈ 1631分辨率约1632级大于256仍然满足8位音频的需要。反过来如果你希望提高PWM分辨率到16位ARR 65535那PWM频率就只能是72000000 / 65536 ≈ 1098Hz这个频率做音频载波是远远不够的。所以“高分辨率”和“高载波频率”在同一个定时器配置里是矛盾的这就是前面说的取舍。对音频应用来说PWM频率尽可能高一点比较好这样滤波比较容易所以实际工程中常常用8位或10位PWM而不是16位。3.3 基于CubeMX的配置步骤与核心代码下面是STM32CubeMX配置PWM输出的关键步骤我用的是STM32F103C8T6工程用Keil MDK编译。打开CubeMX选择芯片STM32F103C8T6配置系统时钟为72MHzPLL 9倍频。在左侧“Timers”中选择TIM2勾选“PWM Generation CH1”设置Prescaler为0Counter Period为8999对应8kHz采样率。在TIM2的“Parameter Settings”里把Pulse设为0模式选PWM Mode 1Pulse极性选High。切换到“DMA Settings”添加DMA请求方向选“Memory To Peripheral”外设地址选TIM2的CCR1寄存器模式选Circular数据宽度按实际音频数据大小设置。如果音频数据是8位的外设数据宽度最好也要匹配但注意TIM2的CCR1是32位寄存器DMA搬运到CCR1时如果两边宽度不匹配会出问题。稳妥的做法是把音频数组定义成uint32_t类型每一个元素的高8位或低8位存放占空比然后DMA宽度都设为Word。初始化完成后生成代码。核心代码就几行#define SAMPLING_RATE 8000 #define AUDIO_LENGTH 256 const uint32_t sine_wave[AUDIO_LENGTH] { /* 预先生成的256点正弦波数据 */ }; // 在main函数中启动播放 HAL_TIM_PWM_Start(htim2, TIM_CHANNEL_1); HAL_TIM_PWM_Start_DMA(htim2, TIM_CHANNEL_1, (uint32_t *)sine_wave, AUDIO_LENGTH);DMA启动后TIM2每次溢出更新就会自动从sine_wave数组里取下一个值写入CCR1占空比随之变化。因为设置了Circular循环模式数组播完以后会循环回到开头继续播这样就实现了一个持续的正弦波输出。使用DMA的好处是CPU基本不用管波形播放音频数据按固定节奏搬运波形稳定不卡顿。如果不用DMA你得在定时器中断里手动更新CCR中断频繁进入会很占CPU资源还容易出现时序抖动导致音调不稳定。3.4 播放完整WAV音频和实时音调生成播放完整WAV音频的原理和正弦波一模一样只是把sine_wave数组换成你转换好的音频数组。但有一个问题DMA是循环播放的如果音频数组播完以后又回到开头就会形成循环播放这在很多场景下不是我们想要的。想实现只播一次DMA模式改成Normal播完自动停止。实际项目里我建议的做法是把WAV文件转成16进制数据放到外部Flash里用FATFS文件系统读取到SRAM再做DMA播放。这样代码灵活可以随时换音频文件不用每次重新编译下载。不过这是进阶方案对新手来说先在一个C文件里放一段几百毫秒的人声样本跑通了再说。如果你只是想播放某个频率的提示音甚至可以不用查表直接在主循环里改变CCR的值实现斜坡变化从而产生“哔哔”或者“滴——”的声音。比如一个500Hz报警音设置PWM频率为500HzCCR设为ARR的一半就是50%占空比声音持续响。想让声音鸣响一下再停用定时器控制开关就行。4. 常见问题与排查技巧实录做这个项目的时候很多问题不是代码跑没跑起来而是输出声音不对。下面几个是我自己实际调试过程中踩过的坑整理成速查表方便你遇到问题直接对照。4.1 问题速查表现象可能原因解决思路播放时有明显的“嘶嘶”高频噪声PWM载波泄漏滤波不足增加滤波阶数或把滤波器截止频率再调低让载波衰减更多声音发闷高音完全听不见滤波器截止频率太低把有效音频频段滤掉了提高截止频率或提高采样率让有效频段和高频载波分开播出来的声音速度不对感觉变慢定时器溢出频率和音频采样率不匹配重新计算ARR确保定时器更新频率等于音频采样率声音很小几乎听不到滤波后信号直接驱动了低阻负载信号被拉低了增加运放缓冲或增加一级放大电路有“滋滋”的周期性杂音DMA和PWM更新不同步更新点上出现毛刺检查DMA配置是否Circular、数据宽度是否匹配播放过程中声音偶尔卡顿或重复DMA循环模式导致数组播完又从头播需要播放一次就改成Normal模式或用标志位控制播放时单片机程序卡死DMA配置出错访问非法地址检查DMA外设地址是否指向CCR1数组首地址是否对齐4.2 典型问题滤波到底怎么调才合适滤波是新手最容易困惑的地方这里多说一句。滤波的目标不是在“完全滤掉载波”和“保留全部音频”之间找个完美点而是尽量拉开PWM载波频率和音频有效频段之间的距离让滤波变得容易。比如你要播放的电话语音采样率是8kHz有效音频带宽大概是3.4kHzPWM载波频率如果是8kHz那滤波截止频率设在3.4kHz载波只比截止频率高不到2.4倍一阶RC根本压不住。这种情况下要么提高PWM频率到32kHz采样率变成32kHz但音频数据得重新采样要么把滤波器做到二阶、三阶。另一种思路是PWM频率不跟着采样率走而是固定在一个高频率比如100kHz然后每隔若干个PWM周期更新一次占空比这样相当于把采样和载波解耦了。但这个做法需要额外的定时器来控制占空比更新时间代码复杂度高一些。对刚开始玩的朋友我建议先用“PWM频率 采样率 二阶滤波”这个组合能解决80%的新手问题。4.3 典型问题播放音频时地线和电源引起的底噪如果你把电路搭在面包板上用USB给STM32供电然后用耳机线直接勾到滤波输出端很容易听到持续的“嗡嗡”底噪。这个底噪不是PWM滤波惹的祸而是电源和地线的问题。USB供电本身噪声很大特别是有电机、继电器这类感性负载在同一个电源上工作时噪声会直接跑进音频回路。解决办法是音频部分的地要单点接地不要形成环路电源上并联一个100μF的大电容和100nF的小电容分别滤低频和高频干扰如果条件允许音频部分用独立的LDO单独供电。还有一个容易忽略的点耳机线的地线如果接在STM32的地上STM32的数字地噪声会干扰音频信号地。最好的做法是模拟地和数字地分开走线在一个点汇合。这个细节在PCB设计时要特别注意面包板调试阶段很难完全避免但至少要保证共地可靠。5. 一点扩展从提示音到语音播报聊到这里基础方案已经完整了PWM模拟DAC、RC滤波、运放输出、DMA播放。最后分享几个我觉得很有用的扩展思路。如果你在做一个带人声语音播报的产品比如“温度正常”“请刷卡”方案是先在PC端用工具把语音压缩成8kHz 8bit WAV再转成C数组用DMA循环或单次播放。这种音质水平在提示音场景完全够用而且资源占用极小一段2秒的语音占用16KB Flash在STM32F103的64KB Flash里可以存好几段。如果觉得PWM DAC音质不够好但又不舍得换成真正的音频DAC芯片可以看看STM32系列里带真正DAC的型号比如F103已经带了只是引脚和代码需要重新适配。也可以把目光放到G0系列或者L4系列它们的DAC外设更完善还支持硬件触发DMA。但核心思路依然是模拟信号出来之后滤波和放大电路的设计才是决定音质的关键。还有一个比较好玩的方向是PWM输出配合快速傅里叶变换做音频频谱显示。你已经在播放音频了同时把同一个音频数组送进FFT计算驱动一个小屏或者LED点阵显示频谱。这个项目作为进阶训练非常推荐因为音频采集、播放、处理都在一颗MCU里闭环了对理解整个数字信号链路也有帮助。我在实际调试过程中最大的体会是不要一上来就追求高保真音质先把“能出声、声音不破、播放速度正确”这个目标达成再逐步优化滤波和放大。最怕的就是滤波电路堆了一堆元件结果连波形都没调出来出了问题都不知道该查模拟部分还是数字部分。先用示波器看PWM引脚再看滤波输出一步一级往上查问题基本都能快速定位。
返回列表