HDLBits:在线学习 Verilog (一 · Problem 0-9)

2021-04-15 11:40:40 浏览数 (2)

本系列文章将和读者一起巡礼数字逻辑在线学习网站 HDLBits 的教程与习题,并附上解答和一些作者个人的理解,相信无论是想 7 分钟精通 Verilog,还是对 Verilog 和数电知识查漏补缺的同学,都能从中有所收获。

本文是系列文章的第一篇,讨论下前十道习题和解答,HDLBits 共有约 180 题。Step one - HDLBits

附上网站的链接,在没有特别指出的情况下,题目,教程,图片均为 HDLBits 的内容,本文对其进行了翻译,翻译严格遵循作者 爱怎么来怎么来 的翻译准则。

首先附上传送门,同时打开两个页面一起阅读,收获双份的快乐哦。

https://hdlbits.01xz.net/wiki/Step_onehdlbits.01xz.net

Step one - HDLBitsStep one - HDLBitshdlbits.01xz.net

Problem 0 : Step one

欢迎来到 HDLBits!

数字逻辑电路的学习的挂挡起步往往是艰难的,因为你一开始就发现有一堆东西等着你去学习:新的概念,一种新的硬件描述语言(Hardware Description Language,比如 Verilog),几种软件工具(ISE,Vivado,Modelsim,Quartus等等),以及可能包括一块 FPGA 开发板,所有一切都同时等着你去学习,学习曲线略微陡峭。好在 HDLBits 提供了在线练习简单数字的设计与调试的功能,只需要通过执行页面上的 Simulate 按钮,就能为你的学习助力。

设计一个数字电路需要以下几步:编写 HDL 硬件描述语言,比如使用 Verilog;编译(综合)代码为一个数字电路;仿真分析电路的功能和时序;最后,Kill those bugs。

编写代码

最简单的方式是直接在 HDLBits 网页下方的代码框内编写你的代码,因为网页已经帮你生成了部分代码,比如模块的输入输出端口。

你也可以在其他的编辑器上完成代码的编写,然后使用上传功能,上传你的 v 文件,这样会有些繁琐,但你可以利用到编辑器的语法补全,代码片段等特性。这里推荐开源免费,全功能编辑器,微软推出的 VSCode。

ljgibbs:VSCode 布道指南 V1.0 (一)zhuanlan.zhihu.com

之后点击页面上的 Simulate 按钮,对代码进行编译(综合),仿真。

编译(逻辑综合)

你的代码会通过 Altera Quartus 的综合器综合为硬件电路。Quartus 会生成一组综合信息,通过 Show Quartus messages 可以显示/隐藏他们。通过这些信息可以进行相应的修改,减少代码中的警告,但有些警告就随它去吧。

仿真

你的综合电路会通过功能仿真来检查其功能是否正确。HDLBits 使用 ModelSim 同时仿真你的代码和参考解决方案,然后比较两者的输出。仿真报告关注两个方面:

一是,会报告你的电路的输出与参考电路的输出是否完全一致,(不存在 mismatch)。mismatch 代表某一时刻两者的输出并不一致,正确的电路中不存在 mismatch。

此外,仿真报告会产生你的电路运行测试向量时的输出时序,时序分为三组:输入,你的电路的输出,参考电路的输出。

值得注意的是,不要修改题目中给定的模块以及端口的名称,否则会造成仿真错误。

结果状态

如果你的电路完全正确,那么你会看到 Status: Success! 当然情况也有不妙的时候:

  • Compile Error — 电路综合失败
  • Simulation Error — 电路综合成功,但是仿真存在错误
  • Incorrect — 电路综合,仿真成功,但输出结果和参考结果不同。

你可以通过 My Stats 页面查询自己在所有题目中的状态,以及完成度在所有参与用户中的排名,通过注册账号,可以在多个浏览器上进行访问。

牛刀小试

说了这么多,终于到这道题的练习环节,构建一个电路,没有输入端口,只有一个输出端口,输出端口时钟驱动逻辑 1 ,即逻辑高。

模块的端口已经给出

解答与分析

这个题目很简单,assign one = 1; 即可。

1 在数字逻辑中代表 logic high,而 0 代表 logic low。

通过这题可以了解 HDLBits 的基本操作以及数字逻辑的一些简单概念,在完成正确的提交后,可以通过题目下方 Show Solution 查看到解答。目前看下来大部分的题目是有解答的,但你只有正确提交之后才能查看解答。

