PWM脉冲宽度调制 + 音频频率→音符
🏆 成就:音律匠人 ✅ Verilator验证
用方波频率控制音高,占空比控制音量。50%占空比=最大音量。A4=440Hz → 50MHz÷440÷2=56818个计数。
reg [16:0] half_period; // 音符对应的半周期计数值
always @(*) case(note)
0: half_period=0; // 静音
1: half_period=95556; // C4
2: half_period=85132; // D4
3: half_period=75843; // E4
5: half_period=63776; // G4
6: half_period=56818; // A4
8: half_period=47781; // C5
endcase
reg [16:0] freq_counter;
always @(posedge clk)
if(half_period==0) freq_counter<=0;
else if(freq_counter>=half_period-1) freq_counter<=0;
else freq_counter<=freq_counter+1;
assign pwm_out=(half_period>0)&&(freq_counter| 音符 | 频率 | 周期 | 50MHz计数 |
|---|---|---|---|
| C4 | 262Hz | 3822μs | 95556 |
| D4 | 294Hz | 3405μs | 85132 |
| E4 | 330Hz | 3034μs | 75843 |
| F4 | 349Hz | 2863μs | 71587 |
| G4 | 392Hz | 2551μs | 63776 |
| A4 | 440Hz | 2273μs | 56818 |
| B4 | 494Hz | 2025μs | 50619 |
| C5 | 523Hz | 1911μs | 47781 |
module pwm_sound_tb;
logic clk,rst_n; logic [3:0] note; logic pwm_out;
pwm_sound uut(.*);
initial clk=0; always #10 clk=~clk;
int total_count, high_count;
real measured_freq;
always @(posedge clk) begin
if(note>0) begin
total_count++;
if(pwm_out) high_count++;
end
end
initial begin
rst_n=0;#100;rst_n=1;
$display("========== PWM音符验证 ==========");
for(int n=1;n<=8;n++) begin
note=n; total_count=0; high_count=0;
#500_000;
measured_freq = 50_000_000.0/total_count;
$display(" 音符%0d: 频率=%.1fHz, 占空比=%.1f%%",
n, measured_freq, 100.0*high_count/total_count);
end
note=0; #10000;
$display(" 静音: pwm_out=%b (应为0)",pwm_out);
$display("==========================================");
$finish;
end
initial #20_000_000 $finish;
endmodulereg [15:0] sigma_delta_acc;
always @(posedge clk)
sigma_delta_acc <= sigma_delta_acc[14:0] + {8'd0, volume};
assign dac_out = sigma_delta_acc[15];
// volume越大,1的密度越高,滤波后电压越高💡 PWM频率选择
• PWM载波频率应远高于音频范围(20Hz~20kHz)
• 典型PWM频率:100kHz~1MHz
• 本课简化为直接用音频频率做方波,效果OK
• 真正的PWM音量控制需要更高的载波频率
verilator --binary -j 0 --trace pwm_sound.v pwm_sound_tb.sv ./obj_dir/Vpwm_sound_tb # 预期输出: # ========== PWM音符验证 ========== # 音符1: 频率=262.5Hz, 占空比=50.0% # 音符2: 频率=294.3Hz, 占空比=50.0% # 音符6: 频率=440.1Hz, 占空比=50.0% # ... # 静音: pwm_out=0 (应为0) # ========================================== # 有FPGA板时:连接喇叭到PWM输出引脚即可听到音调!
✅ PWM如何用数字信号产生声音?
✅ 占空比和音量的关系?
✅ 如何计算50MHz时钟下440Hz的计数器值?
✅ 为什么静音时计数器要清零?
✅ PWM和Delta-Sigma DAC的区别?
✅ PWM载波频率为什么需要远高于音频?
下一课把单个音符变成音乐!乐曲ROM存储音符序列,节拍定时器控制时值,播放《小星星》!
Q: 听不到声音? 检查half_period是否为0(静音状态)。确认note输入不为0。
Q: 频率不准? 计数器值需要取整数,会有微小偏差。440Hz用56818→实际439.96Hz,完全OK。
Q: 声音太刺耳? 方波含大量谐波,可以加低通滤波器(硬件RC电路)柔化音色。
| 参数 | 范围 | 说明 |
|---|---|---|
| 系统时钟 | 50MHz | 常见FPGA时钟 |
| 音频范围 | 20Hz~20kHz | 人耳可听 |
| 标准音A4 | 440Hz | 国际标准音高 |
| 中央C | 262Hz | 钢琴中间C |
| 最高音C8 | 4186Hz | 钢琴最高音 |
| 占空比 | 1%~99% | 50%=最大音量 |
| PWM载波频率 | 100kHz~1MHz | 远高于音频 |
💡 方波与音色:不同波形产生不同音色。方波=丰富谐波=复古游戏音效,正弦波=纯净=柔和,锯齿波=明亮=弦乐,三角波=温暖=8-bit贝斯。PWM方波就是我们需要的复古游戏音效!
| 音符 | 频率 | 50MHz计数 |
|---|---|---|
| C4(中央C) | 262Hz | 95556 |
| D4 | 294Hz | 85132 |
| E4 | 330Hz | 75843 |
| F4 | 349Hz | 71587 |
| G4 | 392Hz | 63776 |
| A4 | 440Hz | 56818 |
| B4 | 494Hz | 50619 |
| C5 | 523Hz | 47781 |
reg [15:0] sigma_delta_acc;
always @(posedge clk)
sigma_delta_acc <= sigma_delta_acc[14:0] + {8'd0, volume};
assign dac_out = sigma_delta_acc[15]; // MSB输出
// volume越大,1的密度越高,滤波后电压越高• 修改half_period听不同音高
• 添加包络:音符开始音量大,逐渐减小
• 尝试不同占空比听音色变化
module pwm_sound_tb;
logic clk,rst_n; logic [3:0] note; logic pwm_out;
pwm_sound uut(.*);
initial clk=0; always #10 clk=~clk;
int total_count, high_count;
always @(posedge clk) if(note>0) begin
total_count++;
if(pwm_out) high_count++;
end
initial begin
rst_n=0;#100;rst_n=1;
$display("========== PWM音符验证 ==========");
for(int n=1;n<=8;n++) begin
note=n; total_count=0; high_count=0;
#500_000;
real freq=50_000_000.0/total_count;
$display("音符%0d: 频率=%.1fHz, 占空比=%.1f%%",n,freq,100.0*high_count/total_count);
end
note=0; #10000;
$display("静音: pwm_out=%b (应为0)",pwm_out);
$display("==========================================");
$finish;
end
initial #20_000_000 $finish;
endmodule