数字钟的FPGA实现并在VGA上显示
1赞
之前用FPGA实现数字钟,并用数码管和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显示
虽然说是实现功能了,但是现在去看去年写的代码,感觉当时写的代码风格可真是挫啊。还好是自己写的,思路还记得一些,所以隔了这么久来看,还能看得懂。但是如果代码是别人写的话,可就不容易看懂了。
不过,看看以前写的代码,再对比现在自己写的,明显感受到了自己在技术方面的成长。