演奏電路設(shè)計(jì):從分頻原理到Verilog實(shí)現(xiàn))
簡(jiǎn)介本資源是一套基于FPGA實(shí)現(xiàn)音樂(lè)自動(dòng)演奏的完整Verilog開(kāi)發(fā)工程面向數(shù)字電路初學(xué)者、嵌入式硬件愛(ài)好者及高校電子類(lèi)課程實(shí)踐者解決從音符編碼、頻率生成到音頻輸出的全流程硬件設(shè)計(jì)問(wèn)題。壓縮包共166個(gè)文件約13.85MB涵蓋核心Verilog源碼如decoder.v、clk_divide.v、music_top.v等、Quartus工程配置文件.qpf、.qsf、.bdf、綜合與仿真報(bào)告.rpt、.vwf、.logdb、IP配置信息.mif、.sof及備份與調(diào)試輔助文件.bak、.hdb、.cdb結(jié)構(gòu)完整便于理解FPGA音樂(lè)系統(tǒng)各模塊協(xié)同邏輯。已有750人學(xué)習(xí)下載資源包含可直接編譯下載的頂層工程、分模塊注釋清晰的RTL代碼、典型音階頻率映射表及基礎(chǔ)DA轉(zhuǎn)換接口說(shuō)明有助于掌握時(shí)序控制、分頻設(shè)計(jì)、ROM查表發(fā)聲等關(guān)鍵技能并為擴(kuò)展多音軌、PWM音量調(diào)節(jié)等進(jìn)階功能提供可靠起點(diǎn)。1. 項(xiàng)目概述一塊FPGA板子能演奏什么做FPGA的第一年我干過(guò)一件特別有成就感的事讓一塊普通的FPGA開(kāi)發(fā)板上電之后從蜂鳴器里完整地放出了《小星星》。說(shuō)實(shí)話(huà)音質(zhì)和早年那種叮咚門(mén)鈴差不多但旁邊不懂硬件的室友聽(tīng)完愣了半天——這玩意兒還能唱歌這個(gè)基于FPGA的音樂(lè)演奏電路項(xiàng)目本質(zhì)上就是做一個(gè)以FPGA為控制核心的音調(diào)發(fā)生器。它能做兩件事一是按預(yù)設(shè)曲譜自動(dòng)播放音樂(lè)二是通過(guò)按鍵當(dāng)作簡(jiǎn)易電子琴來(lái)實(shí)時(shí)演奏。無(wú)論哪種模式核心都是同一件事把樂(lè)譜上的音符翻譯成硬件邏輯能理解的頻率再把頻率轉(zhuǎn)換成驅(qū)動(dòng)揚(yáng)聲器的電信號(hào)。為什么選FPGA來(lái)做這個(gè)看似殺雞用牛刀的項(xiàng)目我自己做完之后體會(huì)很深首先是時(shí)序的可控性音高要求的是高精度的周期性波形FPGA的時(shí)鐘分頻能做到百萬(wàn)分之一量級(jí)的頻率精度其次是并行性學(xué)完這個(gè)項(xiàng)目你會(huì)對(duì)狀態(tài)機(jī)數(shù)據(jù)通路這套FPGA設(shè)計(jì)的核心方法論有實(shí)實(shí)在在的體感而不是停留在課本上。無(wú)論你是剛學(xué)Verilog的初學(xué)者還是想給學(xué)生找一個(gè)綜合性教學(xué)項(xiàng)目的老師這個(gè)題目都值得動(dòng)手做一遍。它涵蓋的模塊拆解、參數(shù)計(jì)算、仿真調(diào)試、上板驗(yàn)證幾乎是一部微型FPGA開(kāi)發(fā)流程的縮影。2. 核心原理數(shù)字世界里怎么描述一個(gè)音符2.1 十二平均律與音符頻率表要生成音樂(lè)得先回答一個(gè)基本問(wèn)題一個(gè)音符在物理上到底是什么答案是振動(dòng)頻率。國(guó)際標(biāo)準(zhǔn)把A4定為440Hz也就是鋼琴上小字一組的la每秒振動(dòng)440次。其他所有音符的頻率都不是拍腦袋定的而是按照十二平均律算出來(lái)的每相鄰兩個(gè)半音之間的頻率比是常數(shù)等于2的十二次方根。[ f(n) 440 \times 2^{\frac{n-69}{12}} ]這里n是MIDI標(biāo)準(zhǔn)里的音符編號(hào)A4對(duì)應(yīng)69。剛才這個(gè)公式說(shuō)的是從A4往上數(shù)n-69個(gè)半音每上升一個(gè)半音頻率乘一次1.059463。拿C4來(lái)算C4比A4低9個(gè)半音所以C4頻率約等于440除以1.059463的9次方算出來(lái)就是261.63Hz。這個(gè)公式可以一直往上套也一直往下套所以整個(gè)十二平均律體系是自洽的任何一個(gè)音高都能用唯一一個(gè)頻率來(lái)描述。我在做查表之前習(xí)慣把要用的音符頻率先算好列成一張表再根據(jù)系統(tǒng)時(shí)鐘算出分頻參數(shù)。下面這張表是C大調(diào)一個(gè)八度里最常見(jiàn)的8個(gè)音以及50MHz系統(tǒng)時(shí)鐘下對(duì)應(yīng)的分頻計(jì)數(shù)參數(shù)后面寫(xiě)代碼會(huì)直接用。音符頻率(Hz)50MHz下分頻半周期計(jì)數(shù)值四舍五入休止符00輸出直接置低C4261.6395553D4293.6685129E4329.6375833F4349.2371581G4392.0063776A4440.0056818B4493.8850619C5523.2547776這張表看著簡(jiǎn)單但其中兩個(gè)細(xì)節(jié)值得說(shuō)清楚。第一分頻計(jì)數(shù)值用的是半周期因?yàn)槲覀兿胼敵龇讲ㄒ葦?shù)半個(gè)周期的時(shí)鐘數(shù)翻轉(zhuǎn)一次電平再數(shù)半個(gè)周期再翻轉(zhuǎn)回來(lái)這樣一個(gè)完整周期才是目標(biāo)頻率。第二四舍五入很重要取整誤差經(jīng)過(guò)人耳放大后一般聽(tīng)不出來(lái)但如果你直接截?cái)嗳≌`差方向是固定的個(gè)別音符的偏差會(huì)比四舍五入明顯。為了這一點(diǎn)差別不值得老老實(shí)實(shí)四舍五入。2.2 三種產(chǎn)生音調(diào)的方法我為什么選了查表分頻確定了目標(biāo)頻率之后下一步就是怎么用數(shù)字電路產(chǎn)生這個(gè)頻率。我自己嘗試過(guò)三種方案這里把它們的思路和取舍都講一下。方案一純分頻計(jì)數(shù)器。最直觀的做法是給每個(gè)音符單獨(dú)寫(xiě)一個(gè)計(jì)數(shù)器各自數(shù)到對(duì)應(yīng)的半周期計(jì)數(shù)值然后翻轉(zhuǎn)輸出。好處是邏輯清晰初學(xué)者一眼就能看懂壞處是代碼冗余8個(gè)音符就得寫(xiě)8個(gè)幾乎一樣的計(jì)數(shù)器模塊如果要擴(kuò)展到多個(gè)八度代碼會(huì)膨脹得非常難受。方案二查表動(dòng)態(tài)裝載分頻器。這是我在最終版里采用的方案。分頻器模塊只有一個(gè)它的計(jì)數(shù)上限不是寫(xiě)死的而是來(lái)自一個(gè)外部的輸入信號(hào)。系統(tǒng)切換音符時(shí)只需要把查表得到的新計(jì)數(shù)值送給分頻器分頻器就會(huì)自動(dòng)按新頻率工作。這樣音符越多只需要擴(kuò)展查表不需要新增硬件邏輯。這個(gè)方案的缺點(diǎn)是需要想清楚切換邊界如果計(jì)數(shù)到一半突然改計(jì)數(shù)值當(dāng)前這個(gè)周期不算完整但對(duì)應(yīng)到聽(tīng)覺(jué)上這一瞬間的過(guò)渡對(duì)人耳來(lái)說(shuō)是無(wú)法感知的所以實(shí)際用起來(lái)完全沒(méi)問(wèn)題。方案三DDS直接數(shù)字頻率合成。DDS的思路和前面兩種完全不同它用一個(gè)累加器來(lái)做相位累加輸出查找表里的波形幅度值。核心公式是相位增量 (目標(biāo)頻率 × 2^N) / 系統(tǒng)時(shí)鐘。以32位累加器、50MHz時(shí)鐘為例A4對(duì)應(yīng)的相位增量約等于37796。DDS的優(yōu)勢(shì)是可以無(wú)縫切換頻率可以做滑音、掃頻、多聲道疊加改頻率只需要改一個(gè)增量值。但代價(jià)是邏輯復(fù)雜還要配波形ROM對(duì)只想播放一首固定曲子的入門(mén)項(xiàng)目來(lái)說(shuō)屬于重量級(jí)武器。我個(gè)人的建議是第一次做這個(gè)項(xiàng)目老老實(shí)實(shí)用方案二把響→準(zhǔn)→唱完整曲這條鏈路跑通等你理解了底層邏輯再回到DDS感受會(huì)完全不一樣。2.3 音頻輸出路蜂鳴器、喇叭還是功放信號(hào)生成出來(lái)了最后還得讓它出聲。這一環(huán)節(jié)的教訓(xùn)特別多我自己在這里栽過(guò)跟頭新手尤其要注意。有源蜂鳴器 vs 無(wú)源蜂鳴器這是第一個(gè)必須搞清楚的坑。有源蜂鳴器內(nèi)部自帶振蕩電路通電就響而且頻率固定你給它外部方波信號(hào)它也不會(huì)變調(diào)它只響應(yīng)通還是斷。這種蜂鳴器只能做報(bào)警器做不了音樂(lè)演奏。無(wú)源蜂鳴器沒(méi)有內(nèi)部振蕩源你給它什么頻率的方波它就發(fā)出什么頻率的聲音這才是我們要的。區(qū)分方法很簡(jiǎn)單看引腳旁邊的絲印或者拿萬(wàn)用表測(cè)電阻無(wú)源的通常電阻都偏小。買(mǎi)之前直接問(wèn)清楚無(wú)源電磁式蜂鳴器不然快遞到了你會(huì)收獲一個(gè)只能嗶——的喇叭。驅(qū)動(dòng)方式也要注意。FPGA引腳的驅(qū)動(dòng)能力有限直接驅(qū)動(dòng)蜂鳴器經(jīng)常出現(xiàn)聲音發(fā)悶、響度不夠的問(wèn)題。我試過(guò)用NPN三極管S8050這類(lèi)做開(kāi)關(guān)放大效果立竿見(jiàn)影。三極管基極接FPGA引腳通過(guò)一個(gè)1kΩ電阻限流集電極接蜂鳴器負(fù)極發(fā)射極接地蜂鳴器正極接電源。FPGA輸出高電平時(shí)三極管導(dǎo)通蜂鳴器通電發(fā)聲輸出低電平時(shí)斷開(kāi)。如果你用的是帶功放的開(kāi)發(fā)板板載喇叭或者音頻接口那更省事FPGA直接輸出方波到功放輸入就行。外部喇叭功放情況也常見(jiàn)比如手頭只有普通8Ω小喇叭就需要LM386或者PAM8403這類(lèi)功放模塊。方波信號(hào)通過(guò)功放放大后驅(qū)動(dòng)喇叭音量比蜂鳴器大得多適合在教室里演示。如果想追求更好的音質(zhì)可以用PWM輸出加RC低通濾波把方波變成近似正弦波但這是后話(huà)了第一版不用追求到這個(gè)程度。3. 實(shí)操實(shí)現(xiàn)從模塊到一首完整的曲子3.1 頂層架構(gòu)與管腳規(guī)劃老規(guī)矩設(shè)計(jì)之前先畫(huà)頂層框圖。我的劃分是四個(gè)模塊按鍵消抖模塊、曲譜播放狀態(tài)機(jī)、音符查表分頻器、音頻輸出驅(qū)動(dòng)。每個(gè)模塊各司其職互相之間只通過(guò)清晰的總線(xiàn)信號(hào)連接。clk_50MHz ──→ 按鍵消抖 ──→ 播放狀態(tài)機(jī) ──→ 音符查表 ──→ 分頻器 ──→ 蜂鳴器 rst_n ──→ 模式切換 按鍵[3:0] ──→ 手動(dòng)演奏輸入管腳規(guī)劃這一環(huán)看似瑣碎但特別容易出錯(cuò)。開(kāi)發(fā)板上的物理引腳五花八門(mén)蜂鳴器可能連在某個(gè)特定引腳上按鍵分布在另一組引腳。務(wù)必先在原理圖或者板卡手冊(cè)里找到蜂鳴器、按鍵對(duì)應(yīng)的FPGA引腳編號(hào)再寫(xiě)約束文件。我之前見(jiàn)過(guò)有同學(xué)憑感覺(jué)寫(xiě)引腳號(hào)結(jié)果綜合下載后沒(méi)反應(yīng)查了半天才發(fā)現(xiàn)引腳約束根本不對(duì)。另外如果蜂鳴器是負(fù)邏輯驅(qū)動(dòng)低電平響還要在代碼里多取一次反這個(gè)細(xì)節(jié)在約束文件旁邊順手記一筆免得忘了。3.2 音符查表與分頻器實(shí)現(xiàn)這里把最終版的Verilog貼出來(lái)代碼不復(fù)雜但包含幾個(gè)容易踩坑的點(diǎn)我會(huì)逐個(gè)說(shuō)明。首先是音符查表模塊。它就是一個(gè)純組合邏輯case輸入音符索引輸出對(duì)應(yīng)的半周期計(jì)數(shù)值module note_lut ( input wire [4:0] note_idx, // 0休止, 1C4, 2D4 ... 8C5 output reg [16:0] half_period ); always (*) begin case (note_idx) 5d0: half_period 17d0; // 休止 5d1: half_period 17d95553; // C4 261.63Hz 5d2: half_period 17d85129; // D4 293.66Hz 5d3: half_period 17d75833; // E4 329.63Hz 5d4: half_period 17d71581; // F4 349.23Hz 5d5: half_period 17d63776; // G4 392.00Hz 5d6: half_period 17d56818; // A4 440.00Hz 5d7: half_period 17d50619; // B4 493.88Hz 5d8: half_period 17d47776; // C5 523.25Hz default: half_period 17d0; // 未定義音高默認(rèn)休止 endcase end endmodule然后是分頻器模塊。它接收查表過(guò)來(lái)的半周期計(jì)數(shù)值輸出方波module tone_gen ( input wire clk, input wire rst_n, input wire [16:0] half_period, // 0 表示休止 output reg tone ); reg [16:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 17d0; tone 1b0; end else if (half_period 17d0) begin cnt 17d0; tone 1b0; // 休止符輸出低電平 end else if (cnt half_period - 1) begin cnt 17d0; tone ~tone; // 到半周期翻轉(zhuǎn)一次 end else begin cnt cnt 1b1; end end endmodule這個(gè)模塊有兩個(gè)細(xì)節(jié)值得注意。第一counter的位寬選17位是因?yàn)樽畲笥?jì)數(shù)值95553對(duì)應(yīng)的二進(jìn)制需要17位如果位寬不夠綜合工具不會(huì)報(bào)錯(cuò)但計(jì)數(shù)器會(huì)提前回繞輸出的頻率就完全錯(cuò)了。保險(xiǎn)的做法是用系統(tǒng)函數(shù)$clog2計(jì)算localparam WIDTH $clog2(MAX_CNT 1);一勞永逸。第二休止符的處理是獨(dú)立的因?yàn)闊o(wú)聲和堵住嘴的方波是兩回事把tone固定拉低才是真正的休止。3.3 節(jié)拍控制與曲譜狀態(tài)機(jī)有了音高下一步是節(jié)奏。我一直覺(jué)得能響不算成功能按節(jié)拍把一首曲子彈完才算。節(jié)拍的本質(zhì)就是每個(gè)音符響多久這同樣需要一個(gè)計(jì)數(shù)器。以120BPM每分鐘120拍為例一拍持續(xù)0.5秒在50MHz時(shí)鐘下就是25000000個(gè)周期。我定義一個(gè)beat_cnt計(jì)數(shù)器每數(shù)到2500萬(wàn)就認(rèn)為過(guò)去了1拍。曲譜里每個(gè)音符的時(shí)長(zhǎng)用拍數(shù)表示比如四分音符是1拍八分音符是半拍二分音符是2拍。曲譜怎么存我用的方法是把一首曲子切成一個(gè)個(gè)步驟每個(gè)步驟包含音符索引和持續(xù)拍數(shù)用一個(gè)case語(yǔ)句描述播放狀態(tài)機(jī)逐條讀取。以《小星星》第一句為例簡(jiǎn)譜是 1 1 5 5 6 6 5后面接休止對(duì)應(yīng)的狀態(tài)機(jī)片段如下localparam NOTE_C4 5d1; localparam NOTE_G4 5d5; localparam NOTE_A4 5d6; localparam REST 5d0; // 每個(gè) step 用 {duration(拍數(shù)), note_idx} 表示 // 以 4 拍為一小節(jié)這里只示意第一句 always (posedge clk or negedge rst_n) begin if (!rst_n) begin step_idx 0; beat_cnt 0; note_idx REST; state IDLE; end else begin case (state) IDLE: begin if (start_flag) begin state PLAY; step_idx 0; beat_cnt 0; end end PLAY: begin // 讀取當(dāng)前step的音符 step_note_sel(step_idx); // 設(shè)置 note_idx if (beat_cnt BEAT_CNT_MAX - 1) begin beat_cnt 0; // 當(dāng)前拍結(jié)束進(jìn)入下一個(gè)step if (step_idx LAST_STEP) begin state DONE; // 播完 end else begin step_idx step_idx 1b1; end end else begin beat_cnt beat_cnt 1b1; end end // ... DONE 狀態(tài)回到 IDLE endcase end end這里我把半拍的處理方式說(shuō)一下。如果曲子里有八分音符簡(jiǎn)單起見(jiàn)可以全部用最小節(jié)拍單位比如半拍作基準(zhǔn)四分音符記作2八分音符記作1二分音符記作4。這樣整個(gè)曲譜狀態(tài)機(jī)只需要一個(gè)統(tǒng)一的拍號(hào)邏輯不用為半拍能不能整除操心。3.4 按鍵消抖與手動(dòng)演奏模式自動(dòng)播放只是聽(tīng)手動(dòng)演奏才是玩。這需要把按鍵接進(jìn)來(lái)每個(gè)按鍵代表一個(gè)音符按下就發(fā)對(duì)應(yīng)的頻率松開(kāi)就回到休止。機(jī)械按鍵有個(gè)典型毛病按下和松開(kāi)的瞬間觸點(diǎn)會(huì)彈跳持續(xù)5~20ms期間電平忽高忽低。如果不做消抖你按一次按鍵FPGA可能看到的是七八個(gè)一次按壓演奏出來(lái)的就是一團(tuán)亂音。消抖的原理也不復(fù)雜連續(xù)采樣按鍵電平只有當(dāng)電平穩(wěn)定持續(xù)一段時(shí)間我習(xí)慣用20ms才認(rèn)為按鍵狀態(tài)真正變化了。在50MHz下20ms是100萬(wàn)個(gè)時(shí)鐘周期需要一個(gè)20位的計(jì)數(shù)器module key_debounce #( parameter CNT_MAX 1_000_000 // 20ms 50MHz )( input wire clk, input wire key_in, output wire key_pressed_pulse // 按下瞬間的單脈沖 ); reg [19:0] cnt; reg key_stable, key_prev; always (posedge clk) begin if (key_in ! key_stable) begin // 輸入與穩(wěn)定值不一致開(kāi)始計(jì)數(shù) if (cnt CNT_MAX) begin cnt 20d0; key_stable key_in; end else begin cnt cnt 1b1; end end else begin cnt 20d0; end end // 邊沿檢測(cè)輸出一個(gè)時(shí)鐘周期的脈沖 always (posedge clk) begin key_prev key_stable; end assign key_pressed_pulse key_stable ~key_prev; endmodule另一個(gè)細(xì)節(jié)是手動(dòng)演奏模式和自動(dòng)播放模式需要互斥。我的做法是一個(gè)模式選擇鍵每按一次在兩種模式之間切換。切到手動(dòng)模式時(shí)自動(dòng)播放狀態(tài)機(jī)進(jìn)入IDLE播放用音符全部置為休止避免兩個(gè)信號(hào)同時(shí)在驅(qū)動(dòng)蜂鳴器。同一條信號(hào)線(xiàn)被兩個(gè)模塊同時(shí)賦值在FPGA里是致命的Multiple Driver錯(cuò)誤這個(gè)后面排查章節(jié)還會(huì)說(shuō)。3.5 上板調(diào)試流程最后說(shuō)說(shuō)從寫(xiě)完代碼到聽(tīng)到聲音的調(diào)試順序。我的習(xí)慣是嚴(yán)格按下面四步走每步都驗(yàn)證通過(guò)再進(jìn)下一步避免問(wèn)題疊加后無(wú)從排查。第一步仿真驗(yàn)證。不用寫(xiě)太復(fù)雜的testbench就測(cè)一件最重要的事給一個(gè)音符索引比如C4看仿真波形里tone信號(hào)的周期是不是約等于1/261.63秒也就是3.82ms。這一步能排除大部分邏輯錯(cuò)誤。第二步單音驗(yàn)證。把代碼燒進(jìn)板子讓蜂鳴器持續(xù)播放一個(gè)固定的音符比如A4。用耳朵聽(tīng)對(duì)比標(biāo)準(zhǔn)音叉或手機(jī)調(diào)音軟件確認(rèn)音高基本準(zhǔn)確。如果有示波器或者邏輯分析儀直接量FPGA引腳出的頻率更靠譜。第三步播放全曲。把狀態(tài)機(jī)換成完整的曲譜聽(tīng)整首曲子的節(jié)奏是否自然、音符切換是否有突兀的雜音。第四步加入按鍵模式和模式切換。到這一步再處理的才是交互層面的問(wèn)題比如消抖時(shí)長(zhǎng)是否需要調(diào)整、多個(gè)按鍵同時(shí)按下的優(yōu)先級(jí)怎么定。我自己強(qiáng)烈建議不要跳步。很多同學(xué)急性子一上來(lái)就整曲播放加按鍵模式一起上結(jié)果上板后又是無(wú)聲又是亂音根本分不清是分頻的錯(cuò)還是節(jié)拍的錯(cuò)。踏實(shí)走完四步反而最快。4. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄這個(gè)項(xiàng)目我做了不止一遍也幫同學(xué)排查過(guò)無(wú)數(shù)回下面這幾個(gè)問(wèn)題出現(xiàn)的頻率最高我按癥狀—原因—解法的格式整理成速查表。癥狀大概率原因排查手法上電完全無(wú)聲用了有源蜂鳴器只響應(yīng)通斷換無(wú)源蜂鳴器或先單獨(dú)給不同頻率方波測(cè)試上電完全無(wú)聲引腳約束錯(cuò)誤信號(hào)沒(méi)送到蜂鳴器腳看原理圖核對(duì)約束用ILA或邏輯分析儀抓引腳上電完全無(wú)聲使能信號(hào)沒(méi)拉高播放狀態(tài)機(jī)停在IDLE加一個(gè)調(diào)試LED輸出觀察狀態(tài)是否流轉(zhuǎn)有聲音但音高偏低系統(tǒng)時(shí)鐘實(shí)際不是50MHz查板卡手冊(cè)確認(rèn)時(shí)鐘頻率重新計(jì)算分頻參數(shù)有聲音但音偏高計(jì)數(shù)器位寬不夠提前回繞檢查counter聲明位寬用$clog2計(jì)算按一次按鍵出好幾個(gè)音消抖時(shí)間太短或沒(méi)做消抖把消抖計(jì)數(shù)器加大到20ms確認(rèn)輸出是單脈沖自動(dòng)播放和手動(dòng)模式互相干擾兩個(gè)模塊同時(shí)驅(qū)動(dòng)蜂鳴器信號(hào)用模式信號(hào)與門(mén)隔離確保任一時(shí)刻只有一路有效綜合報(bào)Multiple Driver同一個(gè)reg在多個(gè)always塊里賦值重構(gòu)代碼把邏輯合并到一個(gè)always塊或拆成獨(dú)立信號(hào)4.1 上電無(wú)聲先分清三類(lèi)原因無(wú)聲是新手遇到最多的攔路虎但好消息是它的原因就三大類(lèi)硬件選型、管腳連接、邏輯使能。硬件選型在有源/無(wú)源蜂鳴器這一關(guān)就能卡住一批人我前面已經(jīng)強(qiáng)調(diào)過(guò)了。管腳連接排錯(cuò)的思路是信號(hào)到底有沒(méi)有到引腳用開(kāi)發(fā)板自帶的LED做驗(yàn)證很有效——把驅(qū)動(dòng)蜂鳴器的信號(hào)同時(shí)接一個(gè)LED如果LED有規(guī)律閃爍而蜂鳴器不響說(shuō)明信號(hào)到了引腳問(wèn)題在蜂鳴器驅(qū)動(dòng)電路如果LED也不閃那就是邏輯層面根本沒(méi)輸出回頭查狀態(tài)機(jī)和使能信號(hào)。4.2 音準(zhǔn)不對(duì)幾乎都是分頻比的問(wèn)題音準(zhǔn)不對(duì)十有八九是分頻參數(shù)和實(shí)際系統(tǒng)時(shí)鐘不匹配。最常見(jiàn)的情況是初學(xué)者以為所有開(kāi)發(fā)板都是50MHz結(jié)果板子是25MHz或100MHz一上來(lái)頻率就差一倍整個(gè)曲子聽(tīng)起來(lái)像開(kāi)了倍速或慢放。還有的情況是計(jì)數(shù)器比較條件寫(xiě)錯(cuò)cnt half_period - 1和cnt half_period - 1區(qū)別在于前者永遠(yuǎn)不會(huì)漏掉最大值后者如果half_period變化而cnt還沒(méi)來(lái)得及歸零可能永遠(yuǎn)等不到相等那一刻。我用的是穩(wěn)妥。4.3 按鈕失控與亂音消抖和狀態(tài)機(jī)邊界按鍵問(wèn)題大多不是消抖不夠而是沒(méi)有輸出單脈沖。很多人寫(xiě)完消抖模塊直接把穩(wěn)定的電平信號(hào)接到播放邏輯上結(jié)果按下期間狀態(tài)機(jī)一直在觸發(fā)自然就亂了。記得在消抖模塊后面加一個(gè)上升沿檢測(cè)輸出一個(gè)時(shí)鐘周期的脈沖。另外手動(dòng)演奏模式下如果有多個(gè)按鍵同時(shí)按下我的處理是給按鍵編號(hào)設(shè)定優(yōu)先級(jí)比如低編號(hào)的優(yōu)先這樣至少在邏輯上可預(yù)期不至于兩個(gè)音符信號(hào)的邊沿打架。狀態(tài)機(jī)的邊界條件也要檢查播完最后一拍能不能正確回到IDLERESET按下時(shí)能不能從任意狀態(tài)回到初始這些邊界問(wèn)題在仿真里不一定會(huì)暴露上板后卻會(huì)變成偶爾卡死的玄學(xué)問(wèn)題。4.4 綜合時(shí)序報(bào)錯(cuò)的排查最后一個(gè)常見(jiàn)問(wèn)題是綜合和實(shí)現(xiàn)階段的報(bào)錯(cuò)。Multiple Driver是我見(jiàn)過(guò)最多的錯(cuò)誤它的含義是一個(gè)信號(hào)wire或reg被多個(gè)always塊或者assign語(yǔ)句賦值。FPGA里每個(gè)信號(hào)只能有一個(gè)驅(qū)動(dòng)源如果你在播放狀態(tài)機(jī)里給note_idx賦值又在手動(dòng)模式邏輯里給note_idx賦值綜合工具就會(huì)報(bào)錯(cuò)。解法是把這兩段邏輯合并到一個(gè)always塊用模式信號(hào)做分支或者把信號(hào)拆成兩個(gè)內(nèi)部信號(hào)最后用一個(gè)三目選擇器合并。另一個(gè)高頻問(wèn)題是計(jì)數(shù)器位寬不夠綜合工具不會(huì)報(bào)錯(cuò)但行為就是不對(duì)所以我一再?gòu)?qiáng)調(diào)用$clog2算位寬別靠目測(cè)。5. 擴(kuò)展方向與幾點(diǎn)個(gè)人心得5.1 從單聲部到多聲部DDS帶來(lái)的升級(jí)空間做完上面這些你已經(jīng)擁有一個(gè)能自動(dòng)播放、能按鍵演奏的單音音樂(lè)電路了。如果還想往前走我建議的下一站是把單音升級(jí)成多聲部和弦。這時(shí)候前面提到的DDS方案就派上用場(chǎng)了一個(gè)NCO產(chǎn)生一個(gè)聲部?jī)蓚€(gè)NCO同時(shí)工作各自產(chǎn)生對(duì)應(yīng)音高的方波/正弦波然后在輸出端做加法就得到了雙音效果。配合相位累加器改一個(gè)增量值就能無(wú)縫切換頻率的特性你還可以做滑音、顫音這些音樂(lè)效果。具體來(lái)說(shuō)32位DDS累加器、50MHz時(shí)鐘下生成一個(gè)440Hz的A4信號(hào)相位增量是(440 × 2^32) / 50000000 ≈ 37796。想生成兩個(gè)聲部就做兩個(gè)相位累加器增量分別按各自音高算好輸出相加后經(jīng)過(guò)脈沖密度調(diào)制或者PWM送給功放。當(dāng)然輸出加法要注意幅度兩個(gè)信號(hào)相加可能溢出需要做截位或者縮放。這一步做通之后再去研究用ROM存波形、做包絡(luò)控制就是一條清晰的學(xué)習(xí)路徑。5.2 我踩過(guò)的坑與最終建議最后說(shuō)幾句掏心窩子的話(huà)。這個(gè)項(xiàng)目我前前后后做過(guò)三次每次都有新收獲也每次都會(huì)踩不同的坑。第一次栽在有源蜂鳴器上買(mǎi)回來(lái)怎么調(diào)都不變調(diào)最后才意識(shí)到硬件選型就錯(cuò)了。第二次在引腳約束上栽了跟頭信號(hào)邏輯沒(méi)問(wèn)題但物理上沒(méi)送到蜂鳴器白查了好幾天代碼。最讓我印象深刻的還是先仿真后上板這條鐵律——仿真里波形對(duì)了上板才敢保證大概率對(duì)仿真里稀里糊涂上板面對(duì)的就只能是玄學(xué)。如果讓我給正在做或者準(zhǔn)備做這個(gè)項(xiàng)目的人一條最核心的建議那就是第一版一定用最簡(jiǎn)單的結(jié)構(gòu)查表分頻也好單音播放也好把鏈路完整跑通。先讓它響再讓它準(zhǔn)最后再談讓它好聽(tīng)。任何一步跨過(guò)去都會(huì)在后面的環(huán)節(jié)花幾倍的時(shí)間來(lái)還債。這個(gè)項(xiàng)目的可擴(kuò)展空間其實(shí)比想象中大得多。曲譜存在ROM里之后換歌只是改數(shù)據(jù)文件的事按鍵加多了就是一臺(tái)小型電子琴再多研究一步I2S協(xié)議和DAC就能輸出真正的立體聲音樂(lè)。從一個(gè)蜂鳴器到一套完整的數(shù)字音頻系統(tǒng)中間缺的每一塊知識(shí)都會(huì)在下一步擴(kuò)展中逼著你補(bǔ)上。而這恰恰是FPGA學(xué)習(xí)最迷人的地方。本文還有配套的精品資源點(diǎn)擊獲取