FPGA基础入门【5】数码管仿真及实现

上一篇博文介绍了NEXYS 4的第一个工程blink闪烁,初步了解了FPGA使用的流程,从这一篇开始以难度从低到高的层次逐个介绍开发板上的接口。这次就用上按钮和数码管。

每次把过程写出来都担心写的不清楚,如果有看不懂请随时留言,我会尽快回复并添加内容。

FPGA基础入门【5】数码管

  • 数码管介绍
  • 功能设计
  • 代码
    • 按键消抖
    • 十位计数器
    • 数码管译码器
  • 仿真
  • 编译及烧写

数码管介绍

细节可以参考
NEXYS 4 DDR manual

数码管是用来显示数字的led阵列,由7个LED显示数字并加上一个LED显示小数点。数码管分共阴极和共阳极,共阳极的意思是这八个LED的阳极是连到同一个信号上的,共阴极则是八个LED的阴极是连到同一个信号上的。之所以这么做是因为控制8个LED原本需要16根线,如此可以减少控制线。
FPGA基础入门【5】数码管仿真及实现_第1张图片
并且共阳极和共阴极的信号还可以作为使能端,只有共阳极的信号为高或者共阴极信号为低时那一个数码管才会显示。再加上人眼只能识别24Hz的闪烁,因此只要以一定的频率持续扫描各个数码管,就不需要保持它们持续点亮,于是另外8个LED的控制信号也可以在数码管中共享,更减少了控制信号。这种控制方式叫做分时复用,典型例子就是红绿灯
FPGA基础入门【5】数码管仿真及实现_第2张图片
查阅手册,可以看到NEXYS 4中的数码管相关引脚。由于8个AN引脚是由NPN型三极管接到高电平的,而NPN型三极管在低电平是触发联通,因此要想数码管内特定LED亮起,比如最低位的D,则要AN0和CD都输出低电平
FPGA基础入门【5】数码管仿真及实现_第3张图片
另外,我们这次只使用低4位的数码管,要求高4位不亮。但CA-CG端口是八位共用的,我们需要把AN4到AN7接高电平。

功能设计

这次希望完成的功能是计数器,每次按键按下就加一,而开关可以将其清零。

这里需要按键消抖模块,每次按钮按下时输出一个脉冲。由于人手按下按键相对于电路不是很快,会造成几个毫秒的不稳定信号,而按住的时间往往在百毫秒级以上,因此不能仅仅靠按钮信号上升沿来计算,不然按一次会上升几个数。采取的做法是每5ms读取一次按钮输入,并且保存上一次按钮输入状态,仅仅当这两个状态显示由低到高的时候才输出高电平,否则输出低电平。

十进制的计数器的输入是按钮脉冲,输出是4个4-bit数,分别代表4位数码管要输出的数。每个4位数共有11个状态,数字0-9以及4’ha用来表示空白,这样数码管翻译器可以在这个状态时输出空白。初始状态为全空白,随着每次按钮按下而加一,在进位时会将原来空白的高位填充上。

数码管翻译器输入是来自十进制计数器的4个4位数,输出是8位数码管CA-CG、DP,以及8位AN0-AN7。此翻译器需要每1ms切换到下一个数码管并输出,4个数码管总共是4ms,刷新频率250Hz,高于人眼识别频率。

设计如下:

FPGA基础入门【5】数码管仿真及实现_第4张图片

代码

顶层模块的设计较为简单,主要按照设计将输入输出以及各个内部组成相互连接。

// Verilog code for segment project

module segment(
    input clock,
    input reset,
    input button,
    output [7:0] segment_out,
    output [7:0] digit
);

// Pushdown detection
wire pushdown;

pushdown_detect pd(
    .clock(clock),
    .button(button),
    .pushdown(pushdown)
);

// Decimal counter digit0-3
wire [3:0] counter0;
wire [3:0] counter1;
wire [3:0] counter2;
wire [3:0] counter3;
wire [2:0] carry;

decimal_counter d0(
    .clock     (clock),
    .reset     (reset),
    .carryin   (pushdown),
    .carryout  (carry[0]),
    .result    (counter0)
);

decimal_counter d1(
    .clock     (clock),
    .reset     (reset),
    .carryin   (carry[0]),
    .carryout  (carry[1]),
    .result    (counter1)
);

