基于小脚丫FPGA(Lattice MXO2-C)实现电子琴设计
本项目基于小脚丫FPGA(Lattice MXO2-C)实现电子琴设计,最多可以同时按下六个按键不失真,音程为三个八度,支持蜂鸣器和扬声器演奏,可以存入多首歌曲,选择播放上一曲或者下一曲
标签
FPGA
数字逻辑
DDS
2022暑假在家练
zhiwu
更新2022-09-06
南京邮电大学
1048

 

目标:

自己组装,并通过编程驱动模拟扬声器实现电子琴的功能

 

需完成的任务:

基于我们提供的套件和工具,自己组装电子琴

自己编程基于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

电子琴功能示意图

FtXs4bR_TTLPXGv4-KE7IMm2c-ys

电子琴原理图

Fr-4rCI8oSYaWjK7lUjz3dnNHf81

 

二、程序解析

模块组成

 

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信号得到波形

Fn9bdt9_B5TnIw2zJewmGnVpnxEf

向功放输入500khz信号得到波形

FvyR2w7t57oqzZBrgaT3TddO1tjh

可以看出500khz信号衰减了许多,而200khz信号幅值较大,故200khz的PWM-DAC对音质影响较大,结论得证。

 

5、资源使用报告

根据diamond的Design Summary文件可以得出以下主要信息:

项目使用了21%的寄存器,都是作为PFU寄存器使用。

使用了86%的slice,slice由两个LUT组成,故LUT也使用了86%。LUT多作为ROM,主要用在波形生成相关模块上

使用了32个端口,其中四个是JTAG所用

嵌入式块RAM(BRAM) 十个都被使用,主要用在波形合成模块上,需要多次例化

FkePW4M3Ot-u6JEXTt0gjK8xXeNN

FgZiTtrTQ36em3GkGF-df2o-ORXH

 

附件下载
piano.zip
团队介绍
朱康 南京邮电大学
团队成员
zhiwu
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号