数字钟的FPGA实现并在
数字钟的
之前用FPGA实现数字钟,并用数码管和VGA进行显示,同时还能用按键改变时间。下面我就讲解一下当初是怎
么做这个东西的。
实现并在VGA上显示上显示
上图是整个代码文件结构。文件的名字取得很奇怪,因为当时是在其他的文件基础上改的,所以从名字看起来似乎和设计没有
什么关系。这个地方大家可以要注意,代码文件的名字要命名得一看就知道功能是什么。
这里说一下,各个文件的作用:
1、LCD_TOP:顶层文件,只是例化了下面的各个模块
2、Led_display:这个其实是实现数字钟的模块,实现时分秒的计时以及改变
3、Display_shanshuo:这个是实现时分秒在数码管上显示,并且在改变时间的时候,实现数码管闪烁,这样,一看就知道是
改变哪个时间。
4、VGA_sig:实现VGA显示,这个是整个设计的比较难的模块。
5、Time_vga_data:这个是实现获取时间的字模数据。这样,才能使时间再VGA上显示。
6、LCD_top.ucf:这个看后缀就知道了,约束文件,约束管脚。
下面,我就从最底层的模块开始给大家分析这个设计
一、数字钟模块,实现时分秒的计时以及改变时分秒
先是端口定义
module led_display (
input sys_clk ,
input sys_rstn ,
input change , //时钟改变信号,1表示改变时间
input[3:0] data_5 , //改变的时十位
input[3:0] data_4 , //改变的时个位
input[3:0] data_3 , //改变的分十位
input[3:0] data_2 , //改变的分个位
input[3:0] data_1 , //改变的秒十位
input[3:0] data_0 , //改变的秒个位
output reg [3:0] shi_shi ,
output reg [2:0] shi_ge ,
output reg [2:0] fen_shi ,
output reg [3:0] fen_ge ,
output reg [3:0] miao_shi ,
output reg [1:0] miao_ge
);
分别是使用6个信号来定义时间的时分秒的十位和个位。输入有6个信号对应需要改变的时间的时分秒的十位和个位。
以下以秒的个位为例说明:
always@( posedge sys_clk )
begin
if(!sys_rstn)
begin
miao_ge <= 4'd0 ;
end
else
begin
if( change )
miao_ge <= data_0 ;
else
begin
if( miao_ge == 4'd10 )
miao_ge <= 4'd0 ;
else if(delay_cnt==cnt_number)
miao_ge <= miao_ge + 1'b1 ;
else
miao_ge <= miao_ge ;
end
end
end
在复位的情况下,miao_ge值为0。在没有复位情况下,首先是判断是否change有效,有效说明这个时候在改变时间,那么
miao_ge的值就为data_0的值,这样就实现了时间的改变。如果没有改变时间,1s延迟时间到,就加1。某一时刻的值加到
10,就清零。
其他的时间也是一样的原理。
二、数码管模块,实现时间的数码管显示和当改变时间时,数码管的闪烁,以及改变时间时的加减时间
端口定义
module display_shanshuo(
input clk ,
input rst_n ,
input [2:0] key , //按键的输入
input [3:0] shi_shi ,
input [3:0] shi_ge ,
input [3:0] fen_shi ,
input [3:0] fen_ge ,
input [3:0] miao_shi ,
input [3:0] miao_ge ,
output change , //是否改变时间
output wire [3:0] data_0 ,
output wire [3:0] data_1 ,
output wire [3:0] data_2 ,
output wire [3:0] data_3 ,
output wire [3:0] data_4 ,
output wire [3:0] data_5 ,
output reg [7:0] sm_bit , //数码管位选
output reg [7:0] sm_seg //数码管段选
);
多数信号和数字钟模块的信号一致。我用的数码管是8位的。
模块中有3个按键,一个按键负责控制改变时间的选择,另外两个按键负责对时间进行加或者减操作。所以在程序中,就有检
测按键按下的代码,并且要对按键进行消抖操作。这部分代码就不说明了,比较简单。
关键的地方,是以下3个,确定3个信号值。
assign real_change_buttom = ( doudong_cnt == doudong_number ) ? (!key[1]) : 1'b0 ;
assign real_add_buttom = ( doudong_cnt == doudong_number ) ? (!key[0]) : 1'b0 ;
assign real_sub_buttom = ( doudong_cnt == doudong_number ) ? (!key[2]) : 1'b0 ;
数码管的显示是比较简单的了,网上就有很多教程说明,这里要说一下,怎么样实现闪烁的效果。
如果要一个数码管闪烁,那么就让这个数码管隔一段时间亮,然后再隔一段时间灭就可以了。但是怎么控制亮灭了?最简单的
方法,控制数码管的位选。一般的数码管,位选为0,数码管亮,位选为1,数码管不亮。所以就去控制这个位选就可以了。
always@( posedge clk ) begin
if( !rst_n )
begin
sm_bit <= 8'b11111110 ;
end
else
begin
case( disp_i )
4'h0 : begin
if( 4'h0 == change_i )
sm_bit <= sm_temp | 8'b11111110 ;
else
sm_bit <= 8'b11111110 ;
end
4'h1 : begin
if( 4'h1 == change_i )
sm_bit <= sm_temp | 8'b11111101 ;
else
sm_bit <= 8'b11111101 ;
end
4'h2 : begin
if( 4'h2 == change_i )
sm_bit <= sm_temp | 8'b11111011 ;
else
sm_bit <= 8'b11111011 ;
end
4'h3 : begin
if( 4'h3 == change_i )
sm_bit <= sm_temp | 8'b11110111 ;
else
sm_bit <= 8'b11110111 ;
end
4'h4 : begin
if( 4'h4 == change_i )
sm_bit <= sm_temp | 8'b11101111 ;
else
sm_bit <= 8'b11101111 ;
end
4'h5 : begin
if( 4'h5 == change_i )
sm_bit <= sm_temp | 8'b11011111 ;
else
sm_bit <= 8'b11011111 ;
end
4'h6 : begin
if( 4'h6 == change_i )
sm_bit <= sm_temp | 8'b10111111 ;
else
sm_bit <= 8'b10111111 ;
end
4'h7 : begin
if( 4'h7 == change_i )
sm_bit <= sm_temp | 8'b01111111 ;
else
sm_bit <= 8'b01111111 ;
end
default:
sm_bit <= 8'hff;
endcase
end
end
这里,先说一下几个信号的作用:
disp_i:显示第几位的数码管。
change_i:改变第几位的数码管
sm_bit:数码管的位选
sm_temp:实现数码管闪烁用
代码中,实现闪烁的关键代码就是
sm_bit <= sm_temp | 8'b11111110 ;
sm_temp这个信号是每隔300ms就会取反一次,那么sm_bit对应的位不就每隔300ms取反一次,而sm_bit的每一位控制一个数
码管的位选,这样不就控制数码管的位选,实现数码管闪烁了。
至于加减时间,使用组合逻辑就搞定了,就判断按键的相关信号,然后对时间进行加1或者减1操作。
always@( * ) begin
display_data[2] = 4'ha ;
display_data[5] = 4'ha ;
if( change_i == 0 )
display_data[0] = real_add_buttom ? miao_ge + 1'b1 :
real_sub_buttom ? miao_ge - 1'b1 : miao_ge ;
else
display_data[0] = miao_ge ; //秒个位
if( change_i == 1 )
display_data[1] =real_add_buttom ? miao_shi + 1'b1 :
real_sub_buttom ? miao_shi - 1'b1 : miao_shi ;
else
display_data[1] = miao_shi ; //秒十位
if( change_i == 3 )
display_data[3] =real_add_buttom ? fen_ge + 1'b1 :
real_sub_buttom ? fen_ge - 1'b1 : fen_ge ;
else
display_data[3] = fen_ge ; //分个位
if( change_i == 4 )
display_data[4] =real_add_buttom ? fen_shi + 1'b1 :
real_sub_buttom ? fen_shi - 1'b1 : fen_shi ;
else
display_data[4] = fen_shi ; //分十位
if( change_i == 6 )
display_data[6] =real_add_buttom ? shi_ge + 1'b1 :
real_sub_buttom ? shi_ge - 1'b1 : shi_ge ;
else
display_data[6] = shi_ge ; //时个位
if( change_i == 7 )
display_data[7] =real_add_buttom ? shi_shi + 1'b1 :
real_sub_buttom ? shi_shi - 1'b1 : shi_shi ;
else
display_data[7] = shi_shi ; //时十位
end
其实这个模块也比较简单,先对按键进行检测,不过要注意要进行按键的消抖。然后对按键的判断,实现对时间的改变。稍微
麻烦一点的是对数码管的处理,因为数码管是需要闪烁的。
三、VGA模块,这个模块实现图片和时间的显示
采用VGA显示画面,VGA的知识,网上也太多了,这里就不说明怎么设计代码去驱动VGA了。我这里,是想说一下,怎么设
置在VGA上某一点显示的数据。因为VGA要显示画面,我们就要设置VGA上每个像素点的值,这样显示出来的画面才和我们
想要的一致。
首先,要明确一下我们需要显示什么东西。
上图是要显示的单元的各个位置信息。时间单元,依次是:
时十时个空白冒号分十分个空白冒号秒十秒个
每个单元的大小是16*32。这个时候,就需要取字模软件了,将0-9,冒号,空白的字模都给保存到xilinx的coe初始化文件中,
这样就可以将字模保存到内置的rom中。不过取模方式是要从上至下,从左至右的方向取。这里取的字模是取二值的,因为显
示的时间用单一黑色显示即可,不用多种颜色,这样可以节省字模空间。
以下是得到的coe文件的一部分截图
当然图片也是要取模的。不过这个比较简单。同样保存到一个coe文件中,不过取的图片的每个像素的数据要以3位保存,因
为图片是有各种颜色的,我是使用8种颜色来进行显示,需要3位二进制位表示。
以下是图片coe文件的部分截图。
从显示图可以看出,有两个参数是比较重要的。一个是横坐标,一个是纵坐标,有了这两个,才能知道在该位置要显示什么。
在代码中使用address_x和address_y表示。
因为只有两个东西要显示,一个是图片,一个是时间,所以可以通过判断横纵坐标的值,来决定该位置显示什么。
assign red_0 = time_flag ? ~data:red;
assign red_1 = time_flag ? ~data:red;
assign red_2 = time_flag ? ~data:red;
assign gree_0 = time_flag ? ~data:gree;
assign gree_1 = time_flag ? ~data:gree;
assign gree_2 = time_flag ? ~data:gree;
assign blue_0 = time_flag ? ~data:blue;
assign blue_1 = time_flag ? ~data:blue;
其中data是时间显示的数据,red,gree,blue是图片显示的3种颜色的数据。为什么显示要对data取反了,这个就和颜色合成
有关了。在字模提取中,是让显示的地方为1,不显示的地方为0。而上面讲这个数据直接给都给RGB。对于颜色合成,如果
RGB都是1的话,那么出来的就是白色,在屏幕上就看不到了,而RGB都是0的话,那么出来的就是黑色,就可以看到了。所
以要有个取反的操作。
对于time_flag这个信号的判断,只需判断横纵坐标的范围是否在时间显示区域的范围内即可了。
always@( * ) begin
if( address_x >= 176 && address_x <= 367 &&
address_y >= 300 && address_y <= 331 )
time_flag = 1;
else
time_flag = 0;
end
显示图片之前我有写过博客说过,这里就不说明了,以下说明一下,怎么显示时间。这个是稍微有点麻烦的。
这里以显示第一个时间,也就是时十为例说明。以下是显示时十的区域范围。
从图中,看出,当横坐标在176到191范围内,纵坐标在300到331范围内,说明这个区域应该显示时十。那先要知道时十是什
么数字,假设是数字1。然后将保存在rom的1的字模给取出来,依次的写入到这个区域,那么这个区域不就显示1了。但是问
题是怎么将1的字模数据写入到这个区域了?我们知道VGA显示,是逐行扫描的。当现在的坐标是(191,300)时,下一个坐
标就是(192,300)了,在这个坐标就要显示其他数据去了。
所以,就不能单一的考虑一个单元的显示了。而是要考虑在每个位置应该要显示什么。
假设,现在的横纵坐标是(188,320)。首先判断一下,该坐标应该是处于显示什么的区域。通过简单的判断,得知该点是在
显示时十的区域。然后再看时十的数字是多少,这个通过传入的参数可以获取,假设是1。然后再看一下这个点是位于第几
行,通过320-300=20,在20行。那么就可以从字模rom中得知,显示1的第20行数据应该是多少,怎么找rom了?这里在将
rom的coe文件再次截图:
图中红框中的数据就是1的字模数据。因为每个数字的字模大小是16*32,所以就可以知道字模1的字模数据在rom中的起始地
址是1*32=32。那么字模1的第20行数据的地址不就有了,就是32+20=52。从rom的52地址中取出16位数据,这16位数据就
是第20行显示的数据。假设是0180。
根据横坐标,188-176=12。那么16位数据中的第12位数据0就是该坐标显示的数据。所以来说就有以下公式,假设此时坐标
是(x,y)
行显示数据[15:0] = rom[该点区域显示的数值*32+ y - 300]
该点显示数据=行显示数据[x-该点区域的起始x位置]
该点区域的起始x位置是根据显示的时间的不同值而不一样的,对于时十,为176,对于时个,为192,正好是前一个加16,因
为显示的数字是16像素宽的。
所以翻译成代码:
reg [4:0] y_position;
reg [3:0] x_position;
always@(*) begin
if(address_y >= 300 && address_y <= 331)
begin
y_position = address_y - 300;
if(address_x >= 176 && address_x <= 191 ) begin
mode_xuanze = hour_shi;
x_position = address_x - 176; end
else if(address_x >= 192 && address_x <= 207 ) begin
mode_xuanze = hour_ge;
x_position = address_x - 192; end
else if(address_x >= 208 && address_x <= 223 ) begin
mode_xuanze = kongbai;
x_position = address_x - 208; end
else if(address_x >= 224 && address_x <= 239 ) begin
mode_xuanze = maohao;
x_position = address_x - 224; end
else if(address_x >= 240 && address_x <= 255 ) begin
mode_xuanze = kongbai;
x_position = address_x - 240; end
else if(address_x >= 256 && address_x <= 271 ) begin
mode_xuanze = minu_shi;
x_position = address_x - 256; end
else if(address_x >= 272 && address_x <= 287 ) begin
mode_xuanze = minu_ge;
x_position = address_x - 272; end
else if(address_x >= 288 && address_x <= 303 ) begin
mode_xuanze = kongbai;
x_position = address_x - 288; end
else if(address_x >= 304 && address_x <= 319 ) begin
mode_xuanze = maohao;
x_position = address_x - 304; end
else if(address_x >= 320 && address_x <= 335 ) begin
mode_xuanze = kongbai;
x_position = address_x - 320; end
else if(address_x >= 336 && address_x <= 351 ) begin
mode_xuanze = seco_shi;
x_position = address_x - 336; end
else if(address_x >= 352 && address_x <= 367 ) begin
mode_xuanze = seco_ge;
x_position = address_x - 352; end
else begin
mode_xuanze = kongbai;
x_position = 0; end
end
else
begin
mode_xuanze = kongbai;
y_position = 0;
x_position = 0;
end
end
reg [3:0] data_time;
always@(*) begin
case(mode_xuanze)
hour_shi :
data_time = data_5;
hour_ge :
data_time = data_4;
minu_shi :
data_time = data_3;
minu_ge :
data_time = data_2;
seco_shi :
data_time = data_1;
seco_ge :
data_time = data_0;
maohao :
data_time = 10;
kongbai :
data_time = 11;
default:
data_time = 11;
endcase
end
reg [8:0] address_base;
always@( * ) begin
case(data_time)
4'd0 :
address_base = 0;
4'd1 :
address_base = 32;
4'd2 :
address_base = 64;
4'd3 :
address_base = 96;
4'd4 :
address_base = 128;
4'd5 :
address_base = 160;
4'd6 :
address_base = 192;
4'd7 :
address_base = 224;
4'd8 :
address_base = 256;
4'd9 :
address_base = 288;
4'd10:
address_base = 320;
4'd11:
address_base = 352;
default:
address_base =0;
endcase
end
wire [8:0] addra;
assign addra = address_base + y_position;
wire [15:0] douta;
zimo_rom zimo_rom_u1 (
.clka(clk), // input clka
.addra(addra), // input [8 : 0] addra
.douta(douta) // output [15 : 0] douta
);
wire [15:0] douta_yiwei;
assign douta_yiwei = douta << x_position;
assign data = douta_yiwei[15];
最后在设计一个顶层,将各个模块连接起来,就可以了。
下面贴上效果图:
数码管闪烁
VGA显示
虽然说是实现功能了,但是现在去看去年写的代码,感觉当时写的代码风格可真是挫啊。还好是自己写的,思路还记得一些,
所以隔了这么久来看,还能看得懂。但是如果代码是别人写的话,可就不容易看懂了。
不过,看看以前写的代码,再对比现在自己写的,明显感受到了自己在技术方面的成长。