decimal_counter d2(
    .clock     (clock),
    .reset     (reset),
    .carryin   (carry[1]),
    .carryout  (carry[2]),
    .result    (counter2)
);

decimal_counter d3(
    .clock     (clock),
    .reset     (reset),
    .carryin   (carry[2]),
    .carryout  (),
    .result    (counter3)
);

// Segment translation
segment_trans trans(
    .clock         (clock),
    .reset         (reset),
    .counter0      (counter0),
    .counter1      (counter1),
    .counter2      (counter2),
    .counter3      (counter3),
    .digit         (digit[3:0]),
    .segment_out   (segment_out)
);

assign digit[7:4] = 4'b1111;

endmodule

主框架如下,输入为时钟、复位和按钮信号,输出为数码管AN0-AN7以及CA-CG、DP。

module segment(
    input clock,
    input reset,
    input button,
    output [7:0] segment_out,
    output [7:0] digit
);

endmodule

下面调用了按钮消抖模块,pushdown作为内部信号输入到十进制计数器。调用子模块的方法和testbench里面调用的方式一样。

// Pushdown detection
wire pushdown;

pushdown_detect pd(
    .clock(clock),
    .button(button),
    .pushdown(pushdown)
);

十进制计数器模块做成了每一位都可以拆分的形式,每一位除了时钟和复位信号之外,会在每次carryin端口收到脉冲信号时加一,并在需要进位时在carryout输出高电平传输到更高位的十进制计数器。在复位时默认输出4’hA,在后面的数码管译码器会转成空白输出。

这样设计的原因是以后也有可能用到十进制计数器,位数未知,这样写方便将来调用。

// Decimal counter digit0-3
wire [3:0] counter0;
wire [3:0] counter1;
wire [3:0] counter2;
wire [3:0] counter3;
wire [2:0] carry;

decimal_counter d0(
    .clock     (clock),
    .reset     (reset),
    .carryin   (pushdown),
    .carryout  (carry[0]),
    .result    (counter0)
);

decimal_counter d1(
    .clock     (clock),
    .reset     (reset),
    .carryin   (carry[0]),
    .carryout  (carry[1]),
    .result    (counter1)
);

decimal_counter d2(
    .clock     (clock),
    .reset     (reset),
    .carryin   (carry[1]),
    .carryout  (carry[2]),
    .result    (counter2)
);

decimal_counter d3(
    .clock     (clock),
    .reset     (reset),
    .carryin   (carry[2]),
    .carryout  (),
    .result    (counter3)
);

下面调用数码管译码器,counter0-3是十进制计数器发送的4位数,数码管译码器可以分时复用,以每4ms刷新一次的速度将其翻译成数码管信号输出。由于CA-CG信号共用,需要调用assign语句将AN4-7拉高以防止高4位显示。

前面说过寄存器reg是用always模块来调用赋值的,这里展示的是wire线网的赋值方式,由于它可以看做是一根导线,因此它没有延时概念。

// Segment translation
segment_trans trans(
    .clock         (clock),
    .reset         (reset),
    .counter0      (counter0),
    .counter1      (counter1),
    .counter2      (counter2),
    .counter3      (counter3),
    .digit         (digit[3:0]),
    .segment_out   (segment_out)
);

assign digit[7:4] = 4'b1111;

按键消抖

// Detect button pushdown

module pushdown_detect(
    input clock,    // 100MHz clock input
    input button,
    output reg pushdown
);

reg [31:0] counter;
reg [1:0]  button_p; // previous button status

