weiqi7777

数字钟的FPGA实现并在VGA上显示

1
阅读(10814)

 

           之前用FPGA实现数字钟,并用数码管和VGA进行显示,同时还能用按键改变时间。下面我就讲解一下当初是怎么做这个东西的。

           clip_image001

           上图是整个代码文件结构。文件的名字取得很奇怪,因为当时是在其他的文件基础上改的,所以从名字看起来似乎和设计没有什么关系。这个地方大家可以要注意,代码文件的名字要命名得一看就知道功能是什么。

           这里说一下,各个文件的作用:

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上每个像素点的值,这样显示出来的画面才和我们想要的一致。

首先,要明确一下我们需要显示什么东西。

clip_image003

上图是要显示的单元的各个位置信息。时间单元,依次是:

时十   时个   空白 冒号   分十   分个   空白   冒号    秒十   秒个

每个单元的大小是16*32。这个时候,就需要取字模软件了,将0-9,冒号,空白的字模都给保存到xilinxcoe初始化文件中,这样就可以将字模保存到内置的rom中。不过取模方式是要从上至下,从左至右的方向取。这里取的字模是取二值的,因为显示的时间用单一黑色显示即可,不用多种颜色,这样可以节省字模空间。

以下是得到的coe文件的一部分截图

clip_image005

当然图片也是要取模的。不过这个比较简单。同样保存到一个coe文件中,不过取的图片的每个像素的数据要以3位保存,因为图片是有各种颜色的,我是使用8种颜色来进行显示,需要3位二进制位表示。

以下是图片coe文件的部分截图。

clip_image006

从显示图可以看出,有两个参数是比较重要的。一个是横坐标,一个是纵坐标,有了这两个,才能知道在该位置要显示什么。在代码中使用address_xaddress_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是时间显示的数据,redgreeblue是图片显示的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

显示图片之前我有写过博客说过,这里就不说明了,以下说明一下,怎么显示时间。这个是稍微有点麻烦的。

这里以显示第一个时间,也就是时十为例说明。以下是显示时十的区域范围。

clip_image007

从图中,看出,当横坐标在176191范围内,纵坐标在300331范围内,说明这个区域应该显示时十。那先要知道时十是什么数字,假设是数字1。然后将保存在rom1的字模给取出来,依次的写入到这个区域,那么这个区域不就显示1了。但是问题是怎么将1的字模数据写入到这个区域了?我们知道VGA显示,是逐行扫描的。当现在的坐标是(191,300)时,下一个坐标就是(192,300)了,在这个坐标就要显示其他数据去了。

所以,就不能单一的考虑一个单元的显示了。而是要考虑在每个位置应该要显示什么。

clip_image008

假设,现在的横纵坐标是(188,320)。首先判断一下,该坐标应该是处于显示什么的区域。通过简单的判断,得知该点是在显示时十的区域。然后再看时十的数字是多少,这个通过传入的参数可以获取,假设是1。然后再看一下这个点是位于第几行,通过320-300=20,在20行。那么就可以从字模rom中得知,显示1的第20行数据应该是多少,怎么找rom了?这里在将romcoe文件再次截图:

clip_image010

图中红框中的数据就是1的字模数据。因为每个数字的字模大小是16*32,所以就可以知道字模1的字模数据在rom中的起始地址是1*32=32。那么字模1的第20行数据的地址不就有了,就是32+20=52。从rom52地址中取出16位数据,这16位数据就是第20行显示的数据。假设是0180

根据横坐标,188-176=12。那么16位数据中的第12位数据0就是该坐标显示的数据。所以来说就有以下公式,假设此时坐标是(xy

行显示数据[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];

最后在设计一个顶层,将各个模块连接起来,就可以了。

下面贴上效果图:

数码管闪烁

clip_image012

clip_image014


VGA显示

clip_image016


虽然说是实现功能了,但是现在去看去年写的代码,感觉当时写的代码风格可真是挫啊。还好是自己写的,思路还记得一些,所以隔了这么久来看,还能看得懂。但是如果代码是别人写的话,可就不容易看懂了。

不过,看看以前写的代码,再对比现在自己写的,明显感受到了自己在技术方面的成长。