北航计算机组成原理课程设计-2020秋 PreProject-Verilog HDL与ISE-Verilog工程的设计开发调试

北航计算机学院-计算机组成原理课程设计-2020秋

PreProject-Verilog HDL 与 ISE

Verilog工程的设计开发调试


本系列所有博客,知识讲解、习题以及答案均由北航计算机学院计算机组成原理课程组创作,解析部分由笔者创作,如有侵权联系删除。


从本节开始,课程组给出的教程中增添了很多视频讲解。为了避免侵权,本系列博客将不会搬运课程组的视频讲解,而对于文字讲解也会相应地加以调整,重点在于根据笔者自己的理解给出习题的解析。因此带来的讲解不到位敬请见谅。


Verilog开发与调试综述

在P4,我们便需要编写初具工程化规模的代码;从P5的流水线工程直到最后P8的FPGA实现,我们更是需要对工程进行增量开发。在这个过程中,我们必然会经历众多ISE的复杂报错以及重重Bug的艰难困苦,因而领悟系统化、规范化的开发以及调试的重要性不言而喻,养成良好的开发及测试的习惯,能够帮助我们屏蔽掉更多的无用功。

本章节将通过一个简单的单模块需求实例,带领大家了解Verilog项目开发、测试到综合的基本流程,并且希望大家能够在预习阶段就掌握仿真与调试部分的内容,对于其他如编辑工具推荐、综合等教程,大家可以先尝试阅读,遇到一时不懂或难以接受的地方可以先将页面标记并跳过,他们将在开发多模块项目时起到更大的作用。在后续的计组旅程中,我们会多次提示同学们返回来温习,帮助大家对工程化的开发模式有更好的理解与感悟。

  • 需求分析:使用文档辅助工具协同进行架构设计

  • 需求实现:根据设计文档高效完成模块代码编写

    • 编写代码
    • 关注代码风格
    • 浅谈编辑器
  • 仿真与调试

    对已有模块进行测试

    • 生成Testbench
    • 使用ISim
    • 错误样例
  • **综合工程:**将抽象逻辑转化为实际电路


需求:简单32位ALU

计小组同学为了学习Verilog的开发与调试,从学长那里要来了一份需求——完成支持四种运算的32位ALU设计。

学长为了考验计小组的设计能力,并没有对具体的设计细节(命名、功能选择对应含义等)进行过多的约束,但他在提出需求后反复向她强调学习开发要“慢斟细酌”,不要急于求成。

设计要求:

端口描述方向位宽
第一个32位运算数I32
第二个32位运算数I32
ALU功能选择I2
时钟信号I1
使能信号I1
运算结果O32
  • ALU模块需要支持加、减、与、或运算。
  • 每个时钟上升沿到来时,如果使能信号有效,则更新运算结果,否则保持不变。
  • 在两个时钟上升沿之间,保持输出结果不变。
  • 对模块进行充分仿真测试。

计小组还没有任何开发、测试的经验,秉承着学长的教诲,由此开始了默默的学习之旅。


需求分析

我们希望大家在拿到一道题目,尤其是比较复杂的工程化项目时,不要急于上手写代码,而要预先根据需求进行架构设计。这一点在后续的多模块开发过程中尤为重要,是奠定工程整体质量的关键步骤。

有了前一章节题目分析的铺垫,本文只对该ALU需求分析进行简略说明,主要目的是推荐大家使用一款轻量级的Markdown语言文本编辑器Typora,来作为自己的架构文档协同工具,大家可以视自己能够接受的程度进行阅读。

Markdown是一种可以使用普通文本编辑器编写的标记语言,通过简单的语法可以使内容文本拥有一定的格式,非常易上手。我们带领大家利用Markdown去进行模块的构架。

Typora官网:typora
Markdown推荐教程:菜鸟教程 Markdown

端口定义

本道题目已经对模块的输入和输出端口进行了约束,我们只需要对端口合理命名后整合到文档中即可。

Markdown使用若干数量的连续“#”后紧接一个空格生成不同级别的标题,如一级标题“# 一级标题”,三级标题“### 三级标题”。表格使用“|”来分隔不同的单元格,使用“-”来分隔表头和其他行。如果觉得表格的语法较为繁琐,也可以在Typora的“段落-表格”中根据行列数自动生成。

在这里插入图片描述

在今后的多模块开发中,很多模块的规格都需要我们自行来设计,我们将会在届时的Project中介绍更多模块化开发的内容。

组合逻辑

ALU模块首先要并行计算出四种运算的结果,然后需要通过op输入信号的选择来确定每个周期的输出数据,在前面的教程中,我们了解到,在组合逻辑中对多个信号进行选择可以使用多路选择器,由此我们将其都加入设计文档中。

Markdown使用“*”、“-”、“+”后接一个空格来生成无序列表,在Typora中,使用Enter键可以自动续表,如果继续使用Enter键可以取消续表恢复普通文本,如果继续使用Tab键则可以生成二级无序列表。

在这里插入图片描述

时序逻辑

我们需要在在每个时钟信号的上升沿更新输出的值,也就意味着我们的模块需要具备存储功能,由此引入时序逻辑。然后要判断使能信号有效性,再进一步决定输出。

在这里插入图片描述

很多时候,我们都需要反复斟酌一个模块存在的必要性。将复用频繁的电路独立抽离为一个模块,将功能简单且仅在一个地方使用的电路融入到其父级模块中。避免过度设计,同时又要有合理的抽象,在设计时需要认真取舍。

希望大家能够在后续越来越复杂的Project中养成先进行架构设计再编码实现的习惯,好的工程架构往往能够令我们事半功倍。


需求实现:编写代码

