第15课: 音效

LFSR噪声+包络+频率扫描,3种游戏音效

🏆 爆炸+跳跃+得分音效 ✅ Verilator仿真验证通过

📖 核心概念

💡 关键思路:本课的核心是LFSR伪随机噪声——16位线性反馈移位寄存器生成爆炸噪声。

💻 Verilog设计代码

设计模块源码——这是你真正要理解的硬件逻辑:

// 第15课: 音效 - 爆炸+跳跃+得分音效 module sound_effects ( input wire clk, input wire rst_n, input wire trigger, input wire [1:0] sfx_select, output reg [7:0] pcm_out, output reg playing ); reg [15:0] lfsr; wire noise_bit = lfsr[0] ^ lfsr[2] ^ lfsr[3] ^ lfsr[4]; reg [31:0] phase, phase_step; reg [15:0] env_counter; reg [7:0] env_level; reg [15:0] duration, tick; always @(posedge clk or negedge rst_n) begin if (!rst_n) lfsr <= 16'hACE1; else if (playing) lfsr <= {lfsr[14:0], noise_bit}; end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin pcm_out <= 128; playing <= 0; phase <= 0; phase_step <= 0; env_counter <= 0; env_level <= 0; duration <= 0; tick <= 0; end else begin if (!playing && trigger) begin playing <= 1; phase <= 0; tick <= 0; env_level <= 255; env_counter <= 0; case (sfx_select) 0: begin phase_step <= 32'h00001000; duration <= 500; end 1: begin phase_step <= 32'h00004000; duration <= 150; end 2: begin phase_step <= 32'h00020000; duration <= 200; end default: begin phase_step <= 32'h00008000; duration <= 200; end endcase end if (playing) begin tick <= tick + 1; phase <= phase + phase_step; env_counter <= env_counter + 1; if (env_counter == 0) env_level <= (env_level > 2) ? env_level - 2 : 0; case (sfx_select) 0: pcm_out <= (noise_bit ? 128 + env_level/2 : 128 - env_level/2); 1: begin phase_step <= phase_step + 32'h00000100; pcm_out <= (phase[31] ? 128 + env_level/2 : 128 - env_level/2); end 2: begin if (tick[4] == 0) pcm_out <= (phase[31] ? 128 + env_level/2 : 128 - env_level/2); else pcm_out <= (phase[30] ? 128 + env_level/2 : 128 - env_level/2); end default: pcm_out <= 128; endcase if (tick >= duration || env_level == 0) begin playing <= 0; pcm_out <= 128; end end else pcm_out <= 128; end end endmodule

🧪 测试平台(Testbench)

testbench = 你的"手柄+屏幕",模拟输入、验证输出:

/* verilator lint_off WIDTHEXPAND */ /* verilator lint_off WIDTHTRUNC */ /* verilator lint_off UNOPTFLAT */ module tb; reg clk, rst_n, trigger; reg [1:0] sfx_select; wire [7:0] pcm_out; wire playing; sound_effects uut (.clk(clk),.rst_n(rst_n),.trigger(trigger),.sfx_select(sfx_select),.pcm_out(pcm_out),.playing(playing)); always clk = #10 ~clk; reg [7:0] samples [0:63]; integer i, non_silence; initial begin $dumpfile("soundfx.vcd"); $dumpvars(0, tb); clk = 0; rst_n = 0; trigger = 0; sfx_select = 0; repeat(5) @(posedge clk); rst_n = 1; $display("=== 音效仿真 ==="); $display("爆炸+跳跃+得分音效"); $display(""); $display("--- 测试1: 爆炸音效 ---"); sfx_select = 0; trigger = 1; @(posedge clk); trigger = 0; non_silence = 0; for (i = 0; i < 64; i = i + 1) begin @(posedge clk); samples[i] = pcm_out; if (pcm_out != 128) non_silence = non_silence + 1; end $display(" 前64采样中非静音: %0d/64", non_silence); $display(" 前8采样: %0d %0d %0d %0d %0d %0d %0d %0d", samples[0],samples[1],samples[2],samples[3],samples[4],samples[5],samples[6],samples[7]); if (non_silence > 10) $display(" ✅ 爆炸音效有声音输出"); while (playing) @(posedge clk); repeat(10) @(posedge clk); $display(""); $display("--- 测试2: 跳跃音效 ---"); sfx_select = 1; trigger = 1; @(posedge clk); trigger = 0; non_silence = 0; for (i = 0; i < 64; i = i + 1) begin @(posedge clk); samples[i] = pcm_out; if (pcm_out != 128) non_silence = non_silence + 1; end $display(" 前64采样中非静音: %0d/64", non_silence); if (non_silence > 5) $display(" ✅ 跳跃音效有声音输出"); while (playing) @(posedge clk); repeat(10) @(posedge clk); $display(""); $display("--- 测试3: 得分音效 ---"); sfx_select = 2; trigger = 1; @(posedge clk); trigger = 0; non_silence = 0; for (i = 0; i < 64; i = i + 1) begin @(posedge clk); samples[i] = pcm_out; if (pcm_out != 128) non_silence = non_silence + 1; end $display(" 前64采样中非静音: %0d/64", non_silence); if (non_silence > 5) $display(" ✅ 得分音效有声音输出"); while (playing) @(posedge clk); repeat(10) @(posedge clk); $display(""); $display("--- 测试4: 静音状态 ---"); trigger = 0; repeat(20) @(posedge clk); if (pcm_out == 128 && !playing) $display(" ✅ 未触发时输出静音(128)"); $display(""); $display("✅ 爆炸+跳跃+得分音效验证通过!"); $display("🏆 成就解锁: 爆炸+跳跃+得分音效!"); $finish; end endmodule

