一.项目需求:
目标:自己组装,并通过编程驱动模拟扬声器实现电子琴的功能
一.基于套件和工具,自己组装电子琴
二自己编程基于FPGA实现:
1.存储一段音乐,并可以进行音乐播放,
2.可以自己通过板上的按键进行弹奏,支持两个按键同时按下(和弦)并且声音不能失真,板上的按键只有13个,可以通过有上方的“上“、”下”两个按键对音程进行扩展
3.使用扬声器进行播放时,输出的音调信号除了对应于该音调的单频正弦波外,还必须包含至少一个谐波分量
4.音乐的播放支持两种方式,这两种方式可以通过开关进行切换:
①当开关切换到蜂鸣器端,可以通过蜂鸣器来进行音乐播放
②当开关切换到扬声器端,可以通过模拟扬声器来进行音乐播放,每个音符都必须包含基频 + 至少一个谐波分量
本人已实现全部功能。
二.原理及实现方法:
1.整体原理及框图
上图是整个电子琴实现的框图。首先,是模拟扬声器部分。FPGA可以并行处理,所以我采用14个通道全部是并行存在的,即13个按键通道,一个自动演奏通道。若识别倒按键按下,则查询波表,这个波表是我提前写好的,一个正弦波含一个谐波,用128个点来离散描述,查询到当前时刻的点之后就送入累加器,这样可以保证有两个按键同时按下时,即和弦,可以实现。将两个正弦波叠加后,送入pwm生成的模块生成pwm。硬件上通过低通滤波器将pwm波滤波后形成带谐波的正弦波驱动模拟扬声器。而自动演奏部分就是新增加一个表,使得fpga在一定时间查谱表后再进行生成pwm。接着是蜂鸣器部分。同样采用并行,先读取按键,之后通过查表,得到分频值,在送入pwm生成的模块生成pwm波。蜂鸣器自动演奏部分也是新增加一个表,使得fpga在一定时间查谱表后再进行生成pwm。
2蜂鸣器和模拟喇叭的差别
模拟扬声器在频率响应比蜂鸣器好,模拟扬声器在人耳能听到的频率内电声转化效率差不多,音质较好。蜂鸣器声音较尖,说明在相对高一点的频率电声转化效率高,音质较差。
3用蜂鸣器和模拟喇叭的实现方法差别以及音效差别分析
①模拟喇叭
pwm是脉宽调制技术,当我们固定下pwm频率时,高电平脉冲时间的与脉冲周期的比值为占空比。当占空比越高,则输出的电压有效值越高。fpga只能发出pwm波,为了能发和弦,需要正弦波叠加。这时,我们用pwm波不断改变有效值,使其有效值变为一个正弦波,就是spwm波。
由于我们需要带谐波的正弦波,所以占空比不是单纯的正弦波数据,而是加上谐波后的数据,这样发出的spwm就是带谐波的正弦波。硬件电路上有rc组成的低通滤波器,这样pwm经过低通滤波器之后就变成了真正的带谐波的正弦波模拟信号,用它驱动喇叭就可以发出动人的声音,也可以用正弦波合成一下发出和弦。
②蜂鸣器
蜂鸣器发声就只需要pwm波即可。用计数器的方式对晶振频率进行分频,得到需要发声的频率,设置pwm占空比为50%,用这个pwm经过三极管后驱动蜂鸣器,即可实现电子琴功能。蜂鸣器由于腔体较小,膜片较薄,所以声音较尖,音效远不如模拟喇叭好,还不能叠加发和弦。
3模拟放大电路的仿真及分析
如上图是模拟放大部分。
第一部分为低通滤波,可将pwm数字信号变为相对平滑的模拟信号。这是让fpga输出转化为带谐波的正弦波关键部分,也是实现和弦的关键部分。
如图,是一个仿真部分,上图R1 R2 C1组成低通滤波,R1与R2分压,在50%时,输出电压为3.3*(100/(100+300))*0.5=0.41,与仿真结果相符;在80%时,输出电压为3.3*(100/(100+300))*0.8=0.66,与仿真结果相符;
第二部分为电位器分压调音量与电容通交阻直
如上图,电位器设置50%,从示波器可见小正弦波赋值为大正弦波50%,且由于电容隔直使得小正弦波偏置电压为0。
第三部分为8002功放
由于仿真软件找不到8002的模型,我就将8002根据内部框图拆开仿真。
8002内部框图,右边的运放固定增益,为40k/40k+1=2倍,左边的运放增益为外部设置。由电路图可知,电子琴将8002左边的运放接成反相放大,R23和R18决定倍数为47k/10k=4.7倍。所以总的放大倍数为2*4.7=9.4倍。由仿真图可知400mVp被放大约为4Vp,大致为10倍,与计算9.4倍差不多。
4.主要代码片段及说明
①模拟扬声器弹奏及和弦
驱动模拟扬声器,为了能实现双键按下和弦,那么需要使用正弦波。由于题目要求能带谐波,所以先生成一个含谐波的波表,使用时再去查表。
module wave(
input key,
input sys_clk,
output reg [5:0] put,
input [9:0] tone
);
reg [6:0] address;
reg [9:0] cnt1;
always @(posedge sys_clk )//wave点切换
begin
if(cnt1 < tone)
begin
cnt1 <= cnt1 + 1'b1;
end
else
begin
cnt1 <= 32'd0;
if(address < 127)
begin
address <= address+ 1'b1;
end
else
begin
address <= 7'd0;
end
end
end
always@(*)
begin
if(key==1'd0)
begin
case(address)
7'd0 : put= 6'd32;
7'd1 : put= 6'd33;
7'd2 : put= 6'd35;
7'd3 : put= 6'd37;
7'd4 : put= 6'd39;
7'd5 : put= 6'd41;
7'd6 : put= 6'd42;
7'd7 : put= 6'd44;
7'd8 : put= 6'd45;
7'd9 : put= 6'd46;
7'd10 : put= 6'd47;
7'd11 : put= 6'd47;
7'd12 : put= 6'd48;
7'd13 : put= 6'd48;
7'd14 : put= 6'd48;
7'd15 : put= 6'd48;
7'd16 : put= 6'd48;
7'd17 : put= 6'd47;
7'd18 : put= 6'd46;
7'd19 : put= 6'd45;
7'd20 : put= 6'd44;
7'd21 : put= 6'd43;
7'd22 : put= 6'd43;
7'd23 : put= 6'd41;
7'd24 : put= 6'd41;
7'd25 : put= 6'd40;
7'd26 : put= 6'd38;
7'd27 : put= 6'd37;
7'd28 : put= 6'd36;
7'd29 : put= 6'd36;
7'd30 : put= 6'd35;
7'd31 : put= 6'd35;
7'd32 : put= 6'd35;
7'd33 : put= 6'd35;
7'd34 : put= 6'd35;
7'd35 : put= 6'd35;
7'd36 : put= 6'd36;
7'd37 : put= 6'd37;
7'd38 : put= 6'd38;
7'd39 : put= 6'd39;
7'd40 : put= 6'd40;
7'd41 : put= 6'd41;
7'd42 : put= 6'd42;
7'd43 : put= 6'd43;
7'd44 : put= 6'd43;
7'd45 : put= 6'd44;
7'd46 : put= 6'd46;
7'd47 : put= 6'd46;
7'd48 : put= 6'd47;
7'd49 : put= 6'd47;
7'd50 : put= 6'd48;
7'd51 : put= 6'd48;
7'd52 : put= 6'd48;
7'd53 : put= 6'd47;
7'd54 : put= 6'd47;
7'd55 : put= 6'd46;
7'd56 : put= 6'd45;
7'd57 : put= 6'd44;
7'd58 : put= 6'd43;
7'd59 : put= 6'd42;
7'd60 : put= 6'd40;
7'd61 : put= 6'd37;
7'd62 : put= 6'd36;
7'd63 : put= 6'd34;
7'd64 : put= 6'd32;
7'd65 : put= 6'd32;
7'd66 : put= 6'd29;
7'd67 : put= 6'd28;
7'd68 : put= 6'd26;
7'd69 : put= 6'd23;
7'd70 : put= 6'd22;
7'd71 : put= 6'd20;
7'd72 : put= 6'd20;
7'd73 : put= 6'd18;
7'd74 : put= 6'd17;
7'd75 : put= 6'd17;
7'd76 : put= 6'd16;
7'd77 : put= 6'd16;
7'd78 : put= 6'd15;
7'd79 : put= 6'd16;
7'd80 : put= 6'd16;
7'd81 : put= 6'd17;
7'd82 : put= 6'd17;
7'd83 : put= 6'd18;
7'd84 : put= 6'd20;
7'd85 : put= 6'd20;
7'd86 : put= 6'd21;
7'd87 : put= 6'd22;
7'd88 : put= 6'd23;
7'd89 : put= 6'd24;
7'd90 : put= 6'd25;
7'd91 : put= 6'd26;
7'd92 : put= 6'd27;
7'd93 : put= 6'd28;
7'd94 : put= 6'd29;
7'd95 : put= 6'd29;
7'd96 : put= 6'd29;
7'd97 : put= 6'd29;
7'd98 : put= 6'd29;
7'd99 : put= 6'd29;
7'd100 : put= 6'd28;
7'd101 : put= 6'd28;
7'd102 : put= 6'd27;
7'd103 : put= 6'd26;
7'd104 : put= 6'd24;
7'd105 : put= 6'd24;
7'd106 : put= 6'd23;
7'd107 : put= 6'd21;
7'd108 : put= 6'd22;
7'd109 : put= 6'd20;
7'd110 : put= 6'd19;
7'd111 : put= 6'd19;
7'd112 : put= 6'd17;
7'd113 : put= 6'd17;
7'd114 : put= 6'd16;
7'd115 : put= 6'd17;
7'd116 : put= 6'd16;
7'd117 : put= 6'd17;
7'd118 : put= 6'd17;
7'd119 : put= 6'd18;
7'd120 : put= 6'd18;
7'd121 : put= 6'd19;
7'd122 : put= 6'd21;
7'd123 : put= 6'd22;
7'd124 : put= 6'd24;
7'd125 : put= 6'd26;
7'd126 : put= 6'd27;
7'd127 : put= 6'd30;
endcase
end
else
begin
put<=6'd32;
end
end
endmodule
↑其中tone是顶层文件输入该音调对应的计数器上限,这样当计数器累加到溢出就切换查表,address会加一,address再从case中查表,put输出,从而发出正弦波的每个点。
//*****************************************喇叭弹奏*********************************
wave kk1(.sys_clk (sys_clk), .key (key_in[0]), .put (q1), .tone(tone1));
wave kk2(.sys_clk (sys_clk), .key (key_in[1]), .put (q2), .tone(tone2));
wave kk3(.sys_clk (sys_clk), .key (key_in[2]), .put (q3), .tone(tone3));
wave kk4(.sys_clk (sys_clk), .key (key_in[3]), .put (q4), .tone(tone4));
wave kk5(.sys_clk (sys_clk), .key (key_in[4]), .put (q5), .tone(tone5));
wave kk6(.sys_clk (sys_clk), .key (key_in[5]), .put (q6), .tone(tone6));
wave kk7(.sys_clk (sys_clk), .key (key_in[6]), .put (q7), .tone(tone7));
wave kk8(.sys_clk (sys_clk), .key (key_in[7]), .put (q8), .tone(tone8));
wave kk9(.sys_clk (sys_clk), .key (key_in[8]), .put (q9), .tone(tone9));
wave kk10(.sys_clk (sys_clk), .key (key_in[9]), .put (q10), .tone(tone10));
wave kk11(.sys_clk (sys_clk), .key (key_in[10]), .put (q11), .tone(tone11));
wave kk12(.sys_clk (sys_clk), .key (key_in[11]), .put (q12), .tone(tone12));
wave kk13(.sys_clk (sys_clk), .key (key_in[12]), .put (q13), .tone(tone13));
wave auto(.sys_clk (sys_clk), .key (key_auto), .put (q14), .tone(tone14));
always@(*)begin
if(key_in==13'b0111111111111||key_in==13'b1011111111111||key_in==13'b1101111111111||key_in==13'b1110111111111||key_in==13'b1111011111111||key_in==13'b1111101111111||key_in==13'b1111110111111||key_in==13'b1111111011111||key_in==13'b1111111101111||key_in==13'b1111111110111||key_in==13'b1111111111011||key_in==13'b1111111111101||key_in==13'b1111111111110)
qout<=(q1+q2+q3+q4+q5+q6+q7+q8+q9+q10+q11+q12+q13-416)+32;
else
if(key_auto==0)
qout<=q14;
else
qout<=(q1+q2+q3+q4+q5+q6+q7+q8+q9+q10+q11+q12+q13-416)/2+32;
end
↑
我们将每个键和生成波形点进行例化,可以使顶层文件简洁。下面使用一个always语句,让只按下一个键时输出对应波形,而按下多个键时,就累加两个波形并除以2,保证和弦时输出的音量和单键时输出的音量相等。
②蜂鸣器部分。
蜂鸣器采用直接分频的方式得到对应音符频率。
//*******************************************************************************************
//******************************蜂鸣器*******************************************************
always@(posedge sys_clk)
begin
case(key_in)
13'b1111111111110:freq_data2<=Tone1;
13'b1111111111101:freq_data2<=Tone2;
13'b1111111111011:freq_data2<=Tone3;
13'b1111111110111:freq_data2<=Tone4;
13'b1111111101111:freq_data2<=Tone5;
13'b1111111011111:freq_data2<=Tone6;
13'b1111110111111:freq_data2<=Tone7;
13'b1111101111111:freq_data2<=Tone8;
13'b1111011111111:freq_data2<=Tone9;
13'b1110111111111:freq_data2<=Tone10;
13'b1101111111111:freq_data2<=Tone11;
13'b1011111111111:freq_data2<=Tone12;
13'b0111111111111:freq_data2<=Tone13;
default:freq_data2<=4;
endcase
end
↑根据按键选择freq_data2的值,fre_data2是分频系数,决定方波频率,也就决定了蜂鸣器的音调。
//设置50%占空比:音阶分频计数值的一半即为占空比的高电平数
assign duty_data = freq_data >> 1'b1;
//freq_cnt:当计数到音阶计数值或跳转到下一音阶时,开始重新计数
always@(posedge sys_clk or negedge sys_rst_n)
begin
if(key_auto==1)freq_data=freq_data2;
else freq_data=freq_data1;
if(sys_rst_n == 1'b0)
freq_cnt <= 18'd0;
else if(freq_cnt == freq_data )
freq_cnt <= 18'd0;
else
freq_cnt <= freq_cnt + 1'b1;
end
//beep:输出蜂鸣器波形
always@(posedge sys_clk )
if(sys_rst_n == 1'b0 )
beep <= 1'b0;
else if(freq_cnt >= duty_data && switch==1'b0)
beep <= 1'b1;
else
beep <= 1'b0;
↑duty_data为freq_data的一半,发出来的就是50%的方波。freq_data为我们设好的频率,当计数器累加到duty_data(freq_data的一半)就切换电平,这样就可以发出方波了。
③高低音切换
//**************************切换高低声部*************************
always @(posedge sys_clk)
begin
if (key_high==1'b1 && key_low==1'b0)
begin
tone1<=10'd360;
tone2<=10'd340;
tone3<=10'd322;
tone4<=10'd304;
tone5<=10'd286;
tone6<=10'd270;
tone7<=10'd256;
tone8<=10'd240;
tone9<=10'd228;
tone10<=10'd214;
tone11<=10'd202;
tone12<=10'd192;
tone13<=10'd180;
Tone1<=18'd45801;
Tone2<=18'd43230;
Tone3<=18'd40804;
Tone4<=18'd38514;
Tone5<=18'd36352;
Tone6<=18'd34312;
Tone7<=18'd32386;
Tone8<=18'd30568;
Tone9<=18'd28853;
Tone10<=18'd27233;
Tone11<=18'd25705;
Tone12<=18'd24262;
Tone13<=18'd22900;
end
else
begin
if (key_low==1'b1&&key_high==1'b0)
begin
tone1<=10'd180;
tone2<=10'd170;
tone3<=10'd161;
tone4<=10'd152;
tone5<=10'd143;
tone6<=10'd135;
tone7<=10'd128;
tone8<=10'd120;
tone9<=10'd114;
tone10<=10'd107;
tone11<=10'd101;
tone12<=10'd96;
tone13<=10'd90;
Tone1<=18'd22944;
Tone2<=18'd21656;
Tone3<=18'd20441;
Tone4<=18'd19293;
Tone5<=18'd18211;
Tone6<=18'd17188;
Tone7<=18'd16224;
Tone8<=18'd15313;
Tone9<=18'd14454;
Tone10<=18'd13642;
Tone11<=18'd12877;
Tone12<=18'd12154;
Tone13<=18'd11472;
end
else
begin
tone1 <= tone1;
tone2 <= tone2;
tone3 <= tone3;
tone4 <= tone4;
tone5 <= tone5;
tone6 <= tone6;
tone7 <= tone7;
tone8 <= tone8;
tone9 <= tone9;
tone10 <= tone10;
tone11 <= tone11;
tone12 <= tone12;
tone13 <= tone13;
Tone1<=Tone1;
Tone2<=Tone2;
Tone3<=Tone3;
Tone4<=Tone4;
Tone5<=Tone5;
Tone6<=Tone6;
Tone7<=Tone7;
Tone8<=Tone8;
Tone9<=Tone9;
Tone10<=Tone10;
Tone11<=Tone11;
Tone12<=Tone12;
Tone13<=Tone13;
end
end
end
//**************************************************************************************
↑通过检测按键key_high与key_low的状态来选择高低声部,这部分是扬声器和蜂鸣器同时选高声部或低声部。
④自动演奏部分
//***********************自动演奏*******************************************************
reg [24:0] cnt_yin ; //每个音时常累加器
reg [6:0] cnt_pai ; //音的个数
parameter TIME_cnt = 25'd3_700_000; //每个音阶鸣叫时间
//循环计数器
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
cnt_yin <= 25'd0;
else if(cnt_yin == TIME_cnt )
cnt_yin <= 25'd0;
else
cnt_yin <= cnt_yin + 1'b1;
//对音个数进行计数
always@(posedge sys_clk or negedge sys_rst_n)
if((cnt_yin == TIME_cnt && cnt_pai == 36)||(!sys_rst_n) )
cnt_pai <= 7'd0;
else if(cnt_yin == TIME_cnt)
cnt_pai <= cnt_pai + 1'b1;
//不同时间鸣叫不同的音阶
always@(posedge sys_clk)
case(cnt_pai)
0:begin tone14<=10'd180;freq_data1<=22944;end//1
1:begin tone14<=10'd161;freq_data1<=20441;end//2
2:begin tone14<=10'd143;freq_data1<=18211;end//3
3:begin tone14<=10'd107;freq_data1<=13642;end//6
4:begin tone14<=10'd120;freq_data1<=15313;end//5
5:begin tone14<=10'd107;freq_data1<=13642;end//6
6:begin tone14<=10'd120;freq_data1<=15313;end//5
7:begin tone14<=10'd107;freq_data1<=13642;end//6
8:begin tone14<=10'd120;freq_data1<=15313;end//5
9:begin tone14<=10'd161;freq_data1<=20441;end//2
10:begin tone14<=10'd143;freq_data1<=18211;end//3
11:begin tone14<=10'd107;freq_data1<=13642;end//6
12:begin tone14<=10'd120;freq_data1<=15313;end//5
13:begin tone14<=10'd107;freq_data1<=13642;end//6
14:begin tone14<=10'd120;freq_data1<=15313;end//5
15:begin tone14<=10'd107;freq_data1<=13642;end//6
16:begin tone14<=10'd120;freq_data1<=15313;end//5
17:begin tone14<=10'd143;freq_data1<=18211;end//3
18:begin tone14<=10'd161;freq_data1<=20441;end//2
19:begin tone14<=10'd180;freq_data1<=22944;end//1
20:begin tone14<=10'd214;freq_data1<=27233;end//.6
21:begin tone14<=10'd180;freq_data1<=22944;end//1
22:begin tone14<=10'd161;freq_data1<=20441;end//2
23:begin tone14<=10'd180;freq_data1<=22944;end//1
24:begin tone14<=10'd214;freq_data1<=27233;end//.6
25:begin tone14<=10'd180;freq_data1<=22944;end//1
26:begin tone14<=10'd143;freq_data1<=18211;end//3
27:begin tone14<=10'd143;freq_data1<=18211;end//3
28:begin tone14<=10'd143;freq_data1<=18211;end//3
29:begin tone14<=10'd135;freq_data1<=17188;end//4
30:begin tone14<=10'd143;freq_data1<=18211;end//3
31:begin tone14<=10'd161;freq_data1<=20441;end//2
32:begin tone14<=10'd161;freq_data1<=20441;end//2
33:begin tone14<=10'd1;freq_data1<=4;end//停
34:begin tone14<=10'd1;freq_data1<=4;end
35:begin tone14<=10'd1;freq_data1<=4;end
36:begin tone14<=10'd1;freq_data1<=4;end
endcase
↑这部分是音乐自动演奏,是模拟扬声器和蜂鸣器共用的。如上左图,第一个计数器是是决定每个音符响多久的时间,当第一个计数器累加溢出,第二个计数器就会累加,第二个计数器会去查表,在case语句来选择每个时间对应的音符,把这些音符送入模拟扬声器或蜂鸣器就可以自动演奏了。
5.遇到的主要难题及解决方法
①难题:网页版fpga软件无法使用ROM IP核来存储波形表。
解决方法:在程序中直接采用寄存器来存储。
②难题:不知道怎么进行和弦。
解决方法:查书,书中有讲到正弦波的叠加,有了灵感后就用verilog语言来写正弦波叠加。
6.改进建议
①资源使用还可优化,一下是资源使用报告
②可增加音色变换,来模拟打击乐、吉他、二胡等乐器。
③可增加节拍提示功能。
④可增加录音功能。