目标:
自己组装,并通过编程驱动模拟扬声器实现电子琴的功能
需完成的任务:
基于我们提供的套件和工具,自己组装电子琴
自己编程基于FPGA实现:
1、存储一段音乐,并可以进行音乐播放
完成情况:已完成
2、可以自己通过板上的按键进行弹奏,支持两个按键同时按下(和弦)并且声音不能失真,板上的按键只有13个,可以通过有上方的“上“、”下”两个按键对音程进行扩展
完成情况:最多可同时按下六个按键,考虑到过大音程演奏时不方便,扬声器和蜂鸣器音程都为三个音阶中高低音
3、使用扬声器进行播放时,输出的音调信号除了对应于该音调的单频正弦波外,还必须包含至少一个谐波分量
完成情况:一到五次谐波占比:0.63:0.21:0.06:0.05:0.05
4、音乐的播放支持两种方式,这两种方式可以通过开关进行切换:
完成情况:已完成
5、当开关切换到蜂鸣器端,可以通过蜂鸣器来进行音乐播放
完成情况:已完成
6、当开关切换到扬声器端,可以通过模拟扬声器来进行音乐播放,每个音符都必须包含基频 + 至少一个谐波分量
完成情况:已完成
一、环境配置
项目使用 Lattice Diamond 开发(我使用的版本为3.10.3.144),可参考这篇文章配置环境:https://www.stepfpga.com/doc/%E5%BF%AB%E9%80%9F%E4%B8%8A%E6%89%8Bstep-mxo2-c
电子琴功能示意图
电子琴原理图
二、程序解析
模块组成
top.v:顶层模块
debounce.v:按键消抖模块
synthesizer.v:波形合成模块
wave.v:查表生成波形模块
deltasigma.v:dac模块
piano_speaker.v:音乐盒播放模块
Buzzer.v:蜂鸣器模块
代码解析
查表生成波形模块
项目要求电子琴要能够演奏复音,这意味着会有多路同时查询波表,需要用一个多端口ROM来实现。
module wave(clk,rst,enable,fre,waveout);
input clk,rst,enable;
output reg signed[9:0] waveout;
input [31:0]fre;
wire signed[9:0] sin_out;
reg [23:0] phase_acc;
reg signed[9:0] memory[0:255];//申请256个10位的存储单元
initial
begin
$readmemh("xiebo.txt",memory); //读取xiebo.txt中的数字到memory
end
//生成波形
always @(posedge clk or negedge rst) begin
if(!rst)
phase_acc<=0;
else
phase_acc <= phase_acc + fre;
end
assign sin_out=memory[phase_acc[23:16]];
always @(posedge clk or negedge rst) begin
if(!rst)
waveout<=0;
else if(!enable)
waveout<=sin_out;
else
waveout<=0;
end
endmodule
波形合成器
这里用了音程计数器rise_state,通过它实现波形的倍频,从而使得音程扩大。相较于频繁引用波形生成模块来扩大音程,这种方法更加节约资源,适用于小脚丫FPGA。
module synthesizer(clk,rst,key,up,down,wavecnt,buzzer_out);
input clk,rst,up,down;
input [12:0]key;
output signed[11:0]wavecnt;
output buzzer_out;
reg [1:0]rise_state;
wire signed[9:0] wavec1,wavec_1,waved1,waved_1,wavee1,wavef1,wavef_1,waveg1,waveg_1,wavea1,wavea_1,waveb1,wavec2;
wire buzzerc1,buzzerc_1,buzzerd1,buzzerd_1,buzzere1,buzzerf1,buzzerf_1,buzzerg1,buzzerg_1,buzzera1,buzzera_1,buzzerb1,buzzerc2;
always@(posedge clk or negedge rst)begin
if (!rst)
rise_state<=2'd0;
else if(up==1)
rise_state<=rise_state+2'd1;
else if(down==1)
rise_state<=rise_state-2'd1;
else if(rise_state==2'd3)
rise_state<=2'd0;
else
rise_state<=rise_state;
end
wave c1(.clk(clk),.rst(rst),.enable(key[0]), .fre(366*(1<<rise_state)),.waveout(wavec1));
wave c_1(.clk(clk),.rst(rst),.enable(key[1]), .fre(388*(1<<rise_state)),.waveout(wavec_1));
wave d1(.clk(clk),.rst(rst),.enable(key[2]), .fre(411*(1<<rise_state)),.waveout(waved1));
wave d_1(.clk(clk),.rst(rst),.enable(key[3]), .fre(435*(1<<rise_state)),.waveout(waved_1));
wave e1(.clk(clk),.rst(rst),.enable(key[4]), .fre(461*(1<<rise_state)),.waveout(wavee1));
wave f1(.clk(clk),.rst(rst),.enable(key[5]), .fre(488*(1<<rise_state)),.waveout(wavef1));
wave f_1(.clk(clk),.rst(rst),.enable(key[6]), .fre(517*(1<<rise_state)),.waveout(wavef_1));
wave g1(.clk(clk),.rst(rst),.enable(key[7]), .fre(548*(1<<rise_state)),.waveout(waveg1));
wave g_1(.clk(clk),.rst(rst),.enable(key[8]), .fre(581*(1<<rise_state)),.waveout(waveg_1));
wave a1(.clk(clk),.rst(rst),.enable(key[9]), .fre(615*(1<<rise_state)),.waveout(wavea1));
wave a_1(.clk(clk),.rst(rst),.enable(key[10]), .fre(652*(1<<rise_state)),.waveout(wavea_1));
wave b1(.clk(clk),.rst(rst),.enable(key[11]), .fre(690*(1<<rise_state)),.waveout(waveb1));
wave c2(.clk(clk),.rst(rst),.enable(key[12]), .fre(732*(1<<rise_state)),.waveout(wavec2));
Buzzer c3(.clk(clk),.rst(rst),.tone_en(key[0]),.time_end(5734*(4>>rise_state)),.buzzer_out(buzzerc1));
Buzzer c_3(.clk(clk),.rst(rst),.tone_en(key[1]),.time_end(5412*(4>>rise_state)),.buzzer_out(buzzerc_1));
Buzzer d3(.clk(clk),.rst(rst),.tone_en(key[2]),.time_end(5108*(4>>rise_state)),.buzzer_out(buzzerd1));
Buzzer d_3(.clk(clk),.rst(rst),.tone_en(key[3]),.time_end(4822*(4>>rise_state)),.buzzer_out(buzzerd_1));
Buzzer e3(.clk(clk),.rst(rst),.tone_en(key[4]),.time_end(4551*(4>>rise_state)),.buzzer_out(buzzere1));
Buzzer f3(.clk(clk),.rst(rst),.tone_en(key[5]),.time_end(4295*(4>>rise_state)),.buzzer_out(buzzerf1 ));
Buzzer f_3(.clk(clk),.rst(rst),.tone_en(key[6]),.time_end(4054*(4>>rise_state)),.buzzer_out(buzzerf_1 ));
Buzzer g3(.clk(clk),.rst(rst),.tone_en(key[7]),.time_end(3827*(4>>rise_state)),.buzzer_out(buzzerg1 ));
Buzzer g_3(.clk(clk),.rst(rst),.tone_en(key[8]),.time_end(3612*(4>>rise_state)),.buzzer_out(buzzerg_1 ));
Buzzer a3(.clk(clk),.rst(rst),.tone_en(key[9]),.time_end(3409*(4>>rise_state)),.buzzer_out(buzzera1 ));
Buzzer a_3(.clk(clk),.rst(rst),.tone_en(key[10]),.time_end(3218*(4>>rise_state)),.buzzer_out(buzzera_1 ));
Buzzer b3(.clk(clk),.rst(rst),.tone_en(key[11]),.time_end(3037*(4>>rise_state)),.buzzer_out(buzzerb1 ));
Buzzer c4(.clk(clk),.rst(rst),.tone_en(key[12]),.time_end(2867*(4>>rise_state)),.buzzer_out(buzzerc2 ));
assign buzzer_out = buzzerc1+buzzerc_1+buzzerd1+buzzerd_1+buzzere1+buzzerf1+buzzerf_1+buzzerg1+buzzerg_1+buzzera1+buzzera_1+buzzerb1+buzzerc2;
assign wavecnt=wavec1+wavec_1+waved1+waved_1+wavee1+wavef1+waveg1+wavef_1+waveg_1+wavea1+wavea_1+waveb1+wavec2;
endmodule
按键消抖模块
key按下时,key_pulse会产生一个时钟周期正脉冲,会用即可
module debounce (clk,rst,key,key_pulse);
parameter N = 1; //要消除的按键的数量
input clk;
input rst;
input [N-1:0] key; //输入的按键
output [N-1:0] key_pulse; //按键动作产生的脉冲
reg [N-1:0] key_rst_pre;
reg [N-1:0] key_rst;
wire [N-1:0] key_edge;
always @(posedge clk or negedge rst)
begin
if (!rst) begin
key_rst <= {N{1'b1}};
key_rst_pre <= {N{1'b1}};
end
else begin
key_rst <= key;
key_rst_pre <= key_rst;
end
end
assign key_edge = key_rst_pre & (~key_rst);
reg [17:0] cnt;
always @(posedge clk or negedge rst)
begin
if(!rst)
cnt <= 18'h0;
else if(key_edge)
cnt <= 18'h0;
else
cnt <= cnt + 1'h1;
end
reg [N-1:0] key_sec_pre;
reg [N-1:0] key_sec;
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec <= {N{1'b1}};
else if (cnt==18'h3ffff)
key_sec <= key;
end
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec_pre <= {N{1'b1}};
else
key_sec_pre <= key_sec;
end
assign key_pulse = key_sec_pre & (~key_sec);
endmodule
dac模块
这里的输入是有符号数,需要注意
module deltasigma(
input [11:0]in,//输入有符号数
output reg out,
input clk,
input rst
);
parameter [15:0]MAX = 16'b0000011111111111;
parameter [15:0]MIN = 16'b1111100000000001;
reg [15:0] sigma;
wire [15:0] delta;
always@(posedge clk or negedge rst)begin
if(!rst) begin
sigma<=0;
end
else begin
out<=(sigma[15])?0:1;
sigma<=sigma+({in[11],in[11],in[11],in[11],in}+delta);
end
end
assign delta=out?MIN:MAX;
endmodule
蜂鸣器模块
这里由小脚丫例程修改得来:蜂鸣器模块 [电子森林] (eetree.cn)
module Buzzer
(
input clk,
input rst,
input tone_en,
input [31:0] time_end,
output reg buzzer_out //输出音符
);
reg [31:0] time_cnt;
//当蜂鸣器使能时,计数器按照计数终值(分频系数)计数
always@(posedge clk or negedge rst) begin
if(!rst) begin
time_cnt <= 1'b0;
end else if(tone_en) begin
time_cnt <= 1'b0;
end else if(time_cnt>=time_end) begin
time_cnt <= 1'b0;
end else begin
time_cnt <= time_cnt + 1'b1;
end
end
//根据计数器的周期,翻转蜂鸣器控制信号
always@(posedge clk or negedge rst) begin
if(!rst) begin
buzzer_out <= 1'b0;
end else if(time_cnt==time_end) begin
buzzer_out <= ~buzzer_out; //蜂鸣器控制输出翻转,两次翻转为1Hz
end else begin
buzzer_out <= buzzer_out;
end
end
endmodule
自动播放模块
由例程修改得来基于FPGA的可以播放、切换曲子、显示曲名的音乐播放器 - 电子森林 (eetree.cn)
继续使用多端口rom
initial
begin
$readmemh("xiebo.txt",memory); //读取sin.txt中的数字到memory,这个出来的波形最标准
end
在其中添加了扬声器部分
reg [23:0] phase_acc;
always@(posedge clk or negedge rst) begin
if(!rst) begin
phase_acc<= 1'b0;
end else if(!s_s) begin
phase_acc<= 1'b0;
end else begin
phase_acc<= phase_acc+ FREQ;
end
end
assign wave_out=memory[phase_acc[23:16]];
锁相环倍频模块,这里使用的时diamond的ip
//pll倍频
pll pll_u0(
.CLKI(clk),//12Mhz
.CLKOP(clk_out)//120Mhz
);
代码介绍完成
三、思考
1、蜂鸣器和模拟喇叭的差别
在此项目中,主要体现出两者原理的不同
喇叭其实是一种电能转换成声音的一种转换设备,dds驱动喇叭,可以采用不同的波形,如在正弦波上加入谐波,这样可以使得喇叭获得不同的音色,如果根据一定的比例加入谐波,甚至可以达到接近实际乐器音色效果,可以按照个人喜好来选择。同时喇叭可以加入复音,只需在dds中将两种音符波形叠加即可。
无源激型蜂鸣器的工作发声原理是:方波信号输入谐振装置转换为声音信号输出。这样的优点是驱动方便,输入不同频率的方波即可发声。缺点也在于这一特点,只能简单地控制频率来驱动发出一种音色,并且这种音色不够优美,有点像噪音。更不能多个波形进行合成,无法演奏和弦。
2、遇到的主要难题
不了解PWM-DAC原理,对DDS代码不够熟练
解决方法:参考官方案例和资料2022暑期在家一起练(3) - 基于FPGA的电子琴设计 - 电子森林 (eetree.cn)
单个音符波形失真
解决方法:自己用数学软件生成波表,得到更准确的波形
3、还可以进行的改进
涵盖所有音程
可以十三个按键同时按下而不失真,在这个项目里只能同时按下3到4个按键
通过按键更改扬声器的音色
使用mid文件实现自动播放,避免自己翻译简谱写入FPGA的繁琐
4、对模拟功放电路的分析
这里用了8002b音频功放处理声音,通过上网查阅资料,得知PWM频率小于200khz时音质会变差,500khz时音质较好。故使用锁相环ip核倍频。
同时我进行了仿真(这里用两个运放模拟了8002b芯片的内部结构)
向功放输入200khz信号得到波形
向功放输入500khz信号得到波形
可以看出500khz信号衰减了许多,而200khz信号幅值较大,故200khz的PWM-DAC对音质影响较大,结论得证。
5、资源使用报告
根据diamond的Design Summary文件可以得出以下主要信息:
项目使用了21%的寄存器,都是作为PFU寄存器使用。
使用了86%的slice,slice由两个LUT组成,故LUT也使用了86%。LUT多作为ROM,主要用在波形生成相关模块上
使用了32个端口,其中四个是JTAG所用
嵌入式块RAM(BRAM) 十个都被使用,主要用在波形合成模块上,需要多次例化