在上一小节当中,我们对数码管的静态显示做了一个详细的讲解;但是如果单单只掌握数码管的静态显示这种显示方式是远远不够的,因为数码管的静态显示当中,被选中的数码位它们显示的内容都是相同的,这种显示方式在我们的实际应用当中显然是不合适的;我们希望控制每个数码位能够独立的显示我们想要显示的内容,如何实现这一操作呢?就是本小节所要讲解的内容:数码管的动态显示。
本小节的主要内容分为两个部分:
- 第一部分是理论学习,在这一部分,我们会对数码管的动态显示的工作原理做一个详细的讲解;
- 第二部分是实战演练,在这一部分,会通过实验工程设计并实现数码管的动态显示。
首先是理论学习
1 理论学习
我们征途系列开发板使用的是六位八段数码管,实物图1如下
在开发板的右上角
它的内部结构图如下
由图可知,六位八段数码管当中,每个数码位的段选信号全部连接到了一起,然后进行输出;每个数码位单独引出一个位选信号用来控制数码位的选择,这种连接方式会使得被选中的数码位它们显示的内容都是相同的,因为它们的段选信号已经全部连接到了一起。如何使用这个六位八段数码管来实现我们的数码管的动态显示呢?我们需要使用一种方式:动态扫描
如何使用动态扫描的方式来实现数码管的动态显示呢?我们给大家举一个例子:比如说,我们想要使用我们的六位八段数码管来显示数字 123456
如何使用我们的六位八段数码管来显示数字 123456 呢?
首先我们选中第一个数码位,让这个数码位显示数字 1;然后它显示的时间我们设为 T \text{T} T,这个 T \text{T} T 可以看作一个周期
当我们的第一个数码位完成一个 T \text{T} T 周期数字 1 的显示之后,立刻选中我们的第二个数码位;注意,此时只选中了第二个数码位让它显示数字 2,显示的时间同样是一个周期 T \text{T} T
当我们的第二个数码位完成了一个周期 T \text{T} T 数字 2 的显示之后,立刻选中第三个数码位;这儿注意,也是只选中了第三个数码位让它显示数字 3,同样显示时间为 T \text{T} T
依次往下类推,那么此时就显示 4
然后是 5
然后是 6
当我们第六个数码位完成了一个周期 T \text{T} T 数字 6 的显示之后,再重新选中第一个数码位;这儿也是只选中了第一个数码位,让它继续显示数字 1,然后显示的时间仍然是 T \text{T} T 周期,这样依次往下循环。
通过上述动态显示过程的描述,我们知道这样一个循环是六个周期就是 6 T 6\text{T} 6T;如果说给这个 T \text{T} T 规定一个确切的时间会怎样呢?我们首先给 T \text{T} T 规定一个确切的时间为 1s;如果说 T \text{T} T 等于 1s,我们的六位八段数码管的六个数码位会依次显示 1、2、3、4、5、6 每个数字显示的时间为 1s
如果进一步把这个 T \text{T} T 进行缩短,比如说我们缩短到 0.2s 这时候我们的六位八段数码管,它的六个数码位会进行闪烁显示,显示的内容依次是 1、2、3、4、5、6
如果进一步缩短时间 T \text{T} T 把它缩短为 1ms,这时候实际上我们的六位八段数码管,它的六个数码位也是进行依次闪烁的显示,显示的内容依然依次是 1、2、3、4、5、6 每个数字显示时间是 1ms
但是它们切换的频率太快了,我们的肉眼不能分辨这种闪烁,就误以为我们的六位八段数码管六个数码位在同时进行显示,而且显示的内容是 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
的框图如下
输入信号
- sys_clk:系统时钟信号
- sys_rst_n:系统复位信号
输出信号(传入到 74HC595)
- ds:串行数据
- shcp:移位寄存器时钟
- stcp:存储寄存器时钟
- oe_n:输出使能
下面结合我们的实验目标和层次化的设计思想,将整个系统工程进行子功能的划分。我们的实验目标是使用我们的六位八段数码管来实现数码管的动态显示,显示的内容是 0 到 999999;这个显示的数据肯定需要一个模块来产生,我们就先定义一个新的子功能模块:数据生成模块 data_gen
,让它产生我们需要显示的数据
输入信号
- sys_clk:系统时钟信号
- sys_rst_n:系统复位信号
输出信号
- data[19:0]:待显示的数据。它的最大值是 ( 999999 ) 10 (999999)_{10} (999999)10,换算成二进制就是 ( 1111 _ 0100 _ 0010 _ 0011 _ 1111 ) 2 (1111\_0100\_0010\_0011\_1111)_{2} (1111_0100_0010_0011_1111)2,所以它的位宽是 20 位宽
- point[5:0]:(数码管动态显示模块
segment_595_dynamic
可能会用于温湿度的显示,所以说要增加小数点的生成)小数点信号。六位八段数码管有 6 个小数点段,所以该信号的位宽是 6 位宽 - sign:(数码管动态显示模块
segment_595_dynamic
可能会用于电压测量的显示,所以说要生成符号位)符号位。数值的负号 - seg_en:(为了能够更好的控制我们的数码管动态显示模块
segment_595_dynamic
,生成一个使能信号)当使能信号为有效的高电平时,数码管可以正常的显示;当使能信号为低电平时,数码管就不工作
有了数据产生模块之后,接下来就是数码管动态显示模块 segment_595_dynamic
输入信号
- 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
将位选信号和段选信号转化为 ds
、shcp
、stcp
、oe_n
这四路信号传入到 74HC595 控制芯片。
动态显示驱动模块 segment_dynamic
输入信号
- sys_clk:系统时钟信号
- sys_rst_n:系统复位信号
- data[19:0]:待显示数据
- point[5:0]:待显示数据小数点位
- sign:待显示数据的符号位
- seg_en:数码管动态显示模块使能信号
输出信号
- sel[5:0]:位选信号
- seg[7:0]:段选信号
74HC595 控制模块 hc595_ctrl
输入信号
- sys_clk:系统时钟信号
- sys_rst_n:系统复位信号
- sel[5:0]:位选信号
- seg[7:0]:段选信号
输出信号(就是传入到 74HC595 芯片的四路信号)
- ds:串行数据
- shcp:移位寄存器时钟
- stcp:存储寄存器时钟
- oe_n:输出使能
各子功能模块的模块框图绘制完成,下面开始系统框图的绘制。
首先是数码管动态显示模块 segment_595_dynamic
它的模块功能由两个子功能模块实现,输入到数码管动态显示模块 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
传入到顶层模块 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
的波形图
由模块框图可知,输入信号有两路:时钟信号 sys_clk
和复位信号 sys_rst_n
。
在我们的实验目标当中我们提到:我们想要使用我们的六位八段数码管实现数码管的动态显示,显示的内容是十进制的数字 0 0 0 到最大值 999999 999999 999999 的循环计数,计数的间隔是每 0.1 s 0.1\text{s} 0.1s 加 1 1 1;这个 0.1 s 0.1\text{s} 0.1s 的计数就需要一个计数器来实现,所以我们需要声明一个计数器变量 cnt_100ms
对 0.1 s 0.1\text{s} 0.1s 进行计数。首先,当复位信号 sys_rst_n
有效时给计数器变量 cnt_100ms
赋一个初值 0 0 0;复位信号 sys_rst_n
无效时 cnt_100ms
每个系统时钟周期自加 1 1 1,cnt_100ms
的计数周期是 0.1 s 0.1\text{s} 0.1s 就是 100 ms 100\text{ms} 100ms,cnt_100ms
计数器完成 0.1 s 0.1\text{s} 0.1s 的计数,要计数多少个系统时钟周期呢?我们知道系统时钟的频率是 50 MHz 50\text{MHz} 50MHz 换算为周期就是 T s y s _ c l k = 20 ns \text{T}_{sys\_clk}=20\text{ns} Tsys_clk=20ns,我们的计数器要完成 0.1 s 0.1\text{s} 0.1s 即 100 ms 100\text{ms} 100ms 的计数, 100 ms 100\text{ms} 100ms 换算成 ns \text{ns} ns 就是 T c n t _ 100 m s = 1 × 1 0 8 ns \text{T}_{cnt\_100ms}=1\times10^8\text{ns} Tcnt_100ms=1×108ns,我们的计数器若想要完成 0.1 s 0.1\text{s} 0.1s 的计数,需要在频率为 50 MHz 50\text{MHz} 50MHz 的系统时钟下完成 T c n t _ 100 m s / T s y s _ c l k = ( 20 ns ) / ( 1 × 1 0 8 ns ) = 5 × 1 0 6 \text{T}_{cnt\_100ms}/\text{T}_{sys\_clk}=(20\text{ns})/(1\times10^8\text{ns})=5\times10^6 Tcnt_100ms/Tsys_clk=(20ns)/(1×108ns)=5×106 个时钟周期的计数;因为我们的计数器是从 0 0 0 开始计数,所以说计数的最大值应该是 5 × 1 0 6 − 1 = 4999999 5\times10^6-1=4999999 5×106−1=4999999,cnt_100ms
计数的最大值就是 4999999 4999999 4999999。当 cnt_100ms
计数到最大值就让它归零,开始下一个周期的计数。
还需要一个变量,就是我们的 cnt_flag
信号,那么为什么要声明这个 cnt_flag
信号呢?我们需要使用这个 cnt_flag
信号做一个条件来控制我们输出的待显示数据让 data[19:0]
进行自加。那么有的朋友可能想到:我们也可以使用计数器,当 cnt_100ms
计数到最大值作为一个条件,控制我们输出的待显示数据 data[19:0]
让它进行自加,那么这样也是可以的;但是我们声明这个 cnt_flag
信号是为了让我们的约束条件更加的简洁清晰。首先,当复位信号有效时给 cnt_flag
赋一个初值 0 0 0,当 cnt_100ms
计数到最大值减一( 4999998 4999998 4999998)的时候将 cnt_flag
拉高一个时钟周期,其他时刻 cnt_flag
保持低电平。
输出信号有四路:第一路是待显示的数据 data[19:0]
、第二路是小数点位 point[5:0]
、第三路是符号位 sign
、第四路是使能信号 seg_en
。
首先看一下待显示数据 data[19:0]
的波形。复位信号有效时给 data[19:0]
赋一个初值 20'd0
,在第一个 0.1 s 0.1\text{s} 0.1s 显示时间内数码管显示数字 0 0 0,所以 data[19:0]
在 cnt_flag
信号出现有效的高脉冲前一直保持初值;当 cnt_flag
信号出现有效的高脉冲时就表示 0.1 s 0.1\text{s} 0.1s 计数完成,然后我们输出的待显示数据 data[19:0]
要自加一,因为是时序逻辑,延迟了一个时钟周期是没有问题的;当我们的 cnt_flag
信号为有效的高脉冲 data[19:0]
自加一,自加到最大值 20'd999_999
归零,开始下一个周期的计数。
因为我们征途系列开发板使用的是六位八段数码管,每个数码位都有一个小数点位,所以说 point
它的位宽为六位宽 point[5:0]
,每一位表示每个数码位的小数点位。在这里我们定义为高电平使我们的小数点位有效,通过实验目标可知:我们的显示过程中并没有使用到小数点位,所以说让 point[5:0]
一直保持为无效的低电平。
我们的符号位同样是高电平有效,当我们的数码管进行负数显示的时候,在显示的数据之前会加一个负号用来区分正负数
在我们本次的显示当中是显示数字 0 0 0 到最大值 999999 999999 999999 没有负号显示,所以说让它一直保持为无效的低电平。
最后一路输出信号:使能信号 seg_en
。使能信号 seg_en
控制数码管的显示与否,当它为有效的高电平时,数码管可以进行正常的显示;当它为无效的低电平时,数码管就不能进行显示。在这个实验当中我们实现的是十进制的数字 0 0 0 到最大值 999999 999999 999999 的循环计数,所以使能信号 seg_en
就要一直拉高,这样我们的数码管才能够正常的显示。首先复位期间先给赋一个初值低电平,然后当复位信号无效时,让它一直保持高电平。
数据生成模块整体的波形图绘制完成后,接下来就参照这个波形图进行代码的编写
cnt_100ms
计数的最大值是 5 × 1 0 6 − 1 = ( 4999999 ) 10 5\times10^6-1=(4999999)_{10} 5×106−1=(4999999)10,换算成二进制就是 ( 4999999 ) 10 = ( 100 _ 1100 _ 0100 _ 1011 _ 0011 _ 1111 ) 2 (4999999)_{10}=(100\_1100\_0100\_1011\_0011\_1111)_{2} (4999999)10=(100_1100_0100_1011_0011_1111)2,所以 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 , //系统时钟,50MHzinput 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;elsecnt_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;elsecnt_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;elsedata <= 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;elseseg_en <= 1'b1;//模块结束
endmodule
文件体系
segment_595_dynamic
├─doc
│ segment_595_dynamic.vsdx
├─quartus_prj
├─rtl
│ data_gen.v
└─sim
然后建立一个新的实验工程,添加我们编写的代码,进行全编译;出现了报错信息,我们点击 OK
看一下报错信息
Error (12007): Top-level design entity “top_segment_595” is undefined
提示我们不存在 top_segment_595 模块,这个问题在数码管的静态显示工程中我们也遇到过,在那个时候我们提到了这个问题有两种解决方式:第一种方式是重新编写一个顶层文件,将我们的子功能模块例化到顶层文件,然后进行编译;第二种方式是将我们的子功能模块强制置为顶层。之前我们使用的是第一种方式解决这个问题,在这儿我们使用第二种方式解决这个问题:选中 data_gen
子功能模块,点击鼠标右键,选择第三项 Set as Top-Level Entity 将它置为顶层
然后再次进行编译,编译通过点击 OK
接下来就需要编写我们的仿真文件
//时间参数
`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;//变量初始化
initialbeginsys_clk = 1'b1;sys_rst_n <= 1'b0;#35sys_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
│
└─simtb_data_gen.v
然后回到实验工程,添加我们的仿真模块;进行全编译;编译完成点击 OK
然后进行仿真设置,开始仿真;仿真完成之后打开 sim 窗口添加模块波形;波形界面全选、分组、消除前缀;然后点击 Restart,将时间参数先设置为 10us,运行一次。
发现 cnt_flag
波形有问题
查看 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 , //系统时钟,50MHzinput 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;elsecnt_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;elsecnt_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;elsedata <= 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;elseseg_en <= 1'b1;//模块结束
endmodule
重新编译 data_gen.v
模块,将时间参数设置为 12us 再次仿真,参照我们绘制的波形图,来看一下我们的仿真波形
首先是计数器
下面看一下 cnt_flag
信号
那么下面就看一下待显示数据
下面是小数点信号,始终为 0 没有问题
符号位始终为低电平,也没有问题
然后是使能信号,初值为低电平,复位信号无效时一直保持高电平
这三路信号与我们绘制的波形图是完全一致的,我们的数据生成模块 data_gen
通过了仿真验证。
我们已经实现了系统框图的绘制,完成了数据生成模块 data_gen
它的功能实现;接下来我们将继续生成我们的子功能模块。
首先先来看一下我们的系统框图
由系统框图我们可以知道:顶层模块 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
8421 码: bit3 ‾ bit2 ‾ bit1 ‾ bit0 ‾ 权值: 8 4 2 1 \begin{align} \text{8421 码:}&\underline{\text{bit3}}\quad\underline{\text{bit2}}\quad\underline{\text{bit1}}\quad\underline{\text{bit0}} \\ \text{权值:}&\,\,\,\,8\,\qquad4\,\qquad2\,\qquad1 \end{align} 8421 码:权值:bit3bit2bit1bit08421
5421 码它的权值从左到右就是 5、4、2、1
5421 码: bit3 ‾ bit2 ‾ bit1 ‾ bit0 ‾ 权值: 5 4 2 1 \begin{align} \text{5421 码:}&\underline{\text{bit3}}\quad\underline{\text{bit2}}\quad\underline{\text{bit1}}\quad\underline{\text{bit0}} \\ \text{权值:}&\,\,\,\,5\,\qquad4\,\qquad2\,\qquad1 \end{align} 5421 码:权值:bit3bit2bit1bit05421
2421 码它的权值从左到右就是 2、4、2、1
2421 码: bit3 ‾ bit2 ‾ bit1 ‾ bit0 ‾ 权值: 2 4 2 1 \begin{align} \text{2421 码:}&\underline{\text{bit3}}\quad\underline{\text{bit2}}\quad\underline{\text{bit1}}\quad\underline{\text{bit0}} \\ \text{权值:}&\,\,\,\,2\,\qquad4\,\qquad2\,\qquad1 \end{align} 2421 码:权值:bit3bit2bit1bit02421
其中 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 码所表示的十进制数;我们来计算一下
8421 码: 0 ‾ 1 ‾ 1 ‾ 1 ‾ 权值: 8 4 2 1 相乘: ⇓ ⇓ ⇓ ⇓ 乘积: 0 4 2 1 相加和 ⇔ 十进制数: 0 + 4 + 2 + 1 = 7 \begin{align} \text{8421 码:}&\underline{\text{0}}\quad\underline{\text{1}}\quad\underline{\text{1}}\quad\underline{\text{1}} \\ \text{权值:}&8\quad4\quad2\quad1 \\ \text{相乘:}&\!\Downarrow\quad\Downarrow\quad\Downarrow\quad\Downarrow \\ \text{乘积:}&0\quad4\quad2\quad1 \\ \text{相加和}\Leftrightarrow\text{十进制数:}&0\!+\!4\!+\!2\!+\!1=7 \end{align} 8421 码:权值:相乘:乘积:相加和⇔十进制数:01118421⇓⇓⇓⇓04210+4+2+1=7
其他的有权码也是使用这种方式:将各位二进制数与对应位的权值相乘,乘积相加就可以得到有权码所表示的十进制数。
无权码的计算方式我们这儿就不再进行讲解了,感兴趣的朋友可以自行查阅相关资料。
了解了有权码的编码方式之后,我们来看下前面提出的问题:为什么数据生成模块 data_gen
生成的二进制编码的十进制数,并不能够直接用于产生位选信号 sel[5:0]
和段选信号 seg[7:0]
;而 BCD 码表示的十进制数可以直接用于产生位选信号 sel[5:0]
和段选信号 seg[7:0]
呢?
我们这儿举一个例子,比如说使用多位数码管来示一个十进制数 ( 233 ) 10 (233)_{10} (233)10,十进制数 ( 233 ) 10 (233)_{10} (233)10 使用二进制表示应该是 ( 1110 _ 1001 ) 2 (1110\_1001)_{2} (1110_1001)2
十进制数 ( 233 ) 10 (233)_{10} (233)10 使用 8421 码表示应该是 ( 0010 _ 0011 _ 0011 ) 8421 (0010\_0011\_0011)_{8421} (0010_0011_0011)8421
知道了十进制数 ( 233 ) 10 (233)_{10} (233)10 的两种表示方式之后,接下来继续进行分析:如果使用动态扫描的方式在多位数码管上显示 ( 233 ) 10 (233)_{10} (233)10,就需要在第一个显示周期内点亮数码管的 DIG6 数码位(从左到右依次为DIG1,DIG2,DIG3,DIG4,DIG5,DIG6),让它显示个位 ( 3 ) 10 (3)_{10} (3)10
然后第二个显示周期内点亮 DIG5 数码位,让它显示十位 ( 3 ) 10 (3)_{10} (3)10
在第三个显示周期内点亮 DIG4 数码位,让它显示百位 ( 2 ) 10 (2)_{10} (2)10
如果说扫描的频率足够快,加上人眼的视觉暂留特性以及数码管的余晖效应,我们的肉眼就以为数码管在同时显示 ( 233 ) 10 (233)_{10} (233)10
这样就实现了 ( 233 ) 10 (233)_{10} (233)10 的显示。这种显示方式就要求我们能够从传入的数据当中,直接提取出待显示数据的个位、十位和百位;而传入的使用二进制表示的十进制数,从其中并不能直接提取出个位、十位和百位;而 BCD 码它所表示的十进制数是可以直接进行个位、十位、百位信息的提取:最低 4 位 ( 0011 ) 2 (0011)_{2} (0011)2 表示个位 ( 3 ) 10 (3)_{10} (3)10,中间 4 位 ( 0011 ) 2 (0011)_{2} (0011)2 表示十位 ( 3 ) 10 (3)_{10} (3)10,最高 4 位 ( 0010 ) 2 (0010)_{2} (0010)2 表示百位 ( 2 ) 10 (2)_{10} (2)10
这就是 BCD 码所表示的十进制数据能够直接用于产生位选和段选信号,而二进制表示的十进制数据并不能直接用于产生位选和段选信号的原因。
了解了这些之后,我们引入了一个新的问题:如何将二进制码转化为 BCD 码?接下来就通过一个例子来讲解一下转换的方法,在这里我们以三位十进制数 ( 233 ) 10 (233)_{10} (233)10 为例。
十进制数 ( 233 ) 10 (233)_{10} (233)10 的二进制编码形式是 ( 1110 _ 1001 ) 2 (1110\_1001)_{2} (1110_1001)2。实现转换的第一步:在输入的二进制码之前补上若干个 0;0 的个数规定为: 十进制数的十进制位个数 × 4 \text{十进制数的十进制位个数}\times4 十进制数的十进制位个数×4。
参与转换的十进制数有 n n n 个十进制位,那么就需要 n n n 个 BCD 码; ( 233 ) 10 (233)_{10} (233)10 的十进制位分别是个位 ( 3 ) 10 (3)_{10} (3)10、十位 ( 3 ) 10 (3)_{10} (3)10、百位 ( 2 ) 10 (2)_{10} (2)10 总共三个位,所以 n = 3 n=3 n=3 需要 3 3 3 个 BCD 码;每个 BCD 码是 4 个位宽, n × 4 = 3 × 4 = 12 n\times4=3\times4=12 n×4=3×4=12 所以 ( 1110 _ 1001 ) 2 (1110\_1001)_{2} (1110_1001)2 前面就需要补 12 12 12 个 0
得到 ( 0000 _ 0000 _ 0000 _ 1110 _ 1001 ) 2 (0000\_0000\_0000\_1110\_1001)_{2} (0000_0000_0000_1110_1001)2
如果是其他数值的多位十进制数,比如说 ( 114514 ) 10 = ( 1 _ 1011 _ 1111 _ 0101 _ 0010 ) 2 (114514)_{10}=(1\_1011\_1111\_0101\_0010)_{2} (114514)10=(1_1011_1111_0101_0010)2 它的十进制位个数是 6 6 6,每个 BCD 码需要 4 4 4 位二进制数,所以 ( 1 _ 1011 _ 1111 _ 0101 _ 0010 ) 2 (1\_1011\_1111\_0101\_0010)_{2} (1_1011_1111_0101_0010)2 前面就要补 24 24 24 个 0
得到 ( 0 _ 0000 _ 0000 _ 0000 _ 0000 _ 0000 _ 0001 _ 1011 _ 1111 _ 0101 _ 0010 ) 2 (0\_0000\_0000\_0000\_0000\_0000\_0001\_1011\_1111\_0101\_0010)_{2} (0_0000_0000_0000_0000_0000_0001_1011_1111_0101_0010)2
在这里我们就完成了补 0
的操作,得到了一组新的数据 ( 0000 _ 0000 _ 0000 _ 1110 _ 1001 ) 2 (0000\_0000\_0000\_1110\_1001)_{2} (0000_0000_0000_1110_1001)2,接下来就要对这组数据进行判断运算和移位操作。
首先判断 BCD 码部分,判断每一个 BCD 码所表示的十进制数是否大于等于 5,如果说每一个 BCD 码所表示的十进制数大于等于 5 就将它与 3 相加;如果说每一个 BCD 码所表示的十进制数小于 5 就让它保持原值不变。不论每一个 BCD 码所表示的十进制数大于等于 5 或者小于 5,完成判断运算之后都要向左移一个二进制位。
按照上述两条规则,得到下面的表格(上例十进制数 ( 233 ) 10 (233)_{10} (233)10 的二进制编码形式 ( 1110 _ 1001 ) 2 (1110\_1001)_{2} (1110_1001)2 转换成 8421BCD 码过程)
执行的操作 | 1 0 2 10^{2} 102 | 1 0 1 10^{1} 101 | 1 0 0 10^{0} 100 | 待转换数据(二进制数) |
---|---|---|---|---|
补 12 12 12 个 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 比较, 1 0 0 10^{0} 100 位加 3 | 0000 | 0000 | 1010 | 0_1001 |
整体向左移一个二进制位,第 4 次按位左移 | 0000 | 0001 | 0100 | 1001 |
各个 BCD 码分别和 5 比较,保持原值不变 | 0000 | 0001 | 0100 | 1001 |
整体向左移一个二进制位,第 5 次按位左移 | 0000 | 0010 | 1001 | 001 |
各个 BCD 码分别和 5 比较, 1 0 0 10^{0} 100 位加 3 | 0000 | 0010 | 1100 | 001 |
整体向左移一个二进制位,第 6 次按位左移 | 0000 | 0101 | 1000 | 01 |
各个 BCD 码分别和 5 比较, 1 0 1 、 1 0 0 10^{1}\text{、}10^{0} 101、100 位加 3 | 0000 | 1000 | 1011 | 01 |
整体向左移一个二进制位,第 7 次按位左移 | 0001 | 0001 | 0110 | 1 |
各个 BCD 码分别和 5 比较, 1 0 0 10^{0} 100 位加 3 | 0001 | 0001 | 1001 | 1 |
整体向左移一个二进制位,第 8 次按位左移 | 0010 | 0011 | 0011 | |
输出结果 | ( 2 ) 10 (2)_{10} (2)10 | ( 3 ) 10 (3)_{10} (3)10 | ( 3 ) 10 (3)_{10} (3)10 |
首先是完成补 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 次的判断运算和移位操作得到十进制数 ( 233 ) 10 (233)_{10} (233)10 它的 BCD 码编码形式,将每个 BCD 码换算成十进制数就是 ( 233 ) 10 (233)_{10} (233)10。这里需要注意:移位次数 8 等于待转换数据的二进制位数即位宽, ( 1110 _ 1001 ) 2 (1110\_1001)_{2} (1110_1001)2 的位宽是 8,上例就移位了 8 次;如果说输入的是 ( 999 _ 999 ) 10 (999\_999)_{10} (999_999)10 对应的二进制码 ( 1111 _ 0100 _ 0010 _ 0011 _ 1111 ) 2 (1111\_0100\_0010\_0011\_1111)_{2} (1111_0100_0010_0011_1111)2,它的位宽是 20 位宽就需要移位 20 次。
通过上述的方式,我们就可以将十进制数的二进制码转换为十进制数的 BCD 码。
以上部分就是本小节将会涉及到的 BCD 码的相关知识的一个讲解。
了解了 BCD 码的相关知识之后,我们需要建立一个新的子功能模块用来实现 BCD 码的转码
输入信号
- sys_clk:系统时钟信号
- sys_rst_n:系统复位信号
- data[19:0]:十进制数的二进制编码
实验目标当中六位八段数码管显示的最大值是 ( 999 _ 999 ) 10 (999\_999)_{10} (999_999)10,六位十进制数就需要使用 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
模块框图
修改后的 segment_595_dynamic
模块框图
修改后的 top_segment_595
模块框图
接下来就开始 BCD 转码模块 bcd_8421
的波形图的绘制
首先是输入信号时钟信号 sys_clk
、复位信号 sys_rst_n
以及二进制编码形式的十进制数 data[19:0]
,输入的十进制数据我们定义成一个固定值 20'd114514
,方便后面波形的绘制。输入信号的波形绘制完成之后,我们需要分析一下接下来该如何绘制。我们回看转码过程表格
执行的操作 | 1 0 2 10^{2} 102 | 1 0 1 10^{1} 101 | 1 0 0 10^{0} 100 | 待转换数据(二进制数) |
---|---|---|---|---|
补 12 12 12 个 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 比较, 1 0 0 10^{0} 100 位加 3 | 0000 | 0000 | 1010 | 0_1001 |
整体向左移一个二进制位,第 4 次按位左移 | 0000 | 0001 | 0100 | 1001 |
各个 BCD 码分别和 5 比较,保持原值不变 | 0000 | 0001 | 0100 | 1001 |
整体向左移一个二进制位,第 5 次按位左移 | 0000 | 0010 | 1001 | 001 |
各个 BCD 码分别和 5 比较, 1 0 0 10^{0} 100 位加 3 | 0000 | 0010 | 1100 | 001 |
整体向左移一个二进制位,第 6 次按位左移 | 0000 | 0101 | 1000 | 01 |
各个 BCD 码分别和 5 比较, 1 0 1 、 1 0 0 10^{1}\text{、}10^{0} 101、100 位加 3 | 0000 | 1000 | 1011 | 01 |
整体向左移一个二进制位,第 7 次按位左移 | 0001 | 0001 | 0110 | 1 |
各个 BCD 码分别和 5 比较, 1 0 0 10^{0} 100 位加 3 | 0001 | 0001 | 1001 | 1 |
整体向左移一个二进制位,第 8 次按位左移 | 0010 | 0011 | 0011 | |
输出结果 | ( 2 ) 10 (2)_{10} (2)10 | ( 3 ) 10 (3)_{10} (3)10 | ( 3 ) 10 (3)_{10} (3)10 |
这张表格展示了二进制向 BCD 编码转换的一个完整过程,可以帮助我们进行波形图的绘制。首先我们知道,二进制向 BCD 码转码需要通过判断运算加移位运算实现,并且运算次数规定为和输入的二进制码的位宽相同,比如说,我们输入的十进制数是 ( 114514 ) 10 (114514)_{10} (114514)10,二进制编码形式是 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 , //系统时钟,50MHzinput 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;elseshift_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;elsecnt_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)))begindata_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]);endelse if ((shift_flag==1'b1) && ((cnt_shift>=5'd1)&&(cnt_shift<=5'd20)))data_shift <= data_shift<<1;elsedata_shift <= data_shift;//输出信号的赋值
always@(posedge sys_clk or negedge sys_rst_n)if (sys_rst_n == 1'b0)beginunit <= 4'b0;ten <= 4'b0;hun <= 4'b0;tho <= 4'b0;t_tho <= 4'b0;h_tho <= 4'b0;endelse if (cnt_shift==5'd21)beginunit <= 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
下面开始编写我们的仿真代码,保存为 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;initialbeginsys_clk = 1'b1;sys_rst_n <= 1'b0;#35sys_rst_n <= 1'b1;data <= 20'd114_514;#1000data <= 20'd358_946;#2000data <= 20'd716_230;#3000data <= 20'd999_999;endalways #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
然后进行仿真设置
下面开始仿真,结合我们绘制的波形图查看一下仿真波形图。
首先是移位标志信号 shift_flag
移位标志信号 shift_flag
的初值是低电平,每个系统时钟周期进行取反。
接下来看一下移位计数器 cnt_shift
移位数据 data_shift
的波形就不再进行查看了,因为数据量太大了。
接下来看一下输出信号
那么这样我们就实现了 BCD 编码模块的功能,下面就可以实现动态显示驱动模块 segment_dynamic
动态显示驱动模块的模块框图之前已经绘制完成了
接下来就绘制 segment_dynamic
模块的波形图
输入信号有六路:时钟信号 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]
这两路信号可以得到我们想要显示的数据应该是 987.6 987.6 987.6。
符号位 sign
首先给它一个初值 0,然后当复位信号无效时将它拉高并且让它一直保持高电平,这就表示我们想要显示的是负数;通过 data[19:0]
、point[5:0]
、sign
这三路信号可以知道我们想要显示的数据应该是 − 987.6 -987.6 −987.6。
使能信号 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, T c n t _ 1 m s = 1 ms = 1 × 1 0 6 ns, T s y s _ c l k = 20 ns \text{T}_{cnt\_1ms}=1\text{ms}=1\times10^6\text{ns}\text{,}\text{T}_{sys\_clk}=20\text{ns} Tcnt_1ms=1ms=1×106ns,Tsys_clk=20ns 这就表示要完成 1 ms 1\text{ms} 1ms 的计数,要计数 T c n t _ 1 m s / T s y s _ c l k = ( 1 × 1 0 6 ) ns / 20 ns = 5 × 1 0 4 \text{T}_{cnt\_1ms}/\text{T}_{sys\_clk}=(1\times10^6)\text{ns}/20\text{ns}=5\times10^4 Tcnt_1ms/Tsys_clk=(1×106)ns/20ns=5×104 个系统时钟周期,因为计数器从 0 开始计数,这就表示计数的最大值应该是 5 × 1 0 4 − 1 = 49999 5\times10^4-1=49999 5×104−1=49999,声明计数器变量时为什么它的位宽是 16 呢?我们来算一下:
可以看到, ( 49999 ) 10 (49999)_{10} (49999)10 的二进制形式为 1100 _ 0011 _ 0100 _ 1111 1100\_0011\_0100\_1111 1100_0011_0100_1111 总共 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 , //系统时钟,50MHzinput 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,数码管应该显示 -11451else 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 个数码位上elsedata_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;elsecnt_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;elseflag_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;elsecnt_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;elsesel_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;//显示数字0endcaseelsedata_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;elsedot_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}; //数字04'd1 : seg <= {dot_disp,7'b111_1001}; //数字14'd2 : seg <= {dot_disp,7'b010_0100}; //数字24'd3 : seg <= {dot_disp,7'b011_0000}; //数字34'd4 : seg <= {dot_disp,7'b001_1001}; //数字44'd5 : seg <= {dot_disp,7'b001_0010}; //数字54'd6 : seg <= {dot_disp,7'b000_0010}; //数字64'd7 : seg <= {dot_disp,7'b111_1000}; //数字74'd8 : seg <= {dot_disp,7'b000_0000}; //数字84'd9 : seg <= {dot_disp,7'b001_0000}; //数字94'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;elsesel <= 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
查看一下报错信息
Error (10170): Verilog HDL syntax error at segment_dynamic.v(129) near text “1”; expecting “;”
发现问题代码在 129 行附近,查看修改后保存
回到 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
下面我们开始仿真代码的编写
`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;//对声明的变量进行初始化
initialbeginsys_clk = 1'b1;sys_rst_n <= 1'b0;data <= 20'd0;point <= 6'b000_000;sign <= 1'b0;seg_en <= 1'b0;#35sys_rst_n <= 1'b1;data <= 20'd9876;point <= 6'b000_010;//选中第二个数码位让它显示小数点sign <= 1'b1;//负号seg_en <= 1'b1;//数码管进行显示endalways #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
查看错误信息:
Error (10170): Verilog HDL syntax error at tb_segment_dynamic.v(37) near text “#”; expecting “;”
提示仿真代码第 37 行附近缺少分号,查看代码发现实例化模块的名称位置放错了,修改保存
再次进行全编译,编译通过点击 OK
下面进行仿真设置
接下来进行仿真,时间参数设置为 10us,运行一次,全局视图
我们发现 flag_1ms 的波形没有变化,回到 segment_dynamic.v
文件中查看,发现给 flag_1ms 赋值高电平时的条件编写错误,修改完保存
回到我们的仿真界面,然后全选、删除所有波形,回到 Library 窗口,找到修改的代码对它进行重编译
然后回到 sim 窗口重新添加波形,然后回到波形界面,全选、分组;然后点击 Restart,重新运行 10us 运行一次
再次回到 segment_dynamic.v
文件进行排错,发现当 cnt_sel
为 0 时,cnt_sel-3'd1
作为 point
的索引值出现了负数,修改并保存
再次仿真
接下来参照绘制的波形图查看一下仿真波形
首先是从 bcd_8421
模块引出的 BCD 码
他们的初值是 0,没有问题;然后个位是 6 没有问题,十位是 7,然后是 8、9;最高的两位始终保持 0,这儿没有问题。
然后看一下数据寄存
我们继续往下看
下面看一下 1ms 计数器
那么下面是标志信号
然后是扫描计数器
下面是位选信号的寄存
下面就是显示数据波形
可以看到 data_disp
和 sel_reg
错开了一位,回到代码查看 data_disp
的赋值语句块,修改(时序逻辑节拍延迟,cnt_sel
自加一的条件中有 flag_1ms==1'b1
部分,然后在这里的条件又有 flag_1ms==1'b1
导致延迟两个 flag_1ms
的节拍)并保存
再次仿真查看波形
下面是小数点信号
下面就是位选信号和段选信号,先来看一下位选信号
位选信号是在位选寄存信号打一拍得到的,在图中红框这个位置确实延迟了一个时钟周期,然后他们俩的数据也是对应的。
下面看一下段选信号
可以看到,段选信号是有问题的。回到 segment_dynamic.v
查看,修改给 dot_disp
赋值的部分,因为给 seg
赋值时需要使用 dot_disp
替换最高有效位
还有需要修改 segment_dynamic.v
的 119 行,这里我们错误的把段码赋值给 data_disp
了
为了和绘制的波形图保持一致,还需要修改代码第 149 行
这时再仿真查看波形,观察段选信号 seg
当选中第一个数码位,段码 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 , //系统时钟,50MHzinput 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
查看错误信息:
Error (10170): Verilog HDL syntax error at segment_595_dynamic.v(22) near text “#”; expecting “;”
鼠标左键双击该条 error,跳转到代码文件中检查
发现又犯了同样的错误:对带参数的模块进行实例化时,实例化名称的位置应当在参数后面、对带参数的模块进行实例化时,实例化名称的位置应当在参数后面、对带参数的模块进行实例化时,实例化名称的位置应当在参数后面,但愿之后我能记住吧…………
修改完成保存,回到 Quartus II 进行重新编译,编译通过点击 OK
对于动态显示模块不再进行单独的仿真,等到顶层模块的代码编写完成了,再进行整体的仿真。
下面就参照顶层模块的框图编写最终的顶层模块 top_segment_595
//模块开始 模块名称 端口列表
module top_segment_595
(input wire sys_clk , //系统时钟,50MHzinput 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
我们来看一下报错信息
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
接下来编写仿真文件对顶层文件进行一个整体的仿真
//时间参数
`timescale 1ns/1ns//模块开始 模块名称 端口列表
module tb_top_segment_595();//声明变量 时钟信号和复位信号
reg sys_clk;
reg sys_rst_n;wire ds ;
wire shcp;
wire stcp;
wire oe_n;//对时钟信号和复位信号进行初始化
initialbeginsys_clk = 1'b1;sys_rst_n <= 1'b0;#35sys_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
查看错误信息:
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 行;观察发现参数重定义时本应该使用赋值运算符“=”而不是括号,我们这里使用错误,进行修改并保存
回到实验工程,再次进行编译;编译通过点击 OK
下面我们看一下 RTL 视图。首先看顶层
顶层内部包含两个子功能模块:数据生成模块和动态显示模块,与我们绘制的框图是一致的
动态显示模块内部又包含动态显示驱动模块和 HC595 控制模块
和 segment_595_dynamic
模块的框图是对应的
然后动态显示驱动模块内部又包含了一个 BCD 编码模块
和 segment_dynamic
模块框图也是对应的
接下来进行仿真设置,这里一定记得选择顶层仿真模块,因为是对顶层模块进行逻辑仿真
设置完成开始仿真。仿真编译完成点击 sim 窗口添加模块波形,回到 wave 窗口全选、分组、消除前缀;然后点击 Restart,时间参数先设置为 10us 运行一次、全局视图
在实验工程当中,一些子功能模块在编写的时候已经进行了仿真验证,这儿就不再查看它们的波形,只看一下我们刚刚编写的顶层模块和前面没有仿真的动态显示模块的波形。
首先先来看一下动态显示模块,主要是看两个位置
首先找到动态显示模块,然后找到动态显示驱动模块,看一下 sel
、seg
这两路信号。首先是位选:
两个 sel
信号它们的数据是相同的,没有问题。然后是段选信号
段选信号也是没有问题的。下面就是 ds
、shcp
、stcp
、oe_n
这四路信号,HC595 控制模块输出的四路信号,我们来看一下。首先是 ds 信号
这儿的 ds 信号波形是一致的,没有问题。然后是 oe_n 信号
这儿这两路 oe_n 信号是相同的,没有问题。然后是移位寄存器时钟,我们看一下
那么这两路信号也是没有问题的。然后是存储寄存器时钟
这两个信号也是没有问题的。这样就表明:动态显示模块它是没有问题的
下面我们看一下顶层模块,顶层模块主要看一下这俩部分
首先是待显示数据 data 我们来看一下
这两路信号波形是一致的,没有问题。然后是小数点位和符号位
它们都始终保持低电平,没有问题。然后是使能信号
使能信号它的初值为低电平,后面一直保持高电平,没有问题,两个信号是一致的
下面看一下 ds
、shcp
、stcp
、oe_n
这四路信号;首先是 ds
这三路 ds 信号波形是一致的,没有问题。然后是 oe_n
始终为低电平,没有问题。
然后是移位寄存器时钟
波形一致没有问题。然后是存储寄存器时钟
波形一致没有问题
这样就表示顶层模块没有问题,仿真验证通过
接下来回到实验工程准备上板验证;上板验证开始之前绑定管脚:ds–>R1、oe_n–>L11、shcp–>B1、stcp–>K9、sys_clk–>E1、sys_rst_n–>M15
引脚绑定完成,重新进行编译;编译完成点击 OK
接下来参照下图所示连接板卡、电源、下载器,下载器的另一端连接到电脑,为开发板进行上电
然后回到实验工程,打开下载界面,然后下载程序
可以发现:数码管在进行一个循环的计数,计数的初值是 0,最大值是 999999,计数到最大值会归零开始下一个循环的计数
这里计数的时间应该是挺长的,我们就不再进行等待了。上板验证通过
参考资料:
34-第二十三讲-数码管动态显示(一)
35-第二十三讲-数码管动态显示(二)
36-第二十三讲-数码管动态显示(三)
37-第二十三讲-数码管动态显示(四)
38-第二十三讲-数码管动态显示(五)
39-第二十三讲-数码管动态显示(六)
40-第二十三讲-数码管动态显示(七)
20. 数码管的动态显示
图片来源:立创商城 ↩︎