2021年“暑期一起练”——基于小脚丫FPGA的项目2 :利用PWM制作一个音乐播放器(高政)
基于小脚丫FPGA的综合技能训练平台 项目2 - 利用PWM制作一个音乐播放器
标签
FPGA
北京理工大学——高政
更新2021-09-06
1220

一、项目需求:

本次基于小脚丫FPGA的综合技能训练平台,共计有三个项目可供选择,本次我选择了项目2实现一个音乐播放器。具体要求如下:

   项目二:利用PWM制作一个音乐播放器

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

二、实现思路:

(1)通过PWM产生不同的音调,并驱动板上蜂鸣器将音调输出
首先是如何区分不同的音调,这个可以从网上得到,由下表所示,然后由12000000Hz(D/2K)可以得到在程序中对应的大致16进制数,分别对应的结果为L1= 16'd22935,L2= 16'd20429,L3= 16'd18203,L4= 16'd17182,L5 = 16'd15306,L6 = 16'd13636,L7 = 16'd12148,M1 = 16'd11461,M2 = 16'd10216, M3 = 16'd9100, M4 = 16'd8589,M5 = 16'd7653, M6 = 16'd6818, M7 = 16'd6074,H1 = 16'D5717,H2 = 16'D5107,H3 = 16'D4550,H4 = 16'D4295,H5 = 16'D3826,H6 = 16'D3409,H7 = 16'D3037;

FgeSPReYB_Bv873ObVmVuCFjhxJf

在得到了对应的16进制数之后,根据搜索得到的歌曲简谱就可以得到相应的状态;

FhIIq_O4wNgHvb_o1WfjnwUGUGPi

如上面一段音乐对应的16进制为:

月光色女子香M6 H3 M7 M7 M5 M6

泪断剑 情多长H1 M7 M7 M5 M6 M2

有多痛 无字想M6 M7 H1 H3 H2 M6

忘了你呦H1 M7 M5 M5

孤单魂 随风荡H1 H2 H3 M6 H3 M5

谁去笑 痴情郎M6 M7 H1 M6 M2 M3

对应的代码为:

8'd0:count_end = M6; //月光 
				8'd1:count_end=H3;
				8'd2:count_end=M7;
				8'D3:count_end=M7;
				8'D4:count_end=L5;
				8'D5:count_end=M6;
				8'D6:count_end=16'h0;//月光色女子香
				
				8'D7:count_end=H1;
				8'D8:count_end=M7;
				8'D9:count_end=M7;
				8'D10:count_end=M5;
				8'D11:count_end=M6;
				8'D12:count_end=M2;
				8'D13:count_end=16'h0;//泪断剑情多长
				
				8'D14:count_end=M6;
				8'D15:count_end=M7;
				8'D16:count_end=H1;
				8'D17:count_end=H3;
				8'D18:count_end=H2;
				8'D19:count_end=M6;
				8'D20:count_end=16'h0;//有多痛无字想
				
				8'D21:count_end=H1;
				8'D22:count_end=M7;
				8'D23:count_end=M5;
				8'D24:count_end=M5;
				8'D25:count_end=16'h0;	//忘了你
				
				8'D26:count_end=H1;
				8'D27:count_end=H2;
				8'D28:count_end=H3;
				8'D29:count_end=H3;
				8'D30:count_end=H6;
				8'D31:count_end=M5;
				8'D32:count_end=16'h0;//孤单魂随风荡

