2021暑假一起练—基于小脚丫FPGA平台用PWM制作了一个音乐播放器
本项目利用小脚丫FPGA平台,完成了项目二,使用PWM波制作一个音乐播放器
标签
FPGA
显示
蜂鸣器
按键消抖
柯宁枫
更新2021-09-07
1561

 

项目需求:

  1. 通过PWM产生不同的音调,并驱动板上蜂鸣器将音调输出
  2. 能够播放三首不同的曲子,每个曲子的时间长度为1分钟,可以切换播放
  3. 曲子的切换使用小脚丫核心板上的按键,需要有按键消抖的功能
  4. 播放的曲子的名字在OLED屏幕上显示出来(汉字显示)

FlEiCoDx2rpiUIDDUY_6cvalKsGj

管脚设计:

             FpQ1w9aGD5TCzHXc-0sbHuk8ASCP

使用模块:

  • OLED模块(v)                              (显示时间和汉字歌名)
  • BEEP蜂鸣器模块(beepall.v)              (输出音乐到蜂鸣器)
  • 时钟模块(v FreqDiv.v)                  (进行时钟的分频以及输出时间信号到显示模块)
  • TOP顶层模块(v)                           (将各个模块联系起来)
  • 消抖模块 (xiaodou.v)                   (防止误判进行按键消抖,给予一个延时)

各个模块介绍:

OLED模块(v)

OLED模块是最重要的一个模块之一,也是我花了许久才完成的。这个模块也是我看了硬禾学堂给的例程之后想出来的,通过模仿硬禾学堂的程序,写出了我自己理解的OLED显示模块。

成果如下:

FjbU2xOJCXgLn7jiXRTEQsk6M3_HFnjQh0H2rt7LU3gaeRE-qqjIgyt9FhxR1onLWefmFkL28D5NIzIhEku5

