基于EP4CE6F17C8的FPGA数码管动态显示实例

一、电路模块

1、数码管

开发板板载了6个数码管,全部为共阳型,原理图如下图所示,段码端引脚为DIG[0]~DIG[7]共8位(包含小数点),位选端引脚为SEL[0]~SEL[5]共6位。端口均为低电平有效。

其实物图如下所示。

数码管引脚分配见下表。

2、时钟晶振

开发板板载了一个50MHz的有源晶振,为系统提供时钟。

其实物图如下所示。

时钟输出引脚分配见下表。

3、按键

开发板板载了4个独立按键,其中有3个用户按键(KEY1~KEY3),1个功能按键(RESET)。按键按下为低电平(0),释放为高电平(1),4个按键的原理图如下图所示。本例中只使用了RESET键。

其实物图如下所示。

按键的引脚分配见下表。

 

二、实验代码

本例实现6个数码管显示123456,代码使用Verilog编写,采用例化的形式,共有两个文件。

先编写数码管实现字形的程序,模块名称为seg,文件名称为seg.v,代码如下。

module seg(
    input[3:0]  data,                //显示的字形,可显示0~F十六个字形,所以需要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的编码
        4'ha:seg <= 8'b1000_1000;    //字形A的编码
        4'hb:seg <= 8'b1000_0011;    //字形B的编码
        4'hc:seg <= 8'b1100_0110;    //字形C的编码
        4'hd:seg <= 8'b1010_0001;    //字形D的编码
        4'he:seg <= 8'b1000_0110;    //字形E的编码
        4'hf:seg <= 8'b1000_1110;    //字形F的编码
        default:seg <= 7'b111_1111;//默认不显示
    endcase
end
endmodule

接下来编写数码管显示的程序,并设置为顶层模块,模块名称为seg_show,文件名称为seg_show.v,代码如下。

module seg_show(
    input clk,                    //板载50HMz系统时钟
    input rst,                    //复位按键
    output reg[7:0] seg7,         //段码端口
    output reg[5:0] bit           //位选端口
);

reg[19:0]     cnt;                //定义20位时钟计数器
//下面定义6个存放显示字形的变量
wire[7:0]     seg_data0,seg_data1,seg_data2,seg_data3,seg_data4,seg_data5;
//下面定义6个要显示的数字
wire[3:0]     data0 = 4'd1;    //data0赋值1
wire[3:0]     data1 = 4'd2;    //data1赋值2
wire[3:0]     data2 = 4'd3;    //data2赋值3
wire[3:0]     data3 = 4'd4;    //data3赋值4
wire[3:0]     data4 = 4'd5;    //data4赋值5
wire[3:0]     data5 = 4'd6;    //data5赋值6

//以下例化了6个数码管seg0~seg5,把各自的显示数值与字形编码联系起来
seg seg0(.data(data0), .seg(seg_data0));
seg seg1(.data(data1), .seg(seg_data1));
seg seg2(.data(data2), .seg(seg_data2));
seg seg3(.data(data3), .seg(seg_data3));
seg seg4(.data(data4), .seg(seg_data4));
seg seg5(.data(data5), .seg(seg_data5));

//20毫秒循环计数
always@(posedge clk or negedge rst)    //敏感信号为时钟上沿或复位下沿
begin
    if(!rst)                           //低电平复位
        cnt <= 20'd0;                  //复位时时钟计数器清零
    else if(cnt == 20'd999_999)        //时钟计数器到达20毫秒时
        cnt <= 20'd0;                  //时钟计数器清零
    else
        cnt <= cnt + 20'd1;            //否则时钟计数器加1,即来一次时钟脉冲加一次
end

//数码管扫描显示
always@(posedge clk or negedge rst)    //敏感信号为时钟上沿或复位下沿
begin
    if(!rst)                           //低电平复位时数码管全灭
    begin
        bit <= 6'b111111;
        seg7 <= 8'hff;
    end
    else if(cnt == 20'd166_666)        //时钟到3毫秒时,数码管0显示1
    begin
        bit <= 6'b111110;
        seg7 <= seg_data0;
    end
    else if(cnt == 20'd333_333)        //时钟到6毫秒时,数码管1显示2
    begin
        bit <= 6'b111101;
        seg7 <= seg_data1;
    end
    else if(cnt == 20'd499_999)        //时钟到10毫秒时,数码管2显示3
    begin
        bit <= 6'b111011;
        seg7 <= seg_data2;
    end
    else if(cnt == 20'd666_666)        //时钟到13毫秒时,数码管3显示4
    begin
        bit <= 6'b110111;
        seg7 <= seg_data3;
    end
    else if(cnt == 20'd833_333)        //时钟到16毫秒时,数码管4显示5
    begin
        bit <= 6'b101111;
        seg7 <= seg_data4;
    end
    else if(cnt == 20'd999_999)        //时钟到20毫秒时,数码管5显示6
    begin
        bit <= 6'b011111;
        seg7 <= seg_data5;
    end
end
endmodule

seg_show.v的代码还可以写成如下形式。

module seg_show(
    input clk,                    //板载50HMz系统时钟
    input rst,                    //复位按键
    output reg[7:0] seg7,         //段码端口
    output reg[5:0] bit           //位选端口
);

reg[17:0]     cnt;                //定义20位时钟计数器
reg[2:0]      scan_sel;           //定义扫描位置计数器
//下面定义6个存放显示字形的变量
wire[7:0]     seg_data0,seg_data1,seg_data2,seg_data3,seg_data4,seg_data5;
//下面定义6个要显示的数字
wire[3:0]     data0 = 4'd1;    //data0赋值1
wire[3:0]     data1 = 4'd2;    //data1赋值2
wire[3:0]     data2 = 4'd3;    //data2赋值3
wire[3:0]     data3 = 4'd4;    //data3赋值4
wire[3:0]     data4 = 4'd5;    //data4赋值5
wire[3:0]     data5 = 4'd6;    //data5赋值6

//以下例化了6个数码管seg0~seg5,把各自的显示数值与字形编码联系起来
seg seg0(.data(data0), .seg(seg_data0));
seg seg1(.data(data1), .seg(seg_data1));
seg seg2(.data(data2), .seg(seg_data2));
seg seg3(.data(data3), .seg(seg_data3));
seg seg4(.data(data4), .seg(seg_data4));
seg seg5(.data(data5), .seg(seg_data5));

//3.3毫秒循环计数
always@(posedge clk or negedge rst)        //敏感信号为时钟上沿或复位下沿
begin
    if(rst == 1'b0)                        //低电平复位时计数器全部清零
    begin
        cnt <= 18'd0;
        scan_sel <= 3'd0;
    end
    else if(cnt >= 18'd166_666)            //时钟计数器到达3.3毫秒时
    begin
        cnt <= 18'd0;                      //时钟计数器清零
        if(scan_sel == 3'd5)               //如果扫描位置计数器已经到5则恢复0
            scan_sel <= 3'd0;
        else
            scan_sel <= scan_sel + 3'd1;   //否则扫描位置计数器加1,即每3.3ms加一次
    end
    else
        begin
            cnt <= 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显示1
            begin
                bit <= 6'b111110;
                seg7 <= seg_data0;
            end
            4'd1:                           //数码管0显示2
            begin
                bit <= 6'b111101;
                seg7 <= seg_data1;
            end
            4'd2:                           //数码管0显示3
            begin
                bit <= 6'b111011;
                seg7 <= seg_data2;
            end
            4'd3:                           //数码管0显示4
            begin
                bit <= 6'b110111;
                seg7 <= seg_data3;
            end
            4'd4:                           //数码管0显示5
            begin
                bit <= 6'b101111;
                seg7 <= seg_data4;
            end
            4'd5:                           //数码管0显示6
            begin
                bit <= 6'b011111;
                seg7 <= seg_data5;
            end
            default:                        //数码管全部熄灭
            begin
                bit <= 6'b11_1111;
                seg7 <= 8'hff;
            end
        endcase
end
endmodule

三、代码说明

1、第一种方式中使用了计数器来分频时钟,总计约20ms的循环,并在此间分配了6个数码管进行显示,每个数码管大约分得3.3ms左右的显示时间。
2、时钟计数器选取了20位,对50MHz时钟进行分频,最大能达到20.9ms,刚好大于所需要的20ms,所以取20位进行计数比较合适,太多了浪费硬件资源。
3、第二种方式只用18位计数器来定时3.3ms,然后控制扫描位置计数器增长,并依靠扫描位置计数器的值来选择要点亮的数码管。
4、时钟分频还可以采用PLL的方式,待频率降下来了之后,再使用少量的计数器来得到所需要的时间,可进一步节约资源,但会消耗一个PLL。
5、程序中使用了数码管例化的形式,共例化出了6个数码管。例化的数码管负责解码,即把要显示的数值解码成显示的字形编码,再输出给数码管的段码端口。
6、在定义的变量中,data0~data5分别给了固定值1~6,seg_data0~seg_data5分别接收解码后的字形编码,并在下面的过程语句中,分别送给相应数码管的段端口进行显示。以0号数码管为例,它要显示字符1,先把1赋值给变量data0,然后通过例化seg seg0(.clk(clk), .data(data0), .seg(seg_data0));把data0的值传给元件内部变量data(即文件seg.v中的data),而后得到字形编码,输出给变量seg_data0,然后当时钟计数cnt达到20'd166_666时,把seg_data0的数据送给seg7端口,同时打开0号数码管的位选通端口(bit <= 6'b111110;),此时该数码管显示出字形1。其他数码管的显示原理以此类推。
7、由上可以看出,元件例化有点类似于C语句中的函数调用及返回,但元件的每一个例化都是要产生出实际的电路的(要消耗逻辑器件),可查看生成的RTL电路图。

四、实验步骤

FPGA开发的详细步骤请参见“基于EP4CE6F17C8的FPGA开发流程(以半加器为例)”一文,本例只对不同之处进行说明。

本例工程放在D:\EDA_FPGA\Exam_3文件夹下,工程名称为Exam_3。有两个模块文件,一个名称为seg_show.v,设置为顶层实体,另一个名称为seg.v,用于提供例化。其余步骤与“基于EP4CE6F17C8的FPGA开发流程”中的一样。

接下来看管脚约束,本例中6个数码管一共有16个引脚,再加上时钟晶振和复位按钮,一共18个。具体的端口分配如下图所示。

对于未用到的引脚设置为三态输入方式,多用用途引脚全部做为普通I/O端口,电压设置为3.3-V LVTTL(与”基于EP4CE6F17C8的FPGA开发流程“中的一样)。需要注意,程序中的每个端口都必须为其分配管脚,如果系统中存在未分配的I/O,软件可能会进行随机分配,这将造成不可预料的后果,存在烧坏FPGA芯片的风险。

接下来对工程进行编译,编译完成后,可查看一下逻辑器件的消耗情况,第一种代码的消耗如下图所示。

第二种代码的消耗如下图所示。

可以看到,第二种方式消耗的器件更少。另外,还可以点击菜单Tools->Netlist Viewers->RTL Viewer,查看一下生成的RTL电路图。

最后进行程序下载,并查看结果。下图为6个数码管的动态显示效果。

当按下复位键后,所有数码管均熄灭,如下图所示。

posted @ 2024-03-16 11:25  fxzq  阅读(803)  评论(0编辑  收藏  举报