(这里的原理参考的CSDN https://blog.csdn.net/weifengdq/article/details/103168587

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

在得到了三首歌的简谱以及对应的16进制数字之后,我们可以将所有的歌曲的对应的音符记录下来,这样就可以播放三首不同的曲子,然后我们可以设置音符个数使其播放时间在一分钟左右(其实可以根据12MHz求得应该设置的音符个数,我这里只是根据经验大致设置了音符个数)

然后是音乐的切换问题,我这里设置了三个按键,每当按下一个按键时,他就会切换到对应的歌曲的第一句话的第一个音符,从而实现歌曲的切换,同时此时计时会重新开始。

下面为:按键指向的对应歌曲第一个音符

if(!rst1) begin
		count1=1'b0;
		counts=1'b0;

		end
	else if(!rst2) begin
		count1=1'b0;
		counts=8'd72;

		end
	else if(!rst3) begin
		count1=1'b0;
		counts=10'd211;

下面为:第二首歌/第三首歌的开始

8'D72:count_end=L5;//当
				8'D73:count_end=L6;
				8'D74:count_end=M1;
				8'D75:count_end=M2;
				8'D76:count_end=M1;
				8'D77:count_end=M2;
				8'D78:count_end=M1;
				8'D79:count_end=M2;
				8'D80:count_end=M3;
				8'D81:count_end=M2;	
				8'D82:count_end=16'h0;//1
8'D211:count_end=M1;//少年
				8'D212:count_end=L6;
				8'D213:count_end=M1;
				8'D214:count_end=M1;
				8'D215:count_end=L6;
				8'D216:count_end=M1;
				8'D217:count_end=L6;
				8'D218:count_end=M1;
				8'D219:count_end=L6;
				8'D220:count_end=M1;
				8'D221:count_end=M1;
				8'D222:count_end=16'h0;//1

(3)曲子的切换使用小脚丫核心板上的按键,需要有按键消抖的功能

从上面的分析我们可以得到,已经实现了按键切换歌曲的功能。

然后是按键消抖功能,首先需要明确的是按键作为基本的人机输入接口,由于其机械特性,在按键按下或松开的时候,都是会有抖动的。按键消抖的方式有很多。我的方法是通过计时来消抖,通过一个计数器,当按键输入有变化时,计数器清零,否则就累加,直到加到一个预定值,就认为按键稳定,输出按键值,这样就得到了没有抖动的按键值。
(以上原理及下面的代码参考CSDN:https://blog.csdn.net/zhang_ze1234/article/details/109261546

具体代码如下

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

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

将曲子的名称用汉字显示出来主要用到了”PCtoLCD2002“软件,我们可以在该软件中进行”写字“,然后得到该字的字模,然后将其导入到程序之中。

FggTqHCZm-ECayqhQSPVFdljzBYL

如上图,我们得到了”当“字,然后可以得到其字模如下:{0x89,0xAB,0xAA,0xA8,0xAF},
{0xAF,0xA8,0xBA,0xFB,0xF9},同理我们可以得到其他汉字的字模。

然后将其导入到程序中,程序如下:

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'd18, 8'd19,8'd20,8'd21," "}; 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," "," "," "}; state <= SCAN; 									
									end
								    2'd2:begin 
									y_p <= 8'hb1; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd 6; char <= {8'd22, 8'd23,8'd24, 8'd25," "," "}; 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

此时已经完成该任务
三、主要模块

(1)DS18B20模块与OLED模块
这两个模块参考的硬禾学堂给的例程,这里不再赘述,想一探究竟的小伙伴直接在硬禾学堂的网站上查看即可。

(2)时钟模块
即构建一个时钟,这部分可以在网上找到许多参考,我主要参考了李卓然同学的代码

always@(posedge clk1h)
begin
	six <= six_past;
	five <= five_past;
	four <= four_past;
	three <= three_past;
	two <= two_past;
	one <= one_past;
end

always@(posedge clk1h)
begin
	if(fresh_flag)
	begin
		six_past <= six_now;
		five_past <= five_now;
		four_past <= four_now;
		three_past <= three_now;
		two_past <= two_now;
		one_past <= one_now;		
	end
	else
	begin
		six_past <= six_past;
		five_past <= five_past;
		four_past <= four_past;
		three_past <= three_past;
		two_past <= two_past;
		one_past <= one_past;			
	end
end


