制作基于ICE40_FPGA_PICO利用PWM驱动蜂鸣器可以播放、切换曲子并显示中文曲名的音乐播放器
使用ICE40_FPGA_PICO开发板,用PWM驱动无源蜂鸣器发出不同的音调,然后对音调的节拍时间进行统计,然后用SPI的方式驱动0.96英寸的oled,把中文曲名和时间显示在上面,可以使用消抖后的按键K1复位,K2进行切歌。
标签
FPGA
2022寒假在家练
音乐播放器
mosu
更新2022-03-02
安徽理工大学
1176

一、项目要求

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

二、项目设计思路、实现功能和资源占用

1.设计基本思路

使用PWM驱动无源蜂鸣器发出不同的音调,然后对音调的节拍时间进行统计,然后用SPI的方式驱动0.96英寸的oled,把中文曲名和时间显示在上面,然后通过按键K1复位,K2进行切歌。

2.设计框图

Fi9kq6iIZIaKzim1iaZ0lLEGYee0

 

3.实现功能分析

Fub_4mGtGLrIDEB3LPh-vZk4pLTw

 

 

三、主要功能实现

1.演奏单元

用pwm驱动蜂鸣器发出不同音调,如下图,输入不同的频率,就会产生不同的音调。

FrAmsdW9JbAcI9DuZL3AAFELPBsc

//根据不同的音节控制,选择对应的计数终值(分频系数)
//低音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 =	16'd65535;	
	endcase
end

 

然后就根据乐谱,就可以写出连续的音符,这里用第一首曲子举例。

 