Problem 1 : Zero

这题同样要求构建一个电路,没有输入端口,只有一个输出端口,但这次输出端口时钟驱动逻辑 0 ,如果你完成了前一道题,那么这题也自然不在话下。

部分题目提供了 Hint,可以给你提供一些帮助。

比如这题的提示:如果你什么也不做,在 Quartus 中,输出端口会被默认赋值为 0,所以这题超简单,直接提交即可。(当然,使用默认值是危险,相当不推荐的行为)

解答与分析

代码语言:javascript复制
module top_module(
    output zero
);// Module body starts after semicolon
    assign zero = 0;
endmodule

Problem 2 : Wire

这题中我们将认识到 Wire,Verilog 中重要的一种信号类型,我们将通过建立一个模块 (module) 来实现 wire。所谓模块就是前两题中我们构建的东西,拥有输入输出端口的黑盒,在之后我们会详细讲解模块,

wire 的中文可以翻译为导线,但 Verilog 中的 wire 和现实中的导线不同,wire 应该理解为一个信号,信号是有方向性的,wire 从 A 点输出,输入到 B 点和 C 点。wire 一般只有一个 source,即从某一点输出,但可以有多个 sinks,即输入到多个点。A 点通常会被称为一个驱动(driver),把某个值驱动到 wire 上。

把驱动的概念引进到 Verilog 中,可以写作:

assign left_side = right_side;

right_side 的值就被驱动到 left_side 中,以上的语法结构名为连续赋值(continous assignment)。但请注意与软件中的赋值操作做区分,Verilog 中的赋值是使用一条带有方向的导线连接了两个信号,所以 left_side 始终等于 right_side,随 right_side 变化而变化。而软件中的赋值是一种事件,某个时刻 left_side 的值变成了和 right_side 相同的值。

模块中的端口也带有方向性,主要分为输入 input 和输出 output 端口。输入端口是由模块外部的信号驱动的,而输出端口则又会驱动另一个外部信号。如果我们通过一个模块来模拟 wire,那么从模块内部来看,输入端口就直接驱动输出端口。

图片来自 HDLBits

图片来自

我们的题目和上图息息相关,模块和端口已经被定义好了,黑色的框图以及箭头代表模块和端口。而外部的驱动信号和模块下游的信号也已经给出,即图中灰色的部分。你要做的工作是完成图中绿色的部分,即完成这条连线。

你可以在模块体中使用一条 assign 语句,将输入端口的值赋给输出端口来完成这个模块。你不需要考虑黑框外部的信号,那些事用来测试你模块的信号,将会由我们来完成。

除了连续赋值,Verilog 还有三种其他的赋值方式,这三种赋值方式都只能在过程块中使用,我们将在后续的题目中讨论。

解答与分析

代码语言:javascript复制
module top_module( input in, output out );
	assign out = in;
// Note that wires are directional, so "assign in = out" is not equivalent.
//注意 wire 是有方向的 因此 assign in = out 是不等价的
endmodule

代码非常简洁,但始终要注意思考代码和电路,和上图中模块描述的关联。

另外 wire 是 Verilog 中的一种数据类型,代表的是信号,而不是连线。

在这里可以对 module 和连续赋值抱有疑惑,我们将在后续的内容中继续讨论。

Problem 3 : Wire4

前文中我们讨论过,wire 的源一般只能有一个,终点确可以有多个。一个源可以驱动多个信号。本题中,我们要使用三个信号源 a,b,c 驱动四个信号 w,x,y,z.

a -> w b -> x b -> y c -> z

从模块的角度来说,有三个输入端口和四个输出端口,上图给出了信号的流向。

当你使用多条 assign 语句时,他们之间的顺序是无关紧要的,这点同顺序执行的软件代码不同。事实上,大部分 Verilog 代码之间的顺序都不会对结果产生影响。assign 描述的是端口之间的连接关系,而不是一次复制右值,赋给左值的复制黏贴,连接关系不存在先后之分。

这里要澄清一个容易混淆的概念,图中的绿线代表的是 wire 之间的连接,而不是 wire 本身。即 wire 是连线两端的信号,而不是连线本身。上图中的模块实际声明了 7 个 wire 信号(a, b, c, w, x, y, z)。这是因为模块的输入输出端口实际上都是 wire。

我们一般这么声明端口信号

input a;

但实际上我们声明的是

