[FPGA 学习记录] 数码管动态显示


数码管动态显示

在上一小节当中,我们对数码管的静态显示做了一个详细的讲解;但是如果单单只掌握数码管的静态显示这种显示方式是远远不够的,因为数码管的静态显示当中,被选中的数码位它们显示的内容都是相同的,这种显示方式在我们的实际应用当中显然是不合适的;我们希望控制每个数码位能够独立的显示我们想要显示的内容,如何实现这一操作呢?就是本小节所要讲解的内容:数码管的动态显示。

本小节的主要内容分为两个部分:

  1. 第一部分是理论学习,在这一部分,我们会对数码管的动态显示的工作原理做一个详细的讲解;
  2. 第二部分是实战演练,在这一部分,会通过实验工程设计并实现数码管的动态显示。

首先是理论学习

1 理论学习

我们征途系列开发板使用的是六位八段数码管,实物图1如下

6位0.36英寸红色数码管 共阳极

在开发板的右上角

dfvhdkyerj32qekfhdfiguyd9fg328872r6e9rfwy

它的内部结构图如下

image-20231124162809795

由图可知,六位八段数码管当中,每个数码位的段选信号全部连接到了一起,然后进行输出;每个数码位单独引出一个位选信号用来控制数码位的选择,这种连接方式会使得被选中的数码位它们显示的内容都是相同的,因为它们的段选信号已经全部连接到了一起。如何使用这个六位八段数码管来实现我们的数码管的动态显示呢?我们需要使用一种方式:动态扫描

如何使用动态扫描的方式来实现数码管的动态显示呢?我们给大家举一个例子:比如说,我们想要使用我们的六位八段数码管来显示数字 123456

image-20231124164305622

如何使用我们的六位八段数码管来显示数字 123456 呢?

首先我们选中第一个数码位,让这个数码位显示数字 1;然后它显示的时间我们设为 [FPGA 学习记录] 数码管动态显示,这个 [FPGA 学习记录] 数码管动态显示 可以看作一个周期

image-20231124164729258

当我们的第一个数码位完成一个 [FPGA 学习记录] 数码管动态显示 周期数字 1 的显示之后,立刻选中我们的第二个数码位;注意,此时只选中了第二个数码位让它显示数字 2,显示的时间同样是一个周期 [FPGA 学习记录] 数码管动态显示

image-20231124164904207

当我们的第二个数码位完成了一个周期 [FPGA 学习记录] 数码管动态显示 数字 2 的显示之后,立刻选中第三个数码位;这儿注意,也是只选中了第三个数码位让它显示数字 3,同样显示时间为 [FPGA 学习记录] 数码管动态显示

image-20231124165016384

依次往下类推,那么此时就显示 4

image-20231124165125710

然后是 5

image-20231124165137840

然后是 6

image-20231124165150869

当我们第六个数码位完成了一个周期 [FPGA 学习记录] 数码管动态显示 数字 6 的显示之后,再重新选中第一个数码位;这儿也是只选中了第一个数码位,让它继续显示数字 1,然后显示的时间仍然是 [FPGA 学习记录] 数码管动态显示 周期,这样依次往下循环。

通过上述动态显示过程的描述,我们知道这样一个循环是六个周期就是 [FPGA 学习记录] 数码管动态显示;如果说给这个 [FPGA 学习记录] 数码管动态显示 规定一个确切的时间会怎样呢?我们首先给 [FPGA 学习记录] 数码管动态显示 规定一个确切的时间为 1s;如果说 [FPGA 学习记录] 数码管动态显示 等于 1s,我们的六位八段数码管的六个数码位会依次显示 1、2、3、4、5、6 每个数字显示的时间为 1s

间隔1秒动态显示123456

如果进一步把这个 [FPGA 学习记录] 数码管动态显示 进行缩短,比如说我们缩短到 0.2s 这时候我们的六位八段数码管,它的六个数码位会进行闪烁显示,显示的内容依次是 1、2、3、4、5、6

间隔20毫秒动态显示123456

如果进一步缩短时间 [FPGA 学习记录] 数码管动态显示 把它缩短为 1ms,这时候实际上我们的六位八段数码管,它的六个数码位也是进行依次闪烁的显示,显示的内容依然依次是 1、2、3、4、5、6 每个数字显示时间是 1ms

间隔1毫秒动态显示123456

但是它们切换的频率太快了,我们的肉眼不能分辨这种闪烁,就误以为我们的六位八段数码管六个数码位在同时进行显示,而且显示的内容是 123456。

这样就使用动态扫描的方式实现了数码管的动态显示。

使用动态扫描的方式实现数码管的动态显示,实际上是利用了两个现象:人眼的视觉暂留特性和数码管的余晖效应。我们的人眼在观察景物时,光信号传入到大脑神经需要经过一段时间,光的作用结束之后我们的视觉影像并不会立刻的消失,这种残留的视觉被称为后像,这种现象就被称为视觉暂留;数码管的余晖效应是什么意思呢?当我们停止向我们的发光二极管供电时,我们的发光二极管它的亮度仍能够维持一段时间。我们的动态扫描利用这两个特性就实现了数码管的动态显示。

以上内容就是数码管动态显示的工作原理,接下来就开始进行实战演练

2 实战演练

在实战演练部分,我们会通过实验工程设计并实现数码管的动态显示。

首先,先来说一下我们的实验目标。我们的实验目标是使用我们的六位八段数码管,来实现数码管的动态显示,显示的内容是十进制的 0 到十进制的最大值 999999,当计数到最大值让它归零,循环显示;每 0.1s 加 1,也就是说第一个 0.1s 显示的是 0,第二个 0.1s 显示的是 1,第三个 0.1s 显示的是 2,然后依次往后排

六位数码管显示0~359999

六位数码管显示360000~719999

六位数码管显示720000~999999

了解了实验目标之后,下面开始程序的设计。

首先建立一个文件体系

segment_595_dynamic
├─doc
├─quartus_prj
├─rtl
└─sim

然后打开 doc 文件夹建立一个 Visio 文件,用来绘制框图和波形图

segment_595_dynamic
├─doc
│      segment_595_dynamic.vsdx
│
├─quartus_prj
├─rtl
└─sim

首先,先来绘制模块框图,顶层模块 top_segment_595 的框图如下

image-20231127150107105

输入信号

  • sys_clk:系统时钟信号
  • sys_rst_n:系统复位信号

输出信号(传入到 74HC595)

  • ds:串行数据
  • shcp:移位寄存器时钟
  • stcp:存储寄存器时钟
  • oe_n:输出使能

下面结合我们的实验目标和层次化的设计思想,将整个系统工程进行子功能的划分。我们的实验目标是使用我们的六位八段数码管来实现数码管的动态显示,显示的内容是 0 到 999999;这个显示的数据肯定需要一个模块来产生,我们就先定义一个新的子功能模块:数据生成模块 data_gen,让它产生我们需要显示的数据

image-20231127151634608

输入信号

  • sys_clk:系统时钟信号
  • sys_rst_n:系统复位信号

输出信号

  • data[19:0]:待显示的数据。它的最大值是 [FPGA 学习记录] 数码管动态显示,换算成二进制就是 [FPGA 学习记录] 数码管动态显示,所以它的位宽是 20 位宽
  • point[5:0]:(数码管动态显示模块 segment_595_dynamic 可能会用于温湿度的显示,所以说要增加小数点的生成)小数点信号。六位八段数码管有 6 个小数点段,所以该信号的位宽是 6 位宽
  • sign:(数码管动态显示模块 segment_595_dynamic 可能会用于电压测量的显示,所以说要生成符号位)符号位。数值的负号
  • seg_en:(为了能够更好的控制我们的数码管动态显示模块 segment_595_dynamic,生成一个使能信号)当使能信号为有效的高电平时,数码管可以正常的显示;当使能信号为低电平时,数码管就不工作

有了数据产生模块之后,接下来就是数码管动态显示模块 segment_595_dynamic

image-20231127154004679

输入信号

  • sys_clk:系统时钟信号
  • sys_rst_n:系统复位信号
  • data[19:0]:待显示数据
  • point[5:0]:待显示数据的小数点
  • sign:待显示数据的负号
  • seg_en:控制数码管动态显示模块工作与否的使能信号

数码管动态显示模块 segment_595_dynamic 在后面的实验工程中会经常用到,这里为了提高它的复用性,所以增加小数点、符号、使能等信号端口。

输出信号(就是传入到 74HC595 芯片的四路信号)

  • ds:串行数据
  • shcp:移位寄存器时钟
  • stcp:存储寄存器时钟
  • oe_n:输出使能

下面对数码管动态显示模块 segment_595_dynamic 继续进行功能的划分,划分的方式参照数码管的静态显示将它划分为两个功能模块:第一个模块是动态显示驱动模块 segment_dynamic、第二个模块是 74HC595 控制模块 hc595_ctrl

为什么要进行模块的进一步划分呢?因为 74HC595 控制模块 hc595_ctrl 在数码管的静态显示当中已经完全实现了,我们可以直接调用。我们使用动态显示驱动模块 segment_dynamic 生成位选信号和段选信号,然后 74HC595 控制模块 hc595_ctrl 将位选信号和段选信号转化为 dsshcpstcpoe_n 这四路信号传入到 74HC595 控制芯片。

动态显示驱动模块 segment_dynamic

image-20231127154814410

输入信号

  • sys_clk:系统时钟信号
  • sys_rst_n:系统复位信号
  • data[19:0]:待显示数据
  • point[5:0]:待显示数据小数点位
  • sign:待显示数据的符号位
  • seg_en:数码管动态显示模块使能信号

输出信号

  • sel[5:0]:位选信号
  • seg[7:0]:段选信号

74HC595 控制模块 hc595_ctrl

image-20231127160616230

输入信号

  • sys_clk:系统时钟信号
  • sys_rst_n:系统复位信号
  • sel[5:0]:位选信号
  • seg[7:0]:段选信号

输出信号(就是传入到 74HC595 芯片的四路信号)

  • ds:串行数据
  • shcp:移位寄存器时钟
  • stcp:存储寄存器时钟
  • oe_n:输出使能

各子功能模块的模块框图绘制完成,下面开始系统框图的绘制。

首先是数码管动态显示模块 segment_595_dynamic

image-20231127161650380

它的模块功能由两个子功能模块实现,输入到数码管动态显示模块 segment_595_dynamic 当中的时钟信号 sys_clk 和复位信号 sys_rst_n 分别传给两个子功能模块,数码管动态显示模块 segment_595_dynamic 产生的位选信号 sel[5:0] 和段选信号 seg[7:0] 传给 74HC595 控制模块 hc595_ctrl

下面开始系统框图的绘制。顶层模块 top_segment_595 包含两个子功能模块:一个是数据产生模块 data_gen,另一个是数码管动态显示模块 segment_595_dynamic

image-20231127162442251

传入到顶层模块 top_segment_595 的时钟信号 sys_clk 和复位信号 sys_rst_n 要传给两个子功能模块,然后数据生成模块 data_gen 产生的待显示数据 data[19:0]、小数点位 point[5:0]、符号位 sign 和使能信号 seg_en 要传给数码管动态显示模块 segment_595_dynamic
通过这个系统框图,我们可以了解各个模块之间的层次关系以及信号的走向。