现在我们已经设计好了ALU模块的输入输出端口组合逻辑时序逻辑三部分内容,可以开始着手用代码将设计实现。

在前面的章节中,我们已经了解了Verilog的基本语法、题目编写的方法,这部分便不再赘述。

计小组同学对照自己刚刚写好的设计文档,利用已经学习到的知识,很快完成了编码:

在这里插入图片描述

相信同学们也注意到,计小组同学的代码非常难以阅读,这也引出了Verilog的代码风格问题,我们将在下一节介绍。


需求实现:关注代码风格

本节将讲述部分行业内约定俗成的代码风格习惯,旨在引导大家写出格式规范观感良好的代码。

学习编程的初期,我们接触到的都是一些逻辑简单,代码行数较少的题目,要实现的功能非常单一,而且完成目标需求达成AC后就不再去进行任何维护,因而代码风格似乎显得没那么重要。

但如果现在翻出大一下学期的数据结构大作业,并要求在很短时间内解释清楚每个变量的作用,亦或是在其基础上进行一定程度的增量开发,相信所有同学都要花费一定时间去回忆实现的逻辑,而完成这个步骤的效率快慢,则很大程度上取决于代码风格的优劣

我们对工程进行调试或增量开发时,可能会不断遇到新的Bug。而相同的逻辑下,调试一份简洁清爽的代码要远比调试一份混乱不堪的代码容易得多,这一点单单通过文字叙述是无法传达的。同学们需要不断地探索尝试,并在调试遇到阅读代码的阻力碰壁的过程中,逐步提升对代码风格的重视程度,养成良好的编程习惯。

在今后的项目开发当中,代码的阅读者将不再只有编者自己,为了让其他阅读者很快了解编者的意图,很多企业都制定了标准化的规范体系来对代码风格进行约束。但在计组实验,我们希望同学们初步拥有一个意识即可,因此不对代码进行规范化考量。

下面我们将对计小组同学的ALU代码进行改善,使其的可读性大大增加。

符号两侧空格部分规则

多数代码规范对运算符两侧的空格有明确的约束,在此只列举一部分较为影响代码观感的进行说明。

  • 单目运算符与变量间不添加空格,如“~”、“!”。
  • 双目运算符(除逗号外)和三目运算符两侧添加空格,如“+”、“=”、“<”、“&&”。
  • 分号和逗号要紧附前面内容,不应添加空格。
  • 避免连续使用多个空格。

根据这几点规则,我们可以将assign语句改成如下样式:

在这里插入图片描述

换行的使用

修改后的assign语句已经没有了拥挤的感觉,但很明显仍然不适合阅读——我们很难迅速地判别出每个条件所对应的语句,因此需要考虑在合适的地方进行换行处理

在不违背语法规则的前提下,推荐在两个较高级的表达式间换行,具体情况具体分析,保证换行后最适宜阅读逻辑为最佳。

在这里插入图片描述

此外,为了代码阅读者更高效地理解代码,可以增加括号来使同一行代码间降低耦合度;为了代码的美观性,也可以使用空格令各行代码对齐;对于处理不同逻辑的不同代码块,尽可能使用空行将其分开,对自身调试、他人阅读都有很大的帮助。

模块缩进

计小组的代码还有一个较为突出的风格问题,那就是最后的几行几乎堆砌满了“end”相关的语句,直观上无法很快定位每一个“end”所对应的语句块,因此采用缩进的方式对其进行优化对齐

经过一定的优化后,代码不再有拥挤的感觉:

在这里插入图片描述

合理命名

作为一名测试者,如果代码中充满了没有任何意义的“a”、“b”样式的命名,毫无疑问会给调试增加不小的阻碍,你甚至不知道出现bug的端口或变量的作用是什么。

关于变量与端口命名,有如下建议:

  • 符合其自身的功能,尽量做到“望文生义”。
  • 杜绝拼音与英文的命名混用,推荐使用英文单词的缩写,使名称长度合理。
  • 不同单词间有明显的边界划分,如使用下划线,常见的“大驼峰”、“小驼峰”命名法等。

添加注释

注释的作用就不需要再做过多的赘述了,其可以将抽象的计算机语言转化为人类交流所依赖的语言,能够很大程度上提高代码的阅读效率,快速帮助编者与读者理清各部分代码的作用。

常量定义

一些使用或改动频繁的字面量往往可以使用宏或parameter(下一章节将介绍宏的使用)等Verilog语法特性进行定义,在增量开发时保证最快寻找到定义位置,且对代码的改动最小,如我们后面设计CPU中的指令存储器模块时:

在这里插入图片描述