input wire a;

所以,assign 语句并不是创建 wire ,而是将创建 wire 之间的连接。

解答与分析

代码语言:javascript复制
module top_module (
	input a,
	input b,
	input c,
	output w,
	output x,
	output y,
	output z  );
	
	assign w = a;
	assign x = b;
	assign y = b;
	assign z = c;
	
endmodule

wire 是信号,而 assign 语句则建立了信号之间的连接,这种连接是有方向性。

模块的输入输出端口也同样是 wire

Problem 4 : Notgate

非门在数字电路中十分常见,本题我们要通过 assign 语句以及 Verilog 的逻辑操作符,实现一个非门模块。

与 wire 模块相同,非门模块中 in 被连接到 out,相比 wire 模块,唯一的区别在于:输出信号 out 是将输入信号 in 取反得到。

我们在 assign 语句中增加的逻辑操作符为 ~(逐位取反),由于我们的信号位宽为 1 位,我们也可以使用!(逻辑取反)。二者的区别在于逻辑取反的结果时钟只有一位,而逐位取反结果的位宽和输入信号位宽相同,在每一个位上逐位(bitwise)取反。

解答与分析

代码语言:javascript复制
module top_module (
	assign	out = ~ in;
endmodule

非门在 wire 的模块上稍加改造,对 assign 语句添加逻辑运算符实现。

Problem 5 : Andgate

本题要求使用 Verilog 语言描述一个模块,实现与门的作用。

从第 4 题开始,是用 Verilog 描述各种 “门”,这也就是 Verilog 硬件描述语言中,描述二次的由来。描述就是我们用 Verilog 的语法,通过写下几句代码来实现一个电路。从最简单的门到 CPU 都可以使用 HDL 描述。

题目给出的模块如下图,有三个 wire : a,b 以及 out。a,b 信号已经由模块的输入端口驱动,但图中黑色的部分中,wire out 还没有被任何信号驱动。本题要写一个 assign 语句,使 a,b 信号经过与门的输出驱动 wire out 信号。

显然,assign 语句的实现和前一题非常接近,只是增加了一个输入信号。和前一题不同的是,我们在这里强调了信号是被驱动(drive)的,被驱动的含义可以理解为,该信号的取值取决于另一个连接到它的信号的值,该信号的值随着另一个信号的值改变而改变。下图中模块的输入端口 input wire 被外部连接到模块的信号所驱动。assign 语句映射到具体的硬件上,就是产生了信号的驱动,由右值驱动左值。

说道 assign,如果你有过一些思考的话(你有思考嚒),一个 wire 信号不能被多个信号同时驱动(当一个信号说往东,另一个信号说往西,两个信号还要同时驱动我时,我到底该往哪?)。另一个方面,一个没有驱动者(driver)的信号的值会处于未定义的状态,可怜的家伙,都没有司机,好在综合器一般会免费给他安排一个,将其信号值驱动为 0.

解答与分析

代码语言:javascript复制
module top_module(
    input a,
    input b,
    output out );
	assign	out = a & b;
endmodule

值得注意的是 & 和 && 的区别,& 是逐位与,而 && 是逻辑与。

Problem 6 : Norgate

本题要求使用 Verilog 实现一个 NOR 门,注意这里其实是或非门,而不是更常见的异或门,或非门是或门的输出取反。

assign 语句将某个值赋予 wire 信号,这个 value 可以是常量,也可以是一个复杂的逻辑表达式,综合器会综合出相应的逻辑门实现。assign 语句代表的始终是连续赋值,因为当输入信号改变时,输出信号会重新“计算”。和一个逻辑门的工作方式相同,输入改变,输出对应改变。

解答与分析

代码语言:javascript复制
module top_module(
    input a,
    input b,
    output out );
    assign	out =~ (a | b);
endmodule

注意括号的由来,因为 ~ 非逻辑的优先级大于 | 或。

Problem 7 : XNorgate

XNor 的中文是什么,作者也愣了愣神,其实应该是同或门。

我们首先复习下数电,同或门 (XNor Gate) 是异或门 (Nor Gate) 的取反输出。异或门的输入输出可以概括为:(输入)相同(输出)为 0 ,不同为 1 。

解答与分析

代码语言:javascript复制
module top_module(
    input a,
    input b,
    output out );
    assign	out =~( a ^ b);
endmodule

这里你就会发现硬件描述语言的好处,其实你把数电的知识还给老师了,不记得相同为 0 ,不同为 1,似乎写出这道题问题也不大……

^ 为逐位异或,Verilog 中不存在逻辑异或符号。

Problem 8 : Declaring wires

到本题为止,我们的电路都十分简单,你是否觉得 Verilog 就这么简单呢,那我要说 是的,就这么简单。(逃<=)

言归正传,之前电路足够简单,我们能直接表示出输入输出信号的逻辑关系,但如果电路变得复杂,那么我们就需要一些中间信号来帮助我们简化描述电路的难度。

定义中间信号的语法格式为

代码语言:javascript复制
wire foo ;

信号定义语句需要放置于模块的 body 中,就好比 C 语言中,你的中间临时变量需要定义在 main 函数函数体中。模块的 body 指的就是 module 和 endmodule 中间的部分。

这里建议先定义信号,再使用信号,就像 C 语言中一样。原则上,你可以在任何位置定义你的信号,使用前使用后都可以,正如之前的课程中说的那样,语句的顺序对于 Verilog 来说没有关系。但有些仿真工具需要你在使用信号之前定义信号,So,你就这么来吧。

举个栗子

上述模块中,存在三个 wire (in, out, and not_in),其中两个信号已经随着模块的定义而定义了,分别定义为模块的输入输出 wire,这也就是为什么在前面的题目中不需要额外定义 wire 信号的原因。而 not_in 信号定义于模块中,对于模块外部来说它是不可见的。然后,我们通过 assign 语句定义了两个非门,使用到了中间信号 not_in。

看到这里,你说我不需要中间信号,我只需要 assign out = ~~in; 就行。没错。

但现在电路仍然比较简单,很容易描述出前一级的输出,但如果前一级的输出很复杂,那么要使用 assign 语句描述出两级电路的输入输出关系集合就比较困难。使用硬件描述语言的好处在于,你可以描述出前一个模块的输出,将其赋给中间信号,并将中间信号作为下一级信号的输入。这样,你永远只需要一次描述一个模块。

牛刀小试

实现下图中的模块。首先创建两个中间信号将与门和或门连接起来,信号的名字随你的便,但好的名字往往影响一个信号的一生,若干年后,你还能依稀记起当年定义这个信号的峥嵘岁月。

注意,与门的输出信号也就是或门的输入信号,所以你不需要再定义或门的输入信号。再提醒一下,信号只能被一个信号驱动,但能驱动多个信号。

按照下图中的逻辑关系,你的代码应该有 4 个 assign 语句,对应四个逻辑门,或者说模块。

解答与分析

代码语言:javascript复制
`default_nettype none
module top_module(
    input a,
    input b,
    input c,
    input d,
    output out,
    output out_n   );
    wire	and_1 = a & b;
    wire	and_2 = c & d;
    wire	or_1  = and_1 | and_2;
    assign	out   = or_1;
    assign	out_n = ~or_1;
endmodule

`default_nettype none 是一个宏定义语句,我们将在后续的课程中探讨它。

你问我的答案为什么没有说好的 4 个 assign 语句,因为我在定义 3 个中间信号的同时,还给它们赋了值,这在 Verilog 语法中也是允许的。如果你想看 4 个 assign 语句的答案,可以在完成提交后,通过 Show solution 查看解答。

Problem 9 : 7458

本题要实现个稍稍复杂的电路:数电芯片 7458 。它有 10 个输入信号,2 个输出信号。你可以选择对每个输出信号,使用一个 assign 语句,也可以先产生第一级逻辑门输出的 4 个中间信号。有时间的话,两种方式都可以尝试下。

解答与分析

代码语言:javascript复制
module top_module (
    input p1a, p1b, p1c, p1d, p1e, p1f,
    output p1y,
    input p2a, p2b, p2c, p2d,
    output p2y );
    assign p1y = (p1a & p1b & p1c) | (p1d & p1e & p1f);
    assign p2y = (p2a & p2b) | (p2d & p2c);
endmodule

作者使用的是第一种方法,你可以在这里尝试一下上一题中讲授的创建中间信号的方法。

显然,完成这题需要你认识逻辑门的符号,一点儿耐心和好一点的视力,后两者对于 IC 从业人员来说很重要。

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

原文作者:ljgibbs 原文地址:https://zhuanlan.zhihu.com/c_1029044037684183040 本公众号授权发布

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

0 人点赞