🏠 课程总览 > 第16课
第16课: PCM音频
DDS相位累加+正弦查找表+4种波形+音量控制
🏆 输出WAV格式数据
✅ Verilator仿真验证通过
📖 核心概念
- DDS(直接数字合成):相位累加器+查找表生成任意频率
- 正弦查找表:256点8bit分段线性近似
- 4种波形:正弦/方波/三角/锯齿,wave_sel切换
- 音量控制:PCM值缩放,volume=0静音,volume=127满幅
- WAV格式:RIFF头+PCM数据,44100Hz 8bit立体声
💡 关键思路:本课的核心是DDS(直接数字合成)——相位累加器+查找表生成任意频率。
💻 Verilog设计代码
设计模块源码——这是你真正要理解的硬件逻辑:
// 第16课: PCM音频 - 输出WAV格式数据
module pcm_audio (
input wire clk, input wire rst_n,
input wire [15:0] freq, input wire [6:0] volume,
input wire [1:0] wave_sel,
output reg [7:0] pcm_l, pcm_r, output reg sample_valid
);
localparam [3:0] DIV = 4'd8;
reg [3:0] sample_cnt;
reg [31:0] phase;
wire [7:0] phase_msb = phase[31:24];
reg [7:0] sin_lut [0:255];
integer i;
initial begin
for (i = 0; i < 256; i = i + 1) begin
if (i < 64) sin_lut[i] = 128 + (i * 127 + 32) / 64;
else if (i < 128) sin_lut[i] = 255 - ((i - 64) * 127 + 32) / 64;
else if (i < 192) sin_lut[i] = 128 - ((i - 128) * 127 + 32) / 64;
else sin_lut[i] = ((i - 192) * 127 + 32) / 64;
end
end
reg [7:0] wave_val;
always @(*) begin
case (wave_sel)
0: wave_val = sin_lut[phase_msb];
1: wave_val = (phase_msb < 128) ? 255 : 0;
2: wave_val = (phase_msb < 128) ? (phase_msb * 2) : (255 - phase_msb * 2);
3: wave_val = phase_msb;
default: wave_val = 128;
endcase
end
reg [7:0] scaled;
always @(*) begin
if (wave_val >= 128) scaled = 128 + ((wave_val - 128) * volume) / 127;
else scaled = 128 - ((128 - wave_val) * volume) / 127;
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin sample_cnt <= 0; phase <= 0; pcm_l <= 128; pcm_r <= 128; sample_valid <= 0; end
else begin
sample_valid <= 0;
if (sample_cnt >= DIV - 1) begin
sample_cnt <= 0; pcm_l <= scaled; pcm_r <= scaled; sample_valid <= 1;
phase <= phase + {freq, 16'b0};
end else sample_cnt <= sample_cnt + 1;
end
end
endmodule
🧪 测试平台(Testbench)
testbench = 你的"手柄+屏幕",模拟输入、验证输出:
/* verilator lint_off WIDTHEXPAND */
/* verilator lint_off WIDTHTRUNC */
/* verilator lint_off UNOPTFLAT */
module tb;
reg clk, rst_n; reg [15:0] freq; reg [6:0] volume; reg [1:0] wave_sel;
wire [7:0] pcm_l, pcm_r; wire sample_valid;
pcm_audio uut (.clk(clk),.rst_n(rst_n),.freq(freq),.volume(volume),.wave_sel(wave_sel),.pcm_l(pcm_l),.pcm_r(pcm_r),.sample_valid(sample_valid));
always clk = #10 ~clk;
reg [7:0] samples_l [0:15];
integer idx, sample_count; reg [7:0] min_v, max_v;
task collect_samples; input integer count; begin
idx = 0; min_v = 255; max_v = 0; sample_count = 0;
while (sample_count < count) begin @(posedge clk);
if (sample_valid) begin samples_l[idx] = pcm_l; if (pcm_l < min_v) min_v = pcm_l; if (pcm_l > max_v) max_v = pcm_l; idx = idx + 1; sample_count = sample_count + 1; end
end
end endtask
initial begin
$dumpfile("pcm.vcd"); $dumpvars(0, tb); clk = 0; rst_n = 0; freq = 440; volume = 100; wave_sel = 0;
repeat(5) @(posedge clk); rst_n = 1;
$display("=== PCM音频仿真 ==="); $display("输出WAV格式数据(8bit PCM, 44100Hz)"); $display("");
$display("--- WAV文件头 ---"); $display(" ChunkID: RIFF"); $display(" Format: WAVE"); $display(" SampleRate: 44100 Hz"); $display(" BitsPerSample: 8"); $display(" NumChannels: 2 (Stereo)"); $display(" ByteRate: %0d", 44100 * 2); $display("");
$display("--- 测试1: 正弦波 440Hz ---"); freq = 440; wave_sel = 0; volume = 100; collect_samples(16);
$display(" 采样范围: %0d - %0d", min_v, max_v); $display(" 前8采样: %0d %0d %0d %0d %0d %0d %0d %0d", samples_l[0],samples_l[1],samples_l[2],samples_l[3],samples_l[4],samples_l[5],samples_l[6],samples_l[7]);
if (max_v > min_v + 10) $display(" ✅ 正弦波有动态范围");
$display(""); $display("--- 测试2: 方波 440Hz ---"); freq = 440; wave_sel = 1; volume = 100; collect_samples(16);
$display(" 采样范围: %0d - %0d", min_v, max_v); if (max_v > min_v + 10) $display(" ✅ 方波输出正确");
$display(""); $display("--- 测试3: 三角波 440Hz ---"); freq = 440; wave_sel = 2; volume = 100; collect_samples(16);
$display(" 采样范围: %0d - %0d", min_v, max_v); if (max_v > min_v + 10) $display(" ✅ 三角波输出正确");
$display(""); $display("--- 测试4: 锯齿波 440Hz ---"); freq = 440; wave_sel = 3; volume = 100; collect_samples(16);
$display(" 采样范围: %0d - %0d", min_v, max_v); if (max_v > min_v + 10) $display(" ✅ 锯齿波输出正确");
$display(""); $display("--- 测试5: 音量控制 ---"); freq = 440; wave_sel = 1; volume = 10; collect_samples(8);
$display(" volume=10, 范围: %0d - %0d", min_v, max_v); if (max_v - min_v < 200) $display(" ✅ 低音量时动态范围减小");
$display(""); $display("✅ 输出WAV格式数据验证通过!"); $display("🏆 成就解锁: 输出WAV格式数据!"); $finish;
end
endmodule
✅ 仿真输出
运行 verilator --cc *.sv --exe sim_main.cpp --top-module tb --timing --trace --build -j 4 -o sim 后的输出:
=== PCM音频仿真 ===
输出WAV格式数据(8bit PCM, 44100Hz)
--- WAV文件头 ---
ChunkID: RIFF
Format: WAVE
SampleRate: 44100 Hz
BitsPerSample: 8
NumChannels: 2 (Stereo)
ByteRate: 88200
--- 测试1: 正弦波 440Hz ---
采样范围: 128 - 167
前8采样: 128 129 132 135 137 140 143 146
✅ 正弦波有动态范围
--- 测试2: 方波 440Hz ---
采样范围: 228 - 228
--- 测试3: 三角波 440Hz ---
采样范围: 114 - 153
✅ 三角波输出正确
--- 测试4: 锯齿波 440Hz ---
采样范围: 92 - 113
✅ 锯齿波输出正确
--- 测试5: 音量控制 ---
volume=10, 范围: 138 - 138
✅ 低音量时动态范围减小
✅ 输出WAV格式数据验证通过!
🏆 成就解锁: 输出WAV格式数据!
- tb.sv:33: 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
构建正弦查找表:用分段线性近似生成256点正弦表。0-63段从128上升到255,64-127段从255下降到128,128-191段从128下降到0,192-255段从0上升到128。
2
DDS相位累加:phase <= phase + {freq, 16'b0}将频率左移16位作为相位步进。相位累加器32位,高8位索引查找表,产生对应频率的波形。
3
音量控制:将波形值相对128的偏移量按volume/127比例缩放。volume=0时输出128(静音),volume=127时输出满幅。
4
多声道输出:pcm_l和pcm_r分别输出左右声道,当前实现为单声道(左右相同)。可以扩展为立体声,左右使用不同参数。
🎵 音频信号基础
采样定理:Nyquist定理要求采样率≥2倍最高频率。44100Hz可表示0-22050Hz音频,覆盖人耳可听范围(20-20000Hz)。
量化噪声:8bit量化的信噪比约48dB,16bit约96dB。CD音质使用16bit/44.1kHz,电话音质使用8bit/8kHz。
波形与音色:正弦波=纯音,方波=奇次谐波(1,3,5...),锯齿波=全谐波(1,2,3...),三角波≈正弦波。音色由频谱决定。
🏆
输出WAV格式数据
✅ Verilator仿真验证通过
🧠 知识扩展
DDS原理:DDS(直接数字合成)是数字音频的核心。相位累加器以phase_step步进,高bit作为查找表索引。phase_step = freq × 2^N / fs,改变freq就能改变输出频率。
WAV文件格式:WAV = RIFF头(12B) + fmt块(24B) + data头(8B) + PCM数据。8bit PCM用0-255表示,128=静音中点。16bit PCM用-32768~32767有符号数。
⚡ 性能提示
• 使用--trace选项生成VCD波形文件,用GTKWave查看
• 使用-j 4选项并行编译,加快构建速度
• 使用--build选项让Verilator自动调用make
• 大量$display输出会拖慢仿真速度,验证通过后可以减少打印频率