而在多模块开发中,我们可以专门编写一个宏定义的头文件,使用``include`,方便其他所有的模块跨文件引用其中的常量。

数据位宽

关于数据位宽有两点说明:

  • 使用常数时,声明数据位宽,避免连线时出现位宽不一致的Bug。
  • 如果模块要使用数据的某一部分位,如instruction[25:21],使用wire变量直接截取,重新赋予一个合理的命名,减少字面量以增强可读性。

避免复制粘贴代码

我们提出这部分,并不是在说不要复制别人的代码(那样的后果想必大家都清楚),而是在说不要复制粘贴自己的代码

在后续的开发中,大家可能会遇到非常多带有重复部分的代码段,于是想减少一下编写的工夫,对其进行复制粘贴,然后再做微小调整。这样做会让粘贴的代码带有极大的隐患,你很可能因为看错某个变量的一个字母,或者粘贴到了重复度高的错误位置没有察觉,导致工程出现难以预料的Bug。

一个规避这种风险的方法是,对想要复制的代码块进行抽象,独立为一个模块或者使用宏对其定义,然后谨慎编写不同的部分。

计小组同学对照自己刚刚编写的代码,对代码风格的重要性开始有初步的意识了,于是她完成了ALU代码风格的优化,并开始准备进行测试工作。

在这里插入图片描述


需求实现:浅谈编辑器

ISE是一款强大的硬件开发工具,但它并不是一款良好的编辑器。

便捷的编辑器对提高编码效率维护代码风格调试代码漏洞,甚至对编写者的心情都有较大的影响。本文使用VSCode作为例子,向没有使用过的同学稍作讲解。

Visual Studio Code集成了所有一款现代编辑器应该具备的特性,包括语法高亮可定制的热键绑定括号匹配以及代码片段收集,在网络上有很多渠道下载。

首先我们在左侧的工具栏中选择“扩展”,然后在商店搜索“verilog”,选择图中所示的插件Verilog HDL/SystemVerilog进行安装,我们的VSCode便能够支持Verilog语言编辑

在这里插入图片描述

VSCode有很强大的自动补全功能,可以补全变量名称、保留字、代码块等,如我们要输入always @(posedge clk)语句块,在VSCode中键入“al”,便自动弹出目标选项,使用上下键进行选择后按下Enter或Tab确认。

在这里插入图片描述

将光标移动至变量上,使用Ctrl + F2对该变量进行重命名。

在这里插入图片描述

VSCode还拥有非常多的快捷键,包括全局文字查找替换批量代码注释转到定义或实现等,并且支持Git版本控制与大多数主流的语言编辑,有相当一部分语言稍加配置便可以在其中完成运行与调试。

感兴趣的同学可以对Verilog的编辑器做更多的探索,如怎样让VSCode完成仿真输出波形、Linux系统下的Vim工具等,并将经验分享在讨论区。


仿真与调试:生成Testbench

FPGA的基本开发流程可分为功能定义与器件选取设计输入功能仿真综合优化综合后仿真实现与布局布线时序仿真板级运行验证,在P8之前,我们只会接触到前三个步骤。

其中功能仿真步骤用于验证设计的模块是否符合预期要求,在我们的开发流程中占据着至关重要的地位,请大家认真学习接下来的三节【仿真与调试】教程!

生成测试文件

我们要生成的Testbench(测试器),其本质是一个module,用于测试已编写好的module的正确性,可以将其看作一个“驱动装置”。

首先,与新建Verilog源文件方法相同,右击工作管理视窗,选择“New Source”,在弹出的窗口中,我们选择“Verilog Test Fixture”。

在这里插入图片描述

单击**“Next”**后,我们需要选择要仿真的模块,这里只有alu,于是我们直接进入后一步并完成生成。

在这里插入图片描述

解析测试文件

下面我们对生成的alu_tb模块进行解析。

在这里插入图片描述

  • 标注有“Inputs”和“Outputs”注释的地方,是我们模块输入输出端口的转化,其中仿真模板将输入用reg变量替代,便于我们直接对其值进行设置。
  • 下面标注有“uut”的部分是对我们要测试的模块进行实例化,具体含义将在后文进行讲解。
  • initial语句块是我们需要修改的部分,使用关键字“#”开头的延迟控制语句进行时间控制,将输入端口在不同的时间赋予我们期望的数据。请注意,该语句声明的是延迟时间,而不是整个仿真过程的时间戳。

Testbench本质与一个模块相同,我们也可以为其添加临时变量,组合逻辑等内容来辅助仿真。

编写测试逻辑

我们开始编写测试逻辑,当模块包含时钟信号这类以固定频率变化的输入时,我们要使用always,如我们欲将时钟周期设置为10ns,可以写always #5 clk = ~clk;

在这里插入图片描述

上图对加法、减法、使能信号、时序逻辑进行了简单的测试,接下来我们便可以用ISim进行仿真了。

模块实例化

我们将在这部分专门对模块实例化进行讲解。

在Verilog中,我们编写的模块都是抽象的逻辑,如果要将其交由其他父模块调用,就需要进行模块的实例化。简而言之,实例化就是抽象转化为具体的过程。

模块实例化的方法

对电路元件模块实例化最常见的语法是:<模块名> <实例名>(端口信号映射);

其中,端口信号映射方式也有两种:

  • 位置映射:<模块名> <实例名>(信号1, 信号2, ...);,其中信号n对应被实例化模块声明时排在第n位的端口。
  • 名映射:<模块名> <实例名>(.端口名a(信号1), .端口名b(信号2), ...);,其中信号n对应其前的端口名。

值得注意的是,在实例化元件时,wire类型信号可以被连接至任意端口上,但reg类型的信号只能被连接至元件的输入端口上。在声明元件时,我们可以将任意端口声明为wire类型,但只能将输出端口声明为reg类型,否则会出现问题。

我们也可以悬空部分不需要连接的端口,下图的uut_0、uut_1、uut_2分别对应位置映射、名映射与悬空端口的实例。建议每一行只连接一个端口信号避免混乱。

在这里插入图片描述

调用ISE实例化模板

ISE为我们提供了方便实例化的模板工具,我们首先必须位于Design栏的“Implementation”视图下,然后选择我们需要生成模板的模块,右击“Design Utilities”下的“View HDL Instantiation Template”,选择最下方“Process Properties”,将“Value”修改为“Verilog”点击“OK”。最后我们双击“View HDL Instantiation Template”就可以得到实例化模板,能够节省大量手动完成映射的时间。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

此外,ISE还有语言模板工具,用于查询常用FPGA语法以及大量开发实例

在这里插入图片描述


仿真与调试:使用ISim

本节将延续上节内容讲解使用ISim仿真的方法,并对常用的一些功能进行说明。请大家观看视频讲解,然后学习使用ISim对ALU进行仿真的实例。

语法检查与运行

我们需要将Design栏的视图切换到“Simulation”,选中我们编写好的仿真模块alu_tb,然后双击下方“Behavioral Check Syntax”对模块行为进行语法检查,如果显示绿色对勾说明语法检查无误(问号通常也表示没有严重问题),如果显示红叉,可以参考下一部分【仿真与调试:错误样例】,进行Bug修复。我们继续双击“Simulate Behavioral Model”启动ISim进行仿真。

在这里插入图片描述

进入ISim后,我们可看到如下界面。

在这里插入图片描述

大家可能会觉得界面非常复杂,且无从下手。请不要感到慌乱,我们将一步步讲解调试的基本流程。

观察波形

相信大家还记得在刚刚编写的测试文件中,我们使用了很多时间延迟语句来控制每个时刻各个输入的变化。波形图就是用来直观表现各个模块的变量信号随时间如何变化的工具,与我们在物理学中学过的振动图像非常相似。

但最初始状态下,这个图的时间粒度非常细,要远小于我们设定的时钟周期,很难观察出信号的变化,因此我们需要使用下一小节工具栏图中的[2]来缩小波形,即放大时间粒度。相对的,我们也可以使用[1]来放大波形,缩小时间粒度。

将波形缩放到合适范围内后,我们发现,所有信号全都是以二进制的格式显示的,对于加法与减法的检查很不方便。选中Value一栏下的全部变量,右击可以对Radix进制进行设置(对Objects栏中的变量也可以使用该操作)。

在这里插入图片描述

拖动波形,观察输入信号与输出的result信号,我们就能够检验编写的alu模块是否符合预期了。

工具栏介绍

在复杂工程的调试过程中,我们可能还会用到工具栏中的其他工具。

在这里插入图片描述

  • [1] [2]放大(缩小):对波形图放大(缩小),缩小(增大)时间粒度。

  • [3] [4]跳转到上(下)一次变化:选中一个波形后,点击可以跳转至该波形对应的信号上(下)一次发生变化的时刻,如clk的上升沿,点击后可跳至上(下)一个下降沿。

  • [5]标记:对某个选中的时刻进行标记,标记后会产生一条上图中所示的蓝色标线。

  • [6] [7]跳转至上(下)一个标记:从某一时刻可以跳转到临近标记处。

  • [8]重新开始:清空仿真波形,回退到0时刻,通常在添加了波形信号后使用。

  • [9]运行:开始持续仿真,需要使用后面的暂停终止。

  • [10]运行指定时间间隔:初始进入时默认为1000ns,可多次点击使用。

  • [11]指定时间间隔设置:可以设定每次点击[9]所运行的时间。

  • [12]单步调试:类比软件的调试,可以在左侧双击实例打开源文件,然后通过点击文件行号左侧空白可以设置断点。

    在这里插入图片描述

  • [13]重启:重新加载ISim仿真进程,通常用于修改了源代码的情况。

查看其他变量方法

大多数时候,我们只观察输入输出的波形是不容易发现Bug所在位置的,需要引入“中间变量”。在我们的alu模块中,要查看temp_result变量的取值,可以先从左侧选择包含该wire型变量的实例uut,在其右侧的对象栏中右击想要查看的变量,加入波形图,或直接拖动到波形图中。

如果波形图没有改变,需要点击[8] [10]重新仿真,以正常显示加入信号的变化。

在这里插入图片描述

查看存储器数据方法

在后续的工程Project中,我们需要对指令存储器、内存与寄存器阵列进行仿真。要查看内存中的取值,首先要选择Memory,双击目标存储器,可以得到下图所示的显示。

当然,这可能与刚打开的格式有所差异,我们可以使用地址反转工具,将地址由高到低变为由低到高显示,另外可以改变每一行显示数据的数目,以及显示的进制格式等。

在这里插入图片描述


仿真与调试:错误样例

部分ISE报错与解决方案

很多时候,我们在语法检查或运行时会遇到ISE报错的情况,需要返回去对代码进行核实修正。下面列出了部分ISE的报错与解决方案。

  • **仿真报错:**ERROR:Simulator:861 – Failed to link the design.

    在这里插入图片描述

    解决方案:对于64位ISE,找到安装目录“\Xilinx\14.x\ISE_DS\ISE\gnu\MinGW\5.0.0\nt\libexec\gcc\mingw32\3.4.2\”下的 “collect2.exe”并将其删除,重新运行仿真器。如果删除后重新运行依旧报错,则尝试下载MinGW编译器,并使用此压缩包中MinGW文件夹下的内容替换…\Xilinx\14.7\ISE_DS\ISE\gnu\MinGW\5.0.0\nt目录下的所有文件。完成上述操作后重新运行,如果问题仍然存在,请尝试使用32位ISE。

  • 仿真报错:ERROR:Simulator:904 – Unable to remove previous simulation.

    在这里插入图片描述

    解决方案:结束已有的仿真进程,再重新仿真。

  • Verilog语法错误:Line 29: Procedural assignment to a non-register Out is not permitted, left-hand side should be reg/integer/time/genvar

    在这里插入图片描述

    解决方案:reg型wire型变量使用错误。检查语法,如果我们在always块中向一个wire型变量赋值,就会出现以上报错。若还不能很好地区分这两种变量,可以结合Logisim中的电路与前面的语法讲解——reg类变量通常相当于存储单元(初学时可以想象为寄存器),wire类变量相当于物理电路的连线,回忆两者分别对应哪种逻辑,以及该怎样赋值。

  • Verilog语法错误:Error (10110): variable “sign” has mixed blocking and nonblocking Procedural Assignments – must be all blocking or all nonblocking assignments.

    **解决方案:**禁止混用阻塞赋值与非阻塞赋值,推荐在描述时序逻辑的always块中全部使用非阻塞赋值,在描述组合逻辑的代码中使用阻塞赋值。

  • Verilog语法错误:Error:this signal is connected to multiple drivers.

    可能的错误原因:对wire类变量赋予了初始值。

  • Verilog语法错误:Error (10170): Verilog HDL syntax error at test_vga.v(57) near text “<”; expecting “<=”, or “=”

    解决方案:非阻塞赋值符号“<=”编写错误,改正即可。

  • Verilog语法错误:Illegal redeclaration of module <name>

    解决方案:模块重复声明,检查是否存在重名模块,确认该模块是否被其他文件用include`引用,是则有两种解决方法:如果该模块都是宏定义,可以直接在头文件写宏定义;使用ifndef`语句,避免重复定义。

  • Verilog语法错误:Internal Compiler Error in file … at line 5239…

    解决方案:通常代码中出现了不当的语法,编译器无法定位Bug位置,只能通过“观察法”寻找并修复。提倡大家养成边编写边进行语法检查测试的习惯,可以有效防范该问题出现。

部分仿真Bug与调试方法

本部分将列举一些设计中会出现的Bug,并给予解决方法。

  • 波形出现不定值x:这种情况在没有initial语句的情况下会出现在reg类变量的波形中,通常在第一次对reg变量修改后便会消失,可以添加initial语句对reg赋予初始值来避免。但如果整个仿真的过程中,某个reg变量一直维持不定值,需要检查时序逻辑的编写是否正确,是否有按照架构对相应变量进行赋值。

    在这里插入图片描述

  • 波形出现高阻z:电路存在没有连线的变量信号,会显示高阻。需要返回到代码中检查相应wire变量是否被正确的assign语句赋值,reg变量连接的wire变量是否正确连线。

    在这里插入图片描述

计小组同学学习了仿真与调试的方法后,对她的ALU进行了充分的测试。学长满意地验收后,又向计小组提出了“综合”的需求。


综合工程

综合,是将硬件描述语言,翻译为物理电路的过程。

在计组实验的P8之前,我们都不会使用到综合,但由于P8涉及到从前面Project开始的扩展,且能够仿真的代码不一定支持综合,我们在此对综合的条件作出一部分说明,让各位有志之士提前做好准备,避免在P8对代码进行大量修改,徒增麻烦。

Tips:综合的速度普遍较慢,当工程规模较大时综合甚至会花费数小时。我们还需要注意语法检查通过并不代表综合能够通过。如何写出可综合的Verilog代码,将是未来学习过程中必须面对的问题。当然,为了前面Project能够有效通过测试,仍需要在抵达P8之前保留一部分不可综合的代码。

与仿真不同的常见综合化要求包括但不限于:

  • 不使用initial、fork、join、casex、casez、延时语句(例如#10)、系统任务(例如$display)等语句,具体可自行查阅学习。
  • 用always过程块描述组合逻辑时,应在敏感信号列表中列出所有的输入信号(或使用星号*)。
  • 用always过程块描述时序逻辑时,敏感信号只能为时钟信号
  • 所有的内部寄存器都应该能够被复位。
  • 不能在一个以上的always过程块中对同一个变量赋值。而对同一个赋值对象不能既使用阻塞式赋值,又使用非阻塞式赋值。
  • 尽量避免出现锁存器(latch),具体避免方法有许多。例如,如果不打算把变量推导成锁存器,那么必须在if语句或case语句的所有条件分支中都对变量明确地赋值。
  • 避免混合使用上升沿和下降沿触发的触发器。
  • ……

还有一些其他的细节要求,具体可以自行查阅学习。

计小组同学按照视频中的方法,将ALU模块进行了综合,并自豪地向学长分享了自己这次开发的成果与收获。

在这里插入图片描述

但粗心的计小组并没有发现综合结束后控制台给出的信息,感兴趣的同学可以自行探索,帮助计小组进行分析。


练习:计数器

计数器是定时产生中断信号从而使系统产生一系列既定动作的关键。在这里,我们可以先初步了解一下计数器的内部结构:

在这里插入图片描述

这里做一些简要的解释:控制寄存器可以被输入的数据改变其内容,从而实现对计数器的RESET操作和ENABLE操作。初值寄存器决定了计数器从多大的数开始“倒数”,而计数器在ENABLE的前提下会随着时钟周期的推进不断地减小,直到减为0后会对外界产生一个中断信号,表示“现在我完成了你让我进行的倒数工作”,而系统就会根据这个信号结合自身当前的状态决定是否要进行别的操作。

计数器的实现还需要规定大量的细节,不过这里我们就不多做解释了。

现在,在对Verilog和ISE有了初步的了解之后,我们来尝试设计一个经过了大量简化处理的计数器,它只有一些非常简单的功能,端口定义如下:

在这里插入图片描述

我们要实现的功能非常简单:

  1. 在任意一个时钟上升沿到来的时候,如果复位信号有效,则将两个计数器同时清零;

  2. 每个时钟上升沿到来的时候,如果使能信号有效,则称其为一个“有效时钟周期”,如果:

    a) 选择信号为0,则将这个有效时钟信号计入计数器0,每经过1个属于计数器0的有效时钟周期,计数器0累加1;

    b) 选择信号为1,则将这个有效时钟信号计入计数器1,每经过4个属于计数器1的有效时钟周期,计数器1累加1;

  3. 在满足1时,即使2的条件满足,也不必执行2;

  4. 注意复位操作也会复位每个计数器当前的有效时钟周期。

  5. 请提交相应的.v文件

  6. 文件内模块名:code

注意初始时每个计数器的值都为0。

参考波形如下:
在这里插入图片描述

本题的逻辑比较简单,根据Slt、Reset和En三个信号在时钟上升沿时刻的状态对两个计数器进行相应的更新即可。其中计数器0每一个有效的时钟周期都自增,计数器1要等待4个有效时钟周期才自增一次,因此要为计数器1设置内部计数器,每个时钟周期为内部计数器自增,当内部计数器是4的倍数时令计数器1自增。具体的模块设计如下:

// code.v
module code(
 input Clk,
 input Reset,
 input Slt,
 input En,
 output reg [63:0] Output0,
 output reg [63:0] Output1
 );

reg [1:0] interCounter = 2'b01;

always @(posedge Clk) begin
 if (Reset == 1'b1) begin
     // If Reset, Output0 and Output1 should be set zero.
     Output0 <= 64'h0000_0000;
     Output1 <= 64'h0000_0000;
 end
 else if (En == 1'b1) begin
     // If En is valid,
     if (Slt == 1'b0) begin
         // If select counter 0, Output0 increases.
         Output0 <= Output0 + 64'h0000_0001;
         Output1 <= Output1;
     end
     else begin 
         // If select conter 1, interCounter incereases.
         interCounter <= interCounter + 2'b01;
         if (interCounter == 2'b00) begin
             // If interCounter counts 4, Output1 increases.
             Output0 <= Output0;
             Output1 <= Output1 + 64'h0000_0001;
         end
         else begin
             // If interCounter doesn't count 4, nothing changes.
             Output0 <= Output0;
             Output1 <= Output1;
         end
     end
 end 
 else begin
     // If En is invalid, nothing changes in this cycle.
     Output0 <= Output0;
     Output1 <= Output1;
 end
end

endmodule

练习:字符自动机

许多人在网上注册账号时,会使用"Jack1996",“toad301"这样的用户名。为了分析取这一类“大小写字母+数字”为ID的用户的特征,我们需要准确识别这样的字符串。下面请你用Verilog HDL编写一个能识别符合这个模式的字符串的电路。我们简称这类字符串为“ID”。ID即所有符合"大小写字母+数字”(严格来说是符合正则表达式**^[a-zA-Z]+[0-9]+$**)的字符串组成的集合。

a.模块规格:

在这里插入图片描述

模块名:id_fsm。

b.功能描述:

每个clk上升沿到来的瞬间,从char读入一个字符c。注意,这里的c是用ASCII编码的,也就是说char这个8位寄存器的值刚好就是c的ASCII码的8位二进制。

设已经读入的字符串为S,换句话说,这个S是由每个clk上升沿时读入的c拼接而成的。我们需要你在每个clk的上升沿完成如下判断:

设读入前的字符串为S,则读入新字符c后字符串为Sc。假如Sc的一个后缀符合本题ID的定义,out置为1;否则out置为0。

一个字符串S=c1c2c3…cn的后缀组成的集合是P={s=cici+1…cn|1<=i<=n}。

比如已读入的字符串为01010abc123,它的一个后缀abc123符合ID的定义,out应输出1;假如是01010abc%,它没有符合要求的后缀(因为所有非空后缀都包含非法符号’%’),因此out应输出0。

波形示例:

输入:char = a b c d 1 2 3 4 /

输出:out = 0 0 0 0 1 1 1 1 0

在这里插入图片描述

本题也是一道比较简单的状态机问题,设置三个状态:00表示读到非法字符,01表示后缀中有连续的字母,10表示后缀中有连续的字母和数字,状态转移表见下面代码中的注释:

// id_fsm.v
module id_fsm(
 input [7:0] char,
 input clk,
 output out
 );

reg [1:0] status = 2'b00;
wire [1:0] charType;

assign out = (status == 2'b10) ? 1'b1 : 1'b0;

/*
 charType 2'b00 -> current char is an illegal charactor
 charType 2'b01 -> current char is an alphabet charactor
 charType 2'b10 -> current char is a digital charactor
*/
assign charType = (char >= 8'd48 && char <= 8'd57) ? 2'b10 : 
               ((char >= 8'd65 && char <= 8'd90) || (char >= 8'd97 && char <= 8'd122)) ? 2'b01 : 2'b00;

/*
 status 2'b00 -> reading illegal charactor or nothing
 status 2'b01 -> reading series of alphabets
 status 2'b10 -> reading series of alphabets and series of digits
 status  |   input   |   next status |   output
 2'b00   |   illegal |   2'b00       |   0
 2'b00   |   alpha   |   2'b01       |   0
 2'b00   |   digit   |   2'b00       |   0
 2'b01   |   illegal |   2'b00       |   0
 2'b01   |   alpha   |   2'b01       |   0
 2'b01   |   digit   |   2'b10       |   1
 2'b10   |   illegal |   2'b00       |   0
 2'b10   |   alpha   |   2'b01       |   0
 2'b10   |   digit   |   2'b10       |   1
*/

always @(posedge clk) begin
 if (status == 2'b00) begin
     if (charType == 2'b01) // isAlpha
         status <= 2'b01;
     else status <= 2'b00;
 end
 else if (status == 2'b01) begin
     if (charType == 2'b10) // isDigit
         status <= 2'b10;
     else if (charType == 2'b01) // isAlpha
         status <= 2'b01;
     else status <= 2'b00;
 end
 else if (status == 2'b10) begin
     if (charType == 2'b10) // isDigit
         status <= 2'b10;
     else if (charType == 2'b01) // isAlpha
         status <= 2'b01;
     else status <= 2'b00;
 end
 else begin
     status <= status;
 end
end

endmodule

值得注意的是,我们可以通过抽离出专门的判断信号charType,并辅以注释,使状态机的逻辑更清晰。


练习:CPU输出序列检查

本题,要求使用Verilog编写程序,对输入的字符序列进行格式检查。

格式检查

在后面使用Verilog搭建CPU的实验中,我们对CPU的正确性检查,主要通过控制台输出的寄存器写入信息与数据存储器写入信息来进行。

两种格式分别为:

  • 寄存器信息:<仿真时间>@<程序计数器地址>: $<寄存器编号> <= <写入数据>,如42@00003004: $​28 <= ff00ff00
  • 数据存储器信息:<仿真时间>@<程序计数器地址>: *<数据存储器地址> <= <写入数据>,如 1746@00003704: *00000018 <= 69b5cca3

当然,想必处于初学状态的你对上面的表述一头雾水,因此我们进行一下易理解的简化**(对其中一些数进行了命名,在Challenge部分会使用到)**,且为了降低难度,在前后加入限定字符’^‘与’#’。

  • 寄存器信息序列:

    <字符'^'><十进制数time><字符'@'><8位十六进制数pc><字符':'><0个或若干空格><字符'$'><十进制数grf><0个或若干空格><字符'<'加'='><0个或若干空格><8位十六进制数data><字符'#'>

    • ^66@0000301c: $1 <= 1e1f831e#
    • ^242@000030f4: $31 <= 19260817#
  • 数据存储器信息序列:

    <字符'^'><十进制数time><字符'@'><8位十六进制数pc><字符':'><0个或若干空格><字符'*'><8位十六进制数addr><0个或若干空格><字符'<'加'='><0个或若干空格><8位十六进制数data><字符'#'>

    • ^338@00003130: *00000088 <= ffffb528#
    • ^7836@000030f4: *00002ffc<=19521025#

现要求你用Moore型有限状态机检查上述两种字符串输入,当序列的一个后缀满足格式时,将相应匹配信号置位一个时钟周期

对于上面所有已命名的无符号数

  • 十进制数至少有1位,且不允许超过4位。
  • 十六进制数必须为8位,10 ~ 15的表示只能使用小写字母a ~ f。

顶层模块定义

顶层模块命名为cpu_checker,端口定义如下:

img

补充说明

  • 每个时钟上升沿执行字符识别,状态转移,并更新输出。
  • 同步复位信号reset拥有最高优先级,若在时钟上升沿有效,优先响应。
  • 保证输入序列出现一周期的符号’^‘后,’#‘一定在下一个’^‘前出现(不会出现连续的符号’^’)。可能出现多个匹配的序列,均需要在程序识别到末尾’#'且匹配时置位输出一周期。
  • 运行开始时的输出为0,保证测试数据在输入序列到来前进行复位。

样例

输入:^ 1 0 2 4 @ 0 0 0 0 3 0 f c :   $ 2   < =   8 9 a b c d e f # ^ 6 4
输出:0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0

HINT

  • 两种复位方式的区别:
    • 同步复位:在时钟上升沿到来时执行,与时钟周期同步。
    • 异步复位:只要reset信号达到上升沿就执行,与时钟周期不同步。
  • 本题状态数较多,建议先设计再编码,并在本地进行充分的仿真测试。可以考虑使用以下方法来减少状态:
    • 统一识别两种序列的相同部分,只对不同部分分开处理。
    • 使用计数变量来统计数字与字母的个数。
  • Verilog中可以使用双引号表示字符,例如"#"。

在这里插入图片描述

上图为本题的参考状态图。依据HINT给出的方法:统一识别两种序列的相同部分,只对不同部分分开处理;使用计数变量来统计数字与字母的个数,可以利用额外的寄存器将状态数减少到14个,但需要注意寄存器的复位和自增。本题作为复杂状态机用以考察Verilog语言的使用和设计仿真测试,是非常合适的。具体的代码如下:

// cpu_checker.v
module cpu_checker(
    input clk,
    input reset,
    input [7:0] char,
    output [1:0] format_type
    );

parameter INIT_STATUS = 4'd0;
parameter INIT_DECIMAL_REG = 3'd1;
parameter DECIMAL_TOP = 3'd4;
parameter INIT_HEX_REG = 4'd1;
parameter HEX_TOP = 4'd8;
parameter INIT_TYPE_REG = 1'b0;
parameter YES = 1'b1;
parameter NO = 1'b0;

reg [3:0] status = INIT_STATUS;             // FSM status
reg [2:0] decimalReg = INIT_DECIMAL_REG;    // Counter of the decimal number bits
reg [3:0] hexReg = INIT_HEX_REG;            // Counter of the hexadecima number bits
reg typeReg = INIT_TYPE_REG;                // Recorder of the cpu_info type, 0 for registers, 1 for memory

wire digit = (char >= "0" && char <= "9") ? YES : NO;
wire hexdigit = (digit == YES) ? YES :
                (char >= "a" && char <= "f") ? YES : NO;

assign format_type = (status != 4'd14) ? 2'b00 :
                     (typeReg == 1'b0) ? 2'b01 : 2'b10;

always @(posedge clk) begin
    if (reset == YES) begin    // If reset, reset all the registers.
        status <= INIT_STATUS;
        decimalReg <= INIT_DECIMAL_REG;
        hexReg <= INIT_HEX_REG;
        typeReg <= INIT_TYPE_REG;
    end
    else begin
        case (status)
            4'd0: begin         // Status 0: reading nothing
                if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd1: begin         // Status 1: reading "^"
                if (digit == YES) begin
                    decimalReg <= INIT_DECIMAL_REG;
                    status <= 4'd2;
                end
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd2: begin         // Status 2: reading decimal number time
                if (char == "@") status <= 4'd3;
                else if (digit == YES) begin
                    decimalReg <= decimalReg + 3'b1;
                    if (decimalReg + 3'b1 <= DECIMAL_TOP) status <= 4'd2;
                    else status <= INIT_STATUS;
                end
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd3: begin         // Status 3: reading "@"
                if (hexdigit == YES) begin
                    hexReg <= INIT_HEX_REG;
                    status <= 4'd4;
                end
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd4: begin         // Status 4: reading hexadecimal number PC
                if (char == ":") begin
                    if (hexReg == HEX_TOP) status <= 4'd5;
                    else status <= INIT_STATUS;
                end
                else if (hexdigit == YES) begin
                    hexReg <= hexReg + 4'b1;
                    if (hexReg + 4'b1 <= HEX_TOP) status <= 4'd4;
                    else status <= INIT_STATUS;
                end
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd5: begin         // Status 5: reading ":", with or without space
                if (char == "$") status <= 4'd6;
                else if (char == " ") status <= 4'd5;
                else if (char == "*") status <= 4'd7;
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd6: begin         // Status 6: reading "$"
                typeReg <= 1'b0;
                if (digit == YES) begin
                    decimalReg <= INIT_DECIMAL_REG;
                    status <= 4'd8;
                end
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd7: begin         // Status 7: reading "*"
                typeReg <= 1'b1;
                if (hexdigit == YES) begin
                    hexReg <= INIT_HEX_REG;
                    status <= 4'd9;
                end
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd8: begin         // Status 8: reading decimal number grf
                if (char == " ") status <= 4'd10;
                else if (char == "<") status <= 4'd11;
                else if (digit == YES) begin
                    decimalReg <= decimalReg + 3'b1;
                    if (decimalReg + 3'b1 <= DECIMAL_TOP) status <= 4'd8;
                    else status <= INIT_STATUS;
                end
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd9: begin         // Status 9: reading hexadecimal number addr
                if (char == " " || char == "<") begin
                    if (hexReg == HEX_TOP) begin
                        if (char == " ") status <= 4'd10;
                        else status <= 4'd11;
                    end
                    else status <= INIT_STATUS;
                end
                else if (hexdigit == YES) begin
                    hexReg <= hexReg + 4'b1;
                    if (hexReg + 4'b1 <= HEX_TOP) status <= 4'd9;
                    else status <= INIT_STATUS;
                end
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd10: begin        // Status 10: reading space after grf or after addr
                if (char == "<") status <= 4'd11;
                else if (char == " ") status <= 4'd10;
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd11: begin        // Status 11: reading "<"
                if (char == "=") status <= 4'd12;
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd12: begin        // Status 12: reading "=", with or without space
                if (hexdigit == YES) begin
                    hexReg <= INIT_HEX_REG;
                    status <= 4'd13;
                end
                else if (char == " ") status <= 4'd12;
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd13: begin        // Status 13: reading hexadecimal number data
                if (char == "#") begin
                    if (hexReg == HEX_TOP) status <= 4'd14;
                    else status <= INIT_STATUS;
                end
                else if (hexdigit == YES) begin
                    hexReg <= hexReg + 4'b1;
                    if (hexReg + 4'b1 <= HEX_TOP) status <= 4'd13;
                    else status <= INIT_STATUS;
                end
                else if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            4'd14: begin		// Status 14: reading "#"
                if (char == "^") status <= 4'd1;
                else status <= INIT_STATUS;
            end
            default: status <= INIT_STATUS;
        endcase
    end
end

endmodule

Challenge:CPU输出合法性检查

本题需要在完成上一题格式检查需求的基础上进行扩充。

合法性检查

如果输入序列一个后缀的格式满足条件,需要根据规则对当前匹配到的后缀序列的合法性进行检查,对于不合法但符合格式规范的序列,将序列错误码置位代表不同错误类型

回顾上面两种序列的格式:

  • 寄存器信息序列:^[time]@[pc]: $[grf] <= [data]#
  • 数据存储器信息序列:^[time]@[pc]: *[addr] <= [data]#

检查规则如下:

  • 增加一个模块输入端口freq,表示CPU的仿真周期,time必须是freq一半的整数倍(保证周期均为2的正整数次幂)
  • pc的范围为0x0000_3000 ~ 0x0000_4fff,且必须字对齐,即必须为4的整数倍
  • addr的范围为0x0000_0000 ~ 0x0000_2fff,且必须字对齐,即必须为4的整数倍
  • grf的范围为0 ~ 31,只要数值在该范围内即判为正确(例如,15和0015均合法)。

顶层模块定义

顶层模块仍命名为cpu_checker,端口定义如下:

img

补充说明与HINT

  • 由于实际情况下,乘除运算单元的执行时间是其他单元执行时间的数倍,因此避免使用乘法和取模运算,使用位运算来代替。(评测时会检查文件是否出现了上述两种字符“*”和“%”,所以需要用ASCII码8’d42来替代addr的前导字符"*",不要使用always @*来描述组合逻辑)
  • 在格式符合规范时,可能同时出现多种合法性的错误,使用独热编码来记录,例如同时出现time和pc错误,要将error_code设置为4’b0011。请回忆前面章节或理论课对独热编码的讲解。
  • 如果格式匹配的是寄存器信息序列,不要将error_code的addr错误位置位;反之如果格式匹配数据存储器信息序列,不要将error_code的grf错误位置位。
  • 其余要求与上题相同,运行开始时两个输出端口均为0,保证测试数据在输入序列到来前进行复位。

本题在上题状态图的基础上增设一些寄存器来记录各个数位的合法情况即可,对合法情况的判定也可以单独编写子模块来实现。由于本人的时间分配原因,challenge本题的具体实现将不再给出。有了上一题之后,我们对于Verilog语言基本语法和设计仿真测试方法已经非常熟悉了,本题请大家凭兴趣实现。

  • 11
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值