蜂鸣器音乐发生器实验报告
蜂鸣器音乐发生器实验报告
一、实验目的
(1)学习用数控分频器设计蜂鸣器音乐发生电路。
(2)了解乐谱的基本知识,可以将乐谱转换为Quartus II 文件,掌握其演奏的原理。
(3)掌握设计中各模块的功能,能够填入并演奏新的曲子。
二、实验设备与器件
Quartus II 软件、EP2C8Q208C8实验箱
三、实验方案设计
1. 实验可实现的功能
(1)蜂鸣器可以演奏四首音乐,四首音乐通过两个拨码开关控制,可以随意更改想听的曲目。
(2)在播放音乐的同时,用一位数码管显示当前音乐的简谱,并且用两个发光二极管显示高、中、低不同的音调。
(3)在用拨码开关选择曲目的同时,可以在LCD1602液晶屏上看到当前音乐的名称。
2. 音频方案设计
蜂鸣器音乐发生器的基本原理:组成乐曲的每个音调的频率值以及音长所延续的时间是乐曲能够连续演奏的两个基本数据,所以只要控制输出到蜂鸣器的时钟信号频率的高低和持续的时间,就可以使蜂鸣器发出连续的乐曲声。 (1)音调频率值的控制
简谱中音调与音频的对应关系如表3.2.1所示,表中的低、中、高音的频率遵循二倍规则,就是说中音1是低音1频率的2倍,高音1是中音1频率的2倍,以此类推。
已知低音的频率,可以通过如下的MATLAB 程序计算出中、高音的频率,并且可以得出各音调的分频值与频率预直数,其中预置数是用11位计数器来表示的。
计算中、高音及各音调分频值与频率预置数的MATLAB 程序:
clc;
f=50000000; %50MHz
bilv=2^(1/12); %相邻音调频率之间的比率 a(6)=440.0; %低音6的频率为440Hz a(7)=a(6)*bilv*bilv; %低音7的频率 a(5)=a(6)/bilv/bilv; %低音5的频率 a(4)=a(5)/bilv/bilv; %低音4的频率 a(3)=a(4)/bilv; %低音3的频率 a(2)=a(3)/bilv/bilv; %低音2的频率 a(1)=a(2)/bilv/bilv; %低音1的频率 b=a*2; %中音的频率 c=b*2; %高音的频率
counter=2^11; %分频值对应的位数为11位 f=f/50/2; %50MHz,50分频,再2分频 for i=1:7
zhia(i)=counter-f/a(i); %低音的分频预置数 zhib(i)=counter-f/b(i); %中音的分频预置数 zhic(i)=counter-f/c(i); %高音的分频预置数 end
音调、分频值及频率预置数的表格如下:
(2)音调持续时间的控制
音乐中的银除了有高低音之分外,还有长短之分。简谱中用一条横线“—”在音符的右面或者下面来标注音的长短。表3.2.2列出了常用音符及其长度标记。
从表3.2.2可以看出横线有记在音符后面的,也有记在音符下面的,横线标记的位置不同,被标记音符的时值也不同。要使音符时值延长,在四分音符右边加横线“—”,这时的横线叫做延时线。延时线越多,音调持续的时间就越长。 音乐中除了有音的高低、长短之外,也有音的休止。表示音的休止的符号叫休止符,用“0”标记。每增加一个0,就增加一个四分休止符的时值。
3. 系统工作原理
本系统共分为4个模块组成,其中Songer.v 是顶层设计文件,其内部有4个功能模块:jianpu.v 、zhuanhuan.v 、fenpin.v 、lcd1602.v ,功能模块如图3.3.1所示。
图3.3.1 系统功能模块电路
(1)音符的频率可以由图3.3.1中的模块fenpin 获得。这是一个数控分频器,由其clk 端输入50MHz 的时钟信号,通过fenpin 分频之后由spkout 输出。fenpin 对clk 输入信号的分频比由11位预置数tone 决定。Spkout 的输出频率将决定每一音符的音调。这样,分频预置数tone 与spkout 的输出频率就对应起来了。 (2)音符的持续时间是根据乐曲的速度及每个音符的节拍数来确定的,图3.3.1中模块zhuanhuan 的功能首先是为fenpin 提供决定所发音符分频预置数,而此数在fenpin 输入口停留的时间即为此音符的节拍值。模块zhuanhuan 是乐曲简谱与相应的分频预置数之间的转换电路,其中设置了高、中、低音全部音符所对应的分频预置数,共21个,每一音符的停留时间由音乐节拍和音调发生器模块jianpu 的clk 的输入频率决定,这里为4Hz 。这21个值的输入由对应于zhuanhuan 的5位输入值index 确定,而index 最多有32种可选值。Toneindex 输向zhuanhuan 中的index ,其值与持续时间由模块jianpu 决定。
(3)在jianpu 中设置了,一个9位二进制计数器,作为简谱数据ROM 的地址指针。这个计数器的计数频率选为4Hz ,即每一计数值的停留时间为0.25s ,恰为当全音符设为1s 时,四四拍的4分音符的持续时间。ROM 中的低音用0~7表示,中音在低音的基础上再加7,用8~14表示,同理高音用15~21表示。当jianpu 中的计数器按4Hz 的时钟速率做加法计数时,ROM 中的简谱通过toneindex 端口输向zhuanhuan 模块,乐曲就可以连续的演奏了。
此外还设置了一个两位的乐曲选择端sel ,当sel 为00时,可演奏高、中、低音的循环演奏;当sel 为01时,演奏《两只老虎》;当sel 为10时,演奏《天空之城》;当sel 为11时,演奏《快点告诉你》。 (4)在lcd1602模块中设置了歌曲名称显示功能,由sel 作为选择端口,当sel 为00时,可以在lcd1602液晶显示屏的第一行显示“gao zhong di yin”;当sel 为01时,显示“liang zhi lao hu”;当sel 为10时,显示“tiankongzhicheng ”;当sel 为11时,显示“kuaidiangaosuni ”。
4. 各模块程序设计
(1)蜂鸣器音乐发生器顶层程序设计
module Songer(song_sel,clk,ledpos,ledneg,spkout,sms,smb,rst_n,lcd_data,lcd_e, lcd_rs,lcd_rw,SEL0,SEL1,SEL2);
input [1:0] song_sel; //对四首乐曲进行选择 input clk; //音调频率信号 input rst_n; //节拍频率信号 output [1:0] ledpos; //发光二极管的阳极 output [3:0] ledneg; //发光二极管的阴极 output [7:0] sms; //数码管的段选 output [7:0] smb; //数码管的位选 output spkout; //蜂鸣器输出 output [7:0] lcd_data; //数据总线 output lcd_e; //使能信号
output lcd_rs; //指令、数据选择 output lcd_rw; //读、写选择
output SEL0; //LCD1602读写选择 output SEL1; // LCD1602读写选择 output SEL2; // LCD1602读写选择
wire [10:0] tone; //分频预置数--跟音调相匹 wire [4:0] toneindex; //音符 reg clk4hz;
reg [24:0] cnt1; //计数器 reg [24:0] cnt2; //计数器
always @ (posedge clk) begin if(cnt2>=25'b[***********]100000) begin clk4hz=~clk4hz; //对50MHz 进行4Hz 分频 cnt2
jianpu u1(.sel(song_sel),.clk(clk4hz),.toneindex(toneindex));
zhuanhuan u2(.index(toneindex),.tone(tone),.ledpos(ledpos),.ledneg(ledneg), .sms(sms),.smb(smb));
fenpin u3(.clk(clk),.tone(tone),.spks(spkout));
lcd1602 u4(.clk(clk),.rst_n(rst_n),.lcd_data(lcd_data),.lcd_e(lcd_e),
.lcd_rs(lcd_rs),.lcd_rw(lcd_rw),.SEL0(SEL0),.SEL1(SEL1),.SEL2(SEL2),.sel(song_sel));
endmodule
(2)fenpin模块程序设计
module fenpin(clk,tone,spks); input clk;
input [10:0] tone; //分频预置数--跟音调相匹配 output reg spks; //声音输出 reg preclk,fullspks; always@(posedge clk) begin reg [3:0] count4;
preclk49) begin preclk
always@(posedge preclk) begin reg [10:0] count11; //11位可预置计数器 if(count11==11'h7FF) begin count11=tone; fullspks
always@(posedge fullspks) begin reg count2; count2=~count2; if(count2==1) spks
扬声器有足够功率发音 else spks
(3)zhuanhuan模块程序设计
module zhuanhuan(index,sms,smb,ledpos,ledneg,tone); input [4:0] index; //音符
output reg [7:0] sms; //数码管段选,用于显示简谱 output reg [7:0] smb; //数码管位选,用于显示简谱
output reg [1:0] ledpos; //发光二极管阳极,用于区分低、中、 高音
output reg [3:0] ledneg; //发光二极管阴极,用于区分低、中、
高音
output reg [10:0] tone; //分频预置数--跟音调相匹配 reg [3:0] code; always@(index) begin case(index) //将简谱音符与分频预置数相匹配 5'b00000: tone
5'b10011: tone
always@(index) begin reg [4:0] temp; if(index>=15) begin temp=8) begin temp
always@(code) begin case(code) //八段译码程序 4'd0:sms=8'hC0; 4'd1:sms=8'hF9; 4'd2:sms=8'hA4; 4'd3:sms=8'hB0; 4'd4:sms=8'h99; 4'd5:sms=8'h92; 4'd6:sms=8'h82; 4'd7:sms=8'hF8; default:sms=7'hC0; endcase
end always@(*) begin smb=8'b10000000; //选通第八位数码管 ledneg=4'b0111; //选通二极管 end
endmodule
(4)jianpu模块程序设计
module jianpu(sel,clk,toneindex);
input [1:0] sel; //乐曲选择信号 input clk;
output reg [4:0] toneindex; //乐曲中的音符输出 wire [4:0] toneindex1,toneindex2,toneindex3,toneindex4;
reg [9:0] counter; //计数器,用于读取ROM 中的简 谱 always@(posedge clk) begin if(sel==2'b00) begin if(counter>=88) counter=128) counter=190) counter
else if(sel==2'b11) begin if(counter>=416) counter
music u1(.address(counter),.q(toneindex1),.clock(clk)); laohu u2(.address(counter),.q(toneindex2),.clock(clk)); tiankong u3(.address(counter),.q(toneindex3),.clock(clk)); gaosuni u4(.address(counter),.q(toneindex4),.clock(clk)); always@(*) case(sel) 2'b00:toneindex=toneindex1; //选择第一首歌--低、中、高音依 次循环 2'b01:toneindex=toneindex2; //选择第二首歌--两只老虎 2'b10:toneindex=toneindex3; //选择第三首歌--天空之城 2'b11:toneindex=toneindex4; //选择第四首歌--快点告诉你 default:; endcase endmodule
(5)lcd1602模块程序设计
module lcd1602(clk,rst_n,lcd_data,lcd_e,lcd_rs,lcd_rw,SEL0,SEL1,SEL2,sel); input clk; // 50MHz时钟 input rst_n; // 复位信号 input [1:0] sel;
output reg [ 7:0] lcd_data; // 数据总线 output lcd_e; // 使能信号 output reg lcd_rs; // 指令、数据选择 output lcd_rw; // 读、写选择 output SEL0; // LCD1602读写选择 output SEL1; // LCD1602读写选择 output SEL2; // LCD1602读写选择
reg [127:0] row1_val ; always@(sel) begin
case(sel) 2'b00: row1_val="gao zhong di yin";
2'b01: row1_val="liang zhi lao hu";
2'b10: row1_val="tiankongzhicheng";
2'b11: row1_val="kuaidiangaosuni ";
default:;
endcase
end
assign SEL0 = 1'b0;
assign SEL1 = 1'b0;
assign SEL2 = 1'b1;
// 分频模块 开始
reg [15:0] cnt;
always @ (posedge clk, negedge rst_n)
if (!rst_n)
cnt
else
cnt
wire lcd_clk = cnt[15];
// 分频模块 结束
// LCD1602驱动模块 开始
parameter IDLE = 8'h00;
// 写指令,初始化
parameter DISP_SET = 8'h01;
parameter DISP_OFF = 8'h03;
parameter CLR_SCR = 8'h02;
parameter CURSOR_SET1 = 8'h06;
parameter CURSOR_SET2 = 8'h07;
// 显示第一行
parameter ROW1_ADDR = 8'h05;
parameter ROW1_0 = 8'h04;
parameter ROW1_1 = 8'h0C;
parameter ROW1_2 = 8'h0D;
parameter ROW1_3 = 8'h0F;
parameter ROW1_4 = 8'h0E;
parameter ROW1_5 = 8'h0A;
parameter ROW1_6 = 8'h0B;
parameter ROW1_7 = 8'h09;
parameter ROW1_8 = 8'h08;
parameter ROW1_9 = 8'h18;
parameter ROW1_A = 8'h19;
parameter ROW1_B = 8'h1B;
parameter ROW1_C = 8'h1A;
// 计数子 // 显示模式设置 // 显示关闭 // 显示清屏 // 显示光标移动设置 // 显示开及光标设置 // 写第1行起始地址
parameter ROW1_D = 8'h1E;
parameter ROW1_E = 8'h1F;
parameter ROW1_F = 8'h1D;
// 显示第二行
reg [5:0] current_state, next_state; // 现态、次态
// FSM: always1
always @ (posedge lcd_clk, negedge rst_n)
if(!rst_n) current_state
else current_state
// FSM: always2
always
begin
case(current_state)
IDLE : next_state = DISP_SET;
// 写指令,初始化
DISP_SET : next_state = DISP_OFF;
DISP_OFF : next_state = CLR_SCR;
CLR_SCR : next_state = CURSOR_SET1;
CURSOR_SET1 : next_state = CURSOR_SET2;
CURSOR_SET2 : next_state = ROW1_ADDR;
// 显示第一行
ROW1_ADDR : next_state = ROW1_0;
ROW1_0 : next_state = ROW1_1;
ROW1_1 : next_state = ROW1_2;
ROW1_2 : next_state = ROW1_3;
ROW1_3 : next_state = ROW1_4;
ROW1_4 : next_state = ROW1_5;
ROW1_5 : next_state = ROW1_6;
ROW1_6 : next_state = ROW1_7;
ROW1_7 : next_state = ROW1_8;
ROW1_8 : next_state = ROW1_9;
ROW1_9 : next_state = ROW1_A;
ROW1_A : next_state = ROW1_B;
ROW1_B : next_state = ROW1_C;
ROW1_C : next_state = ROW1_D;
ROW1_D : next_state = ROW1_E;
ROW1_E : next_state = ROW1_F;
ROW1_F : next_state = ROW1_ADDR;
default : next_state = IDLE ;
endcase
end
// FSM: always3
always @ (posedge lcd_clk, negedge rst_n)
begin
if(!rst_n)
begin
lcd_rs
lcd_data
end
else
begin
// 写lcd_rs
case(next_state)
IDLE : lcd_rs
// 写指令,初始化
DISP_SET : lcd_rs
DISP_OFF : lcd_rs
CLR_SCR : lcd_rs
CURSOR_SET1 : lcd_rs
CURSOR_SET2 : lcd_rs
// 写数据,显示第一行
ROW1_ADDR : lcd_rs
ROW1_0 : lcd_rs
ROW1_1 : lcd_rs
ROW1_2 : lcd_rs
ROW1_3 : lcd_rs
ROW1_4 : lcd_rs
ROW1_5 : lcd_rs
ROW1_6 : lcd_rs
ROW1_7 : lcd_rs
ROW1_8 : lcd_rs
ROW1_9 : lcd_rs
ROW1_A : lcd_rs
ROW1_B : lcd_rs
ROW1_C : lcd_rs
ROW1_D : lcd_rs
ROW1_E : lcd_rs
ROW1_F : lcd_rs
endcase
// 写lcd_data
case(next_state)
IDLE : lcd_data
// 写指令,初始化
DISP_SET : lcd_data
DISP_OFF : lcd_data
CLR_SCR : lcd_data
CURSOR_SET1 : lcd_data
CURSOR_SET2 : lcd_data
// 写数据,显示第一行
ROW1_ADDR : lcd_data
ROW1_0 : lcd_data
ROW1_1 : lcd_data
ROW1_2 : lcd_data
ROW1_3 : lcd_data
ROW1_4 : lcd_data
ROW1_5 : lcd_data
ROW1_6 : lcd_data
ROW1_7 : lcd_data
ROW1_8 : lcd_data
ROW1_9 : lcd_data
ROW1_A : lcd_data
ROW1_B : lcd_data
ROW1_C : lcd_data
ROW1_D : lcd_data
ROW1_E : lcd_data
ROW1_F : lcd_data
endcase
end
end
assign lcd_e = lcd_clk; // 数据在时钟高电平被锁存 assign lcd_rw = 1'b0; // 只写
endmodule
(6)ROM文件设计
①低、中、高音ROM 文件 music.mif 如图3.4.1所示:
图3.4.1 高、中、低音演奏简谱
②两只老虎ROM 文件laohu.mif 如图3.4.2所示:
图3.4.2 两只老虎简谱
③天空之城ROM 文件tiankong.mif 如图3.4.3所示:
图3.4.3 天空之城简谱
④快点告诉你ROM 文件gaosuni.mif 如图3.4.4所示:
图3.4.4 快点告诉你简谱
5. 下载电路及引脚分配设计
设计用拨码开关BM8和BM7作为歌曲选择端口,决定四首歌的播放;用实验箱自带的50MHz 时钟信号作为clk 输入;用发光二极管LED3和LED1作为高、中、低音的指示显示,高音时LED3红灯亮,中音时LED1绿灯亮,低音时两个都不亮;将发音输出spkout 接蜂鸣器;并用lcd1602显示歌曲名称。各引脚具体分配方案如图3.5.1所示。
图3.5.1 引脚分配
四、实验仿真及结论
1. 部分模块波形仿真
(1)简谱中的ROM 文件以music.mif 为例,仿真结果如图4.1.1所示:
图4.1.1 ROM文件仿真波形
通过仿真波形,可以看到music.mif 的88个地址中存放了0~21这22个简谱码,并且每个音符占了四个地址,每读一个地址所用时间是四分之一拍所用的时间,也就是说每个音符持续的时间为一拍,这与ROM 文件的设置是一致的。
(2)zhuanhuan.v 文件中的功能仿真结果如图4.1.2所示:
图4.1.2 功能仿真波形
通过图4.1.2可以看到,由index 所表示的0~21的简谱码,前七个音符为低音,所以ledpos 为[0],此时红绿两个发光二极管都不亮,中间七个音符为中音,ledpos 为[1],此时绿灯亮,后七个音符为高音,ledpos 为[2],此时红灯亮,每个音符所对应的八段数码管段选信号及分频预置数与设计完全相符。
(3)lcd1602.v 文件中的显示结果仿真如图4.1.3(a)所示:
图4.1.3(a) lcd1602显示结果仿真波形
通过仿真波形可以看到,当复位信号rst_n为低电平时不工作,当为高电平时,液晶lcd1602的选通端口SEL0、SEL1、SEL2正常工作,并且使能信号lcd_e、指令数据选择端口lcd_rs、读写选择端口lcd_rw都正常工作。此外,当歌曲选择端口sel 为00时,显示应为“gao zhong di yin”,sel 为01时显示“liang zhi lao hu”,sel 为10时显示“tian kong zhi cheng”,sel 为11时显示“kuaidiangaosuni ”,与标准字符库作对比,可以看到仿真得到的结果是正确的。
标准字符库如图4.1.3(b)所示:
图4.1.3(b) 标准字符库
2. 实验结论
通过实验及仿真基本实现了程序设计的功能:蜂鸣器可以连续的演奏完整的音乐,经拨码开关可任意选择四首音乐中的一首,简谱可以在数码管上准确显示,其高中低音可由两个发光二极管正确显示,当前播放音乐的名称可由lcd1602显示出来,说明程序设计符合要求,实验目的基本达到。
五、实验总结与体会
1. 实验总结
通过这次实验,我又复习了前面所学的数控分频的知识,学习了用ROM 文件设计编程的方法,同时还学到一些音乐常识,总体来说我还是收益颇丰的。虽然这个实验做得很顺利,但是在编程和下载时总会遇到一些问题,那么我就主要说一说这些问题吧。
决定做蜂鸣器音乐发生器实验后,我找过一些现成的资料,不过我看到这些程序大多是只能演奏一首歌曲,并且全无其它功能,既然要想做得好必然的在原程序的基础上改进。首先我想到的就是实现多首歌曲可选的功能,实现这样的功能如果还用资料里那些程序写法,就有点太繁杂了,所以我想到了点阵课上老师讲到的ROM 文件的用处,所以我又找了一些应用了ROM 文件资料,按照它们的模式改编成了现在的存放简谱的文件,用地址指针一一读取其中的音符就可以了,并且可以做到歌曲的选择功能。在此基础上我又加入了,简谱显示、高低音显示以及歌曲名称显示。为了结构清晰,我将内部的程序流程例化为四个子模块,增强了程序的可读性。
在实验课上下载程序时,发现四首音乐中的两首音调很不明确,自己查找问题无果后,请教同学才知道我写进ROM 文件中的简谱有一些问题。经过同学的悉心讲解及对简谱的修正,重新下载后听到的音乐果然悦耳多了,这就是我上面提到所学到一点音乐常识。
2. 实验体会
我觉得这次综合实验是我收获最大的一次实验,从设计程序到一步一步实现它的功能都是我亲力亲为的,当然这次试验不可能是完美无缺的,但是能够实现这么多功能,我还是很欣慰的,这让我对FPGA 的学习产生了浓厚的兴趣。记得在上个学期做最后一大实验的时候,老师很是惊讶于我用纯粹的图形法来搭建实现自动售票机的功能,当时就知道了用Verilog HDL语言编程会简单很多,现在我终于用语言实现了一个蜂鸣器的音乐演奏功能,我感觉很自豪。
总之,通过这次实验我又学到了很多知识,确实是受益良多。