基于pwm实现简易音乐播放器
通过PWM产生不同的音调,产生3首曲子,并曲子的切换使用扩展板的按键,有按键消抖的功能,曲子的名字再屏幕上显示出来
标签
FPGA
PWM
ICE40
2022寒假在家练
音乐播放器
wyxeth
更新2022-03-03
北京航空航天大学
1757

项目名称 - 利用PWM制作一个音乐播放器

1,通过PWM产生不同的音调,并驱动板上蜂鸣器将音调输出

2,能够播放三首不同的曲子,每个曲子的时间长度为1分钟,可以切换播放

3,曲子的切换使用扩展板的按键,需要有按键消抖的功能

4,播放的曲子的名字在OLED屏幕上显示出来(汉字显示)

Fl8zCRDfvTfr3RnRsedkQgAqyonfFtnLbMQ7nWyZLhvOHXU118aGrHRbFoWjDYCY56AvXQo_qQGnYuRsMAR-

设计思路,我是考虑用pwm波来控制蜂鸣器的发声,频率的高低可以发出不同高低的声音,然后我规定的0.2s一个音符,在0.2秒内通过改变频率从而达到发出不同的声音,在把各种音符的频率换算成为count的数量,从而实现歌曲的编写,然后,关于oled,我参考了例程的写法,在此基础上进行修改达到我所需要的效果.

 

硬件主要是从硬禾学堂购买的基于ice40up5k的fpga学习平台,本fpga的时钟频率为12MHZ。主要使用的复位按键和状态跳转按键,蜂鸣器,和oled屏幕显示

功能,接通电源后按下复位键,开始播放第一首曲子,为花之舞,并显示,按下k1,蜂鸣器换成群青,oled上面也同时改变,再按下k1,蜂鸣器开始播放小星星,oled上面也同时改变汉字显示。

我的项目主要用到的是三个模块:oled模块,蜂鸣器模块,和按键消抖模块,下图为三者之间的关系Fkw0rwbRq1odBGgZlcFnPMHVXrKX

 

接下来说明一下主要模块的代码

首先是oled模块

当cnt_mian 为9,10,11时,显示汉字。

