数字电路¶
Abstract
数字电路是计算机体系结构的基础,它研究逻辑门、触发器等基本电路元件,用于实现二进制运算和存储。计算机体系结构则在此基础上设计处理器、内存等硬件组件,并定义指令集和系统架构,以构建完整的计算机系统。简言之,数字电路提供底层硬件支持,计算机体系结构则负责系统级设计与优化。
Quote
逻辑电路基础¶
- 
晶体管 Transistor:可以用电压控制的开关
MOSFET
- 
化学原理:
N(negative)型掺杂 P(positive)型掺杂 材料 Si 掺入 P Si 掺入 B 载流子 电子 空穴 - PN 节与扩散作用:N 区的电子向 P 区扩散,显正电性,形成电场阻碍电子扩散,称为耗尽区
 - 二极管:外部施加电场与耗尽区相同时,耗尽区扩大,电路几乎不导通;当外部电场抵消时,耗尽区消失,电路导通
 - :
- 材料双肩做 N 掺杂,其余部做 P 掺杂
 - 两个 N 区连入回路,但因为两处 PN 节均处于反向偏置,电路不导通
 - 在 P 区中间加一个正电极,吸引电子,形成类似 N 区的 N 沟道,使两个 N 区连通
 
 
 - 
结构:\(S\) 源极、\(D\) 漏极、\(G\) 栅极、\(B\) 衬底
 - NMOS 高电压导通
 - PMOS 低电压导通
 - CMOS:非门
- 结构:
- 连接 NMOS 和 PMOS 的漏极作为输出
 - G 连接输入
 - PMOS 源极接输入电压 \(V_{DD}\),NMOS 源极接地 \(V_{SS}\)
 
 - 原理:
- 输入高电平时,NMOS 导通,PMOS 截止,输出接地
 - 输入低电平时,PMOS 导通,NMOS 截止,输出接 \(V_{DD}\)
 
 
 - 结构:
 
 - 
 
组合电路 Combinational Circuits
- 逻辑门 Gate
 
时序电路 Sequential Circuits:与组合电路相比,它能在操作间存储信息
- 同步时序电路:状态变更受时钟信号控制
 - 异步时序电路:可以在任意时刻修改状态
 
时序电路中的组件:
- 锁存器 Latch:只要使能,信号就会传递
- \(SR\) Latch:
- 构造:两个交叉连接的 NOR
 - 信号:\(R\) 复位、\(S\) 置位、\(Q\) 输出、\(\overline{Q}\) 反相输出
 
 - $\overline{SR}:
- 构造:两个交叉连接的 NAND
 - 信号:\(SR\) 引脚全部取反
 
 - \(CSR\):
- 构造:\(\overline{SR}\) 的输入各加一个 NAND 门,并连接到使能 \(C\) 上
 - 信号:\(C\) 使能
 
 - \(D\):消除了 SR Latch 的非法状态
- 构造:将 \(CSR\) Latch 的 \(S\) 和 \(R\) 的输入合并为一个 \(D\) 输入,其中一个分支取反
 - 信号:\(D\) 数据输入,\(C\) 使能
 
 
 - \(SR\) Latch:
 - 
触发器 Flip-Flop:
一般由两个 Latch 级联构成主从结构(Master-slave),前者的控制信号取反连接到后者,错开使能时间,保证信号稳定。
分为脉冲触发(pulse-triggered)和边沿触发(edge-triggered)。边沿触发的 Flip-Flop 更快,设计约束更简单。
- 下降沿触发的 D Flip-Flop:
- 构造:\(D\) Latch 与 \(CSR\) Latch 连接
 
 - 上升沿触发的 D Flip-Flop:
- 构造:前者的时钟信号取反
 
 - JK Flip-Flop
 - T Flip-Flop
 - 寄存器 Register:一组 Flip-Flop 组成,统一的时钟信号和重置信号
 
 - 下降沿触发的 D Flip-Flop:
 
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 --- n4Difference 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语句。此时使用=阻塞赋值,因为组合逻辑不关心执行顺序。