// Count to 500k
// detect button status every 5ms
always @(posedge clock) begin
    if(counter < 32'd500000) begin
        counter <= counter + 32'd1;
    end
    else begin
        button_p[0] <= button;
        button_p[1] <= button_p[0];
        counter <= 32'd0;
    end
end

// If found the falling edge of button status
// Output get high only when button pushed down
always @(posedge clock) begin
    if(button_p[1] && (~button_p[0])) begin
        pushdown <= 1'b1;
    end
    else begin
        pushdown <= 1'b0;
    end
end

endmodule

输入的是100MHz的时钟,首先用最大值为500k的计数器将button信号的采样率降低到每5ms一次,并将新旧button信号状态存入button_p寄存器中。

在旧button信号为高,且新button信号为低,也就是button下降沿时输出一个pushdown高电平脉冲(宽度5ms),也就是探测按钮按下松开的瞬间。

十位计数器

// For the digit 0, carryin is connected to the signal
// For digit higher than 0, carryin is connected to the previous carryout

module decimal_counter(
    input            clock,
    input            reset,
    input            carryin,
    output reg       carryout,
    output reg [3:0] result
);

reg  carryin_p;
wire carryin_rise;
always @(posedge clock) begin
    carryin_p <= carryin;
end
assign carryin_rise = (~carryin_p) & carryin;

always @(posedge clock or posedge reset) begin
    if(reset) begin
        result   <= 4'd10;
        carryout <= 1'b0;
    end
    else if(carryin_rise) begin
        result   <= (result == 4'd10) ? 4'd1 : ( (result < 4'd9) ? result + 4'd1 : 4'd0 );
        carryout <= (result == 4'd9) ? 1'b1 : 1'b0;
    end
    else begin
        carryout <= 1'b0;
    end
end

endmodule

寄存器carryin_p是储存前一时钟carryin信号状态的寄存器,当carryin_p是低而新的carryin是高的时候意味着carryin上升沿,这个信号被赋值给carryin_rise。每次carryin上升沿时计数器应该增加1。

在复位时,该计数器会被赋值为4’hA,这是数码管为空白的代码。复位后第一次有carryin上升沿时会被赋值为1,从那之后就在0-9间递增循环,并且每当数字从9回到0时,carryout信号会输出一个高电平脉冲信号,提示进位。

在网上其他一些十进制加法器的代码中,或者没有考虑低位进位与高位进位,或者将多位十进制计数器写在一起,导致产生一个冗长的逻辑运算。这个十进制计数器可以近无限制叠加,不会给系统造成太大负担。

数码管译码器

// Translates 4-bits binary signal (0-9) to segment_out signal
// 0 -- 8'b00000011
// 1 -- 8'b10011111
// 2 -- 8'b00100101
// 3 -- 8'b00001101
// 4 -- 8'b10011001
// 5 -- 8'b01001001
// 6 -- 8'b01000001
// 7 -- 8'b00011111
// 8 -- 8'b00000001
// 9 -- 8'b00001001
// else -- blank

module segment_trans(
    input       clock,
    input       reset,
    input [3:0] counter0,
    input [3:0] counter1,
    input [3:0] counter2,
    input [3:0] counter3,
    output reg [3:0] digit,
    output reg [7:0] segment_out
);

// Looping digit
reg [31:0] counter;

always @(posedge clock or posedge reset) begin
    if(reset) begin
        counter <= 32'd0;
        digit <= 4'b1110;
    end
    else if(counter < 32'd100000)begin
        counter <= counter + 32'd1;
    end
    else begin
        counter <= 32'd0;
        digit <= {digit[2:0],digit[3]};
    end
end

// Showing digits
always @(posedge clock) begin
    if(~digit[0]) begin
        case(counter0)
        4'd0: begin segment_out <= 8'b00000011; end
        4'd1: begin segment_out <= 8'b10011111; end
        4'd2: begin segment_out <= 8'b00100101; end
        4'd3: begin segment_out <= 8'b00001101; end
        4'd4: begin segment_out <= 8'b10011001; end
        4'd5: begin segment_out <= 8'b01001001; end
        4'd6: begin segment_out <= 8'b01000001; end
        4'd7: begin segment_out <= 8'b00011111; end
        4'd8: begin segment_out <= 8'b00000001; end
        4'd9: begin segment_out <= 8'b00001001; end
        default: begin segment_out <= 8'b11111111; end
        endcase
    end
    else if(~digit[1]) begin
        case(counter1)
        4'd0: begin segment_out <= 8'b00000011; end
        4'd1: begin segment_out <= 8'b10011111; end
        4'd2: begin segment_out <= 8'b00100101; end
        4'd3: begin segment_out <= 8'b00001101; end
        4'd4: begin segment_out <= 8'b10011001; end
        4'd5: begin segment_out <= 8'b01001001; end
        4'd6: begin segment_out <= 8'b01000001; end
        4'd7: begin segment_out <= 8'b00011111; end
        4'd8: begin segment_out <= 8'b00000001; end
        4'd9: begin segment_out <= 8'b00001001; end
        default: begin segment_out <= 8'b11111111; end
        endcase
    end
    else if(~digit[2]) begin
        case(counter2)
        4'd0: begin segment_out <= 8'b00000011; end
        4'd1: begin segment_out <= 8'b10011111; end
        4'd2: begin segment_out <= 8'b00100101; end
        4'd3: begin segment_out <= 8'b00001101; end
        4'd4: begin segment_out <= 8'b10011001; end
        4'd5: begin segment_out <= 8'b01001001; end
        4'd6: begin segment_out <= 8'b01000001; end
        4'd7: begin segment_out <= 8'b00011111; end
        4'd8: begin segment_out <= 8'b00000001; end
        4'd9: begin segment_out <= 8'b00001001; end
        default: begin segment_out <= 8'b11111111; end
        endcase
    end
    else begin
        case(counter3)
        4'd0: begin segment_out <= 8'b00000011; end
        4'd1: begin segment_out <= 8'b10011111; end
        4'd2: begin segment_out <= 8'b00100101; end
        4'd3: begin segment_out <= 8'b00001101; end
        4'd4: begin segment_out <= 8'b10011001; end
        4'd5: begin segment_out <= 8'b01001001; end
        4'd6: begin segment_out <= 8'b01000001; end
        4'd7: begin segment_out <= 8'b00011111; end
        4'd8: begin segment_out <= 8'b00000001; end
        4'd9: begin segment_out <= 8'b00001001; end
        default: begin segment_out <= 8'b11111111; end
        endcase
    end
end

endmodule

这里原本想和十进制计数器一样做成不同位模块化的,但涉及到分时复用的周期,要在模块中添加parameter配置,这个希望之后再讲,因此还是做成了集成化的。

// Looping digit
reg [31:0] counter;

always @(posedge clock or posedge reset) begin
    if(reset) begin
        counter <= 32'd0;
        digit <= 4'b1110;
    end
    else if(counter < 32'd100000)begin
        counter <= counter + 32'd1;
    end
    else begin
        counter <= 32'd0;
        digit <= {digit[2:0],digit[3]};
    end
end

上方是一个移位寄存器,复位时最低位为0,其他都为1,也就是只有最低位显示,计数器计到100k,也就是每1ms,digit就会循环左移显示其他位。

之前还没介绍过这种用法,在Verilog代码中,每个信号都被看做二进制,digit默认为digit[3:0]即从高位到低位顺序下去,而花括号代表集合,即从高到低将一系列信号集中起来形成一个新的信号group,**digit <= {digit[2:0],digit[3]};**意味着digit[0]赋值给digit[1],digit[1]赋值给digit[2],digit[2]赋值给digit[3],digit[3]赋值给digit[0],因此达到了循环移位寄存器的效果。

        case(counter0)
        4'd0: begin segment_out <= 8'b00000011; end
        4'd1: begin segment_out <= 8'b10011111; end
        4'd2: begin segment_out <= 8'b00100101; end
        4'd3: begin segment_out <= 8'b00001101; end
        4'd4: begin segment_out <= 8'b10011001; end
        4'd5: begin segment_out <= 8'b01001001; end
        4'd6: begin segment_out <= 8'b01000001; end
        4'd7: begin segment_out <= 8'b00011111; end
        4'd8: begin segment_out <= 8'b00000001; end
        4'd9: begin segment_out <= 8'b00001001; end
        default: begin segment_out <= 8'b11111111; end
        endcase

这是case语句,跟C语言中的switch是同类,根据输入信号counter0而选择给segment_out寄存器赋值不同的配置。这8位是根据下图配置不同数字,比如数字4是BCFG亮而其他暗,并且输出低电平是点亮该LED,因此数字4对应的是8’b10011001。同理配置其他数字。再用if else语句,分别在不同的digit数位输出对应的十进制数(或者空白)。
FPGA基础入门【5】数码管仿真及实现_第5张图片

仿真

testbench代码如下

`timescale 1ns/1ns

module tb_segment;

reg clock;
reg reset;
reg button;
wire [3:0] digit;
wire [7:0] segment_out;

integer i;

initial begin
    clock = 1'b0;
    reset = 1'b0;
    button = 1'b0;
    // Reset for 1us
    #100 
    reset = 1'b1;
    #1000
    reset = 1'b0;
    for(i=0; i<1000; i=i+1) begin
        #100  button = 1'b1;
        #10   button = 1'b0;
        #20   button = 1'b1;
        #5000 button = 1'b0;
        #3000;
    end
end

// Generate 100MHz clock signal
always #5 clock <= ~clock;

segment segment(
    .clock         (clock),
    .reset         (reset),
    .button        (button),
    .segment_out   (segment_out),
    .digit         (digit)
);

endmodule

与blink的testbench一样,通过always #5 clock <= ~clock;语句产生一个100MHz时钟作为基础,在initial中复位后,把button按下1000次(每次持续5us,间隔3us)。不过为了这个仿真能在有限时间内完成,应该把之前设计中所有计数器的最大值全部缩小1000倍,不然仿真时间超过s量级就不好玩了。

sim.do的代码如下,如果需要的话,可以只拷贝add_wave之前的,自己添加好想要的wave后再运行run 2ms

vlib work
vlog ../src/segment.v ../src/decimal_counter.v ../src/pushdown_detect.v ../src/segment_trans.v ./tb_segment.v
vsim work.tb_segment

add wave -noupdate /tb_segment/segment/button
add wave -noupdate /tb_segment/segment/carry
add wave -noupdate /tb_segment/segment/clock
add wave -noupdate /tb_segment/segment/counter0
add wave -noupdate /tb_segment/segment/counter1
add wave -noupdate /tb_segment/segment/counter2
add wave -noupdate /tb_segment/segment/counter3
add wave -noupdate /tb_segment/segment/digit
add wave -noupdate /tb_segment/segment/pushdown
add wave -noupdate /tb_segment/segment/reset
add wave -noupdate /tb_segment/segment/segment_out
run 2ms

cd
首先需要转到存有所有源文件的根目录,这条命令需要自己手动输入。

之后需要加入其他观察信号时,首先在sim栏内点到相应的模块:
FPGA基础入门【5】数码管仿真及实现_第6张图片
在那之后objects栏中会出现这一模块中所有信号,如果需要观察某一信号,则选中它右键添加wave:
FPGA基础入门【5】数码管仿真及实现_第7张图片
此时你就可以在waveform中看到它了。有几个小技巧:

  1. 下图中左下角的红圈,它可以在toggle leaf name 和全名之间切换,toggle leaf name是在其所在模块内的名称,而全名是包含其路径的,相对较长,用toggle leaf name 会好看很多;
  2. 按住ctrl选中多个信号后右键->Radix(基数)中有不同的进制选项,你可以让其以二进制显示,或者带符号十进制,无符号十进制,十六进制等等;
  3. 在选中十进制之后还可以右键->Format->Analog (automatic),会自动画出曲线,这在数据有规律变化时方便看出其趋势
  4. 如果想要看到这些操作对应的命令行代码,按ctrl+S保存成wave.do,打开就可以看到形成相应波形的操作代码,如果操作繁琐并且你希望重复时,保存成wave.do,下一次需要的时候执行do wave.do即可

这些技巧没有在sim.do中显示,需要的话可以利用第四条技巧提取相应代码并加入sim.do
FPGA基础入门【5】数码管仿真及实现_第8张图片

编译及烧写

下面就进入Vivado编译。和blink中一样的操作,新建一个叫segment的project,添加全部4个代码文件segment.v、pushdown_detect.v、decimal_counter.v和segment_trans.v。这时看Source->Hierachy,可以看到Vivado自动按照模块内部的调用关系做出了模块树,也就选出了这个设计的top_level最顶层。如果发现有多个可能的最顶层,即它们没有调用与被调用的关系,则自动选择一个成为top,其他的被忽略。
FPGA基础入门【5】数码管仿真及实现_第9张图片
下一步加入约束constraint文件segment.xdc,同样这是用标准模板取自己需要部分修改出来的(NEXYS 4 DDR Master XDC):

## This file is a general .xdc for the Nexys4 DDR Rev. C
## To use it in a project:
## - uncomment the lines corresponding to used pins
## - rename the used ports (in each line, after get_ports) according to the top level signal names in the project

## Clock signal
set_property -dict { PACKAGE_PIN E3    IOSTANDARD LVCMOS33 } [get_ports { clock }]; #IO_L12P_T1_MRCC_35 Sch=clk100mhz
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports {clock}];

##Switches

set_property -dict { PACKAGE_PIN J15   IOSTANDARD LVCMOS33 } [get_ports { reset }]; #IO_L24N_T3_RS0_15 Sch=sw[0]

##7 segment display

set_property -dict { PACKAGE_PIN T10   IOSTANDARD LVCMOS33 } [get_ports { segment_out[7] }]; #IO_L24N_T3_A00_D16_14 Sch=ca
set_property -dict { PACKAGE_PIN R10   IOSTANDARD LVCMOS33 } [get_ports { segment_out[6] }]; #IO_25_14 Sch=cb
set_property -dict { PACKAGE_PIN K16   IOSTANDARD LVCMOS33 } [get_ports { segment_out[5] }]; #IO_25_15 Sch=cc
set_property -dict { PACKAGE_PIN K13   IOSTANDARD LVCMOS33 } [get_ports { segment_out[4] }]; #IO_L17P_T2_A26_15 Sch=cd
set_property -dict { PACKAGE_PIN P15   IOSTANDARD LVCMOS33 } [get_ports { segment_out[3] }]; #IO_L13P_T2_MRCC_14 Sch=ce
set_property -dict { PACKAGE_PIN T11   IOSTANDARD LVCMOS33 } [get_ports { segment_out[2] }]; #IO_L19P_T3_A10_D26_14 Sch=cf
set_property -dict { PACKAGE_PIN L18   IOSTANDARD LVCMOS33 } [get_ports { segment_out[1] }]; #IO_L4P_T0_D04_14 Sch=cg
set_property -dict { PACKAGE_PIN H15   IOSTANDARD LVCMOS33 } [get_ports { segment_out[0] }]; #IO_L19N_T3_A21_VREF_15 Sch=dp

set_property -dict { PACKAGE_PIN J17   IOSTANDARD LVCMOS33 } [get_ports { digit[0] }]; #IO_L23P_T3_FOE_B_15 Sch=an[0]
set_property -dict { PACKAGE_PIN J18   IOSTANDARD LVCMOS33 } [get_ports { digit[1] }]; #IO_L23N_T3_FWE_B_15 Sch=an[1]
set_property -dict { PACKAGE_PIN T9    IOSTANDARD LVCMOS33 } [get_ports { digit[2] }]; #IO_L24P_T3_A01_D17_14 Sch=an[2]
set_property -dict { PACKAGE_PIN J14   IOSTANDARD LVCMOS33 } [get_ports { digit[3] }]; #IO_L19P_T3_A22_15 Sch=an[3]
set_property -dict { PACKAGE_PIN P14   IOSTANDARD LVCMOS33 } [get_ports { digit[4] }]; #IO_L8N_T1_D12_14 Sch=an[4]
set_property -dict { PACKAGE_PIN T14   IOSTANDARD LVCMOS33 } [get_ports { digit[5] }]; #IO_L14P_T2_SRCC_14 Sch=an[5]
set_property -dict { PACKAGE_PIN K2    IOSTANDARD LVCMOS33 } [get_ports { digit[6] }]; #IO_L23P_T3_35 Sch=an[6]
set_property -dict { PACKAGE_PIN U13   IOSTANDARD LVCMOS33 } [get_ports { digit[7] }]; #IO_L23N_T3_A02_D18_14 Sch=an[7]


##Buttons

set_property -dict { PACKAGE_PIN N17   IOSTANDARD LVCMOS33 } [get_ports { button }]; #IO_L9P_T1_DQS_14 Sch=btnc

到此就可以点击generate bitstream来走整个编译流程了,最后生成segment.bit文件,就像blink一样,插上板子,打开hardware manager,点open target-> auto connect,然后program device,应该可以看到结果了。注意,如果为了仿真把所有计数器最大值缩小为1/1000,要把它们改回来

最终效果如下
FPGA基础入门【5】数码管仿真及实现_第10张图片

你可能感兴趣的:(FPGA)