MAIN:begin
					if(start == 1'b0)
						cnt_main <= 5'd0;
						if(cnt_main >= 5'd12) cnt_main <= 5'd13;//接下来执行空操作,实现数据只刷新一次
						else cnt_main <= cnt_main + 1'b1;
						case(cnt_main)	//MAIN状态							
							5'd0 :	begin state <= INIT; end
																														
							5'd1 :	begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16;  char <= "                ";state <= SCAN; end
							5'd2 :	begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16;  char <= "                ";state <= SCAN; end												
							5'd3 :	begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd4 :	begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd5 :	begin y_p <= 8'hb4; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd6 :	begin y_p <= 8'hb5; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd7 :	begin y_p <= 8'hb6; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end
							5'd8 :	begin y_p <= 8'hb7; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd16; char <= "                ";state <= SCAN; end							
							5'd9 :	begin y_p <= 8'hb0; x_ph <= 8'h12; x_pl <= 8'h00; mem_hanzi_num <= 8'd0; state <= CHINESE; end
							5'd10:	begin y_p <= 8'hb0; x_ph <= 8'h13; x_pl <= 8'h00; mem_hanzi_num <= 8'd2; state <= CHINESE; end
							5'd11 :	begin y_p <= 8'hb0; x_ph <= 8'h14; x_pl <= 8'h00; mem_hanzi_num <= 8'd4; state <= CHINESE; end
							
							//default: state <= IDLE;   //如果你需要动态刷新一些信息,此行应该取消注
						endcase
					end

接下来就是字库,通过一个zimo的软件生成的

 

	always@(posedge clk or negedge rst_n)
		case(state_o)
		
		one:	
		begin
		mem_hanzi[ 0 ] = {8'h04,8'h04,8'h04,8'h84,8'h6F,8'h04,8'h04,8'h04,8'hE4,8'h04,8'h8F,8'h44,8'h24,8'h04,8'h04,8'h00};
			mem_hanzi[ 1 ] = {8'h04,8'h02,8'h01,8'hFF,8'h00,8'h10,8'h08,8'h04,8'h3F,8'h41,8'h40,8'h40,8'h40,8'h40,8'h78,8'h00};//花
			mem_hanzi[ 2 ] = {8'h00,8'h10,8'h10,8'h10,8'h10,8'h10,8'h11,8'h16,8'h10,8'h90,8'h50,8'h30,8'h10,8'h00,8'h00,8'h00};
			mem_hanzi[ 3 ] = {8'h00,8'h40,8'h20,8'h10,8'h10,8'h28,8'h44,8'h42,8'h41,8'h40,8'h40,8'h40,8'h40,8'h40,8'h40,8'h00};//之
			mem_hanzi[ 4 ] = {8'h00,8'h94,8'h92,8'h93,8'hFE,8'h92,8'hFE,8'h92,8'hFE,8'h92,8'hFE,8'h92,8'h92,8'h92,8'h00,8'h00};
			mem_hanzi[ 5 ] = {8'h80,8'h88,8'h44,8'h57,8'h24,8'h14,8'h0C,8'h00,8'h34,8'h24,8'h24,8'hFF,8'h24,8'h24,8'h20,8'h00};//舞
			
			
		end
		two:
		begin
			
mem_hanzi[ 0 ] = {8'h10,8'h92,8'h92,8'hFE,8'h92,8'h92,8'hFE,8'h10,8'h09,8'h8A,8'h8C,8'hF8,8'h8C,8'h8A,8'h09,8'h00};
			mem_hanzi[ 1 ] = {8'h10,8'h0C,8'hFF,8'h44,8'h44,8'h44,8'hFC,8'h00,8'h08,8'h08,8'h08,8'hFF,8'h08,8'h08,8'h08,8'h00};//群
			mem_hanzi[ 2 ] = {8'h40,8'h44,8'h54,8'h54,8'h54,8'h54,8'h54,8'h7F,8'h54,8'h54,8'h54,8'h54,8'h54,8'h44,8'h40,8'h00};
			mem_hanzi[ 3 ] = {8'h00,8'h00,8'h00,8'hFF,8'h15,8'h15,8'h15,8'h15,8'h15,8'h55,8'h95,8'h7F,8'h00,8'h00,8'h00,8'h00};//青
			
			
			mem_hanzi[ 4 ] = {8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00};
			mem_hanzi[ 5 ] = {8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00};//第三个不显示
	
		end
		thr:
		begin		
			mem_hanzi[ 0 ] = {8'h00,8'h00,8'h00,8'hE0,8'h00,8'h00,8'h00,8'hFF,8'h00,8'h00,8'h00,8'h20,8'h40,8'h80,8'h00,8'h00};
			mem_hanzi[ 1 ] = {8'h08,8'h04,8'h03,8'h00,8'h00,8'h40,8'h80,8'h7F,8'h00,8'h00,8'h00,8'h00,8'h00,8'h01,8'h0E,8'h00};//小
			mem_hanzi[ 2 ] = {8'h00,8'h00,8'h00,8'hBE,8'h2A,8'h2A,8'h2A,8'hEA,8'h2A,8'h2A,8'h2A,8'h3E,8'h00,8'h00,8'h00,8'h00};
			mem_hanzi[ 3 ] = {8'h00,8'h44,8'h42,8'h49,8'h49,8'h49,8'h49,8'h7F,8'h49,8'h49,8'h49,8'h49,8'h41,8'h40,8'h00,8'h00};//星
			mem_hanzi[ 4 ] = {8'h00,8'h00,8'h00,8'hBE,8'h2A,8'h2A,8'h2A,8'hEA,8'h2A,8'h2A,8'h2A,8'h3E,8'h00,8'h00,8'h00,8'h00};
			mem_hanzi[ 5 ] = {8'h00,8'h44,8'h42,8'h49,8'h49,8'h49,8'h49,8'h7F,8'h49,8'h49,8'h49,8'h49,8'h41,8'h40,8'h00,8'h00};//星
		end
	 default:  
	 begin
		    mem_hanzi[ 0 ] = {8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00};
			mem_hanzi[ 1 ] = {8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00};
			mem_hanzi[ 2 ] = {8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00};
			mem_hanzi[ 3 ] = {8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00};
			mem_hanzi[ 4 ] = {8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00};
			mem_hanzi[ 5 ] = {8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00};
			
			end
	    
	   endcase

蜂鸣器模块(ss.v)

蜂鸣器,主要就是计算各种参数,然后把曲子按照顺序排列上去一个一个写即可

本fpga的时钟频率为12MHZ,所以0.2S也就是 12 * 10^6*0.2 = 2400000,又因为从0开始计数

所以最大的数字为2399999,相应的其他频率代表的数字,就用12m去除就行。

#(
parameter cnt_max = 23'd2399999,
parameter do1 = 16'd45871,
parameter re1 = 16'd40857,
parameter mi1 = 16'd36406,
parameter fa1 = 16'd34363,
parameter so1 = 16'd30612,
parameter la1 = 16'd27271,
parameter xi1 = 16'd24295,
parameter do = 16'd22927,
parameter re = 16'd20230,
parameter mi = 16'd18200,
parameter fa = 16'd17177,
parameter so = 16'd15303,
parameter doh = 16'd11463,
parameter sos= 16'd14446,
parameter la = 16'd13633,
parameter xi = 16'd12145,
parameter reh = 16'd10214,
parameter mih = 16'd9298,
parameter fah = 16'd8589,
parameter soh = 16'd7651,
parameter xih = 16'd6073, 
parameter no = 16'd600
)

各种参数是根据板子的时钟频率以及各种音符的频率相除得到的,这些都需要事先计算

之后就是如何写的,首先,我规定,一个音符的时间为0.2s所以设置一个cnt变量,每到一个0.2就重新技术,再每个0.2s之间,通过改变freqdata的数值,达到频率不同的目的,从而达到发出不同的声音的目的。

下面以我的一首歌群青为例子

 9'd0: freqdata <= mi;
		9'd1: freqdata <= mi;
		9'd2: freqdata <= mi  ;
		9'd3: freqdata <= re  ;
		9'd4: freqdata <= mi  ;
		9'd5: freqdata <= la  ;
		9'd6: freqdata <= xi  ;
		9'd7: freqdata <= doh  ;
		9'd8: freqdata <= xi  ;
		9'd9: freqdata <= xi  ;
		9'd10: freqdata <=xi  ;
		9'd11: freqdata <=doh  ;
		9'd12: freqdata <=xi ;
		9'd13: freqdata <=so ;
		9'd14: freqdata <=mi ;
		9'd15: freqdata <=re ;
		9'd16: freqdata <=mi  ;
		9'd17: freqdata <=mi  ;
		9'd18: freqdata <=mi  ;
		9'd19: freqdata <=re  ;
		9'd20: freqdata <=mi  ;
		9'd21: freqdata <=la  ;
		9'd22: freqdata <=so  ;
		9'd23: freqdata <=mi ;
		9'd24: freqdata <=do  ;
		9'd25: freqdata <=do  ;
		9'd26: freqdata <=do  ;
		9'd27: freqdata <=no;
		9'd28: freqdata <=la1  ;
		9'd29: freqdata <=xi1 ;
		9'd30: freqdata <=do ;
		9'd31: freqdata <=re ;
		9'd32: freqdata <=mi  ;
		9'd33: freqdata <=mi  ;
		9'd34: freqdata <=mi  ;
		9'd35: freqdata <=re  ;
		9'd36: freqdata <=mi  ;
		9'd37: freqdata <=mi  ;
		9'd38: freqdata <=la  ;
		9'd39: freqdata <=sos  ;
		9'd40: freqdata <=sos ;
		9'd41: freqdata <=la ;
		9'd42: freqdata <=la  ;
		9'd43: freqdata <=xi  ;
        9'd44: freqdata <=xi  ;
        9'd45: freqdata <=mi  ;
        9'd46: freqdata <=re ;
        9'd47: freqdata <=do ;
        9'd48: freqdata <=re  ;
        9'd49: freqdata <=re ;
        9'd50: freqdata <=do  ;
        9'd51: freqdata <=re  ;	
        9'd52: freqdata <=re	;
        9'd53: freqdata <=so  ;
        9'd54: freqdata <=fa  ;

由于空间关系,只是一部分的代码,基本做法就是对照着谱子,按照顺序写出每个音符即可,我这里是一个八分音符为一个单位,四分音符就用连续的两个单位来代替。

接下来介绍切歌部分

reg[2:0] state;//状态变量
always@(posedge sys_clk or negedge sys_rst)//采用三个状态对应三首曲子
	if(sys_rst == 1'b0)
		state <= one;
else case(state)
	one: if(start == 1'b0)
		state <= two;
		else
		state <= one;
	two: if(start == 1'b0)
		state<= thr;
		else
		state <= two;
	thr: if(start == 1'b0)
		state <= one;
		else
		state <= thr;
	default : state <= one;
	endcase

我这里是用的状态机实现的切歌部分,start代表板上的k1,当k1按下时进行1-2-3-1的变化。

从而实现歌曲的变换。

按键消抖模块(key-filter.v)

然后就是按键消抖的部分,就是检测按下键后有没有20ms的稳定状态,如果不稳定就不开始计时,如果稳定,就开始计数,到20ms后停止,如下图的代码所示

module key_filter
#(
parameter cnt_max =20'd239999
)
(
input wire  sys_clk,
input wire  sys_rest,
input  wire key_in,
output reg key_fil
);

reg  [19:0]  cnt_20ms;
always@(posedge sys_clk or negedge sys_rest)
	if(sys_rest == 1'b0)
		cnt_20ms <= 20'd0;
	else  if(key_in == 1'b1)
		cnt_20ms <= 20'd0;
	else  if(cnt_20ms == cnt_max)
		cnt_20ms <= cnt_max;
	else
		cnt_20ms <= cnt_20ms + 20'd1;

always@(posedge sys_clk or negedge sys_rest)
	if(sys_rest == 1'b0)
		key_fil <= 1'b0;
	else  if(cnt_20ms == cnt_max)
		key_fil <= 1'b1;
	else 
		key_fil <= 1'b0;
endmodule

最后给出相应的引脚连接情况

通过官方给出的原理图我们可以知道 35是连的时钟,6是连的复位信号,38是连的k1按键,48是连接的蜂鸣器,然后36 37 34 32,分别对应的oled的时钟引脚,dat,dcn,和rst引脚

FlYbfHMASl0go3MnFRwqEwN7nHxvFtNqTe1q5YXqGiOHjLpz3WxZavbk

ldc_set_location -site {48} [get_ports beep]
ldc_set_location -site {6} [get_ports sys_rst]
ldc_set_location -site {35} [get_ports sys_clk]
ldc_set_location -site {38} [get_ports start]
ldc_set_location -site {36} [get_ports oled_clk]
ldc_set_location -site {37} [get_ports oled_dat]
ldc_set_location -site {34} [get_ports oled_dcn]
ldc_set_location -site {32} [get_ports oled_rst]

 

最后附上我这个项目的部分资源报告,可以看出其实没有用到多少资源,切片寄存器只用到了4%

然后Lut4用到了32%。

毕竟只是一个主要用蜂鸣器和pwm完成的项目,没有太高的技术力。

Fik0MmLMKDLDfgDG5aWjsNF8kbUSFgdo1iCNKv8KT5agOoP0wQWIcsySFg1NaM_Rjokq99fHj5mmni5umFF4

 

遇到的困难

   由于我之前从来没有接触过这个东西,所以起步比较困难,刚开始总是发出奇怪的声音,结果后来才发现,每次改变状态之后,cnt_200ms没有即时清零,导致一个音乐还没完,另一个音乐又开始播放了,最后发现了并解决了问题,然后就是刚开始时oled的代码没搞懂,后来慢慢慢慢在实践中成长,然后是我第一次用Verilog,感觉写的很重复,很冗余,需要加强代码的可读性和易移植性。

对未来的期望

   希望自己能在明年的电赛中取得一等奖的好成绩,然后准备后面学习一下计算机体系结构的知识,再来看一下关于RISC-v的项目。我也才从机械类转到电类,希望能继续学习有关数字电路的知识,更上一层楼!

附件下载
beebee.zip
全部工程文件
beebee_impl_1.rbt
直接烧录的rbt
automake.log
资源报告
团队介绍
姓名:吴懿行 学校:北京航空航天大学 大二
团队成员
wyxeth
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号