数字电路¶
Abstract
数字电路是计算机体系结构的基础,它研究逻辑门、触发器等基本电路元件,用于实现二进制运算和存储。计算机体系结构则在此基础上设计处理器、内存等硬件组件,并定义指令集和系统架构,以构建完整的计算机系统。简言之,数字电路提供底层硬件支持,计算机体系结构则负责系统级设计与优化。
基础知识¶
- 门 Gate
- 触发器 Flip-Flop:内存的基本单位
- D 触发器
- JK 触发器
- T 触发器
- SR 触发器
- 锁存器 Latch
- 寄存器 Register
FPGA 原理¶
LUT、CLB 和 SB 实现了 FPGA
- LUT:Look Up Table,查找表,是 FPGA 的基本单元,用于实现逻辑功能。
- CLB:Configurable Logic Block,可配置逻辑块,是 FPGA 的基本单元,由 LUT、寄存器和其他逻辑组成。
- SB:Switch Box,开关盒,是 FPGA 的基本单元,用于连接 CLB 和其他 CLB。
其他杂项¶
- BGA:Ball Grid Array,球栅阵列,是一种封装形式。
- IC:Integrated Circuit,集成电路。
- AIC:Application-Specific Integrated Circuit,专用集成电路。
- FPGA:Field-Programmable Gate Array,现场可编程门阵列。
- IP 核:Intellectual Property Core,知识产权核,是一种预先设计好的电路模块,可以在 FPGA 中使用。
硬件描述语言¶
硬件描述语言(Hardware Description Language,HDL)是一种用于描述电子电路和系统的语言。它们允许设计者以文本形式定义电路的结构、行为和时序特性。HDL 主要用于数字电路设计,尤其是在集成电路(IC)和现场可编程门阵列(FPGA)的设计中。
目前主流的硬件描述语言有:
-
Verilog 与 SystemVerilog:
flowchart n1["IEEE Std1364-2005<br>Verilog"] n2["IEEE Std1800-2005<br>SystemVerilog"] n3["IEEE Std1800-2009<br>SystemVerilog"] n2 --- n3 n1 --- n3 n4["IEEE Std1800-2023<br>SystemVerilog"] n3 --- n4
Difference between verilog and system verilog? : r/Verilog
I want to add SV and verilog are not like C and C++ in that they are two different standards that sometimes are in tension or out of sync. SV and Verilog are in the same standard with Verilog a subset of SV, so Verilog should be valid to any SV parser. Though as you say, SV disambiguated Verilog constructs so it is superior in many ways.
Verilog in a way is just archaic System Verilog.
-
VHDL:
flowchart n1["IEEE Std1076-2019<br>VHDL"]
Verilog¶
Quote
- 1364-2005 - IEEE Standard for Verilog Hardware Description Language:Verilog 语言标准。
- Verilog by Example: A Concise Introduction for FPGA Design:短小精悍的一本书。
- Verilog 数字系统设计教程(第 3 版):内容翔实的一本书,但是
- HDL Bits:一个 Verilog 练习网站,内容从最基本的门电路到复杂的状态机。
-
运算符:
分类 运算符 说明 按位 ~
非 NOT\|
或 OR^
异或 XOR&
与 AND~^
同或 XNOR~&
与非 NAND规约:接受一个向量操作数,返回一个单比特值
长度不一时,右端对齐,补零逻辑 &&
与 AND\|\|
或 OR!
非 NOT赋值 =
赋值<=
非阻塞赋值拼接 {a,b}
拼接:将多个操作数连接成一个更大的操作数 重复 {num{replicate}}
重复:将一个操作数重复多次,如 {3'd5, {2{3'd6}}}
位移 <<
左移>>
右移<<<
算术左移>>>
算术右移条件 ?
条件运算符
System Verilog¶
Quote
开发工具¶
Vivado¶
略。
开源工具链¶
Verilator¶
一、数据类型、变量、运算符号、基本语法规则¶
和学习 C 语言的过程一样,我们简单看一眼 Verilog 具有的数据类型、变量和运算符号。
数据类型¶
或许你会想,硬件描述语言为什么要有数据类型。人们只是出于编程语言的惯例,把硬件(如触发器等)抽象成了数据类型。我们可以从数字逻辑电路中抽象出的数据类型有:
wire
:抽象自模块内部的连线,不能存储数据。reg
:抽象自寄存器,可以存储数据。
它们都有位宽的概念,如 wire [7:0] a;
表示 a
是一个 8 位的连线。方括号里的上下限由你自己决定,比如也可以写成 wire [8:1] a;
。习惯上从 0
开始。
此外还有很多数据类型,用得较少,暂不介绍。
常量¶
Verilog 中的常量都是整型(integer),表示方式为:
可以使用的进制有:
- 二进制:
b
或B
- 八进制:
o
或O
- 十进制:
d
或D
- 十六进制:
h
或H
值除了各进制对应的数字外,还可以是 x
(未知值)和 z
(高阻值,也可以表示为 ?
)。此外,还可以用下划线 _
分隔,便于阅读。
举几个例子:
8'b1010_1100 // 8 位二进制数 10101100
4'b10x0 // 4 位二进制数,第三位为不定值
12'dz // 12 位十进制数,全为高阻值
8'h4x // 8 位十六进制数,前 4 位为 4,后 4 位为不定值
二、初识模块和测试¶
接下来我们将认识 Verilog 的基本组件——模块,并理解 Verilog 的三种编程模型:逻辑功能描述、内部连线描述、原语描述。
Verilog 设计层次
除了这三种编程模型,Verilog 还可以在 5 种层次上进行设计:系统级、算法级、寄存器传输级、门级、原语级。这些内容比较综合,暂时不进行介绍。
定义一个模块¶
一个 Verilog 模块就像 C 语言中的函数,我们需要定义它的名称、接口和逻辑功能。Verilog 模块被 module
和 endmodule
包围,module
紧跟着模块名和接口,如下所示:
input
和 output
关键字的部分是 I/O 说明,它也可以写在端口声明语句,如:
习惯上,输出引脚被放在前面。
二选一选择器的三种 Verilog 描述
module muxtwo(out, a, b, sl);
input a, b, sl;
output out;
reg out;
always @ (a or b or sl)
if (sl)
out = b;
else
out = a;
endmodule
这是逻辑功能描述,用类似 C 语言的语法(if-else
语句等)描述了模块的功能。从这段代码中,我们能立刻读懂模块的功能,但无法直接看出模块内部的连线方式。
接下来,我们要编写一个测试来验证我们写的选择器是否正确,验证的思路很简单,就是把所有可能的输入都输入一遍,然后检查输出是否正确。
include "muxtwo.v"
module t;
reg a, b, sl;
wire out;
muxtwo m1(out, a, b, sl);
initial begin
a = 0; b = 0; sl = 0;
end
#10;
a = 0; b = 0; sl = 1;
#10;
a = 0; b = 1; sl = 0;
#10;
a = 0; b = 1; sl = 1;
#10;
a = 1; b = 0; sl = 0;
#10;
a = 1; b = 0; sl = 1;
#10;
a = 1; b = 1; sl = 0;
#10;
a = 1; b = 1; sl = 1;
endmodule
编程模型¶
在书中展示了几种 Verilog 编程方式:
- 逻辑功能描述:使用
if
等逻辑语句描述该模块的功能 - 内部连线描述:使用
wire
定义模块内部的连线,使用assign
连接连线和逻辑 - 原语描述:使用
and
、or
等原语描述模块的功能
数逻教材中的大部分 Verilog 都采用后两种编程方式结合。
模块的结构、数据类型、变量和基本运算符号¶
模块¶
两个部分,I/O 接口和逻辑功能。
基本模块结构如下:
接下来就是内部信号说明(wire
和 reg
等的声明)和功能定义(逻辑部分)
功能定义¶
有 3 种方式能够产生逻辑:
assign
声明语句- 实例元件
and #2 ul(a, b, c);
always
块
assign
最常用,always
既可描述组合逻辑,也可描述时序逻辑,手段较多。
并行性
如果把上面三个部分放到一个模块中,它们之间是并行的。它们之间的顺序不会影响实现的功能。
always
模块内,语句是顺序执行的。
使用模块¶
有两种方式:
- 严格按照定义的端口顺序来连接。
- 用
.portname(signal)
来连接:
第二种方式不必按端口顺序对应,提高可读性和可移植性。
常量-parameter
类型¶
描述方式:parameter <name1> = <expression1>, <name2> = <expression2>, ...;
表达式必须是常量表达式。
示例:
常见用法:
-
实例化时传参指定
-
使用
defparam
语句改变任意模块的参数值
变量¶
-
网络数据类型:表示结构实体之间的连接
-
wire
:单驱动 -
tri
:多驱动 - 寄存器
reg
:数据存储单元的抽象
-
-
memory
型:对reg
建立数组
运算符和表达式¶
四则、位移、逻辑运算不再介绍。
-
位运算
-
三目运算
-
拼接运算
进阶:使用更加基础的工具¶
提前准备、学习以下工具,它们用于 Verilog 的编译、仿真、综合等。
verilator
:Verilog 编译器
看看系统贯通课的一个 Makefile
:
DIR_SRC := src
DIR_BUILD := build
VERILATOR_TOP := Adder_1
VERILATOR_SRCS := $(shell find $(DIR_SRC) -name "*.v" -o -name "*.cpp")
VERILATOR_TFLAGS := -Wno-WIDTH
VERILATOR_FLAGS := --trace --cc --exe --Mdir $(DIR_BUILD) --top-module $(VERILATOR_TOP) -o $(VERILATOR_TOP) -I$(DIR_SRC)
.PHONY: all wave clean
all: clean
verilator $(VERILATOR_TFLAGS) $(VERILATOR_FLAGS) $(VERILATOR_SRCS)
make -C $(DIR_BUILD) -f V$(VERILATOR_TOP).mk $(VERILATOR_TOP)
cd $(DIR_BUILD); ./$(VERILATOR_TOP)
wave:
gtkwave $(DIR_BUILD)/V$(VERILATOR_TOP).vcd
clean:
rm -rf build
编译时抄抄就好。注意到其中几个关键的参数:
--top-module
:编译出的模块名
verilator
将在 DIR_BUILD
编译出一大堆文件,还有一个 .mk
,这是一个 Makefile
。所以上面又 make -C
进入该目录使用该 Makefile
编译得到可执行文件,名称就是 VERILATOR_TOP
指定的。运行即可。此时 Testbench 也在里面。也可以看到 gtkwave
使用了其中的 .vcd
文件。
接下来再看看 Testbench 是怎么写的。
#include "VAdder_1.h"
#include "verilated.h"
#include "verilated_vcd_c.h"
#include <stdio.h>
#include <memory>
#define MAX_SIM_TIME 300
vluint64_t main_time = 0;
void poke(VAdder_1 *topp)
{
topp->A = rand() % 2;
topp->B = rand() % 2;
topp->CI = rand() % 2;
printf("[poke] S(?) CO(?) A(%x) B(%x) CI(%x)\n", topp->A, topp->B, topp->CI);
}
bool peek_and_check(VAdder_1 *topp)
{
printf("[peek] S(%x) CO(%x) A(%x) B(%x) CI(%x)\n", topp->S, topp->CO, topp->A, topp->B, topp->CI);
return (topp->S == topp->A ^ topp->B ^ topp->CI) && (topp->CO == (topp->A & topp->B) | (topp->A & topp->CI) | (topp->B & topp->CI));
}
int main(int argc, char **argv, char **env)
{
Verilated::commandArgs(argc, argv);
VAdder_1 *topp = new VAdder_1;
Verilated::traceEverOn(true);
VerilatedVcdC *tfp = new VerilatedVcdC;
topp->trace(tfp, 99);
tfp->open("VAdder_1.vcd");
while (main_time < MAX_SIM_TIME && !Verilated::gotFinish())
{
printf("[time] %ld\n", main_time);
poke(topp);
topp->eval();
tfp->dump(main_time);
if (!peek_and_check(topp))
{
printf("Verification failed!\n");
break;
}
main_time++;
}
tfp->close();
delete topp;
exit(0);
}
可以看到,我们编写的 Verilog Module 被转换为 C++ 中的一个类。我们将在 main
中实例化该 Module,并使用自己编写的 poke
和 peek_and_check
函数对其进行测试。其中 poke
用于随机生成输入,peek_and_check
用于检查输出是否正确。可以看到,类内的成员均是我们定义的 Label。peek_and_ckeck
函数手写了正确的逻辑表达式,并返回与 Module 输出的比较结果。Testbench 的编写还是比较容易理解的。
tags: - 草稿
Vivado 相关¶
为 IP 核创建自定义参数¶
可以通过模块名后的 #(parameter )
实现:
module myHeartbeat #(parameter nbits = 25)(
input clk,
output reg heartbeat = 0
);
reg [nbits-1:0]divider = 0;
always @(posedge clk) begin
if (divider == 0) begin
heartbeat <= !heartbeat;
end
divider <= divider+1;
end
endmodule
数据类型¶
wire
- 也被称为 signal,
- 有方向:source(driver) → sink
assign
连续赋值- 不能有超过一个 driver,无 driver 值未定义
- 向量
- 声明时维度在名字前面,选择时维度在名字后面
- 声明在名字后面时也称为 unpacked array,一般用于内存数组
- 赋值大小不一致时,零扩展或截断
隐式网表
assign
和模块端口会隐式生成未定义的网表,它们都是 1 位的。
添加指令 `default_nettype none
可以禁用隐式生成,防止你漏掉 wire
。
状态机¶
例子
module state_machine (
input clk,
input reset,
input go,
input kill,
output done
);
reg [6:0] count;
reg done;
reg [1:0] state_reg;
// states
parameter idle = 2'b00;
parameter active = 2'b01;
parameter finish = 2'b10;
parameter abort = 2'b11;
// machine
always @ ( posedge clk or posedge reset )
begin
if ( reset )
begin
state_reg <= idle;
count <= 7'h00;
done <= 1'b0;
end
else
case ( state_reg )
idle:
begin
count <= 7'h00;
done <= 1'b0;
if ( go )
state_reg <= active;
end
active:
begin
count <= count + 1;
done <= 1'b0;
if ( kill )
state_reg <= abort;
else if ( count == 7'd100 )
state_reg <= finish;
end
//...
default:
begin
count <= 7'h00;
done <= 1'b0;
state_reg <= idle;
end
endcase
end
endmodule
一个状态机的大致写法如下:
- 状态寄存器、计数器等的寄存器声明
- 状态码分配
parameter
- 二进制码、格雷码(功耗较低)、独热码(更节省组合逻辑,增加速度和可靠性)
case
状态机- 每个状态自己做的事情
- 每个状态到其他状态的转移
这是用一个 always
块描述整个状态机的方法。课上讲解了三段式状态机:
- 双寄存器,当前状态和下个状态
- 第一段:
always
块,负责移动到下个状态- 非阻塞赋值
- 第二段:
always
块,负责根据当前状态进行操作,并决定下个状态- 阻塞赋值
- 第三段:
assign
赋值语句,定义输出
三段式状态机其实就是把状态迁移的时序独立出来了。它还可以在组合逻辑后再加一级寄存器,滤去组合逻辑的毛刺。
下面的部分不完善
算术运算¶
或许你会因为 Verilog 中数据运算究竟是考虑无符号数还是补码而感到困惑。这里是一些值得注意的地方:
- 首先,和 C 语言一样,数据具体的位模式是不重要的,重要的是解读这个位模式的方式。
- 从 Verilog-1995 以来,
integer
类型时有符号的,reg
、net
都是无符号的。 - Verilog 的加法和乘法操作会先对操作数扩展成相同的位宽
- I/O、总线。
reg
型需要结构化过程语句如always
进行。- 所有可综合的寄存器都应当使用非阻塞赋值
<=
。
下面是一个带复位的 D-flop:
FPGA 开发中好的习惯
- 总是使用一个全局的异步复位信号。
- 使用
parameter
为状态命名。 - 使用
case
语句,必须要有default
分支(综合程序的要求)。
- 模块化设计:IP 核、原语核。
模块实例化时允许空置的输出
总是使用指定名称的端口连接
- 内存:
控制器,FPGA 不能实现。
- FPGA 可实现的有:SRAM、FIFO、LIFO、DP 等。
- 可以由综合程序推断、厂商原语、厂商的专用工具生成。
- 深度和宽度。
- 通常利用片内存储 RAM 块,我们需要写控制逻辑。
reg [7:0] mem [0:255];
表示 256 个 8 位的寄存器。- 简单双口内存、全双口内存、单口内存的实现。
- 读写时序。当读写地址相同时,读写操作哪个需要设计。
- 三态门实现双向总线。
- 测试模块:
initial
、integer
、#
、wait()
一般只用于测试模块。
module tb_sim_sample_1();
integer i;
parameter CLK_PERIOD = 10;
initial sim_clk= 1'b0;
always #(CLK_PERIOD/2)
sim_clk = ~sim_clk;
initial
begin
random_num = $random(1)
wait(reset);
wait(~reset);
@(posedge sim_clk);
for(j = 0; j < 20; j = j + 1)
begin
@(posedge sim_clk);
end
forever
begin
@(posedge sim_clk);
enable = 1'b0;
end
end
endmodule
always
块实现组合逻辑,在其中使用if-else
和case
语句。此时使用=
阻塞赋值,因为组合逻辑不关心执行顺序。