always@(posedge clk1h, negedge rst or negedge rst1 or negedge rst2 or negedge rst3 )
begin
	if(!rst || !rst1 || !rst2 || !rst3)
	begin
		six_now <= 4'd0;	five_now <= 4'd0; four_now <= 4'd0; three_now <= 4'd0; two_now <= 4'd0; one_now <= 4'd0;
	end
	else
	begin
		case({hourdown, minup, mindown})
		3'b111:
		begin
			if(one_now < 9)
				one_now <= one_now + 1;
			else
			begin
				one_now <= 4'd0;
				if(two_now < 5)
					two_now <= two_now + 1;
				else
				begin
					two_now <= 4'd0;
					if(three_now < 9)
						three_now <= three_now + 1;
					else
					begin
						three_now <= 4'd0;
						if(four_now < 5)
							four_now <= four_now + 1;
						else
						begin
							four_now <= 4'd0;
							if(five_now == 3 && six_now == 2)
							begin
								five_now <= 0;
								six_now <= 0;
							end
							else if(five_now < 9)
								five_now <= five_now + 1;
							else
							begin
								five_now <= 4'd0;
								if(six_now < 2)
									six_now <= six_now + 1;
								else
									six_now <= 0;
							end
						end
					end
				end
			end
		end
		3'b110:
		begin
			if(three_now > 0)
				three_now <= three_now - 1;
			else
			begin
				three_now <= 9;
				if(four_now > 0)
					four_now <= four_now - 1;
				else
					four_now <= 5;
			end
		end
		3'b101:
		begin
			if(three_now < 9)
				three_now <= three_now + 1;
			else
			begin
				three_now <= 0;
				if(four_now < 5)
					four_now <= four_now + 1;
				else
					four_now <= 0;
			end
		end
		default:
		begin
			if(five_now > 0)
				five_now <= five_now - 1;
			else
			begin
				if(six_now == 2)	
				begin
					five_now <= 9;
					six_now <= 1;
				end
				else if(six_now == 1)
				begin
					five_now <= 9;
					six_now <= 0;
				end	
				else
				begin
					five_now <= 3;
					six_now <= 2;
				end
			end
		end
		endcase
	end
end

endmodule

(3)蜂鸣器模块
这部分可以参考我上面对于音调播放的解释。

(4)top模块

最后是顶层模块,top模块可以理解为主程序,通过模块化的输入与输出可以是top模块尽可能的简略,这里我展示一下我的代码。

具体代码如下:

debouncing debouncing1(
	.mclk(clk),
	.rst_n(rst),
	.key(rrst1),
	.key_en(rst1)
	
);
debouncing debouncing2(
	.mclk(clk),
	.rst_n(rst),
	.key(rrst2),
	.key_en(rst2)
	
);
debouncing debouncing3(
	.mclk(clk),
	.rst_n(rst),
	.key(rrst3),
	.key_en(rst3)
	
);
OLED12832 OLED12832_v1(
	.clk(clk),
	.rst_n(rst),
	.rst1(rst1),
	.rst2(rst2),
	.rst3(rst3),
	.temp_d(temp_d),
	.temp_h(temp_h),
	.temp_t(temp_t),
	.temp_u(temp_u),
	
	.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)
);

TempDatCvrt TempDatCvrt_v1(
	.datin(data_out),
	.clk(clk),
	.rst(rst),
	.fresh_flag(fresh),
	
	.dat_d(temp_d),
	.dat_h(temp_h),
	.dat_t(temp_t),
	.dat_u(temp_u)
);

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

clock clock_v1(
	.clk1h(clk1h),
	.rst(rst),
	.rst1(rst1),
	.rst2(rst2),
	.rst3(rst3),
	.fresh_flag(fresh),
	
	.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),
	.beep(beep)
);

四、遇到的难题

本次,我完成了所有需要的四个功能,其中遇到的最大的困难就是消抖功能,我从未接触过verilog中的消抖功能,在经过了搜索与考虑之后,我参考了CSDN中的代码,但是我发现他的代码是错误的设置了高低电平,导致我的消抖失败,最终在我的火眼金睛之下,我发现了这个问题,之后消抖功能得以实现,虽然说起来很简单,但是当时花费了我非常多的时间。

五、我的收获

本次非常荣幸能够参加硬禾学堂的2021暑期一起练活动,在此次活动中,我收获颇丰,不仅仅是编程上的收获还有一些模块化思想上的进步。此外,本次编程我也参考了许多同学们和老师以及CSDN上的代码,非常感谢他们。最后,感谢硬禾学堂提供了很好的平台,有老师们的讲解,有同学们的合作,也有大家提供的思路,感谢大家!

附件下载
LittleStep-gz.rar
代码压缩包
团队介绍
北京理工大学信息与电子学院
团队成员
北京理工大学——高政
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号