一、电子琴的原理及框图
由物理知识可知,所有的声音都是由于振动产生的。而根据声乐原理,本项目中我们要模拟的乐器声音,其本质上就是多种特定频率的正弦波及其谐波的叠加。要模拟钢琴发出的声音,一是要产生不同音符对应的音高(即不同音符对应的不同频率的正弦波和谐波),二是要产生对应的音色(即正弦波的基波和谐波有合适的大小比)。
具体来说,将上述理论部署到这款LATTICE的小脚丫FPGA板上,要完成一下几个模块:一是按键输入的消抖模块:因为机械按键的细微震动,在FPGA引脚采样的时候会存在几毫秒的电平不稳定情况。其次就是这次项目要求的分别实现蜂鸣器和扬声器的音乐自动播放和电子琴模拟。
对于蜂鸣器,本次用的是无源蜂鸣器,我们可以通过控制驱动蜂鸣器的PWM的频率和占空比来控制其输出声音的频率和音量大小。
而对于扬声器,我们则需要提供模拟信号,即我们所期望的声音振动波形的电信号给其,这个仅靠FPGA的数字引脚输出是无法做到的,但是电子森林方提供的拓展板上面在FPGA的IO引脚又接了一低通滤波电路,这使得了PWM波实现一位DAC成为可能。根据傅里叶技术变换可知,对于PWM输出的方波,在频域考虑的话就是零频的直流分量和方波基频及其谐波的线性叠加,故而如果使用低通滤波电路,使得截止频率远低于基波频率,在理论上就可以获得与占空比成正比的直流分量,当频率够高,且占空比在不断变化的情况下,最终输出的就是近似连续的模拟波形。
至于要实现自动音乐播放,我们就需要提前把一首歌的包括节拍和音调等各个信息编码并存储起来,然后再根据控制信号对存储信息进行提取,解码,根据上述提供的API然后自动播放音乐。
整个电子琴模块的框图如下所示:
二、分析蜂鸣器和模拟喇叭的区别
蜂鸣器的频率响应比扬声器要差很多。因此,蜂鸣器对于一些乐器的音色的实现情况相较于扬声器会差很多,因为其很容易产生比较多的谐波分量和噪声。
而我们在生活中也一般仅用蜂鸣器用作警报或者其他滴滴响声的场合。
在需要较高水平的音色表现时,会在更多情况下使用模拟喇叭。
从驱动信号来看,蜂鸣器是可以通过IO直接输出的数字信号来驱动控制的,即可以通过数字IO口生成一定频率的PWM方波信号,从而获得对应频率的声波信号,我们可以通过改变PWM的频率和占空比来调节蜂鸣器输出声音的频率和大小。而模拟喇叭则需要模拟电信号来驱动,而这一般都需要我们使用DAC来将FPGA输出的数字信号转化为模拟信号,再经过功率放大和滤波等后期处理电路才能输出驱动模拟喇叭发出声音。
三、用蜂鸣器和模拟喇叭实现方法的差别和音效差别分析
根据上一部分所述,蜂鸣器实现电子琴主要是通过修改FPGA对应引脚输出的PWM的频率来产生相应频率的激励信号驱动三极管实现;而模拟喇叭,由于载板无DAC器件,但可以通过滤波电路实现将占空比不断变化的高频PWM信号转化为近似连续的模拟电路(某一时刻的模拟信号幅值正比于此时的PWM占空比)。
从后期处理上看,对于蜂鸣器产生的信号,我们无法通过简单相加进行和弦的操作,同时对于自行设计谐波加入蜂鸣器产生的信号也比较困难。而对于扬声器产生的声音信号,我们可以比较自由的设计其输出信号的波形。
从获得音效以及声音信号的频谱分析来看,使用蜂鸣器得到的声音信号相对于模拟喇叭获得的信号,杂音较重,噪声较多,产生的对应频率声音的谐波现象比较明显。
如下,上图为C5的蜂鸣器输出,下图为C5的扬声器输出
四、模拟放大电路的仿真和分析
笔者使用Multisim14.0进行仿真,由于缺少原件8002B的SPICE模型,笔者打算使用基本运放741和其他原件模拟8002b的结构,来代替8002b, 查阅手册后搭建了如下图所示模拟放大电路模型。
使用交流分析,发现在输入电信号的频率为10Hz~22kHz之间时,电信号的幅频响应都趋近相同,相频响应近似为线性。
故而在输入的DAC的PWM信号为10~22kHz之间,此放大电路都会有很好的频率响应,失真度较低,而我们所期望的电子琴模拟声音频率刚好在这个范围中!
故而此电路设计在理论仿真方面设计满足要求。
五、主要代码片段及说明
(1)音程控制:首先通过已经消抖处理的按键信号输入,通过一个0-7的循环计数器,按键控制计数器的递加递减,来控制电子琴的音程。
always @(posedge clk) begin a_key_last <= a_key;end
assign a_key_press = a_key_last & ~a_key;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
chose_cnt <= 3;
end
else begin
if(a_key_press[0]) chose_cnt <= chose_cnt - 1;
if(a_key_press[1]) chose_cnt <= chose_cnt + 1;
end
end
(2)音乐自动播放:这个部分我们首先需要将一首曲子的信息进行编码,存储到ROM中,在FPGA需要进行自动播放时再从中依次取出进行译码,根据相应的乐曲信息进行控制。
而对于乐曲信息的压缩,一个是调(声音频率),一个是拍(延续时长)。只要将这两个信息分别编码,即可完成音乐信息的简单存储。
song1 mybeepsongpai(.Address(pai_addr),.OutClock(clk),.OutClockEn(1'b1),.Reset(1'b0),.Q(pai_out));
song1 mybeepsongtone(.Address(tone_addr),.OutClock(clk),.OutClockEn(1'b1),.Reset(1'b0),.Q(tone_out));
always @(posedge clk) begin
if(play_cnt && ~sel) begin
player_base_cnt <= player_base_cnt + 1'b1;
if(player_base_cnt == 0) begin
if( pai_cnt == pai_decode - 4'b1 )
begin
pai_addr <= pai_addr + 8'd2;
tone_addr <= tone_addr + 8'd2;
pai_cnt<=0;
end
else begin pai_cnt <= pai_cnt + 4'b1; end
end
end
else begin
pai_cnt <= 0;
pai_addr <= 8'd1;
tone_addr <= 0;
player_base_cnt <= 1;
end
end
always @(pai_out) begin
case (pai_out)
8'd1: pai_decode = 4'h8;
8'd2: pai_decode = 4'h4;
8'd3: pai_decode = 4'h2;
8'd4: pai_decode = 4'h1;
8'd21: pai_decode = 4'd15;
8'd22: pai_decode = 4'h8;
8'd23: pai_decode = 4'd4;
8'd24: pai_decode = 4'd2;
8'd101: pai_decode = 4'h8;
8'd102: pai_decode = 4'h4;
8'd103: pai_decode = 4'h2;
8'd104: pai_decode = 4'h1;
8'd121: pai_decode = 4'd15;
8'd122: pai_decode = 4'h8;
8'd123: pai_decode = 4'd4;
8'd124: pai_decode = 4'd2;
default: pai_decode = 0;
endcase
end
always @(pai_out or tone_out) begin
case ({pai_out[7:5],tone_out})
// 低音部
{4'h0,8'd11}: tone_decode = 32'd45867;
{4'h3,8'd11}: tone_decode = 32'd43293;
{4'h0,8'd12}: tone_decode = 32'd40863;
{4'h3,8'd12}: tone_decode = 32'd38569;
{4'h0,8'd13}: tone_decode = 32'd36405;
{4'h0,8'd14}: tone_decode = 32'd34362;
{4'h3,8'd14}: tone_decode = 32'd32433;
{4'h0,8'd15}: tone_decode = 32'd30613;
{4'h3,8'd15}: tone_decode = 32'd28894;
{4'h0,8'd16}: tone_decode = 32'd27272;
{4'h3,8'd16}: tone_decode = 32'd25742;
{4'h0,8'd17}: tone_decode = 32'd24297;
//中音部
{4'h0,8'd21}: tone_decode = 32'd22934;
{4'h3,8'd21}: tone_decode = (32'd43293>>1);
{4'h0,8'd22}: tone_decode = (32'd40863>>1);
{4'h3,8'd22}: tone_decode = (32'd38569>>1);
{4'h0,8'd23}: tone_decode = (32'd36405>>1);
{4'h0,8'd24}: tone_decode = (32'd34362>>1);
{4'h3,8'd24}: tone_decode = (32'd32433>>1);
{4'h0,8'd25}: tone_decode = (32'd30613>>1);
{4'h3,8'd25}: tone_decode = (32'd28894>>1);
{4'h0,8'd26}: tone_decode = (32'd27272>>1);
{4'h3,8'd26}: tone_decode = (32'd25742>>1);
{4'h0,8'd27}: tone_decode = (32'd24297>>1);
//高音部
{4'h0,8'd31}: tone_decode = (32'd22934>>1);
{4'h3,8'd31}: tone_decode = (32'd43293>>2);
{4'h0,8'd32}: tone_decode = (32'd40863>>2);
{4'h3,8'd32}: tone_decode = (32'd38569>>2);
{4'h0,8'd33}: tone_decode = (32'd36405>>2);
{4'h0,8'd34}: tone_decode = (32'd34362>>2);
{4'h3,8'd34}: tone_decode = (32'd32433>>2);
{4'h0,8'd35}: tone_decode = (32'd30613>>2);
{4'h3,8'd35}: tone_decode = (32'd28894>>2);
{4'h0,8'd36}: tone_decode = (32'd27272>>2);
{4'h3,8'd36}: tone_decode = (32'd25742>>2);
{4'h0,8'd37}: tone_decode = (32'd24297>>2);
default: tone_decode = 1;
endcase
end
(3)蜂鸣器输出:先根据输入的按键和开关的状态经由二路选择器选择是否为音乐播放还是电子琴模拟,若为音乐播放则由ROM信息驱动PWM模块,若是模拟电子琴则根据键盘信息通过多路选择器选择对应音调的参数进行播放。
reg [31:0] num,compare;
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin num <= 1; compare <= 0; end
else if(play_cnt && ~sel) begin // 自动播放音乐
num <= tone_decode; compare <= (tone_decode>>1);
end
else
begin //
case ({sel,key})
18'h1FFE: begin num <= (32'D45867>>(chose_cnt))<<3; compare <= (32'D45867>>(chose_cnt))<<2; end // C4 : 261.626
18'h1FFD: begin num <= (32'D43293>>(chose_cnt))<<3; compare <= (32'D43293>>(chose_cnt))<<2; end // C#4 : 277.183
18'h1FFB: begin num <= (32'D40863>>(chose_cnt))<<3; compare <= (32'D40863>>(chose_cnt))<<2; end // D4 : 293.665
18'h1FF7: begin num <= (32'D38569>>(chose_cnt))<<3; compare <= (32'D38569>>(chose_cnt))<<2; end // D#4 : 311.127
18'h1FEF: begin num <= (32'D36405>>(chose_cnt))<<3; compare <= (32'D36405>>(chose_cnt))<<2; end // E4 : 329.628
18'h1FDF: begin num <= (32'D34362>>(chose_cnt))<<3; compare <= (32'D34362>>(chose_cnt))<<2; end // F4 : 349.228
18'h1FBF: begin num <= (32'D32433>>(chose_cnt))<<3; compare <= (32'D32433>>(chose_cnt))<<2; end // F#4 : 369.994
18'h1F7F: begin num <= (32'D30613>>(chose_cnt))<<3; compare <= (32'D30613>>(chose_cnt))<<2; end // G4 : 391.995
18'h1EFF: begin num <= (32'D28894>>(chose_cnt))<<3; compare <= (32'D28894>>(chose_cnt))<<2; end // G#4 : 415.305
18'h1DFF: begin num <= (32'D27272>>(chose_cnt))<<3; compare <= (32'd27272>>(chose_cnt))<<2; end // A4 : 440
18'h1BFF: begin num <= (32'D25742>>(chose_cnt))<<3; compare <= (32'D25742>>(chose_cnt))<<2; end // A#4 : 466.164
18'h17FF: begin num <= (32'D24297>>(chose_cnt))<<3; compare <= (32'd24297>>(chose_cnt))<<2; end // B4 : 493.883
18'h0FFF: begin num <= (32'D22934>>(chose_cnt))<<3; compare <= (32'D22934>>(chose_cnt))<<2; end // C5 : 523.251
default: begin num <= 1; compare <= 0 ; end
endcase
end
end
(4)生成带谐波的正弦波部分:使用DDS,先存储1/4周期的正弦波形信息,然后通过提前加入偏置,根据最高位地址对低位寻址结果取反,最终可以获得全周期的正弦波形。同时生成两个正弦波,频率成2倍关系,最终将各自结果叠加输出即可。
// step=2^N*fout/fclk
module DDS_sin #(parameter N = 24) (
input clk,
input[N-1:0] step,
input enable,
output[9:0] sin_out
);
reg [N-1:0] phase1,phase2;
reg [5:0] address1,address2;
wire [1:0] sel1,sek2;
wire [8:0] sine_table_out1,sine_table_out2;
reg [10:0] sine_onecycle_amp1,sine_onecycle_amp2;
assign sin_out = enable ? (step < 24'd732 ? (sine_onecycle_amp1+sine_onecycle_amp2)>>1 : (sine_onecycle_amp1*3+sine_onecycle_amp2)>>2 ): 0 ;
// assign sin_out = enable ? sine_onecycle_amp1: 0 ;
assign sel1 = phase1[N-1:N-2];
assign sel2 = phase2[N-1:N-2];
sin_table u_sin_table1(address1,sine_table_out1);
sin_table u_sin_table2(address2,sine_table_out2);
always @(posedge clk) begin
phase1 <= phase1 + step;
phase2 <= phase2 + step;
end
always @(sel1 or sine_table_out1)
begin
case(sel1)
2'b00: begin
sine_onecycle_amp1[9:0] = 9'h1ff + sine_table_out1[8:0];
address1 = phase1[N-3:N-8];
end
2'b01: begin
sine_onecycle_amp1[9:0] = 9'h1ff + sine_table_out1[8:0];
address1 = ~phase1[N-3:N-8];
end
2'b10: begin
sine_onecycle_amp1[9:0] = 9'h1ff - sine_table_out1[8:0];
address1 = phase1[N-3:N-8];
end
2'b11: begin
sine_onecycle_amp1[9:0] = 9'h1ff - sine_table_out1[8:0];
address1 = ~ phase1[N-3:N-8];
end
endcase
end
always @(sel2 or sine_table_out2)
begin
case(sel2)
2'b00: begin
sine_onecycle_amp2[9:0] = 9'h1ff + sine_table_out2[8:0];
address2 = phase2[N-3:N-8];
end
2'b01: begin
sine_onecycle_amp2[9:0] = 9'h1ff + sine_table_out2[8:0];
address2 = ~phase2[N-3:N-8];
end
2'b10: begin
sine_onecycle_amp2[9:0] = 9'h1ff - sine_table_out2[8:0];
address2 = phase2[N-3:N-8];
end
2'b11: begin
sine_onecycle_amp2[9:0] = 9'h1ff - sine_table_out2[8:0];
address2 = ~ phase2[N-3:N-8];
end
endcase
end
endmodule
(5)扬声器的自动播放以及模拟电子琴:根据输入的按键信息选择使用自动播放音乐的参数或是使用外界键盘信息。如果使用外接键盘信息的话,将外接键盘信息作为使能信号,来对各个音符最终输出数字量进行求和线性映射到PWM的占空比上。经滤波电路后得到了所需要的声音信号。
assign Csign = (24'd5856 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign C_sign = (24'd6208 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign Dsign = (24'd6576 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign D_sign = (24'd6960 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign Esign = (24'd7376 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign Fsign = (24'd7808 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign F_sign = (24'd8272 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign Gsign = (24'd8768 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign G_sign = (24'd9296 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign Asign = (24'd9840 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign A_sign = (24'd10432 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign Bsign = (24'd11040 >> ((play_cnt && sel)?tone_level_decode:chose_cnt)) ;
assign CCsign = Csign<<1 ;
DDS_sin Ctone(.clk(clk),.step(Csign),.sin_out(CtoneOut[9:0]),.enable((play_cnt && sel)? tone_decode[0]:~key[0] ));
DDS_sin C_tone(.clk(clk),.step(C_sign),.sin_out(C_toneOut[9:0]),.enable((play_cnt && sel)? tone_decode[1]:~key[1] ));
DDS_sin Dtone(.clk(clk),.step(Dsign),.sin_out(DtoneOut[9:0]),.enable((play_cnt && sel)? tone_decode[2]:~key[2] ));
DDS_sin D_tone(.clk(clk),.step(D_sign),.sin_out(D_toneOut[9:0]),.enable((play_cnt && sel)? tone_decode[3]:~key[3] ));
DDS_sin Etone(.clk(clk),.step(Esign),.sin_out(EtoneOut[9:0]),.enable((play_cnt && sel)? tone_decode[4]:~key[4] ));
DDS_sin Ftone(.clk(clk),.step(Fsign),.sin_out(FtoneOut[9:0]),.enable((play_cnt && sel)? tone_decode[5]:~key[5] ));
DDS_sin F_tone(.clk(clk),.step(F_sign),.sin_out(F_toneOut[9:0]),.enable((play_cnt && sel)? tone_decode[6]:~key[6] ));
DDS_sin Gtone(.clk(clk),.step(Gsign),.sin_out(GtoneOut[9:0]),.enable((play_cnt && sel)? tone_decode[7]:~key[7] ));
DDS_sin G_tone(.clk(clk),.step(G_sign),.sin_out(G_toneOut[9:0]),.enable((play_cnt && sel)? tone_decode[8]:~key[8] ));
DDS_sin Atone(.clk(clk),.step(Asign),.sin_out(AtoneOut[9:0]),.enable((play_cnt && sel)? tone_decode[9]:~key[9] ));
DDS_sin A_tone(.clk(clk),.step(A_sign),.sin_out(A_toneOut[9:0]),.enable((play_cnt && sel)? tone_decode[10]:~key[10] ));
DDS_sin Btone(.clk(clk),.step(Bsign),.sin_out(BtoneOut[9:0]),.enable((play_cnt && sel)? tone_decode[11]:~key[11] ));
DDS_sin CCtone(.clk(clk),.step(CCsign),.sin_out(CCtoneOut[9:0]),.enable((play_cnt && sel)? tone_decode[12]:~key[12] ));
wire[11:0] ToneOut = CtoneOut + C_toneOut + DtoneOut + D_toneOut + EtoneOut + FtoneOut + F_toneOut + GtoneOut + G_toneOut + AtoneOut + A_toneOut + BtoneOut + CCtoneOut;
wire[31:0] outamp,num ;
assign outamp = (sel) ? (ToneOut>>4) : 0 ;
assign num = (sel) ? 32'h1f : 1 ;
wire clk120;
PLL120 mypll(.CLKI(clk),.CLKOP(clk120));
PWM myampPWM (.clk(clk120),.rst_n(rst_n),.num(32'hFF),.compare(outamp),.pwm_out(amp));
六、遇到的主要难题和解决方法
(1)驱动扬声器发声时噪声较多,谐波较多。经过理论分析和群友讨论,可能是再驱动扬声器时使用的PWM载波频率仅比截止频率高10倍多,对于谐波的抑制还是不够,所以需要更进一步的提高PWM载波的频率。
(2)将扬声器的PWM载波频率提高了很多,但仍存在比较强的杂音和噪声,但是谐波减少了,经群友讨论可能是因为时钟频率有限,而PWM的频率升高导致的占空比分辨率降低了,进而使得正弦波的分辨率降低,解决方法可以修改PWM的生成方式或者是提高时钟频率。
我采用的是后者,使用了PLL的IP将时钟频率提高了十倍,在保持了较高的PWM载波频率的同时增加了3位分辨率,最终获得了比较纯粹的单频声音。
(3)对于音乐自动播放时存储乐谱信息的编码格式不是很确定。经过网上查阅资料,学习了基本的声乐知识,使用了MusicEncode软件完成了对《最伟大的作品》乐谱的编码。
(4)使用case语句时()括号内参数与下面枚举的位宽不匹配,经仔细排查后修改了错误,正确运行了。
(5)蜂鸣器使用时,普通PWM在频率突然切换时,cnt未清理导致了逻辑错误,电子琴卡死。
增加了频率切换时计数器自动清零的逻辑,避免了可能存在的计数器溢出现象。
七、改进建议
1. 感觉本次项目的电子琴拓展板按键手感不太好,每个琴键如果可以独自活动而不是整体用一块板子的话,效果会更好。
2. 拓展板的硬件部分存在一定质量问题,在下有一位朋友的蜂鸣器和扬声器切换的开关没多久就坏了,我的有一个琴键表现也有一些异常。
八、附:FPGA资源使用情况
Number of registers: 897 out of 4635 (19%)
PFU registers: 897 out of 4320 (21%)
PIO registers: 0 out of 315 (0%)
Number of SLICEs: 1919 out of 2160 (89%)
SLICEs as Logic/ROM: 1919 out of 2160 (89%)
SLICEs as RAM: 0 out of 1620 (0%)
SLICEs as Carry: 925 out of 2160 (43%)
Number of LUT4s: 3838 out of 4320 (89%)
Number used as logic LUTs: 1988
Number used as distributed RAM: 0
Number used as ripple logic: 1850
Number used as shift registers: 0
Number of PIO sites used: 24 + 4(JTAG) out of 105 (27%)
Number of block RAMs: 4 out of 10 (40%)
Number of GSRs: 1 out of 1 (100%)
EFB used : No
JTAG used : No
Readback used : No
Oscillator used : No
Startup used : No
POR : On
Bandgap : On
Number of Power Controller: 0 out of 1 (0%)
Number of Dynamic Bank Controller (BCINRD): 0 out of 6 (0%)
Number of Dynamic Bank Controller (BCLVDSO): 0 out of 1 (0%)
Number of DCCA: 0 out of 8 (0%)
Number of DCMA: 0 out of 2 (0%)
Number of PLLs: 1 out of 2 (50%)
Number of DQSDLLs: 0 out of 2 (0%)
Number of CLKDIVC: 0 out of 4 (0%)
Number of ECLKSYNCA: 0 out of 4 (0%)
Number of ECLKBRIDGECS: 0 out of 2 (0%)