✅ 仿真输出

运行 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace --build -j 4 -o sim 后的输出:

=== 音效仿真 === 爆炸+跳跃+得分音效 --- 测试1: 爆炸音效 --- 前64采样中非静音: 64/64 前8采样: 255 254 2 2 254 254 2 254 ✅ 爆炸音效有声音输出 --- 测试2: 跳跃音效 --- 前64采样中非静音: 64/64 ✅ 跳跃音效有声音输出 --- 测试3: 得分音效 --- 前64采样中非静音: 64/64 ✅ 得分音效有声音输出 --- 测试4: 静音状态 --- ✅ 未触发时输出静音(128) ✅ 爆炸+跳跃+得分音效验证通过! 🏆 成就解锁: 爆炸+跳跃+得分音效! - tb.sv:41: Verilog $finish

🔧 编译和运行

# 编译 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace \ --build -j 4 -o sim \ -Wno-WIDTHEXPAND -Wno-WIDTHTRUNC -Wno-UNOPTFLAT \ -Wno-TIMESCALEMOD -Wno-STMTDLY -Wno-WIDTH \ -Wno-UNSIGNED -Wno-SELRANGE -Wno-BLKSEQ # 运行 ./obj_dir/sim # 查看波形(可选) gtkwave sim.vcd

🎮 实战步骤

1
理解LFSR:16位LFSR通过XOR抽头产生伪随机序列。关键代码:noise_bit = lfsr[0] ^ lfsr[2] ^ lfsr[3] ^ lfsr[4]。这个多项式产生最大长度序列(M序列),周期为2^16-1=65535。
2
包络衰减:每256个时钟周期,env_level减2。爆炸音效duration=500,约持续500×1=500个采样周期。跳跃音效duration=150,更短促。
3
频率扫描:跳跃音效中phase_step <= phase_step + 32'h00000100,每个采样周期phase_step增加256,导致输出频率持续上升,模拟"弹跳"效果。
4
得分双音:得分音效利用tick[4]切换两种频率比例,产生"叮咚"双音效果。这是8bit游戏经典的得分音设计。

🎵 经典游戏音效分析

超级马里奥:跳跃=快速上行扫频(200→800Hz, 100ms),金币=双音交替(988→1319Hz),1UP=快速上行三连音

太空侵略者:爆炸=白噪声+低频调制,外星人移动=四音递降序列,UFO=连续低频嗡嗡声

吃豆人:吃豆=快速"哇哇"声(频率锯齿波),能量豆=低频渐升,死亡=下行扫频+噪声

🏆
爆炸+跳跃+得分音效
✅ Verilator仿真验证通过

🧠 知识扩展

LFSR与噪声:线性反馈移位寄存器(LFSR)是FPGA中最常用的伪随机数生成器。16位LFSR的周期为2^16-1=65535,足够用于游戏音效。通过选择不同的反馈抽头(XOR门位置),可以改变噪声的频谱特性。

包络与音色:音色的本质是频谱+包络。爆炸音效需要快速起音+慢衰减,跳跃需要快速起音+快衰减。通过调整env_level递减速率,可以模拟不同材质的声音特性。

⚡ 性能提示

• 使用--trace选项生成VCD波形文件,用GTKWave查看

• 使用-j 4选项并行编译,加快构建速度

• 使用--build选项让Verilator自动调用make

• 大量$display输出会拖慢仿真速度,验证通过后可以减少打印频率