- 功能描述
- 通过PWM产生不同的音调,并驱动板上的蜂鸣器将音调输出
- 能够播放三首不同的曲子,每首曲子的时间长度为1分钟,可以切换播放
- 曲子的切换使用小脚丫核心板上的按键,需要有按键消抖的功能
- 播放的曲子的名字在OLED屏幕上显示出来(汉字显示)
- 模块划分
- 设计思路简述
top模块:
该模块为顶层模块,为各个模块参数传递的“中转站”,包含了Music音乐模块,key_debounce按键消抖模块和OLED显示模块。其中,key_debounce为整个工程的输入模块,该模块通过消抖判断按键K1的按下次数,为顶层模块输入一个键值,顶层模块将该键值传递给Music模块和OLED模块,这两个模块通过判断键值分别控制音乐播放和字模输出。
module top(
input clk_in, //12MHz时钟
input rst_n_in, //复位键K1
input k2, //按键k2
output beep, //蜂鸣器
output oled_csn, //OLED_CS
output oled_rst, //OLED_reset
output oled_dcn, //OLED_D/C
output oled_clk, //OLED_CLK
output oled_dat //OLCD_DATA
);
wire [1:0] key_value;
wire tone_en;
Music music( //音乐模块,其中调用了Beeper模块
.clk_in(clk_in), //系统时钟
.rst_n_in(rst_n_in), //系统复位,低有效
.beep(beep), //蜂鸣器控制输出
.key_value(key_value), //输入键值
.tone_en(tone_en) //输入蜂鸣器使能
);
key_debounce filter( //按键消抖模块
.clk(clk_in),
.rst(rst_n_in),
.key(k2),
.key_value(key_value), //输出键值
.tone_en(tone_en) //输出蜂鸣器使能
);
OLED12832 OLED( //OLED显示模块
.clk_in(clk_in),
.rst_n_in(rst_n_in),
.key_value(key_value), //输入键值
.oled_csn(oled_csn),
.oled_rst(oled_rst),
.oled_dcn(oled_dcn),
.oled_clk(oled_clk),
.oled_dat(oled_dat)
);
endmodule
key_debounce模块:
该模块为按键消抖模块,本项目用到两个按键,其中k0按键作为复位按键,k1按键用于切换音乐。本模块的设计思路是定义一个delay_cnt变量,若按键没有变化,则该变量一直为0,当检测到按键变化时,delay_cnt就被赋值23'd2400000(即每次减1,当减到0为20ms),若按键在20ms内一直在变化,即一直在抖动,则计数值无法计数到1,即认为按键仍在抖动,当计数值达到1,则说明20ms内按键没有变化,即按键已经稳定,说明此时已经按下,此时键值加1(当加到3时,键值变为0)。此外,由于1分钟之后停止播放的要求,当1首音乐播放达到1分钟时,蜂鸣器的使能会关闭,此时需要在键值变化的同时将蜂鸣器的使能打开,并接着计时1分钟,以播放新一首曲子。
module key_debounce( //按键消抖模块
input clk,
input rst,
input key,
output reg [1:0] key_value, //键值,即按下几次
output reg tone_en //蜂鸣器使能
);
reg key_reg ;
reg [22:0] delay_cnt;
reg [63:0] delay_1s_cnt;
reg [7:0] delay_1m_cnt;
always @(posedge clk or negedge rst) begin
if (!rst) begin
key_reg <= 1'b1;
end
else begin
key_reg <= key; //非阻塞赋值
if (key_reg != key) //判断按键是否在变化(抖动或按下),此时key_reg还是上一个时钟沿的key_reg
delay_cnt <= 23'd2400000; //设置20ms的延时
else
if(delay_cnt > 23'd0)
delay_cnt <= delay_cnt - 1'b1;
else
delay_cnt <= 23'd0;
end
end
always @(posedge clk or negedge rst) begin
if (!rst) begin
tone_en <= 1; //使能蜂鸣器
key_value <= 1'b0;
delay_1s_cnt <= 64'd0; //重新计时
delay_1m_cnt <= 6'd0;
end
else begin
if (delay_cnt == 23'd1)begin
//若按键发生变化,则延时1分钟后关闭音乐
tone_en <= 1; //使能蜂鸣器
delay_1s_cnt <= 64'd0; //重新计时
delay_1m_cnt <= 6'd0;
key_value <= key_value + 1'b1;
if(key_value >= 3)
key_value <= 2'b00;
end
else begin //按键没有发生变化
key_value <= key_value;
//1分钟后停止播放
delay_1s_cnt <= delay_1s_cnt + 1'd1;
if(delay_1s_cnt >= 12000000) begin //计时到达1s
delay_1m_cnt <= delay_1m_cnt + 1'd1; //秒数加1
delay_1s_cnt <= 64'd0;
if(delay_1m_cnt >= 60) begin //但秒数加到60,即计时已到达1分钟
tone_en <= 1'd0; //计时到1分钟音乐停止播放
delay_1m_cnt <= 6'd0;
end
end
end
end
end
endmodule
Music模块:
该模块内部调用了电子森林提供的蜂鸣器模块,设计思路是通过延时250ms为半拍,通过将音乐的音调分别按顺序赋值给Beeper模块的tone,若该音调长度为半拍,则延时250ms后播放下一个音调(即将下一个音调赋值给tone),以此类推。当音调传入Beeper模块之后,Beeper模块通过pwm将对应频率的声音输出,从而达到播放音乐的目的。此外,该模块还通过传入的键值不同来播放不同的音乐,以达到切换音乐的目的。
module Music
(
input clk_in, //系统时钟
input rst_n_in, //系统复位,低有效
output beep, //蜂鸣器控制输出
input [1:0] key_value, //按键键值
input tone_en
);
localparam nummax1 = 6'd63; //森林狂想曲音乐的长度
localparam nummax2 = 6'd63; //东方红音乐的长度
localparam nummax3 = 7'd95; //我和我的祖国音乐的长度
reg [6:0] nummax; //当前音乐播放长度
reg [63:0] delay_cnt = 64'd0; //延时计数器
reg [4:0] mem0 = 5'd0; //蜂鸣器静止
reg [4:0] mem1 [nummax1:0]; //森林狂想曲乐码
reg [4:0] mem2 [nummax2:0]; //东方红乐码
reg [4:0] mem3 [nummax3:0]; //我和我的祖国乐码
reg [4:0] tone;
reg [6:0] num = 7'd0; //播放音乐索引
Beeper Beep
(
.clk_in(clk_in), //系统时钟
.rst_n_in(rst_n_in), //系统复位,低有效
.tone_en(tone_en), //蜂鸣器使能信号
.tone(tone), //蜂鸣器音节控制
.piano_out(beep) //蜂鸣器控制输出
);
always@(posedge clk_in or negedge rst_n_in) begin
case(key_value)
2'd1: begin nummax <= nummax1; end
2'd2: begin nummax <= nummax2; end
2'd3: begin nummax <= nummax3; end
default:;
endcase
if(!rst_n_in) begin
num <= 6'd0;
delay_cnt<=64'd0;
end
else if(num <= nummax) begin //如果未播放完音乐,继续计数延时
if(delay_cnt <= 64'd1500000) begin //该音调还未播放完全
delay_cnt <= delay_cnt + 1'd1;
case(key_value)
2'd0: tone <= mem0; //静止不响
2'd1: tone <= mem1[num]; //森林幻想曲
2'd2: tone <= mem2[num]; //东方红
2'd3: tone <= mem3[num]; //我和我的祖国
default: ; //默认播放第一首
endcase
end
else begin
delay_cnt <= 64'd0;
num <= num + 1'd1;
end
end
else begin //音乐播放完,num=0重新播放
num <= 7'd0;
end
end
always@(negedge rst_n_in) begin
//森林狂想曲
mem1[0]<=5'd6;
mem1[1]<=5'd8;
mem1[2]<=5'd10;
mem1[3]<=5'd12;
mem1[4]<=5'd10;
mem1[5]<=5'd10;
mem1[6]<=5'd10;
mem1[7]<=5'd9;
mem1[8]<=5'd10;
mem1[9]<=5'd10;
mem1[10]<=5'd10;
mem1[11]<=5'd9;
mem1[12]<=5'd10;
mem1[13]<=5'd10;
mem1[14]<=5'd6;
mem1[15]<=5'd7;
mem1[16]<=5'd8;
mem1[17]<=5'd10;
mem1[18]<=5'd9;
mem1[19]<=5'd8;
mem1[20]<=5'd6;
mem1[21]<=5'd6;
mem1[22]<=5'd5;
mem1[23]<=5'd5;
mem1[24]<=5'd3;
mem1[25]<=5'd3;
mem1[26]<=5'd3;
mem1[27]<=5'd3;
mem1[28]<=5'd3;
mem1[29]<=5'd3;
mem1[30]<=5'd3;
mem1[31]<=5'd3;
mem1[32]<=5'd6;
mem1[33]<=5'd8;
mem1[34]<=5'd10;
mem1[35]<=5'd12;
mem1[36]<=5'd10;
mem1[37]<=5'd10;
mem1[38]<=5'd10;
mem1[39]<=5'd9;
mem1[40]<=5'd10;
mem1[41]<=5'd10;
mem1[42]<=5'd10;
mem1[43]<=5'd9;
mem1[44]<=5'd10;
mem1[45]<=5'd10;
mem1[46]<=5'd6;
mem1[47]<=5'd7;
mem1[48]<=5'd8;
mem1[49]<=5'd10;
mem1[50]<=5'd9;
mem1[51]<=5'd8;
mem1[52]<=5'd6;
mem1[53]<=5'd6;
mem1[54]<=5'd5;
mem1[55]<=5'd5;
mem1[56]<=5'd6;
mem1[57]<=5'd6;
mem1[58]<=5'd6;
mem1[59]<=5'd6;
mem1[60]<=5'd6;
mem1[61]<=5'd6;
mem1[62]<=5'd6;
mem1[63]<=5'd7;
//东方红
mem2[0]<=5'd12;
mem2[1]<=5'd12;
mem2[2]<=5'd12;
mem2[3]<=5'd13;
mem2[4]<=5'd9;
mem2[5]<=5'd9;
mem2[6]<=5'd9;
mem2[7]<=5'd9;
mem2[8]<=5'd8;
mem2[9]<=5'd8;
mem2[10]<=5'd8;
mem2[11]<=5'd6;
mem2[12]<=5'd9;
mem2[13]<=5'd9;
mem2[14]<=5'd9;
mem2[15]<=5'd9;
mem2[16]<=5'd12;
mem2[17]<=5'd12;
mem2[18]<=5'd12;
mem2[19]<=5'd12;
mem2[20]<=5'd13;
mem2[21]<=5'd15;
mem2[22]<=5'd13;
mem2[23]<=5'd12;
mem2[24]<=5'd8;
mem2[25]<=5'd8;
mem2[26]<=5'd8;
mem2[27]<=5'd6;
mem2[28]<=5'd9;
mem2[29]<=5'd9;
mem2[30]<=5'd9;
mem2[31]<=5'd9;
mem2[32]<=5'd12;
mem2[33]<=5'd12;
mem2[34]<=5'd9;
mem2[35]<=5'd9;
mem2[36]<=5'd8;
mem2[37]<=5'd8;
mem2[38]<=5'd7;
mem2[39]<=5'd6;
mem2[40]<=5'd5;
mem2[41]<=5'd5;
mem2[42]<=5'd12;
mem2[43]<=5'd12;
mem2[44]<=5'd9;
mem2[45]<=5'd9;
mem2[46]<=5'd10;
mem2[47]<=5'd9;
mem2[48]<=5'd8;
mem2[49]<=5'd8;
mem2[50]<=5'd8;
mem2[51]<=5'd6;
mem2[52]<=5'd9;
mem2[53]<=5'd10;
mem2[54]<=5'd9;
mem2[55]<=5'd8;
mem2[56]<=5'd9;
mem2[57]<=5'd8;
mem2[58]<=5'd7;
mem2[59]<=5'd6;
mem2[60]<=5'd5;
mem2[61]<=5'd5;
mem2[62]<=5'd5;
mem2[63]<=5'd5;
//我和我的祖国
mem3[0]<=5'd12;
mem3[1]<=5'd12;
mem3[2]<=5'd13;
mem3[3]<=5'd13;
mem3[4]<=5'd12;
mem3[5]<=5'd12;
mem3[6]<=5'd11;
mem3[7]<=5'd11;
mem3[8]<=5'd10;
mem3[9]<=5'd10;
mem3[10]<=5'd9;
mem3[11]<=5'd9;
mem3[12]<=5'd8;
mem3[13]<=5'd8;
mem3[14]<=5'd8;
mem3[15]<=5'd8;
mem3[16]<=5'd8;
mem3[17]<=5'd8;
mem3[18]<=5'd5;
mem3[19]<=5'd5;
mem3[20]<=5'd5;
mem3[21]<=5'd5;
mem3[22]<=5'd5;
mem3[23]<=5'd5;
mem3[24]<=5'd8;
mem3[25]<=5'd8;
mem3[26]<=5'd10;
mem3[27]<=5'd10;
mem3[28]<=5'd15;
mem3[29]<=5'd15;
mem3[30]<=5'd14;
mem3[31]<=5'd14;
mem3[32]<=5'd13;
mem3[33]<=5'd13;
mem3[34]<=5'd13;
mem3[35]<=5'd10;
mem3[36]<=5'd12;
mem3[37]<=5'd12;
mem3[38]<=5'd12;
mem3[39]<=5'd12;
mem3[40]<=5'd12;
mem3[41]<=5'd12;
mem3[42]<=5'd12;
mem3[43]<=5'd12;
mem3[44]<=5'd12;
mem3[45]<=5'd12;
mem3[46]<=5'd12;
mem3[47]<=5'd12;
mem3[48]<=5'd13;
mem3[49]<=5'd13;
mem3[50]<=5'd14;
mem3[51]<=5'd14;
mem3[52]<=5'd13;
mem3[53]<=5'd13;
mem3[54]<=5'd12;
mem3[55]<=5'd12;
mem3[56]<=5'd11;
mem3[57]<=5'd11;
mem3[58]<=5'd10;
mem3[59]<=5'd10;
mem3[60]<=5'd9;
mem3[61]<=5'd9;
mem3[62]<=5'd9;
mem3[63]<=5'd9;
mem3[64]<=5'd9;
mem3[65]<=5'd9;
mem3[66]<=5'd6;
mem3[67]<=5'd6;
mem3[68]<=5'd6;
mem3[69]<=5'd6;
mem3[70]<=5'd6;
mem3[71]<=5'd6;
mem3[72]<=5'd7;
mem3[73]<=5'd7;
mem3[74]<=5'd6;
mem3[75]<=5'd6;
mem3[76]<=5'd5;
mem3[77]<=5'd5;
mem3[78]<=5'd12;
mem3[79]<=5'd12;
mem3[80]<=5'd8;
mem3[81]<=5'd8;
mem3[82]<=5'd8;
mem3[83]<=5'd9;
mem3[84]<=5'd10;
mem3[85]<=5'd10;
mem3[86]<=5'd10;
mem3[87]<=5'd10;
mem3[88]<=5'd10;
mem3[89]<=5'd10;
mem3[90]<=5'd10;
mem3[91]<=5'd10;
mem3[92]<=5'd10;
mem3[93]<=5'd10;
mem3[94]<=5'd10;
mem3[95]<=5'd10;
end
endmodule
module Beeper
(
input clk_in, //系统时钟
input rst_n_in, //系统复位,低有效
input tone_en, //蜂鸣器使能信号
input [4:0] tone, //蜂鸣器音节控制
output reg piano_out //蜂鸣器控制输出
);
/*
无源蜂鸣器可以发出不同的音节,与蜂鸣器震动的频率(等于蜂鸣器控制信号的频率)相关,
为了让蜂鸣器控制信号产生不同的频率,我们使用计数器计数(分频)实现,不同的音节控制对应不同的计数终值(分频系数)
计数器根据计数终值计数并分频,产生蜂鸣器控制信号
*/
reg [19:0] time_end;
//根据不同的音节控制,选择对应的计数终值(分频系数)
//低音1的频率为261.6Hz,蜂鸣器控制信号周期应为12MHz/261.6Hz = 45871.5,
//因为本设计中蜂鸣器控制信号是按计数器周期翻转的,所以几种终值 = 45871.5/2 = 22936
//需要计数22936个,计数范围为0 ~ (22936-1),所以time_end = 22935
always@(tone) begin
case(tone)
5'd1: time_end = 16'd22935; //L1,
5'd2: time_end = 16'd20428; //L2,
5'd3: time_end = 16'd18203; //L3,
5'd4: time_end = 16'd17181; //L4,
5'd5: time_end = 16'd15305; //L5,
5'd6: time_end = 16'd13635; //L6,
5'd7: time_end = 16'd12147; //L7,
5'd8: time_end = 16'd11464; //M1,
5'd9: time_end = 16'd10215; //M2,
5'd10: time_end = 16'd9100; //M3,
5'd11: time_end = 16'd8589; //M4,
5'd12: time_end = 16'd7652; //M5,
5'd13: time_end = 16'd6817; //M6,
5'd14: time_end = 16'd6073; //M7,
5'd15: time_end = 16'd5740; //H1,
5'd16: time_end = 16'd5107; //H2,
5'd17: time_end = 16'd4549; //H3,
5'd18: time_end = 16'd4294; //H4,
5'd19: time_end = 16'd3825; //H5,
5'd20: time_end = 16'd3408; //H6,
5'd21: time_end = 16'd3036; //H7,
default:time_end = 20'd1048575; //让蜂鸣器不响
endcase
end
reg [17:0] time_cnt;
//当蜂鸣器使能时,计数器按照计数终值(分频系数)计数
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) 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_in or negedge rst_n_in) begin
if(!rst_n_in) begin
piano_out <= 1'b0;
end else if(time_cnt==time_end) begin
piano_out <= ~piano_out; //蜂鸣器控制输出翻转,两次翻转为1Hz
end else begin
piano_out <= piano_out;
end
end
endmodule
OLED12832模块:
该模块作用为显示乐曲的名称,采用了状态机的方式设计,代码参考了电子森林各位同学的代码以及官方代码,通过spi协议对OLED屏幕进行字模写入,在封装过后可以通过对music_data的各个位赋值显示字库内的内容:
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in)
music_data=64'h0E_0F_10_00_00_00_00_00; //项目二
if(key_value==0)
music_data=64'h0E_0F_10_00_00_00_00_00; //项目二
if(key_value==1)
music_data=64'h01_02_03_04_05_00_00_00; //森林幻想曲
else if(key_value==2)
music_data=64'h06_07_08_00_00_00_00_00; //东方红
else if(key_value==3)
music_data=64'h09_0A_09_0B_0C_0D_00_00; //我和我的祖国
else
music_data=64'h0E_0F_10_00_00_00_00_00; //项目二
end
如上代码所示,两个十六进制数代表一个字,OLED屏一共可以显示16*32大小的字8个,所以music_data位64位,字库在代码末尾。
此外,该模块也是通过判断键值显示对应的乐曲名字,与音乐模块相一致。
注:由于该部分代码比较长,故在此不展示,若需要可在附件取。
- 遇到的难题
在实现本项目的过程中,主要遇到了2个难题:
- 在实现音乐播放1分钟就自动停止时,我本意想把该部分的实现代码放在music模块,但是该功能需要在按键切换时将时间计数值重新赋值,由于初学verilog,一直想不通如何才能在music模块判断按键变化,最后想要通过一个按键变化标志位flag从按键消抖模块输出到music模块,但又出现了新的问题,即当flag在music模块起作用之后,需要改变值,该值只能在music模块改变,而实质需要改变的应该是按键消抖模块的flag值。关于这个问题我思考了好久,最后将该部分实现代码放到按键消抖模块,通过按键消抖模块控制蜂鸣器使能端,得以实现。
- 其次比较难的点还有OLED模块,因为之前没有用过状态机设计,也对并行运行的fpga了解不够深入,所以在阅读同学们的代码时有些吃力,花费了很多时间,最后还是学会了。
- 总结感悟
- 首先感谢电子森林提供的这个平台以及各位同学们的开源代码的帮助,让我顺利完成了本项目。
- 其次在FPGA开发时要时刻有并行执行的思想。
- 在解决问题时多方面思考可能会取得意想不到的效果。
- 无论阅读什么代码时最好慢慢细读,可能会比较晦涩难懂,但是读懂后收获会很丰富。
- 多去阅读别人写的优秀代码。