一、实现功能
-
实现一个可定时时钟的功能,用小脚丫FPGA核心模块的4个按键设置当前的时间,OLED显示数字钟的当前时间,精确到分钟即可,到整点的时候比如8:00,蜂鸣器报警,播放音频信号,最长可持续30秒;
-
实现温度计的功能,小脚丫通过板上的温度传感器实时测量环境温度,并同时间一起显示在OLED的屏幕上;
-
定时时钟整点报警的同时,将温度信息通过UART传递到电脑上,电脑上能够显示当前板子上的温度信息(任何显示形式都可以),要与OLED显示的温度值一致;
-
PC收到报警的温度信号以后,将一段音频文件(自己制作,持续10秒钟左右)通过UART发送给小脚丫FPGA,蜂鸣器播放收到的这段音频文件,OLED屏幕上显示的时间信息和温度信息都停住不再更新;
5.音频文件播放完毕,OLED开始更新时间信息和当前的温度信息
二、相关模块和代码:
一、首先是顶层模块
module yanzheng
(input clk,
input rst_n,
input rx,
input [2:0]key,
inout wire one_wire,
output tx,
output fmq,
output oled_csn,
output oled_rst,
output oled_dcn,
output oled_clk,
output oled_dat
);
reg [3:0]data1 ;
reg [3:0]data2 ;
reg [3:0]data3 ;
wire [3:0]m_g1;
wire [3:0]m_s1;
wire [3:0]f_g1;
wire [3:0]f_s1;
wire [3:0]s_g1;
wire [3:0]s_s1;
SHIZHONG u_SHIZHONG(
.clk (clk),
.rst_n (rst_n),
.key1 (key1),
.flag_out (flag_s),
.flag_in (flag_f),
.m_g (m_g1),
.m_s (m_s1),
.f_g (f_g1),
.f_s (f_s1),
.s_g (s_g1),
.s_s (s_s1)
);
wire flag_s;
OLED u_OLED(
.clk (clk),
.rst (rst_n),
.hour_1 (s_s1),
.hour_0 (s_g1),
.min_1 (f_s1),
.min_0 (f_g1),
.m_1 (m_s1),
.m_0 (m_g1),
.tem_1 (data1),
.tem_0 (data2),
.tem_3 (data3),
.oled_csn (oled_csn),
.oled_rst (oled_rst),
.oled_dcn (oled_dcn),
.oled_clk (oled_clk ),
.oled_dat (oled_dat )
);
wire [15:0] data_out;
DS18B20 u_DS18B20(
.clk_in (clk),
.rst_n_in (rst_n),
.one_wire (one_wire),
.data_out1 (data_out),
.flag_in (flag_f)
);
always@(data_out)
begin
data1<=data_out[11:4]/10;
data2<=data_out[11:4]%10;
data3<=data_out[3]*5+data_out[2]*2+data_out[1];
end
wire [2:0]key1;
key_filter u_key_filter1(
.clk (clk),
.rst (rst_n),
.key (key),
.key_pulse (key1)
);
uart_rx u_uart_rx(
.clk (clk),
.rst_n (rst_n),
.rx (rx),
.po_data (po_data),
.po_flag (po_flag)
);
wire po_flag;
wire [7:0]po_data;
reg [7:0]pi_data;
uart_tx u_uart_tx(
.clk (clk),
.rst_n (rst_n),
.pi_data (pi_data),
.pi_flag (flag_s),
.tx (tx)
);
wire flag_f;
FMQ u_FMQ(
.clk (clk),
.rst_n (rst_n),
.flag (flag_s),
.po_data (po_data),
.flag1 (flag_f),
.fmq (fmq)
);
always@(data_out)
begin
pi_data<=data_out[11:4];
end
endmodule
遇到的问题
1、reg/wire的用法;
一开始我并不认为这个能难住我,wire用不了就用reg白,直到我开始做顶层文件了,我发现有很多的中间信号,输入一般为wire,可是我想让他进行一个时序逻辑运算,发现必须是reg,这个真的难为我好久,最后发现了,在定义一个变量reg型就可以了大概这个样子reg<=wire就可以了,
2、模块例化;
这个是是我前期的一个坎,听了很多节课,就是听不懂,一遍一遍听,完全不知道为啥毫不相干的东西就加一个.x (x)就连了起来,不过在某天晚上睡觉前瞎想的是时候让大概逻辑在脑子里过了一遍,恍然大悟就莫名其妙掌握了。
二、时钟模块
本模块我主要使用了很多的计数器,设置其结束条件就可以得到60进制和24进制的计时器,也就是时钟;计时器代码如下
60进制——秒钟
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
m_g<=4'd0;
end
else if(add_m_g)begin
if(end_m_g)
m_g<=4'd0;
else
m_g<=m_g+4'd1;
end
end
assign add_m_g= end_cnt&&flag_in==1'b0;//加一条件
assign end_m_g=add_m_g&&m_g==4'd9;//结束条件
//秒十
always @(posedge clk or negedge rst_n )begin
if(!rst_n)begin
m_s<=4'd0;
end
else if(add_m_s)begin
if(end_m_s)
m_s<=4'd0;
else
m_s<=m_s+4'd1;
end
end
assign add_m_s= end_m_g;//加一条件
assign end_m_s=add_m_s&&m_s==4'd5;//结束条件
24进制——小时
always @(posedge clk or negedge rst_n)begin
if(!rst_n)begin
s_g<=4'd0;
end
else if(add_s_g)begin
if(end_s_g)
s_g<=4'd0;
else
s_g<=s_g+4'd1;
end
end
assign add_s_g=end_f_s||cnt1[2]==1;//加一条件
assign end_s_g=add_s_g&&(s_g==4'd9 || (s_s==4'd2&&s_g==4'd3));//结束条件
//时十
always @(posedge clk or negedge rst_n )begin
if(!rst_n)begin
s_s<=4'd0;
end
else if(add_s_s)begin
if(end_s_s)
s_s<=4'd0;
else
s_s<=s_s+4'd1;
end
end
assign add_s_s= end_s_g;//加一条件
assign end_s_s=add_s_s && s_s==4'd2;//结束条件
在此模块中为了完成那个定时报警系统,和播放音频时暂停的效果我又在其中添加了两个标志信号用来控制此过程。
遇到的问题
1、进制问题,
60进制的那个计时器还没有难到我,但24进制的就不一样了,思索好久,最后在b站上找到了相关视频,又深刻的加深了||(或们)的用法,之后问题得以解决。
三、DS18B20模块
本模块调用了电子森林的代码,但对其进行了稍加修改,可以命令其是否输出数据,什么时候输出数据(好吧,其实就是一个标志信号)
module DS18B20
(
input clk_in, //系统时钟
input rst_n_in, //系统复位,低有效
inout one_wire,
input flag_in,//DS18B20Z传感器单总线,双向管脚
output reg [15:0] data_out1 //DS18B20Z有效温度数据输出
);
/*
本设计通过驱动DS18B20Z芯片获取温度数据,
需要了解inout类型的接口如何实现双向通信,
中间涉及各种不同的延时和寄存器指令操作,注释部分以作简要说明,更多详情需参考数据手册
*/
localparam IDLE = 3'd0;
localparam MAIN = 3'd1;
localparam INIT = 3'd2;
localparam WRITE = 3'd3;
localparam READ = 3'd4;
localparam DELAY = 3'd5;
//计数器分频产生1MHz的时钟信号
reg clk_1mhz;
reg [2:0] cnt_1mhz;
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) begin
cnt_1mhz <= 3'd0;
clk_1mhz <= 1'b0;
end else if(cnt_1mhz >= 3'd5) begin
cnt_1mhz <= 3'd0;
clk_1mhz <= ~clk_1mhz; //产生1MHz分频
end else begin
cnt_1mhz <= cnt_1mhz + 1'b1;
end
end
reg [2:0] cnt;
reg one_wire_buffer;
reg [3:0] cnt_main;
reg [7:0] data_wr;
reg [7:0] data_wr_buffer;
reg [2:0] cnt_init;
reg [19:0] cnt_delay;
reg [19:0] num_delay;
reg [3:0] cnt_write;
reg [2:0] cnt_read;
reg [15:0] temperature;
reg [7:0] temperature_buffer;
reg [2:0] state = IDLE;
reg [2:0] state_back = IDLE;
//使用1MHz时钟信号做触发完成下面状态机的功能
always@(posedge clk_1mhz or negedge rst_n_in) begin
if(!rst_n_in) begin
state <= IDLE;
state_back <= IDLE;
cnt <= 1'b0;
cnt_main <= 1'b0;
cnt_init <= 1'b0;
cnt_write <= 1'b0;
cnt_read <= 1'b0;
cnt_delay <= 1'b0;
one_wire_buffer <= 1'bz;
temperature <= 16'h0;
end else begin
case(state)
IDLE:begin //IDLE状态,程序设计的软复位功能,各状态异常都会跳转到此状态
state <= MAIN; //软复位完成,跳转之MAIN状态重新工作
state_back <= MAIN;
cnt <= 1'b0;
cnt_main <= 1'b0;
cnt_init <= 1'b0;
cnt_write <= 1'b0;
cnt_read <= 1'b0;
cnt_delay <= 1'b0;
one_wire_buffer <= 1'bz;
end
MAIN:begin //MAIN状态控制状态机在不同状态间跳转,实现完整的温度数据采集
if(cnt_main >= 4'd11) cnt_main <= 1'b0;
else cnt_main <= cnt_main + 1'b1;
case(cnt_main)
4'd0: begin state <= INIT; end //跳转至INIT状态进行芯片的复位及验证
4'd1: begin data_wr <= 8'hcc;state <= WRITE; end //主设备发出跳转ROM指令
4'd2: begin data_wr <= 8'h44;state <= WRITE; end //主设备发出温度转换指令
4'd3: begin num_delay <= 20'd750000;state <= DELAY;state_back <= MAIN; end //延时750ms等待转换完成
4'd4: begin state <= INIT; end //跳转至INIT状态进行芯片的复位及验证
4'd5: begin data_wr <= 8'hcc;state <= WRITE; end //主设备发出跳转ROM指令
4'd6: begin data_wr <= 8'hbe;state <= WRITE; end //主设备发出读取温度指令
4'd7: begin state <= READ; end //跳转至READ状态进行单总线数据读取
4'd8: begin temperature[7:0] <= temperature_buffer; end //先读取的为低8位数据
4'd9: begin state <= READ; end //跳转至READ状态进行单总线数据读取
4'd10: begin temperature[15:8] <= temperature_buffer; end //后读取的为高8为数据
4'd11: begin state <= IDLE;data_out <= temperature; end //将完整的温度数据输出并重复以上所有操作
default: state <= IDLE;
endcase
end
INIT:begin //INIT状态完成DS18B20Z芯片的复位及验证功能
if(cnt_init >= 3'd6) cnt_init <= 1'b0;
else cnt_init <= cnt_init + 1'b1;
case(cnt_init)
3'd0: begin one_wire_buffer <= 1'b0; end //单总线复位脉冲拉低
3'd1: begin num_delay <= 20'd500;state <= DELAY;state_back <= INIT; end //复位脉冲保持拉低500us时间
3'd2: begin one_wire_buffer <= 1'bz; end //单总线复位脉冲释放,自动上拉
3'd3: begin num_delay <= 20'd100;state <= DELAY;state_back <= INIT; end //复位脉冲保持释放100us时间
3'd4: begin if(one_wire) state <= IDLE; else state <= INIT; end //根据单总线的存在检测结果判定是否继续
3'd5: begin num_delay <= 20'd400;state <= DELAY;state_back <= INIT; end //如果检测正常继续保持释放400us时间
3'd6: begin state <= MAIN; end //INIT状态操作完成,返回MAIN状态
default: state <= IDLE;
endcase
end
WRITE:begin //按照DS18B20Z芯片单总线时序进行写操作
if(cnt <= 3'd6) begin //共需要发送8bit的数据,这里控制循环的次数
if(cnt_write >= 4'd6) begin cnt_write <= 1'b1; cnt <= cnt + 1'b1; end
else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
end else begin
if(cnt_write >= 4'd8) begin cnt_write <= 1'b0; cnt <= 1'b0; end //两个变量都恢复初值
else begin cnt_write <= cnt_write + 1'b1; cnt <= cnt; end
end
//对于WRITE状态中cnt_write来讲,执行过程为:0;[1~6]*8;7;8;
case(cnt_write)
//lock data_wr
4'd0: begin data_wr_buffer <= data_wr; end //将需要写出的数据缓存
//发送 1bit 数据的用时在60~120us之间,参考数据手册
4'd1: begin one_wire_buffer <= 1'b0; end //总线拉低
4'd2: begin num_delay <= 20'd2;state <= DELAY;state_back <= WRITE; end //延时2us时间,保证15us以内
4'd3: begin one_wire_buffer <= data_wr_buffer[cnt]; end //先发送数据最低位
4'd4: begin num_delay <= 20'd80;state <= DELAY;state_back <= WRITE; end //延时80us时间
4'd5: begin one_wire_buffer <= 1'bz; end //总线释放
4'd6: begin num_delay <= 20'd2;state <= DELAY;state_back <= WRITE; end //延时2us时间
//back to main
4'd7: begin num_delay <= 20'd80;state <= DELAY;state_back <= WRITE; end //延时80us时间
4'd8: begin state <= MAIN; end //返回MAIN状态
default: state <= IDLE;
endcase
end
READ:begin //按照DS18B20Z芯片单总线时序进行读操作
if(cnt <= 3'd6) begin //共需要接收8bit的数据,这里控制循环的次数
if(cnt_read >= 3'd5) begin cnt_read <= 1'b0; cnt <= cnt + 1'b1; end
else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
end else begin
if(cnt_read >= 3'd6) begin cnt_read <= 1'b0; cnt <= 1'b0; end //两个变量都恢复初值
else begin cnt_read <= cnt_read + 1'b1; cnt <= cnt; end
end
case(cnt_read)
//读取 1bit 数据的用时在60~120us之间,总线拉低后15us时间内读取数据,参考数据手册
3'd0: begin one_wire_buffer <= 1'b0; end //总线拉低
3'd1: begin num_delay <= 20'd2;state <= DELAY;state_back <= READ; end //延时2us时间
3'd2: begin one_wire_buffer <= 1'bz; end //总线释放
3'd3: begin num_delay <= 20'd5;state <= DELAY;state_back <= READ; end //延时5us时间
3'd4: begin temperature_buffer[cnt] <= one_wire; end //读取DS18B20Z返回的总线数据,先收最低位
3'd5: begin num_delay <= 20'd60;state <= DELAY;state_back <= READ; end //延时60us时间
//back to main
3'd6: begin state <= MAIN; end //返回MAIN状态
default: state <= IDLE;
endcase
end
DELAY:begin //延时控制
if(cnt_delay >= num_delay) begin //延时控制,延时时间由num_delay指定
cnt_delay <= 1'b0;
state <= state_back; //很多状态都需要延时,延时后返回哪个状态由state_back指定
end else cnt_delay <= cnt_delay + 1'b1;
end
endcase
end
end
assign one_wire = one_wire_buffer;
reg [15:0]data_out;
always@(posedge clk_in or negedge rst_n_in)begin
if(!rst_n_in)
data_out1<=15'b0;
else if(flag_in==1'b0)
data_out1<=data_out;
else if(flag_in==1'b1)
data_out1<=data_out1;
end
endmodule
遇到的问题
1、数值转换,
就是那个16位的二进制码,一开始不知道如何处理,根据视频教学上的来,发现并不理想,偶然间我在翻阅其他同学的成果时发现了分开计数的方法,吧中间八位先取出来换算成整数,再取后面四位换算成小数,问题得以解决
oled和蜂鸣器都有参照电子森林的代码:(spi_oled)(pwm)(电子琴)在此不过多赘述,我所做的代码发送到了附件里,有需要的话可以借鉴一下。
三、心路历程
这个才是我最想说的,我是一名20级新生,可以说对编程一无所知,这次算是我第一次接触编程,第一次摸到电路板,这段经历对于我来说简直是史无前例。
别的不说,这个假期是我最认真的一个假期,哪怕和高考那时候比,一开始拿到这块板子踌躇满志,我已经在幻想自己在“代码“的战场上大杀四方,可是呢在我刚刚步入这条道路时就遇到了几次迎头痛击。
首先在于这个软件,它是全英的,刚打开的时候我就一脸茫然,这是啥,那又是啥,点这个会不会坏,这个框是干嘛的,咋样能写代码,一个个问题整的我,想动,又不敢动,不过这些问题都在后面一遍遍的做工程时慢慢解决了。
其次是学习,一开始我可以说是啥代码也看不懂,一直以为这是c语言,准备去学c语言,不过看啦两集后发现不太对劲,然后我在快速入门的文档里找到了Verilog HDL我就想着是不是这个,之后我去搜了搜,还真是,不过在此我不得不说一个软件,就是b站,它可以说是伴随了我一整个学习过程,在我点亮我的第一个led灯的时候,我简直高兴地不可言语,之后我凭着自己做了一个倒计时的数码管,当时真的可以说是目中无“码”,自以为任务很简单,当我把温度计直接连到数码管上 (当时我直接 input wdj,然后assign wdj=cnt;cnt是把数值转换成数码管的数值,就这么直接连了起来) ,发现数码管不反应时还没意识到问题的严重性(不知道温度计还有一系类流程),当我做出时钟,直接把秒,分,时,链接oled的时候发现管脚少了一个(spi协议5个引脚,秒钟个位和十位等等有六个),我才发现事情不是呢么简单,记得当时有半个星期都自闭了。或许就是知道的越少越觉得自己厉害吧,直至后面学的多了才发现这些并不简单。
再次就是仿真的问题,我一开始写的东西简单,编译完直接综合,然后就可以烧了,我就认为那么麻烦的仿真是没用的,直到一次编译成功,运行不出来,两次编译成功,运行不出来,,,,,,我就发现仿真的作用了,又重新回去学仿真。
温度计,不得不说,这是我最大的坎,我在这上面消耗的时间有半个月,可以说大部分时间都花费到它身上了,我原本并不知道电子森林有温度计代码,就自己跟着b站的up主学习写温度计的源码,一点一点理解它的驱动原理,然后一步一步做,但我跟着两三个up主学,写的代码跟他们的一模一样我也运行不出来,我20分钟的视频10集,从头到底刷了好多遍,一开始我很执着于我的源代码,即使发现电子森林有可用的,也不愿意用现成的,可时间不多了,我也发现有些力不从心,最终不得不暂时放弃我的手打温度计的道路,但我不会就此罢手,迟早我要自己手撸一个温度计的源代码出来,还有oled这个本身也是大头,可是呢,不巧我找遍了能找的所有地方,就没有教原理和写代码的,所以我只能借用电子森林的代码,再参考一下其他同学的代码,然后改动,在此感谢一下杨彧同学的oled代码,给我很多启发,他对代码的改动让我了解到如何改动才能得到我想要的效果。
之后就比较顺利了,串口我找到了教学视频然后跟着学,就好很多,蜂鸣器那个资料比较多也学的比较顺利。一定原因是应为后期逻辑思维比较清晰,做多了很多东西都了然于胸。
在最后的衔接阶段还是有些小问题,比如,我不会写乐谱,也不会自己做程序转换乐谱,这里感谢一下李卓然同学让我看到了那个上位机,让我发现了另一种达到目的的方法,我在内部用不同的16进制数代表不同频率,也就是相当于输入不同的数就代表不同的音,就像钢琴一样,不同键对应不同的音,那个上位机又可以批量逐条发送,也就是说我可以用不同数字组成很多旋律,这样目的就达到了,这个上位机帮了我很大的忙,最后停止刷新着一条件,我直接用了两个标志信号贯穿几个关键代码也就达到了。
总之在最后出成果的时候我也真的是欣喜若狂,我也感到这个项目对我帮助十分的大,我甚至打游戏看小说的时间都减少了好多,极致的丰富了我的假期生活,让我感觉每一天都过得很有意义,我以后会做很多其它项目,它们或许比这个更大,更好,更复杂,但是相比其意义,这次才算是最重要的。
很高兴可以参加此次项目,希望日后还有机会继续参与其中。
四、项目总结
本次项目让我学习到了FPGA的知识,并且体验到了相关方向的魅力,对我日后的方向选着有着一定的影响,经历了一次次失败,让我了解到坚韧不拔的意志有多么重要,等真正拨开乌云见到太阳时,才会发现它如此的耀眼。
在此感谢平台给我的这次机会,也希望平台的个个项目越做越好!!!