:WM8731與DE2板I2C/I2S全鏈路打通)
簡介一個基于Altera DE2開發(fā)板EP2C35F672C6 FPGA與WM8731音頻編解碼器的完整音頻處理工程覆蓋從模擬音頻輸入、ADC數(shù)字化采樣、FPGA內(nèi)部處理、DAC重建到最終模擬輸出的全鏈路適合正在學習FPGA數(shù)字邏輯設(shè)計、音頻編解碼協(xié)議或嵌入式音頻應用的開發(fā)者。音頻信號先由WM8731內(nèi)置ADC完成采樣量化通過I2S串行總線和分頻后的SCLK/LRCK時鐘送入FPGAFPGA內(nèi)部可執(zhí)行濾波、增益或混音等處理隨后將結(jié)果送回同芯片的DAC輸出。開發(fā)者參照DE2官方范例重寫了代碼精簡了時序控制與數(shù)據(jù)搬運邏輯使整體結(jié)構(gòu)更清晰便于理解Master/Slave同步機制與采樣字長匹配等關(guān)鍵點。壓縮包共133個文件體積約911KB以Verilog源碼為主要學習對象同時包含Quartus工程配置、編譯報告、綜合數(shù)據(jù)庫與FPGA下載文件能夠從RTL級到布局布線逐層查看工程生成過程方便復盤與二次開發(fā)。已有826人學習下載。其中不僅有可直接運行的音頻通路代碼還展示了如何用I2S接口讓FPGA與WM8731可靠協(xié)同工作對掌握ADC/DAC數(shù)據(jù)交互、時鐘樹設(shè)計和基礎(chǔ)音頻處理算法都有實際幫助適合課設(shè)或畢設(shè)階段參考。1. 從「音頻回環(huán)」入門 FPGA 數(shù)字信號鏈路WM8731 與 DE2 板的一次完整打通手頭有一塊 DE2-EP2C35F672C6 開發(fā)板想驗證 FPGA 在真實信號鏈路里到底能干什么最簡單的切入點不是寫流水燈而是把板載的 WM8731 音頻編解碼芯片用起來LINE IN 進模擬信號芯片內(nèi)部 ADC 轉(zhuǎn)成數(shù)字流FPGA 拿到數(shù)據(jù)后原樣丟給 DAC再從耳機口聽到自己的聲音。這個項目看起來只是「輸入到輸出」的直通實際拆開會發(fā)現(xiàn)里面藏著一整套數(shù)字音頻系統(tǒng)的骨架——I2C 寄存器配置、I2S 時序?qū)R、主從時鐘同步、左右聲道采樣拼接任何一個環(huán)節(jié)沒做對出來的就是噪聲或者干脆靜音。適合手里有同款或類似開發(fā)板、想從點燈進階到真實信號處理的開發(fā)者也適合正在啃音頻協(xié)議、想找個能跑通的參考實現(xiàn)的人。項目最終交付的不是一句「Hello World」而是一條能通、能改、能繼續(xù)往上加效果的完整音頻通路。2. 硬件鏈路與工程結(jié)構(gòu)從 WM8731 芯片手冊到 FPGA 引腳約束2.1 信號鏈路全景模擬入、數(shù)字處理、模擬出整條鏈路的核心是板載的 WM8731 編解碼芯片這是一顆低功耗立體聲音頻 codec內(nèi)部集成了 ADC、DAC、耳機驅(qū)動和完整的時鐘管理單元。信號走向很直觀外部音頻從 LINE IN 接口進入經(jīng)過 WM8731 內(nèi)部 ADC 按設(shè)定的采樣率轉(zhuǎn)成數(shù)字信號通過 I2S 總線送給 FPGAFPGA 在內(nèi)部做處理——本項目里是直通或軟件可控的回環(huán)處理完的數(shù)據(jù)再經(jīng) I2S 總線送回 WM8731 的 DAC轉(zhuǎn)為模擬信號從耳機接口輸出。FPGA 夾在中間既是數(shù)據(jù)搬運工也是未來接入 DSP 算法的主戰(zhàn)場。WM8731 與 FPGA 之間的接口分兩路一路是 I2C負責配置芯片內(nèi)部寄存器包括采樣率、輸入輸出增益、電源管理等另一路是 I2S承載實時音頻數(shù)據(jù)流。I2C 配置通路決定了芯片的工作模式I2S 數(shù)據(jù)通路決定了音頻數(shù)據(jù)能否無損傳輸兩條路缺一不可。所以項目里最核心的工作就是兩件事寫一個能用的 I2C 控制器完成初始化再寫一個時序正確的 I2S 收發(fā)器把數(shù)據(jù)跑起來。2.2 引腳分配與工程目錄EP2C35F672C6 的板級約束EP2C35F672C6 是 Cyclone II 系列 FPGA672 個引腳的 FBGA 封裝DE2 板把 WM8731 接到了 FPGA 的特定引腳上。在寫邏輯之前必須先確認引腳約束。常見的分配方式如下set_location_assignment PIN_AC21 -to AUD_ADC_DAT set_location_assignment PIN_AF24 -to AUD_BCLK set_location_assignment PIN_AB23 -to AUD_DAC_DAT set_location_assignment PIN_AC22 -to AUD_DACLRCK set_location_assignment PIN_AB22 -to AUD_ADCLRCK set_location_assignment PIN_AE23 -to AUD_MCLK set_location_assignment PIN_AE25 -to AUD_I2C_SCLK set_location_assignment PIN_AD25 -to AUD_I2C_SDAT這段約束里AUD_ADC_DAT是 WM8731 的 ADC 串行數(shù)據(jù)輸出數(shù)據(jù)從芯片流向 FPGAAUD_DAC_DAT是 DAC 數(shù)據(jù)輸入數(shù)據(jù)從 FPGA 流向芯片AUD_BCLK是位時鐘AUD_DACLRCK和AUD_ADCLRCK分別是 DAC 和 ADC 的左右聲道幀同步信號AUD_MCLK是主時鐘。這些引腳名在 DE2 板附帶的使用手冊里有明確標注約束文件的本質(zhì)就是把 FPGA 的物理引腳和邏輯信號對應起來。如果這里寫錯一個引腳編號整個工程燒進去之后音頻通路就是斷的而且很難排查。工程目錄我習慣按功能拆分rtl目錄放 RTL 代碼包含頂層模塊、I2C 控制器、I2S 收發(fā)器db和incremental_db是綜合布局布線產(chǎn)生的中間目錄quartus目錄放工程文件sim放仿真用的 testbench。頂層模塊負責例化子模塊和連接信號I2C 控制器負責初始化 codecI2S 收發(fā)器負責音頻數(shù)據(jù)搬運。這樣的結(jié)構(gòu)保證了音頻鏈路每一段都是獨立可替換的后續(xù)加效果器或濾波器時不影響基礎(chǔ)通路。3. 寄存器配置編寫手寫 I2C 控制器驅(qū)動 WM8731 初始化3.1 WM8731 寄存器映射與默認值推導WM8731 內(nèi)部寄存器是 7 位地址加 9 位數(shù)據(jù)通過 I2C 總線訪問芯片的 I2C 地址是 0x1A7 位地址寫操作時發(fā)送 0x34。項目需要配置的寄存器不多核心是以下幾個寄存器 0 是左聲道輸入音量寄存器 2 是右聲道輸入音量寄存器 4 是模擬音頻通路控制寄存器 6 是數(shù)字音頻通路控制寄存器 8 是采樣率控制寄存器 12 是激活使能寄存器 15 是重置。localparam CFG_START 8d0; localparam CFG_RESET {7h0F, 9h000}; // 重置信道復位所有寄存器 localparam CFG_LINEIN {7h00, 9h017}; // 左聲道輸入音量 0dB開啟輸入 localparam CFG_LINEIN_R {7h02, 9h017}; // 右聲道輸入音量 0dB開啟輸入 localparam CFG_ANAPATH {7h04, 9h012}; // 模擬通路DAC 直通不經(jīng)過混音 localparam CFG_DEEMP {7h06, 9h000}; // 數(shù)字通路關(guān)掉去加重關(guān)掉軟靜音 localparam CFG_SRATE {7h08, 9h01E}; // 采樣率 48kHzMCLK18.432MHz localparam CFG_ACTIVE {7h0C, 9h001}; // 激活接口開始傳輸音頻CFG_RESET這條配置要在上電后第一時間發(fā)送把芯片恢復到已知狀態(tài)。0x0F 寄存器寫入 0x000 后所有寄存器回到默認值后續(xù)配置從干凈狀態(tài)開始。CFG_LINEIN和CFG_LINEIN_R把左右聲道輸入音量設(shè)在 0dB確保信號不被衰減。CFG_ANAPATH選擇了 DAC 到耳機輸出的直通路徑繞開模擬混音器——這個項目里我們用數(shù)字回環(huán)模擬混音器只會增加噪聲。CFG_SRATE的 0x01E 對應 48kHz 采樣率這里寫的是芯片手冊里的推薦配置值MCLK 由 FPGA 側(cè)提供 18.432MHz 時鐘。3.2 I2C 控制器狀態(tài)機實現(xiàn)與字節(jié)級時序I2C 控制器是本項目第一個硬骨頭。WM8731 的 I2C 時序要求起始條件 SCL 高電平時 SDA 下降沿停止條件 SCL 高電平時 SDA 上升沿數(shù)據(jù)在 SCL 高電平期間必須保持穩(wěn)定。FPGA 作為主機需要主動產(chǎn)生這些時序信號。我一般用一個三段式狀態(tài)機來寫把發(fā)送過程拆成 IDLE、START、SHIFT、ACK、STOP 等幾個狀態(tài)。module i2c_master ( input clk, // 50MHz 板載時鐘 input start, // 啟動一次傳輸 input [15:0] data, // 要發(fā)送的數(shù)據(jù)高 8 位是寄存器地址低 8 位是數(shù)據(jù) output reg done, // 傳輸完成標志 output reg scl, // I2C 時鐘線 inout sda // I2C 數(shù)據(jù)線 ); parameter CLK_DIV 50; // 50MHz 分頻到約 100kHz SCL符合 I2C 標準 reg [6:0] clk_cnt; reg [4:0] state; reg [4:0] bit_cnt; reg [15:0] tx_buf; reg sda_out; localparam IDLE 5d0, START 5d1, SEND 5d2, ACK 5d3, STOP 5d4; assign sda sda_out ? 1bz : 1b0; always (posedge clk) begin if (clk_cnt CLK_DIV) clk_cnt 0; else clk_cnt clk_cnt 1; end always (posedge clk) begin if (clk_cnt 0) begin case (state) IDLE: begin if (start) begin state START; tx_buf data; bit_cnt 16; scl 1b1; sda_out 1b1; end end START: begin sda_out 1b0; // SCL 高電平期間 SDA 拉低產(chǎn)生起始條件 scl 1b0; state SEND; end SEND: begin scl 1b1; sda_out tx_buf[15]; // 先發(fā)最高位 tx_buf {tx_buf[14:0], 1b0}; bit_cnt bit_cnt - 1; if (bit_cnt 1) state ACK; // 數(shù)據(jù)段發(fā)送完畢進入應答 else scl 1b0; end ACK: begin sda_out 1b1; // 釋放 SDA讀應答位 scl 1b0; state STOP; end STOP: begin scl 1b1; sda_out 1b1; // SCL 高電平期間 SDA 拉高產(chǎn)生停止條件 state IDLE; done 1b1; end endcase end end endmodule這個控制器的每個狀態(tài)都在 SCL 為低電平時切換 SDA在 SCL 為高電平時數(shù)據(jù)保持穩(wěn)定嚴格滿足 I2C 協(xié)議。CLK_DIV是個關(guān)鍵參數(shù)50MHz 主頻分頻到 100kHz SCL這個是標準模式的上限WM8731 完全兼容。如果后續(xù)想加快初始化速度可以把分頻系數(shù)改小但要注意芯片的 SCL 最高頻率限制。data端口接收 16 位數(shù)據(jù)WM8731 實際使用的是 7 位寄存器地址加 9 位數(shù)據(jù)多余的位數(shù)在發(fā)送時會是無效位但芯片會忽略。3.3 配置時序與上電初始化流程I2C 控制器寫好后還不能直接往 WM8731 灌配置。芯片上電后需要等待電源穩(wěn)定然后依次發(fā)送配置項。我用一個簡單的計數(shù)器來觸發(fā)逐條發(fā)送上電后延時 100ms拉高start信號第一條配置是 RESET等待done標志回落后發(fā)下一條。當十二條配置全部發(fā)送完成置位cfg_done這時 FPGA 才能開始往 I2S 數(shù)據(jù)線上送數(shù)據(jù)。實際調(diào)試中容易翻車的地方在 ACK 狀態(tài)。我習慣把 I2C 的sda管腳配置成inout在 ACK 狀態(tài)下把sda_out拉高釋放總線然后等 WM8731 把 SDA 拉低表示應答。如果芯片沒有正確上電或者地址寫錯SDA 會一直保持高電平這時done標志照樣會拉起但 WM8731 根本沒收到配置。排查這種問題的方法很簡單在仿真里看 ACK 位是不是低電平或者用一個 IO 拉高連接到邏輯分析儀上看波形。4. 數(shù)據(jù)通路實現(xiàn)I2S 收發(fā)器與頂層的持續(xù)回環(huán)4.1 I2S 時序協(xié)議位時鐘、幀同步與數(shù)據(jù)對齊WM8731 配置完成后音頻數(shù)據(jù)通過 I2S 總線傳輸。I2S 協(xié)議有三個關(guān)鍵信號BCLK 是位時鐘每個時鐘周期傳輸一位數(shù)據(jù)LRCK 是幀同步信號高電平表示右聲道低電平表示左聲道DAT 是串行數(shù)據(jù)線。WM8731 工作在從機模式時BCLK 和 LRCK 都可以由 FPGA 產(chǎn)生。但要注意本項目中 WM8731 是主機它自己會產(chǎn)生 BCLK 和 LRCKFPGA 只需要提供主時鐘 MCLK 并讀取數(shù)據(jù)。我用的是主機模式FPGA 自己產(chǎn)生 BCLK 和 LRCKWM8731 作為從機接收。這樣做的優(yōu)點是不用擔心 MCLK 分頻系數(shù)的計算錯誤只要保證 BCLK 是 48kHz 的整數(shù)倍即可。BCLK 頻率通常是 采樣率乘以位寬乘以通道數(shù)48kHz 乘 16 位乘 2 通道等于 1.536MHz。LRCK 的頻率就是采樣率本身每隔 32 個 BCLK 周期翻轉(zhuǎn)一次。4.2 I2S 收發(fā)模塊的 Verilog 實現(xiàn)module i2s_transceiver ( input clk, // 板載 50MHz 時鐘 input rst_n, // 異步復位 input mclk, // WM8731 主時鐘輸出18.432MHz input i2s_bclk, // 位時鐘由 FPGA 內(nèi)部產(chǎn)生 input i2s_lrck, // 左右聲道幀同步 output reg dac_dat, // FPGA 到 WM8731 DAC 的數(shù)據(jù) input adc_dat, // WM8731 ADC 到 FPGA 的數(shù)據(jù) output reg [15:0] left_out, // 左聲道數(shù)據(jù)輸出 output reg [15:0] right_out, // 右聲道數(shù)據(jù)輸出 input [15:0] left_in, // 左聲道數(shù)據(jù)輸入 input [15:0] right_in // 右聲道數(shù)據(jù)輸入 ); reg [4:0] bit_count; reg [15:0] shift_out; reg [15:0] shift_in; always (posedge i2s_bclk or negedge rst_n) begin if (!rst_n) begin bit_count 0; shift_out 0; shift_in 0; end else begin if (i2s_lrck 1b0) begin // 左聲道采樣周期 if (bit_count 0) begin left_out {shift_in[14:0], adc_dat}; // 串行轉(zhuǎn)并行累積 shift_out left_in; shift_in 0; end bit_count bit_count 1; shift_out {shift_out[14:0], 1b0}; dac_dat shift_out[15]; shift_in {shift_in[14:0], adc_dat}; end else begin // 右聲道采樣周期 // 邏輯與左聲道一致將 right_in / right_out 對應替換 bit_count bit_count 1; end end end endmodule這段代碼的核心思路是在 LRCK 為低電平時每個 BCLK 上升沿從adc_dat采一位寫入shift_in16 個位時鐘后得到一個完整的左聲道樣本發(fā)送側(cè)則把left_in載入shift_out每個 BCLK 上升沿從最高位逐位移出到dac_dat。LRCK 翻轉(zhuǎn)后進入右聲道周期邏輯相同。數(shù)據(jù)位寬是 16 位這也是 WM8731 的默認字長。bit_count計數(shù)到 16 時自動歸零等待下一個聲道周期開始。需要留意的是I2S 數(shù)據(jù)是 MSB 先行且數(shù)據(jù)比 LRCK 翻轉(zhuǎn)晚一個位時鐘。這意味著發(fā)送側(cè)在 LRCK 變低后的第一個 BCLK 上升沿不能立即發(fā)送數(shù)據(jù)的最高位而是要延時一拍。我在模塊里用shift_out預先載入數(shù)據(jù)在 LRCK 有效后的第二個 BCLK 沿開始輸出就是為了滿足這個時序要求。如果這里不處理左右聲道的數(shù)據(jù)會整體錯位一個位時鐘聽感上出現(xiàn)明顯的卡頓。4.3 頂層模塊把 I2C 與 I2S 拼接成完整通路頂層模塊的任務(wù)是實例化前面兩個子模塊把它們連接成一條可用的通路。I2C 控制器用 50MHz 時鐘驅(qū)動I2S 收發(fā)器用 BCLK 和 LRCK 驅(qū)動。這里有個容易踩坑的地方——不同時鐘域的信號不能直接混用。I2C 的done標志是 50MHz 時鐘域的信號I2S 的left_out數(shù)據(jù)是 BCLK 時鐘域的信號如果直接互相賦值綜合工具會報跨時鐘域警告而且要擔心亞穩(wěn)態(tài)問題。module audio_top ( input clk_50m, // 板載 50MHz 時鐘 input rst_n, // 復位按鈕 inout AUD_I2C_SDAT, // WM8731 I2C 數(shù)據(jù)線 output AUD_I2C_SCLK, // WM8731 I2C 時鐘線 output AUD_MCLK, // WM8731 主時鐘 input AUD_ADC_DAT, // ADC 數(shù)據(jù)輸入 output AUD_DAC_DAT, // DAC 數(shù)據(jù)輸出 output AUD_BCLK, // 位時鐘 output AUD_LRCK // 幀同步 ); wire cfg_done; wire [15:0] adc_left, adc_right; wire [15:0] dac_left, dac_right; // 相位累加器產(chǎn)生 MCLK18.432MHz用 PLL 或計數(shù)器分頻產(chǎn)生 assign AUD_LRCK bclk_div ? ... // 從 BCLK 計數(shù)器產(chǎn)生 48kHz 幀同步 i2c_master u_i2c ( .clk(clk_50m), .start(start_config), .data(config_data), .done(config_done), .scl(AUD_I2C_SCLK), .sda(AUD_I2C_SDAT) ); i2s_transceiver u_i2s ( .clk(clk_50m), .rst_n(rst_n), .i2s_bclk(AUD_BCLK), .i2s_lrck(AUD_LRCK), .adc_dat(AUD_ADC_DAT), .dac_dat(AUD_DAC_DAT), .left_out(adc_left), .right_out(adc_right), .left_in(dac_left), .right_in(dac_right) ); assign dac_left adc_left; // 直通回環(huán) assign dac_right adc_right; endmodule頂層模塊里最容易被忽視的是 MCLK 的產(chǎn)生。WM8731 需要一個主時鐘輸入頻率必須是采樣率的整數(shù)倍常見的有 12.288MHz 和 18.432MHz。DE2 板上有 50MHz 晶振直接分頻得不到這兩個頻率所以項目里用 FPGA 內(nèi)置 PLL 來生成。如果你用的開發(fā)板沒有音頻專用晶振這一步是跑不通的。一個替代方案是把 WM8731 工作在自動時鐘檢測模式通過芯片內(nèi)部 PLL 從 BCLK 恢復主時鐘但這樣配置寄存器和 MCLK 引腳連接都要改不推薦第一次做就選這條路。5. 常見問題排查音頻回環(huán)不出聲的五個典型原因5.1 現(xiàn)象是靜音原因是 I2C 配置根本沒送進去上板之后最典型的失敗現(xiàn)象是無論怎么調(diào)整音量旋鈕耳機里一點聲音都沒有。先不急著懷疑代碼先看 I2C 波形。把邏輯分析儀接到AUD_I2C_SCLK和AUD_I2C_SDAT上抓上電瞬間的信號。如果 SCL 沒有時鐘或者 SDA 一直為高說明 I2C 控制器沒有工作。這時檢查復位信號是否釋放、狀態(tài)機是否卡在 IDLE、start脈沖是否正確產(chǎn)生。有時候 I2C 波形正常但 WM8731 還是不工作這時候就要檢查 ACK 位。芯片正確應答時會在第 9 個時鐘周期把 SDA 拉低如果 SDA 一直高說明芯片根本沒收到完整數(shù)據(jù)。常見原因是 I2C 地址錯誤——WM8731 的 CSB 引腳接地時地址是 0x1A接高電平是 0x1B如果板子上的地址引腳被拉高而你用了 0x1A芯片自然不會回應。這個坑我見過多次。解決用萬用表量一下 CSB 引腳電平或者干脆在代碼里把兩個地址都試一遍。5.2 現(xiàn)象是刺耳噪聲原因是左右聲道數(shù)據(jù)錯位另一種常見故障是音頻能出聲但聲音像機器人說話一樣機械且斷續(xù)或者左右聲道明顯錯位。這種問題基本出在 I2S 時序?qū)R上。I2S 協(xié)議規(guī)定數(shù)據(jù)比 LRCK 翻轉(zhuǎn)晚一個 BCLK如果發(fā)送模塊在 LRCK 翻轉(zhuǎn)的同一個沿就送出第一位數(shù)據(jù)接收端會把前一個聲道的最后一位當成新聲道的起始位整個數(shù)據(jù)流錯位一位。解決在發(fā)送模塊里增加一拍延時確保第一個 bit 出現(xiàn)在 LRCK 翻轉(zhuǎn)后的第二個 BCLK 沿。還有一種錯位是左右聲道數(shù)據(jù)互換聽起來像聲場翻轉(zhuǎn)但聲音本身是連續(xù)的。這是 LRCK 極性接反了。WM8731 的 LRCK 高電平對應右聲道如果你在代碼里把高電平當成了左聲道左右就換過來了。檢查一下接收模塊里處理 LRCK 電平的分支邏輯交換兩個分支即可。5.3 現(xiàn)象是電流底噪原因是模擬地與數(shù)字地處理不當如果聲音能出來但伴隨持續(xù)的白噪聲或 50Hz 交流聲且音量隨輸入增益增大而增大那問題大概率在模擬前端。WM8731 是混合信號芯片模擬地和數(shù)字地雖然是同一個封裝但 PCB 上通常分開鋪設(shè)。DE2 板上已經(jīng)做了隔離我們能做的有限但有一點必須注意耳機插頭地線和 LINE IN 地線如果接觸不良噪聲會異常明顯。這種情況在開發(fā)板上沒有太好的軟件優(yōu)化手段可以試著降低輸入增益減少模擬前端的噪聲放大。如果是 FPGA 側(cè)帶來的噪聲檢查 MCLK 的質(zhì)量。MCLK 抖動過大會直接惡化 ADC 和 DAC 的時鐘同步精度導致輸出波形毛刺增多。用示波器看 MCLK 引腳如果上升沿不陡峭或存在明顯抖動檢查 PLL 配置是否正確。我在項目里把 PLL 的輸出時鐘定為 18.432MHz并且加了一級 BUFG 全局時鐘緩沖確保到 WM8731 的時鐘路徑足夠干凈。5.4 現(xiàn)象是上電后必須按復位才發(fā)聲原因是初始化時序沒做完有些開發(fā)者會遇到一種奇怪現(xiàn)象每次上電后需要手動按一下復位按鈕音頻通路才正常。這說明 WM8731 的初始化流程在時序上落后于上電I2C 配置是在復位釋放之前就開始發(fā)送或者復位釋放后 FPGA 立刻去讀 ADC 數(shù)據(jù)而芯片還沒來得及完成內(nèi)部初始化。解決在復位釋放后加一個計數(shù)器延時至少等待 10ms 讓 WM8731 的模擬電路穩(wěn)定再啟動 I2C 配置流程。這個延時用 50MHz 時鐘計數(shù)很容易實現(xiàn)500000 個時鐘周期就是 10ms。5.5 現(xiàn)象是采樣率聽起來不對原因是幀同步頻率偏了最后一種現(xiàn)象是音頻能播放但音調(diào)明顯偏高或偏低。這說明AUD_LRCK的頻率不是 48kHz。LRCK 是 BCLK 分頻產(chǎn)生的BCLK 如果由 PLL 產(chǎn)生要檢查 PLL 分頻參數(shù)是否算錯。比如 18.432MHz 的 MCLK 如果被誤當成 50MHz 來分頻得到的 LRCK 頻率會差近三倍。這種情況下音樂速率變得離譜基本可以斷定是分頻參數(shù)的問題。用頻率計或示波器測一下 LRCK 的實際頻率把它調(diào)回 48kHz 附近即可。6. 二次開發(fā)給音頻通路加一個實時音量調(diào)節(jié)和數(shù)字增益6.1 在數(shù)據(jù)通路上插入乘法器實現(xiàn)音量控制基礎(chǔ)回環(huán)跑通之后最能體現(xiàn) FPGA 優(yōu)勢的改造是加數(shù)字增益控制。既然 ADC 輸出的數(shù)據(jù)已經(jīng)以 16 位并行形式到達 FPGA完全可以在直通鏈路上插入一個乘法器用撥碼開關(guān)控制增益系數(shù)實現(xiàn)數(shù)字音量調(diào)節(jié)。設(shè)計思路如下把左右聲道的left_in和right_in分別乘以一個可配置的增益系數(shù)乘法結(jié)果截位后送入 DAC。增益系數(shù)用一個 16 位寄存器保存每撥動一次撥碼開關(guān)寄存器加或減一個固定步進。這個改造有個典型陷阱16 位乘以 16 位得到 32 位結(jié)果如果直接把低 16 位送給 DAC聲音會嚴重失真甚至爆音。正確做法是保留高 16 位或者做帶符號飽和處理。我一般這樣處理乘法結(jié)果右移 8 位再判斷是否溢出若溢出則鉗位到最大正值或最小負值。這樣音量調(diào)節(jié)的步進是 1/256足夠細膩同時避免了截位帶來的削波噪聲。6.2 順著通路加一個簡單的低通濾波驗證可擴展性音量控制之外另一條值得動手的路徑是插一個數(shù)字濾波器。在i2s_transceiver的數(shù)據(jù)輸出端串聯(lián)一個一階 IIR 低通濾波器系數(shù)固定截止頻率設(shè)在 8kHz 左右。這個改動只涉及組合邏輯和一個狀態(tài)寄存器不需要改 I2C 配置和 I2S 時序。濾波器結(jié)構(gòu)是標準的差分方程y[n] y[n-1] alpha * (x[n] - y[n-1])alpha 取 0.2 左右。乘法和加法都放在 BCLK 域內(nèi)注意控制時序避免產(chǎn)生組合邏輯環(huán)路。這樣做的好處是驗證 FPGA 處理信號的能力已經(jīng)從「搬運數(shù)據(jù)」進化到「變換數(shù)據(jù)」而這正是后續(xù)做均衡器、回聲、降噪算法的第一步。把 alpha 系數(shù)也做成可配置就能在運行時切換濾波效果。再往后如果對時延不敏感還可以把數(shù)據(jù)緩存到片內(nèi) RAM 做延遲效果配合撥碼開關(guān)做簡單的回聲。6.3 從直通到算法的改造驗證方法每次改動后最直接的驗證方法是播放一段自己熟悉的音樂——最好是對人聲和低頻都有清晰辨識度的曲目然后切換增益系數(shù)聽音量變化是否線性切換濾波器參數(shù)聽高頻是否明顯衰減。用示波器量 DAC 輸出腳的波形也能看出濾波器的痕跡原始方波信號經(jīng)過低通濾波后邊緣會變得圓滑。我從這個項目里學到的最重要一課是FPGA 工程調(diào)試永遠先從鏈路末端往前查。音頻聲卡無聲先摸 I2CI2C 通了查幀同步幀同步對了我才去動信號處理邏輯。順序一亂就會陷入同時懷疑所有模塊的狀態(tài)誰也查不出來。從那以后我每加一個模塊都會在它入口打一個debug標志先確認真數(shù)據(jù)進來了再看它出去的是什么。這個習慣讓我少走很多彎路希望也能幫你在調(diào) FPGA 音頻的時候少踩幾個坑。本文還有配套的精品資源點擊獲取