:PWM模擬DAC與濾波電路設計)
STM32做音頻輸出很多人第一反應是不是得外掛一個DAC芯片或者至少用上芯片內部的自帶DAC。但實際上一個最普通的定時器PWM引腳配合幾顆電阻電容就能把音頻信號“造”出來。這個方案在成本敏感型產品里非常常見像一些智能小家電的提示音、玩具的語音播放、工控設備的蜂鳴報警升級版基本都是這么干的。這篇文章就帶你完整走一遍“STM32 PWM 濾波電路”的音頻輸出方案從原理到濾波設計再到CubeMX配置和DMA播放代碼最后附上一份實際問題排查清單。適合想低成本給產品加聲音功能的人或者剛入門STM32想搞點有意思實戰(zhàn)項目的朋友。本質上說這個方案就是用PWM去模擬一個DAC再用模擬濾波器把PWM的高頻載波濾掉留下平滑的音頻波形。思路不復雜但里面的坑不少濾波截止頻率怎么定、PWM頻率和采樣率怎么配合、音頻數據怎么送到定時器里、為什么播放出來聲音發(fā)悶。下面一條條說清楚。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方案的優(yōu)勢很多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分辨率和采樣率共同決定即使你把濾波做得很好量化噪聲的底子也擺在那跟專業(yè)的音頻DAC還是差幾個數量級。另外如果你要求輸出真正意義上的雙極性音頻信號正負半周都有那就得在濾波輸出后面加隔直電容或者偏置電路這部分我會在濾波設計里專門講。2. 濾波電路設計RC、LC還是有源濾波電路是整個PWM DAC方案里最關鍵的部分也是很多人忽略的地方。PWM信號經過濾波后如果濾波不干凈你聽到的就是“嘶嘶”的高頻噪聲疊加在音頻上如果濾波太狠音頻的高頻細節(jié)又會被濾掉聲音變得悶悶的。所以這個“度”必須把握好。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直接串聯(lián)但要注意兩級RC直接級聯(lián)時后一級的輸入阻抗會成為前一級的負載實際截止頻率會偏離理論值計算時不能簡單地把兩級公式相乘。如果兩級之間加一個運放電壓跟隨器做隔離那每一級的截止頻率就可以獨立計算。這種方法在成本允許的情況下是首選。另一種更常用的思路是用π型RC濾波也就是在輸入端串一顆電阻然后分成兩個并聯(lián)支路每個支路各有一顆對地的電容。這種結構對高頻的衰減比一階好而且元件數量也不多。實際搭電路的時候我比較推薦用“R1kΩC1C210nF”這種組合在STM32的PWM輸出上表現很不錯。LC濾波在PWM輸出電路里也有應用特別是在D類功放的輸出端用來濾除高頻開關頻率。它比RC濾波的衰減特性更陡峭但電感有兩個問題一是成本高、體積大二是有可能產生諧振峰如果沒有加阻尼電阻在某些頻率點反而會放大信號。對PWM DAC這種小信號場景除非你對體積和成本不敏感否則RC已經足夠不推薦一開始就去動LC。2.3 有源濾波和電平偏置想要正常音質的必經之路如果你直接用RC濾波后的信號去驅動耳機或者喇叭大概率會失望。一方面RC濾波輸出的是高阻信號帶不動低阻抗負載音質會明顯變差另一方面STM32的PWM輸出電壓范圍是0V到3.3V經過濾波后得到的是一個純正的單極性信號也就是只有正半周沒有負半周。真正的音頻信號是交流的有正有負你直接把單極性信號懟到耳機里聲音會嚴重失真而且如果信號平均值不為零還會導致耳機振膜一直偏向一側長時間工作還可能損壞耳機。解決這個問題有幾個辦法。第一個辦法是串聯(lián)一個隔直電容。在濾波電路輸出端接一個電容比如10μF到100μF的電解電容再串聯(lián)一個電阻到耳機。這個電容把直流分量隔斷只讓交流音頻信號通過。這個辦法最簡單但音質也就那樣低頻信號會被電容衰減特別是電容容量太小的時候低音基本沒了。第二個辦法是加法偏置電路。用兩個電阻把信號偏置到1.65V然后用運放做電壓跟隨再做隔直輸出。STM32很多芯片內部有VREFINT或者你可以直接用3.3V電源分壓得到一個1.65V的參考電壓。這樣音頻信號的中間點就在1.65V附近交流信號可以在0V到3.3V之間擺動再串隔直電容輸出就能還原出比較完整的雙極性音頻。第三個辦法更專業(yè)一點用運放做電平移位和放大典型電路是用反相放大器把0V到3.3V的單極性信號轉換成以0V為中點的雙極性信號。這個方案需要正負雙電源或者用軌到軌運放配合虛擬地電路復雜度上去了但音質上限也更高。我個人在產品里最常用的方案是PWM引腳 → π型RC濾波R1kC10nF×2→ 運放電壓跟隨器 → 隔直電容10μF→ 耳機或功放。這個組合成本低、性能穩(wěn)定而且對新手友好。3. STM32軟件實現定時器PWM DMA播放音頻硬件方案定下來以后軟件部分分兩大塊音頻數據怎么來以及怎么把數據以正確的節(jié)奏送到PWM輸出上。3.1 整體流程與音頻數據準備軟件的整體流程可以總結為準備音頻數據 → 將數據映射為占空比數值 → 定時器PWM生成載波 → DMA把占空比數值逐個搬運到定時器的比較寄存器 → 占空比實時變化 → 輸出端得到音頻波形。音頻數據有幾種常見來源。第一種是直接生成“簡易音調”比如正弦波查表。如果你只是想播放一個“叮咚”的提示音根本不需要外部音頻文件在代碼里生成一個正弦波表用DMA循環(huán)播放同一個表再配合定時器的頻率就能得到持續(xù)的正弦波音調。這個方法做報警器、門鈴、鍵盤提示音非常合適。第二種是播放WAV文件。把音頻文件轉成C語言數組常見工具是“WAV to C array”或者Python腳本。轉換時需要注意音頻格式單聲道、8位PCM是比較方便直接播放的因為每個采樣點正好一個字節(jié)可以直接作為PWM的占空比數據。如果原來的音頻是16位PCM可以取高8位作為輸出數據不過這樣相當于做了取整會引入一定的量化噪聲實際聽感會稍微有點“粗糙”。想減少這種粗糙感可以加簡單的噪聲整形但新手階段不建議一上來就搞這么復雜。第三種是使用語音合成芯片比如SYN6288這類它輸出的本來就是文本轉語音的音頻信號可以接到STM32的ADC里再重采樣播放。但這個偏題了這里不展開。3.2 關鍵參數計算采樣率、PWM頻率和ARR這一節(jié)直接關系到你能不能正確播放先說結論采樣率是多少PWM頻率就必須是采樣率的整數倍最直接的做法是讓PWM頻率等于采樣率。為什么因為每個采樣點需要保持一個完整的PWM周期這樣在每個PWM周期內占空比不變?yōu)V波后電壓穩(wěn)定。如果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分辨率匹配。這里有個細節(jié)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配置系統(tǒng)時鐘為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時如果兩邊寬度不匹配會出問題。穩(wěn)妥的做法是把音頻數組定義成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循環(huán)模式數組播完以后會循環(huán)回到開頭繼續(xù)播這樣就實現了一個持續(xù)的正弦波輸出。使用DMA的好處是CPU基本不用管波形播放音頻數據按固定節(jié)奏搬運波形穩(wěn)定不卡頓。如果不用DMA你得在定時器中斷里手動更新CCR中斷頻繁進入會很占CPU資源還容易出現時序抖動導致音調不穩(wěn)定。3.4 播放完整WAV音頻和實時音調生成播放完整WAV音頻的原理和正弦波一模一樣只是把sine_wave數組換成你轉換好的音頻數組。但有一個問題DMA是循環(huán)播放的如果音頻數組播完以后又回到開頭就會形成循環(huán)播放這在很多場景下不是我們想要的。想實現只播一次DMA模式改成Normal播完自動停止。實際項目里我建議的做法是把WAV文件轉成16進制數據放到外部Flash里用FATFS文件系統(tǒng)讀取到SRAM再做DMA播放。這樣代碼靈活可以隨時換音頻文件不用每次重新編譯下載。不過這是進階方案對新手來說先在一個C文件里放一段幾百毫秒的人聲樣本跑通了再說。如果你只是想播放某個頻率的提示音甚至可以不用查表直接在主循環(huán)里改變CCR的值實現斜坡變化從而產生“嗶嗶”或者“滴——”的聲音。比如一個500Hz報警音設置PWM頻率為500HzCCR設為ARR的一半就是50%占空比聲音持續(xù)響。想讓聲音鳴響一下再停用定時器控制開關就行。4. 常見問題與排查技巧實錄做這個項目的時候很多問題不是代碼跑沒跑起來而是輸出聲音不對。下面幾個是我自己實際調試過程中踩過的坑整理成速查表方便你遇到問題直接對照。4.1 問題速查表現象可能原因解決思路播放時有明顯的“嘶嘶”高頻噪聲PWM載波泄漏濾波不足增加濾波階數或把濾波器截止頻率再調低讓載波衰減更多聲音發(fā)悶高音完全聽不見濾波器截止頻率太低把有效音頻頻段濾掉了提高截止頻率或提高采樣率讓有效頻段和高頻載波分開播出來的聲音速度不對感覺變慢定時器溢出頻率和音頻采樣率不匹配重新計算ARR確保定時器更新頻率等于音頻采樣率聲音很小幾乎聽不到濾波后信號直接驅動了低阻負載信號被拉低了增加運放緩沖或增加一級放大電路有“滋滋”的周期性雜音DMA和PWM更新不同步更新點上出現毛刺檢查DMA配置是否Circular、數據寬度是否匹配播放過程中聲音偶爾卡頓或重復DMA循環(huán)模式導致數組播完又從頭播需要播放一次就改成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供電然后用耳機線直接勾到濾波輸出端很容易聽到持續(xù)的“嗡嗡”底噪。這個底噪不是PWM濾波惹的禍而是電源和地線的問題。USB供電本身噪聲很大特別是有電機、繼電器這類感性負載在同一個電源上工作時噪聲會直接跑進音頻回路。解決辦法是音頻部分的地要單點接地不要形成環(huán)路電源上并聯(lián)一個100μF的大電容和100nF的小電容分別濾低頻和高頻干擾如果條件允許音頻部分用獨立的LDO單獨供電。還有一個容易忽略的點耳機線的地線如果接在STM32的地上STM32的數字地噪聲會干擾音頻信號地。最好的做法是模擬地和數字地分開走線在一個點匯合。這個細節(jié)在PCB設計時要特別注意面包板調試階段很難完全避免但至少要保證共地可靠。5. 一點擴展從提示音到語音播報聊到這里基礎方案已經完整了PWM模擬DAC、RC濾波、運放輸出、DMA播放。最后分享幾個我覺得很有用的擴展思路。如果你在做一個帶人聲語音播報的產品比如“溫度正?!薄罢埶⒖ā狈桨甘窍仍赑C端用工具把語音壓縮成8kHz 8bit WAV再轉成C數組用DMA循環(huán)或單次播放。這種音質水平在提示音場景完全夠用而且資源占用極小一段2秒的語音占用16KB Flash在STM32F103的64KB Flash里可以存好幾段。如果覺得PWM DAC音質不夠好但又不舍得換成真正的音頻DAC芯片可以看看STM32系列里帶真正DAC的型號比如F103已經帶了只是引腳和代碼需要重新適配。也可以把目光放到G0系列或者L4系列它們的DAC外設更完善還支持硬件觸發(fā)DMA。但核心思路依然是模擬信號出來之后濾波和放大電路的設計才是決定音質的關鍵。還有一個比較好玩的方向是PWM輸出配合快速傅里葉變換做音頻頻譜顯示。你已經在播放音頻了同時把同一個音頻數組送進FFT計算驅動一個小屏或者LED點陣顯示頻譜。這個項目作為進階訓練非常推薦因為音頻采集、播放、處理都在一顆MCU里閉環(huán)了對理解整個數字信號鏈路也有幫助。我在實際調試過程中最大的體會是不要一上來就追求高保真音質先把“能出聲、聲音不破、播放速度正確”這個目標達成再逐步優(yōu)化濾波和放大。最怕的就是濾波電路堆了一堆元件結果連波形都沒調出來出了問題都不知道該查模擬部分還是數字部分。先用示波器看PWM引腳再看濾波輸出一步一級往上查問題基本都能快速定位。