計FPGA電子琴:從按鍵消抖到音符分頻)
簡介一套基于Quartus原理圖設(shè)計方法的電子琴FPGA工程文件包面向數(shù)字邏輯與FPGA入門及進(jìn)階學(xué)習(xí)者完整呈現(xiàn)從原理圖規(guī)劃、模塊例化到編譯仿真、下載調(diào)試的完整設(shè)計流程。設(shè)計圍繞音符頻率生成、定時計數(shù)、數(shù)模轉(zhuǎn)換、播放控制與樂曲存儲等核心環(huán)節(jié)展開通過查找表存放頻率值并利用計數(shù)器產(chǎn)生精確時長整體構(gòu)架清晰便于逐級拆解。壓縮包內(nèi)共413個文件主要包含原理圖文件、符號文件、仿真波形文件以及工程設(shè)置、編譯報告、樂曲初始化數(shù)據(jù)和少量文本描述代碼壓縮后僅5.42兆字節(jié)方便快速下載和導(dǎo)入Quartus環(huán)境學(xué)習(xí)。資源已有1045人學(xué)習(xí)使用適合課程設(shè)計、競賽準(zhǔn)備或業(yè)余電子琴制作項目參考。通過研讀可學(xué)習(xí)如何將音樂理論中的音名編碼轉(zhuǎn)換為硬件可處理的頻率參數(shù)理解計數(shù)器分頻與節(jié)拍控制機(jī)制并借助現(xiàn)成工程模板修改樂譜和音色避開常見編譯時序陷阱顯著提高設(shè)計效率與成功率。1. 原理圖電子琴不是玩具它把時序電路變成了聽得見的反饋用 Quartus 原理圖畫一臺電子琴聽起來像教科書里的教學(xué)實(shí)驗但實(shí)際做出來的東西可以直接在 FPGA 開發(fā)板上彈奏甚至能跑出一段完整旋律。這個項目最特別的地方在于從按鍵消抖到音符分頻所有關(guān)鍵邏輯都以原理圖符號的形式呈現(xiàn)沒有一行 HDL 代碼它把一個時序電路的真實(shí)運(yùn)行狀態(tài)直接轉(zhuǎn)化為聲音反饋——跑對了耳朵能聽見跑錯了同樣聽得出來。適合正在學(xué) FPGA 基礎(chǔ)、想驗證計數(shù)器與觸發(fā)器行為的初學(xué)者也適合需要一份能快速改造成課程設(shè)計原型的開發(fā)者。下文從一個可運(yùn)行的工程角度把模塊劃分、分頻系數(shù)、連線方式和改造路徑逐一講透盡量讓你看完就能照著搭。2. 工程拆解Quartus 原理圖輸入與電子琴發(fā)聲鏈路2.1 原理圖工程的文件構(gòu)成與設(shè)計流程用原理圖方式做項目最容易翻車的地方是把所有邏輯都堆在一張 BDF 圖里最后復(fù)雜到連自己都看不清連線。我拿到一套電子琴工程時第一反應(yīng)是先看文件組織。一個規(guī)范的 Quartus 原理圖工程通常由三類文件組成.bdf 塊設(shè)計文件作為頂層圖紙.bsf 符號文件作為可復(fù)用的模塊封裝.v 或 .vhd 文件承載底層邏輯。一個 8 音符電子琴工程的典型目錄結(jié)構(gòu)長這樣project/ ├── top.bdf -- 頂層原理圖模塊之間的連線都在這里完成 ├── keyboard_ctrl.bsf -- 按鍵掃描與消抖模塊的符號文件 ├── tone_divider.bsf -- 音符分頻模塊的符號文件 ├── freq_table.mif -- 音符分頻系數(shù)存儲表供 ROM 讀取 ├── pin_assignments.qsf -- 引腳分配文件避免每次重新分配引腳 └── sim/ └── ekey_tb.v -- 可選仿真激勵驗證分頻輸出波形不要小看這個結(jié)構(gòu)。頂層 BDF 只做連線不寫邏輯分頻器、掃描器都封裝成獨(dú)立符號。這樣做的直接收益是改一個模塊的參數(shù)不需要動頂層圖紙Quartus 在重新生成網(wǎng)表時會自動同步排查問題時也能按圖索驥單獨(dú)仿真某一個模塊。實(shí)際操作順序我一般這樣排先新建工程指定頂層實(shí)體名然后新建 Block Diagram/Schematic File在 Symbol 庫中搜索需要的計數(shù)器、觸發(fā)器或自己生成的模塊符號放置到圖紙中接下來用連線工具把符號引腳連起來再用引腳屬性指定輸入輸出最后編譯一次打開 Pin Planner 分配引腳下載到板子。每完成一步就編譯一次不要等到全部畫完再一次性編譯否則幾十個錯誤堆在一起神仙也難定位。2.2 音符頻率與分頻系數(shù)先算清楚再連線電子琴發(fā)聲的本質(zhì)是產(chǎn)生不同頻率的方波。以常見的 50MHz 開發(fā)板主時鐘為例要得到 262Hz 的中央 C需要把時鐘頻率分頻到 262Hz。不同音符對應(yīng)的方波頻率不同原理圖設(shè)計里最常見的方式是做一個計數(shù)器計數(shù)到預(yù)定值后翻轉(zhuǎn)一次輸出電平這樣輸出方波的周期就是時鐘周期的兩倍。因此分頻系數(shù) K 與目標(biāo)頻率 f 的關(guān)系是K 50_000_000 / (2 × f)計數(shù)到 K-1 時觸發(fā)翻轉(zhuǎn)。以 C 大調(diào)一個八度為例分頻系數(shù)可以這樣算音符頻率約值 (Hz)分頻系數(shù) K推薦計數(shù)位寬C4261.639555617D4293.668513817E4329.637584617F4349.237158817G4392.006377617A4440.005681817B4493.885061917C5523.254777816 亦可建議統(tǒng)一 17 位注意分頻系數(shù) K 是一個整數(shù)而 50MHz 除以兩倍頻率往往帶小數(shù)。我建議所有音符統(tǒng)一向下取整不要有的四舍五入、有的直接截斷否則高音區(qū)會明顯偏低。另外計數(shù)器的計數(shù)范圍從 0 開始所以實(shí)際邏輯里是計數(shù)到 K-1 時清零或翻轉(zhuǎn)如果你誤寫成計數(shù)到 K 再清零實(shí)際分頻周期就變成了 K1每個音符都偏低一點(diǎn)。這類問題在仿真波形里很難一眼看出但一上板人耳馬上就發(fā)現(xiàn)。2.3 發(fā)聲鏈路與模塊邊界整臺電子琴的信號路徑可以切成四段每一段對應(yīng)原理圖上的一個區(qū)域按鍵輸入8 個按鍵引腳對應(yīng) C 大調(diào)八度音消抖與編碼按鍵信號先做防抖處理再編碼成 3 位音符索引分頻查表根據(jù)音符索引從分頻系數(shù)表中拿到對應(yīng) K 值驅(qū)動音調(diào)分頻器音頻輸出分頻器輸出方波經(jīng)引腳送到無源蜂鳴器。這里有一個非常容易被忽略的硬件細(xì)節(jié)蜂鳴器分有源和無源兩類。有源蜂鳴器通電自帶固定頻率給它一個高電平就會響無源蜂鳴器需要外部提供振蕩方波才能發(fā)聲。這個電子琴項目的輸出必須接無源蜂鳴器或小喇叭否則你辛辛苦苦生成一個 262Hz 的翻轉(zhuǎn)電平它響的卻是自己內(nèi)部固定的那個音完全不聽你的分頻器。連線之前先查板子原理圖確認(rèn)揚(yáng)聲器類型能省掉后面一整晚的調(diào)試時間。這段鏈路也決定了調(diào)試策略如果按鍵模塊輸出的索引正確、分頻模塊輸出的方波頻率正確聲音就必然正確。所以排查時可以分段驗證——先看按鍵編碼再看分頻輸出波形最后才懷疑硬件連線。3. 核心模塊拆解一個音符從按鍵到揚(yáng)聲器的完整路徑3.1 按鍵掃描與消抖模塊的原理圖實(shí)現(xiàn)按鍵模塊要解決兩件事消抖和編碼。機(jī)械按鍵按下時會有約 5 到 20 毫秒的抖動如果不處理一次按下去可能觸發(fā)兩三次音符切換。常見做法是引入一個約 10 毫秒的計數(shù)窗口檢測到電平變化后開始計時計時結(jié)束電平仍然穩(wěn)定才認(rèn)為按鍵有效。在原理圖里我一般這樣設(shè)計模塊內(nèi)部結(jié)構(gòu) 1. 50MHz 時鐘經(jīng) 20 級計數(shù)器分頻得到約 1kHz 采樣時鐘 2. 每次采樣時鐘上升沿將按鍵電平移入 4 位移位寄存器 3. 僅當(dāng) 4 位寄存器全為 0 或全為 1 時判定為有效電平 4. 有效電平送入 3 位優(yōu)先編碼器生成音符索引原理圖里體現(xiàn)為一個分頻計數(shù)器生成采樣脈沖兩個串行移位寄存器做延時濾波再加一個優(yōu)先編碼器輸出索引。Quartus 的符號庫里能找到 7493 這類現(xiàn)成計數(shù)器也可以直接用 LPM_COUNTER 宏功能模塊不需要自己搭門電路。消抖時間參數(shù)可以這樣調(diào)采樣時鐘頻率取 1kHz每個電平需要連續(xù) 4 次采樣確認(rèn)對應(yīng)約 4ms 的確認(rèn)窗口。如果你覺得 4ms 對比較硬的按鍵還是偏短把采樣頻率降到 500Hz確認(rèn)時間就變成約 8ms。原理圖里只需要改分頻計數(shù)器的模值不影響其他區(qū)域。3.2 音符分頻模塊查表驅(qū)動計數(shù)器分頻模塊是發(fā)聲的關(guān)鍵。它接收按鍵編碼器輸出的 3 位索引通過查表取得對應(yīng)的分頻系數(shù)然后用這個系數(shù)控制計數(shù)器的翻轉(zhuǎn)時刻。這里有兩種常見做法一種是用多路選擇器在 8 個預(yù)置分頻系數(shù)里選一個另一種是用只讀存儲器 ROM 存放一張 8 深度的分頻系數(shù)表按地址讀取。我更推薦第二種因為當(dāng)你要擴(kuò)展成 12 個半音甚至 16 個音符時只需要改 mif 文件內(nèi)容原理圖一根連線都不用動。一個 8 音符版本的 mif 文件長這樣-- freq_table.mif 片段8 音符版本 DEPTH 8; WIDTH 17; ADDRESS_RADIX DEC; DATA_RADIX DEC; CONTENT BEGIN 0 : 95556; -- C4 261.63Hz 1 : 85138; -- D4 293.66Hz 2 : 75846; -- E4 329.63Hz 3 : 71588; -- F4 349.23Hz 4 : 63776; -- G4 392.00Hz 5 : 56818; -- A4 440.00Hz 6 : 50619; -- B4 493.88Hz 7 : 47778; -- C5 523.25Hz END;mif 文件寫好之后在原理圖里放一個 altsyncram 或 LPM_ROM 宏功能模塊數(shù)據(jù)文件指向這個 mifROM 地址來自按鍵編碼器輸出數(shù)據(jù)輸出直接接到計數(shù)器預(yù)置值輸入端。整個過程不需要寫任何 HDL 代碼這是原理圖設(shè)計最舒服的地方。ROM 輸出的分頻系數(shù)需要配合一個加載信號配合使用按鍵變化時產(chǎn)生一個 load 脈沖把新系數(shù)寫入計數(shù)器預(yù)置端沒有按鍵變化時計數(shù)器保持現(xiàn)有分頻系數(shù)繼續(xù)運(yùn)行音符不會斷。對應(yīng)到原理圖就是一個計數(shù)器模塊模值輸入端接 ROM 數(shù)據(jù)輸出加載端接按鍵變化脈沖。為了幫你理解這個模塊在原理圖里的行為我用一句 Verilog 描述它的參考邏輯實(shí)際工程中你不需要寫這行代碼只需要在原理圖里把 LPM_COUNTER 的模值輸入連好// 分頻計數(shù)器參考模型原理圖中用宏功能模塊實(shí)例化 module tone_div #(parameter WIDTH 17)( input clk, // 50MHz 系統(tǒng)時鐘 input [WIDTH-1:0] div,// 來自 ROM 的分頻系數(shù) input load, // 按鍵變化時產(chǎn)生一個加載脈沖 output reg tone_out // 輸出方波接無源蜂鳴器 ); reg [WIDTH-1:0] cnt; always (posedge clk) begin if (load) begin cnt 0; tone_out 1b1; end else if (cnt div - 1) begin cnt 0; tone_out ~tone_out; end else begin cnt cnt 1; end end endmodule邏輯說明load 為高時計數(shù)器清零并輸出高電平之后每個時鐘周期 cnt 加 1計數(shù)到 div-1 時輸出翻轉(zhuǎn)。輸出方波周期等于 2 × div × 時鐘周期因此 div 就是前面算的分頻系數(shù) K。WIDTH 參數(shù)由最大分頻系數(shù)決定中央 C 的 95556 需要 17 位二進(jìn)制表示所以統(tǒng)一取 17 位后續(xù)擴(kuò)展到更低音區(qū)也能放下。3.3 頂層原理圖連線方式頂層 BDF 是把各模塊串起來的地方。一張 8 音符頂層圖的關(guān)鍵連線可以整理成下面的清單實(shí)際畫圖時在圖紙上對應(yīng)拖放即可源信號連接目標(biāo)說明8 個按鍵輸入引腳keyboard_ctrl 模塊輸入按鍵默認(rèn)低電平有效keyboard_ctrl 輸出 note_index[2:0]ROM 地址端選擇音符分頻系數(shù)keyboard_ctrl 輸出 key_pressedtone_div 加載端 load按下或釋放時觸發(fā)重新加載50MHz 時鐘引腳keyboard_ctrl 與 tone_div 時鐘端所有模塊共用同一個時鐘源ROM 數(shù)據(jù)輸出 div_coef[16:0]tone_div 分頻系數(shù)輸入位寬必須匹配tone_div 輸出 tone_out輸出引腳 spk_out接無源蜂鳴器或小喇叭連線時注意幾點(diǎn)所有模塊的時鐘必須來自同一個全局時鐘引腳不要在原理圖中間用門電路產(chǎn)生派生時鐘否則 Quartus 在做時序分析時會給你報一堆時鐘偏斜的問題ROM 地址和數(shù)據(jù)位寬要跟計數(shù)器預(yù)置值匹配不一致時編譯會提示長度不匹配輸出引腳建議加一個輸出緩沖或直接連普通 IO避免下載后出現(xiàn)多個驅(qū)動源互相拉扯電平。編譯順序建議這樣先單獨(dú)編譯 keyboard_ctrl 模塊用仿真確認(rèn)消抖和編碼正確再單獨(dú)編譯 tone_div 模塊給定一個固定分頻系數(shù)看輸出頻率對不對最后拼裝頂層統(tǒng)一編譯。每個模塊先獨(dú)立驗證再做系統(tǒng)集成能減少至少一半的排查時間。4. 常見問題與避坑仿真、下載與硬件調(diào)試實(shí)錄4.1 現(xiàn)象仿真波形正常上板完全無聲這是我自己踩過最狠的一個坑。分頻器仿真波形每個翻轉(zhuǎn)點(diǎn)都對下載到板子后卻一點(diǎn)聲音沒有。排查到最后才發(fā)現(xiàn)板子上那個蜂鳴器是有源的它只需要直流電平就能響并不需要外部方波。工程輸出引腳接錯到了有源蜂鳴器的控制端方波的快速高低變化在里面被當(dāng)成開關(guān)信號處理效果就是極其微弱甚至聽不到聲音。解決辦法是查板子原理圖確認(rèn)揚(yáng)聲器類型。無源蜂鳴器才適合接收分頻方波。如果板子上同時有兩種蜂鳴器檢查是否有跳線切換把輸出引腳切到無源那一路。另外還有一個容易被忽略的原因輸出方波頻率如果落在 200Hz 以下或 20kHz 以上人耳敏感度很低聽起來就像沒聲音。用示波器或邏輯分析儀測一下 spk_out 引腳的波形頻率很快能確認(rèn)。4.2 現(xiàn)象按鍵響應(yīng)時快時慢甚至一鍵觸發(fā)多個音符這是消抖窗口設(shè)置太短的典型表現(xiàn)。機(jī)械按鍵抖動一般持續(xù) 5 到 20 毫秒如果采樣確認(rèn)窗口只有 2 毫秒抖動期間的雜散電平會被當(dāng)成有效信號編碼器在一個按鍵事件里輸出了多個不同的音符索引聽感上就是一個鍵按下去聲音亂跳。解決方法是在消抖模塊里把采樣時鐘降到 1kHz 以下讓確認(rèn)窗口覆蓋整個抖動區(qū)間。原理圖上的改動通常是把分頻計數(shù)器的模值調(diào)大把采樣頻率從 2kHz 降到 1kHz確認(rèn)窗口從 2ms 變成 4ms。如果按鍵質(zhì)量比較差可以繼續(xù)降到 500Hz。不要用遮蔽法處理抖動就是那種檢測到變化后封鎖輸入一段時間的方法這種方式容易漏掉連續(xù)按下的第二個按鍵。4.3 現(xiàn)象每個音符都偏高或偏低且越往高音偏差越大這類現(xiàn)象跟分頻系數(shù)的取整方式有關(guān)。以 A4 的 440Hz 為例50MHz 除以兩倍頻率等于 56818.18取整到 56818 時輸出頻率誤差極小人耳聽不出來。但如果你有的音符向下取整有的四舍五入誤差就會被放大。更常見的錯誤是翻轉(zhuǎn)點(diǎn)寫錯計數(shù)器按 K 計數(shù)時如果誤在計數(shù)到 K 時才清零由于計數(shù)器從 0 開始實(shí)際等于走了 K1 個周期每個音符都偏低約一個時鐘周期的比例。解決辦法是統(tǒng)一取整規(guī)則并且嚴(yán)格按“計數(shù)到 K-1 清零或翻轉(zhuǎn)”來設(shè)計。高音區(qū)偏差更明顯是因為頻率越高 K 越小取整的相對誤差就越大。對 8 音符單手琴來說誤差在 1% 以內(nèi)幾乎聽不出區(qū)別但如果擴(kuò)展成 12 半音版本建議每個系數(shù)先算到小數(shù)點(diǎn)后兩位再統(tǒng)一向下取整然后用仿真確認(rèn)每個音的實(shí)際輸出頻率。4.4 現(xiàn)象下載成功但按鍵引腳電平異常相鄰按鍵互相影響第一反應(yīng)是查原理圖連線但實(shí)際上很多是引腳分配沖突。開發(fā)板上的按鍵往往和 LED 或擴(kuò)展排針共用同一個 IO默認(rèn)情況下該引腳可能已經(jīng)被分配成別的用途比如方向被設(shè)為輸出或者在電路上接了強(qiáng)下拉。解決辦法是打開 Pin Planner重新指定按鍵輸入引腳并檢查 .qsf 文件里有沒有同一個引腳被分配給兩個信號。另一類電平異常來自上拉設(shè)置。FPGA 輸入引腳默認(rèn)可能有弱上拉如果按鍵電路是按鍵一端接地、另一端接 IO那么默認(rèn)上拉是合理設(shè)計按下時拉低。如果按鍵接的是電源就需要關(guān)掉內(nèi)部上拉或把消抖模塊里的有效電平判斷反過來。我一般把按鍵統(tǒng)一設(shè)計成低電平有效消抖模塊只判斷低電平狀態(tài)這樣換不同的開發(fā)板時改動最小。4.5 現(xiàn)象增加消抖延遲后出現(xiàn)長按無響應(yīng)這個坑很隱蔽。為了讓消抖更可靠有人會把采樣確認(rèn)時間加長結(jié)果快速連續(xù)按兩個鍵時第二個鍵沒了反應(yīng)。原因是確認(rèn)窗口太長第一個鍵釋放和第二個鍵按下都落進(jìn)了同一個窗口邏輯認(rèn)為還是沒有變化。解決思路是采樣與確認(rèn)分離持續(xù)以固定周期采樣按鍵電平但只在電平狀態(tài)從穩(wěn)定變成另一個穩(wěn)定時才產(chǎn)生脈沖。換句話說消抖模塊的職責(zé)是過濾毛刺而不是阻塞輸入。每次采樣都運(yùn)行只是輸出脈沖的條件變嚴(yán)格了。這個做法在原理圖里實(shí)現(xiàn)也不復(fù)雜把移位寄存器的輸出端加一個異或門檢邊檢測到邊沿再觸發(fā)加載即可。5. 二次開發(fā)思路從單音電子琴擴(kuò)展成雙音與音色切換5.1 先給音調(diào)分頻器加一個第二聲部最簡單有效的改造是把 tone_div 模塊復(fù)制一份新模塊使用另一組 ROM 表兩個模塊的輸出通過一個或門合并到同一個蜂鳴器引腳。注意不能直接短路兩個輸出FPGA 引腳內(nèi)部驅(qū)動并聯(lián)會互相沖突應(yīng)該在輸出級加一個邏輯門或三態(tài)緩沖器。這樣左手按低音區(qū)按鍵、右手按高音區(qū)按鍵時兩個分頻器同時工作就實(shí)現(xiàn)了雙音效果。改造后按這樣的順序驗證先單獨(dú)聽低音分頻器輸出確認(rèn)音準(zhǔn)再單獨(dú)聽高音分頻器確認(rèn)音準(zhǔn)最后把兩個輸出合并聽有沒有相互搶占時序?qū)е碌拿搪?。如果合并后聲音變悶或變炸多半是輸出?qū)動能力不足在合并點(diǎn)后面加一個緩沖器就能解決。5.2 用占空比調(diào)節(jié)模擬音色如果你不滿足于方波可以給 tone_div 加一個占空比參數(shù)。具體做法是在計數(shù)器后面加兩個比較器計數(shù)器計到半周期時輸出翻高再計到一個偏移值后翻低。例如把高電平時間固定為周期 50%就是標(biāo)準(zhǔn)方波改成 25%輸出窄脈沖波諧波成分更豐富聽起來更尖銳改成 75% 則圓潤一些。在原理圖上這個改動只需要加兩個常數(shù)寄存器和一個比較器不需要重新算分頻系數(shù)。占空比參數(shù)可以做成兩個撥碼開關(guān)輸入運(yùn)行時切換音色。加上前面的雙音改造這臺電子琴就從單音玩具變成了可以玩出一點(diǎn)樣子的實(shí)驗平臺。我把這套工程拆完之后的習(xí)慣是每次改動只動一個模塊下載前先在仿真器里看一條音符的輸出頻率確認(rèn)誤差可接受后再上板試聽。這樣手里拿到任何一塊新板子半天之內(nèi)都能把引腳、分頻系數(shù)、蜂鳴器類型這些環(huán)境變量全部適配完。希望這篇拆解幫到你也少走一些我曾經(jīng)走過的彎路。本文還有配套的精品資源點(diǎn)擊獲取