FPGA开发板上的蜂鸣器可以用来播放音乐,只需要控制蜂鸣器信号的方波频率、占空比和持续时间即可。
1、简谱原理
简谱上的4/4表示该简谱以4分音符为一拍,每小节4拍,简谱上应该也会标注每分钟多少拍。音符时值对照表如下图所示,这表示了每个音符的演奏时长。
音符是记录音的高低和长短的符号,简谱中的音符是七个阿拉伯数字,它们是:1(Do)、2(Re)、3(Mi)、4(Fa)、5(Sol)、6(La)、7(Ti),为了标记更高或更低的音,则在基本符号的上面或下面加上小圆点。在简谱中,不带点的基本符号叫中音。记在简谱基本音符号下面的小圆点,叫低音点,它表示将基本音符降低一个音组,即降低一个纯八度。在基本符号下面加一个点叫低音,加两个点叫倍低音,加三个点叫超低音。记在简谱基本音符号上面的小圆点,叫高音点,它表示将基本音符升高一个音组,即升高一个纯八度。在基本符号上面加一个点叫高音,加两个点叫倍高音,加三个点叫超高音。
音符所对应的频率如下表所示。
音符 | 频率 |
低音1 | 261Hz |
低音2 | 293Hz |
低音3 | 329Hz |
低音4 | 349Hz |
低音5 | 392Hz |
低音6 | 440Hz |
低音7 | 499Hz |
中音1 | 523Hz |
中音2 | 587Hz |
中音3 | 659Hz |
中音4 | 698Hz |
中音5 | 784Hz |
中音6 | 880Hz |
中音7 | 998Hz |
高音1 | 1046Hz |
高音2 | 1174Hz |
高音3 | 1318Hz |
高音4 | 1396Hz |
高音5 | 1568Hz |
高音6 | 1760Hz |
高音7 | 1976Hz |
2、结构设计
2.1、按键消抖模块
由于要是用按键控制音乐开始播放,所以需要一个按键消抖模块,具体可以在FPGA开发:按键消抖一文中找到。
Debounce debounce_0
(.clk (clk),.rst (rst_n),.button_in (button_in),.button_out (button_out)
);
同时我们还需要一个边沿检测的机制来保证一次按下只触发一次按键操作。
always @ (posedge clk or posedge rst)beginif(rst == 1'b1)beginbutton_out_d0 <= 1'b1;button_negedge <= 1'b0;endelse beginbutton_out_d0 <= button_out;button_negedge <= button_out_d0 & ~button_out;end
end
2.2、ROM模块
使用ROM保存音符时长和音调,创建ROM的过程可以根据不同的FPGA开发环境而定,如果是Quartus的话步骤如下:
首先新建两个个MIF文件,它们是用来初始化ROM的,如下图所示。
根据你的简谱长度,设置深度,如下图所示。
随后根据简谱填入对应信息并保存,如下图所示。
接着在IP窗口搜索ROM IP,如下图所示。
选好模块名和HDL类型并保存,这里选择Verilog HDL,如下图所示。
在ROM创建菜单中选择创建的ROM大小(这里应该要和刚才的MIF文件一致),如下图所示。
在初始化界面,选择使用刚才创建的MIF文件并Finish即可完成ROM的创建,如下图所示。
2.3、频率译码模块
规定中音1使用十进制数11表示,而低音1使用01表示,中音2使用12表示。译码模块根据对应的音符频率,输出相应的周期,其中CLK_FRE根据开发板的频率而定。
module music_hz(
input [7:0] hz_sel,
output reg [19:0] cycle
);parameter CLK_FRE = 50 ;always @(*)begincase(hz_sel)8'h01 : cycle = CLK_FRE*1000000/261 ; //low 1 261Hz8'h02 : cycle = CLK_FRE*1000000/293 ; //low 2 293Hz8'h03 : cycle = CLK_FRE*1000000/329 ; //low 3 329Hz8'h04 : cycle = CLK_FRE*1000000/349 ; //low 4 349Hz8'h05 : cycle = CLK_FRE*1000000/392 ; //low 5 392Hz8'h06 : cycle = CLK_FRE*1000000/440 ; //low 6 440Hz8'h07 : cycle = CLK_FRE*1000000/499 ; //low 7 499Hz8'h11 : cycle = CLK_FRE*1000000/523 ; //middle 1 523Hz8'h12 : cycle = CLK_FRE*1000000/587 ; //middle 2 587Hz8'h13 : cycle = CLK_FRE*1000000/659 ; //middle 3 659Hz8'h14 : cycle = CLK_FRE*1000000/698 ; //middle 4 698Hz8'h15 : cycle = CLK_FRE*1000000/784 ; //middle 5 784Hz8'h16 : cycle = CLK_FRE*1000000/880 ; //middle 6 880Hz8'h17 : cycle = CLK_FRE*1000000/998 ; //middle 7 998Hz8'h21 : cycle = CLK_FRE*1000000/1046 ; //high 1 1046Hz8'h22 : cycle = CLK_FRE*1000000/1174 ; //high 2 1174Hz8'h23 : cycle = CLK_FRE*1000000/1318 ; //high 3 1318Hz8'h24 : cycle = CLK_FRE*1000000/1396 ; //high 4 1396Hz8'h25 : cycle = CLK_FRE*1000000/1568 ; //high 5 1568Hz8'h26 : cycle = CLK_FRE*1000000/1760 ; //high 6 1760Hz8'h27 : cycle = CLK_FRE*1000000/1976 ; //high 7 1976Hzdefault : cycle = 20'd0 ;endcaseend
endmodule
2.4、状态机演奏模块
状态机设有四个状态,IDLE,PLAY,PLAY_WAIT和PLAY_END,其中PLAY状态使用一个计数器对每个音符的演奏时长进行计数,PLAY_WAIT用于检查是否全部音符演奏完毕,如果否,则会对演奏时长计数器清零并再次进入PLAY状态。
always @(*)begincase(state)IDLE:beginif (button_negedge)next_state = PLAY;elsenext_state = IDLE; endPLAY:beginif (play_cnt == music_time) next_state = PLAY_WAIT;elsenext_state = PLAY;endPLAY_WAIT:beginif (music_cnt == music_len - 1)next_state = PLAY_END;elsenext_state = PLAY;endPLAY_END:next_state = IDLE;default:next_state = IDLE;endcase
end
周期计数器用于对音符的每个周期进行计数,并提供计数值给输出信号模块。
always @(posedge clk or negedge rst_n)beginif (~rst_n)hz_cnt <= 20'd0; else if (state == PLAY || state == PLAY_WAIT)beginif (hz_cnt == cycle - 1)hz_cnt <= 20'd0;elsehz_cnt <= hz_cnt + 1'b1;endelse hz_cnt <= 20'd0;
end
输出信号模块根据计数值输出信号,其中还可以控制占空比。
always @(posedge clk or negedge rst_n)beginif (~rst_n)buzzer <= 1'b1; else if (state == PLAY || state == PLAY_WAIT)beginif (hz_cnt < cycle/32) //控制占空比buzzer <= 1'b0;elsebuzzer <= 1'b1;endelse if (state == IDLE || state == PLAY_END)buzzer <= 1'b1;
end
演奏时长计数器用于对每个音符的演奏时间计数。
always @(posedge clk or negedge rst_n)beginif (~rst_n)play_cnt <= 32'd0; else if (state == PLAY)play_cnt <= play_cnt + 1'b1;else play_cnt <= 32'd0;
end
演奏个数计数器用于对演奏的音符数计数。
always @(posedge clk or negedge rst_n)beginif (~rst_n)music_cnt <= 32'd0; else if (state == PLAY_WAIT)music_cnt <= music_cnt + 1'b1;else if (state == IDLE || state == PLAY_END)music_cnt <= 32'd0;
end
最后实例化ROM,并且注意,这里规定演奏时长rom值以8为一拍,所以读取rom值后需要进行转换,假设一分钟85拍。
music_hz hz0
(.hz_sel(rom_hz_data),.cycle(cycle)
) ;music_rom hz_rom
(.address(music_cnt[8:0]),.clock(clk),.q(rom_hz_data));music_time_rom time_rom
(.address(music_cnt[8:0]),.clock(clk),.q(rom_time_data));always @(posedge clk or negedge rst_n)beginif (~rst_n)music_time <= 32'hffff_ffff; elsemusic_time <= rom_time_data*(CLK_FRE*1000000*60/85/8);
end