下面是核心代码:

                        MAIN:begin
						if(cnt_main >= 5'd6) cnt_main <= 5'd5;
						else cnt_main <= cnt_main + 1'b1;
						case(cnt_main)	
							5'd0:	begin state <= INIT; end
							5'd1:	begin y_p <= 8'hb0; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd21; char <= "         TITLE             ";state <= SCAN; end
							5'd2:	begin y_p <= 8'hb1; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd21; char <= "                           ";state <= SCAN; end
							5'd3:	begin y_p <= 8'hb2; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd21; char <= "          TIME             ";state <= SCAN; end
							5'd4:	begin y_p <= 8'hb3; x_ph <= 8'h10; x_pl <= 8'h00; num <= 5'd21; char <= "                           ";state <= SCAN; end
 
							5'd5:case(a)						
									2'd0:begin 
									y_p <= 8'hb1; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd 5; char <= {8'd26, 8'd27,8'd24,8'd25," "}; state <= SCAN; 	//显示 不该								
									end
								    2'd1:begin 
									y_p <= 8'hb1; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd 5; char <= {8'd16, 8'd17,8'd18,8'd19," "}; state <= SCAN; 		//显示  阳光							
									end
								    2'd2:begin 
									y_p <= 8'hb1; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd 6; char <= {8'd20, 8'd21,8'd18, 8'd19, 8'd22, 8'd23}; state <= SCAN; 	//显示追光者							
									end
									endcase
							5'd6:	begin 
									y_p <= 8'hb3; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd 8; char <= {4'd0, six, 4'd0, five, ":", 4'd0,four, 4'd0, three, ":",4'd0,two, 4'd0, one}; state <= SCAN; 									
									end
 
							default: state <= IDLE;
						endcase
					end
				

但cnt_main为1,2,3,4时,显示“TITLE”和"TIME";当cnt_main为5时显示三首歌不同的名字。当cnt_main为6时显示时间。

接下来就是字库,这里我输入了六个汉字:阳,光,追,者,不,该。

mem[ 16] = {8'h00,8'h7F,8'h15,8'h15,8'h1F};   // 49  1
mem[ 17] = {8'h7F,8'h49,8'h49,8'h49,8'h7F};   // 50  2阳 
mem[ 18] = {8'h48,8'h49,8'h2A,8'h3C,8'h1F};   // 51  3
mem[ 19] = {8'h38,8'h6C,8'h4A,8'h69,8'h00};   // 52  4光
mem[ 20] = {8'hA5,8'hFA,8'h80,8'h80,8'hFE};   // 53  5
mem[ 21] = {8'hDB,8'hDA,8'hDA,8'hE6,8'h80};   // 54  6追
mem[ 22] = {8'h48,8'h28,8'h1A,8'hFA,8'hAF};   // 55  7
mem[ 23] = {8'hAA,8'hAE,8'hFB,8'h08,8'h08};   // 56  8者
mem[ 24] = {8'h04,8'hFD,8'h40,8'h00,8'h4A};   // 57  9
mem[ 25] = {8'hAE,8'h5B,8'h2B,8'h56,8'h82};   // 65  A该
mem[ 26] = {8'h00,8'h20,8'h32,8'h1A,8'h0E};   // 66  B
mem[ 27] = {8'hFE,8'h0A,8'h12,8'h22,8'h00};   // 67  C不

我是通过PCtoLCD2002软件,输入汉字,然后得到的位点,再放到字库里面,不过有的字太大,系统设计的位点比较别扭。

FkWwgAU25APD09IrnP-TPmAZjs_X

我选择了绘画模式,一个一个点画进去,最后得到六个字的位点。

 

比较一下下面两段程序

 

							
			//5'd 4:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	//将5*8点阵编程8*8
			//5'd 5:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	//将5*8点阵编程8*8				   
            //5'd 6:	begin oled_dcn <= DATA; char_reg <= 8'h00; state <= WRITE; state_back <= SCAN; end	//将5*8点阵编程8*8
             //******************上面是示例代码*****************

            5'd 4:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][39:32]; state <= WRITE; state_back <= SCAN; end
			5'd 5:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][31:24]; state <= WRITE; state_back <= SCAN; end
			5'd 6:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][23:16]; state <= WRITE; state_back <= SCAN; end
			5'd 7:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][15: 8]; state <= WRITE; state_back <= SCAN; end
			5'd 8:	begin oled_dcn <= DATA; char_reg <= mem[char[(num*8)+:8]][ 7: 0]; state <= WRITE; state_back <= SCAN; end

 

前面三行是案例里加上的,被我后来删除,只留下了后五行,它给的先将5*8的点阵编程为8*8的点阵,也就是说有三列是空白的,如果是显示数字和字母的话,这样改是很好看的,因为数字和字母整体的占地就是高比宽大,5*8完全可以显示完,剩下三列当作间隔很完美。

但是对于汉字来说很多都是方方正正的,甚至大多数都是宽比高要大,那可能一个5*8的点位就只能显示一半的字如果再有这三列的间隔,一个字就会被一分为二,所以我删掉了这三行代码,用两个5*8的点阵一个显示一半,拼接成一个完整的汉字,中间也没有间隔,近似看作是一个10*8的点阵。

当然更好的办法应该是使用RAM来保存字的位点,而不是直接将其加到了程序中,所以我的方法泛用性不强,以后可以继续学习改进。

 

BEEP蜂鸣器模块(beepall.v)

这个模块主要就是设置每个音符的对应参数,以及按照乐谱写出音符的排列顺序,将其输出到蜂鸣器。D=F/2K,下面是我计算出来的参数:

parameter         //D=F/2K
     L1 = 16'd22988,
     L2 = 16'd19800,
     L3 = 16'd18203,
     L4 = 16'd17192,
     L5 = 16'd15305,
     L6 = 16'd13635,
     L7 = 16'd12148,
     M1 = 16'd11464,	
     M2 = 16'd10215,	
     M3 = 16'd9100,	
     M4 = 16'd8519,
     M5 = 16'd7652,	
     M6 = 16'd6817,		
     M7 = 16'd6079,
     H1 = 16'D5733,
     H2 = 16'D5108;
parameter	TIME = 6_000_000;

上面的这段是我计算出来的各个音符对应的值,经过测试是对的。

下面这一段是将音符发送给蜂鸣器:

	begin
		count <= count + 1'b1;		
		if(count == count_end)
		begin	
			count <= 17'h0;			
			beep_r <= !beep_r;		
		end
	end

举个例子,当发送M1,也就是中音的哆,count_end在这一秒就被赋值为上面我所定义的M1=11464,再经过count计数,加到count_end再归零,这样蜂鸣器就可以发出中哆音符了。

下面是切歌功能:

if(!rst1) begin
		count1=1'b0;
		state=1'b0;      //跳转到第一首歌开头

		end
	else if(!rst2) begin
		count1=1'b0;
		state=8'd122;    //跳转到第二首歌开头

		end
	else if(!rst3) begin
		count1=1'b0;
		state=10'd279;   //跳转到第三首歌开头

		end
	if(count1 < TIME)             
      count1 = count1 + 1'b1;
	else 
	begin
		count1 = 24'd0;
		if(state == 10'd500)
			state = 8'd0;
		else
			state = state + 1'b1;

如果按下对应的rst123键,会给state不同赋值到对应歌曲所在的位置并开始播放,当一首歌结束时我又将state赋值到最开始的值,就可以实现单曲循环,只能通过按键进行切歌。

再来看我是如何输入乐谱的,我使用周杰伦的《不该》举例:

                                FmsNA3zlGK9ULhLsWSQmKOQ8vuvL

		count1 = 24'd0;
		if(state == 10'd500)
			state = 8'd0;
		else
			state = state + 1'b1;     //state记录秒数
			case(state)
				8'd0:count_end = M1;  //count_end记录这一秒的状态
				8'd1:count_end=M1;
				8'd2:count_end=M2;
				8'D3:count_end=M1;
				8'D4:count_end=L7;
				8'D5:count_end=M1;
				8'D6:count_end=L7;
				8'D7:count_end=L6;
				8'D8:count_end=16'h0;     //假装我们还在一块

				8'D9:count_end=M1;
				8'D10:count_end=M1;
				8'D11:count_end=M3;
				8'D12:count_end=M2;
				8'D13:count_end=M1;
				8'D14:count_end=L5;
				8'D15:count_end=L5;
				8'D16:count_end=16'h0;    //我真的演不出来

				8'D17:count_end=M1;
				8'D18:count_end=M1;
				8'D19:count_end=M2;
				8'D20:count_end=M1;
				8'D21:count_end=L7;
				8'D22:count_end=M1;
				8'D23:count_end=M3;
				8'D24:count_end=M2;
				8'D25:count_end=16'h0;	 //还是不习惯你不在

				8'D26:count_end=L5;
				8'D27:count_end=M2;
				8'D28:count_end=M1;
				8'D29:count_end=M1;
				8'D30:count_end=M2;
				8'D31:count_end=M1;
				8'D32:count_end=16'h0;   //这身份转变的太快

				8'D33:count_end=M1;
				8'D34:count_end=M1;
				8'D35:count_end=M2;	
				8'D36:count_end=M1;
				8'D37:count_end=L7;
				8'D38:count_end=M1;
				8'D39:count_end=L7;
				8'd40:count_end=L6;  
				8'd41:count_end=16'h0;   //画面里不需要旁白

				8'd42:count_end=M1;
				8'D43:count_end=M1;
				8'D44:count_end=M3;
				8'D45:count_end=M2;
				8'D46:count_end=M1;
				8'D47:count_end=L5;
				8'D48:count_end=L5;
				8'D49:count_end=16'h0;    //却谁都看得出来

				8'D50:count_end=M1;
				8'D51:count_end=M1;
				8'D52:count_end=M2;
				8'D53:count_end=M1;
				8'D54:count_end=L7;
				8'D55:count_end=M1;
				8'D56:count_end=M3;
				8'D57:count_end=M2;
				8'D58:count_end=16'h0;    //是我情绪涌了上来

				8'D59:count_end=L5;
				8'D60:count_end=M3;
				8'D61:count_end=M2;
				8'D62:count_end=M1;
				8'D62:count_end=M1;
				8'D64:count_end=M2;
				8'D65:count_end=M1;	
				8'D66:count_end=16'h0;    //想哭却一片空白

				8'D67:count_end=M6;
				8'D68:count_end=M5;
				8'D69:count_end=M6;
				8'D70:count_end=M5;
				8'D71:count_end=M6;
				8'D72:count_end=M6;
				8'D73:count_end=M5;
				8'D74:count_end=M6;
				8'D75:count_end=M5;
				8'D76:count_end=M6;
				8'D77:count_end=M6;
				8'D78:count_end=M7;
				8'D79:count_end=M6;
				8'D80:count_end=M5;
				8'D81:count_end=M5;	
				8'D82:count_end=M3;
				8'D83:count_end=M5;
				8'D84:count_end=M3;
				8'D85:count_end=M5;
				8'D86:count_end=16'h0;//雪地里相爱他们说零下已结晶的誓言不会坏
				8'D87:count_end=M7;
				8'D88:count_end=M6;
				8'D89:count_end=M7;
				8'D90:count_end=M6;
				8'D91:count_end=M7;
				8'D92:count_end=M7;
				8'D93:count_end=M6;
				8'D94:count_end=M7;
				8'D95:count_end=M6;
				8'D96:count_end=M7;
				8'D97:count_end=M7;
				8'D98:count_end=H1;
				8'D99:count_end=M7;
				8'D100:count_end=M6;
				8'D101:count_end=M6;
				8'D102:count_end=M6;
				8'D103:count_end=M5;
				8'D104:count_end=M6;
				8'D105:count_end=16'H0;//但爱的状态却不会永远都冰封而透明的存在

				8'D106:count_end=L5;
				8'D107:count_end=L6;
				8'D108:count_end=M3;
				8'D109:count_end=L5;
				8'D110:count_end=L6;
				8'D111:count_end=M3;
				8'D112:count_end=M3;
				8'D113:count_end=M2;
				8'D114:count_end=M1;
				8'D115:count_end=M2;
				8'D116:count_end=M2;
				8'D117:count_end=M1;
				8'D118:count_end=M2;
				8'D119:count_end=M3;
				8'D120:count_end=M2;
				8'D121:state=8'D0;     //轻轻摇落下来,雪下的梦融化的太快
				default: count_end = 16'h0;
			endcase

我开始也在这里犯了一个错误,可以注意一下,8'd121在这一段乐谱里面是最后一个,2的八次方是256,说明他的范围是0~255,如果是三首歌的话那么后面的十进制秒数就需要到达360以上,这时就需要写成9’d300类似的形式。

时钟模块(v FreqDiv.v)

one~six分别是时间xx:xx:xx的最小位到最高位,one计数到9,然后one变为1,然后two加1,two变到5再进位时,three加1,two变为0,以此类推。为了能通过按键调整时间(这里面我用3个按键分别对应分钟数的增加,减少和小时数的减少),比如小时数已经是00了,我再往下减就变成了最高的23了。



always@(posedge clk, negedge rst)
begin 
	if(!rst)
		clkp <= 0;
	else
	begin
		if(cntp <= N/2)
			clkp <= 1;
		else
			clkp <= 0;
	end
end


always@(negedge clk, negedge rst)
begin 
	if(!rst)
		clkn <= 0;
	else
	begin
		if(cntn <= N/2)
			clkn <= 1;
		else
			clkn <= 0;     //cntn计数到达N/2时,clkn变为低电位
	end
end

always@(posedge clk, negedge rst)
begin
	if(!rst)
		cntp <= 0;
	else
		if(cntp == N-1)
			cntp <= 0;
		else
			cntp <= cntp + 1;
end

always@(negedge clk, negedge rst)
begin
	if(!rst)
		cntn <= 0;
	else
		if(cntn == N-1)
			cntn <= 0;
		else
			cntn <= cntn + 1;   //计数并且分频
end

assign clkout = clkp & clkn;

这里输出六个量,分别对应时分秒的个位和十位,输出到OLED模块里显示歌曲播放时间时使用。

消抖模块 (xiaodou.v)

Frjs2a4uC1ZAtYVuWaM8B9CJIgSM

我们可以看到,但按键按下的那一刻,存在一段时间的抖动,同时在释放按键的一段时间里也是存在抖动的,这就可能导致状态在识别的时候可能检测为多次的按键,因为运行过程中普通的检测一次状态key为1就执行一次按键操作。所以我们在使用按键时往往需要消抖。消抖方式有很多种,这里我提供一种相对而言比较简单容易理解的方式,通过延时来消抖。我们知道,抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。大家看原理图,其实我们要做的就是,但按键按下去后,只在中间稳定的某一个时刻(10ms)取一个真正按键的使能值就好了。

 always @(posedge mclk or negedge rst_n)
 begin
  if(!rst_n)
   cnt <= 11'd0;
  else if(ken_enable == 1) begin
   if(cnt == DURATION)
    cnt <= cnt;
   else 
    cnt <= cnt + 1'b1;
   end
  else
   cnt <= 16'b0;
 end
 
 always @(posedge mclk or negedge rst_n) 
 begin
  if(!rst_n) key_en <= 4'd0;
  else if(key) key_en <= (cnt == DURATION-1'b1) ? 1'b0 : 1'b1;
  else key_en <= key_en;
 end

 

这个模块也是我在网上学习到的,有兴趣可以移步学习 https://blog.csdn.net/qq_40789587/article/details/84205870

 

TOP顶层模块(v)

最后是顶层模块,通过这次verilog的编程训练,我深刻体会到模块化编程的好处,先实现一个个基本的小功能,比如clock时钟信号的输入,然后分频得到一个clk1时钟信号,然后又分为计时模块,显示模块,音乐模块。最后通过一个个模块之间的配合工作,完成一个大的多功能的项目,也算是积少成多吧,就像搭积木一样。

module top(
	input clk,
	input rst,
	input rrst1,
	input rrst2,
	input rrst3,
	
	input tone_en,
	inout one_wire,

	
	output				oled_csn,	
	output				oled_rst,	
	output				oled_dcn,	
	output				oled_clk,	
	output				oled_dat,	
	
	output 				uart_out,	
	
	output				beep
);

	
wire[3:0]	six;
wire[3:0]	five;
wire[3:0]	four;
wire[3:0]	three;
wire[3:0]	two;
wire[3:0]	one;
wire [1:0]rst1;
wire [1:0]rst2;
wire [1:0]rst3;

wire 		clk1h;
wire		fresh_flag;
Xiaodou Xiaodou_v1(
	.mclk(clk),
	.rst_n(rst),
	.key(rrst1),
	.key_en(rst1)
	
);
Xiaodou Xiaodou_v2(
	.mclk(clk),
	.rst_n(rst),
	.key(rrst2),
	.key_en(rst2)
	
);
Xiaodou Xiaodou_v3(
	.mclk(clk),
	.rst_n(rst),
	.key(rrst3),
	.key_en(rst3)
	
);
OLED12832 OLED12832_v1(
	.clk(clk),
	.rst_n(rst),
	.rst1(rst1),
	.rst2(rst2),
	.rst3(rst3),

	
	.six(six),
	.five(five),
	.four(four),
	.three(three),
	.two(two),
	.one(one),
	
	
	.oled_clk(oled_clk),
	.oled_csn(oled_csn),
	.oled_dat(oled_dat),
	.oled_dcn(oled_dcn),
	.oled_rst(oled_rst)
);

DS18B20Z DS18B20Z_v1(
	.clk_in(clk),
	.rst_n_in(rst),

	.one_wire(one_wire),
	
	.data_out(data_out)
);



FreqDiv FreqDiv_v1(
	.clk(clk),
	.rst(rst),
	
	.clkout(clk1h)
);

clock clock_v1(
	.clk1h(clk1h),
	.rst(rst),
	.rst1(rst1),
	.rst2(rst2),
	
	.fresh_flag(fresh_flag),
	
	.six(six),
	.five(five),
	.four(four),
	.three(three),
	.two(two),
	.one(one)
);


song song_v1(
	.clk(clk),
	.clk1h(clk1h),
	.rst(rst),
	.rst1(rst1),
	.rst2(rst2),
	.rst3(rst3),
	.tone_en(tone_en),

	
	.two(two),
	.three(three),
	.four(four),
	.fresh_flag(fresh_flag),
	.beep(beep)
);
endmodule

以上就是代码部分的全部内容。

完成情况:

所有要求已全部完成,但某些方面还需要完善,比如不像MP3一样可以滚动歌名以及随机播放,这些功能也可以完成,但没来得及实现。代码文件已经全部上传附件,里面还有一些不完美的代码,希望可以和大家互相交流学习。

总结:

这次实践项目我完成了基本的内容,花了一个星期学习,虽然很累当时确实学到了许多知识,也在网站上参考学习了很多别人的代码思路,对我做出这个任务有了很大的启发,当然这次也有很多是我没有找到相似思路的问题,但经过思考最终也得到了解决。我以前也上过选修课,但做的都是比较简单的任务,并且相互之间没有什么关联,比如流水灯,数字钟,SPI和VGA小游戏,这次的项目包含了几个小任务,需要一个个解决,也锻炼了我解决项目问题的能力。希望以后可以继续学习,把本次实践学到知识加以利用,更上一层楼!

 

附件下载
knf de FPGA.rar
团队介绍
北京理工大学信息与电子学院学生-柯宁枫
团队成员
柯宁枫
北京理工大学信息与电子学院的一个同学
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号