基于小脚丫FPGA的电子琴设计
南京邮电大学21级
本科生微电子科学与工程专业
吴婕
- 工作原理
- 使用蜂鸣器、扬声器进行琴键的播放音乐或支持弹奏电子琴
- 使用pwm波支持蜂鸣器的发声
- 使用钢琴音色录入与一位DAC实现扬声器的发声
- 框图
三、蜂鸣器与模拟喇叭的差别
-
蜂鸣器是产生声音的信号装置,有机械型、机电型及压电型。蜂鸣器的典型应用包括警笛、报警装置、火灾警报器、防空警报器、防盗器、定时器等。常见的电子产品中使用的小型蜂鸣器都是压电式蜂鸣器,主要依靠压电效应来产生振动,发出声音。 此类蜂鸣器一般分为自带震荡源或不带震荡源两种。驱动自带震荡源的蜂鸣器时直接使用直流电即可;而驱动不带震荡源的蜂鸣器则需要使用交流信号。、
- 喇叭,也称扬声器、音箱、扩音器,是将电子信号转换成为声音的换能器、电子组件,可以由一个或多个组成音响组。扬声器是由磁铁、线圈、喇叭振膜组成。扬声器把电流频率转化为声音。物理学原理,当电流通过线圈产生电磁场,磁场的方向为右手法则。假设,扬声器播放C调,其频率为256Hz,即每秒振动256次,扬声器输出256Hz的交流电,每秒256次电流改变,发出C当电线圈与扬声器薄膜一起振动,推动周围的空气振动,扬声器由此产生声音。人耳可以听到的声波的频率一般在20赫兹至20000赫兹之间,所以一般的扬声器都会把程序设定在这个范围内。工作原理和上述相同。能量的转换过程是由电能转换为磁能,再由磁能转换为机械能,再从机械能转换为声音。
四、用蜂鸣器和模拟喇叭的实现方法差别以及音效差别分析
-
蜂鸣器实现方法和音效
驱动不带震荡源的无源蜂鸣器则需要使用交流信号,通常采用方波信号。对于数字电路来说,可以采用计数器产生不同频率的方波,来实现不同音符的输出。由于方波的信号特点,发声中带有丰富的杂波信号,音效较差,音色单一。
-
模拟喇叭实现方法和音效
扬声器一般需要模拟电流驱动,响应带宽比蜂鸣器大非常多。通常可采用DAC+放大电路或者集成的音频芯片驱动。本文中是由FPGA的IO口产生PWM信号,使用PWM-DAC技术经过RC低通滤波器,滤除pwm尖峰,再经过调幅变阻器、AC耦合电容到放大电路,转换为较为平滑的低频信号来驱动扬声器。音效比蜂鸣器好,可以表现出模拟信号的细节,本例中改变谐波的种类及含量可以发出不同音色的音符。
五、模拟放大电路的仿真及分析
先经过差分放大器进行音频信号的分离,再经过放大器进行音量的放大
五、主要代码片段及说明
- 接受按键信号模块(Key_board)
module KeyBoard(
input clk,
input rst_n,
input [12:0] col,
output [12:0] key_out
);
wire clk_200hz;
reg [12:0]key_out_reg;
clk_maker u15(
.clk(clk),
.rst_n(rst_n),
.CNT(32'd60000),
.clk_changed(clk_200hz)
);
reg [12:0] key_r;
reg [12:0] key;
always@(posedge clk_200hz or negedge rst_n)
begin
if(!rst_n)
key_out_reg<=13'b1_1111_1111_1111;
else
begin
key<=col;
key_r<=key;
key_out_reg<=key_r|key;
end
end
//2-----------------------------------------------------------------
//2--------------------独立按键扫描---------------------------------- -
assign key_out=key_out_reg;
endmodule
实现键盘信号的接受和处理,并且防消抖
- 根据键盘信号进行频率选择
module fre(
input clk,
input rst_n,
input [12:0] key_out,
input tone_up,
input tone_down,
output reg [12:0] frequence1,
output reg [15:0] time_end1,
output reg [12:0] frequence2,
output reg [15:0] time_end2
);
reg [1:0] pitch;
wire tone_up_pulse;
wire tone_down_pulse;
debounce u17(
.clk(clk),
.rst_n(rst_n),
.key(tone_up),
.key_pulse(tone_up_pulse)
);
debounce u18(
.clk(clk),
.rst_n(rst_n),
.key(tone_down),
.key_pulse(tone_down_pulse)
);
always@(posedge clk or negedge rst_n) begin //时间控制,上一首或者下一首切换歌曲(手动或者自动)
if(!rst_n) begin
pitch=2'd1; end
else if(tone_up_pulse) begin
case(pitch)
2'd0: begin pitch<=2'd1; end
2'd1: begin pitch <=2'd2;end
2'd2: begin pitch <= 2'd2; end
2'd3: begin pitch<=2'd1;end
default:begin pitch<=pitch; end
endcase
end
else if(tone_down_pulse) begin//按键消抖后会输出一个相反的脉冲,不用!chose_up,下面chose_down同理
case(pitch)
2'd0: begin pitch<=2'd0; end
2'd1: begin pitch <= 2'd0; end
2'd2: begin pitch <= 2'd1; end
2'd3: begin pitch<=2'd1;end
default:begin pitch <= pitch;end
endcase
end
end
always@(key_out or pitch)
begin
case(pitch)
2'b1:
case(key_out[12:0])
13'b0_1111_1111_1111: begin frequence1 <= 13'd523; frequence2<=0; time_end1<=16'd11472; time_end2<=0;end//M1,
13'b1_0111_1111_1111: begin frequence1 <= 13'd554; frequence2<=0; time_end1<=16'd10832; time_end2<=0;end//M1#,
…………
default:
begin
if(key_out[0]==0) begin frequence1 <= 13'd523; time_end1<=16'd11472;end
else if(key_out[1]==0) begin frequence1 <= 13'd554; time_end1<=16'd10832;end
…………(从左往右扫)
if(key_out[12]==0) begin frequence2 <= 13'd1047; time_end2<=16'd5730; end
else if(key_out[11]==0) begin frequence2 <= 13'd988; time_end2<=16'd6079; end
………………(从右往左扫)
endcase
2'd2:
case(key_out[12:0])
13'b0_1111_1111_1111: begin frequence1 <= 13'd1047; frequence2<=0; time_end1<=16'd5730; time_end2<=0;end//H1,
……
……
(就像这样扩展音域)
end
endmodule
- 制造较慢的时钟信号消抖(使用CNT使能产生的信号的频率不固定,以便制造后面播放音乐时的节奏)
module clk_maker(
input clk,
input rst_n,
input [31:0] CNT,
output clk_changed
);
//1---------------------------------------------------------------
reg [31:0] cnt;
reg clk_changed_reg;
always@(posedge clk or negedge rst_n)
begin
if(!rst_n)
begin
cnt<=16'd0;
clk_changed_reg<=1'b0;
end
else
begin
if(cnt>=((CNT>>1)-1))
begin
cnt<=16'd0;
clk_changed_reg<=~clk_changed_reg;
end
else
begin
cnt<=cnt+1'b1;
clk_changed_reg<=clk_changed_reg;
end
end
end
assign clk_changed=clk_changed_reg;
endmodule
- 钢琴波形查找表
module piano_table(
input [8:0] address,
output [7:0] piano_table_out_wire
);
reg [8:0] address_reg;
reg [8:0] piano_table_out;
always@(address or address_reg)
begin
address_reg<=address;
case(address_reg)
9'd0:piano_table_out<=9'd256;
9'd1:piano_table_out<=9'd261;
…………
9'd510:piano_table_out<=9'd245;
9'd511:piano_table_out<=9'd251;
default:piano_table_out<=9'd0;
endcase
end
assign piano_table_out_wire=piano_table_out[8:1];
endmodule
- 长相位累加器,dac1_en来传递“存在和声”的信号
module long_adder(
input clk,
input [12:0] frequence,
output [7:0] dac_data,
output data_e
);
reg [23:0] phase_acc;
reg [15:0] frequence_ver;
reg data_en;
wire [7:0] dac_data_in;
always@(frequence)
case(frequence)
13'd262:begin frequence_ver<=16'd367;data_en<=1;end
13'd277:begin frequence_ver<=16'd388;data_en<=1;end
13'd294:begin frequence_ver<=16'd412;data_en<=1;end
13'd311:begin frequence_ver<=16'd435;data_en<=1;end
…………
13'd2093:begin frequence_ver<=16'd2930;data_en<=1;end
default: begin frequence_ver<=16'd0; data_en<=0;end
endcase
assign data_e=data_en;
always@(posedge clk)
phase_acc<=phase_acc+frequence_ver;
piano_table u8(
.address(phase_acc[8:0]),
.piano_table_out_wire(dac_data_in)
);
assign dac_data=dac_data_in;
endmodule
7.扬声器,输出音乐信号或音键信号
module outspeaker(
input [7:0] dac_data2,
input [7:0] outspeaker_music,
input dac2_en,
input music_en,
input clk,
input tone_en,
output outspeaker
);
reg [7:0] dac_data2_reg;
reg [7:0] outspeaker_music_reg;
reg [7:0] outspeaker_reg;
reg [8:0] outspeaker_accumulate;
always@(posedge clk )
begin
dac_data2_reg<=dac_data2;
outspeaker_music_reg<=outspeaker_music;
if(dac2_en)
outspeaker_reg<=dac_data2_reg;
else outspeaker_reg<=outspeaker_reg;
if(!tone_en)
if(music_en)
outspeaker_reg<=outspeaker_music_reg;
else outspeaker_reg<=outspeaker_reg;
else outspeaker_reg<=0;
end
always@(posedge clk)
outspeaker_accumulate<=outspeaker_accumulate[7:0]+outspeaker_reg;
assign outspeaker=outspeaker_accumulate[8];
endmodule
- 按键消抖,产生脉冲信号
module debounce(
input clk,
input rst_n,
input key,
output key_pulse
);
reg key_rst_n_pre; //定义一个寄存器型变量存储上一个触发时的按键值
reg key_rst_n; //定义一个寄存器变量储存储当前时刻触发的按键值
wire key_edge; //检测到按键由高到低变化是产生一个高脉冲
//利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
always @(posedge clk or negedge rst_n)
begin
if (!rst_n) begin
key_rst_n <= 1'b1; //初始化时给key_rst_n赋值全为1,{}中表示N个1
key_rst_n_pre <= 1'b1;
end
else begin
key_rst_n <= key; //第一个时钟上升沿触发之后key的值赋给key_rst_n,同时key_rst_n的值赋给key_rst_n_pre
key_rst_n_pre <= key_rst_n; //非阻塞赋值。相当于经过两个时钟触发,key_rst_n存储的是当前时刻key的值,key_rst_n_pre存储的是前一个时钟的key的值
end
end
assign key_edge = key_rst_n_pre & (~key_rst_n);//脉冲边沿检测。当key检测到下降沿时,key_edge产生一个时钟周期的高电平
reg [17:0] cnt; //产生延时所用的计数器,系统时钟12MHz,要延时20ms左右时间,至少需要18位计数器
//产生20ms延时,当检测到key_edge有效是计数器清零开始计数
always @(posedge clk or negedge rst_n)
begin
if(!rst_n)
cnt <= 18'h0;
else if(key_edge)
cnt <= 18'h0;
else
cnt <= cnt + 1'h1;
end
reg key_sec_pre; //延时后检测电平寄存器变量
reg key_sec;
//延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
key_sec <= {1'b1};
else if (cnt==18'h3ffff)
key_sec <= key;
end
always @(posedge clk or negedge rst_n)
begin
if (!rst_n)
key_sec_pre <={1'b1};
else
key_sec_pre <= key_sec;
end
assign key_pulse = key_sec_pre & (~key_sec);
endmodule
- 产生PWM波信号驱动蜂鸣器
module PWM(
input clk,
input [15:0] time_end,
output pwm
);
reg [15:0] cnt;
always@(posedge clk)
if(cnt>=(time_end<<1))
cnt<=0;
else
cnt<=cnt+1'b1;
assign pwm=(time_end>cnt);
endmodule
储存并输出音乐信号
module music(
input clk,
input rst_n,
input play,
output [7:0] outspeaker_music,
output beeper_music,
output music_en
);
wire start;
wire clk_1hz;
reg playing;
reg [7:0] flag;
reg [2:0] notation [206:0];
reg [15:0] music_time_end_reg;
reg [12:0] music_frequence_reg;
wire [15:0] music_time_end;
wire [12:0] music_frequence;
wire [7:0] outspeaker_music_wire;
wire music_pwm_wire;
debounce u16_debounce(
.clk(clk),
.key(play),
.rst_n(rst_n),
.key_pulse(start)
);
clk_maker u19_clk_maker(
.clk(clk),
.rst_n(rst_n),
.CNT(100_0000),
.clk_changed(clk_1hz)
);
always@(posedge clk_1hz)
begin
notation[1]={3'd1};
notation[2]={3'd1};
notation[3]={3'd1};
notation[4]={3'd1};
notation[5]={3'd0};//输出一定时间音时也要输出较短时间空隙,不然听着不像
…………
notation[205]={3'd0};
notation[206]={3'd0};
end
always@(posedge clk or negedge rst_n)
if(!rst_n)
begin playing=0;end
else if(start)
begin playing<=1;end
else if(flag>=8'd206)
begin playing=0;end
else playing<=playing;
always@(posedge clk_1hz)
begin
if(playing==0)
flag<=0;
else flag<=flag+6'd1;
end
always@(posedge clk)
begin
case(notation[flag])
3'd1: begin music_frequence_reg<= 13'd523; music_time_end_reg<=16'd11472; end//M1,
3'd2: begin music_frequence_reg<= 13'd587; music_time_end_reg<=16'd10221; end//M2,
3'd3: begin music_frequence_reg<= 13'd659; music_time_end_reg<=16'd9104; end//M3,
3'd4: begin music_frequence_reg<= 13'd698; music_time_end_reg<=16'd8596; end//M4,
3'd5: begin music_frequence_reg<= 13'd784; music_time_end_reg<=16'd7653; end//M5,,
3'd6: begin music_frequence_reg<= 13'd880; music_time_end_reg<=16'd6818; end//M6,
3'd7: begin music_frequence_reg<= 13'd987; music_time_end_reg<=16'd6079; end//M7,
default: begin music_frequence_reg<= 13'd0; music_time_end_reg<=16'd0; end//没摁
endcase end
assign music_frequence=music_frequence_reg;
assign music_time_end=music_time_end_reg;
PWM u19_PWM(
.clk(clk),
.time_end(music_time_end),
.pwm(music_pwm_wire)
);
long_adder u20_long_adder(
.clk(clk),
.frequence(music_frequence),
.dac_data(outspeaker_music_wire)
);
assign outspeaker_music=outspeaker_music_wire;
assign beeper_music=music_pwm_wire;
assign music_en=playing;
endmodule
- 蜂鸣器,选择音乐信号或是按键信号进行播放
module beeper(
input pwm1,
input clk,
input tone_en,
input beeper_music,
input music_en,
output beeper
);
reg music_reg;
reg pwm1_reg;
reg beeper_reg;
always@(posedge clk)
begin
music_reg<=beeper_music;
pwm1_reg<=pwm1;
if( tone_en)
if(music_en)
beeper_reg<=music_reg;
else beeper_reg<=pwm1_reg;
else beeper_reg<=0;
end
assign beeper=beeper_reg;
endmodule
六、主要难题及解决办法
1.怎么样获得钢琴波形的数据
在网上查了非常非常多的资料但是没找到直接数据
解决办法:群里有个大佬推荐autopiano网站并且推荐波形基波和各次谐波比例大概为63%,21%,6%,5%,5%并按这个配出了波形
- 蜂鸣器声音太小
找了好久bug没找到,用了几种别的方式测试实验,发现我的这块板子的蜂鸣器本来就相较于扬声器偏小
- 播放音乐部分的代码找不到什么bug但是上机摁了play键还是没有声音,解决办法:观察波形图发现我的clk_maker模块里cnt的容量上限太小导致输出不了很慢的时钟信号
七、改进建议
1.多设计几个fre频率选择模块出口来实现更多的键的和声
2.建议拓展板的按键使用静音的按键减少对钢琴音播放的影响