文章目录
一、项目要求
任务:使用FPGA开发板上的6位数码管以动态方式从0开始计数,每100ms计数值增加一,当计数值从0增加到999999后重新从0开始计数。
二、问题分析与思路设计
2.1 从基本的动态显示入手
我们首先下图这种两个数码管的情况来进行分析:假设我们现在需要让这两个数码管显示:“12”,那应该怎么做呢?
通过上图我们可以看出:数码管的位选信号(10引脚和5引脚)是独立的,而且是高电平有效。这也就意味着如果我们给 10 引脚高电平,5引脚低电平(即 “10” 信号),那么就可以让左边的数码管亮,右边的数码管不亮。反之,如果给 “01” 信号,那么就是左边的数码管不亮,右边的数码管亮。
不过值得注意的是:如果只是看上图的位选,那么确实是高电平有效。但是在实际的FPGA开发板,驱动数码管所需要的电流要比较大,因此需要使用三极管来对位选信号进行放大,笔者所使用的FPGA开发板上位选信号输入的原理图如下:
上图中的 S E L 0 _ T SEL0\_T SEL0_T ~ S E L 5 _ T SEL5\_T SEL5_T就是我们刚刚所讨论的位选信号,而 S E L 0 SEL0 SEL0~ S E L 5 SEL5 SEL5就是真正作用于数码管的信号,可见位选信号是 PNP 三极管的基级,因此当 S E L 0 _ T SEL0\_T SEL0_T ~ S E L 5 _ T SEL5\_T SEL5_T 中某一些位是低电平时, S E L 0 SEL0 SEL0~ S E L 5 SEL5 SEL5 中的对应位才变为高电平,用于驱动对应位置的数码管。因此在本例中,数码管的位选信号是低电平有效的。
【不过在2.1节的剩余部分,考虑到内容的连贯性,还是假设位选信号就是第一幅图里面的10口和5口,且为高电平有效,但是在代码部分,我们将会重新考虑实际的原理图】
然而我们又发现:这两个数码管他们的段选信号是公共的,而且是低电平有效。 段选信号指的就是让数码管显示具体的哪一个数字,段选信号与数字显示的对应关系如下图所示。 比如说,咱们现在给位选信号是 “10”,这样就是左边的数码管亮,右边的数码管不亮;同时我们给段选信号是:【注:我们这里暂且假设a位是高位,这个具体看实际电路的情况】 ( a b c d e f g D P ) = ( 1 0 0 1 1 1 1 1 ) (a\space b\space c\space d \space e \space f \space g \space D_P)= (1\space 0\space 0\space 1 \space 1\space 1\space1\space1) (a b c d e f g DP)=(1 0 0 1 1 1 1 1)
那么就是说我们让左边的数码管显示 “1”,而右边的数码管不亮。
那么如果我们给位选信号是 “01”,段选信号是: ( a b c d e f g D P ) = ( 0 0 1 0 0 1 0 1 ) (a\space b\space c\space d \space e \space f \space g \space D_P)= (0\space 0\space 1\space 0 \space 0\space 1\space0\space1) (a b c d e f g DP)=(0 0 1 0 0 1 0 1)
这样一来,就是左边的数码管不亮,右边的数码管亮,且显示数字 “2”。
可是我们希望能够显示 “12”,如果说给位选信号是 “11”,也就是两个数码管同时亮,但是这样的话因为段选信号是公共的,那么也就是说两个数码管一定会显示相同的数字。
幸运的是,我们的人眼存在 “视觉暂留现象”,我们假设:左边数码管显示“1”,右边数码管不亮这个状态为 “状态1”;左边数码管不亮,右边数码管显示 “2”这个状态为 “状态2”。那么,只要状态1 和状态2 他俩之间来回切换的频率高于一定的程度,我们的人眼就分辨不出来,从而以为是这两个数码管分别显示了不同的数字。 一般来讲,状态1和状态2之间的间隔时间可以是 1ms ~ 2ms。
2.2 算法设想
现在我们已经了解了数码管的动态显示是如何工作的,那么回到我们本次项目的需求:
使用FPGA开发板上的6位数码管以动态方式从0开始计数,每100ms计数值增加一,当计数值从0增加到999999后重新从0开始计数。
那么显示 1到999999之间的任何数字 这一环节就是用我们在上一个section 里面提到的方法.
那么我们可以知道需要两个定时器:
- 一个是 1ms 定时器(也就是我们在 2.1 节提到的在每隔 1ms 就分别让6个数码管中的其中一个点亮,其余不亮),它每隔1ms就可以输出一个 flag 信号指示数码管的切换。
- 另外一个就是 100ms 定时器,他的作用是每隔100ms就产生一个 data信号(1到999999)。
然后我们还需要一个数码管显示模块,它接收来自 1ms 定时器输出的 flag 信号,每隔1ms就切换数码管的显示状态,同时它也需要接受 100ms 定时器输出的 data 信号,用于给当前时刻被点亮的数码管赋予一个需要显示的数字。
这里数码管显示模块又涉及到两种问题:
- 如何切换数码管的显示状态?
- 如何给当前时刻被点亮的数码管赋予数字?
对于第一个问题,首先我们的 1ms定时器 在定时到1ms时会给我们的数码管显示模块发送一个切换指示信号 flag,那么我们可以再在数码管显示模块里面设一个 reg 类型的信号 c n t _ s e l cnt\_sel cnt_sel,每来一个 flag,那么我的 c n t _ s e l cnt\_sel cnt_sel 就加一(最多到5),因此 c n t _ s e l cnt\_sel cnt_sel 每隔 1ms 就会在 0, 1,2,3,4,5这五个数字之间来回变化,那么最后我们只需要给一个 case 语句,分配好这6个数字与每一个数码管亮的对应关系即可。
对于第二个问题,我们可以首先获取输入到数码管显示模块的 data 的各个位。把他们分别转换成二进制,最后还是用一个 case 判断不同的二进制对应的段选信号情况。(值得注意的是:由于我们将要显示的数字里面没有 A,B,C,D,E,F这几个情况,因此在case的default里面,除了1~10其他情况都应该给所有段选信号高电平)。
三、实际操作
3.1 程序架构
显然,对于我们的工程,1个verilog文件是不够用的,元件例化就非常重要,我们可以设计好一个一个的小模块,然后把这些小模块连接起来。下面是笔者的工程文件架构:
3.2 Verilog代码
3.2.1 main模块
module main(
input clk,
input rst,
output wire[5:0] sel,
output wire[7:0] seg_led);
wire [19:0] data;
wire en;
wire flag;
counter_100ms u0(
.clk(clk),
.rst(rst),
.en(en),
.data(data));
counter_1ms u1(
.clk(clk),
.rst(rst),
.flag(flag));
seg u2(
.clk(clk),
.rst(rst),
.data(data),
.flag(flag),
.sel(sel),
.seg_led(seg_led));
endmodule
3.2.2 100ms定时器模块
module counter_100ms(
input clk,
input rst,
output reg en,
output reg[19:0] data);
parameter max_count_100ms = 23'd5000_000;
reg[22:0] cnt;
reg flag1;
always@(posedge clk or negedge rst)
begin
if(!rst) begin
cnt <= 23'b0;
flag1 <= 1'b0;
end
else begin
if(cnt < max_count_100ms -1) begin
cnt <= cnt + 23'b1;
flag1 <= 1'b0;
end
else begin
cnt <= 23'b0;
flag1 <= 1'b1;
//When flag1 = 1'b1,it means that the data should be self increasing
end
end
end
always@(posedge clk or negedge rst)
begin
if(!rst) begin
en <= 1'b0; //close the enable signal
data <= 20'b0; //clear the data
end
else begin
en <= 1'b1;
if(flag1) begin
if(data < 20'd999_999)
data <= data + 20'b1;
else
data <= 20'b0;
end
else
data <= data;
end
end
endmodule
3.2.1.1 100ms定时器Testbench文件
`timescale 1ns/1ns
module counter_100ms_tb();
reg clk;
reg rst;
wire en;
wire[19:0] data;
parameter T = 20;
always #(T/2) clk = ~clk;
initial begin
clk = 0;
rst = 0;
#(T) rst = 1;
end
counter_100ms u0(
.clk(clk),
.rst(rst),
.en(en),
.data(data));
endmodule
3.2.2 1ms定时器模块
module counter_1ms(
input clk,
input rst,
output reg flag);
parameter max_count_1ms = 50000;
reg[15:0] cnt1;
always@(posedge clk or negedge rst)
begin
if(!rst) begin
cnt1 <= 16'b0;
flag <= 1'b0;
end
else begin
if(cnt1 < max_count_1ms - 1) begin
cnt1 <= cnt1 + 16'b1;
flag <= 1'b0;
end
else begin
cnt1 <= 16'b0;
flag <= 1'b1;
end
end
end
endmodule
3.2.2.1 1ms定时Testbench文件
`timescale 1ns/1ns
module counter_1ms_tb();
reg clk;
reg rst;
wire flag;
parameter T = 20;
always #(T/2) clk = ~clk;
initial begin
clk = 0;
rst = 0;
#(T) rst = 1;
end
counter_1ms u0(
.clk(clk),
.rst(rst),
.flag(flag));
endmodule
3.2.3 数码管显示模块
module seg(
input clk,
input rst,
input [19:0] data,
input flag, //flag is used for deciding which tube should be chosen.
output reg[5:0] sel, //Digital tube selection
output reg[7:0] seg_led);
//-----------------------Set 1 ------------------------------
reg [2:0] cnt_sel;
//-----------------------------------------------------------
//-----------------------Set 2 ------------------------------
reg [3:0] num_disp;
wire[3:0] data0; //Individual
wire[3:0] data1; //Ten
wire[3:0] data2; //Hundred
wire[3:0] data3; //Thousand
wire[3:0] data4; //Ten thousand
wire[3:0] data5; //One hundred thousand
reg [23:0] num;
/*Note:Why the reg num is 24 bit?
For example: if the data is 789210,it means we want the
six tube display :"7","8","9","2","1","0" respectively.
Let the digital tube display these numbers, we need to
turn them into binary.Like:
"0111","1000","1001","0010","0001","0000".
so 6-bit decimal number needs to 24 bit binary number at most
*/
//-----------------------------------------------------------
//It is used to switch the state of digital tube every 1ms
always@(posedge clk or negedge rst)
begin
if(!rst)
cnt_sel <= 3'b0;
else begin
if(flag) begin
if(cnt_sel < 3'd5)
cnt_sel <= cnt_sel + 1'b1;
else
cnt_sel <= 3'b0;
end
else
cnt_sel <= cnt_sel;
end
end
assign data0 = data % 4'd10;
assign data1 = data / 4'd10 % 4'd10;
assign data2 = data / 7'd100 % 4'd10;
assign data3 = data / 10'd1000 % 4'd10;
assign data4 = data / 14'd10000 % 4'd10;
assign data5 = data / 17'd100000;
always@(posedge clk or negedge rst)
begin
if(!rst)
num <= 24'b0;
else begin
if(data5) begin //If data is six digits
num[23:0] <= {data5,data4,data3,data2,data1,data0};
end
else begin
if(data4) begin
num[19:0] <= {data4,data3,data2,data1,data0};
num[23:20] <= 4'd10; //Don't show anything
end
else begin
if(data3) begin
num[15:0] <= {data3,data2,data1,data0};
num[23:16] <= {2{4'd10}};
end
else begin
if(data2) begin
num[11:0] <= {data2,data1,data0};
num[23:12] <= {3{4'd10}};
end
else begin
if(data1) begin
num[7:0] <= {data1,data0};
num[23:8] <= {4{4'd10}};
end
else begin
if(data0) begin
num[3:0] <= data0;
num[23:4] <= {5{4'd10}};
end
end
end
end
end
end
end
end
always@(posedge clk or negedge rst)
begin
if(!rst) begin
sel <= 6'b111111; //Bit select signal low level valid
num_disp <= 4'b0;
end
else begin
case(cnt_sel)
3'd0: begin
sel <= 6'b111110;
num_disp <= num[3:0];
end
3'd1: begin
sel <= 6'b111101;
num_disp <= num[7:4];
end
3'd2: begin
sel <= 6'b111011;
num_disp <= num[11:8];
end
3'd3: begin
sel <= 6'b110111;
num_disp <= num[15:12];
end
3'd4: begin
sel <= 6'b101111;
num_disp <= num[19:16];
end
3'd5: begin
sel <= 6'b011111;
num_disp <= num[23:20];
end
endcase
end
end
always@(posedge clk or negedge rst)
begin
if(!rst)
seg_led <= 8'hc0;
else begin
case(num_disp)
4'd0 : seg_led <= 8'b1100_0000;
4'd1 : seg_led <= 8'b1111_1001;
4'd2 : seg_led <= 8'b1010_0100;
4'd3 : seg_led <= 8'b1011_0000;
4'd4 : seg_led <= 8'b1001_1001;
4'd5 : seg_led <= 8'b1001_0010;
4'd6 : seg_led <= 8'b1000_0010;
4'd7 : seg_led <= 8'b1111_1000;
4'd8 : seg_led <= 8'b1000_0000;
4'd9 : seg_led <= 8'b1001_0000;
default:
//when num_disp is 4'd10, The default branch will be executed
//hence the tube will not show anything.
seg_led <= 8'b11111111;
endcase
end
end
endmodule
3.3 实验结果
下图是本次项目的RTL图:
上板发现实验结果符合预期。