Skip to content

数字电路

Abstract

数字电路是计算机体系结构的基础,它研究逻辑门、触发器等基本电路元件,用于实现二进制运算和存储。计算机体系结构则在此基础上设计处理器、内存等硬件组件,并定义指令集和系统架构,以构建完整的计算机系统。简言之,数字电路提供底层硬件支持,计算机体系结构则负责系统级设计与优化。

Quote

基础知识

  • 门 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

  • 运算符:

    分类 运算符 说明
    按位 ~ 非 NOT
    \| 或 OR
    ^ 异或 XOR
    & 与 AND
    ~^ 同或 XNOR
    ~& 与非 NAND
    规约:接受一个向量操作数,返回一个单比特值
    长度不一时,右端对齐,补零
    逻辑 && 与 AND
    \|\| 或 OR
    ! 非 NOT
    赋值 = 赋值
    <= 非阻塞赋值
    拼接 {a,b} 拼接:将多个操作数连接成一个更大的操作数
    重复 {num{replicate}} 重复:将一个操作数重复多次,如 {3'd5, {2{3'd6}}}
    位移 << 左移
    >> 右移
    <<< 算术左移
    >>> 算术右移
    条件 ? 条件运算符

System Verilog

开发工具

Vivado

略。

开源工具链

Verilator


一、数据类型、变量、运算符号、基本语法规则

和学习 C 语言的过程一样,我们简单看一眼 Verilog 具有的数据类型、变量和运算符号。

数据类型

或许你会想,硬件描述语言为什么要有数据类型。人们只是出于编程语言的惯例,把硬件(如触发器等)抽象成了数据类型。我们可以从数字逻辑电路中抽象出的数据类型有:

  • wire:抽象自模块内部的连线,不能存储数据。
  • reg:抽象自寄存器,可以存储数据。

它们都有位宽的概念,如 wire [7:0] a; 表示 a 是一个 8 位的连线。方括号里的上下限由你自己决定,比如也可以写成 wire [8:1] a;。习惯上从 0 开始。

此外还有很多数据类型,用得较少,暂不介绍。

常量

Verilog 中的常量都是整型(integer),表示方式为:

<位宽>'<进制><值>

可以使用的进制有:

  • 二进制:bB
  • 八进制:oO
  • 十进制:dD
  • 十六进制:hH

值除了各进制对应的数字外,还可以是 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 模块被 moduleendmodule 包围,module 紧跟着模块名和接口,如下所示:

module block_name(a, b, c, d);
    output a, b;
    input c, d;
    // logic
endmodule

inputoutput 关键字的部分是 I/O 说明,它也可以写在端口声明语句,如:

module block_name(output a, output b, input c, input d);

习惯上,输出引脚被放在前面。

二选一选择器的三种 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 语句等)描述了模块的功能。从这段代码中,我们能立刻读懂模块的功能,但无法直接看出模块内部的连线方式。

module muxtwo(out, a, b, sl);
    input a, b, sl;
    output out;
    wire out
    assign out = sl ? b : a;
endmodule

这是内部连线描述,使用 wire 声明了模块内部的连线,使用 assign 连接连线和逻辑。这种描述方式更加接近硬件的实现。

module muxtwo(out, a, b, sl);
    input a, b, sl;
    output out;
    and #2 u1(out, a, ~sl);
    and #2 u2(out, b, sl);
    or #2 u3(out, u1, u2);
endmodule

计算机科学中所谓原语(primitive)的概念,就是别人提供给你的最底层能调用的功能,不能再分割得更小了。

在 Verilog 中,原语就是一些基本的逻辑门,如 andornot 等。我们自然可以用这些基本逻辑门实现各种各样的逻辑电路。

接下来,我们要编写一个测试来验证我们写的选择器是否正确,验证的思路很简单,就是把所有可能的输入都输入一遍,然后检查输出是否正确。

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 连接连线和逻辑
  • 原语描述:使用 andor 等原语描述模块的功能

数逻教材中的大部分 Verilog 都采用后两种编程方式结合。

模块的结构、数据类型、变量和基本运算符号

模块

两个部分,I/O 接口和逻辑功能。

基本模块结构如下:

