文章目录
在我们的许多项目设计当中,我们通常需要一些显示设备来显示我们需要的信息。我们可以选择的显示设备有很多,而我们的数码管就是使用最多、最简单的显示设备之一。数码管它是一种半导体发光器件,它具有响应时间短、体积小、重量轻、寿命长的优点。
在本讲视频当中,我们就来学习了解一下数码管的静态显示。本讲视频的主要内容分为两个部分:理论学习部分和实战演练部分。在理论部分,我们会对本章节涉及到的数码管静态显示的相关内容,做一个系统性的讲解;在实战演练部分,我们会通过一个小实验,实现数码管的静态显示。
那么首先是理论学习
1 理论学习
1.1 数码管简介
数码管它是一种半导体发光器件,其基本单元是发光二极管。我们常见的数码管有七段数码管和八段数码管,那么八段数码管如下图1所示
七段数码管比八段数码管少一个小数点位
除了常见的七段管和八段管之外,还有其他类型的数码管:比如说:米字管,如下图2所示
还有 N 型管,以及工业科研领域用的 16 段管、24 段管等
1.2 八段数码管
我们本次实验使用的是八段数码管
那么由上图可知,八段数码管是一个 8 字形数码管,像一个数字 8。它分为八段,对应了 a、b、c、d、e、f、g 和小数点 dp,每一段就是一个发光二极管;这样的 8 段我们称为段选。一位八段数码管它常用的有十个管脚,每一段对应一根管脚,那么另外两根管脚就是 com 端是公共端(这两根管脚它们内部是导通的)。
八段数码管还分为共阴极数码管和共阳极数码管。对于共阴极数码管来说,它的八个发光二极管的阴极在数码管的内部全部连接在一起,而阳极是独立的,所以称为共阴,如下图所示
那么对于共阳极数码管来说,它八个发光二极管的阳极在数码管内部是连接在一块儿的,而它的阴极是独立的,如下图所示
了解了共阴极数码管和共阳极数码管的区别之后,有的朋友肯定要问:我们的数码管是怎么显示信息的?
我们这儿以共阳极数码管为例,下面这个表格是共阳极数码管对应的编码形式
待显示内容 | 段码(二进制格式) | 段码(十六进制格式) | |||||||
---|---|---|---|---|---|---|---|---|---|
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 |
我们的八段数码管它可以显示数字 0 到 9、字母 A 到 F 共 16 种字符。我们来举个例子,假如说要显示数字 0,因为我们使用的是共阳极数码管,共阳极数码管它所有的阳极都是连接在一块儿的,只有在每段对应的端口输入低电平,才可以点亮对应的二极管。我们从数字 0 对应的二进制格式可以看出 a、b、c、d、e、f 这 6 段都是低电平,所以说是点亮;然后 g 和小数点位 dp 是熄灭状态;那么这样就显示了一个数字 0,如下图所示
那么如果显示数字 1 呢?就点亮 b、c 两段,其他的是熄灭状态,就可以显示数字 1,如下图所示
那么表格的最后一列表示的是 16 种字符对应的十六进制格式段码,我们来看一下。以数字 0 为例,它的十六进制段码是 8'hC0
,我们化简为二进制看一下,8'hC0
对应就是 8'b1100_0000
;那么与二进制格式结合一下我们就可以知道,最低位是 a 位,最高位是小数点位 dp 这儿大家要注意一下。
那么以上部分讲解的就是八段数码管的相关内容。
但是在我们的实际工程中,我们可能要显示很多位的数据,那么一位八段数码管是不能满足这种要求的,所以说就出现了多位八段数码管。我们征途系列开发板使用的就是六位八段数码管,如下图3所示
在刚刚的一位八段数码管的讲解当中,我们已经提到了:那么一位八段数码管它是由八段构成,每一段对应一个端口信号;那么六位八段数码管是不是需要 48 个端口信号呢?当然不是。
当我们使用多位数码管时,为了减少数码管占用的 I/O 口,我们将段选就是数码管的 a、b、c 等引脚连接在一起引出来,而位选信号独立控制;这样我们就可以通过位选信号控制哪几个数码管点亮,就是通过位选信号控制选中哪一位数码管;而在同一时刻,位选选通的数码管显示的数字始终都是一样的,因为它们的段选是连接在一起的,所以说送入所有的数码管的段选信号都是相同的,这种显示方式我们叫它为静态显示。
什么意思呢?我们来解释一下。比如说,六位八段数码管我们选中第 0 位、第 2 位、第 3 位来显示一个数字 0;那么被选中的这三位都显示的是数字 0 因为它们的段选信号是连接在一起的,而位选信号就是选通它让它们进行显示。如下图所示
如果说数字 0 切换为数字 1,那么它们三个同时显示数字 1。
那么对于静态显示,还有一种方法就是:每一位的每一个段选信号都对应一个单独的 I/O 口进行驱动,它的优点是编程特别简单,但是缺点是会占用很多的 I/O 口资源;比如说六位八段数码管就会使用 48 个 I/O 口,所以说我们一般会选择上图所示的连接方式,那么六位八段数码管采用这种连接方式只会使用 14 个 I/O 口。
1.3 74HC595 简介
但是我们还是觉得使用 14 个I/O口占用资源比较多,所以说在征途系列开发板上我们使用了位移缓存器,就是我们的 74HC595。
74HC595 它是一个芯片,是一个 8 位串行输入、并行输出的移位缓存器,那么它的功能就是将串行数据转化为并行数据,它的内部具有八位移位寄存器和一个存储器,还有三态输出功能。如下图所示
由上图可知,输入数据的最低位和 Q7 是对应的、最高位和 Q0 是对应的。
那么看到这儿,有的学员肯定会问:它是一个八位串行输入、并行输出的位移缓存器,而我们前面已经说到了,我们使用的是六位八段数码管,有 6 个位选加 8 个段选,那么这样不是 14 位的数据吗?这儿只是一个八位串行输入,根本不够啊。
大家不需要担心,74HC595 它可以进行级联。我们来看一下我们的电路图
我们征途系列开发板使用了两片 74HC595 芯片。第一片(上图左侧)的 Q7S 端口与第二片(上图右侧)的 DS 端口相连接就构成了一个级联,那么这样它最多可以实现 16 位数据的串并转换。我们的第二片 74HC595 芯片它是与位选信号相连接,DIG6 对应的是位选的第 0 位 sel[0]
、DIG1 对应的是位选的第 5 位 sel[5]
;第一片 74HC595 芯片与段选信号线连接。那么这样我们的数据应该怎么输入呢?我们回看一下下图
由上图可知:输入数据的最低位对应的是 Q7 输入数据的最高位对应的是 Q0。我们再回到原理图,那么这样就表示:我们应该先传入位选信号的最低位,然后一直到位选信号的最高位。那我们的段选信号应该怎么传入呢?我们再看一下下面所示这张表格
待显示内容 | 段码(二进制格式) | 段码(十六进制格式) | |||||||
---|---|---|---|---|---|---|---|---|---|
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 |
上面这张表格中,十六进制格式当中的最低位对应的是 a 段,如果说我们直接传入段选信号的最低位,那么输出的时候 a 就从第一片 74HC595 的 Q7端口输出、dp 从 Q0 端口输出,如下图所示
这样显然是不对的,所以说:段选信号我们要先传入最高位,然后再传最低位(按照上面表格段码列所示就是从右到左),如下图所示
那么这个位置可能有点绕,但是大家一定要弄清楚。
刚才我们讲解了串行数据传输的一个顺序,大家一定要搞清楚这个位置
那么下面我们讲一下 74HC595 芯片应该怎么使用。
我们使用 74HC595 芯片的目的是为了节省 I/O 口,它可以节省多少个 I/O 口呢?
如果说我们使用 74HC595 芯片,我们只需要将它的 4 个端口连接到我们的 FPGA 芯片,那么这样 14 − 4 = 10 14-4=10 14−4=10 就节省了 10 个端口。连接到 FPGA 芯片当中的是哪四个端口呢?就是我们的 D S DS DS、 S H C P SHCP SHCP、 S T C P STCP STCP 和 O E ‾ \overline{OE} OE 信号。
那么下面就讲一下我们的 74HC595 芯片应该怎么使用。首先说一下 M R ‾ \overline{MR} MR 复位端
这个端口是主复位端口,引脚名称 M R ‾ \overline{MR} MR 上面有一根横线代表是低电平有效,当它为低电平时它可以将移位寄存器内的数据进行清零通。常我们把它接到 VCC 目的是防止数据的清零。
第二个端口是 D S DS DS 端口,这个端口与我们的 FPGA 相连接,通过这个端口我们将串行数据传入到移位寄存器当中
下面的是 S H C P SHCP SHCP 端口,这个端口是移位寄存器时钟输入,在它的上升沿时将输入的串行数据移入到移位寄存器当中;需要注意的是:它是一个移位寄存器,也就是说当下一个脉冲到来时,上一个脉冲移入的数据就会往下进行移位。如果我们串行数据输入 8 比特数据,那么 8 比特数据输入完之后,第一位输入的数据就会自动移到最后面;如果我们一次输入的数据超过 8 比特,就像我们的六位八段数码管会输入 14 位数据,那么最前面输入的六位数据就会通过 Q7S 端口输出,这个端口与下一个 74HC595 芯片的 D S DS DS 端口相连接,就相当于先前输入的六位数据会输入到下一个 74HC595 芯片当中当
我们 14 位的串行数据都输入到 74HC595 芯片之后怎么控制它的输出呢?
我们的 74HC595 芯片,它的内部有一个八位存储寄存器,它由 S T C P STCP STCP 这个信号控制它,叫存储寄存器时钟;在存储寄存器时钟的上升沿时,74HC595 芯片会将移位寄存器当中的数据写入到我们的存储寄存器当中当
我们的输出使能信号 O E ‾ \overline{OE} OE 为有效的低电平时,74HC595 芯片就将存储寄存器当中的数据通过 Q0、Q1、Q2、……、Q7 这八个端口传输出去这
八个端口就与我们的数码管相连接,这样就完成了串行输入到并行输出的一个转换,这就是我们的 74HC595 芯片。
那么了解了 74HC595 芯片的工作原理之后,我们来看一下它的使用步骤。
首先我们要通过 D S DS DS 端口传入串行数据,然后要产生 S H C P SHCP SHCP 时钟将 D S DS DS 上的数据串行的移入移位寄存器当中,还要产生 S T C P STCP STCP 时钟将移位寄存器里的数据写入存储寄存器,还要产生一个 O E ‾ \overline{OE} OE 信号将存储寄存器的数据通过 Q0、Q1、Q2、……、Q7 这八个端口进行输出。
那么以上内容就是理论部分的全部讲解,那么下面就开始实战演练。
2 实战演练
在实战演练部分,我们会通过一个小实验来实现数码管的静态显示。
2.1 实验目标
首先要明确一下我们的实验目标:我们的实验目标是驱动我们的六位八段数码管实现 000000~FFFFFF 的循环静态显示,每个字符的显示时间是 0.5s
那么了解了实验原理之后,下面开始程序的设计。
2.2 程序设计
首先建立一个文件体系
然后打开 doc 文件夹建立一个 Visio 文件,绘制我们的框图和波形图
那么首先,先来绘制我们的框图。
2.2.3 框图绘制
那么它的输入信号只有时钟信号和复位信号
输出信号应该有四路,连接到我们的 74HC595 芯片。第一路是串行数据、第二路是移位寄存器时钟、第三路是存储寄存器时钟、第四路是输出使能信号
那么模块框图绘制完成之后,我们下面就要考虑:怎么来实现这个功能?
2.2.3.1 模块划分
结合前面学到的层次化设计思想,可以把整个系统划分为两个子功能模块;那么第一个模块用来生成位选和段选信号,第二个模块负责驱动我们的 74HC595 芯片。
2.2.3.2 数码管静态驱动模块框图
那么下面就开始子功能模块框图的绘制。那么首先是产生我们的段选和位选信号,它的输入信号也是时钟信号和复位信号,但是输出信号只有两路:一路是位选信号,它的位宽是 6 位宽,因为有六位;一路是段选信号,位宽是 8 位宽,对应八段
2.2.3.3 74HC595 驱动模块框图
下面就是 74HC595 驱动模块它的框图绘制。它的输入端口除了时钟信号和复位信号之外,还要把 segment_595_static 这个模块产生的位选信号 sel[5:0]
和段选信号 seg[7:0]
输入;通过传入的位选信号和段选信号产生
d
s
ds
ds、
s
h
c
p
shcp
shcp、
s
t
c
p
stcp
stcp、
o
e
‾
\overline{oe}
oe 这四路信号传输到我们的 74HC595 芯片当中
2.2.3.4 系统模块框图
绘制完两个子功能模块的框图之后,下面就开始系统框图的绘制。为什么要进行系统框图的绘制呢?
因为通过系统框图,我们可以了解各个子功能模块之间的层次关系,也可以了解到各个子功能模块的信号传递方向。
那么首先是顶层模块,然后将我们的子功能模块移入其中,这样表示了一个包含与被包含的关系,就是子功能模块包含在顶层模块之中,也表示了两个子功能模块之间的层次关系,它们是同一个层次的
首先看一下输入信号,输入到顶层模块的时钟信号和复位信号要分别传入到两个子功能模块,那么这儿需要用线来连接一下;然后连线交叉位置用点来表示导通
然后将 segment_static 这个子功能模块产生的位选信号和段选信号传入到 74HC595 控制模块当中,最后 74HC595 控制芯片产生的四路输出信号也通过顶层模块传出
那么这样,数码管静态显示的系统框图绘制完成。通过这个系统框图,我们可以明确的知道各模块之间的一个层次关系,而且了解了内部信号的一个走向。
2.2.4 数码管静态驱动模块
2.2.4.1 波形图
那么接下来就分别实现各个模块的功能。首先是产生我们的位选信号和段选信号,我们来绘制一下它的波形图。
首先是输入信号,输入信号只有时钟信号和复位信号
输入信号和输出信号的波形绘制完成。
通过实验目标我们知道:每个字符显示的时间要保持 0.5s,所以说这儿需要一个 0.5s 的计数器。我们声明一个变量 cnt
绘制它的波形图,首先要给它一个初值 0 然后每个时钟周期自加 1 我们的系统时钟是 50MHz 一个时钟周期是 20ns,我们的计数器要从 0 计数到 24_999_999 才能实现 0.5s 的一个计数;当我们的计数器计数到最大值让它归零,开始下一个周期的计数
那么这样,计数器的波形绘制完成。
我们的实验目标要求我们的六位八段数码管要完成 000000~FFFFFF 十六个字符的一个循环显示,这十六个字符刚好就对应十六进制的 0 到 F,那么这儿就声明一个变量 data
来控制字符的切换。它的初值应该是 0 因为是从 0 开始显示,当我们的 0.5s 计数器计数到最大值,也就是说我们的 0 已经完成了 0.5s 的显示就要跳到下一个字符,那么就是 1;当我们的字符 1 完成 0.5s 的显示之后就会跳到下一个字符 2 那么这样依次循环,一直到我们的最后一个字符 F 的显示;当最后一个字符 F 完成了一个 0.5s 的显示之后又会跳到最初的一个字符就是 0 那么这样就实现了一个循环的显示
那么为了控制字符的切换,我们再声明一个信号就是我们的 cnt_flag 信号,cnt_flag 信号初值为低电平,当我们的 0.5s 计数器计数到最大值减一的时候,给它拉高一个时钟周期的高电平,然后我们的字符变量就可以以 cnt_flag 信号为条件进行字符的切换
那么这样 cnt_flag 信号的波形绘制完成。
那么接下来就是输出信号波形的绘制——我们的位选信号和段选信号。我们的要求是六位数码管都要显示,所以说六位数码管都要被选中,我们的输出信号位选信号初值给它一个 0,那么既然要选中所有的数码管,那么它的输出信号就让它保持为全 1 就表示选中所有的数码管
那么接下来就是段选信号,那么段选信号的波形应该怎么变化呢?
当我们输出的字符为 0 时,我们的段选信号就输出 0 对应的十六进制段码 8'hC0
待显示内容 | 段码(二进制格式) | 段码(十六进制格式) | |||||||
---|---|---|---|---|---|---|---|---|---|
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 |
那么这儿因为是时序逻辑,要延迟一个时钟周期;当我们要显示的字符为 1 时,这儿就输出 1 对应的十六进制段码 8’hF9 那么按照这个规律向下绘制
2.2.4.2 RTL 代码
这样,第一个模块的波形图绘制完成。接下来根据这个波形图编写第一个模块的代码
那么首先是我们的模块开始,然后是模块名、端口列表,然后是模块结束
下面是输入信号和输出信号
那么接下来声明变量
那么接下来开始变量的赋值,我们使用 always
语句。那么当我们的复位信号有效时,我们的 0.5s 计数器给一个初值是 0 然后当他计数到最大值,那么最大值这儿来定义一个参数,当它计数到最大值就让我们的计数器归零,如果说这两个条件都不满足让他继续计数
那么下面赋值我们的字符变量。首先复位信号有效时给它一个初值 0 它什么时候进行清零呢?到最大值 F 的时候,而且计数器也计数到最大值进行清零,那么 F 是十六进制,对应十进制是 15,这儿就有两个条件:当我们的计数器也计数到最大值,而且 data
它也计数到最大值 15 就可以进行清零;那么它如何进行字符的切换呢?当 0.5s 计数器计数到最大值就进行字符的一个切换,那么条件就这样写;那么它的切换就是自加 1 那么其他时刻保持原来的值不变
那么然后就是我们的 cnt_flag
信号,同样使用 always
语句。那么初值是低电平也就是 0 那么什么时候保持高电平呢?当我们的计数器计数到最大值减一的时候保持高电平,那么其他时刻一样是 0 我们生成这个 cnt_flag
信号的目的是什么呢?是要切换字符的显示,那么 data
的 always
语句块中 cnt == CNT_MAX
这个条件就可以去掉了,换成我们的 cnt_flag
信号为高电平时
那么这样,中间变量的赋值完成。
下面开始输出信号的赋值。首先是位选信号,同样使用 always 语句,那么信号类型这个位置就要改一下就是 reg 型;给它一个初值就是全是 0 就表示所有位都不选;如果复位信号无效,就选中全部的六位数码管,那么这儿就是全是 1
那么下面就是段选信号的一个赋值。段选信号的赋值,初值让它是 8'bhC0
然后后面我们使用 case
语句;那么当 data 为 0 时,我们输出与之对应的段码就是 8'bhC0
然后为 1 时输出 1 对应的段码;那么按照顺序我们补全,那么最后要加上 default 语句,那么所有的都不满足让它显示 0 那么或者说所有的状态都不满足时我们让它不显示,那么不显示就给它全 1 就可以了
代码编写完成,保存
segment_static.v
module segment_static
#(
parameter CNT_MAX = 25'd24_999_999
)
(
input wire sys_clk ,
input wire sys_rst_n ,
output reg [5:0] sel ,
output reg [7:0] seg
);
reg [24:0] cnt;
reg [3:0] data;
reg cnt_flag;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt <= 25'd0;
else if (cnt == CNT_MAX)
cnt <= 25'd0;
else
cnt <= cnt + 25'd1;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
data <= 4'd0;
else if ((cnt_flag == 1'b1)
&&(data == 4'd15))
data <= 4'd0;
else if (cnt == CNT_MAX)
data <= data + 4'd1;
else
data <= data;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt_flag <= 1'b0;
else if (cnt == (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)
sel <= 6'b000_000;
else
sel <= 6'b111_111;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
seg <= 8'hFF;
else case(data)
4'd0 :seg <= 8'hC0;
4'd1 :seg <= 8'hF9;
4'd2 :seg <= 8'hA4;
4'd3 :seg <= 8'hB0;
4'd4 :seg <= 8'h99;
4'd5 :seg <= 8'h92;
4'd6 :seg <= 8'h82;
4'd7 :seg <= 8'hF8;
4'd8 :seg <= 8'h80;
4'd9 :seg <= 8'h90;
4'd10:seg <= 8'h88;
4'd11:seg <= 8'h83;
4'd12:seg <= 8'hC6;
4'd13:seg <= 8'hA1;
4'd14:seg <= 8'h86;
4'd15:seg <= 8'h8E;
default:seg <= 8'hFF;
endcase
endmodule
那么下面就进行代码的编译。
2.2.4.3 代码编译
我们回到我们的桌面,然后建立一个新的实验工程,然后添加我们编写的代码,然后进行全编译
这儿出现了一个报错信息,我们来看一下
那么这个报错信息提示我们:我们的工程当中不存在一个名称为 segment_595_static 的模块。这是什么意思呢?
我们的工程名称是 segment_595_static 那么它就默认为工程当中应该存在一个相同名称的模块作为顶层模块,那么如果没有顶层模块就不能进行编译。那么这个问题应该怎么解决呢?
有两种方式。那么第一种方式就是:我们重新编写一个顶层模块,将我们的子功能模块实例化在顶层模块当中,对顶层模块进行编译,进而对我们的子功能模块进行编译;那么第二种方式就是在 Quartus II 软件中鼠标右键选中我们的子功能模块 Project Navigator–>Files–>…/rtl/segment_static.v 选择将它强制置为顶层
那么在这里,我们选择第一种方式来解决这个问题:重新编写一个顶层模块,进行进一步的编译。
到了现在,我们只完成了一个子功能模块,另一个子功能模块还没有完成,怎么编写我们的顶层文件呢?
只需要将它直接实例化就可以了,我们来看一下。首先是模块开始,然后是我们的模块名称、端口列表、模块结束
然后我们看一下我们的模块框图。那么框图显示它有两路输入信号和四路输出信号
那么接下来就是实例化。
将我们已经编写完成的 segment_static 模块实例化在我们的顶层文件中。在这个位置将传入顶层的时钟引入,然后引入复位;在这个位置要声明两个变量将位选和段选信号引出来,这儿一定要注意位宽,不要忘记了
这样第一个子功能模块的实例化就已经完成。
那么 d s ds ds、 s h c p shcp shcp、 s t c p stcp stcp、 o e ‾ \overline{oe} oe 这四路输出信号是由第二个子功能模块传出,那么第二个子功能模块还没有进行编写,那么这个位置就先忽略,我们先保存
segment_595_static.v
module segment_595_static
(
input wire sys_clk ,
input wire sys_rst_n ,
output wire ds ,
output wire shcp ,
output wire stcp ,
output wire oe_n
);
wire [5:0] sel;
wire [7:0] seg;
segment_static
#(
.CNT_MAX (25'd24)
)
segment_static_inst
(
.sys_clk (sys_clk),
.sys_rst_n(sys_rst_n),
.sel (sel),
.seg (seg)
);
endmodule
回到我们的工程,添加我们刚刚编写的顶层文件。那么这儿顶层文件虽然没有编写完成,但是应该不会影响我们的编译,我们试一下,进行全编译,编译完成点击 OK
我们来看一下
那么这个位置提示我们 d s ds ds、 s h c p shcp shcp、 s t c p stcp stcp、 o e ‾ \overline{oe} oe 这四路信号还没有驱动,就是因为我们还没有信号输出,我们先忽略。
那么编译通过就表示我们编写的第一个子功能模块,它的代码没有语法错误。
2.2.4.4 逻辑仿真
那么下面就对第一个子功能模块它的代码进行一个仿真。首先建立一个仿真文件
那么下面编写我们的仿真文件
然后声明变量。首先是时钟和复位,然后要引出我们的位选和断选
然后是初始化,生成时钟信号
然后是实例化我们的模块,我们直接可以从 segment_595_static.v 中复制
那么这样,第一个子功能模块它的仿真代码编写完成,我们保存
tb_segment_static.v
`timescale 1ns/1ns
module tb_segment_static();
reg sys_clk;
reg sys_rst_n;
wire [5:0] sel;
wire [7:0] seg;
initial
begin
sys_clk = 1'b1;
sys_rst_n <= 1'b0;
#20
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
segment_static
#(
.CNT_MAX (25'd24)
)
segment_static_inst
(
.sys_clk (sys_clk),
.sys_rst_n(sys_rst_n),
.sel (sel),
.seg (seg)
);
endmodule
回到实验工程,把它进行一个添加,然后全编译;那么编译通过点击 OK
然后是仿真设置
然后开始仿真
仿真编译通过,打开 sim 窗口添加我们的模块波形;打开波形界面,全选、分组、消除这个前缀,然后 Restart;运行 10us
然后接下来参照我们绘制的波形图看一下仿真波形。
那么首先看一下计数器
然后看一下我们的字符
下面看一下 cnt_flag 信号
那么下面就看一下位选和段选信号
那么首先是我们的位选信号,我们看一下
然后是我们的段选信号,我们来看一下
我们的仿真波形与绘制波形图是完全一致的,那么仿真验证通过。
2.2.5 74HC595 驱动模块
那么接下来就绘制第二个功能模块的波形图。
2.2.5.1 波形图
那么首先是输入信号,那么在这个位置我们采用一个简略的画法,因为数据一直在变化,我们这儿不可能完全表示
那么这样,输入信号的波形绘制完成。
下面应该怎么绘制呢?首先我们要声明一个变量,声明一个 14 位宽的变量 data[13:0]
那么声明这个变量的目的是什么呢?拼接一下我们的位选和段选数据,方便后面的传输。
在前面我们已经提到了,我们要按照 {seg[0], seg[1], seg[2], seg[3], seg[4], seg[5], seg[6], seg[7], sel[5:0]}
这个顺序传输我们的数据,所以说在这个位置先按照这个数据顺序进行一下拼接,这儿不要搞错了
那么这样,拼接后的数据它的一个波形就绘制完成了。
那么下面我们声明一个计数器,为什么要声明一个计数器呢?我们要对我们的时钟进行一个分频,为什么要分频呢? s h c p shcp shcp、 s t c p stcp stcp 这两个信号它们是时钟信号,它们的频率不能太高也不能太低,我们进行四分频 50MHz 的四分频就是 12.5Mz 那么这个频率就比较合适。所以说需要一个计数器。
那么计数器的初值给一个 0 然后每个周期加一,那么计数到最大值 3 它就归零;因为是四分频,所以说计数的最大值是 3 就是 0 到 3 计数四次
那么这样,四分频计数器它的波形也绘制完成
接下来我们继续分析:我们的信号
d
s
ds
ds 输出的是串行数据,它 14 个数据为一个循环,这 14 个数据有 6 个位选 8 个段选这是一个循环,那么怎么确保 14 个数据一个循环呢?我们再声明一个计数器 cnt_bit[3:0]
这个计数器对输出的比特进行计数。
首先给他一个初值 0 那他什么时候开始计数呢?我们来看一下 cnt[1:0]
这个计数器,它是四分频计数器,它的每一个计数周期对应的是分频后时钟信号的一个时钟周期;分频后的时钟信号的每一个时钟周期我们要传一个比特位,那么这样就可以确定我们比特计数器它的计数条件就是我们四分频计数器每完成一个周期的计数,我们进行一次计数,那么就这样
当我们的比特计数器计数到最大值 13 就对它进行归零,开始下一个周期的计数。
那么这样,比特计数器的波形绘制完成。
那么下面开始输出信号波形的绘制。首先是我们的
d
s
ds
ds 信号:当我们的复位信号有效时给我们的输出信号
d
s
ds
ds 赋一个初值 0,其他时刻让它等于 data[cnt_bit]
这个数值
这样的话,如果说我们的比特计数器等于 0 时,我们的
d
s
ds
ds 的值就等于变量 data
它的最低位 sel[0]
;如果比特计数器等于 1 时就等于 sel[1]
。那么这样刚好就是按照 这个顺序向外传输的数据,那么这样就满足了要求。
到了这里,输出信号
d
s
ds
ds 的波形已经绘制完成。
那么下面开始移位时钟
s
h
c
p
shcp
shcp 这个信号的波形绘制,那么这个时钟的作用是对输入的
d
s
ds
ds 信号进行移位。为了让我们的时钟信号能够正确的采集到我们的数据,那么时钟信号的上升沿应该对准我们数据信号的稳定状态,也就是中间位置,那么它的波形就应该是这样
首先给它一个初值低电平,然后当我们的计数器 cnt
计数到 2 时给它拉高然后保持它的高电平,当我们的计数器计数到 0 时给它拉低然后保持低电平,到计数值为 2 时再给它拉高,那么按照这个规律绘制它的波形。
那么这样既实现了四分频,又能准确的采集到我们的数据。
移位时钟信号的波形绘制完成,那么下面开始绘制存储寄存器它的时钟波形。那么首先给它赋一个初值电平,当我们所有的 14 位数据都传输完成给它拉高,然后让它保持高电平;cnt 等于 2 时再给它拉低。那么为什么要这样绘制呢?
因为在 cnt 等于 2、cnt_bit 等于 0 这个位置我们的 14 位数据已经传输完成,在这儿拉高将移位寄存器当中的数据输入到我们的存储寄存器,而且不会影响下一次数据的移位
那么这样,存储寄存器它的时钟信号波形绘制完成。
那么下面就是最后一个输出信号 oe_n
输出使能信号,oe_n
信号是低电平有效,当它有效时就把存储寄存器当中的数据并行传出,所以说只要让它保持低电平就好了
那么这样 74HC595 控制模块它的波形图绘制完成。
2.2.5.2 RTL 代码
下面可以根据这个波形图进行代码的编写
hc595_ctrl.v
//模块开始 模块名称 端口列表
module hc595_ctrl
(
input wire sys_clk , //系统时钟,50MHz
input wire sys_rst_n , //系统复位,低电平有效
input wire [5:0] sel , //六位数码管位选
input wire [7:0] seg , //六位数码管段选
//
output reg ds , //输出给74HC595的串行数据
output reg shcp , //移位寄存器时钟
output reg stcp , //存储寄存器时钟
output reg oe_n //74HC595的输出使能,低电平有效
);
// 中间变量
wire [13:0] data ; //位拼接
reg [1:0] cnt ; //分频计数器
reg [3:0] cnt_bit ; //传输比特计数器
//变量赋值
assign data[13:0] = {seg[0],seg[1],seg[2],seg[3],seg[4],seg[5],seg[6],seg[7],sel[5:0]};
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt <= 2'd0;
else if (cnt == 2'd3)
cnt <= 2'd0;
else
cnt <= cnt + 2'd1;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt_bit <= 4'd0;
else if ((cnt==2'd3) && (cnt_bit==4'13))
cnt_bit <= 4'd0;
else if (cnt==2'd3)
cnt_bit <= cnt_bit + 4'd1;
else
cnt_bit <= cnt_bit;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
ds <= 1'b0;
else if (cnt==2'd0)
ds <= data[cnt_bit];
else
ds <= ds;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
shcp <= 1'b0;
else if (cnt==2'd2)
shcp <= 1'b1;
else if (cnt==2'd0)
shcp <= 1'b0;
else
shcp <= shcp;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
stcp <= 1'b0;
else if ((cnt==2'd0) && (cnt_bit==4'0))
stcp <= 1'b1;
else if ((cnt==2'd2) && (cnt_bit==4'0))
stcp <= 1'b0;
else
stcp <= stcp;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
oe_n <= 1'b1;
else
oe_n <= 1'b0;
endmodule //模块结束
然后将它实例化到顶层
segment_595_static.v
module segment_595_static
(
input wire sys_clk ,
input wire sys_rst_n ,
output wire ds ,
output wire shcp ,
output wire stcp ,
output wire oe_n
);
wire [5:0] sel;
wire [7:0] seg;
segment_static
#(
.CNT_MAX (25'd24)
)
segment_static_inst
(
.sys_clk (sys_clk),
.sys_rst_n(sys_rst_n),
.sel (sel),
.seg (seg)
);
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
2.2.5.3 代码编译
那么这样,两个模块都完成了实例化,顶层模块也编写完成,我们保存;然后回到我们的实验工程,添加我们刚刚编写的模块,然后全编译,出现报错提示,点击 OK
鼠标左键双击错误提示,跳转到 hc595_ctrl.v
文件的第 34、62 行查找错误,发现漏写了进制符号,修改并且保存
重新回到实验工程,再次进行编译,还是出现错误提示,点击 OK
查看错误信息,发现还是同样的问题:漏写了进制符号,我们找到位置修改并且保存
hc595_ctrl.v
//模块开始 模块名称 端口列表
module hc595_ctrl
(
input wire sys_clk , //系统时钟,50MHz
input wire sys_rst_n , //系统复位,低电平有效
input wire [5:0] sel , //六位数码管位选
input wire [7:0] seg , //六位数码管段选
//
output reg ds , //输出给74HC595的串行数据
output reg shcp , //移位寄存器时钟
output reg stcp , //存储寄存器时钟
output reg oe_n //74HC595的输出使能,低电平有效
);
// 中间变量
wire [13:0] data ; //位拼接
reg [1:0] cnt ; //分频计数器
reg [3:0] cnt_bit ; //传输比特计数器
//变量赋值
assign data[13:0] = {seg[0],seg[1],seg[2],seg[3],seg[4],seg[5],seg[6],seg[7],sel[5:0]};
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt <= 2'd0;
else if (cnt == 2'd3)
cnt <= 2'd0;
else
cnt <= cnt + 2'd1;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
cnt_bit <= 4'd0;
else if ((cnt==2'd3) && (cnt_bit==4'd13))
cnt_bit <= 4'd0;
else if (cnt==2'd3)
cnt_bit <= cnt_bit + 4'd1;
else
cnt_bit <= cnt_bit;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
ds <= 1'b0;
else if (cnt==2'd0)
ds <= data[cnt_bit];
else
ds <= ds;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
shcp <= 1'b0;
else if (cnt==2'd2)
shcp <= 1'b1;
else if (cnt==2'd0)
shcp <= 1'b0;
else
shcp <= shcp;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
stcp <= 1'b0;
else if ((cnt==2'd0) && (cnt_bit==4'd0))
stcp <= 1'b1;
else if ((cnt==2'd2) && (cnt_bit==4'd0))
stcp <= 1'b0;
else
stcp <= stcp;
always@(posedge sys_clk or negedge sys_rst_n)
if (sys_rst_n == 1'b0)
oe_n <= 1'b1;
else
oe_n <= 1'b0;
endmodule //模块结束
回到实验工程,重新进行编译,编译通过,点击 OK
我们查看一下 RTL 视图,并且将它和我们绘制的系统框图对比一下
可以看到,它和我们的系统框图是一模一样的。
2.2.5.4 逻辑仿真
那么下面编写我们的仿真代码,我们直接对顶层模块进行仿真
tb_segment_595_static.v
`timescale 1ns/1ns
module tb_segment_595_static();
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;
#20
sys_rst_n <= 1'b1;
end
always #10 sys_clk = ~sys_clk;
segment_595_static segment_595_static_inst
(
.sys_clk (sys_clk ),
.sys_rst_n(sys_rst_n),
.ds (ds ),
.shcp (shcp ),
.stcp (stcp ),
.oe_n (oe_n )
);
endmodule
我们保存,然后回到我们的实验工程添加 tb_segment_595_static.v
然后进行全编译。编译通过点击 OK
然后进行仿真设置
开始仿真,仿真编译完成打开 sim 选项卡添加各模块的仿真波形;打开波形界面 全选、分组、消除前缀;然后点击 Restart 时间参数设置为 10us 运行一次。
第一个功能模块的波形我们已经仿真通过,就不再进行查看了。
首先先看一下第二个功能模块,先查看一下我们拼接的数据
位拼接的仿真波形与我们绘制的波形图是一致的,没有问题。
那么下面看一下分频计数器
也是正确的,没有问题。
然后看一下比特计数器
和波形图也是对应的,没有问题。
那么接下来看一下移位时钟信号
和我们绘制的波形图也是一致的。
下面看一下存储寄存器时钟
它的波形变化与我们绘制的波形图是一致的。
那么最后是使能信号 o e ‾ \overline{oe} oe
这样 74HC595 控制模块它的仿真验证通过,两个子功能模块的仿真验证通过;那么顶层模块的仿真波形就不再进行查看了。
2.2.6 管脚绑定
那么下面回到我们的实验工程,绑定我们的管脚:ds–>R1、oe_n–>L11、shcp–>B1、stcp–>K9、sys_clk–>E1、sys_rst_n–>M15
然后回到我们的实验工程,进行重新编译,编完成点击 OK
2.2.7 上板验证
如图所示
连接我们的下载器和我们的电源,下载器另一端连接到电脑,为我们的开发板上电。
回到实验工程,点击 Programmer 这个位置打开下载界面,添加我们的 SOF 文件,点击 Start 下载程序
程序下载完成,但是数码管并没有像我们预想的从 0 到 F 循环显示,一直显示 888888。
原来是我们在顶层模块 segment_595_static 实例化 segment_static 模块时,忘记将 CNT_MAX 这个参数修改回来
我们将 CNT_MAX
修改成 25'd24_999_999
修改后的 segment_595_static.v
:
module segment_595_static
(
input wire sys_clk ,
input wire sys_rst_n ,
output wire ds ,
output wire shcp ,
output wire stcp ,
output wire oe_n
);
wire [5:0] sel;
wire [7:0] seg;
segment_static
#(
.CNT_MAX (25'd24_999_999)
)
segment_static_inst
(
.sys_clk (sys_clk),
.sys_rst_n(sys_rst_n),
.sel (sel),
.seg (seg)
);
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
保存代码,重新编译、下载
现在就可以看到我们的数码管是从 0 到 F 循环显示。
那么这样,上板验证成功。
参考资料: