基础设计一——FPGA学习笔记<2>

目录

零.设计流程

一.按键点亮LED灯

1.硬件资源

 2.项目设计

 3.波形设计

 4.创建Vivado工程​编辑

<1>添加设计文件

<2>添加仿真文件

5.引脚约束

6.生成比特流文件

7.下载验证

8.程序固化

二.多路选择器

1.实现方法

<1>always 中 if-else 实现方法

 <2>always 中 case 实现方法​编辑

 <3>assign 中条件运算符(三元运算符)实现方法

2.仿真

三.译码器

1.实现方法

2.仿真

四.半加器

1.实现

2.仿真

五.层次化设计

1.全加器

六.避免Latch的产生

七.寄存器

1.同步复位 D 触发器和异步复位 D 触发器

<1>同步复位 D 触发器

<2>异步复位 D 触发器

2.仿真

八.阻塞赋值与非阻塞赋值

九.计数器

1.实现方法

2.仿真

十.分频器

1.偶分频

2.奇分频

十一.按键消抖

1.硬件消抖

2.软件消抖

十二.流水灯

1.实现

2.仿真

十三.状态机

1.简易可乐机

2.复杂可乐机

十四.无源蜂鸣器

十五.动态数码管显示

1.实现

<1>data_gen

 <2>bcd_8421

<3> seg_dynamic

 <4>top_seg_dynamic

2.仿真


前置学习:

verilog语法——FPGA学习笔记<1>

FPGA基本概念及资源整理——FPGA学习笔记<0>

 参考书目:《野火FPGA Verilog 开发实战指南》

零.设计流程

基础设计一——FPGA学习笔记<2>_第1张图片

基础设计一——FPGA学习笔记<2>_第2张图片

熟悉Vivado界面参考:4.1 Vivado使用技巧(1):了解主界面icon-default.png?t=N7T8https://blog.csdn.net/L20902/article/details/86542768

[走近FPGA]之工具篇(上)-Vivadoicon-default.png?t=N7T8https://zhuanlan.zhihu.com/p/152589392

一.按键点亮LED灯

1.硬件资源

基础设计一——FPGA学习笔记<2>_第3张图片

 2.项目设计

①项目分解成模块;②模块设计:模块、输入信号、输出信号

基础设计一——FPGA学习笔记<2>_第4张图片

 3.波形设计

信号之间的逻辑或时序上关系,众多信号使用波形图表达(注意信号间规范区分,比如绿色输入信号,黄色之间变量,红色输出变量等等)

基础设计一——FPGA学习笔记<2>_第5张图片

 4.创建Vivado工程基础设计一——FPGA学习笔记<2>_第6张图片

<1>添加设计文件

基础设计一——FPGA学习笔记<2>_第7张图片

 文件会复制到xxx.srcs文件夹内;创建文件路径也在此文件夹中;复制选项上面的选项指复制搜寻并复制子模块RTL文件

示例RTL代码:

`timescale  1ns/1ns
//
// Company: 
// Engineer: 
// 
// Create Date: 2023/08/16 22:03:43
// Design Name: 
// Module Name: led_creat
// Project Name: 
// Target Devices: 
// Tool Versions: 
// Description: 
// 
// Dependencies: 
// 
// Revision:
// Revision 0.01 - File Created
// Additional Comments:
// 
//

module  led
(
    input   wire    key_in  ,   //输入按键

    output  wire    led_out     //输出控制led
);

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//led_out:led灯输出的结果为key_in按键取反的输入
assign  led_out  =  ~key_in;

endmodule

<2>添加仿真文件

逻辑仿真时使用的Testbench脚本也是用HDL语言写的,其原理如下:

第一步创建Testbench脚本,名字通常为被测试模块前加tb_

基础设计一——FPGA学习笔记<2>_第8张图片

基础设计一——FPGA学习笔记<2>_第9张图片

代码示例:

`timescale  1ns/1ns

module  tb_led();

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//wire  define
wire    led_out ;

//reg   define
reg     key_in  ;

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//初始化输入信号
initial key_in <= 1'b0;

//key_in:产生输入随机数,模拟按键的输入情况
always #10 key_in <= {$random} % 2; /*取模求余数,产生非负随机数0、1
                                      每隔10ns产生一次随机数*/

//********************************************************************//
//**************************** Instantiate ***************************//
//********************************************************************//
//------------- led_inst -------------
led led_inst
(
    .key_in (key_in ),  //input     key_in

    .led_out(led_out)   //output    led_out
);

endmodule

        这里定义的变量类型如果要手动赋值仿真,使用reg变量;如果是输出则用使用wire变量引出,理解变量的含义即可。

然后运行仿真即可,可选择Vivado仿真或Modelsim联合仿真。仿真过程类似Quartus II

基础设计一——FPGA学习笔记<2>_第10张图片

此处可设定仿真参数

基础设计一——FPGA学习笔记<2>_第11张图片基础设计一——FPGA学习笔记<2>_第12张图片

基础设计一——FPGA学习笔记<2>_第13张图片

5.引脚约束

<1>方式一——RTL ANALYSIS

基础设计一——FPGA学习笔记<2>_第14张图片

基础设计一——FPGA学习笔记<2>_第15张图片

基础设计一——FPGA学习笔记<2>_第16张图片

注:相关文件信息及更改在中间框中如下

基础设计一——FPGA学习笔记<2>_第17张图片

 <2>方式二——直接编写并添加.xdc连接文件

注:.xdc文件使用#注释

6.生成比特流文件

基础设计一——FPGA学习笔记<2>_第18张图片

7.下载验证

 注意事项

基础设计一——FPGA学习笔记<2>_第19张图片

即先将JTAG连接至FPGA,然后打开FPGA电源,然后JTAG链接电脑

基础设计一——FPGA学习笔记<2>_第20张图片

基础设计一——FPGA学习笔记<2>_第21张图片

基础设计一——FPGA学习笔记<2>_第22张图片

基础设计一——FPGA学习笔记<2>_第23张图片

可以看到生成的bit文件保存在.runs文件夹中

8.程序固化

首先在.xdc连接文件添加以下代码,目的是使用spi4加速(如果不添加后续只能使用spi1)

基础设计一——FPGA学习笔记<2>_第24张图片

set_property CFGBVS VCCO [current_design]
set_property CONFIG_VOLTAGE 3.3 [current_design]
set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 4 [current_design]
set_property CONFIG_MODE SPIx4 [current_design]
set_property BITSTREAM.CONFIG.CONFIGRATE 50 [current_design]

参考:XILINX 7系列配置4线SPI_spi_setconfig

MSC文件是xilinx的二进制文件

基础设计一——FPGA学习笔记<2>_第25张图片

基础设计一——FPGA学习笔记<2>_第26张图片

基础设计一——FPGA学习笔记<2>_第27张图片

基础设计一——FPGA学习笔记<2>_第28张图片

基础设计一——FPGA学习笔记<2>_第29张图片

 进程两步操作:擦除FLASH芯片、写入FLASH芯片;

基础设计一——FPGA学习笔记<2>_第30张图片

生成bin文件之后,右键之前生成的存储器配置文件,更改“programme file”为生成的bin文件如下

基础设计一——FPGA学习笔记<2>_第31张图片

基础设计一——FPGA学习笔记<2>_第32张图片

 之后便可以在这里进行程序烧录

最终简化流程为“Generate BItstrean”->“Program Configuration Memory Device”

基础设计一——FPGA学习笔记<2>_第33张图片

二.多路选择器

1.实现方法

<1>always 中 if-else 实现方法

基础设计一——FPGA学习笔记<2>_第34张图片

基础设计一——FPGA学习笔记<2>_第35张图片基础设计一——FPGA学习笔记<2>_第36张图片

 <2>always 中 case 实现方法基础设计一——FPGA学习笔记<2>_第37张图片

 <3>assign 中条件运算符(三元运算符)实现方法

基础设计一——FPGA学习笔记<2>_第38张图片

 注:assign和always@(*)区别

2.仿真

基础设计一——FPGA学习笔记<2>_第39张图片

基础设计一——FPGA学习笔记<2>_第40张图片

基础设计一——FPGA学习笔记<2>_第41张图片

基础设计一——FPGA学习笔记<2>_第42张图片

三.译码器

1.实现方法

基础设计一——FPGA学习笔记<2>_第43张图片

基础设计一——FPGA学习笔记<2>_第44张图片

基础设计一——FPGA学习笔记<2>_第45张图片

2.仿真

基础设计一——FPGA学习笔记<2>_第46张图片

基础设计一——FPGA学习笔记<2>_第47张图片

四.半加器

1.实现

基础设计一——FPGA学习笔记<2>_第48张图片

2.仿真

基础设计一——FPGA学习笔记<2>_第49张图片

基础设计一——FPGA学习笔记<2>_第50张图片

五.层次化设计

        自上而下设计:自上而下的设计是从系统级开始把系统分为基本单元,然后再把每个单元划分为下 一层次的基本单元,一直这样做下去,直到直接可以用 EDA 元件库中的原件来实现为止。在自顶向下设计方法中,我们首        先定义顶层功能块,进而分析需要哪些构成顶层模块的必 要子模块;然后进一步对各个子模块进行分解,直到到达无法进一步分解的底层功能块。 

        自下而上设计:自底向上的设计是一种传统的设计方法,对设计进行逐次划分的过程是从存在的基本单元出发的,设计树最末枝上的单元要么是已经构造出的单元,要么是其他 项目开发好的单元或者是可外购得到的单元。在自底向上建模方法中,我们首先对现有的功能块进行分 析,然后使用这些模块来搭建规模大一些的功能块,如此继续直至顶层模块

基础设计一——FPGA学习笔记<2>_第51张图片

1.全加器

<1>实现

基础设计一——FPGA学习笔记<2>_第52张图片

基础设计一——FPGA学习笔记<2>_第53张图片

基础设计一——FPGA学习笔记<2>_第54张图片

基础设计一——FPGA学习笔记<2>_第55张图片

基础设计一——FPGA学习笔记<2>_第56张图片

六.避免Latch的产生

        Latch 其实就是锁存器,是一种在异步电路系统中,对输入信号电平敏感的单元,用来存储信息。锁存器在数据未锁存时,输出端的信号随输入信号变化,就像信号通过一个缓冲器,一旦锁存信号有效,则数据被锁存,输入信号不起作用。因此,锁存器也被称为透明锁存器,指的是不锁存时输出对于输入是透明的。

        在同步电路中 Latch 会产生不好的效果,如对毛刺敏感;不能异步复位,上电后处于不定态;还会让静态时序分析变得十分复杂;在 FPGA 的资源中,大部分器件没有锁存器 这个东西,所以需要用使用寄存器来组成锁存器所以会占用更多逻辑资源;在 ASIC 设计 中,锁存器也会带来额外的延时和 DFT,并不利于提高系统的工作频率,所以要避免产 生。在这里我们把会产生组合逻辑的几种情况列举出来,希望大家以后能够避免出现类似 的问题。

基础设计一——FPGA学习笔记<2>_第57张图片

七.寄存器

        时序逻辑最基本的单元就是寄存器, 寄存器具有存储功能,一般是由 D 触发器构成,由时钟脉冲控制,每个 D 触发器(D Flip Flop ,DFF)能够存储一位二进制码。

        我们先给模块取一个名字叫 flip_flop,接下来是分析端口信号:D 触发器能够正常工 作一定有时钟,每当时钟的“沿(上升沿或下降沿)”来到时我们采集到稳定有效的数 据;其次还需要的就是复位信号,用于让触发器的回到初始状态把数据清零;因为是用按 键控制 led 灯的亮灭,所以输入端我们还需要一个按键控制信号;输出就只有一个控制 led 灯的信号,这里我们的输入输出信号都是 1bit 的。

基础设计一——FPGA学习笔记<2>_第58张图片

1.同步复位 D 触发器和异步复位 D 触发器

基础设计一——FPGA学习笔记<2>_第59张图片基础设计一——FPGA学习笔记<2>_第60张图片

        因为时 序电路只有在沿到来时才检测信号是否有效,所以在两个上升沿之间的毛刺都会被自然的 过滤掉,可以大大减少毛刺现象产生的干扰,提高了电路中数据的可靠性。

        时序电路还有一个特点,就是“延一拍”的效果。上面两个图最左边的一组红色竖线所表达的就是这个现象。key_in 在复位后的第一个时钟的上升沿来到时拉高,我们可以发 现此时 led_out 并没有在同一时刻也跟着拉高,而在之前的组合逻辑中输出是在输入变化的 同一时刻立刻变化的

        因为我们所画的波形图都是基于前仿真的,没有加入门延时的信息,所以很多时候数 据的变化都是和时钟直接对齐的。当表达时序逻辑时如果时钟和数据是对齐的,则默认当 前时钟沿采集到的数据为在该时钟上升沿前一时刻的值;当表达组合逻辑时如果时钟和数据是对齐的,则默认当前时钟沿采集到的数据为在该时钟上升沿同一时刻的值。而仿真工 具在进行 RTL 代码的仿真时也遵循这个规则,我们也可以理解为仿真寄存器是按照建立时间 Tsu(指触发器的时钟信号上升沿到来以前,数据稳定不变的最小时间)最大(一个时 钟周期),保持时间 Th(指触发器的时钟信号上升沿到来以后,数据稳定不变的最小时 间)最小(为 0)的理想环境下进行的;而在仿真组合逻辑时因为没有时钟也就没有建立时间和保持时间的概念,所以数据只要有变化就立刻有效。这里我们在画波形图的时候一 定要记住这个“延一拍”的效果,否则我们绘制的波形图就会和最后的仿真结果不符,也 可能会导致最后的逻辑混乱。

<1>同步复位 D 触发器

基础设计一——FPGA学习笔记<2>_第61张图片

基础设计一——FPGA学习笔记<2>_第62张图片

<2>异步复位 D 触发器

基础设计一——FPGA学习笔记<2>_第63张图片

基础设计一——FPGA学习笔记<2>_第64张图片

2.仿真

基础设计一——FPGA学习笔记<2>_第65张图片

基础设计一——FPGA学习笔记<2>_第66张图片

        begin...end 是一个串行块在 Testbench 中被使用时其内部的语句是顺序执行的,在本例中,我们多次进行延时,其时间是在之前基础上叠加的,而不是从 0 时 刻开始计算时间。

八.阻塞赋值与非阻塞赋值

        阻塞赋值的赋值号用“=”表示。对应的电路结构往往与触发沿没有关系,只与输入电平的变化有关系。阻塞赋值的操作可以认为 是只有一个步骤的操作,即计算赋值号右边的语句并更新赋值号左边的语句,此时不允许 有来自任何其他 Verilog 语句的干扰,直到现行的赋值完成时刻,即把当前赋值号右边的值 赋值给左边的时刻完成后,它才允许下一条的赋值语句的执行。串行块(begin-end)中的各条阻塞型过程赋值语句将以它们在顺序块后的排列次序依次执行。阻塞型过程赋值语句的执行过程是:首先计算赋值号右边的值,然后立即将计算结果赋值给左边,赋值语句结束,变量值立即发生改变。阻塞的概念是指在同一个 always 块中,其后面的赋值语句从概念上是在前一句赋值语句结束后再开始下面的赋值。

       非阻塞赋值的赋值号用“<=”表示。对应的电路结构往往与触发沿有关系,只有在触发沿的时刻才能进行非阻塞赋值。非阻塞操作开始时计算非阻塞赋值符的赋值号右边的语句,赋值操作结束时刻才更新赋值号左 边的语句,可以认为是两个步骤(赋值开始时刻和结束时刻)来完成非阻塞赋值。在计算 非阻塞语句赋值号右边的语句和更新赋值号左边的语句期间,其他的 Verilog 语句包括其他 的 Verilog 非阻塞赋值语句都能同时计算赋值号右边的语句和更新赋值号左边的语句,允许 其他的 Verilog 语句同时进行操作。非阻塞赋值的操作可以看作为两个步骤的过程:在赋值开始时刻,计算赋值号右边的语句。在赋值结束时刻,更新赋值号左边的语句。注意:非阻塞操作只能用于对寄存器类型变量进行赋值,因此只能用于“initial”和“always”块 中,不允许用于连续赋值“assign”

        在描述逻辑电路时使用阻塞赋值,在描述时序逻辑电路时要用非阻塞赋值,这也 是官方的推荐写法。

基础设计一——FPGA学习笔记<2>_第67张图片

九.计数器

1.实现方法

基础设计一——FPGA学习笔记<2>_第68张图片基础设计一——FPGA学习笔记<2>_第69张图片

基础设计一——FPGA学习笔记<2>_第70张图片

基础设计一——FPGA学习笔记<2>_第71张图片

基础设计一——FPGA学习笔记<2>_第72张图片

基础设计一——FPGA学习笔记<2>_第73张图片

        原理图可以看出由触发器组成的状态机,累加器,选择器等

        通过对比我们可以发现第一种实现方式用了 2 个 always 块,其 RTL 视图分别对应两组 触发器,而第二种实现方式用了 3 个 always 块,其 RTL 视图分别对应了 3 组触发器

2.仿真

基础设计一——FPGA学习笔记<2>_第74张图片

基础设计一——FPGA学习笔记<2>_第75张图片

十.分频器

        无论分频和倍频,我们都有两种方式可以选择,一种是器件厂商提供的锁相环(PLL,后面章节会讲解),另一种是自己动手来用 Verilog 代码描述。用 Verilog 代码描述的往往是分频电路,即分频器。

1.偶分频

方法 1 实现:仅实现分频功能

基础设计一——FPGA学习笔记<2>_第76张图片

基础设计一——FPGA学习笔记<2>_第77张图片

基础设计一——FPGA学习笔记<2>_第78张图片

方法 2 实现:实用的降频方法

方法一中的 clk_out 输出信号是我们想要的分频后的信号,然后很多同学就直接把这个 信号当作新的低频时钟来使用,并实现了自己想要的功能。大家肯定会觉得能够实现功能 就一切 OK 了,而往往忽略了一些隐患的存在,如果你对 FPGA 的了解多一些就会理解其 实这是不严谨的做法,这种做法所衍生的潜在问题在低速系统中不易察觉,而在高速系统 中就很容易出现问题。因为我们通过这种方式分频得到的时钟虽然表面上是对系统时钟进 行了分频产生了一个新的低频时钟,但实际上和真正的时钟信号还是有很大区别的。因为在 FPGA 中凡是时钟信号都要连接到全局时钟网络上,全局时钟网络也称为全局时钟树, 是 FPGA 厂商专为时钟路径而特殊设计的,它能够使时钟信号到达每个寄存器的时间都尽 可能相同,以保证更低的时钟偏斜(Skew)和抖动(Jitter)。而我们用这种分频的方式产 生的 clk_out 信号并没有连接到全局时钟网络上,但 sys_clk 则是由外部晶振直接通过管脚 连接到了 FPGA 的专用时钟管脚上,自然就会连接到全局时钟网络上,所以在 sys_clk 时钟 工作下的信号要比在 clk_out 时钟工作下的信号更容易在高速系统中保持稳定,既然发现了 问题那我们该怎么办呢?这时可不要忘记了上一章中刚学到的 flag 标志信号,这里我们就 可以用上了,我们可以产生一个用于标记 6 分频的 clk_flag 标志信号,这样每两 clk_flag 脉 冲之间的频率就是对 sys_clk 时钟信号的 6 分频,但是计数器计数的个数我们需增加一些, 如图 17-4 所示需要从 0~5 共 6 个数,否则不能实现 6 分频的功能。和方法 1 对比可以发 现,相当于把 clk_out 的上升沿信号变成了 clk_flag 的脉冲电平信号(和上一章方法 2 中的 cnt_flag 是一样的道理),为后级模块实现相同的降频效果。虽然这样会多使用一些寄存器资源,不过不用担心我们的系统是完全可以承担的起的,而得到的好处却远远大于这点资源的使用,能让系统更加稳定。基础设计一——FPGA学习笔记<2>_第79张图片

基础设计一——FPGA学习笔记<2>_第80张图片

基础设计一——FPGA学习笔记<2>_第81张图片

2.奇分频

<1>方法一:分频(两结果或运算)

这里 clk1 和 clk2 都是低电平 2 个时钟周期,高电平 3 个时钟周期,clk1 和 clk2 相与的结果就是 clk_out 的波形。

基础设计一——FPGA学习笔记<2>_第82张图片

基础设计一——FPGA学习笔记<2>_第83张图片

基础设计一——FPGA学习笔记<2>_第84张图片

<2>方法二:降频(同前)

<3>后面的 IP 核章节还会讲解到通过 PLL(Phase Locked Loop,即锁相环)的方法来实现对时钟的任意分频、倍频、相位 移动。

十一.按键消抖

1.硬件消抖

        图中两个与非门构成一个 RS 触发器。当按键未按下时,输出为 0;当键按下时, 输出为 1。此时即使用按键的机械性能,使按键因弹性抖动而产生瞬时断开(抖动跳开 B), 只要按键不返回原始状态 A,双稳态电路的状态不改变,输出保持为 0,不会产生抖动的波形。也就是说,即使 B 点的电压波形是抖动的,但经双稳态电路之后,其输出为正规的矩形波。

基础设计一——FPGA学习笔记<2>_第85张图片

2.软件消抖

基础设计一——FPGA学习笔记<2>_第86张图片

基础设计一——FPGA学习笔记<2>_第87张图片

基础设计一——FPGA学习笔记<2>_第88张图片

基础设计一——FPGA学习笔记<2>_第89张图片

基础设计一——FPGA学习笔记<2>_第90张图片

十二.流水灯

1.实现

基础设计一——FPGA学习笔记<2>_第91张图片

三个计数器、cnt_en使led由暗到亮再由亮到暗

基础设计一——FPGA学习笔记<2>_第92张图片

基础设计一——FPGA学习笔记<2>_第93张图片

基础设计一——FPGA学习笔记<2>_第94张图片

可用异或简化判断语句

2.仿真

基础设计一——FPGA学习笔记<2>_第95张图片

减小三个MAX的值方便仿真观察

十三.状态机

        状态机(FSM,Finite State Machine),若最后的输出只和当前状态有关而与输入无关则称为 Moore 型状态机; 若最后的输出不仅和当前状态有关还和输入有关则称为 Mealy 型状态机。

        最原始的状态我们称之为 IDLE 状态

1.简易可乐机

基础设计一——FPGA学习笔记<2>_第96张图片

基础设计一——FPGA学习笔记<2>_第97张图片

        上面是一个用 Verilog 描述的简单状态机,我们可以发现它是按照我们总结好的一套格 式来编写的,我们按照这种格式再结合状态转移图可以编写出更复杂的状态机代码,所以 我们总结一下我们套用的格式有哪些主要部分构成:

其中 01-09 行是端口列表部分;

17-19 行是状态编码部;

22 行是定义的状态变量;

29-49 行是第一段状态机部分;

52-58 是第二段 状态机部分。

        一共有五部分,我们写状态机代码的时候根据这 5 部分对照着状态机依次编写,非常容易的就可以实现。 第一部分:第一部分是端口列表,和之前的设计一样没有什么特殊之处。 第二部分、第三部分:第二部分是状态编码,第三部分是状态变量,这两个是有联系 的,所以放到一起讲解。17-19 行是状态编码,状态转移图中有多少个状态数就需要有多少 个状态编码,这里一共有 3 个状态数,所以就需要 3 个状态编码。22 行是状态变量,这里 为什么状态变量的位宽是 3 呢?因为我们采用了独热码的编码方式,每个状态数只有 1 比 特为 1,其余比特都为 0,所以 3 个状态就要用 3 位宽的变量,如果是 4 个状态那就要用 4 位宽的变量,也就是一共有几个状态数就需要几位宽的状态变量。那么除了用独热码的方 式对状态进行编码,还有其他的方法吗?当然有,我们还可以采用二进制码或格雷码的方式对状态进行编码,上面的例子中如果我们用二进制码编码 3 个状态则为:2’b00, 2’b01,2’b10;而用格雷码编码 3 个状态则为:2’b00,2’b01,2’b11,都只需要 2 位宽的状态变量即可,即便是有 4 个状态数,我们使用 2 位宽的状态变量依然可以解决问题,要比独热码更节省状态变量的位宽个数。

        为什么例子中我们使用的是独热码而非二进制码或格雷码呢?那就要从每种编码的特性上说起了,首先独热码因为每个状态只有 1bit 是不同的,所以在执行到 55 行时的(state == TWO)这条语句时,综合器会识别出这是一个比较器,而因为只有 1 比特为 1,所以综合器会进行智能优化为(state[2] == 1’b1),这就相当于把之前 3 比特的比较器变为了 1 比特的比较器,大大节省了组合逻辑资源,但是付出的代价就是状态变量的位宽需要的比较多,而我们 FPGA 中组合逻辑资源相对较少,所以比较宝贵,而寄存器资源较多,所以很完美。而二进制编码的情况和独热码刚好相反,他因为使用了较少的状态变量,使之在减 少了寄存器状态的同时无法进行比较器部分的优化,所以使用的寄存器资源较少,而使用 的组合逻辑资源较多,我们还知道 CPLD 就是一个组合逻辑资源多而寄存器逻辑资源少的器件,因为这里我们使用的是 FPGA 器件,所以使用独热码进行编码。就因为这个比较部分的优化,还使得使用独热码编码的状态机可以在高速系统上运行,其原因是多比特的比 较器每个比特到达比较器的时间可能会因为布局布线的走线长短而导致路径延时的不同, 这样在高速系统下,就会导致采集到不稳定的状态,导致比较后的结果产生一个时钟的毛刺,使输出不稳定,而单比特的比较器就不用考虑这种问题。下面是示意图解析。

基础设计一——FPGA学习笔记<2>_第98张图片

        用独热码编码虽然好处多多,但是如果状态数非常多的话即使是 FPGA 也吃不消独热码对寄存器的消耗,所以当状态数特别多的时候可以使用格雷码对状态进行编码。格雷码虽然也是和二进制编码一样使用的寄存器资源少,组合逻辑资源多,但是其相邻状态转换 时只有一个状态发生翻转,这样不仅能消除状态转换时由多条信号线的传输延迟所造成的毛刺,又可以降低功耗,所以要优于二进制码的方式,相当于是独热码和二进制编码的折中。

基础设计一——FPGA学习笔记<2>_第99张图片

基础设计一——FPGA学习笔记<2>_第100张图片

基础设计一——FPGA学习笔记<2>_第101张图片

基础设计一——FPGA学习笔记<2>_第102张图片

        第 40 行重新定义了一个 2bit 名为 state 的变量,然后通过在 Testbench 模块中实例化 RTL 模块的名字与“.”定位到 RTL 模块中的信号,如果要引入到 Testbench 模块中的信号是 RTL 模块多层实例化中最底层的信号则需要从顶层的实例化 RTL 模块的名字与“.”依次传递,直到最后定位到内部的信号。这样我们就把RTL 模块中的内部信号引入到 Testbench 模块中了。之所以这样做是因为我们要在 ModelSim 的 “Transcript”界面中打印 RTL 模块中内部信号的信息以方便观察验证,直接实例化 RTL 模块的方式只能够将 RTL 模块中的端口信号引入到 Testbench 模块中,而不能将 RTL 模块 的内部信号引入到 Testbench 模块中,所以无法在 ModelSim 的“Transcript”界面中观察打印的信息。

2.复杂可乐机

        输入信号除了可以投 1 元外,还可以投 0.5 元,一次只投一个币;可乐机的输出除了可乐还可能会有找零(找零的结果只有一种即找回 0.5 元),我们将可乐机输出购买可乐的信号取名为 po_cola,找零的信号取名为 po_money。

基础设计一——FPGA学习笔记<2>_第103张图片

基础设计一——FPGA学习笔记<2>_第104张图片

        大家在画状态转移图时容易出现状态跳转情况遗漏的问题,这里我们给大家总结一个 小技巧:我们可以观察到,输入有多少种情况(上一节是两种输入情况,本节是三种输入 情况),每个状态的跳转就有多少种情况(上一节每个状态都有两种跳转情况。这样根据输入来确定状态的跳转就能够保证我们不漏掉任何一 种状态跳转。

`timescale  1ns/1ns

module  complex_fsm
(
    input   wire    sys_clk         ,   //系统时钟50MHz
    input   wire    sys_rst_n       ,   //全局复位
    input   wire    pi_money_one    ,   //投币1元
    input   wire    pi_money_half   ,   //投币0.5元
                    
    output  reg     po_money        ,   //po_money为1时表示找零
                                        //po_money为0时表示不找零
    output  reg     po_cola             //po_cola为1时出可乐
                                        //po_cola为0时不出可乐
);

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//

//parameter define
//只有五种状态,使用独热码
parameter   IDLE     = 5'b00001;
parameter   HALF     = 5'b00010;
parameter   ONE      = 5'b00100;
parameter   ONE_HALF = 5'b01000;
parameter   TWO      = 5'b10000;

//reg   define
reg     [4:0]   state;

//wire  define
wire    [1:0]   pi_money;

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//pi_money:为了减少变量的个数,我们用位拼接把输入的两个1bit信号拼接成1个2bit信号
//投币方式可以为:不投币(00)、投0.5元(01)、投1元(10),每次只投一个币
assign pi_money = {pi_money_one, pi_money_half};

//第一段状态机,描述当前状态state如何根据输入跳转到下一状态
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        state <= IDLE;  //任何情况下只要按复位就回到初始状态
    else	case(state)
                IDLE    : if(pi_money == 2'b01)   //判断一种输入情况
                              state <= HALF;
                          else    if(pi_money == 2'b10)//判断另一种输入情况
                              state <= ONE;
                          else
                              state <= IDLE;
    
                HALF    : if(pi_money == 2'b01)
                              state <= ONE;
                          else    if(pi_money == 2'b10)
                              state <= ONE_HALF;
                          else
                              state <= HALF;
    
                ONE     : if(pi_money == 2'b01)
                              state <= ONE_HALF;
                          else    if(pi_money == 2'b10)
                              state <= TWO;
                          else
                              state <= ONE;
    
                ONE_HALF: if(pi_money == 2'b01)
                              state <= TWO;
                          else    if(pi_money == 2'b10)
                              state <= IDLE;
                          else
                              state <= ONE_HALF;
    
                TWO     : if((pi_money == 2'b01) || (pi_money == 2'b10))
                              state <= IDLE;
                          else
                              state <= TWO;
        //如果状态机跳转到编码的状态之外也回到初始状态
                default :       state <= IDLE;
            endcase

//第二段状态机,描述当前状态state和输入pi_money如何影响po_cola输出
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        po_cola <= 1'b0;
    else    if((state == TWO && pi_money == 2'b01) || (state == TWO && 
          pi_money == 2'b10) || (state == ONE_HALF && pi_money == 2'b10))
        po_cola <= 1'b1;
    else
        po_cola <= 1'b0;

//第二段状态机,描述当前状态state和输入pi_money如何影响po_money输出
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n ==	1'b0)
        po_money <= 1'b0;
    else if((state == TWO) && (pi_money == 2'b10))
        po_money <= 1'b1;
    else
        po_money <= 1'b0;

endmodule
`timescale  1ns/1ns

module  tb_complex_fsm();

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//

//reg   define
reg         sys_clk;
reg         sys_rst_n;
reg         pi_money_one;
reg         pi_money_half;
reg         random_data_gen;

//wire  define
wire        po_cola;
wire        po_money;

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//初始化系统时钟、全局复位
initial begin
    sys_clk    = 1'b1;
    sys_rst_n <= 1'b0;
    #20
    sys_rst_n <= 1'b1;
end

//sys_clk:模拟系统时钟,每10ns电平翻转一次,周期为20ns,频率为50MHz
always  #10 sys_clk = ~sys_clk;

//random_data_gen:产生非负随机数0、1
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        random_data_gen <= 1'b0;
    else
        random_data_gen <= {$random} % 2;

//pi_money_one:模拟投入1元的情况
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        pi_money_one <= 1'b0;
    else
        pi_money_one <= random_data_gen;

//pi_money_half:模拟投入0.5元的情况
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        pi_money_half <= 1'b0;
    else
    //取反是因为一次只能投一个币,即pi_money_one和pi_money_half不能同时为1
        pi_money_half <= ~random_data_gen;

//------------------------------------------------------------
//将RTL模块中的内部信号引入到Testbench模块中进行观察
wire    [4:0]   state    = complex_fsm_inst.state;
wire    [1:0]   pi_money = complex_fsm_inst.pi_money;

initial begin
    $timeformat(-9, 0, "ns", 6);
    $monitor("@time %t: pi_money_one=%b pi_money_half=%b pi_money=%b state=%b po_cola=%b po_money=%b", $time, pi_money_one, pi_money_half, pi_money, state, po_cola, po_money);
end
//------------------------------------------------------------

//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//

//------------------------complex_fsm_inst------------------------
complex_fsm complex_fsm_inst(
    .sys_clk        (sys_clk        ),  //input     sys_clk
    .sys_rst_n      (sys_rst_n      ),  //input     sys_rst_n
    .pi_money_one   (pi_money_one   ),  //input     pi_money_one
    .pi_money_half  (pi_money_half  ),  //input     pi_money_half
                    
    .po_cola        (po_cola        ),  //output    po_money
    .po_money       (po_money       )   //output    po_cola
);  

endmodule

十四.无源蜂鸣器

        蜂鸣器按其是否带有震荡源又分为有源蜂鸣器和无源蜂鸣器。有源蜂鸣器的内部装有 集成电路,不需要音频驱动电路,只需要接通直流电源就能直接发出声响。而无源蜂鸣器 只有外加音频驱动信号才能发出声响。

基础设计一——FPGA学习笔记<2>_第105张图片

        这里占空比我们保持为 50%。cnt_500ms:该信号我们定义为蜂鸣器的鸣叫状态计数。由于我们需鸣叫七个音调,所 以我们需要计 7 个数(0~6),而每个音调的鸣叫时间即计数值的持续时间。所以这里我们 需要用一个鸣叫持续时间计数器去控制蜂鸣器各音调的鸣叫持续时间。 cnt:蜂鸣器各音调鸣叫持续时间计数器。在“计数器”章节我们详细的介绍了该如何 用计数器进行时间计数,这里就不详细的介绍了。本次实验我们设计让每个音调持续鸣叫 0.5s,故这里我们计数到 24999999(0.5s)时让鸣叫状态计数器加 1。当最后一个音调 (cnt_500ms = 3’d6)鸣叫了 0.5s 时,我们让状态计数器跳转回第一个音调鸣叫状态 (cnt_500ms = 3’d0),以此循环,我们就能实现蜂鸣器七个基本音调的循环鸣叫了。

基础设计一——FPGA学习笔记<2>_第106张图片

        首先是音调的频率我们该如何产生?我们先计算该频率单个方波的时间:1 / 262 ≈ 0.003816794s=3816794ns;而我们单个系统时钟(50MHz)的时间为:1 / 50000000 =0.00000002s = 20ns;所以我们需用:3816794 / 20 ≈ 190840 个系统时钟去产生一个 PWM 波,该 PWM 波形的频率即为 262。故我们需先对 190840 个系统时钟进行计数,这里我们 声明 freq_cnt 信号进行计数。

基础设计一——FPGA学习笔记<2>_第107张图片

`timescale  1ns/1ns

module  beep
#(
    parameter   TIME_500MS =   25'd24999999,   //0.5s计数值
    parameter   DO  =   18'd190839 ,   //"哆"音调分频计数值(频率262)
    parameter   RE  =   18'd170067 ,   //"来"音调分频计数值(频率294)
    parameter   MI  =   18'd151514 ,   //"咪"音调分频计数值(频率330)
    parameter   FA  =   18'd143265 ,   //"发"音调分频计数值(频率349)
    parameter   SO  =   18'd127550 ,   //"梭"音调分频计数值(频率392)
    parameter   LA  =   18'd113635 ,   //"拉"音调分频计数值(频率440)
    parameter   XI  =   18'd101214     //"西"音调分频计数值(频率494)
)
(
    input   wire        sys_clk     ,   //系统时钟,频率50MHz
    input   wire        sys_rst_n   ,   //系统复位,低有效

    output  reg         beep            //输出蜂鸣器控制信号
);

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//

//reg   define
reg     [24:0]  cnt         ;   //0.5s计数器
reg     [17:0]  freq_cnt    ;   //音调计数器
reg     [2:0]   cnt_500ms   ;   //0.5s个数计数
reg     [17:0]  freq_data   ;   //音调分频计数值

//wire  define
wire    [16:0]  duty_data   ;   //占空比计数值

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//设置50%占空比:音阶分频计数值的一半即为占空比的高电平数
assign  duty_data   =   freq_data   >>    1'b1;

//cnt:0.5s循环计数器
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt <=  25'd0;
    else    if(cnt == TIME_500MS )
        cnt <=   25'd0;
    else
        cnt <=  cnt +   1'b1;

//cnt_500ms:对500ms个数进行计数,每个音阶鸣叫时间0.5s,7个音节一循环
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_500ms   <=  3'd0;
    else    if(cnt == TIME_500MS && cnt_500ms ==  6)
        cnt_500ms   <=  3'd0;
    else    if(cnt == TIME_500MS)
        cnt_500ms   <=  cnt_500ms + 1'b1;

//不同时间鸣叫不同的音阶
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        freq_data   <=  DO;
    else    case(cnt_500ms)
        0:  freq_data   <=   DO;
        1:  freq_data   <=   RE;
        2:  freq_data   <=   MI;
        3:  freq_data   <=   FA;
        4:  freq_data   <=   SO;
        5:  freq_data   <=   LA;
        6:  freq_data   <=   XI;
        default:  freq_data   <=   DO;
    endcase

//freq_cnt:当计数到音阶计数值或跳转到下一音阶时,开始重新计数
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        freq_cnt    <=  18'd0;
    else    if(freq_cnt == freq_data || cnt == TIME_500MS)
        freq_cnt    <=  18'd0;
    else
        freq_cnt    <=  freq_cnt +  1'b1;

//beep:输出蜂鸣器波形
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        beep    <=  1'b0;
    else    if(freq_cnt >= duty_data)
        beep    <=  1'b1;
    else
        beep    <=  1'b0;

endmodule
`timescale  1ns/1ns

module  tb_beep();

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//

//reg   define
reg     sys_clk     ;   //时钟
reg     sys_rst_n   ;   //复位

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//对时钟,复位信号赋初值
initial
    begin
        sys_clk     =   1'b1;
        sys_rst_n   <=  1'b0;
        #100
        sys_rst_n   <=  1'b1;
    end

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

//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//

beep
#(
    .TIME_500MS(25'd2499 ),   //0.5s计数值
    .DO        (18'd190  ),   //"哆"音调分频计数值(频率262)
    .RE        (18'd170  ),   //"来"音调分频计数值(频率294)
    .MI        (18'd151  ),   //"咪"音调分频计数值(频率330)
    .FA        (18'd143  ),   //"发"音调分频计数值(频率349)
    .SO        (18'd127  ),   //"梭"音调分频计数值(频率392)
    .LA        (18'd113  ),   //"拉"音调分频计数值(频率440)
    .XI        (18'd101  )    //"西"音调分频计数值(频率494)
)
beep_inst
(
    .sys_clk     (sys_clk   ),   //系统时钟,频率50MHz
    .sys_rst_n   (sys_rst_n ),   //系统复位,低有效

    .beep        (beep      )    //输出蜂鸣器控制信号
);

endmodule

十五.动态数码管显示

让六位数码管显示从十进制数 0 开始计数,每 0.1s 加 1,一直到加到十进制数 999999。到达 999999 之后回到 0 开始重新计数。

1.实现

基础设计一——FPGA学习笔记<2>_第108张图片

基础设计一——FPGA学习笔记<2>_第109张图片

基础设计一——FPGA学习笔记<2>_第110张图片

<1>data_gen

        生成0-999999的计数值,用20位二进制表示。bcd_8421生成6位十进制8421码。再转化成相应段码。

`timescale  1ns/1ns

module  data_gen
#(
    parameter   CNT_MAX = 23'd4999_999, //100ms计数值
    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  reg             seg_en      ,   //数码管使能信号,高电平有效
    output  wire            sign            //符号位,高电平显示负号
);

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//

//reg   define
reg     [22:0]  cnt_100ms   ;   //100ms计数器
reg             cnt_flag    ;   //100ms标志信号

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//不显示小数点以及负数
assign  point   =   6'b000_000;
assign  sign    =   1'b0;

//cnt_100ms:用50MHz时钟从0到4999_999计数即为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 + 1'b1;

//cnt_flag:每100ms产生一个标志信号
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'b1)
        cnt_flag    <=  1'b1;
    else
        cnt_flag    <=  1'b0;

//数码管显示的数据:0-999_999
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data    <=  20'd0;
    else    if((data == DATA_MAX) && (cnt_flag == 1'b1))
        data    <=  20'd0;
    else    if(cnt_flag == 1'b1)
        data    <=  data + 1'b1;
    else
        data    <=  data;

//数码管使能信号给高即可
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

 <2>bcd_8421

`timescale  1ns/1ns

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_hun           //十万位BCD码
);

//********************************************************************//
//******************** Parameter And Internal Signal *****************//
//********************************************************************//

//reg   define
reg     [4:0]   cnt_shift   ;   //移位判断计数器
reg     [43:0]  data_shift  ;   //移位判断数据寄存器
reg             shift_flag  ;   //移位判断标志信号

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//cnt_shift:从0到21循环计数
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 + 1'b1;
    else
        cnt_shift   <=  cnt_shift;
       
//data_shift:计数器为0时赋初值,计数器为1~20时进行移位判断操作
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_shift  <=  44'b0;
    else    if(cnt_shift == 5'd0)
        data_shift  <=  {24'b0,data};
    else    if((cnt_shift <= 20) && (shift_flag == 1'b0))
        begin
            data_shift[23:20]   <=  (data_shift[23:20] > 4) ? (data_shift[23:20] + 2'd3) : (data_shift[23:20]);
            data_shift[27:24]   <=  (data_shift[27:24] > 4) ? (data_shift[27:24] + 2'd3) : (data_shift[27:24]);
            data_shift[31:28]   <=  (data_shift[31:28] > 4) ? (data_shift[31:28] + 2'd3) : (data_shift[31:28]);
            data_shift[35:32]   <=  (data_shift[35:32] > 4) ? (data_shift[35:32] + 2'd3) : (data_shift[35:32]);
            data_shift[39:36]   <=  (data_shift[39:36] > 4) ? (data_shift[39:36] + 2'd3) : (data_shift[39:36]);
            data_shift[43:40]   <=  (data_shift[43:40] > 4) ? (data_shift[43:40] + 2'd3) : (data_shift[43:40]);
        end
    else    if((cnt_shift <= 20) && (shift_flag == 1'b1))
        data_shift  <=  data_shift << 1;
    else
        data_shift  <=  data_shift;

//shift_flag:移位判断标志信号,用于控制移位判断的先后顺序
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        shift_flag  <=  1'b0;
    else
        shift_flag  <=  ~shift_flag;

//当计数器等于20时,移位判断操作完成,对各个位数的BCD码进行赋值
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_hun   <=  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_hun   <=  data_shift[43:40];
        end

endmodule

 BCD码分类:

基础设计一——FPGA学习笔记<2>_第111张图片

 BCD码转换原理:

        如上图所示,十进制数 234 其对应的二进制数为 1110_1010,首先第一步我们在其前面补上若干个 0,这里我们就需要 12 位 BCD 码,故我们就在前面补 12 个 0。 第二步我们需要进行判断运算移位操作,首先判断每一个 BCD 码其对应的十进制数是否大于 4,如果大于 4 就对 BCD 码做加 3 操作,若小于等于 4 就让其值保持不变。当对每 一个 BCD 码进行判断运算后,都需要将运算后的数据像左移 1 位。移完位后我们仍按前面所述进行判断运算,判断运算后需再次移位,以此循环,当我们进行 8 次判断移位后的BCD 码部分数据就是我们转换的数据。这里需要注意的是我们输入转换的二进制码有多少位我们就需要进行多少次判断移位操作,这里输入的是 8 位二进制,我们就进行 8 次判断移位操作。 

        cnt_shift:移位判断计数器,前面我们说到我们输入转换的二进制码有多少位我们就需要进行多少次判断移位操作,这里我们 data 数据的位宽为 20 位,所以这里我们声明移位判断计数器对移位 20 次进行判断控制。

        data_shift:移位判断数据寄存器,该寄存器用于存储移位判断操作过程中的数据,这里我们输入的二进制位宽为 20 位,待转换成的 BCD 码位宽为 24 位,所以这里我们声明该 寄存器的位宽为输入的二进制位宽和待转换完成的 BCD 码位宽之和,即 44 位。根据波形 图可知,这里我们设计当移位计数器等于 0 时寄存器的低 20 位即为待转换数据,而由于还 没开始进行转换,高 24 位的 BCD 码我们补 0 即可。

        shift_flag:移位判断操作标志信号。前面说到我们需要对数据进行移位和判断,判断在前移位在后,所以这里我们声明一个标志信号,用于控制判断和移位的先后顺序,当 shift_flag 为低时对数据进行判断,当 shift_flag 为高时对数据进行移位。需要注意的是无论 是移位操作和判断操作都是在单个系统时钟下完成的,故我们判断 20 次移位 20 次在 40 个 系统时钟内就能完成。 

<3> seg_dynamic

`timescale  1ns/1ns

module  seg_dynamic
(
    input   wire            sys_clk     , //系统时钟,频率50MHz
    input   wire            sys_rst_n   , //复位信号,低有效
    input   wire    [19:0]  data        , //数码管要显示的值
    input   wire    [5:0]   point       , //小数点显示,高电平有效
    input   wire            seg_en      , //数码管使能信号,高电平有效
    input   wire            sign        , //符号位,高电平显示负号

    output  reg     [5:0]   sel         , //数码管位选信号
    output  reg     [7:0]   seg           //数码管段选信号
);

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//

//parameter define
parameter   CNT_MAX =   16'd49_999;  //数码管刷新时间计数最大值 1ms

//wire  define
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_hun       ;   //十万位数

//reg   define
reg     [23:0]  data_reg    ;   //待显示数据寄存器
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   ;   //当前数码管显示的数据
reg             dot_disp    ;   //当前数码管显示的小数点

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//data_reg:控制数码管显示数据
 always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_reg    <=  24'b0;
//若显示的十进制数的十万位为非零数据或需显示小数点,则六个数码管全显示
    else    if((h_hun) || (point[5]))
        data_reg    <=  {h_hun,t_tho,tho,hun,ten,unit};
//若显示的十进制数的万位为非零数据或需显示小数点,则值显示在5个数码管上
//打比方我们输入的十进制数据为20’d12345,我们就让数码管显示12345而不是012345
    else    if(((t_tho) || (point[4])) && (sign == 1'b1))//显示负号
        data_reg <= {4'd10,t_tho,tho,hun,ten,unit};//4'd10我们定义为显示负号
    else    if(((t_tho) || (point[4])) && (sign == 1'b0))
        data_reg <= {4'd11,t_tho,tho,hun,ten,unit};//4'd11我们定义为不显示
//若显示的十进制数的千位为非零数据或需显示小数点,则值显示4个数码管
    else    if(((tho) || (point[3])) && (sign == 1'b1))
        data_reg <= {4'd11,4'd10,tho,hun,ten,unit};
    else    if(((tho) || (point[3])) && (sign == 1'b0))
        data_reg <= {4'd11,4'd11,tho,hun,ten,unit};
//若显示的十进制数的百位为非零数据或需显示小数点,则值显示3个数码管
    else    if(((hun) || (point[2])) && (sign == 1'b1))
        data_reg <= {4'd11,4'd11,4'd10,hun,ten,unit};
    else    if(((hun) || (point[2])) && (sign == 1'b0))
        data_reg <= {4'd11,4'd11,4'd11,hun,ten,unit};
//若显示的十进制数的十位为非零数据或需显示小数点,则值显示2个数码管
    else    if(((ten) || (point[1])) && (sign == 1'b1))
        data_reg <= {4'd11,4'd11,4'd11,4'd10,ten,unit};
    else    if(((ten) || (point[1])) && (sign == 1'b0))
        data_reg <= {4'd11,4'd11,4'd11,4'd11,ten,unit};
//若显示的十进制数的个位且需显示负号
    else    if(((unit) || (point[0])) && (sign == 1'b1))
        data_reg <= {4'd11,4'd11,4'd11,4'd11,4'd10,unit};
//若上面都不满足都只显示一位数码管
    else
        data_reg <= {4'd11,4'd11,4'd11,4'd11,4'd11,unit};

//cnt_1ms:1ms循环计数
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 + 1'b1;

//flag_1ms:1ms标志信号
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        flag_1ms    <=  1'b0;
    else    if(cnt_1ms == CNT_MAX - 1'b1)
        flag_1ms    <=  1'b1;
    else
        flag_1ms    <=  1'b0;

//cnt_sel:从0到5循环数,用于选择当前显示的数码管
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        cnt_sel <=  3'd0;
    else    if((cnt_sel == 3'd5) && (flag_1ms == 1'b1))
        cnt_sel <=  3'd0;
    else    if(flag_1ms == 1'b1)
        cnt_sel <=  cnt_sel + 1'b1;
    else
        cnt_sel <=  cnt_sel;

//数码管位选信号寄存器
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        sel_reg <=  6'b000_000;
    else    if((cnt_sel == 3'd0) && (flag_1ms == 1'b1))
        sel_reg <=  6'b000_001;
    else    if(flag_1ms == 1'b1)
        sel_reg <=  sel_reg << 1;
    else
        sel_reg <=  sel_reg;

//控制数码管的位选信号,使六个数码管轮流显示
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        data_disp    <=  4'b0;
    else    if((seg_en == 1'b1) && (flag_1ms == 1'b1))
        case(cnt_sel)
        3'd0:   data_disp    <=  data_reg[3:0]  ;  //给第1个数码管赋个位值
        3'd1:   data_disp    <=  data_reg[7:4]  ;  //给第2个数码管赋十位值
        3'd2:   data_disp    <=  data_reg[11:8] ;  //给第3个数码管赋百位值
        3'd3:   data_disp    <=  data_reg[15:12];  //给第4个数码管赋千位值
        3'd4:   data_disp    <=  data_reg[19:16];  //给第5个数码管赋万位值
        3'd5:   data_disp    <=  data_reg[23:20];  //给第6个数码管赋十万位值
        default:data_disp    <=  4'b0        ;
        endcase
    else
        data_disp   <=  data_disp;

//dot_disp:小数点低电平点亮,需对小数点有效信号取反
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        dot_disp    <=  1'b1;
    else    if(flag_1ms == 1'b1)
        dot_disp    <=  ~point[cnt_sel];
    else
        dot_disp    <=  dot_disp;

//控制数码管段选信号,显示数字
always@(posedge sys_clk or  negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        seg <=  8'b1111_1111;
    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;
        endcase

//sel:数码管位选信号赋值
always@(posedge sys_clk or negedge sys_rst_n)
    if(sys_rst_n == 1'b0)
        sel <=  6'b000_000;
    else
        sel <=  sel_reg;

//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//

//---------- bsd_8421_inst ----------
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_hun       (h_hun    )    //十万位BCD码
);

endmodule

 <4>top_seg_dynamic

`timescale  1ns/1ns

module  top_seg_dynamic
(
    input   wire            sys_clk     ,   //系统时钟,频率50MHz
    input   wire            sys_rst_n   ,   //复位信号,低电平有效

    output  wire    [5:0]   sel         ,   //数码管位选信号
    output  wire    [7:0]   seg             //数码管段选信号
);

//********************************************************************//
//******************** Parameter And Internal Signal *****************//
//********************************************************************//
//wire  define
wire    [19:0]  data    ;   //数码管要显示的值
wire    [5:0]   point   ;   //小数点显示,高电平有效top_seg_595
wire            seg_en  ;   //数码管使能信号,高电平有效
wire            sign    ;   //符号位,高电平显示负号

//********************************************************************//
//**************************** Main Code *****************************//
//********************************************************************//
//-------------data_gen_inst--------------
data_gen    data_gen_inst
(
    .sys_clk     (sys_clk  ),   //系统时钟,频率50MHz
    .sys_rst_n   (sys_rst_n),   //复位信号,低电平有效
    
    .data        (data     ),   //数码管要显示的值
    .point       (point    ),   //小数点显示,高电平有效
    .seg_en      (seg_en   ),   //数码管使能信号,高电平有效
    .sign        (sign     )    //符号位,高电平显示负号
);

//-------------seg7_dynamic_inst--------------

seg_dynamic seg_dynamic_inst
(
    .sys_clk     (sys_clk  ),   //系统时钟,频率50MHz
    .sys_rst_n   (sys_rst_n),   //复位信号,低有效
    .data        (data     ),   //数码管要显示的值
    .point       (point    ),   //小数点显示,高电平有效
    .seg_en      (seg_en   ),   //数码管使能信号,高电平有效
    .sign        (sign     ),   //符号位,高电平显示负号

    .sel         (sel      ),   //数码管位选信号
    .seg         (seg      )    //数码管段选信号

);

endmodule

2.仿真

`timescale  1ns/1ns

// Author  : EmbedFire
// 实验平台: 野火FPGA系列开发板
// 公司    : http://www.embedfire.com
// 论坛    : http://www.firebbs.cn
// 淘宝    : https://fire-stm32.taobao.com


module  tb_top_seg_dynamic();

//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//wire  define
wire    sel    ;   //数码管位选信号
wire    seg    ;   //数码管段选信号


//reg   define
reg     sys_clk     ;
reg     sys_rst_n   ;

//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//

//对sys_clk,sys_rst_n赋初始值
initial
    begin
        sys_clk     =   1'b1;
        sys_rst_n   <=  1'b0;
        #100
        sys_rst_n   <=  1'b1;
    end

//clk:产生时钟
always  #10 sys_clk <=  ~sys_clk;

//重新定义参数值,缩短仿真时间
defparam  top_seg_dynamic_inst.seg_dynamic_inst.CNT_MAX =    9;
defparam  top_seg_dynamic_inst.data_gen_inst.CNT_MAX    =  239;

//********************************************************************//
//*************************** Instantiation **************************//
//********************************************************************//
//------------- seg_595_static_inst -------------
top_seg_dynamic  top_seg_dynamic_inst
(
    .sys_clk     (sys_clk   ),  //系统时钟,频率50MHz
    .sys_rst_n   (sys_rst_n ),  //复位信号,低电平有效

    .sel         (sel       ),  //串行数据输入
    .seg         (seg       )   //输出使能信号
);

endmodule

你可能感兴趣的:(FPGA,学习笔记,学习,笔记)