接下来就是内部信号说明(wirereg 等的声明)和功能定义(逻辑部分)

功能定义

有 3 种方式能够产生逻辑:

  • assign 声明语句
  • 实例元件 and #2 ul(a, b, c);
  • always
always @ (a or b)
    c = a & b;

assign 最常用,always 既可描述组合逻辑,也可描述时序逻辑,手段较多。

并行性

如果把上面三个部分放到一个模块中,它们之间是并行的。它们之间的顺序不会影响实现的功能。

always 模块内,语句是顺序执行的。

使用模块

有两种方式:

  • 严格按照定义的端口顺序来连接。
block MyBlock(a, b, c, d);
  • .portname(signal) 来连接:
block MyBlock(.a(a), .b(b), .c(c), .d(d));

第二种方式不必按端口顺序对应,提高可读性和可移植性。

常量-parameter 类型

描述方式:parameter <name1> = <expression1>, <name2> = <expression2>, ...;

表达式必须是常量表达式。

示例:

parameter r = 10, f = 20;
parameter average_delay = (r + f) / 2;

常见用法:

  • 实例化时传参指定

    module Decode(A,F);
        parameter Width=1, Polarity=1;
    endmodule
    module Top;
        Decode #(Width=4, Polarity=0) D1(A,F);
        Decode #(4, 0) D2(A,F); // 也可以省去关键字赋值
    endmodule
    
  • 使用 defparam 语句改变任意模块的参数值

    module Decode(A,F);
        parameter Width=1, Polarity=1;
    endmodule
    module Top;
        Decode D1(A,F);
        Decode D2(A,F);
        defparam D1.Width = 4, D1.Polarity = 0;
        defparam D2.Width = 4, D2.Polarity = 0;
    endmodule
    

变量

  • 网络数据类型:表示结构实体之间的连接

    • wire:单驱动

      wire a;
      wire [7:0] b;
      
    • tri:多驱动

    • 寄存器 reg:数据存储单元的抽象
    reg [n-1:0] a;
    
  • memory 型:对 reg 建立数组

    reg[7:0] mem[0:255]; // 256 个 8 位寄存器组 mem
    reg[n-1:0] rega; // n 位寄存器 rega
    reg mema[n-1:0]; // n 个 1 位寄存器组 mema
    mema[3]=0; // 必须使用下标访问其中的单个寄存器进行读写
    rega=0;
    

运算符和表达式

四则、位移、逻辑运算不再介绍。

  • 位运算

    ~ & | ^ ^~
    
  • 三目运算

  • 拼接运算

    {a, b, c}
    

进阶:使用更加基础的工具

提前准备、学习以下工具,它们用于 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,并使用自己编写的 pokepeek_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 类型时有符号的,regnet 都是无符号的。
  • Verilog 的加法和乘法操作会先对操作数扩展成相同的位宽
  • I/O、总线。
  • reg 型需要结构化过程语句如 always 进行。
  • 所有可综合的寄存器都应当使用非阻塞赋值 <=

下面是一个带复位的 D-flop:

always @(posedge clk or posedge rst) 
    begin
        if (rst) 
            q <= 1'b0;
        else 
            q <= d;
    end

FPGA 开发中好的习惯

  • 总是使用一个全局的异步复位信号。
  • 使用 parameter 为状态命名。
  • 使用 case 语句,必须要有 default 分支(综合程序的要求)。
  • 模块化设计:IP 核、原语核。

模块实例化时允许空置的输出

总是使用指定名称的端口连接

  • 内存: 控制器,FPGA 不能实现。
    • FPGA 可实现的有:SRAM、FIFO、LIFO、DP 等。
    • 可以由综合程序推断、厂商原语、厂商的专用工具生成。
    • 深度和宽度。
    • 通常利用片内存储 RAM 块,我们需要写控制逻辑。
    • reg [7:0] mem [0:255]; 表示 256 个 8 位的寄存器。
    • 简单双口内存、全双口内存、单口内存的实现。
    • 读写时序。当读写地址相同时,读写操作哪个需要设计。
    • 三态门实现双向总线。
  • 测试模块:
    • initialinteger#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-elsecase 语句。此时使用 = 阻塞赋值,因为组合逻辑不关心执行顺序。