基于EP4CE6F17C8的FPGA数码管时钟显示实例
一、电路模块
本例的电路模块与“基于EP4CE6F17C8的FPGA数码管动态显示实例”中的完全一样,此处就不再给出了。
二、实验代码
本例使用6个数码管显示时钟的时、分、秒,时与分之间及分与秒之间通过小数点来分隔,代码使用Verilog编写,采用例化的形式,使用了两种方式来实现。
第一种方式,共有七个文件,具体如下。
先编写数码管实现显示字形解码的程序,有两个,一个为不带小数点显示的,一个是带小数点显示的,先看不带小数点的,模块名称为seg_decode,文件名称为seg_decode.v,代码如下。
module seg_decode( input[3:0] data, //显示的字形,可显示0~9十个字形,所以需要4位 output reg[7:0] seg //字形编码,包含小数点,共8位 ); always@(*) //敏感信号为所有输入量 begin case(data) 4'd0:seg <= 8'b1100_0000; //字形0的编码 4'd1:seg <= 8'b1111_1001; //字形1的编码 4'd2:seg <= 8'b1010_0100; //字形2的编码 4'd3:seg <= 8'b1011_0000; //字形3的编码 4'd4:seg <= 8'b1001_1001; //字形4的编码 4'd5:seg <= 8'b1001_0010; //字形5的编码 4'd6:seg <= 8'b1000_0010; //字形6的编码 4'd7:seg <= 8'b1111_1000; //字形7的编码 4'd8:seg <= 8'b1000_0000; //字形8的编码 4'd9:seg <= 8'b1001_0000; //字形9的编码 default:seg <= 7'b111_1111; //默认不显示 endcase end endmodule
再来看带小数点的,模块名称为seg_decode_dot,文件名称为seg_decode_dot.v,代码如下。
module seg_decode_dot( input[3:0] data, //显示的字形,可显示0~9十个字形,所以需要4位 output reg[7:0] seg //字形编码,包含小数点,共8位 ); always@(*) //敏感信号为所有输入量 begin case(data) 4'd0:seg <= 8'b0100_0000; //字形0的编码(带小数点) 4'd1:seg <= 8'b0111_1001; //字形1的编码(带小数点) 4'd2:seg <= 8'b0010_0100; //字形2的编码(带小数点) 4'd3:seg <= 8'b0011_0000; //字形3的编码(带小数点) 4'd4:seg <= 8'b0001_1001; //字形4的编码(带小数点) 4'd5:seg <= 8'b0001_0010; //字形5的编码(带小数点) 4'd6:seg <= 8'b0000_0010; //字形6的编码(带小数点) 4'd7:seg <= 8'b0111_1000; //字形7的编码(带小数点) 4'd8:seg <= 8'b0000_0000; //字形8的编码(带小数点) 4'd9:seg <= 8'b0001_0000; //字形9的编码(带小数点) default:seg <= 7'b111_1111; //默认不显示 endcase end endmodule
接下来编写模10和模6的两个计数模块,名称分别为count_m10和count_m6。先看count_m10的模块,文件名称为count_m10.v,代码如下。
module count_m10( input clk, //板载50HMz系统时钟 input rst_n, //复位按键 input en, //计数使能位 output reg[3:0]data, //计数值,从0~9共10位,所以用4位 output reg t //进位位 ); always@(posedge clk or negedge rst_n) //敏感信号为时钟上沿或复位下沿 begin if(rst_n==0) //低电平复位 begin data <= 4'd0; //复位时计数值及进位位清零 t <= 1'd0; end else if(en) //如果计数使能,则执行计数,否则保持上一次的值不变 begin if(data==4'd9) //如果计数到达9时 begin t<= 1'b1; //进位位置1 data <= 4'd0; //计数值清零 end else begin t <= 1'b0; //否则进位位清零,计数值加1 data <= data + 4'd1; end end else //如果计数不使能,进位位置0 t <= 1'b0; end endmodule
再来看count_m6的模块,文件名称为count_m6.v,代码如下。
module count_m6( input clk, //板载50HMz系统时钟 input rst_n, //复位按键 input en, //计数使能位 output reg[3:0]data, //计数值,从0~9共10位,所以用4位 output reg t //进位位 ); always@(posedge clk or negedge rst_n) //敏感信号为时钟上沿或复位下沿 begin if(rst_n==0) //低电平复位 begin data <= 4'd0; //复位时计数值及进位位清零 t <= 1'd0; end else if(en) //如果计数使能,则执行计数,否则保持上一次的值不变 begin if(data==4'd5) //如果计数到达5时 begin t<= 1'b1; //进位位置1 data <= 4'd0; //计数值清零 end else begin t <= 1'b0; //否则进位位清零,计数值加1 data <= data + 4'd1; end end else //如果计数不使能,进位位置0 t <= 1'b0; end endmodule
接下来设计一个模60的计数模块,但它是通过例化前模10和模6模块来实现的,模块名称为count_m60,文件名称为count_m60.v,代码如下。
module count_m60( input clk, //板载50HMz系统时钟 input rst, //复位按键 input en, //计数使能位 output [7:0] data, //计数值从00~59共,使用BCD方式 output t //进位位 ); wire [3:0] count_data0,count_data1; //定义计数值的个位和十位 wire t0,t1; //定义个位和十位的进位位 //下面例化个位计数单元(十进制) count_m10 u0(.clk(clk), .rst_n(rst), .en(en), .data(count_data0), .t(t0)); //下面例化十位计数单元(六进制) count_m6 u1(.clk(clk), .rst_n(rst), .en(t0), .data(count_data1), .t(t1)); assign t = t1; //连接输出高位进位位 assign data = {count_data1, count_data0}; //并位连接输出60进制计数值 endmodule
接下来是模24的计数模块,模块名称为count_m24,文件名称为count_m24.v,代码如下。
module count_m24( input clk, //板载50HMz系统时钟 input rst_n, //复位按键 input en, //计数使能位 output reg[7:0]data //计数值从00~23共,使用BCD方式 ); always@(posedge clk or negedge rst_n) //敏感信号为时钟上沿或复位下沿 begin if(rst_n==0) //低电平复位 begin data <= 8'd0; //复位时计数值清零 end else if(en) //如果计数使能,则执行计数,否则保持上一次的值不变 begin if(data == 8'b00100011) //如果计数值BCD码为23,则计数值清零 begin data <= 8'd0; end else if(data[3:0] == 4'b1001) //如果计数值低位等于9,则低位清零,高位加1 begin data[3:0] <= 4'd0; data[7:4] <= data[7:4] + 1'd1; end else begin data <= data + 1'd1; //否则计数值加1 end end end endmodule
最后是数码管时钟显示模块,并设置为顶层模块,模块名称为seg_clock,文件名称为seg_clock.v,代码如下。
module seg_clock( input clk, //板载50HMz系统时钟 input rst, //复位按键 output reg[7:0] seg7, //段码端口 output reg[5:0] bit //位选端口 ); wire t0,t1,t2; //定义进位信号 reg [25:0] cnt; //定义26位时钟计数器 reg sec; //定义秒信号 always@(posedge clk or negedge rst) //敏感信号为时钟上沿或复位下沿 begin if(rst == 0) //低电平复位时秒计数清零 begin sec <= 1'b0; end else if(cnt == 26'd49_999_999) //时钟计数器到达1秒时 begin cnt <= 26'd0; //时钟计数器清零 sec <= 1'b1; //产生秒信号 end else begin sec <= 1'b0; //否则秒信号清零 cnt <= cnt + 26'd1; //时钟计数器加1,即来一次时钟脉冲加一次 end end //下面定义6个数码管显示数值的存储变量 wire [3:0] count_data0,count_data1,count_data2,count_data3,count_data4,count_data5; //下面定义时、分、秒的存储变量 wire [7:0] count_data_0,count_data_1,count_data_2; //下面定义6个数码管的字形码存储变量 wire [7:0] seg_0,seg_1,seg_2,seg_3,seg_4,seg_5; //下面例化秒的计数单元(六十进制) count_m60 u1(.clk(clk), .rst(rst), .en(sec), .data(count_data_0), .t(t0)); //下面例化分的计数单元(六十进制) count_m60 u2(.clk(clk), .rst(rst), .en(t0), .data(count_data_1), .t(t1)); //下面例化时的计数单元(二十四进制) count_m24 u3(.clk(clk), .rst_n(rst), .en(t1), .data(count_data_2)); //下面分别取出6个数码管的显示值 assign count_data0 = count_data_0[3:0]; assign count_data1 = count_data_0[7:4]; assign count_data2 = count_data_1[3:0]; assign count_data3 = count_data_1[7:4]; assign count_data4 = count_data_2[3:0]; assign count_data5 = count_data_2[7:4]; //下面例化秒的个位字形解码单元 seg_decode seg0(.data(count_data0), .seg(seg_0)); //下面例化秒的十位字形解码单元 seg_decode seg1(.data(count_data1), .seg(seg_1)); //下面例化分的个位字形解码单元 seg_decode_dot seg2(.data(count_data2), .seg(seg_2)); //下面例化分的十位字形解码单元 seg_decode seg3(.data(count_data3), .seg(seg_3)); //下面例化时的个位字形解码单元 seg_decode_dot seg4(.data(count_data4), .seg(seg_4)); //下面例化时的十位字形解码单元 seg_decode seg5(.data(count_data5), .seg(seg_5)); reg[17:0] time_cnt; //定义20位时钟计数器 reg[3:0] scan_sel; //定义扫描位置计数器 //3.3毫秒循环计数 always@(posedge clk or negedge rst) //敏感信号为时钟上沿或复位下沿 begin if(rst == 1'b0) //低电平复位时计数器全部清零 begin time_cnt <= 18'd0; scan_sel <= 4'd0; end else if(time_cnt >= 18'd166_666) //时钟计数器到达3.3毫秒时 begin time_cnt <= 18'd0; //时钟计数器清零 if(scan_sel == 4'd5) //如果扫描位置计数器已经到1则恢复0 scan_sel <= 4'd0; else scan_sel <= scan_sel + 4'd1; //否则扫描位置计数器加1,即每3.3ms加一次 end else begin time_cnt <= time_cnt + 18'd1; //否则时钟计数器加1,即来一次时钟脉冲加一次 end end //数码管扫描显示 always@(posedge clk or negedge rst) //敏感信号为时钟上沿或复位下沿 begin if(!rst) //低电平复位时数码管全灭 begin bit <= 6'b111111; seg7 <= 8'hff; end else case(scan_sel) 4'd0: //数码管0显示秒的个位 begin bit <= 6'b111110; seg7 <= seg_0; end 4'd1: //数码管1显示秒的十位 begin bit <= 6'b111101; seg7 <= seg_1; end 4'd2: //数码管2显示分的个位 begin bit <= 6'b111011; seg7 <= seg_2; end 4'd3: //数码管3显示分的十位 begin bit <= 6'b110111; seg7 <= seg_3; end 4'd4: //数码管4显示时的个位 begin bit <= 6'b101111; seg7 <= seg_4; end 4'd5: //数码管5显示时的十位 if (count_data5 == 4'd0) begin bit <= 6'b011111; //如果十位为0则不显示 seg7 <= 8'hff; end else begin bit <= 6'b011111; seg7 <= seg_5; end default: //数码管全部熄灭 begin bit <= 6'b111111; seg7 <= 8'hff; end endcase end endmodule
第二种方式,共有五个文件,它没有使用模10和模6的模块,而是直接写了一个模60的模块,模块名称为count_m60,文件名称仍为count_m60.v,代码如下。
module count_m60( input clk, //板载50HMz系统时钟 input rst, //复位按键 input en, //计数使能位 output reg[7:0]data, //计数值从00~59共,使用BCD方式 output reg t //进位位 ); always@(posedge clk or negedge rst) //敏感信号为时钟上沿或复位下沿 begin if(rst==0) //低电平复位 begin data <= 8'd0; //复位时计数值及进位位清零 t <= 1'd0; end else if(en) //如果计数使能,则执行计数,否则保持上一次的值不变 begin if(data == 8'b01011001) //如果计数值BCD码为59,则计数值清零,进位位置1 begin data <= 8'd0; t<= 1'b1; end else if(data[3:0] == 4'b1001) //如果计数值低位等于9,则低位清零,高位加1 begin data[3:0] <= 4'd0; data[7:4] <= data[7:4] + 1'd1; end else begin data <= data + 1'd1; //否则计数值加1,进位位清零 t<= 1'b0; end end else //如果计数不使能,进位位置0 t<= 1'b0; end endmodule
从上面的代码中可以看到,该模块与第一种方式中的不一样。其余四个文件(seg_clock.v、seg_decode.v、seg_decode_dot.v、count_m24.v)与第一种方式中的一样。
三、代码说明
1、本例采用了两种方式来实现,第一种方式使用了层层例化的方法,先实现最基本的模10和模6的计数模块,再通过例化方式形成上一层的模60计数模块,然后再实现一个模24的计数模块,共同为顶层的显示模块提供例化。而第二种方式则是独立实现了模60和模24的两个计数模块,为顶层的显示模块提供例化。从编译后的结果来看,第一种方式要节省器件一些。
2、为了提供带小数点的显示效果,额外又编写了一个带小数点的字形解码模块,为时钟分的个位及时的个位提供小数点显示,以作为隔离。
3、在顶层的显示模块中,除了例化需要的模块之外,还承担了产生秒信号和进行数码管扫描的任务,具体可参看“基于EP4CE6F17C8的FPGA双数码管六十进制秒计数实例”一文。
4、在顶层模块中,按时、分、秒的计数只例化出3个实体,即时通过模24的计数单元例化出1个,分通过模60的计数单元例化出1个,秒通过模60的计数单元例化出1个。每个单元的个位和十位的计数规律则由各自的例化元件来实现,这也体现出元件例化的好处。
5、由于显示部分是按位来进行的,所以需要把各个计数单元中的个位和十位分别取出来进行字形解码并通过动态扫描显示。这里规定,每个计数单元使用了BCD编码,所以对应的低4位就是个位部分,高4位就是十位部分,通过位截取的方式分别进行赋值,就可得到六个数码管显示的值了。
6、以秒为例,先产生出秒信号sec,然后通过例化一个模60的计数实体,把sec传入其中,从而得到一个60进制的秒计数值count_data_0。然后把count_data_0的低4位(秒的个位)赋值给秒个位存储变量count_data0,高4位(秒的十位)赋值给秒十位存储变量count_data1。接着通过例化两个字形解码实体,分别把count_data0、count_data1传入,从而得到秒个位的字形编码seg_0和秒十位的字形编码seg_1,最后通过数码管扫描方式把seg_0、seg_1送到数码管对应的位上去显示。分和时的显示也一样,只不过在例化字形解码实体时,个位需要带小数点的,十位的不带。
7、本例中,时显示部分最高位进行了灭零处理,即当时只有个位数时,其最高位不显示(熄灭)。另外,顶层模块中的数码管扫描部分也可独立出来作为一个例化元件,这样顶层文件的内容会更简洁一些。
四、实验步骤
FPGA开发的详细步骤请参见“基于EP4CE6F17C8的FPGA开发流程(以半加器为例)”一文,本例只对不同之处进行说明。
本例工程放在D:\EDA_FPGA\Exam_6文件夹下,工程名称为Exam_6。有七个模块文件,一个名称为seg_clock.v,设置为顶层实体,另外六个名称分别为seg_decode.v、seg_decode_dot.v、count_m10.v、count_m6.v、count_m60.v和count_m24.v,用于提供例化。其余步骤与“基于EP4CE6F17C8的FPGA开发流程”中的一样。
接下来看管脚约束,本例中6个数码管一共有14个引脚,再加上时钟晶振和复位按钮,一共16个。具体的端口分配如下图所示。
对于未用到的引脚设置为三态输入方式,多用用途引脚全部做为普通I/O端口,电压设置为3.3-V LVTTL(与”基于EP4CE6F17C8的FPGA开发流程“中的一样)。需要注意,程序中的每个端口都必须为其分配管脚,如果系统中存在未分配的I/O,软件可能会进行随机分配,这将造成不可预料的后果,存在烧坏FPGA芯片的风险。
接下来对工程进行编译,编译完成后,可查看一下逻辑器件的消耗情况,第一种方式的器件消耗量如下图所示。
第二种方式的器件消耗量如下图所示。
可以看到,第二种方式消耗的器件更多一些,因此推荐使用第一种方式。另外,还可以点击菜单Tools->Netlist Viewers->RTL Viewer,查看一下生成的RTL电路图。
最后进行程序下载,并查看结果。下图为数码管时钟显示的效果,其中第一张的时部分为个位数,所以十位没有点亮(灭零)。
当按下复位键后,时钟清零,所有数码管均熄灭,如下图所示。