系统框图绘制完成之后,接下来就开始各个子功能模块它们功能的实现。

首先是我们的数据生成模块 data_gen,我们先来绘制一下数据生成模块 data_gen 的波形图

image-20231127174420027

由模块框图可知,输入信号有两路:时钟信号 sys_clk 和复位信号 sys_rst_n
在我们的实验目标当中我们提到:我们想要使用我们的六位八段数码管实现数码管的动态显示,显示的内容是十进制的数字 [FPGA 学习记录] 数码管动态显示 到最大值 [FPGA 学习记录] 数码管动态显示 的循环计数,计数的间隔是每 [FPGA 学习记录] 数码管动态显示[FPGA 学习记录] 数码管动态显示;这个 [FPGA 学习记录] 数码管动态显示 的计数就需要一个计数器来实现,所以我们需要声明一个计数器变量 cnt_100ms[FPGA 学习记录] 数码管动态显示 进行计数。首先,当复位信号 sys_rst_n 有效时给计数器变量 cnt_100ms 赋一个初值 [FPGA 学习记录] 数码管动态显示;复位信号 sys_rst_n 无效时 cnt_100ms 每个系统时钟周期自加 [FPGA 学习记录] 数码管动态显示cnt_100ms 的计数周期是 [FPGA 学习记录] 数码管动态显示 就是 [FPGA 学习记录] 数码管动态显示cnt_100ms 计数器完成 [FPGA 学习记录] 数码管动态显示 的计数,要计数多少个系统时钟周期呢?我们知道系统时钟的频率是 [FPGA 学习记录] 数码管动态显示 换算为周期就是 [FPGA 学习记录] 数码管动态显示,我们的计数器要完成 [FPGA 学习记录] 数码管动态显示[FPGA 学习记录] 数码管动态显示 的计数,[FPGA 学习记录] 数码管动态显示 换算成 [FPGA 学习记录] 数码管动态显示 就是 [FPGA 学习记录] 数码管动态显示,我们的计数器若想要完成 [FPGA 学习记录] 数码管动态显示 的计数,需要在频率为 [FPGA 学习记录] 数码管动态显示 的系统时钟下完成 [FPGA 学习记录] 数码管动态显示 个时钟周期的计数;因为我们的计数器是从 [FPGA 学习记录] 数码管动态显示 开始计数,所以说计数的最大值应该是 [FPGA 学习记录] 数码管动态显示cnt_100ms 计数的最大值就是 [FPGA 学习记录] 数码管动态显示。当 cnt_100ms 计数到最大值就让它归零,开始下一个周期的计数。

还需要一个变量,就是我们的 cnt_flag 信号,那么为什么要声明这个 cnt_flag 信号呢?我们需要使用这个 cnt_flag 信号做一个条件来控制我们输出的待显示数据让 data[19:0] 进行自加。那么有的朋友可能想到:我们也可以使用计数器,当 cnt_100ms 计数到最大值作为一个条件,控制我们输出的待显示数据 data[19:0] 让它进行自加,那么这样也是可以的;但是我们声明这个 cnt_flag 信号是为了让我们的约束条件更加的简洁清晰。首先,当复位信号有效时给 cnt_flag 赋一个初值 [FPGA 学习记录] 数码管动态显示,当 cnt_100ms 计数到最大值减一([FPGA 学习记录] 数码管动态显示)的时候将 cnt_flag 拉高一个时钟周期,其他时刻 cnt_flag 保持低电平。

输出信号有四路:第一路是待显示的数据 data[19:0]、第二路是小数点位 point[5:0]、第三路是符号位 sign、第四路是使能信号 seg_en

首先看一下待显示数据 data[19:0] 的波形。复位信号有效时给 data[19:0] 赋一个初值 20'd0,在第一个 [FPGA 学习记录] 数码管动态显示 显示时间内数码管显示数字 [FPGA 学习记录] 数码管动态显示,所以 data[19:0]cnt_flag 信号出现有效的高脉冲前一直保持初值;当 cnt_flag 信号出现有效的高脉冲时就表示 [FPGA 学习记录] 数码管动态显示 计数完成,然后我们输出的待显示数据 data[19:0] 要自加一,因为是时序逻辑,延迟了一个时钟周期是没有问题的;当我们的 cnt_flag 信号为有效的高脉冲 data[19:0] 自加一,自加到最大值 20'd999_999 归零,开始下一个周期的计数。

因为我们征途系列开发板使用的是六位八段数码管,每个数码位都有一个小数点位,所以说 point 它的位宽为六位宽 point[5:0],每一位表示每个数码位的小数点位。在这里我们定义为高电平使我们的小数点位有效,通过实验目标可知:我们的显示过程中并没有使用到小数点位,所以说让 point[5:0] 一直保持为无效的低电平。

我们的符号位同样是高电平有效,当我们的数码管进行负数显示的时候,在显示的数据之前会加一个负号用来区分正负数

fsdof74873wqerdoaisefueroit2_negative

在我们本次的显示当中是显示数字 [FPGA 学习记录] 数码管动态显示 到最大值 [FPGA 学习记录] 数码管动态显示 没有负号显示,所以说让它一直保持为无效的低电平。

最后一路输出信号:使能信号 seg_en。使能信号 seg_en 控制数码管的显示与否,当它为有效的高电平时,数码管可以进行正常的显示;当它为无效的低电平时,数码管就不能进行显示。在这个实验当中我们实现的是十进制的数字 [FPGA 学习记录] 数码管动态显示 到最大值 [FPGA 学习记录] 数码管动态显示 的循环计数,所以使能信号 seg_en 就要一直拉高,这样我们的数码管才能够正常的显示。首先复位期间先给赋一个初值低电平,然后当复位信号无效时,让它一直保持高电平。

数据生成模块整体的波形图绘制完成后,接下来就参照这个波形图进行代码的编写

cnt_100ms 计数的最大值是 [FPGA 学习记录] 数码管动态显示,换算成二进制就是 [FPGA 学习记录] 数码管动态显示,所以 cnt_100ms 的位宽是 23 位宽 cnt_100ms[22:0]

数据生成模块的代码编写完成后,我们保存为 data_gen.v

//模块开始 模块名称 端口列表
module data_gen
#(
    parameter   CNT_MAX = 23'd4_999_999,//计数 0.1s 计数最大值
    parameter   DATA_MAX= 20'd999_999   //待显示数据最大值
)
(
    input   wire        sys_clk     , //系统时钟,50MHz
    input   wire        sys_rst_n   , //系统复位,低电平有效
    
    output  reg  [19:0] data        , //待显示数据
    output  wire [5:0]  point       , //小数点
    output  wire        sign        , //负号
    output  reg         seg_en        //数码管动态显示模块工作使能
);

// 中间变量
reg [22:0]  cnt_100ms;  //100ms 计数器
reg         cnt_flag;   //100ms 计时时间到达标志

