项目需求
- 实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
- 实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
- 定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
- PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
实现思路
- DS18B20模块(Ds18b20.v v)
- OLED模块(v)
- UART串口发送温度,时间模块(uart_t.v)
- UART串口接受音乐模块(uart_r.v)
- BEEP蜂鸣器模块(beepall.v)
- 时钟模块(v FreqDiv.v)
- TOP顶层模块(v)
模块间的调用关系参看top.v文件就可以一目了然
- DS18B20模块(Ds18b20.v v)
Ds18b20.v是参考的硬禾学堂给的例程,这里不再赘述,想一探究竟的小伙伴可以从我打包的文件中查看或者直接在硬禾学堂的网站上查看即可
TempDatCvrt.v是我把16位的温度数据转换成百位十位个位以及小数位后的数据,为了最后给OLED屏显示和通过串口UART发送给PC端。因为代码逻辑也很简单,所以就不在这里展开,想一探究竟的小伙伴可以下载附件进行查看。不过后面在说明fresh标志变量(标志OLED是否接收到PC串口发来的数据,以决定是否更新当前的温度和时间信息)时,会提到这里面的一部分代码。
- OLED模块(v)
这里的核心内容依然是硬禾学堂给的例程中的代码,我当初看到这个代码的说明是要把此代码的OLED驱动移植到其他项目要花费一定的周折。但是我仔细拜读了官方的代码,感觉真是如沐春风。状态机应用的出神入化,在我理解更深入后,发现只要稍稍修改一下MAIN部分就能为我所用。所以我就不重复造轮子了,直接在例程代码的基础上进行了修改。
OLED显示效果如下
核心部分如下,想要查看完整代码的小伙伴可以下载附件进行查看
MAIN:begin
if(cnt_main >= 5'd6) cnt_main <= 5'd5;
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 <= " TEMP ";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 <= " TIME ";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:if(temp_h == 4'd2)
begin
y_p <= 8'hb1; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd 5; char <= {"-", 4'd0, temp_t, 4'd0, temp_u, ".", 4'd0, temp_d}; state <= SCAN;
end
else if(temp_h == 4'd1)
begin
y_p <= 8'hb1; x_ph <= 8'h12; x_pl <= 8'h00; num <= 5'd 5; char <= {4'd0, temp_h, 4'd0, temp_t, 4'd0, temp_u, ".", 4'd0, temp_d}; state <= SCAN;
end
else //if(temp_h == 4'd0)
begin
y_p <= 8'hb1; x_ph <= 8'h13; x_pl <= 8'h00; num <= 5'd 4; char <= {4'd0, temp_t, 4'd0, temp_u, ".", 4'd0, temp_d}; state <= SCAN;
end
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可以有5‘d0-5’d6这么多值,5‘d1是显示TEMP,5’d3是显示TIME。
5’d5是在显示具体的温度信息,之所以在代码中对temp_h进行多次判断,是因为温度可能为负,也可是一百多度,我用temp_h=1代表温度是一百多度,temp_h=2代表温度为负,temp_h=0代表温度为正但是没有超过一百度
- UART串口发送温度,时间模块(uart_t.v)
这个模块我参考了叶开好兄弟的代码,我在开始我的项目前,已经有好些同学上传了自己的项目,我拜读了叶开好兄弟的的串口发送的代码,感觉很不错,我在他的基础上做了一定的修改后为我所用了。在这里表示感谢,纵然串口的代码网络上到处都是,但学谁的不是学呢?哈哈哈。
这里我就不把代码贴出来了。我这里的代码比较基础,可圈可点的地方比较少
- UART串口接受音乐模块(uart_r.v)
这里代码初始也是参考的CSDN https://blog.csdn.net/weifengdq/article/details/103168587这个好兄弟的,他原本的代码是实现串口本地回环实验,我则在他的基础上改编直接把收到的数据(其实在这里就是音乐数据)用于后续的蜂鸣器发声。
最开始的音频数据分别是L_6, M_1, M_3, M_5, M_3, M_3, M_3, M_2,其他的大家伙可以查看我的附件,是不是很简单呢,哈哈哈!
这里的逻辑也是通用的,自己创新发挥的地方很少,故不贴代码了,感兴趣的小伙伴可以下载附件进行研读。
- BEEP蜂鸣器模块(beepall.v)
这里的蜂鸣器我是参考CSDN上的这位好兄弟的,并在他的基础上进行了完善https://blog.csdn.net/koala_cola/article/details/106904927
他用的音乐是吴金黛的森林狂想曲,我听了听,感觉很不错,然后我用的也是这个歌曲,想听的小伙伴可以看看我发布的视频,视频中有两处都播放的这个音乐(一个是整点报时,一个是温度报警后,电脑发送的音乐数据)
我根据乐谱参数:D=F/2K (D:参数,F:时钟频率,K:音高频率),来计算相关参数
代码中,count_end1是分频(12MHz)的系数
//串口控制 曲谱 产生分频的系数并描述出曲谱
always @(posedge clk)
begin
if(uart_done)
begin
case(uart_data)
8'h1:count_end1 <=16'd22935; //L1,
8'h2:count_end1 <=16'd20428; //L2,
8'h3:count_end1 <=16'd18203; //L3,
8'h4:count_end1 <=16'd17181; //L4,
8'h5:count_end1 <=16'd15305; //L5,
8'h6:count_end1 <=16'd13635; //L6,
8'h7:count_end1 <=16'd12147; //L7,
8'h8:count_end1 <=16'd11464; //M1,
8'h9:count_end1 <=16'd10215; //M2,
8'ha:count_end1 <=16'd9100; //M3,
8'hb:count_end1 <=16'd8589; //M4,
8'hc:count_end1 <=16'd7652; //M5,
8'hd:count_end1 <=16'd6817; //M6,
8'he:count_end1 <=16'd6073; //M7,
8'hf:count_end1 <=16'd5740; //H1,
8'h10:count_end1 <=16'd5107; //H2,
8'h11:count_end1 <=16'd4549; //H3,
8'h12:count_end1 <=16'd4294; //H4,
8'h13:count_end1 <=16'd3825; //H5,
8'h14:count_end1 <=16'd3408; //H6,
8'h15:count_end1 <=16'd3036; //H7,
default:count_end1 <=16'd65535;// 无声
endcase
end
end
看到这里,大家肯定疑惑为什么分频系数是count_end1呢,原来这个蜂鸣器有两种情况需要播放音乐,一个是整点报时的音乐,一个是PC根据报警的温度要发送音乐数据。所以两个的分频系数一个是count_end另一个就是count_end1啦。
由于两者功能及思路有一定重合,我就不在这里过多阐述另一种了,count_end1明白了,count_end自然就明白了。
这里还要说明的是如何在PC端通过串口发送音乐数据(其实就是分频数据的“下标”)时让时间和温度都停止更新,但是发送音乐数据完毕,时间和温度又能突然的更新呢。
请看以下代码
always@(posedge uart_done or posedge clk1h)
begin
if(uart_done)
begin
uart_done_cnt <= 1'b1;
end
else
if(uart_done_cnt)
begin
fresh <= 1'b0;
uart_done_cnt <= 1'b0;
end
else
fresh <= 1'b1;
end
其中uart_done在串口每发完一个数据都要有一个小的脉冲,因为我发数据的间隔是250ms(这样音乐的效果很完美),所以uart_done相比于clk1h(1Hz时钟)要快很多,只要每次clk1h上升沿来临uart_done_cnt(表征1s内uart_done是否有数据发送)不是0就说明PC的串口还在发送音乐数据,此时fresh(更新标志位)就是0(代表不更新)。
一旦clk1h上升沿来临时发现uart_done_cnt为0说明此时PC的串口已经停止。所以就让fresh为1表示,开始更新。
这个fresh作为输入给了OLED显示模块,使得OLED可以根据fresh这个刷新标志变量来决定当前的OLED数据是当前的还是曾经的。
其中TempDatCvrt.v中
reg [3:0] dat_h_now;
reg [3:0] dat_t_now;
reg [3:0] dat_u_now;
reg [3:0] dat_d_now;
reg [3:0] dat_h_past;
reg [3:0] dat_t_past;
reg [3:0] dat_u_past;
reg [3:0] dat_d_past;
always@(posedge clk)
begin
dat_h <= dat_h_past;
dat_t <= dat_t_past;
dat_u <= dat_u_past;
dat_d <= dat_d_past;
end
always@(posedge clk)
begin
if(fresh_flag)
begin
dat_h_past <= dat_h_now;
dat_t_past <= dat_t_now;
dat_u_past <= dat_u_now;
dat_d_past <= dat_d_now;
end
else
begin
dat_h_past <= dat_h_past;
dat_t_past <= dat_t_past;
dat_u_past <= dat_u_past;
dat_d_past <= dat_d_past;
end
end
上面代码展现了温度数据如何通过fresh标志来决定当前的数据是实时更新还是用接收PC端音乐数据初始时刻的温度。
同样的逻辑也用于了时间部分(提前陈述clock.v模块的内容),因为逻辑相似,就不在赘述
reg [3:0] six_now;
reg [3:0] five_now;
reg [3:0] four_now;
reg [3:0] three_now;
reg [3:0] two_now;
reg [3:0] one_now;
reg [3:0] six_past;
reg [3:0] five_past;
reg [3:0] four_past;
reg [3:0] three_past;
reg [3:0] two_past;
reg [3:0] one_past;
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
- 时钟模块(v FreqDiv.v)
上面已经说明了部分的时钟模块clock.v的内容(根据fresh标志变量决定是否实时更新)
接下来我说明一下剩余的部分,其实核心逻辑也比较简单
其中one~six分别是时间xx:xx:xx的最小位到最高位
无非是one计数到9,然后one变为1,然后two加1,two变到5再进位时,three加1,two变为0,以此类推。
然后为了能通过按键调整时间(这里面我用3个按键分别对应分钟数的增加,减少和小时数的减少),正如视频中说到的那样,调整时间是循环的,比如小时数已经是00了,我再往下减就变成了最高的23了。
大家可以看看代码,看了我上面的逻辑说明,代码应该就浅显很多了。
always@(posedge clk1h, negedge rst)
begin
if(!rst)
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://mindown
begin
if(three_now > 0)
three_now <= three_now - 1;
else//three_now = 0
begin
three_now <= 9;
if(four_now > 0)
four_now <= four_now - 1;
else//four_now = 0
four_now <= 5;
end
end
3'b101://minup
begin
if(three_now < 9)
three_now <= three_now + 1;
else//three_now = 9
begin
three_now <= 0;
if(four_now < 5)
four_now <= four_now + 1;
else//four_now = 5
four_now <= 0;
end
end
default://hourdown 011
begin
if(five_now > 0)
five_now <= five_now - 1;
else//five_now = 0
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//if(six_now == 0)
begin
five_now <= 3;
six_now <= 2;
end
end
end
endcase
end
end
还要说明的是分频模块,这里我其实用的是硬禾学堂例程中的时钟分频模块,它的特色是不仅能实现通常的偶数分频,还能实现奇数分频,思路很巧妙,正巧我在开始我的工程前自己实现过这个代码,所以就直接用了我之前编写好的代码了。代码很巧妙,用原始时钟的上升沿和下降沿进行计数,然后巧妙的利用逻辑相与实现了奇数分频。
代码如下,对比着图看,真是能感受到数字逻辑的美妙!
module FreqDiv(
clk,
rst,
clkout
);
input clk, rst;
output clkout;
// clock clock1(
// .clk(clk),
// .rst(rst),
// .clk1h(clkout)
// );
//默认参数是1Hz
parameter N = 12000000;//N分频
parameter WIDTH = 32;//N用¿制表示时的宽度
reg clkp;//clk上升沿产生的分频
reg clkn;//clk下降沿产生的分频
reg [WIDTH-1:0]cntp;//计数clk上升沿数释reg [WIDTH-1:0]cntn;//计数clk下降沿数释
//产生clkp
always@(posedge clk, negedge rst)
begin
if(!rst)
clkp <= 0;
else
begin
if(cntp <= N/2)
clkp <= 1;
else
clkp <= 0;
end
end
//产生clkn
always@(negedge clk, negedge rst)
begin
if(!rst)
clkn <= 0;
else
begin
if(cntn <= N/2)
clkn <= 1;
else
clkn <= 0;
end
end
//产生cntp
always@(posedge clk, negedge rst)
begin
if(!rst)
cntp <= 0;
else
if(cntp == N-1)
cntp <= 0;
else
cntp <= cntp + 1;
end
//产生cntn
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;
endmodule
- TOP顶层模块(v)
最后是顶层模块,通过这次verilog的编程训练,我深刻体会到模块化编程的好处,实现一个一个相对独立的功能,留下输入与输出接口,就像搭积木般的能在数字逻辑世界中实现自己的摩天大楼,上面所说到的模块或者是没说到的模块在top中互相调用,用区区的9个输入和7个输出实现了本次寒假在家练的全部功能,感受到了数字世界的美妙。我贴出代码感受一下。
感兴趣的小伙伴可以下载附件仔细研究,毕竟这里的代码不方便进一步的学习。
module top(
input clk,
input rst,//k1
input mindown,//按键控制时间(分钟数)减少,k2
input minup,//按键控制时间(分钟数)增加,k3
input hourdown,//按键控制时间(小时数)减少,k4
inout tone_en,//FPGA声音开关sw1
input onoff_uart_t,//发送串口开关sw2
inout one_wire,
input uart_rxd, //串口输入
output oled_csn, //OLCD液晶屏使胊
output oled_rst, //OLCD液晶屏复佊
output oled_dcn, //OLCD数据指令控制
output oled_clk, //OLCD时钟信号
output oled_dat, //OLCD数据信号
output uart_out, //串口输出
output beep
);
wire[3:0] temp_h;
wire[3:0] temp_t;
wire[3:0] temp_u;
wire[3:0] temp_d;
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[15:0] data_out;
wire[7:0] uart_data;
wire clk1h;
wire fresh_flag;
OLED12832 OLED12832_v1(
.clk(clk),
.rst_n(rst),
.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_flag),
.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),
.mindown(mindown),
.minup(minup),
.hourdown(hourdown),
.fresh_flag(fresh_flag),
.six(six),
.five(five),
.four(four),
.three(three),
.two(two),
.one(one)
);
uart_tx uart_tx_v1(
.clk_in(clk),
.onoff_uart_t(onoff_uart_t),
.temp_d(temp_d),
.temp_t(temp_t),
.temp_u(temp_u),
.six(six),
.five(five),
.four(four),
.three(three),
.two(two),
.one(one),
.uart_out(uart_out)
);
song song_v1(
.clk(clk),
.clk1h(clk1h),
.rst(rst),
.tone_en(tone_en),
.uart_done(uart_done),
.uart_data(uart_data),
.two(two),
.three(three),
.four(four),
.fresh_flag(fresh_flag),
.beep(beep)
);
uart_recv uart_recv_v1(
.sys_clk(clk),
.sys_rst_n(rst),
.uart_rxd(uart_rxd),
.uart_done(uart_done),
.uart_data(uart_data)
);
endmodule
完成的功能
完成了项目需求中的全部功能,在这里不再赘述
遇到的难题
这里遇到的难题最终都解决了,我罗列一下我之前遇到的难题,纵使现在看也不算什么难题了
1.PC如何通过串口如何定时给FPGA发送音乐数据
手动发送是不可能的了,自动发送的话怎么办呢,正在我一筹莫展之时,想通过写代码来自动进行发送时,突然发现我的串口调试助手有自动间隔时间发送的功能,真是太好了,省的我自己编写代码发送音乐数据了
2.PC端发送音乐数据时,FPGA要停止更新时间和温度数据,我就想用一个更新标志位来决定OLED是否进行更新,但是我写好代码,逻辑检查来检查去,实在找不出问题,但是实验结果就是和预期不一致,在万般无奈下,在warning中找到了答案,原来是我fresh标志位用了3个变量的边沿进行了判断进行了赋值操作,结果相关代码根本就没有综合,导致了最后的失误,进过我改正后,终于成果了,相关代码其实我在上面已经贴出来过了,现在我再贴出来让大家看看
always@(posedge uart_done or posedge clk1h)
begin
if(uart_done)
begin
uart_done_cnt <= 1'b1;
end
else
if(uart_done_cnt)
begin
fresh <= 1'b0;
uart_done_cnt <= 1'b0;
end
else
fresh <= 1'b1;
end
3.还有就是蜂鸣器如何生成音乐呢?因为我之前玩过51单片机,当时我记得有个工程就是播放音乐,我记忆中那个工程涉及到了很多乐理知识,很是劝退,正在我惊慌失措不知所以时,我看到了CSDN上一位好兄弟的简化的播放音乐知识(上面提到了,要了解的小伙伴可以通过链接进行查看),原来通过乐谱就可以简单的算出分频的系数,然后每个音符只需要同样的250ms就能实现完美的音乐了,真是太妙了。
未来计划与建议
未来的计划我其实想把OLED和DS18B20以及串口部分的通过通信协议进行verilog实现的部分自己实现一下,因为我觉得这才是数字逻辑的核心,只是会调用和能自己亲自实现一个通信协议完全不是一个等级。而且这个小脚丫的项目做完,后续我还有很多其他东西要学习,时间很紧迫,所以我要抓紧寒假前的一切时间先把最核心的最硬核的部分抓紧学一学,如果现在不学,以后也很难再抽出时间学了。本次的项目之所以这么晚才发布也是因为有各种各样的学习任务使得小脚丫不得不一次又一次的延期。不过万幸的是,硬禾学堂提供了很好的平台,有老师们提供的入门代码,有好兄弟们提供的好代码,也有好小伙伴们提供的好思路,感谢大家!(好像跑题了)