目录
文章目录
一、Verilog编程入门
1.1 门电路入门练习
(1)与门
本题要求使用 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.
代码:
module top_module(
input a,
input b,
output out );
assign out=a&b;
endmodule
结果:
(2)或非门
本题要求使用 Verilog 实现一个 NOR 门,注意这里其实是或非门,而不是更常见的异或门,或非门是或门的输出取反。
assign 语句将某个值赋予 wire 信号,这个 value 可以是常量,也可以是一个复杂的逻辑表达式,综合器会综合出相应的逻辑门实现。assign 语句代表的始终是连续赋值,因为当输入信号改变时,输出信号会重新“计算”。和一个逻辑门的工作方式相同,输入改变,输出对应改变。
代码:
module top_module(
input a,
input b,
output out );
assign out=~(a|b);
endmodule
结果:
(3)同或门
XNor 的中文是什么,其实应该是同或门。
我们首先复习下数电,同或门 (XNor Gate) 是异或门 (Nor Gate) 的取反输出。异或门的输入输出可以概括为:(输入)相同(输出)为 0 ,不同为 1 。
代码:
module top_module(
input a,
input b,
output out );
assign out = ~(a^b);
endmodule
结果:
1.2 组合电路入门练习
(1)Declaring wires
之前电路足够简单,我们能直接表示出输入输出信号的逻辑关系,但如果电路变得复杂,那么我们就需要一些中间信号来帮助我们简化描述电路的难度。
定义中间信号的语法格式为
wire foo ;
信号定义语句需要放置于模块的 body 中,就好比 C 语言中,你的中间临时变量需要定义在 main 函数函数体中。模块的 body 指的就是 module 和 endmodule 中间的部分。
这里建议先定义信号,再使用信号,就像 C 语言中一样。原则上,你可以在任何位置定义你的信号,使用前使用后都可以,正如之前的课程中说的那样,语句的顺序对于 Verilog 来说没有关系。但有些仿真工具需要你在使用信号之前定义信号,So,你就这么来吧。
实现下图中的模块。首先创建两个中间信号将与门和或门连接起来,信号的名字随你的便,但好的名字往往影响一个信号的一生,若干年后,你还能依稀记起当年定义这个信号的峥嵘岁月。
注意,与门的输出信号也就是或门的输入信号,所以你不需要再定义或门的输入信号。再提醒一下,信号只能被一个信号驱动,但能驱动多个信号。
按照下图中的逻辑关系,你的代码应该有 4 个 assign 语句,对应四个逻辑门,或者说模块。
代码:
`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
结果:
(2)7458芯片
本题要实现个稍稍复杂的电路:数电芯片 7458 。它有 10 个输入信号,2 个输出信号。你可以选择对每个输出信号,使用一个 assign 语句,也可以先产生第一级逻辑门输出的 4 个中间信号。有时间的话,两种方式都可以尝试下。
代码:
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
结果:
(3)7420 chip
7420 chip是拥有两组4输入的与非门芯片,本练习需要构造一个与7420 chip功能一样的电路,拥有8个输入与2个输出。
代码:
module top_module
(
input p1a, p1b, p1c, p1d,
output p1y,
input p2a, p2b, p2c, p2d,
output p2y
);
assign p1y = ~(p1a & p1b & p1c & p1d);
assign p2y = ~(p2a & p2b & p2c & p2d);
endmodule
结果:
1.3 时序电路入门练习
(1)D flip-flop (Dff)
接下来的题目是属于触发器,锁存器的专题。我们会从用 Verilog 实现基础 D 触发器开始,学习触发器这一数字电路中最重要的电路之一。
D 触发器是一个电路,存储 1bit 数据,并定期地根据触发器的输入(d)更新这 1 bit 数据,更新通常发生在时钟上升沿(clk)。存储的数据会通过输出管脚(q)输出。
t1时刻: d -> 0
t2时刻: clk->1 上升沿到来,触发器存储的数据变成 0,输出 q 保持为存储的值:0,直到下一个时钟上升沿到来。
t3时刻: d -> 1(d:我变了),q 仍保持 0 不动摇(时钟沿还没来呢)
t4时刻: clk->1 上升沿到来,q->1(q:时钟沿来了,我该变身了)
绝大多数时候,我们不会在 Verilog 代码中显示例化一个触发器(作者没这么做过,但应该是可以做的),我们在时钟敏感的 always 块中的语句一般都会被综合工具转换为相应的触发器。
D 触发器可以认为是一个触发器和一段最简单的组合逻辑块(blob :想表达逻辑块的时候用我,别用 block)的组合。其中组合逻辑块仅仅是一段 wire。(q 直接输出了触发器的存储值)
代码:
module top_module (
input clk, // Clocks are used in sequential circuits
input d,
output reg q );//
// Use a clocked always block
// copy d to q at every positive edge of clk
// Clocked always blocks should use non-blocking assignments
always@(posedge clk) begin
q <= d;
end
endmodule
结果:
在每个时钟上升沿,输出 q 的值变为输入 d 的值。我们在 always 块中的语句就会被综合工具转换为触发器。这里使用 clk,q,d 对应于触发器的三个端口,反应了转换的对应关系。
(2)D flip-flops (Dff8)
实现 8 个 D 触发器,听上去好像很累的样子,实则 Verilog 语言的抽象帮助我们省去不少麻烦。输入 a,b 的位宽变为 8 位,但 always 块中的语句与上一题完全相同。
综合工具根据位宽,综合出了 8 个 D 触发器。一般没有八位触发器的说法。这里反映了综合工具能分析代码,生成相应的触发器电路,其实综合器还能将复杂得多的语句转为相应的电路。
代码:
module top_module (
input clk,
input [7:0] d,
output [7:0] q
);
always@(posedge clk) begin
q <= d;
end
endmodule
结果:
(3)DFF with reset (Dff8r)
在上题的 8 个 D 触发器基础上,这题我们要给触发器配上同步复位端口。
什么同步复位?当时钟上升沿到来时,如果同步复位端有效(本题中复位高电平有效,即 reset),那么任凭你触发器此前输出或者输入的是 0,是 1,输出一律变为 0。
复位电路对于那些经常需要恢复到初始状态的电路是必要的,复位相较于断电重新加载程序恢复到初始状态的速度要快得多。但也有一些电路则不需要复位设计。(作者也是有所耳闻那些不需要复位的电路,平常自己还是会加上复位电路)
代码:
module top_module (
input clk,
input reset,
input [7:0] d,
output [7:0] q
);
always@(posedge clk) begin
q <= (~{8{reset}} & d);
end
endmodule
从语法的角度来说,reset 实现就是加一个 if 语句判断 reset 是否有效,有效就将输出 q 置为 0 。
但从电路的角度来说,电路的角度往往更加重要,是使用一个带有复位端的 D 触发器 fdr,另一种 D 触发器单元。
结果:
二、Logisim进行仿真设计
2.1 认识全加器
2.1.1 半加器
半加器是指对输入的两个一位二进制数相加(A与B),输出一个结果位(SUM)和进位(C),没有进位的输入加法器电路,是一个实现一位二进制数的加法电路。
真值表
被加数a | 加数a | 和s | 进位c |
---|---|---|---|
0 | 0 | 0 | 0 |
0 | 1 | 1 | 0 |
1 | 0 | 1 | 0 |
1 | 1 | 0 | 1 |
逻辑表达式
根据上述的真值表,当A和B相同时SUM为0,否则为1;逻辑关系属于异或;当A和B同时为1时,C等于1,其余都为零,逻辑关系为与。
所以我们可以得到如下的逻辑表达式:
逻辑电路图
2.1.2 全加器
全加器是指对输入的两个二进制数相加(A与B)同时会输入一个低位传来的进位(Ci-1),得到和数(SUM)和进位(Ci);一位全加器可以处理低位进位,并输出本位加法进位。多个一位全加器进行级联可以得到多位全加器。常用二进制四位全加器74LS283。
真值表
被加数a | 加数b | 低进位cin | 和s | 进位cout |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 0 | 1 | 1 | 0 |
0 | 1 | 0 | 1 | 0 |
1 | 0 | 0 | 1 | 0 |
1 | 1 | 0 | 0 | 1 |
0 | 1 | 1 | 0 | 1 |
1 | 0 | 1 | 0 | 1 |
1 | 1 | 1 | 1 | 1 |
逻辑表达式
由全加器的定义理解我们可以知道当Ai和Bi异或后再与Ci-1进行异或得到SUMi,结合真值表,我们可以知道当Ai、Bi、Ci-1只要有两个以上的1是进位Ci就等于1;所以只需要每两变量求与,结果再求或就可以满足要求。由此我们可以得到最常用的逻辑表达式:
逻辑电路图
根据逻辑表达式绘制两种逻辑电路图如下:
2.2 Logisim完成一个1位全加器的设计并测试
2.2.1 先设计设计一个1位半加器电路
采用异或门和与门结合设计一个简单的半加器:
2.2.2 在半加器电路基础上,实现一位全加器电路
在设计全加器时,因为会有三个数进行运算,因此可以简化为先进行两个输入数的运算,再进行其和与进位数的运算,即两次1bit运算。同时,在计算结果时,除了结果数,还要提出进位数供下一位计算。因此,在Logisim中,我们可以采用两个半加器相结合的方式设计一个全加器,参考如下:
三、基于Quartus进行实验并仿真
3.1 输入原理图实现1位加法器
创建工程项目过程:
启动 Quartus软件,选择File->New Project Wizard
填写工程名称:
选择第一个,直接Next
这个界面不用管,直接next
根据使用的FPGA,进行选择芯片系列及类型
直接next
完成工程创建,点击finish
3.1.1 半加器原理图输入
(1)绘制过程实现
首先选择File->New,进入后选择Block Diagram/Schematic File
选择元件
添加输入输出,完成效果:
保存文件,并编译
通过tool->Netlist Viewers->RTL Viewer,查看电路图
(2)仿真实现
创建一个向量波形文件,选择菜单项 File→New->VWF
出现这个页面后,双击左侧空白处
出现这个弹窗,点击Node Finder
然后添加进来,点击ok
最后出现波形:
随便编辑一下信号
保存文件并启动功能仿真,出现错误,别慌,进行仿真配置,选择tool->launch simulation library complier
(3)仿真结果
重新测试仿真:
功能仿真结果:
时序仿真结果:
通过仿真结果,可以发现得到的结果与真值表中是相吻合的。
3.1.2 全加器原理图输入
利用半加器元件实现全加器
(1)将设计项目设置为可调用的元件
在打开半加器原理图文件half_adder.bdf的情况下,选择菜中File中的Create/Update→CreateSymbolFilesforCurrentFile项,即可将当前文件h_adder.bdf变成一个元件符号存盘,以待在高层次设计中调用
(2)绘制过程实现
首先选择File->New,进入后选择Block Diagram/Schematic File
选择元件
添加输入输出,完成效果
保存文件,并编译
通过tool->Netlist Viewers->RTL Viewer,查看电路图
(3)仿真实现
创建一个向量波形文件,选择菜单项 File→New->VWF
跟半加器同理添加信号,然后编辑信号
保存文件并启动功能仿真,出现错误,进行仿真配置,选择tool->launch simulation library complier(同理半加器)
(4)仿真测试结果
功能仿真结果
时序仿真结果
通过仿真结果,可以发现得到的结果与真值表中是相吻合的。
四、Verilog编程实现1位加法器
创建Verilog文件
4.1 代码实现
module full_adder(
//输入信号,ain表示被加数,bin表示加数,cin表示低位向高位的进位
input ain,bin,cin,
//输出信号,cout表示向高位的进位,sum表示本位的相加和
output reg cout,sum
);
reg s1,s2,s3;
always @(ain or bin or cin) begin
sum=(ain^bin)^cin;//本位和输出表达式
s1=ain&cin;
s2=bin&cin;
s3=ain&bin;
cout=(s1|s2)|s3;//高位进位输出表达式
end
endmodule
保存并编译文件
通过tool->Netlist Viewers->RTL Viewer,查看电路图
4.2 仿真实现
创建一个向量波形文件,选择菜单项 File→New->VWF
同理添加信号并编辑信号
功能仿真结果
时序仿真结果
通过仿真结果,可以发现得到的结果与真值表中是相吻合的。
五、下载测试
5.1 1位全加器测试
(1)点击引脚配置
(2)配置引脚
使用3个SW作为输入信号,2个LED作为输出信号
(3)原理图
(4)代码下载
代码下载到FPGA板子上
(5)测试结果
a为,b为1,输入的cin为1,则s=1,进位cout=1,其他测试也通过
六、四位全加器(实验重点)
6.1 四位全加器的原理图设计
同理将一位全加器设置为可调用的元件,然后设计出原理图
(1)原理图
(2)RTL电路图如下
(3)仿真测试
设置信号:
(4)仿真结果
功能仿真结果
时序仿真结果
6.2 四位全加器的Verilog编程实现
(1)创建文件:
(2)代码实现
//数据流描述4位全加器
module four_adder1(
input[3:0] a,b,
output[3:0] s,
output cout,
input cin
);
assign{cout,s} = a+b+cin;
endmodule
(3)RTL电路图
编译代码得到的RTL电路图
(4)上板烧录
引脚选择
引脚在板子上的位置对应关系:
实物图:
(5)测试结果(重点检验)
此次测试的板子LED1电压不稳定,始终保持亮状态,测试中我们忽略
(1)b3为1,其余为0,结果为1 0 0 0 ,SW7为1,LED4亮,无进位,测试成功
(2)a3和b3为1,其余为0,结果为1 0 0 0 0,SW3和SW7为1,LED0亮,有进位,cout=1,测试成功
(3)b2为1,其余为0,结果为0 1 0 0,SW6为1,LED3亮,无进位,测试成功
结果为跟其真值表相吻合。nice!
总结
这次实验内容很多,基本上是全新的知识点(对于大二没有学过模电数电的我),从软件安装到环境配置过程中踩了很多雷,基本上每个坑都花了几十分钟上网寻找解决方案,但是逐渐摸索的感觉真的很有自豪感,尤其是自己懂得半加器、全加器和四位全加器的原理之后,画电路原理图很是得心应手,但是在组件接线的时候一定要细心,稍有不慎就有一个点漏接了(我就是因为线没接上,一直以为板子有问题,调试了半个钟)。同样这次实验也学会了很多,初步了解了Verilog编程的思想,总体来说,遇到困难和bug不要畏惧,学会使用搜索工具,总能在互联网上找到大佬给出的解决方法,在这里非常感谢大佬给出的各种建议和帮助。
参考
详细讲解半加器、全加器、四位全加器,并使用FPGA实现半加器、全加器
利用Logisim设计半加器、全加器及补码电路