always@(posedge rst_n_in) begin//音符表
    //漠河舞厅
    //第一小节
    note[0] = {5'd3};
    note[1] = {5'd6};
    note[2] = {5'd6};
    note[3] = {5'd7};
    note[4] = {5'd8};
    note[5] = {5'd8};
    note[6] = {5'd9};
    note[7] = {5'd10};
    note[8] = {5'd8};
    //第二小节
    note[9] = {5'd10};
    note[10] = {5'd10};
    note[11] = {5'd10};
    note[12] = {5'd12};
    note[13] = {5'd10};
    note[14] = {5'd10};
    note[15] = {5'd0};
    note[16] = {5'd10};
    note[17] = {5'd10};
    //第三小节
    note[18] = {5'd9};
    note[19] = {5'd9};
    note[20] = {5'd9};
    note[21] = {5'd10};
    note[22] = {5'd11};
    note[23] = {5'd11};
    note[24] = {5'd0};
    note[25] = {5'd5};
    //第四小节
    note[26] = {5'd10};
    note[27] = {5'd10};
    note[28] = {5'd10};
    note[29] = {5'd9};
    note[30] = {5'd14};
    note[31] = {5'd7};
    note[32] = {5'd0};
    //第五小节
    note[33] = {5'd6};
    note[34] = {5'd7};
    note[35] = {5'd8};
    note[36] = {5'd9};
    note[37] = {5'd10};
    note[38] = {5'd10};
    note[39] = {5'd8};
    note[40] = {5'd10};
    note[41] = {5'd8};
    //第六小节
    note[42] = {5'd10};
    note[43] = {5'd10};
    note[44] = {5'd10};
    note[45] = {5'd12};
    note[46] = {5'd10};
    note[47] = {5'd10};
    note[48] = {5'd0};
    note[49] = {5'd10};
    note[50] = {5'd10};
    //第七小节
    note[51] = {5'd9};
    note[52] = {5'd9};
    note[53] = {5'd9};
    note[54] = {5'd10};
    note[55] = {5'd11};
    note[56] = {5'd0};
    //第八小节
    note[57] = {5'd5};
    note[58] = {5'd10};
    note[59] = {5'd10};
    note[60] = {5'd9};
    note[61] = {5'd14};
    note[62] = {5'd0};
    //第九小节
    note[63] = {5'd13};
    note[64] = {5'd10};
    note[65] = {5'd6};
    note[66] = {5'd0};
    note[67] = {5'd6};
    note[68] = {5'd12};
    //第十小节
    note[69] = {5'd11};
    note[70] = {5'd10};
    note[71] = {5'd11};
    note[72] = {5'd11};
    note[73] = {5'd0};
    //十一小节
    note[74] = {5'd12};
    note[75] = {5'd9};
    note[76] = {5'd5};
    note[77] = {5'd0};
    note[78] = {5'd5};
    note[79] = {5'd12};
    //十二小节
    note[80] = {5'd10};
    note[81] = {5'd11};
    note[82] = {5'd10};
    note[83] = {5'd11};
    note[84] = {5'd10};
    note[85] = {5'd0};
    //十三小节
    note[86] = {5'd13};
    note[87] = {5'd10};
    note[88] = {5'd6};
    note[89] = {5'd0};
    note[90] = {5'd6};
    note[91] = {5'd12};
    //十四小节
    note[92] = {5'd11};
    note[93] = {5'd10};
    note[94] = {5'd11};
    note[95] = {5'd11};
    note[96] = {5'd0};
    note[97] = {5'd11};
    //十五小节
    note[98] = {5'd10};
    note[99] = {5'd11};
    note[100] = {5'd10};
    note[101] = {5'd11};
    note[102] = {5'd11};
    note[103] = {5'd10};
    note[104] = {5'd11};
    note[105] = {5'd10};
    note[106] = {5'd11};
    //十六小节
    note[107] = {5'd10};
    note[108] = {5'd0};
    note[109] = {5'd0};
    note[110] = {5'd0};
    //十七小节
    note[111] = {5'd5};
    note[112] = {5'd5};
    note[113] = {5'd5};
    note[114] = {5'd6};
    note[115] = {5'd7};
    note[116] = {5'd0};
    note[117] = {5'd3};
    note[118] = {5'd10};
    note[119] = {5'd7};
    //十八小节
    note[120] = {5'd9};
    note[121] = {5'd8};
    note[122] = {5'd8};
    note[123] = {5'd7};
    note[124] = {5'd8};
    note[125] = {5'd0};
    note[126] = {5'd8};
    note[127] = {5'd8};
    note[128] = {5'd8};
    //十九小节
    note[129] = {5'd9};
    note[130] = {5'd5};
    note[131] = {5'd9};
    note[132] = {5'd5};
    note[133] = {5'd9};
    note[134] = {5'd0};
    note[135] = {5'd9};
    note[136] = {5'd9};
    note[137] = {5'd11};
    //二十小节
    note[138] = {5'd11};
    note[139] = {5'd10};
    note[140] = {5'd10};
    note[141] = {5'd9};
    note[142] = {5'd10};
    note[143] = {5'd0};
    note[144] = {5'd10};
    note[145] = {5'd10};
    note[146] = {5'd10};
    //二十一小节
    note[147] = {5'd9};
    note[148] = {5'd9};
    note[149] = {5'd9};
    note[150] = {5'd10};
    note[151] = {5'd11};
    note[152] = {5'd0};
    note[153] = {5'd11};
    note[154] = {5'd11};
    note[155] = {5'd12};
    //二十二小节
    note[156] = {5'd8};
    note[157] = {5'd8};
    note[158] = {5'd8};
    note[159] = {5'd9};
    note[160] = {5'd10};
    note[161] = {5'd10};
    note[162] = {5'd0};
    note[163] = {5'd10};
    note[164] = {5'd10};
    //二十三小节
    note[165] = {5'd9};
    note[166] = {5'd9};
    note[167] = {5'd10};
    note[168] = {5'd10};
    note[169] = {5'd10};
    note[170] = {5'd13};
    note[171] = {5'd10};
    note[172] = {5'd9};
    note[173] = {5'd9};
    note[174] = {5'd8};
    note[175] = {5'd14};
    //二十四小节
    note[176] = {5'd8};
    note[177] = {5'd6};
    note[178] = {5'd0};
    note[179] = {5'd0};
    note[180] = {5'd7};
    note[181] = {5'd8};

一个一个写太慢了,我写了一个简单的Python脚本来帮助我快速写音符表

# Python转换脚本
tone_dic = {
            'L1':'d1',
            'L2':'d2',
            'L3':'d3',
            'L4':'d4',
            'L5':'d5',
            'L6':'d6',
            'L7':'d7',
            'M1':'d8',
            'M2':'d9',
            'M3':'d10',
            'M4':'d11',
            'M5':'d12',
            'M6':'d13',
            'M7':'d14',
            'H1':'d15',
            'H2':'d16',
            'H3':'d17',
            'H4':'d18',
            'H5':'d19',
            'H6':'d20',
            'H7':'d21',
            'L0':'d0',
            'M0':'d0',
            'H0':'d0'
}

Str1 = input("输入开始标号: ")
flag = int(Str1)  # 把Str1转为数字型
Str2 = input("请输入音符: ")

# 字符串以2个字符为单位切割, 切割后组成列表
list = []
for i in range(0, len(Str2), 2):
    list.append(Str2[i:i+2])

# 输出
for i in range(len(list)):
    a = list[i]
    print(f"    note[{flag+i}] = " + "{" + f"5'{tone_dic[a]}" + "};")

FmZoB2J3ChhQ5_uGayUTfZiFFLBE

给音调分配节拍

因为我没有学过乐理,在开始的时候,我给每个音调分配相同的时间,演奏出来的效果很差,根本就不像一首歌。

然后我才知道每个音调分配的节拍是不一样的,节拍就是给每个音调分配演奏的时间。

比如乐谱上面标注 J60,就是说1分钟演奏60拍,一拍也就是1秒,而1秒在我们12Mhz晶振的板子上就是计数12000000次。

always@(tcon or flag) begin
		if(flag == 3'd1) begin
		case(tcon)//漠河舞厅,一小节四拍,拍速为80拍/分钟
			5'd 1:  time_delay =    28'd2250000;//四分之一拍
			5'd 2:  time_delay =    28'd4500000;//半拍
			5'd 3:  time_delay =    28'd6750000;//四分之三拍
			5'd 4:  time_delay =    28'd9_000_000;//一拍
			5'd 6:  time_delay =    28'd13500000;//一拍半
			5'd 8:  time_delay =    28'd18000000;//两拍
			5'd12:  time_delay =    28'd27000000;//三拍
			5'd16:  time_delay =    28'd36000000;//四拍
			5'd24:  time_delay =    28'd72000000;//八拍
			default:time_delay =    28'd0;  
		endcase
	end

	else if(flag == 3'd2) begin
		case(tcon)//漂洋过海来看你,一小节四拍,拍速为60拍/分钟
			5'd 1:  time_delay =    28'd3_000_000;//四分之一拍
			5'd 2:  time_delay =    28'd6_000_000;//半拍
			5'd 3:  time_delay =    28'd9_000_000;//四分之三拍
			5'd 4:  time_delay =    28'd12_000_000;//一拍
			5'd 6:  time_delay =    28'd18_000_000;//一拍半
			5'd 8:  time_delay =    28'd24_000_000;//两拍
			5'd12:  time_delay =    28'd36_000_000;//三拍
			5'd16:  time_delay =    28'd48_000_000;//四拍
			5'd24:  time_delay =    28'd96_000_000;//八拍
			default:time_delay =    28'd0;  
		endcase
	end

	else if(flag == 3'd3) begin
		case(tcon)//青花瓷,一小节四拍,拍速为80拍/分钟
			5'd 1:  time_delay =    28'd2250000;//四分之一拍
			5'd 2:  time_delay =    28'd4500000;//半拍
			5'd 3:  time_delay =    28'd6750000;//四分之三拍
			5'd 4:  time_delay =    28'd9_000_000;//一拍
			5'd 6:  time_delay =    28'd13500000;//一拍半
			5'd 8:  time_delay =    28'd18000000;//两拍
			5'd12:  time_delay =    28'd27000000;//三拍
			5'd16:  time_delay =    28'd36000000;//四拍
			5'd24:  time_delay =    28'd72000000;//八拍
			default:time_delay =    28'd0;  
		endcase
	end
    
end

使用flag标志位信号来确定播放曲目

always@(posedge clk_in or negedge rst_n_in) begin   //时间控制,上一首或者下一首切换歌曲(手动或者自动)
    if(rst_n_in == 1'b0) begin //复位
        cnt_delay <= 1'b0; num <= 1'b0; flag <= 3'd1; end
    else if(tone_en == 1'b0) begin
        cnt_delay <= cnt_delay; end
    else if(chose_down) begin//按键消抖后会输出一个相反的脉冲,不用!chose_up,下面chose_down同理
        case(flag)
            3'd1:   begin flag <= 3'd2;num <= 10'd184; end
            3'd2:   begin flag <= 3'd3;num <= 10'd374; end
            3'd3:   begin flag <= 3'd1;num <= 1'b0; end
            default:begin flag <= 3'd1;num <= 1'b0; end
        endcase
    end
    else if(cnt_delay >= time_delay) begin //自动切换
        cnt_delay <= 1'b0; 
        case(num)
            10'd  0:    begin flag <= 3'd1;num <= num+1'b1; end
            10'd184:    begin flag <= 3'd2;num <= num+1'b1; end
            10'd374:    begin flag <= 3'd3;num <= num+1'b1; end
            10'd609:    begin flag <= 3'd1;num <= 1'b0; end
            default:    begin num <= num+1'b1; end
        endcase
    end
    else begin cnt_delay <= cnt_delay + 1'b1; end
end

2.显示单元

使用0.96英寸的OLED屏幕,这部分的代码参考电子森林上的项目,做了修改以满足我的需求。

使用PCtoLCD软件进行取字模。

 //16*16点阵字库数据
    always@(posedge rst_n)
        begin
            mem[  0] = {8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00};//空
            mem[  1] = {8'h10,8'h60,8'h02,8'h0C,8'hC0,8'h04,8'hE4,8'hAF};//漠
            mem[  2] = {8'hA4,8'hA4,8'hA4,8'hAF,8'hE4,8'h04,8'h00,8'h00};
            mem[  3] = {8'h04,8'h04,8'h7C,8'h03,8'h80,8'h88,8'h4B,8'h2A};
            mem[  4] = {8'h1A,8'h0E,8'h1A,8'h2A,8'h4B,8'h88,8'h80,8'h00};
            mem[  5] = {8'h10,8'h60,8'h02,8'h8C,8'h00,8'h04,8'hE4,8'h24};//河
            mem[  6] = {8'h24,8'hE4,8'h04,8'h04,8'hFC,8'h04,8'h04,8'h00};
            mem[  7] = {8'h04,8'h04,8'h7E,8'h01,8'h00,8'h00,8'h0F,8'h04};
            mem[  8] = {8'h04,8'h0F,8'h40,8'h80,8'h7F,8'h00,8'h00,8'h00};
            mem[  9] = {8'h00,8'h94,8'h92,8'h93,8'hFE,8'h92,8'hFE,8'h92};//舞
            mem[ 10] = {8'hFE,8'h92,8'hFE,8'h92,8'h92,8'h92,8'h00,8'h00};
            mem[ 11] = {8'h80,8'h88,8'h44,8'h57,8'h24,8'h14,8'h0C,8'h00};
            mem[ 12] = {8'h34,8'h24,8'h24,8'hFF,8'h24,8'h24,8'h20,8'h00};
            mem[ 13] = {8'h00,8'h00,8'hFE,8'h02,8'h22,8'h22,8'h22,8'h22};//厅
            mem[ 14] = {8'h22,8'hE2,8'h22,8'h22,8'h22,8'h22,8'h22,8'h00};
            mem[ 15] = {8'h80,8'h60,8'h1F,8'h00,8'h00,8'h00,8'h00,8'h40};
            mem[ 16] = {8'h80,8'h7F,8'h00,8'h00,8'h00,8'h00,8'h00,8'h00};
            mem[ 17] = {8'h7A,8'h4A,8'h7E,8'h4A,8'h7E,8'h4A,8'h7A,8'h00};//飘
            mem[ 18] = {8'hFE,8'h42,8'h82,8'h62,8'hFE,8'h00,8'h00,8'h00};
            mem[ 19] = {8'h24,8'h55,8'h85,8'h7D,8'h05,8'h15,8'hA4,8'h60};
            mem[ 20] = {8'h1F,8'h04,8'h03,8'h04,8'h3F,8'h40,8'hF8,8'h00};
            mem[ 21] = {8'h10,8'h60,8'h02,8'h8C,8'h00,8'h10,8'h91,8'h96};//洋
            mem[ 22] = {8'h90,8'hF0,8'h90,8'h94,8'h93,8'h10,8'h00,8'h00};
            mem[ 23] = {8'h04,8'h04,8'h7E,8'h01,8'h00,8'h04,8'h04,8'h04};
            mem[ 24] = {8'h04,8'hFF,8'h04,8'h04,8'h04,8'h04,8'h04,8'h00};
            mem[ 25] = {8'h40,8'h40,8'h42,8'hCC,8'h00,8'h08,8'h48,8'h88};//过
            mem[ 26] = {8'h08,8'h08,8'h08,8'hFF,8'h08,8'h08,8'h08,8'h00};
            mem[ 27] = {8'h00,8'h40,8'h20,8'h1F,8'h20,8'h40,8'h40,8'h41};
            mem[ 28] = {8'h40,8'h48,8'h50,8'h4F,8'h40,8'h40,8'h40,8'h00};
            mem[ 29] = {8'h10,8'h60,8'h02,8'h0C,8'hC0,8'h10,8'h08,8'hF7};//海
            mem[ 30] = {8'h14,8'h54,8'h94,8'h14,8'hF4,8'h04,8'h00,8'h00};
            mem[ 31] = {8'h04,8'h04,8'h7C,8'h03,8'h00,8'h01,8'h1D,8'h13};
            mem[ 32] = {8'h11,8'h55,8'h99,8'h51,8'h3F,8'h11,8'h01,8'h00};
            mem[ 33] = {8'h00,8'h08,8'h08,8'h28,8'hC8,8'h08,8'h08,8'hFF};//来
            mem[ 34] = {8'h08,8'h08,8'h88,8'h68,8'h08,8'h08,8'h00,8'h00};
            mem[ 35] = {8'h21,8'h21,8'h11,8'h11,8'h09,8'h05,8'h03,8'hFF};
            mem[ 36] = {8'h03,8'h05,8'h09,8'h11,8'h11,8'h21,8'h21,8'h00};
            mem[ 37] = {8'h20,8'h22,8'h2A,8'h2A,8'hAA,8'h6A,8'h3A,8'h2E};//看
            mem[ 38] = {8'h29,8'h29,8'h29,8'h29,8'h29,8'h20,8'h20,8'h00};
            mem[ 39] = {8'h08,8'h04,8'h02,8'h01,8'hFF,8'h55,8'h55,8'h55};
            mem[ 40] = {8'h55,8'h55,8'h55,8'hFF,8'h00,8'h00,8'h00,8'h00};
            mem[ 41] = {8'h00,8'h80,8'h60,8'hF8,8'h07,8'h40,8'h20,8'h18};//你
            mem[ 42] = {8'h0F,8'h08,8'hC8,8'h08,8'h08,8'h28,8'h18,8'h00};
            mem[ 43] = {8'h01,8'h00,8'h00,8'hFF,8'h00,8'h10,8'h0C,8'h03};
            mem[ 44] = {8'h40,8'h80,8'h7F,8'h00,8'h01,8'h06,8'h18,8'h00};
            mem[ 45] = {8'h40,8'h44,8'h54,8'h54,8'h54,8'h54,8'h54,8'h7F};//青
            mem[ 46] = {8'h54,8'h54,8'h54,8'h54,8'h54,8'h44,8'h40,8'h00};
            mem[ 47] = {8'h00,8'h00,8'h00,8'hFF,8'h15,8'h15,8'h15,8'h15};
            mem[ 48] = {8'h15,8'h55,8'h95,8'h7F,8'h00,8'h00,8'h00,8'h00};
            mem[ 49] = {8'h04,8'h04,8'h04,8'h84,8'h6F,8'h04,8'h04,8'h04};//花
            mem[ 50] = {8'hE4,8'h04,8'h8F,8'h44,8'h24,8'h04,8'h04,8'h00};
            mem[ 51] = {8'h04,8'h02,8'h01,8'hFF,8'h00,8'h10,8'h08,8'h04};
            mem[ 52] = {8'h3F,8'h41,8'h40,8'h40,8'h40,8'h40,8'h78,8'h00};
            mem[ 53] = {8'h20,8'h22,8'hE4,8'h10,8'h08,8'h90,8'h88,8'h47};//瓷
            mem[ 54] = {8'h24,8'h1C,8'h24,8'h44,8'h94,8'h8C,8'h00,8'h00};
            mem[ 55] = {8'h00,8'h02,8'h02,8'hE2,8'h9E,8'h4A,8'h4A,8'h1A};
            mem[ 56] = {8'h2A,8'h0A,8'h7A,8'h82,8'h82,8'hE2,8'h00,8'h00};   
            mem[ 57] = {8'h00,8'hE0,8'h10,8'h08,8'h08,8'h10,8'hE0,8'h00};//0
            mem[ 58] = {8'h00,8'h0F,8'h10,8'h20,8'h20,8'h10,8'h0F,8'h00};
            mem[ 59] = {8'h00,8'h00,8'h10,8'h10,8'hF8,8'h00,8'h00,8'h00};//1
            mem[ 60] = {8'h00,8'h00,8'h20,8'h20,8'h3F,8'h20,8'h20,8'h00};
            mem[ 61] = {8'h00,8'h70,8'h08,8'h08,8'h08,8'h08,8'hF0,8'h00};//2
            mem[ 62] = {8'h00,8'h30,8'h28,8'h24,8'h22,8'h21,8'h30,8'h00};
            mem[ 63] = {8'h00,8'h30,8'h08,8'h08,8'h08,8'h88,8'h70,8'h00};//3
            mem[ 64] = {8'h00,8'h18,8'h20,8'h21,8'h21,8'h22,8'h1C,8'h00};
            mem[ 65] = {8'h00,8'h00,8'h80,8'h40,8'h30,8'hF8,8'h00,8'h00};//4
            mem[ 66] = {8'h00,8'h06,8'h05,8'h24,8'h24,8'h3F,8'h24,8'h24};
            mem[ 67] = {8'h00,8'hF8,8'h88,8'h88,8'h88,8'h08,8'h08,8'h00};//5
            mem[ 68] = {8'h00,8'h19,8'h20,8'h20,8'h20,8'h11,8'h0E,8'h00};
            mem[ 69] = {8'h00,8'hE0,8'h10,8'h88,8'h88,8'h90,8'h00,8'h00};//6
            mem[ 70] = {8'h00,8'h0F,8'h11,8'h20,8'h20,8'h20,8'h1F,8'h00};
            mem[ 71] = {8'h00,8'h18,8'h08,8'h08,8'h88,8'h68,8'h18,8'h00};//7
            mem[ 72] = {8'h00,8'h00,8'h00,8'h3E,8'h01,8'h00,8'h00,8'h00};
            mem[ 73] = {8'h00,8'h70,8'h88,8'h08,8'h08,8'h88,8'h70,8'h00};//8
            mem[ 74] = {8'h00,8'h1C,8'h22,8'h21,8'h21,8'h22,8'h1C,8'h00};
            mem[ 75] = {8'h00,8'hF0,8'h08,8'h08,8'h08,8'h10,8'hE0,8'h00};//9
            mem[ 76] = {8'h00,8'h01,8'h12,8'h22,8'h22,8'h11,8'h0F,8'h00};
            mem[ 77] = {8'h00,8'h00,8'h00,8'h00,8'hC0,8'h38,8'h04,8'h00};// /
            mem[ 78] = {8'h00,8'h60,8'h18,8'h07,8'h00,8'h00,8'h00,8'h00};
            mem[ 79] = {8'h00,8'h00,8'h00,8'hC0,8'hC0,8'h00,8'h00,8'h00};//:
            mem[ 80] = {8'h00,8'h00,8'h00,8'h30,8'h30,8'h00,8'h00,8'h00};
        end

 

然后是屏幕上显示的字幕显示位置进行选择,然后这里使用中间量flag来判断,然后显示出当前的曲名。

always@(posedge clk or negedge rst_n)//字幕选择数据
    begin
        if(!rst_n) begin
            choice[0] = 128'h0102_0000_0506_0000_090a_0000_0d0e_0000;
            choice[1] = 128'h0304_0000_0708_0000_0b0c_0000_0f10_0000;
            choice[2] = time_1;
            choice[3] = time_2; end
        else if(!tone_en) begin
            choice[0] = 128'h0000_0000_0000_0000_0000_0000_0000_0000;
            choice[1] = 128'h0000_0000_0000_0000_0000_0000_0000_0000;
            choice[2] = 128'h0000_0000_0000_0000_0000_0000_0000_0000;
            choice[3] = 128'h0000_0000_0000_0000_0000_0000_0000_0000; end
        else if(flag==3'd1) begin
            choice[0] = 128'h0102_0000_0506_0000_090a_0000_0d0e_0000;
            choice[1] = 128'h0304_0000_0708_0000_0b0c_0000_0f10_0000;
            choice[2] = time_1;
            choice[3] = time_2; end
        else if(flag==3'd2) begin
            choice[0] = 128'h1112_1516_191a_1d1e_2122_2526_292a_0000;
            choice[1] = 128'h1314_1718_1b1c_1f20_2324_2728_2b2c_0000;
            choice[2] = time_1;
            choice[3] = time_2; end
        else if(flag==3'd3) begin
            choice[0] = 128'h2d2e_0000_3132_0000_3536_0000_0000_0000;
            choice[1] = 128'h2f30_0000_3334_0000_3738_0000_0000_0000;
            choice[2] = time_1;
            choice[3] = time_2; end
        else begin
            choice[0] = 128'h0000_0000_0000_0000_0000_0000_0000_0000;
            choice[1] = 128'h0000_0000_0000_0000_0000_0000_0000_0000;
            choice[2] = 128'h0000_0000_0000_0000_0000_0000_0000_0000;
            choice[3] = 128'h0000_0000_0000_0000_0000_0000_0000_0000; end
    end

3.消抖单元

按键抖动会项目产生干扰,所以我们要进行按键消抖,下面模块参考了电子森林的案例

module debounce (clk,rst,key,key_pulse);
 
        parameter       N  =  1;                      //要消除的按键的数量
 
        input             clk;
        input             rst;
        input   [N-1:0]   key;                        //输入的按键
        output  [N-1:0]   key_pulse;                  //按键动作产生的脉冲
 
        reg     [N-1:0]   key_rst_pre;                //定义一个寄存器型变量存储上一个触发时的按键值
        reg     [N-1:0]   key_rst;                    //定义一个寄存器变量储存储当前时刻触发的按键值
 
        wire    [N-1:0]   key_edge;                   //检测到按键由高到低变化是产生一个高脉冲
 
        //利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
        always @(posedge clk  or  negedge rst)
          begin
             if (!rst) begin
                 key_rst <= {N{1'b1}};                //初始化时给key_rst赋值全为1,{}中表示N个1
                 key_rst_pre <= {N{1'b1}};
             end
             else begin
                 key_rst <= key;                     //第一个时钟上升沿触发之后key的值赋给key_rst,同时key_rst的值赋给key_rst_pre
                 key_rst_pre <= key_rst;             //非阻塞赋值。相当于经过两个时钟触发,key_rst存储的是当前时刻key的值,key_rst_pre存储的是前一个时钟的key的值
             end    
           end
 
        assign  key_edge = key_rst_pre & (~key_rst);//脉冲边沿检测。当key检测到下降沿时,key_edge产生一个时钟周期的高电平
 
        reg [17:0]    cnt;                       //产生延时所用的计数器,系统时钟12MHz,要延时20ms左右时间,至少需要18位计数器     
 
        //产生20ms延时,当检测到key_edge有效是计数器清零开始计数
        always @(posedge clk or negedge rst)
           begin
             if(!rst)
                cnt <= 18'h0;
             else if(key_edge)
                cnt <= 18'h0;
             else
                cnt <= cnt + 1'h1;
             end  
 
        reg     [N-1:0]   key_sec_pre;                //延时后检测电平寄存器变量
        reg     [N-1:0]   key_sec;                    
 
 
        //延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效
        always @(posedge clk  or  negedge rst)
          begin
             if (!rst) 
                 key_sec <= {N{1'b1}};                
             else if (cnt==18'h3ffff)
                 key_sec <= key;  
          end
       always @(posedge clk  or  negedge rst)
          begin
             if (!rst)
                 key_sec_pre <= {N{1'b1}};
             else                   
                 key_sec_pre <= key_sec;             
         end      
       assign  key_pulse = key_sec_pre & (~key_sec);     
 
endmodule

 

4.计时单元

把时间显示到OLED下半部分,这里的逻辑和上面显示汉字的逻辑是一样的。

 

always@(posedge clk) begin 
    case(minute)
        2'd0:   begin out_1[127:120] <= 8'h39;
                      out_2[127:120] <= 8'h3a; end
        2'd1:   begin out_1[127:120] <= 8'h3b;
                      out_2[127:120] <= 8'h3c; end
        2'd2:   begin out_1[127:120] <= 8'h3d;
                      out_2[127:120] <= 8'h3e; end
        default:begin out_1[127:120] <= 8'h00;
                      out_2[127:120] <= 8'h00; end
    endcase
    out_1[119:112] <= 8'h4f;
    out_2[119:112] <= 8'h50;
    
    case(second_shi)
        4'd0:   begin out_1[111:104] <= 8'h39;
                      out_2[111:104] <= 8'h3a; end
        4'd1:   begin out_1[111:104] <= 8'h3b;
                      out_2[111:104] <= 8'h3c; end
        4'd2:   begin out_1[111:104] <= 8'h3d;
                      out_2[111:104] <= 8'h3e; end
        4'd3:   begin out_1[111:104] <= 8'h3f;
                      out_2[111:104] <= 8'h40; end
        4'd4:   begin out_1[111:104] <= 8'h41;
                      out_2[111:104] <= 8'h42; end
        4'd5:   begin out_1[111:104] <= 8'h43;
                      out_2[111:104] <= 8'h44; end
        default:begin out_1[111:104] <= 8'h00;
                      out_2[111:104] <= 8'h00; end
    endcase
    case(second_ge)
        4'd0:   begin out_1[103:96] <= 8'h39;
                      out_2[103:96] <= 8'h3a; end
        4'd1:   begin out_1[103:96] <= 8'h3b;
                      out_2[103:96] <= 8'h3c; end 
        4'd2:   begin out_1[103:96] <= 8'h3d;
                      out_2[103:96] <= 8'h3e; end
        4'd3:   begin out_1[103:96] <= 8'h3f;
                      out_2[103:96] <= 8'h40; end
        4'd4:   begin out_1[103:96] <= 8'h41;
                      out_2[103:96] <= 8'h42; end
        4'd5:   begin out_1[103:96] <= 8'h43;
                      out_2[103:96] <= 8'h44; end
        4'd6:   begin out_1[103:96] <= 8'h45;
                      out_2[103:96] <= 8'h46; end
        4'd7:   begin out_1[103:96] <= 8'h47;
                      out_2[103:96] <= 8'h48; end
        4'd8:   begin out_1[103:96] <= 8'h49;
                      out_2[103:96] <= 8'h4a; end
        4'd9:   begin out_1[103:96] <= 8'h4b;
                      out_2[103:96] <= 8'h4c; end
        default:begin out_1[103:96] <= 8'h00;
                      out_2[103:96] <= 8'h00; end
    endcase
    out_1[95:0] <= 96'h0000_0000_0000_0000_0000_0000;
    out_2[95:0] <= 96'h0000_0000_0000_0000_0000_0000;
end

处理时间的变化,并设定了播放的时间

always@(posedge clk or negedge rst) begin
    if(!rst) begin
        minute <= 2'd1; 
        second_ge <= 1'b0;
        second_shi <= 1'b0;
        flag_pre <= 3'd1;
        count <= 1'b0; end
    else if(!tone_en) begin
        count <= count; 
        end
    else if(flag_pre != flag) begin
        case(flag)
            3'd1:   begin second_ge <= 1'b0;second_shi <= 1'b0;minute <= 2'd1; end
            3'd2:   begin second_ge <= 1'b0;second_shi <= 1'b0;minute <= 2'd1; end
            3'd3:   begin second_ge <= 1'b0;second_shi <= 1'b0;minute <= 2'd1; end
        endcase
        flag_pre <= flag;
        count <= 1'b0;
        end
    else if((count >= 24'd12000000)&&(second_ge == 4'd0)&&(second_shi != 4'd0)) begin
        second_ge <= 4'd9;
        second_shi <= second_shi - 1'b1;
        count <= 1'b0;
        end
    else if((count >= 24'd12000000)&&(second_ge == 4'd0)&&(second_shi == 4'd0)&&(minute !=2'd0)) begin
        second_ge <= 4'd9;
        second_shi <= 4'd5;
        minute <= minute - 1'b1;
        count <= 1'b0;
        end
    else if((count >= 24'd12000000)&&(second_ge == 4'd0)&&(second_shi == 4'd0)&&(minute ==2'd0)) begin
        second_ge <= 1'b0; 
        second_shi <= 1'b0;  
        minute <= 1'b0;
        count <= 1'b0; end
    else if(count >= 24'd12000000) begin
        count <= 1'b0;
        second_ge <= second_ge - 1'b1; end
    else begin count <= count + 1'b1; end
end

 

四、项目总结

在本次项目中,我实现了结合音符和节拍演奏曲子,使用中间变量flag来切换不同的曲子,然后显示不同的内容,进行了按键消抖,基本完成了项目的所有要求。

在这一个寒假中,我通过参加“2022寒假在家练”活动,学会了FPGA的开发语言Verilog的基本语法,重温了很多数电的知识,掌握了FPGA的基本开发流程。平常做一些东西,我使用过uno、esp8266和stm32这些开发板,开发它们和开发FPGA的差别很大,我一开始很不习惯,但是为了能完成项目,我去b站找了一个FPGA的入门教程,因为在群里苏老师说使用FPGA和开车是一样的,原理都是相同的,看的教程除了晶振是50Mhz的,和我们用的不一样,基本一致,很好移植。

会遇到很多问题,但是只要耐心去看代码,不会的去参考搜索内容琢磨,只要花时间,都可以解决。这个项目是我大学以来做过最有意思的一个项目,从0开始,最终实现,虽然这个项目对很多人不难,但是完成它,增强了我继续学习下去的信心!

02/27/2022

附件下载
beeper_impl_1.rbt
可直接烧录的rbt文件
Beeper.zip
项目全部文件
automake.log
资源报告
团队介绍
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号