//变量赋值
always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        cnt_100ms <= 23'd0;
    else if (cnt_100ms == CNT_MAX)
        cnt_100ms <= 23'd0;
    else
        cnt_100ms <= cnt_100ms + 23'd1;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        cnt_flag <= 1'b0;
    else if (cnt_flag == (CNT_MAX-1))
        cnt_flag <= 1'b1;
    else
        cnt_flag <= 1'b0;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        data <= 20'd0;
    else if ((cnt_flag==1'b1) && (data==DATA_MAX))
        data <= 20'd0;
    else if (cnt_flag==1'b1)
        data <= data + 20'd1;
    else
        data <= data;

assign point = 6'b000_000;

assign sign = 1'b0;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        seg_en <= 1'b0;
    else
        seg_en <= 1'b1;

//模块结束
endmodule

文件体系

segment_595_dynamic
├─doc
│      segment_595_dynamic.vsdx
├─quartus_prj
├─rtl
│      data_gen.v
└─sim

然后建立一个新的实验工程,添加我们编写的代码,进行全编译;出现了报错信息,我们点击 OK

image-20231127203435064

看一下报错信息

Error (12007): Top-level design entity “top_segment_595” is undefined

提示我们不存在 top_segment_595 模块,这个问题在数码管的静态显示工程中我们也遇到过,在那个时候我们提到了这个问题有两种解决方式:第一种方式是重新编写一个顶层文件,将我们的子功能模块例化到顶层文件,然后进行编译;第二种方式是将我们的子功能模块强制置为顶层。之前我们使用的是第一种方式解决这个问题,在这儿我们使用第二种方式解决这个问题:选中 data_gen 子功能模块,点击鼠标右键,选择第三项 Set as Top-Level Entity 将它置为顶层

image-20231127204143555

然后再次进行编译,编译通过点击 OK

image-20231127204518207

接下来就需要编写我们的仿真文件

//时间参数
`timescale 1ns/1ns

//模块开始 模块名称 端口列表(空)
module tb_data_gen();

//变量声明
reg     sys_clk;
reg     sys_rst_n;

//声明变量将输出信号引出
wire [19:0] data  ;
wire [5:0]  point ;
wire        sign  ;
wire        seg_en;

//变量初始化
initial
    begin
        sys_clk = 1'b1;
        sys_rst_n <= 1'b0;
        #35
        sys_rst_n <= 1'b1;
    end

//产生频率为 50MHz 的系统时钟
always #10 sys_clk = ~sys_clk;

//模块的实例化
data_gen
#(
    .CNT_MAX (23'd49),//计数 0.1s 计数最大值
    .DATA_MAX(20'd9 ) //待显示数据最大值
)
data_gen_inst
(
    .sys_clk  (sys_clk  ), //系统时钟,50MHz
    .sys_rst_n(sys_rst_n), //系统复位,低电平有效
    
    .data     (data     ), //待显示数据
    .point    (point    ), //小数点
    .sign     (sign     ), //负号
    .seg_en   (seg_en   )  //数码管动态显示模块工作使能
);

//模块结束
endmodule

仿真模块编写完成后,保存为 tb_data_gen.v

文件体系

segment_595_dynamic
├─doc
│     segment_595_dynamic.vsdx
│
├─quartus_prj
│  │  top_segment_595.qpf
│  │  top_segment_595.qsf
│  │
│  ├─db
│  │      logic_util_heursitic.dat
│  │      ......
│  │      top_segment_595.vpr.ammdb
│  │
│  ├─incremental_db
│  │  │  README
│  │  │
│  │  └─compiled_partitions
│  │          top_segment_595.db_info
│  │          ......
│  │          top_segment_595.root_partition.map.kpt
│  │
│  ├─output_files
│  │      top_segment_595.asm.rpt
│  │      ......
│  │      top_segment_595.sta.summary
│  │
│  └─simulation
│      └─modelsim
│              top_segment_595.sft
│              ......
│              top_segment_595_v.sdo
│
├─rtl
│     data_gen.v
│
└─sim
      tb_data_gen.v

然后回到实验工程,添加我们的仿真模块;进行全编译;编译完成点击 OK

image-20231127210810514

然后进行仿真设置,开始仿真;仿真完成之后打开 sim 窗口添加模块波形;波形界面全选、分组、消除前缀;然后点击 Restart,将时间参数先设置为 10us,运行一次。

发现 cnt_flag 波形有问题

image-20231127212651253

查看 data_gen.v 中关于 cnt_flag 的赋值部分,发现第 33 行给 cnt_100ms 赋值高电平的条件编写错误,将 (cnt_flag == (CNT_MAX-1)) 修改成 (cnt_100ms == (CNT_MAX-1)) 并且保存

data_gen.v

//模块开始 模块名称 端口列表
module data_gen
#(
    parameter   CNT_MAX = 23'd4_999_999,//计数 0.1s 计数最大值
    parameter   DATA_MAX= 20'd999_999   //待显示数据最大值
)
(
    input   wire        sys_clk     , //系统时钟,50MHz
    input   wire        sys_rst_n   , //系统复位,低电平有效
    
    output  reg  [19:0] data        , //待显示数据
    output  wire [5:0]  point       , //小数点
    output  wire        sign        , //负号
    output  reg         seg_en        //数码管动态显示模块工作使能
);

// 中间变量
reg [22:0]  cnt_100ms;  //100ms 计数器
reg         cnt_flag;   //100ms 计时时间到达标志

//变量赋值
always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        cnt_100ms <= 23'd0;
    else if (cnt_100ms == CNT_MAX)
        cnt_100ms <= 23'd0;
    else
        cnt_100ms <= cnt_100ms + 23'd1;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        cnt_flag <= 1'b0;
    else if (cnt_100ms == (CNT_MAX-1))
        cnt_flag <= 1'b1;
    else
        cnt_flag <= 1'b0;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        data <= 20'd0;
    else if ((cnt_flag==1'b1) && (data==DATA_MAX))
        data <= 20'd0;
    else if (cnt_flag==1'b1)
        data <= data + 20'd1;
    else
        data <= data;

assign point = 6'b000_000;

assign sign = 1'b0;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        seg_en <= 1'b0;
    else
        seg_en <= 1'b1;

//模块结束
endmodule

重新编译 data_gen.v 模块,将时间参数设置为 12us 再次仿真,参照我们绘制的波形图,来看一下我们的仿真波形
首先是计数器
image-20231127215039803

下面看一下 cnt_flag 信号

image-20231127215644416

image-20231127220934056

image-20231127221516373

符号位始终为低电平,也没有问题

image-20231127221650742

然后是使能信号,初值为低电平,复位信号无效时一直保持高电平

image-20231127221825356

这三路信号与我们绘制的波形图是完全一致的,我们的数据生成模块 data_gen 通过了仿真验证。

我们已经实现了系统框图的绘制,完成了数据生成模块 data_gen 它的功能实现;接下来我们将继续生成我们的子功能模块。

首先先来看一下我们的系统框图

image-20231127162442251

由系统框图我们可以知道:顶层模块 top_segment_595 包含两个子功能模块,一个是已经实现了的数据生成模块 data_gen,另一个是动态显示模块 segment_595_dynamic;动态显示模块 segment_595_dynamic 又包含两个子功能模块,一个是动态显示驱动模块 segment_dynamic,另一个是 74HC595 控制模块 hc595_ctrl。74HC595 控制模块 hc595_ctrl 在上一小节数码管的静态显示当中已经完全实现了,这儿就可以直接调用。

接下来就需要实现动态显示驱动模块 segment_dynamic。在实现动态显示驱动模块 segment_dynamic 之前,我们这儿要补充一个知识点:BCD 码。在这里为什么要补充 BCD 码的相关知识呢?

动态显示驱动模块 segment_dynamic 它的作用是将传入的待显示的十进制数据 data[19:0] 转化为可以输出的位选信号 sel[5:0] 和段选信号 seg[7:0],传入的数据 data[19:0] 是由数据生成模块 data_gen 产生并传入的,data[19:0] 它是使用二进制表示的多位十进制数,这种编码方式并不能够直接用于产生位选信号 sel[5:0] 和段选信号 seg[7:0];我们需要将 data[19:0] 转化为以 BCD 码表示的十进制数,然后通过得到的 BCD 码表示的十进制数来产生位选信号 sel[5:0] 和段选信号 seg[7:0],这是为什么呢?在解答这个问题之前我们先来学习一下什么是 BCD 码

BCD 码的英文全称是 Binary-Coded Decimal,翻译为二进制编码的十进制,又称为二—十进制码,它使用 4 位二进制数来表示一位十进制数中的 0~9 这十个数码,是一种二进制的数字编码形式,是用二进制编码的十进制代码。简要概括一下就是:BCD 码它是使用 4 位二进制数来表示一位十进制数 0~9,一种编码形式。

BCD 码根据权值的有无可以分为有权码和无权码,“权”表示权值;有权码的 4 位二进制数的每一位都有一个固定的权值,而无权码没有权值;常见的有权码有 8421 码、5421 码和 2421 码,8421 码它的权值从左到右是 8、4、2、1
[FPGA 学习记录] 数码管动态显示
5421 码它的权值从左到右就是 5、4、2、1
[FPGA 学习记录] 数码管动态显示
2421 码它的权值从左到右就是 2、4、2、1
[FPGA 学习记录] 数码管动态显示
其中 8421 码是最为常用的 BCD 编码,也是本实验当中使用的编码形式;常用的无权码有余 3 码、余 3 循环码。

下面这张表格表示的就是几种常见的 BCD 编码方式所对应的十进制数 0~9 的编码格式

十进制数 0 1 2 3 4 5 6 7 8 9
8421 码 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001
5421 码 0000 0001 0010 0011 0100 1000 1001 1010 1011 1100
2421 码 0000 0001 0010 0011 0100 1011 1100 1101 1110 1111
余 3 码 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100
余 3 循环码 0010 0110 0111 0101 0100 1100 1101 1111 1110 1010

接下来我们讲解一下如何使用我们的 BCD 码来表示我们的十进制数,我们以 8421 码为例:比如说十进制数数字 7,它的 8421 码编码格式是 0111;那么我们怎么通过 0111 这个编码格式得到我们的十进制数数字 7 呢?将 BCD 码的每位二进制数与其对应的权值相乘,之后将所有的乘积相加,相加和就是 BCD 码所表示的十进制数;我们来计算一下
[FPGA 学习记录] 数码管动态显示
其他的有权码也是使用这种方式:将各位二进制数与对应位的权值相乘,乘积相加就可以得到有权码所表示的十进制数。

无权码的计算方式我们这儿就不再进行讲解了,感兴趣的朋友可以自行查阅相关资料。

了解了有权码的编码方式之后,我们来看下前面提出的问题:为什么数据生成模块 data_gen 生成的二进制编码的十进制数,并不能够直接用于产生位选信号 sel[5:0] 和段选信号 seg[7:0];而 BCD 码表示的十进制数可以直接用于产生位选信号 sel[5:0] 和段选信号 seg[7:0] 呢?

我们这儿举一个例子,比如说使用多位数码管来示一个十进制数 [FPGA 学习记录] 数码管动态显示,十进制数 [FPGA 学习记录] 数码管动态显示 使用二进制表示应该是 [FPGA 学习记录] 数码管动态显示

image-20231128180518756

十进制数 [FPGA 学习记录] 数码管动态显示 使用 8421 码表示应该是 [FPGA 学习记录] 数码管动态显示

知道了十进制数 [FPGA 学习记录] 数码管动态显示 的两种表示方式之后,接下来继续进行分析:如果使用动态扫描的方式在多位数码管上显示 [FPGA 学习记录] 数码管动态显示,就需要在第一个显示周期内点亮数码管的 DIG6 数码位(从左到右依次为DIG1,DIG2,DIG3,DIG4,DIG5,DIG6),让它显示个位 [FPGA 学习记录] 数码管动态显示

68w3eyiofsdnfdifiw3o4rmgdf_digit3

然后第二个显示周期内点亮 DIG5 数码位,让它显示十位 [FPGA 学习记录] 数码管动态显示

d8y948we7r5soidjfgdfg98w34kfs_sel1_digit3

在第三个显示周期内点亮 DIG4 数码位,让它显示百位 [FPGA 学习记录] 数码管动态显示

3496fvb76xdfowse4jt45tko_sel2_digit2

如果说扫描的频率足够快,加上人眼的视觉暂留特性以及数码管的余晖效应,我们的肉眼就以为数码管在同时显示 [FPGA 学习记录] 数码管动态显示

sel012_digit233_vbi76fworhwdfvisdhfwl

这样就实现了 [FPGA 学习记录] 数码管动态显示 的显示。这种显示方式就要求我们能够从传入的数据当中,直接提取出待显示数据的个位、十位和百位;而传入的使用二进制表示的十进制数,从其中并不能直接提取出个位、十位和百位;而 BCD 码它所表示的十进制数是可以直接进行个位、十位、百位信息的提取:最低 4 位 [FPGA 学习记录] 数码管动态显示 表示个位 [FPGA 学习记录] 数码管动态显示,中间 4 位 [FPGA 学习记录] 数码管动态显示 表示十位 [FPGA 学习记录] 数码管动态显示,最高 4 位 [FPGA 学习记录] 数码管动态显示 表示百位 [FPGA 学习记录] 数码管动态显示

这就是 BCD 码所表示的十进制数据能够直接用于产生位选和段选信号,而二进制表示的十进制数据并不能直接用于产生位选和段选信号的原因。

了解了这些之后,我们引入了一个新的问题:如何将二进制码转化为 BCD 码?接下来就通过一个例子来讲解一下转换的方法,在这里我们以三位十进制数 [FPGA 学习记录] 数码管动态显示 为例。

十进制数 [FPGA 学习记录] 数码管动态显示 的二进制编码形式是 [FPGA 学习记录] 数码管动态显示。实现转换的第一步:在输入的二进制码之前补上若干个 0;0 的个数规定为:[FPGA 学习记录] 数码管动态显示

参与转换的十进制数有 [FPGA 学习记录] 数码管动态显示 个十进制位,那么就需要 [FPGA 学习记录] 数码管动态显示 个 BCD 码;[FPGA 学习记录] 数码管动态显示 的十进制位分别是个位 [FPGA 学习记录] 数码管动态显示、十位 [FPGA 学习记录] 数码管动态显示、百位 [FPGA 学习记录] 数码管动态显示 总共三个位,所以 [FPGA 学习记录] 数码管动态显示 需要 [FPGA 学习记录] 数码管动态显示 个 BCD 码;每个 BCD 码是 4 个位宽,[FPGA 学习记录] 数码管动态显示 所以 [FPGA 学习记录] 数码管动态显示 前面就需要补 [FPGA 学习记录] 数码管动态显示0 得到 [FPGA 学习记录] 数码管动态显示

如果是其他数值的多位十进制数,比如说 [FPGA 学习记录] 数码管动态显示 它的十进制位个数是 [FPGA 学习记录] 数码管动态显示,每个 BCD 码需要 [FPGA 学习记录] 数码管动态显示 位二进制数,所以 [FPGA 学习记录] 数码管动态显示 前面就要补 [FPGA 学习记录] 数码管动态显示0 得到 [FPGA 学习记录] 数码管动态显示
在这里我们就完成了补 0 的操作,得到了一组新的数据 [FPGA 学习记录] 数码管动态显示,接下来就要对这组数据进行判断运算和移位操作。

首先判断 BCD 码部分,判断每一个 BCD 码所表示的十进制数是否大于等于 5,如果说每一个 BCD 码所表示的十进制数大于等于 5 就将它与 3 相加;如果说每一个 BCD 码所表示的十进制数小于 5 就让它保持原值不变。不论每一个 BCD 码所表示的十进制数大于等于 5 或者小于 5,完成判断运算之后都要向左移一个二进制位。

按照上述两条规则,得到下面的表格(上例十进制数 [FPGA 学习记录] 数码管动态显示 的二进制编码形式 [FPGA 学习记录] 数码管动态显示 转换成 8421BCD 码过程)

执行的操作 [FPGA 学习记录] 数码管动态显示 [FPGA 学习记录] 数码管动态显示 [FPGA 学习记录] 数码管动态显示 待转换数据(二进制数)
[FPGA 学习记录] 数码管动态显示 个 0 0000 0000 0000 1110_1001
各个 BCD 码分别和 5 比较,保持原值不变 0000 0000 0000 1110_1001
整体向左移一个二进制位,第 1 次按位左移 0000 0000 0001 110_1001
各个 BCD 码分别和 5 比较,保持原值不变 0000 0000 0001 110_1001
整体向左移一个二进制位,第 2 次按位左移 0000 0000 0011 10_1001
各个 BCD 码分别和 5 比较,保持原值不变 0000 0000 0011 10_1001
整体向左移一个二进制位,第 3 次按位左移 0000 0000 0111 0_1001
各个 BCD 码分别和 5 比较,[FPGA 学习记录] 数码管动态显示 位加 3 0000 0000 1010 0_1001
整体向左移一个二进制位,第 4 次按位左移 0000 0001 0100 1001
各个 BCD 码分别和 5 比较,保持原值不变 0000 0001 0100 1001
整体向左移一个二进制位,第 5 次按位左移 0000 0010 1001 001
各个 BCD 码分别和 5 比较,[FPGA 学习记录] 数码管动态显示 位加 3 0000 0010 1100 001
整体向左移一个二进制位,第 6 次按位左移 0000 0101 1000 01
各个 BCD 码分别和 5 比较,[FPGA 学习记录] 数码管动态显示 位加 3 0000 1000 1011 01
整体向左移一个二进制位,第 7 次按位左移 0001 0001 0110 1
各个 BCD 码分别和 5 比较,[FPGA 学习记录] 数码管动态显示 位加 3 0001 0001 1001 1
整体向左移一个二进制位,第 8 次按位左移 0010 0011 0011
输出结果 [FPGA 学习记录] 数码管动态显示 [FPGA 学习记录] 数码管动态显示 [FPGA 学习记录] 数码管动态显示

首先是完成补 0 操作得到的第一组数据,它的 BCD 码最高位表示的是 0、次高位也是 0、最低位也是 0,判断后它们都小于 5,保持原值不变;

经过判断运算之后,整体向左移一位得到了第二组数据;

完成移位操作之后继续进行判断运算,最高位同样是 0、次高位也是 0、最低位是 1,都小于5,继续保持原不变;

然后完成判断运算,第 2 次进行按位左移操作就得到了第四组数据;

继续判断运算,最高位同样是 0、次高位也是 0、最低位是 3,3 个 8421 码都小于 5,保持原值不变;

然后第 3 次按位左移就得到第六组数据;

判断运算,最高位同样是 0、次高位也是 0、最低位是 7,最低位的 7 大于 5,最低位加上一个 3 等于 1010,最高、次高位保持原值不变,得到一组新的数据;

然后第 4 次进行按位左移就得到了第八组数据,最高位是 0、次高位是 1、最低位是 4,都小于5,继续保持原不变;

第 5 次进行移位得到第十组数据,最高位是 0、次高位是 2、最低位是 9,最低位的 9 大于 5,最低位加上一个 3 等于 1100,最高、次高位保持原值不变,得到一组新的数据;

……

……

最后完成了 8 次的判断运算和移位操作得到十进制数 [FPGA 学习记录] 数码管动态显示 它的 BCD 码编码形式,将每个 BCD 码换算成十进制数就是 [FPGA 学习记录] 数码管动态显示。这里需要注意:移位次数 8 等于待转换数据的二进制位数即位宽, [FPGA 学习记录] 数码管动态显示 的位宽是 8,上例就移位了 8 次;如果说输入的是 [FPGA 学习记录] 数码管动态显示 对应的二进制码 [FPGA 学习记录] 数码管动态显示,它的位宽是 20 位宽就需要移位 20 次。

通过上述的方式,我们就可以将十进制数的二进制码转换为十进制数的 BCD 码。

以上部分就是本小节将会涉及到的 BCD 码的相关知识的一个讲解。

了解了 BCD 码的相关知识之后,我们需要建立一个新的子功能模块用来实现 BCD 码的转码

image-20231129222017563

输入信号

  • sys_clk:系统时钟信号
  • sys_rst_n:系统复位信号
  • data[19:0]:十进制数的二进制编码

实验目标当中六位八段数码管显示的最大值是 [FPGA 学习记录] 数码管动态显示,六位十进制数就需要使用 6 个 BCD 码表示,在这里我们将每个 BCD 码都单独进行输出,输出信号就应该是六路 BCD 码

输出信号

  • unit[3:0]:个位 BCD 码
  • ten[3:0]:十位 BCD 码
  • hun[3:0]:百位 BCD 码。hun 是 hundred 的简写
  • tho[3:0]:千位 BCD 码。tho 是 thousand 的简写
  • t_tho[3:0]:万位 BCD 码。t_tho 是 ten thousand 的简写
  • h_tho[3:0]:十万位 BCD 码。h_tho 是 one hundred thousand 的简写

由于我们加入了新的子功能模块 bcd_8421,包含 bcd_8421 的模块也要做一下修改

修改后的 segment_dynamic 模块框图

image-20231129234951681

修改后的 segment_595_dynamic 模块框图

image-20231129235108516

修改后的 top_segment_595 模块框图

image-20231130094756670

接下来就开始 BCD 转码模块 bcd_8421 的波形图的绘制

image-20231130134008155

首先是输入信号时钟信号 sys_clk、复位信号 sys_rst_n 以及二进制编码形式的十进制数 data[19:0],输入的十进制数据我们定义成一个固定值 20'd114514,方便后面波形的绘制。输入信号的波形绘制完成之后,我们需要分析一下接下来该如何绘制。我们回看转码过程表格

执行的操作 [FPGA 学习记录] 数码管动态显示 [FPGA 学习记录] 数码管动态显示 [FPGA 学习记录] 数码管动态显示 待转换数据(二进制数)
[FPGA 学习记录] 数码管动态显示 个 0 0000 0000 0000 1110_1001
各个 BCD 码分别和 5 比较,保持原值不变 0000 0000 0000 1110_1001
整体向左移一个二进制位,第 1 次按位左移 0000 0000 0001 110_1001
各个 BCD 码分别和 5 比较,保持原值不变 0000 0000 0001 110_1001
整体向左移一个二进制位,第 2 次按位左移 0000 0000 0011 10_1001
各个 BCD 码分别和 5 比较,保持原值不变 0000 0000 0011 10_1001
整体向左移一个二进制位,第 3 次按位左移 0000 0000 0111 0_1001
各个 BCD 码分别和 5 比较,[FPGA 学习记录] 数码管动态显示 位加 3 0000 0000 1010 0_1001
整体向左移一个二进制位,第 4 次按位左移 0000 0001 0100 1001
各个 BCD 码分别和 5 比较,保持原值不变 0000 0001 0100 1001
整体向左移一个二进制位,第 5 次按位左移 0000 0010 1001 001
各个 BCD 码分别和 5 比较,[FPGA 学习记录] 数码管动态显示 位加 3 0000 0010 1100 001
整体向左移一个二进制位,第 6 次按位左移 0000 0101 1000 01
各个 BCD 码分别和 5 比较,[FPGA 学习记录] 数码管动态显示 位加 3 0000 1000 1011 01
整体向左移一个二进制位,第 7 次按位左移 0001 0001 0110 1
各个 BCD 码分别和 5 比较,[FPGA 学习记录] 数码管动态显示 位加 3 0001 0001 1001 1
整体向左移一个二进制位,第 8 次按位左移 0010 0011 0011
输出结果 [FPGA 学习记录] 数码管动态显示 [FPGA 学习记录] 数码管动态显示 [FPGA 学习记录] 数码管动态显示

这张表格展示了二进制向 BCD 编码转换的一个完整过程,可以帮助我们进行波形图的绘制。首先我们知道,二进制向 BCD 码转码需要通过判断运算加移位运算实现,并且运算次数规定为和输入的二进制码的位宽相同,比如说,我们输入的十进制数是 [FPGA 学习记录] 数码管动态显示,二进制编码形式是 20'd114514 20 位宽就需要进行 20 次的判断运算加移位操作才能够实现二进制向 BCD 码的一个转换,所以说我们需要一个移位计数器 cnt_shift[4:0] 对判断运算和移位操作的次数进行计数;其次在进行判断计算和移位操作的过程中产生的中间数据(24bitBCD码加上20bit二进制码)需要使用一个变量 data_shift[43:0] 储存。需要注意的是,二进制到 BCD 码转换的过程中,判断运算与移位操作各自在一个时钟周期内完成,而且这两个过程有先后顺序:判断运算在前,移位操作在后;所以声明移位标志信号 shift_flag 区分这两个操作步骤;并且移位次数的更新都是在判断计算和移位操作之后完成的,所以移位标志信号 shift_flag 也可以作为移位次数计数器 cnt_shift[4:0] 计数的一个条件。

完成了变量的声明之后,接下来开始变量信号波形的绘制。首先是移位计数器 cnt_shift[4:0],当复位信号 sys_rst_n 有效时给它赋一个初值 0,移位计数器从 0 开始计数,因为移位次数是 20 次,cnt_shift[4:0] 计数 20 次时最大值应该是 19,但是我们这里还需要增加两个计数状态,cnt_shift[4:0] 一共计数 22 次,为什么呢?根据上面的表格可知,移位计数器 cnt_shift[4:0] 为 0 时,实际上是对应二进制的补零操作,当移位计数器 cnt_shift[4:0] 计数范围为 1~20 时才是进行判断运算和移位操作的时候,当移位计数器 cnt_shift[4:0] 计数到最大值 21 时对应的操作是输出结果,提取转换完成的 BCD 码。移位计数器 cnt_shift[4:0] 计数范围确定之后,我们要考虑一下移位计数器 cnt_shift[4:0] 的计数条件是什么?我们前面已经提到,可以使用移位标志信号 shift_flag 作为移位计数器 cnt_shift[4:0] 的计数条件,所以说我们首先绘制移位标志信号 shift_flag 的波形。

当复位信号有效时给移位标志信号 shift_flag 赋一个初值 0,移位标志信号 shift_flag 的作用是区分判断运算与移位操作,这两个阶段各自在一个时钟周期内完成,那么当复位信号无效时在每个系统时钟周期下对移位标志信号 shift_flag 进行不断的取反。当移位标志信号 shift_flag 为低电平时进行判断运算,当移位标志信号 shift_flag 为高电平时进行移位操作,移位标志信号 shift_flag 为高电平也作为条件控制移位计数器 cnt_shift[4:0] 进行计数;移位计数器 cnt_shift[4:0] 计数到最大值 21 并且移位标志信号 shift_flag 为高电平,移位计数器 cnt_shift[4:0] 归零开始下一个周期的计数。当移位标志信号 shift_flag 为高电平时移位计数器 cnt_shift[4:0] 进行加一计数,当移位标志信号 shift_flag 为低电平时移位计数器 cnt_shift[4:0] 保持原来的值不变,因为是时序逻辑,所以说延迟一个时钟周期;当移位计数器 cnt_shift[4:0] 计数到最大值 21 而且移位标志信号 shift_flag 为高电平,移位计数器 cnt_shift[4:0] 归零开始下一个周期的计数。

下面开始中间变量移位数据 data_shift[43:0] 的波形绘制。当复位信号有效时给它赋一个初值 0,当复位信号无效时并且移位计数器 cnt_shift[4:0] 为 0 时,将输入的二进制数 20'd114514 更新到移位数据的 [19:0] 位,就是给 data[19:0] 前面补 0:{24'b0,data[19:0]};当移位计数器 cnt_shift[4:0] 的计数范围为 1~20 时,如果移位标志信号 shift_flag 为低电平就进行判断运算的操作,如果说移位标志信号 shift_flag 为高电平就进行移位的操作;当移位计数器 cnt_shift[4:0] 的计数值为 21 时,移位数据 data_shift[43:0] 保持原来的值不变,移位标志信号 shift_flag 为高电平移位计数器 cnt_shift[4:0] 归零,移位数据 data_shift[43:0] 也要进行重新的更新,开始下一次的转码。

接下来开始输出信号波形的绘制。当复位信号有效时给输出信号赋一个初值 0;当移位计数器 cnt_shift[4:0] 的计数值为 21 时就提取输出信号对应的 BCD 码,输出信号个位 unit[3:0] 所对应的 BCD 码就应该是 data_shift[23:20],输出信号十位 ten[3:0] 对应的 BCD 码就应该是 data_shift[27:24],百位 hun[3:0] 对应的是 data_shift[31:28],千位 tho[3:0] 对应的是 data_shift[35:32],万位 t_tho[3:0] 对应的应该是 data_shift[39:36],十万位 h_tho[3:0] 对应的就应该是 data_shift[43:40]

完成了 BCD 编码模块 的波形图绘制后,接下来可以根据这个波形图进行代码的编写,保存为 bcd_8421.v

//模块开始 模块名称 端口列表
module bcd_8421
(
    input   wire        sys_clk     , //系统时钟,50MHz
    input   wire        sys_rst_n   , //系统复位,低电平有效
    input   wire [19:0] data        , //待转码十进制数的二进制编码
    
    output  reg  [3:0]  unit        , //转码结果的个位 BCD 码
    output  reg  [3:0]  ten         , //转码结果的十位 BCD 码
    output  reg  [3:0]  hun         , //转码结果的百位 BCD 码
    output  reg  [3:0]  tho         , //转码结果的千位 BCD 码
    output  reg  [3:0]  t_tho       , //转码结果的万位 BCD 码
    output  reg  [3:0]  h_tho         //转码结果的十万位 BCD 码
);

//变量声明
reg         shift_flag  ;
reg [4:0]   cnt_shift   ;
reg [43:0]  data_shift  ;

//变量赋值
always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        shift_flag <= 1'b0;
    else
        shift_flag <= ~shift_flag;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        cnt_shift <= 5'd0;
    else if ((cnt_shift==5'd21) && (shift_flag==1'b1))
        cnt_shift <= 5'd0;
    else if (shift_flag == 1'b1)
        cnt_shift <= cnt_shift + 5'd1;
    else
        cnt_shift <= cnt_shift;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        data_shift <= 44'd0;
    else if (cnt_shift==5'd0)
        data_shift <= {24'b0,data};
    else if ((shift_flag==1'b0) && ((cnt_shift>=5'd1)&&(cnt_shift<=5'd20)))
        begin
            data_shift[23:20] <= (data_shift[23:20]>=4'd5)? (data_shift[23:20]+4'd3): (data_shift[23:20]);
            data_shift[27:24] <= (data_shift[27:24]>=4'd5)? (data_shift[27:24]+4'd3): (data_shift[27:24]);
            data_shift[31:28] <= (data_shift[31:28]>=4'd5)? (data_shift[31:28]+4'd3): (data_shift[31:28]);
            data_shift[35:32] <= (data_shift[35:32]>=4'd5)? (data_shift[35:32]+4'd3): (data_shift[35:32]);
            data_shift[39:36] <= (data_shift[39:36]>=4'd5)? (data_shift[39:36]+4'd3): (data_shift[39:36]);
            data_shift[43:40] <= (data_shift[43:40]>=4'd5)? (data_shift[43:40]+4'd3): (data_shift[43:40]);
        end
    else if ((shift_flag==1'b1) && ((cnt_shift>=5'd1)&&(cnt_shift<=5'd20)))
        data_shift <= data_shift<<1;
    else
        data_shift <= data_shift;

//输出信号的赋值
always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        begin
            unit  <= 4'b0;
            ten   <= 4'b0;
            hun   <= 4'b0;
            tho   <= 4'b0;
            t_tho <= 4'b0;
            h_tho <= 4'b0;
        end
    else if (cnt_shift==5'd21)
        begin
            unit  <= data_shift[23:20];
            ten   <= data_shift[27:24];
            hun   <= data_shift[31:28];
            tho   <= data_shift[35:32];
            t_tho <= data_shift[39:36];
            h_tho <= data_shift[43:40];
        end

//模块结束
endmodule

BCD 编码模块的代码编写完成后,回到实验工程添加 bcd_8421.v 模块,然后进行全编译查找语法错误;编译完成点击 OK

image-20231130140932156

下面开始编写我们的仿真代码,保存为 tb_bcd_8421.v

// 时间参数
`timescale 1ns/1ns
`include "../rtl/bcd_8421.v"

module tb_bcd_8421();

reg         sys_clk     ;
reg         sys_rst_n   ;
reg [19:0]  data        ;

wire [3:0]  unit ;
wire [3:0]  ten  ;
wire [3:0]  hun  ;
wire [3:0]  tho  ;
wire [3:0]  t_tho;
wire [3:0]  h_tho;

initial
    begin
        sys_clk = 1'b1;
        sys_rst_n <= 1'b0;
        #35
        sys_rst_n <= 1'b1;
        data <= 20'd114_514;
        #1000
        data <= 20'd358_946;
        #2000
        data <= 20'd716_230;
        #3000
        data <= 20'd999_999;
    end

always #10 sys_clk = ~sys_clk;

bcd_8421 bcd_8421_inst
(
    .sys_clk  (sys_clk  ), //系统时钟,50MHz
    .sys_rst_n(sys_rst_n), //系统复位,低电平有效
    .data     (data     ), //待转码十进制数的二进制编码
    
    .unit     (unit ), //转码结果的个位 BCD 码
    .ten      (ten  ), //转码结果的十位 BCD 码
    .hun      (hun  ), //转码结果的百位 BCD 码
    .tho      (tho  ), //转码结果的千位 BCD 码
    .t_tho    (t_tho), //转码结果的万位 BCD 码
    .h_tho    (h_tho)  //转码结果的十万位 BCD 码
);

endmodule

仿真模块编写完成后,回到实验工程,然后添加 tb_bcd_8421.v 模块;仿真模块添加完成之后进行全编译,编译通过点击 OK

image-20231130143335348

然后进行仿真设置

image-20231130143623589

下面开始仿真,结合我们绘制的波形图查看一下仿真波形图。

首先是移位标志信号 shift_flag

image-20231201102314897

移位标志信号 shift_flag 的初值是低电平,每个系统时钟周期进行取反。

接下来看一下移位计数器 cnt_shift

image-20231201103214831

移位数据 data_shift 的波形就不再进行查看了,因为数据量太大了。

接下来看一下输出信号

image-20231201104916854

那么这样我们就实现了 BCD 编码模块的功能,下面就可以实现动态显示驱动模块 segment_dynamic

动态显示驱动模块的模块框图之前已经绘制完成了

image-20231129234951681

接下来就绘制 segment_dynamic 模块的波形图

image-20231201190637933

输入信号有六路:时钟信号 sys_clk、复位信号 sys_rst_n、待显示数据 data[19:0]、小数点位 point[5:0]、符号位 sign 和使能信号 seg_en。待显示数据 data[19:0] 我们赋给它一个固定的值 20'd9876,这个数据是随机定义的,大家可以自己设置。小数点位 point[5:0] 给它赋一个固定值:6'b000_010,表示我们选中倒数第二个小数点位;通过 data[19:0]point[5:0] 这两路信号可以得到我们想要显示的数据应该是 [FPGA 学习记录] 数码管动态显示
符号位 sign 首先给它一个初值 0,然后当复位信号无效时将它拉高并且让它一直保持高电平,这就表示我们想要显示的是负数;通过 data[19:0]point[5:0]sign 这三路信号可以知道我们想要显示的数据应该是 [FPGA 学习记录] 数码管动态显示
使能信号 seg_en 在复位信号有效期间赋给它一个初值 0,当复位信号无效时让它一直保持高电平,这就表示数码管一直处于显示状态。

六路输入信号的波形绘制完成,接下来应该怎么绘制呢?
首先我们要声明六路信号 unit[3:0]ten[3:0]hun[3:0]tho[3:0]t_tho[3:0]h_tho[3:0] 将 BCD 编码模块 bcd_8421 输出的 BCD 编码引出来。

首先赋给它们一个初值 0,输入的十进制数据 data[19:0] 等于 20'd9876,(最低位)个位 unit[3:0] 应该是数字 6,然后十位 ten[3:0] 应该是 7,然后百位 hun[3:0] 应该是 8,千位 tho[3:0] 就应该是 9,万位 t_tho[3:0] 和十万位 h_tho[3:0] 就是 0。这儿有一点要注意:BCD 编码模块完成二进制到 BCD 的转码需要一段时间(22 个系统时钟周期),在波形图上使用省略号形状表示一段时间的延时。

六路 BCD 编码信号已经全部引出来后,我们再声明一个变量 data_reg[23:0] 对待显示数据 data[19:0] 进行寄存(只是对符号位和数据位进行寄存,不包含小数点位)。首先赋给它一个初值 0,然后使用数据位和符号位给它赋值,数据位就是 unit[3:0]ten[3:0]hun[3:0]tho[3:0]t_tho[3:0]h_tho[3:0] 这六路 BCD 编码信号,首先 data_reg[23:0] 最低 4 位是个位 unit[3:0] 就是 6、然后是十位 7、然后是百位 8、然后是千位 9,因为符号位 sign 为高电平需要显示负号,data_reg[23:0] 最高 4 位不显示用 X 来表示。

接下来还要声明一个计数器变量 cnt_1ms[15:0],为什么要声明这个计数器变量呢?因为之前已经约定好了,在数码管的动态扫描显示过程中,每个数码位只显示 1ms 的时长;对 1ms 进行计数就需要计数器,所以说我们要声明这一个计数器变量 cnt_1ms[15:0]

首先赋给计数器 cnt_1ms[15:0] 一个初值 0,[FPGA 学习记录] 数码管动态显示 这就表示要完成 [FPGA 学习记录] 数码管动态显示 的计数,要计数 [FPGA 学习记录] 数码管动态显示 个系统时钟周期,因为计数器从 0 开始计数,这就表示计数的最大值应该是 [FPGA 学习记录] 数码管动态显示,声明计数器变量时为什么它的位宽是 16 呢?我们来算一下:

image-20231201170328856

可以看到,[FPGA 学习记录] 数码管动态显示 的二进制形式为 [FPGA 学习记录] 数码管动态显示 总共 16 位,所以它的位宽应该是 16 位宽 [15:0]。当 1ms 计数器计数到最大值归零,开始下一个周期的计数。

1ms 计数器的波形绘制完成之后,我们还要声明一个变量 flag_1ms 就是 1ms 标志信号,为什么要声明一个标志信号呢?这个标志信号可以作为一个条件,控制数码位的选择
首先赋给 flag_1ms 一个初值 0,当计数器 cnt_1ms[15:0] 计数到最大值减一的时候,让它保持一个系统时钟周期的高脉冲,其他时刻让它保持低电平。

1ms 标志信号的波形绘制完成后,我们还要声明一个计数器变量 cnt_sel[2:0],为什么还要声明一个计数器变量呢?数码管的动态显示使用的是动态扫描的方式,只有每个数码位都完成 1ms 的显示,才是一个完整的扫描周期,为了对这个扫描周期进行计数,所以说我们要声明一个计数器变量;我们的开发板使用的是六位八段数码管,就表示一个扫描周期就是六个数码位分别进行显示,扫描计数器变量它的计数最大值就应该是 6,因为是从 0 开始计数 0~6 计数了七次(加消隐)。

首先赋给 cnt_sel[2:0] 一个初值 0,当 flag_1ms 信号为高电平时就表示有一个数码位完成了 1ms 的显示,扫描计数器就加一;当扫描计数器计数到最大值 6 而且 flag_1ms 信号为高电平时将 cnt_sel[2:0] 归零,开始下一个周期的计数。

接下来绘制输出位选信号 sel_reg[5:0] 的波形。首先赋给它一个初值 6'b111_111,这表示选中所有数码位进行消隐,当扫描计数器 cnt_sel[2:0] 为 0、标志信号 flag_1ms 为高电平时给它赋值 6'b000_001 表示选中第一个数码位,就是个位;当扫描计数器 cnt_sel[2:0] 为 1、标志信号 flag_1ms 为高电平时将位选信号 sel_reg[5:0] 向左移一位就得到了 6'b000_010,这表示选中了第二个数码位;当扫描计数器 cnt_sel[2:0] 为 2、标志信号 flag_1ms 为高电平时继续左移,位选信号 sel_reg[5:0] 的值就应该是 6'b000_100,表示选中了第三个数码位;……;当扫描计数器 cnt_sel[2:0] 的计数值再次为 1、标志信号 flag_1ms 为高电平时就对位选信号 sel_reg[5:0] 重新赋值,为什么要重新赋值呢?因为此时再进行移位的话 sel_reg[5:0] 是消隐时的 6'b111_111 表示所有的数码位都选中,所以说在这儿要给它赋一个新的值 6'b000_001 这就表示选中第一个数码位,然后继续进行移位操作。

在开始给段选信号 seg[7:0] 赋值之前,我们还需要声明两个变量 data_disp[3:0]dot_disp

data_disp[3:0] 这个变量表示即将要显示的数据。首先给它赋一个初值 0,当选中第一个数码位的时候让它进行显示,显示的内容应该是 unit[3:0] 对应的数字 6;当选中第二个数码位,让它显示 7;当选中第三个数码位让它显示的应该是 8;然后是第四个数码位就是 9;当显示第五个数码位要显示负号,负号在这儿用 10 表示,你也可以选择除了 0~9 之外的其他数值来表示;最高位是不显示的,用 11 表示。然后完成了一个周期的扫描,消隐后又回到了最低位让它显示数字 6;第二位让它显示 7。

dot_disp 表示即将显示的小数点位,它的初值应该是高电平(共阳极数码管),表示小数点位不显示,什么时候显示小数点位呢?由 point[5:0] 的值 6'b000_010 可知在第二位的时候显示小数点位,所以说应该是在 cnt_sel[2:0] 等于 2 期间让它保持低电平,表示在第二个数码位显示小数点;然后其他时刻保持高电平,表示不显示小数点位。

输出的段选信号 seg[7:0] 因为我们使用的是共阳数码管,所以说初值赋给它 8'hFF 就表示不点亮进行消隐;其他时刻,段选信号 seg[7:0] 要根据待显示数据 data[19:0] 进行一个编码,因为这儿直接输出的数据是不能够用于显示的,必须进行编码;因为是使用的时序逻辑,所以说应该延迟一个时钟周期;第一个数码位显示的是数字 6,我们来看一下数字 6 的编码

待显示内容 段码(二进制格式) 段码(十六进制格式)
dp g f e d c b a
0 1 1 0 0 0 0 0 0 8’hC0
1 1 1 1 1 1 0 0 1 8’hF9
2 1 0 1 0 0 1 0 0 8’hA4
3 1 0 1 1 0 0 0 0 8’hB0
4 1 0 0 1 1 0 0 1 8’h99
5 1 0 0 1 0 0 1 0 8’h92
6 1 0 0 0 0 0 1 0 8’h82
7 1 1 1 1 1 0 0 0 8’hF8
8 1 0 0 0 0 0 0 0 8’h80
9 1 0 0 1 0 0 0 0 8’h90
A 1 0 0 0 1 0 0 0 8’h88
b 1 0 0 0 0 0 1 1 8’h83
C 1 1 0 0 0 1 1 0 8’hC6
d 1 0 1 0 0 0 0 1 8’hA1
E 1 0 0 0 0 1 1 0 8’h86
F 1 0 0 0 1 1 1 0 8’h8E

数字 6 所对应的十六进制格式是 8'h82;然后是第二个数码位,第二个数码位对应的数字是 7,我们看一下 7,字符 7 对应的应该是 8'hF8,但是有一点要注意:小数点位要点亮,所以说 dp 应该是 0,这样看就应该是 8'h78 而不是 8'hF8;然后是数字 8,数字 8 它的段码是 8'h80;然后是数字 9,数字 9 的段码是 8'h90;然后是用 10 表示的负号,负号应该怎么显示呢?负号只需要点亮段选信号 g 就可以了,也就是说其他段都为高电平, g 段为低电平,这样就应该是 8'hBF;到了 11,11 就表示这个数码位不进行显示也就是说不点亮,就是 8'hFF。经过一个周期的扫描,完成了数据的显示;接着又进行消隐,然后回到了第一个数码位就是 8'h82

段选信号的波形绘制完成。这样我们就完成了动态显示驱动模块 segment_dynamic 整个模块的波形图的绘制。

接下来就开始代码的编写

//模块开始 模块名称 端口列表
module segment_dynamic
#(
    parameter CNT_MAX = 16'd49_999//1ms 计数器计数最大值
)
(
    input   wire        sys_clk     , //系统时钟,50MHz
    input   wire        sys_rst_n   , //系统复位,低电平有效
    input   wire [19:0] data        , //待转码十进制数的二进制编码
    input   wire [5:0]  point       , //小数点
    input   wire        sign        , //符号(负号是否显示)
    input   wire        seg_en      , //数码管显示使能
    
    output  reg  [7:0]  seg         , //段选
    output  reg  [5:0]  sel           //位选
);

wire [3:0]  unit ; //转码结果的个位 BCD 码
wire [3:0]  ten  ; //转码结果的十位 BCD 码
wire [3:0]  hun  ; //转码结果的百位 BCD 码
wire [3:0]  tho  ; //转码结果的千位 BCD 码
wire [3:0]  t_tho; //转码结果的万位 BCD 码
wire [3:0]  h_tho; //转码结果的十万位 BCD 码

reg  [23:0] data_reg;  //寄存待显示数据的 BCD 码
reg  [15:0] cnt_1ms;   //1ms 计数器
reg         flag_1ms;  //1ms 计数时间到达
reg  [2:0]  cnt_sel;   //位选计数
reg  [5:0]  sel_reg;   //寄存位选值
reg  [3:0]  data_disp; //将要显示的 BCD 码
reg         dot_disp;  //将要显示的小数点

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        data_reg <= 24'b0;
    //如果待显示十进制数的十万位不等于 0 或者需要显示小数点,十进制数则显示在 6 个数码位上
    else if ((h_tho!=4'b0000) || (point[5]==1'b1))
        data_reg <= {h_tho, t_tho, tho, hun, ten, unit};
    //如果待显示十进制数是负数且它的万位不等于 0 或者需要显示小数点,则十进制数的绝对值显示在 5 个数码位上
    //例如:待显示的十进制数为 -11451,数码管应该显示 -11451
    else if ((sign==1'b1) && ((t_tho!=4'b0000)||(point[4]==1'b1)))
        data_reg <= {4'd10, t_tho, tho, hun, ten, unit};//4'd10我们定义为显示负号
    //如果待显示十进制数是正数且它的万位不等于 0 或者需要显示小数点,则十进制数显示在 5 个数码位上
    else if ((sign==1'b0) && ((t_tho!=4'b0000)||(point[4]==1'b1)))
        data_reg <= {4'd11, t_tho, tho, hun, ten, unit};//4'd11我们定义为不显示
    //如果待显示十进制数是负数且它的千位不等于 0 或者需要显示小数点,则十进制数的绝对值显示在 4 个数码位上
    else if ((sign==1'b1) && ((tho!=4'b0000)||(point[3]==1'b1)))
        data_reg <= {4'd11, 4'd10, tho, hun, ten, unit};
    //如果待显示十进制数是正数且它的千位不等于 0 或者需要显示小数点,则十进制数显示在 4 个数码位上
    else if ((sign==1'b0) && ((tho!=4'b0000)||(point[3]==1'b1)))
        data_reg <= {4'd11, 4'd11, tho, hun, ten, unit};
    //如果待显示十进制数是负数且它的百位不等于 0 或者需要显示小数点,则十进制数的绝对值显示在 3 个数码位上
    else if ((sign==1'b1) && ((hun!=4'b0000)||(point[2]==1'b1)))
        data_reg <= {4'd11, 4'd11, 4'd10, hun, ten, unit};
    //如果待显示十进制数是正数且它的百位不等于 0 或者需要显示小数点,则十进制数显示在 3 个数码位上
    else if ((sign==1'b0) && ((hun!=4'b0000)||(point[2]==1'b1)))
        data_reg <= {4'd11, 4'd11, 4'd11, hun, ten, unit};
    //如果待显示十进制数是负数且它的十位不等于 0 或者需要显示小数点,则十进制数的绝对值显示在 2 个数码位上
    else if ((sign==1'b1) && ((ten!=4'b0000)||(point[1]==1'b1)))
        data_reg <= {4'd11, 4'd11, 4'd11, 4'd10, ten, unit};
    //如果待显示十进制数是正数且它的十位不等于 0 或者需要显示小数点,则十进制数显示在 2 个数码位上
    else if ((sign==1'b0) && ((ten!=4'b0000)||(point[1]==1'b1)))
        data_reg <= {4'd11, 4'd11, 4'd11, 4'd11, ten, unit};
    //如果待显示十进制数是负数且它的个位不等于 0 或者需要显示小数点,则十进制数的绝对值显示在 1 个数码位上
    else if ((sign==1'b1) && ((unit!=4'b0000)||(point[0]==1'b1)))
        data_reg <= {4'd11, 4'd11, 4'd11, 4'd11, 4'd10, unit};
    //其它情况就只显示在 1 个数码位上
    else
        data_reg <= {4'd11, 4'd11, 4'd11, 4'd11, 4'd11, unit};

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        cnt_1ms <= 16'd0;
    else if (cnt_1ms == CNT_MAX)
        cnt_1ms <= 16'd0;
    else
        cnt_1ms <= cnt_1ms + 16'd1;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        flag_1ms <= 1'b0;
    else if (flag_1ms == (CNT_MAX-1))
        flag_1ms <= 1'b1;
    else
        flag_1ms <= 1'b0;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        cnt_sel <= 3'd0;
    else if ((flag_1ms==1'b1) && (cnt_sel==3'd6))
        cnt_sel <= 3'd0;
    else if (flag_1ms == 1'b1)
        cnt_sel <= cnt_sel + 3'd1;
    else
        cnt_sel <= cnt_sel;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        sel_reg <= 6'b111_111;//消隐
    else if ((flag_1ms==1'b1) && (cnt_sel==3'd6))
        sel_reg <= 6'b111_111;//消隐
    else if ((flag_1ms==1'b1) && (cnt_sel==3'd0))
        sel_reg <= 6'b000_001;
    else if (flag_1ms==1'b1)
        sel_reg <= sel_reg << 1'b1;
    else
        sel_reg <= sel_reg;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        data_disp <= 4'b0000;
    else if ((seg_en==1'b1) && (flag_1ms==1'b1)) case (cnt_sel)
        3'd0: data_disp <= 4'b1111;//消隐,不显示(共阳数码管,高电平熄灭)
        3'd1: data_disp <= data_reg[3:0];   //个
        3'd2: data_disp <= data_reg[7:4];   //十
        3'd3: data_disp <= data_reg[11:8];  //百
        3'd4: data_disp <= data_reg[15:12]; //千
        3'd5: data_disp <= data_reg[19:16]; //万
        3'd6: data_disp <= data_reg[23:20]; //十万
        default: data_disp <= 4'b0000;//显示数字0
    endcase
    else
        data_disp <= data_disp;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        dot_disp <= 1'b1;//共阳数码管,高电平小数点段dp熄灭
    else if (flag_1ms==1'b1)
        dot_disp <= point[cnt_sel-1]==1'b1? 1'b0: 1'b1;
    else
        dot_disp <= dot_disp;

always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        seg <= 8'hFF;
    else case (data_disp)
        4'd0  : seg <= {dot_disp,7'b100_0000}; //数字0
        4'd1  : seg <= {dot_disp,7'b111_1001}; //数字1
        4'd2  : seg <= {dot_disp,7'b010_0100}; //数字2
        4'd3  : seg <= {dot_disp,7'b011_0000}; //数字3
        4'd4  : seg <= {dot_disp,7'b001_1001}; //数字4
        4'd5  : seg <= {dot_disp,7'b001_0010}; //数字5
        4'd6  : seg <= {dot_disp,7'b000_0010}; //数字6
        4'd7  : seg <= {dot_disp,7'b111_1000}; //数字7
        4'd8  : seg <= {dot_disp,7'b000_0000}; //数字8
        4'd9  : seg <= {dot_disp,7'b001_0000}; //数字9
        4'd10 : seg <= 8'b1011_1111 ; //负号"-"
        4'd11 : seg <= 8'b1111_1111 ; //不显示
        default:seg <= 8'b1100_0000;  //数字0,不带小数点
    endcase

//同步 sel_reg 和 seg 的时钟
always@(posedge sys_clk or negedge sys_rst_n)
    if (sys_rst_n == 1'b0)
        sel <= 6'b111_111;
    else
        sel <= sel_reg;

bcd_8421 bcd_8421_inst
(
    .sys_clk  (sys_clk  ), //系统时钟,50MHz
    .sys_rst_n(sys_rst_n), //系统复位,低电平有效
    .data     (data     ), //待转码十进制数的二进制编码
    
    .unit     (unit ), //转码结果的个位 BCD 码
    .ten      (ten  ), //转码结果的十位 BCD 码
    .hun      (hun  ), //转码结果的百位 BCD 码
    .tho      (tho  ), //转码结果的千位 BCD 码
    .t_tho    (t_tho), //转码结果的万位 BCD 码
    .h_tho    (h_tho)  //转码结果的十万位 BCD 码
);

//模块结束
endmodule

那么这样我们就完成了动态显示驱动模块 segment_dynamic 的代码编写,保存为 segment_dynamic.v
回到实验工程添加 segment_dynamic.v 模块;然后进行全编译,编译出错点击 OK

image-20231202103119615

查看一下报错信息

Error (10170): Verilog HDL syntax error at segment_dynamic.v(129) near text “1”; expecting “;”

发现问题代码在 129 行附近,查看修改后保存

image-20231202104859898

回到 Quartus II 再次编译,依然报错

Error (10228): Verilog HDL error at bcd_8421.v(2): module “bcd_8421” cannot be declared more than once

tb_bcd_8421.v 文件的第 3 行:tb_bcd_8421.v 删除,再次编译工程,编译通过点击 OK

image-20231202110311454

下面我们开始仿真代码的编写

`timescale 1ns/1ns //时间参数

//模块开始 模块名称 端口列表
module tb_segment_dynamic();

reg        sys_clk  ;
reg        sys_rst_n;
reg [19:0] data     ;
reg [5:0]  point    ;
reg        sign     ;
reg        seg_en   ;

//声明变量,将两路输出信号引出来
wire [7:0] seg;
wire [5:0] sel;

//对声明的变量进行初始化
initial
    begin
        sys_clk = 1'b1;
        sys_rst_n <= 1'b0;
        data <= 20'd0;
        point <= 6'b000_000;
        sign <= 1'b0;
        seg_en <= 1'b0;
        #35
        sys_rst_n <= 1'b1;
        data <= 20'd9876;
        point <= 6'b000_010;//选中第二个数码位让它显示小数点
        sign <= 1'b1;//负号
        seg_en <= 1'b1;//数码管进行显示
    end

always #10 sys_clk = ~sys_clk; //时钟信号

segment_dynamic segment_dynamic_inst
#(
    .CNT_MAX (16'd9)//1ms 计数器计数最大值
)
(
    .sys_clk  (sys_clk  ), //系统时钟,50MHz
    .sys_rst_n(sys_rst_n), //系统复位,低电平有效
    .data     (data     ), //待转码十进制数的二进制编码
    .point    (point    ), //小数点
    .sign     (sign     ), //符号(负号是否显示)
    .seg_en   (seg_en   ), //数码管显示使能
    
    .seg      (seg      ), //段选
    .sel      (sel      )  //位选
);

//模块结束
endmodule

仿真代码编写完成后保存为 tb_segment_dynamic.v
回到实验工程添加 tb_segment_dynamic.v 文件;然后进行全编译,编译报错点击 OK

image-20231202164440743

查看错误信息:

Error (10170): Verilog HDL syntax error at tb_segment_dynamic.v(37) near text “#”; expecting “;”

提示仿真代码第 37 行附近缺少分号,查看代码发现实例化模块的名称位置放错了,修改保存

image-20231202165516816

再次进行全编译,编译通过点击 OK

image-20231202165614850

下面进行仿真设置

image-20231202165804322

接下来进行仿真,时间参数设置为 10us,运行一次,全局视图

image-20231202200011647

我们发现 flag_1ms 的波形没有变化,回到 segment_dynamic.v 文件中查看,发现给 flag_1ms 赋值高电平时的条件编写错误,修改完保存

image-20231202200226702

回到我们的仿真界面,然后全选、删除所有波形,回到 Library 窗口,找到修改的代码对它进行重编译

image-20231202200534462

然后回到 sim 窗口重新添加波形,然后回到波形界面,全选、分组;然后点击 Restart,重新运行 10us 运行一次

image-20231202201329909

再次回到 segment_dynamic.v 文件进行排错,发现当 cnt_sel 为 0 时,cnt_sel-3'd1 作为 point 的索引值出现了负数,修改并保存

image-20231202202058800

再次仿真

image-20231202202228816

接下来参照绘制的波形图查看一下仿真波形
首先是从 bcd_8421 模块引出的 BCD 码

image-20231202202550311

他们的初值是 0,没有问题;然后个位是 6 没有问题,十位是 7,然后是 8、9;最高的两位始终保持 0,这儿没有问题。

然后看一下数据寄存

image-20231203092340970

下面看一下 1ms 计数器
image-20231203093633504

image-20231203094016864

然后是扫描计数器
image-20231203094441734

image-20231203101759194

可以看到 data_dispsel_reg 错开了一位,回到代码查看 data_disp 的赋值语句块,修改(时序逻辑节拍延迟,cnt_sel 自加一的条件中有 flag_1ms==1'b1 部分,然后在这里的条件又有 flag_1ms==1'b1 导致延迟两个 flag_1ms 的节拍)并保存

image-20231203103019695

再次仿真查看波形

image-20231203103854816

image-20231203104912443

位选信号是在位选寄存信号打一拍得到的,在图中红框这个位置确实延迟了一个时钟周期,然后他们俩的数据也是对应的。

下面看一下段选信号

image-20231203105457353

可以看到,段选信号是有问题的。回到 segment_dynamic.v 查看,修改给 dot_disp 赋值的部分,因为给 seg 赋值时需要使用 dot_disp 替换最高有效位

image-20231203111625798

还有需要修改 segment_dynamic.v 的 119 行,这里我们错误的把段码赋值给 data_disp

image-20231203112127824

为了和绘制的波形图保持一致,还需要修改代码第 149 行

image-20231203112700220

这时再仿真查看波形,观察段选信号 seg

image-20231203113125616

当选中第一个数码位,段码 82 与绘制波形是对应的,没有问题;当选中第二个数码位的时候是 78 没有问题;第三个 80,第四个 90,第五个 bf 都没有问题;当选中第六个数码位,段码是 ff 与绘制波形图也是一致的
那么这样仿真验证就通过了。
到了这里,我们已经实现了动态显示驱动模块它的模块功能,接下来就可以实现其他模块的模块功能。

动态显示驱动模块 segment_dynamic 的功能实现之后,我们就可以通过动态显示驱动模块 segment_dynamic 和 HC595 控制模块 hc595_ctrl 生成动态显示模块 segment_595_dynamic

首先,复用一下 HC595 控制模块:将 segment_595_static/rtl/hc595_ctrl.v 复制粘贴到 segment_595_dynamic/rtl/hc595_ctrl.v;HC595 控制模块复用完成后,接下来就编写动态显示模块

//模块开始 模块名称 端口列表
module segment_595_dynamic
(
    input   wire        sys_clk     , //系统时钟,50MHz
    input   wire        sys_rst_n   , //系统复位,低电平有效
    input   wire [19:0] data        , //待转码十进制数的二进制编码
    input   wire [5:0]  point       , //小数点
    input   wire        sign        , //符号(负号是否显示)
    input   wire        seg_en      , //数码管显示使能
    
    output  wire        ds          , //输出给74HC595的串行数据
    output  wire        shcp        , //移位寄存器时钟
    output  wire        stcp        , //存储寄存器时钟
    output  wire        oe_n          //74HC595的输出使能,低电平有效
);

//声明两个变量,将 segment_dynamic 模块输出的位选、段选输入给 hc595_ctrl
wire [5:0]  sel;
wire [7:0]  seg;

segment_dynamic segment_dynamic_inst
#(
    .CNT_MAX  (16'd49_999)//1ms 计数器计数最大值
)
(
    .sys_clk  (sys_clk  ), //系统时钟,50MHz
    .sys_rst_n(sys_rst_n), //系统复位,低电平有效
    .data     (data     ), //待转码十进制数的二进制编码
    .point    (point    ), //小数点
    .sign     (sign     ), //符号(负号是否显示)
    .seg_en   (seg_en   ), //数码管显示使能
    
    .seg      (seg      ), //段选
    .sel      (sel      )  //位选
);

hc595_ctrl hc595_ctrl_inst
(
    .sys_clk  (sys_clk  ), //系统时钟,50MHz
    .sys_rst_n(sys_rst_n), //系统复位,低电平有效
    .sel      (sel      ), //六位数码管位选
    .seg      (seg      ), //六位数码管段选
    
    .ds       (ds       ), //输出给74HC595的串行数据
    .shcp     (shcp     ), //移位寄存器时钟
    .stcp     (stcp     ), //存储寄存器时钟
    .oe_n     (oe_n     )  //74HC595的输出使能,低电平有效
);

//模块结束
endmodule

参照着模块框图完成动态显示模块的代码编写后,将它保存为 segment_595_dynamic.v;然后回到实验工程添加刚刚编写的模块;然后进行编译,编译报错点击 OK

image-20231203122815110

查看错误信息:

Error (10170): Verilog HDL syntax error at segment_595_dynamic.v(22) near text “#”; expecting “;”

鼠标左键双击该条 error,跳转到代码文件中检查

image-20231203123757056

发现又犯了同样的错误:对带参数的模块进行实例化时,实例化名称的位置应当在参数后面对带参数的模块进行实例化时,实例化名称的位置应当在参数后面对带参数的模块进行实例化时,实例化名称的位置应当在参数后面,但愿之后我能记住吧…………

修改完成保存,回到 Quartus II 进行重新编译,编译通过点击 OK

image-20231203124447518

对于动态显示模块不再进行单独的仿真,等到顶层模块的代码编写完成了,再进行整体的仿真。

下面就参照顶层模块的框图编写最终的顶层模块 top_segment_595

//模块开始 模块名称 端口列表
module top_segment_595
(
    input   wire        sys_clk     , //系统时钟,50MHz
    input   wire        sys_rst_n   , //系统复位,低电平有效
    
    output  wire        ds          , //输出给74HC595的串行数据
    output  wire        shcp        , //移位寄存器时钟
    output  wire        stcp        , //存储寄存器时钟
    output  wire        oe_n          //74HC595的输出使能,低电平有效
);

wire [19:0] data  ;
wire [5:0]  point ;
wire        sign  ;
wire        seg_en;

data_gen
#(
    .CNT_MAX  (23'd4_999_999),//计数 0.1s 计数最大值
    .DATA_MAX (20'd999_999  ) //待显示数据最大值
)
data_gen_inst
(
    .sys_clk  (sys_clk  ), //系统时钟,50MHz
    .sys_rst_n(sys_rst_n), //系统复位,低电平有效
    
    .data     (data  ), //待显示数据
    .point    (point ), //小数点
    .sign     (sign  ), //负号
    .seg_en   (seg_en)  //数码管动态显示模块工作使能
);

segment_595_dynamic segment_595_dynamic
(
    .sys_clk  (sys_clk  ), //系统时钟,50MHz
    .sys_rst_n(sys_rst_n), //系统复位,低电平有效
    .data     (data     ), //待转码十进制数的二进制编码
    .point    (point    ), //小数点
    .sign     (sign     ), //符号(负号是否显示)
    .seg_en   (seg_en   ), //数码管显示使能
    
    .ds       (ds       ), //输出给74HC595的串行数据
    .shcp     (shcp     ), //移位寄存器时钟
    .stcp     (stcp     ), //存储寄存器时钟
    .oe_n     (oe_n     )  //74HC595的输出使能,低电平有效
);

//模块结束
endmodule

那么这样,顶层模块的代码编写完成,保存为 top_segment_595.v;回到实验工程添加顶层模块(这儿有一点要注意:要把它置为顶层),然后进行全编译,报错点击 OK

image-20231203132006703

我们来看一下报错信息

Error (12006): Node instance “hc595_ctrl_inst” instantiates undefined entity “hc595_ctrl”

在顶层模块 top_segment_595.v 中对 hc595_ctrl 模块进行了实例化,但是我们并没有将 hc595_ctrl.v 文件添加到 Quartus II 的工程中,所以提示我们实体没有定义。回到实验工程,添加 hc595_ctrl.v 文件,然后重新编译,编译通过点击 OK

image-20231203132557571

接下来编写仿真文件对顶层文件进行一个整体的仿真

//时间参数
`timescale 1ns/1ns

//模块开始 模块名称 端口列表
module tb_top_segment_595();

//声明变量 时钟信号和复位信号
reg     sys_clk;
reg     sys_rst_n;

wire    ds  ;
wire    shcp;
wire    stcp;
wire    oe_n;

//对时钟信号和复位信号进行初始化
initial
    begin
        sys_clk = 1'b1;
        sys_rst_n <= 1'b0;
        #35
        sys_rst_n <= 1'b1;
    end

//生成时钟信号
always #10 sys_clk = ~sys_clk;

//为了缩短仿真时间,对子功能模块当中的两个计时参数进行重定义
defparam top_segment_595_inst.data_gen_inst.CNT_MAX(23'd49);
defparam top_segment_595_inst.segment_595_dynamic.segment_dynamic_inst.CNT_MAX(16'd9);

//实例化顶层模块
top_segment_595 top_segment_595_inst
(
    .sys_clk  (sys_clk  ), //系统时钟,50MHz
    .sys_rst_n(sys_rst_n), //系统复位,低电平有效
    
    .ds       (ds       ), //输出给74HC595的串行数据
    .shcp     (shcp     ), //移位寄存器时钟
    .stcp     (stcp     ), //存储寄存器时钟
    .oe_n     (oe_n     )  //74HC595的输出使能,低电平有效
);

//模块结束
endmodule

顶层模块的仿真代码编写完成后,保存为 tb_top_segment_595.v;回到实验工程添加仿真模块;然后进行全编译,编译失败点击 OK

image-20231203150441481

查看错误信息:

Error (10170): Verilog HDL syntax error at tb_top_segment_595.v(29) near text “(”; expecting “.”, or “[”, or “=”Error (10170): Verilog HDL syntax error at tb_top_segment_595.v(30) near text “(”; expecting “.”, or “[”, or “=”Error (10112): Ignored design unit “tb_top_segment_595” at tb_top_segment_595.v(5) due to previous errors

双击错误信息,跳转到 tb_top_segment_595.v 文件的第 29 行;观察发现参数重定义时本应该使用赋值运算符“=”而不是括号,我们这里使用错误,进行修改并保存

image-20231203150945985

回到实验工程,再次进行编译;编译通过点击 OK

image-20231203151108145

下面我们看一下 RTL 视图。首先看顶层

image-20231203151208806

顶层内部包含两个子功能模块:数据生成模块和动态显示模块,与我们绘制的框图是一致的

image-20231130094756670

动态显示模块内部又包含动态显示驱动模块和 HC595 控制模块

image-20231203151638743

segment_595_dynamic 模块的框图是对应的

image-20231129235108516

image-20231203152234834

segment_dynamic 模块框图也是对应的

image-20231129234951681

接下来进行仿真设置,这里一定记得选择顶层仿真模块,因为是对顶层模块进行逻辑仿真

image-20231203152609143

设置完成开始仿真。仿真编译完成点击 sim 窗口添加模块波形,回到 wave 窗口全选、分组、消除前缀;然后点击 Restart,时间参数先设置为 10us 运行一次、全局视图

image-20231203154600662

在实验工程当中,一些子功能模块在编写的时候已经进行了仿真验证,这儿就不再查看它们的波形,只看一下我们刚刚编写的顶层模块和前面没有仿真的动态显示模块的波形。

首先先来看一下动态显示模块,主要是看两个位置
image-20231203160517745

image-20231203161007924

两个 sel 信号它们的数据是相同的,没有问题。然后是段选信号

image-20231203161115807

段选信号也是没有问题的。下面就是 dsshcpstcpoe_n 这四路信号,HC595 控制模块输出的四路信号,我们来看一下。首先是 ds 信号

image-20231203161413812

这儿的 ds 信号波形是一致的,没有问题。然后是 oe_n 信号

image-20231203161621968

这儿这两路 oe_n 信号是相同的,没有问题。然后是移位寄存器时钟,我们看一下

image-20231203161751590

那么这两路信号也是没有问题的。然后是存储寄存器时钟

image-20231203161843721

这两个信号也是没有问题的。这样就表明:动态显示模块它是没有问题的
下面我们看一下顶层模块,顶层模块主要看一下这俩部分

image-20231203162133823

首先是待显示数据 data 我们来看一下

image-20231203162307944

这两路信号波形是一致的,没有问题。然后是小数点位和符号位

image-20231203162438325

它们都始终保持低电平,没有问题。然后是使能信号

image-20231203162629160

使能信号它的初值为低电平,后面一直保持高电平,没有问题,两个信号是一致的
下面看一下 dsshcpstcpoe_n 这四路信号;首先是 ds

image-20231203163033473

这三路 ds 信号波形是一致的,没有问题。然后是 oe_n

image-20231203163158566

始终为低电平,没有问题。

然后是移位寄存器时钟

image-20231203163327493

波形一致没有问题。然后是存储寄存器时钟

image-20231203163431027

波形一致没有问题
这样就表示顶层模块没有问题,仿真验证通过
接下来回到实验工程准备上板验证;上板验证开始之前绑定管脚:ds–>R1、oe_n–>L11、shcp–>B1、stcp–>K9、sys_clk–>E1、sys_rst_n–>M15

image-20231203164306366

引脚绑定完成,重新进行编译;编译完成点击 OK

image-20231203164418801

接下来参照下图所示连接板卡、电源、下载器,下载器的另一端连接到电脑,为开发板进行上电

然后回到实验工程,打开下载界面,然后下载程序

image-20231203164743269


上板验证视频

可以发现:数码管在进行一个循环的计数,计数的初值是 0,最大值是 999999,计数到最大值会归零开始下一个循环的计数
这里计数的时间应该是挺长的,我们就不再进行等待了。上板验证通过

参考资料:

34-第二十三讲-数码管动态显示(一)

35-第二十三讲-数码管动态显示(二)

36-第二十三讲-数码管动态显示(三)

37-第二十三讲-数码管动态显示(四)

38-第二十三讲-数码管动态显示(五)

39-第二十三讲-数码管动态显示(六)

40-第二十三讲-数码管动态显示(七)

20. 数码管的动态显示


  1. 图片来源:立创商城 ↩︎

版权声明:本文为博主作者:cedtek原创文章,版权归属原作者,如果侵权,请联系我们删除!

原文链接:https://blog.csdn.net/CardistAlive/article/details/134767270

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2024年4月16日
下一篇 2024年4月16日

相关推荐