项目需求:
-
通过PWM产生不同的音调,并驱动板上蜂鸣器将音调输出
-
能够播放三首不同的曲子,每个曲子的时间长度为1分钟,可以切换播放
-
曲子的切换使用小脚丫核心板上的按键,需要有按键消抖的功能
-
播放的曲子的名字在OLED屏幕上显示出来(汉字显示)
管脚设计:
使用模块:
- OLED模块(v) (显示时间和汉字歌名)
- BEEP蜂鸣器模块(beepall.v) (输出音乐到蜂鸣器)
- 时钟模块(v FreqDiv.v) (进行时钟的分频以及输出时间信号到显示模块)
- TOP顶层模块(v) (将各个模块联系起来)
- 消抖模块 (xiaodou.v) (防止误判进行按键消抖,给予一个延时)
各个模块介绍:
OLED模块(v)
OLED模块是最重要的一个模块之一,也是我花了许久才完成的。这个模块也是我看了硬禾学堂给的例程之后想出来的,通过模仿硬禾学堂的程序,写出了我自己理解的OLED显示模块。
成果如下:
下面是核心代码:
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软件,输入汉字,然后得到的位点,再放到字库里面,不过有的字太大,系统设计的位点比较别扭。
我选择了绘画模式,一个一个点画进去,最后得到六个字的位点。
比较一下下面两段程序
//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赋值到最开始的值,就可以实现单曲循环,只能通过按键进行切歌。
再来看我是如何输入乐谱的,我使用周杰伦的《不该》举例:
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)
我们可以看到,但按键按下的那一刻,存在一段时间的抖动,同时在释放按键的一段时间里也是存在抖动的,这就可能导致状态在识别的时候可能检测为多次的按键,因为运行过程中普通的检测一次状态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小游戏,这次的项目包含了几个小任务,需要一个个解决,也锻炼了我解决项目问题的能力。希望以后可以继续学习,把本次实践学到知识加以利用,更上一层楼!