type
slug
status
summary
icon
category
date
tags
password

2.1 Verilog HDL概述

2.1.1 Verilog HDL简介

  • Verilog HDL是一种硬件语言,最终是为了产生实际的硬件电路或对硬件电路进行仿真。
    • 目的: 它的最终目标不是运行在CPU上的软件程序,而是为了:
      • 生成实际的硬件电路(通过综合工具,将代码转化为门级网表,再映射到FPGA或 ASIC芯片上)。
      • 对硬件电路进行仿真(在电脑上模拟电路的行为,验证功能是否正确)。
    • 写Verilog 就是在“画”电路图,只是用文本代码的形式。
💡
注意:编程思维转换:从软件到硬件。
  • 利用Verilog HDL编程时,要时刻牢记Verilog是硬件语言,要时刻将Verilog HDL语句与硬件电路对应起来;
  • 软件思维:代码是按顺序一行一行执行的(串行)。
    • CPU 是单核/多核,但指令是按顺序一条条取指、译码、执行的。
    • if-elsefor循环等控制结构是靠程序计数器(PC)来实现的。
  • 硬件思维:电路在物理上是并行工作
    • 一旦上电,所有门电路、触发器、连线都在同时工作。信号在导线中传播有延迟,但这个延迟是并行发生的。不存在“先执行 A 再执行 B”这种概念,除非你人为地用时钟去同步它们。
    • 要求在Verilog HDL的module中,所有描述语句(包括连续赋值语句assign、行为语句块always/initial、模块实例化)都是并发执行的;
  • 电路行为的先后顺序通过时钟节拍顺序来体现。
notion image
  • 并发执行(Concurrent Execution)
    • 这是Verilog的核心特性之一。在一个module(模块)内部,所有的语句都是并发执行的,它们没有先后顺序(除了在always块内部的顺序语句)。
    • 主要包括以下几类语句:
      • 连续赋值语句(assign):用于描述组合逻辑,如assign y = a | b;
      • 行为语句块(always, initial):用于描述时序逻辑或初始化行为。
      • 模块实例化 (inst_name uut (...)):实例化一个子模块,相当于在电路图中放置一个子模块。
    • 形象比喻:想象一个工厂车间,里面有很多机器(assign, always, 模块实例),它们都在同时运转,互不干扰。只有当某个机器需要等待“时钟信号”(就像工厂的统一指令)时,它才会在特定时刻动作。
  • 表达“先后顺序”?—— 时钟节拍 (Clock Cycle)
    • 既然所有语句都是并发的,那怎么表示“先做 A,再做 B”呢?
      • 答案:使用时钟信号 (Clock Signal)。
      • 数字电路中的“顺序”是通过时钟节拍来控制的。在一个时钟周期内,某些操作完成;下一个时钟周期,再进行下一步操作。
      • notion image
 

2.1.2 Verilog HDL多种描述方法

Verilog HDL 的四种主要建模方式(描述层次):决定了你如何用代码来“表达”和“构建”一个数字电路。简单来说,这就像用不同的“画笔”或“工具”来描绘同一个电路:你可以从最底层的晶体管开始画,也可以直接画出一个逻辑门,或者用算法来描述它的功能。每种方式都有其适用的场景和优缺点。
notion image
  • 开关级描述方式(开关级建模)
    • 这是最底层的建模方式,直接描述电路中晶体管(MOSFET)和二极管的行为。
    • 能够使用内置开关级原语对设计完整建模;开关级基本结构模型:内置pmos、nmos等
notion image
notion image
  • 门级描述方式(门级建模)
    • 描述电路是由哪些标准逻辑门(如与门、或门、非门等)构成的。
    • 能够使用内置门级原语对设计完整建模,如and、or、nand
    • notion image
  • 数据流级描述方式(数据流级建模)
    • 描述数据是如何在电路中流动的,关注的是信号之间的逻辑关系
    • 能够使用内置数据流级原语assign、连续赋值语句和位运算符对设计完整建模;
    • 位运算符有:&、︳、~、 ˆ、~ˆ等。
    • notion image
      “所以在使用数据流级描述的时候,需要特别注意不能用到其他原语或者符号了。”
    • 这句话的意思是,在写assign语句时,你应该只使用基本的逻辑运算符和信号名,不要混入always块或其他结构。数据流级描述是纯粹的组合逻辑描述。
  • 行为级描述方式(行为级建模)
    • 最高层级的建模方式,描述的是电路的功能和行为,完全不关心内部结构。
    • 能够使用结构和算法对设计完整建模;
    • 常用语句有:initial(只执行一次),always (循环执行);还提供一些高级语言结构,如if语句、case语句、循环语句等。
notion image
【叽里呱啦什么呢,到时候再回来看看在说什么】

2.2 Verilog HDL基本语法

2.2.1 标识符

  • 在Verilog中,标识符就是你用来给各种东西“起名字”的字符串。简单来说,凡是你自己定义的名字,都叫标识符。这些“东西”包括:
    • 模块名 (module my_module)
    • 信号名/变量名 (reg clk;, wire data_out;)
    • 端口名 (input a, b; output y;)
    • 参数名 (parameter WIDTH = 8;)
    • 实例名 (my_submod uut (...))
  • Verilog HDL中的标识符是由任意的字母、数字、$符和下划线组成的字符序列。
    • 相较于高级程序设计语言而言,多了一个$符
    • 标识符的第一个字符必须是字母或者下划线,不能以数字或者&开头。
    • 大小写敏感:标识符是区分大小写的。例如,count和COUNT是不同的。
  • 关键词 (Keywords) ——不能用作标识符的“保留字”
    • Verilog定义了一套自己的“关键词”,它们有特殊的含义和功能,不能被用作自己定义的标识符。
    • 关键词是Verilog语言中预定义的、具有特定语法功能的单词。
      • 例如:module, endmodule, input, output, reg, wire, assign, always, initial, if, else, case, endcase等等。
    • 关键点:只有全部小写的关键词才是真正的“保留字”。
      • 这是 Verilog 的一个独特之处!
      • always是关键词,因为它全小写。
      • ALWAYS不是关键词,因为它全是大写。
      • Always也不是关键词,因为它是大小写混合。
notion image

2.2.2 数值、常量、数据类型及变量

  • 这部分内容主要回答了两个关键问题:
    • Verilog 能表示哪些“值”? (不仅仅是0和1)
    • 如何在代码里写这些“值”? (特别是带位宽和进制的常量)
  • 🎯 核心概念速览
    • 数值(Value): Verilog 中信号可以取的四种基本状态。
    • 常量(Constant): 在代码中直接写出的、固定不变的数值。
    • 数据类型(Data Type): 用于定义变量(如 reg, wire)能存储什么类型的数据。

2.2.2.1 数值

  • Verilog HDL的数值有四类:
    • 0(逻辑0)
    • 1(逻辑1)
    • Z/z(高阻)
      • 这里的高阻就是指三态门中的高阻态,也就是相当于断路
    • X/x(未知)
      • Verilog中的未知不是使用d来表示的
notion image

2.2.2.2 常量

  • 整数型常量 (Integer Constants)
    • 在代码中写数字时最常用的方式。
    • 定义格式:<位宽>'<进制><数字>
      • <位宽>:指定这个常量占用多少位。例如8表示8位。如果省略位宽,则默认为32位。
      • ':单引号,是分隔符。
      • <进制>:指定数字的进制。
        • b或B:二进制 (Binary)
        • o或O:八进制 (Octal)
        • d或D:十进制 (Decimal)
        • h或H:十六进制 (Hexadecimal)
      • <数字>:具体的数值。
    • 注意:单引号'和进制字母之间不能有空格。
    • 示例
      • 关于负数:Verilog不支持在常量定义中直接写负数。
        • 因为在硬件层面,“负号”不是一个物理实体。计算机(和FPGA)内部所有的数,都是用一串0和1来表示的。为了表示负数,我们使用“二进制补码”这种编码方式。
        • 方式一:直接写出补码
          • 确定你需要的位宽(比如4位)。
          • 计算该负数的二进制补码
            • 计算负数的补码
              首先看绝对值部分的二进制(注意位宽),然后所有位取反,然后末位+1得到这个负数的补码。
              notion image
              已知一个负数的补码,求这个负数
              • 在补码中最高位表示符号位。这个符号位不是独立存在的,它本身就是数值的一部分,参与运算,这就是补码的关键:补码的符号位是“内嵌”的,不是像原码那样单独拿出来的一个标志位。这就是为什么补码能实现加减法统一的原因。
              • 判断符号位是1即为负数,对补码按位取反,末位+1,得到的结果就是负数的绝对值,添上负号就得到负数本身。
              notion image
              快速求补码的办法
              notion image
          • 直接用这个二进制数作为常量。
            • notion image
        • 方式二:使用s前缀
          • 告诉仿真器/综合器:这个数是一个有符号数的补码表示。
            • notion image
          • 6’so72 //位宽为6的有符号八进制数111010 //它是十进制下的-6
            • 这里计算先不管s,把72当作普通的八进制数对待:72=111010
            • 因为加了s前缀,表示这是一个6位的有符号数。判断111010的最高位(符号位)是1,所以它是一个负数。我们又知道这是这个负数的补码,所以我们根据补码得到原来的负数:取反+1为绝对值6,添上负号,所以是十进制下的-6
      • 注意:
        • x/z在不同进制下的含义
          • x或z在十六进制数值中代表4位二进制x或z,在八进制数值中代表3位二进制x或z,在二进制数值中代表1位x或z。
        • 位宽不匹配时的处理规则
          • 定义的位宽 > 常量指定的位宽(位宽扩展)
            • 对于无符号数
              • 在数的左边填0补齐
              • 例如:10'b10→实际是10'b0000000010
            • 对于有符号数
              • 在左边填符号位补齐
              • 例如:8'sb101101→最高位是1,所以左边填充1,结果是 8'b11101101
              • 这就是所谓的 “符号扩展”,它保证了数值的大小不变
            • 特殊情况:如果常量的最高位是x或z
              • 则左边也填充xz
              • 例如:10'bx0x1→左边填充x,结果是 10'bxxxxxx0x1
          • 定义的位宽 < 常量指定的位宽(位宽截断)
            • 当目标变量的位宽比常量本身的位宽小时,Verilog 会自动截断高位,只保留低位无论常量是有符号还是无符号,都是从右边开始取低位。高位会被丢弃。
            • notion image
              notion image
     
    • 实数型(小数)常量
      • 形式:由十进制计数法或科学计数法的形式组成。
        • notion image
    • 字符串型常量
      • 形式:由双引号括起来的任意字符序列
      • 内部表示:字符串中的每个字符在Verilog内部都被存储为一个8位的ASCII码。例如:”hello”、”abc”
      • 在Verilog中同样有转义字符
        • \n:表示换行符
        • \t:表示制表符
        • \\:表示反斜杠(\)本身
        • \”:表示双引号字符(”)
        • notion image
          notion image
          注意这里的正确显示两个“”
      • 补充:Verilog 字符串的局限性
        • 不可综合: 这是最重要的限制。任何包含字符串的逻辑都无法被综合成硬件。
        • 无字符串操作函数:Verilog没有像C或Python 那样的内置字符串处理函数(如 strlen, strcpy, strcat 等)。你不能对字符串进行拼接、查找、修改等操作。
        • 存储方式: 虽然概念上是一个“字符串”,但在仿真器内部,它通常被处理为一个由reg [7:0]组成的数组,每个元素存储一个字符的ASCII码。

    2.2.3 数据类型及变量

    数据类型与基本逻辑单元建库有关,由半导体厂家和EDA工具厂家共同提供。Verilog HDL有两大类数据类型

    2.2.3.1 线网类型(Nets Type)

    • 定义:表示Verilog结构化元件间的物理连线
    • 本质:它没有存储能力,其完全由驱动它的元件决定。
      • 如果驱动元件的值变了,线网的值会立刻跟着变。
      • 如果没有驱动元件,它的值就是缺省值Z(高阻态),表示“悬空”。
      • nets型变量指输出始终根据输入变化而更新其值的变量,一般是指硬件电路中的各种物理连接。
    • 常用的线网类型:wire
      • 在Verilog中,wire是最常用、最基础的线网类型。
      • 默认类型:模块的输入/输出端口,如果没有显式声明类型,默认就是wire类型。
    • 定义格式
      • [n:0]:指定位宽。如果省略,则默认为1位。
      • 可以同时定义多个变量。
    • 示例
      • 如何赋值
        • 只能通过assign语句或模块实例化来赋值。
        • 不能在alwaysinitial块中直接赋值!因为wire是“连线”,它的值是由外部驱动决定的,而不是由程序逻辑“主动”改变的。
        • 💡
          assign语句又被称为连续赋值语句,作用是持续不断地将右边表达式的值赋给左边的wire变量。
          assign <wire变量名> = <表达式>;
          notion image
          wire类型的变量通常是不能在过程块中赋值的
          notion image

      2.2.3.2 寄存器类型(Register Type)

      • 定义:表示一个抽象的数据存储单元
        • 寄存器型变量对应的是具有状态保持作用的电路元件,如触发器、寄存器等。
      • 本质:它具有“状态保持”作用,能在两次赋值之间“记住”前一次的值。
        • 对应硬件:最终会被综合工具映射到触发器或锁存器上。
        • 关键点:reg不是“寄存器”的字面意思,而是一个“能保存值”的变量。它可以被赋值,也可以在没有新赋值时保持旧值。
      • 主要的寄存器类型
        • notion image
        • reg型变量
          • 定义格式
            • [n:0]:指定位宽。如果省略,则默认为1位。
            • 可以同时定义多个变量。
          • 示例
            • 如何赋值
              • 只能在alwaysinitial块中赋值。
              • 不能通过assign语句赋值!因为reg是“存储单元”,它的值需要通过程序逻辑(如时钟边沿)来“主动”更新,而不是被动地被驱动。
            • reg型变量与nets型变量的根本区别
              • reg型变量需要被明确地赋值,并且在被重新赋值前一直保持原值。所以在设计中要将reg型变量放在过程块语句(如Initial , always)中赋值。
                • notion image
              • 缺省值为未知x:是因为寄存器中总是会存储一个值的,只不过这个值在使用之前可能不知道是多少,也可能是无效的,因此寄存器变量的缺省值为X
          • Integer型变量
            • 定义格式
              • integer是一种寄存器类型,它只能在alwaysinitial块中被赋值。
              • 它默认是32位有符号数
            • 示例
              • 注意:integer的定义格式和reg非常相似,但它不能带位宽声明。如果你需要指定位宽,应该使用reg类型。
                • notion image

          2.2.4 数组

          2.2.4.1 数组的声明

          • 定义:把多个相同类型的变量组织在一起,通过索引来访问。
          • 格式
            • <数据类型>:可以是reg, wire, integer等。
            • <索引范围>:指定数组的大小和索引方向。例如[7:0]表示8个元素,索引从7到0。
          • 特性
            • 数组的元素可以是标量(1位)也可以是向量(多位)。
            • 数组的维数可以是一维、二维、三维甚至更高维。
            • 数组的引用可以针对某一个元素或某一个元素的一部分
          • 示例
            • reg x[11:0];
              • reg :这部分整体定义了数组元素的类型。
                • reg是数据类型。
                • 没有[位宽]:reg默认是1位的。
                • 所以,数组的每个元素都是1位的reg(标量)。
              • x[11:0]:这是数组x的索引范围,表示有12个这样1位的reg元素(从11到0)。
            • wire [7:0] y[5:0]; (这是最常用、最推荐的写法!)
              • wire [7:0]:这部分整体定义了数组元素的类型。
                • wire是数据类型。
                • [7:0]是该类型(wire)的位宽
                • 所以,数组中的每一个元素都是一个8位的wire(向量)
              • y[5:0]:这是数组y的索引范围,表示有6个这样的8位wire元素。
            • reg [23:0] image_rgb[0:1023][0:1023];
              • 这是一个二维数组。
              • reg [23:0]:这部分整体定义了数组元素的类型
                • reg是数据类型。
                • [23:0]是该类型(wire)的位宽
                • 所以,数组的每个元素都是一个24位的reg向量。
              • image_rgb[0:1023][0:1023]:定义了image_rgb数组的维度。
                • 第一个[0:1023]:表示有1024行。
                • 第二个[0:1023]:表示每行有1024列。
              • 总结:一个1024x1024的“表格”,表格里的每个“格子”里都存着一个24位的数据。
            • reg arrayb[7:0][0:255];
              • 这也是一个二维数组,但它是“标量”的二维数组。
              • reg :这部分整体定义了数组元素的类型
                • reg:定义了数组元素的类型。
                • 没有[位宽]:reg默认是1位的。所以,数组的每个元素都是1位的reg(标量)。
              • arragyb[7:0][0:255]:定义了arrayb数组的维度。
                • 第一个[7:0]:表示有8行。
                • 第二个[0:255]:表示每行有256列。
              • 总结:一个 8x256 的“表格”,表格里的每个“格子”里都存着1bit(0或1)。
           
          • wire [7:0] y[5:0] vs wire y[5:0][7:0]
            • 这两种写法最终定义的硬件结构是一样的:都是6个8位的wire
            • notion image

          2.2.4.2 数组的赋值

          • 数组的赋值方式与普通变量类似,但需要指定索引
            • Q:我有点奇怪的是为什么有的时候索引写高到低,有的时候写低到高
              • A:Verilog本身对位宽方向没有强制规定,[7:0][0:7] 都是合法的,但它们代表了不同的“位序约定”。 选择哪一种,更多是工程习惯偏好的问题。在实际工程中,[高:低](降序)是绝对的主流和强烈推荐的标准。
              • reg [7:0] data;
                • 降序:data[7]是最高位,data[0]是最低位。
                • 索引大的是高位
                  • data = 8'b10110000;
                    • data[7]=1, data[0]=0,符合我们从左到右写二进制数的习惯(左边是高位)。
              • reg [0:7] data;
                • 升序:data[0]是最高位,data[7]是最低位。
                • 索引小的是高位
                  • data = 8'b10110000;
                    • data[0]=1, data[7]=0,这与我们的直觉相反。
             

            2.2.4.3 数组访问

            • 这就是之前所说的另外一种写法的形式:最左和最右定义元素类型,中间是数组的维度
              • wire [7:0]:这是最核心的元素类型。它定义了数组中最小单元是一个8位的wire向量。
              • [255:0][255:0]:这是数组的维度。
                • 第一个[255:0]:表示有256行。
                • 第二个[255:0]:表示每行有256列。
              • 最终结构:可以把它想象成一个256 x 256的“大表格”,而 表格里的每一个“格子”里都装着一个8位的数据。
            • 完整引用一个格子: threed_array[行索引][列索引] → 得到一个8位的向量。
            • 引用一个格子里的某一位: threed_array[行索引][列索引][位索引] → 得到1位。
            • 引用一个格子里的某几位: threed_array[行索引][列索引][位范围] → 得到一个子向量。
             

            2.3 Verilog HDL的操作符

            2.3.1 算术操作符

            Verilog支持6中算术运算符:加法(+)、减法(-)、乘法(*)、除法(/)、取模(%)和幂运算(**)
            • 算术运算的注意点
              • 整数除法(/)将截断结果的小数部分,例如:7/4的运算结果为1。
              • 取模(%)操作符求出与第一个操作符符号相同的余数,例如:7%4的运算结果为3,-7%4的运算结果为-3。
              • 算术操作符中任意操作数中只要有一位为x或z,则整个运算结果为x
                • 例如:’b10x1+’b0111的运算结果为不确定数’bxxxx
                • 因为在真实硬件中,如果一个信号是未知的(x) 或高阻的(z),那么基于这个信号进行的任何计算都是不可靠的,所以结果也应该是未知的(x)。

            2.3.3.1 算术运算结果的位宽

            • 对于纯算术表达式(未赋值给变量):
              • 结果的位宽参与运算的最大操作数的位宽决定。
              • 例如:a[4:0] + b[3:0] → 结果是5位。
            • 在赋值语句中(=<=):
              • 结果的位宽等号左边目标变量的位宽决定。
              • 如果右边计算结果的位宽大于左边,高位会被截断
              • 如果右边计算结果的位宽小于左边,高位会根据变量类型进行填充(无符号填0,有符号填符号位)。
                • a+b本身是一个4位的算术运算(因为a和b都是4位)。当赋值给c时,结果保持4位。当赋值给d时,结果会被扩展到6位(高位填0,因为d是无符号的)。
                • 注意:如果a+b的结果超出了4位(比如 15 + 1 = 16),赋值给c时会发生溢出,结果会是0(因为16的二进制是10000,截断后只剩0000)。这在设计中需要特别小心!

            2.3.1.2 有符号数和无符号数

            (Signed vs Unsigned)
            • 无符号数与有符号数的存储
              • notion image
                notion image
            类型
            存储位置
            表示方式
            无符号数 (Unsigned)
            wire, reg, 没有标s的基数格式表示的整型数中
            没有s标记的基数格式(如 4'b1101
            有符号数 (Signed)
            reg signed, wire signed, integer
            s标记的基数格式(如 4'sb1101
            reg和wire默认是无符号的。如果你想让它们作为有符号数使用,必须显式声明为reg signed 或 wire signed。
             
            • 系统函数 $signed$unsigned
              • Verilog 提供了两个强大的系统函数,用于在有符号和无符号形式之间进行转换。
              • $signed(<expression>):将表达式转换为有符号数。
              • $unsigned(<expression>):将表达式转换为无符号数。
                • 第一行:s1101为有符号数,为负数,补码是1101,那么取反+1为绝对值0011=3,添上-,即为-3
                • 第二行:去掉s即hA,十六进制的A为10.原来是1010,有符号位,负数,即取反+1为绝对值6,添上-,即原来表示-6.
                • 注意:这些函数只是改变了解释数据的方式,并不会改变数据本身的二进制表示。
             

            2.3.2 关系操作符

            Verilog支持4种关系操作符:大于(>)、小于(<)、大于等于(>=)、小于等于(<=)
            • 基本行为
              • 关系运算符对两个操作数逐位进行比较,结果为真(值为1)或假(值为0)
              • x/z的传播规则:若操作数中有一位为x或z,则结果为x
                • 这与算术运算符的规则一致,体现了仿真中对“不确定状态”的保守处理。
            • 操作数位宽不匹配:位宽扩展
              • 如果所有操作数都是无符号数:
                • 位宽较小的操作数在高位0补齐。
              • 如果所有操作数都是有符号数:
                • 位宽较小的操作数在高位填符号位补齐。
            • 若表达式中有一个操作数是无符号数,则该表达式的其余操作数都被当作无符号数处理
              • 这就解决了上一个注意点中可能存在的问题:一个数是有符号数,一个数是无符号数。而实际上,在任意一个表达式中都是这样的。
             

            2.3.3 等价操作符

            Verilog HDL支持4种等价操作符:逻辑相等(= =)、逻辑不等(!=)、全等(= = =)、非全等(! = =)
            • 基本行为
              • 等价操作符对两个操作数逐位进行比较,对于逻辑相等(= =)和逻辑不等(!=)的比较结果是真(值为1)或假(值为0)
              • 操作数中有一位为x或z,则结果为x
              • 对于全等(= = =)和非全等(! = =),则是将x或z当作数值(不考虑其物理含义)严格地按字符值进行比较的,因此其结果不是1就是0,没有未知的情况。
            • 操作数位宽不匹配:位宽扩展
              • 如果所有操作数都是无符号数:
                • 位宽较小的操作数在高位0补齐。
              • 如果所有操作数都是有符号数:
                • 位宽较小的操作数在高位填符号位补齐。
            • 如果表达式中有一个无符号数,那么所有的数值都将被转换为无符号数
             

            2.3.4 位操作符

            Verilog支持5种位操作符:取反(~)、与(&)、或(|)、异或(^)和同或(^~或~^)
            • 基本行为
              • 顾名思义是对输入的操作数进行逐位操作(即对应位进行操作)。结果是一个与操作数位宽相同的向量。
              • 对有符号数而言,进行位运算的时候也需要包含符号位
            • 操作数位宽不匹配:位宽扩展
              • 如果所有操作数都是无符号数:
                • 位宽较小的操作数在高位0补齐。
              • 如果所有操作数都是有符号数:
                • 位宽较小的操作数在高位填符号位补齐。
            • 如果表达式中存在无符号数的话,那么其余数都将被视为无符号数。
            • x/z传播规则(位运算篇)
              • 在Verilog的位运算中,只有当一个输出位的结果“无法确定”时,该位才会被置为 x。如果结果是“可以确定”的,即使输入中有x或z,输出位也可能是0或1。
              • 这里举个例子:0&x=0,这是因为结果能确定为0;而1&x=x,这是因为结果无法确定。
             

            2.3.5 逻辑操作符

            Verilog支持3种逻辑操作符:逻辑与(&&)、逻辑或(||)和逻辑非(!)
            • 基本行为
              • 布尔表达式进行操作,结果是1位的布尔值。
              • 如果操作数中没有x或z,则逻辑操作的结果是1位宽的布尔值(0或1);
              • 如果操作数中有x或z,则逻辑操作的结果是1位宽的x
            • 逻辑操作符和位操作符不同。逻辑操作符用于连接布尔表达式,而位操作符用于电路信号的连接
              • 重要提醒: 不要混淆&&&。前者是逻辑与,后者是位与。

            2.3.6 缩减操作符

            Verilog支持6种缩减操作符:缩减与(&)、缩减与非(~ &)、缩减或(|)、缩减或非(~ |)、缩减异或(^)、缩减同或(~ ^或^ ~)。
            • 基本行为
              • 单个多位操作数所有位进行操作,最终产生一个1位的结果
              • x/z的传播:如果操作数中有x或z,则结果为x。
            • 缩减与(&)
              • 只要操作数中任意一位的值为0,则操作的结果便为0
              • 只要操作数中任意一位的值为x或z,则操作的结果便为x;
              • 否则其操作结果为1。
            • 缩减与非(~&)
              • 缩减与的操作结果求反
            • 缩减或(|)
              • 只要操作数中任意一位的值为1,则操作的结果便为1
              • 只要操作数中任意一位的值为x或z,则操作的结果便为x;
              • 否则其操作结果为0。
            • 缩减或非(~|)
              • 缩减或的操作结果求反
            • 缩减异或(^)
              • 只要操作数中任意一位的值为x或z,则操作的结果便为x;
              • 若操作数中有偶数个1,则操作结果为0
              • 若操作数中有奇数个1,则操作结果为1
              • 用途: 计算奇偶校验位。
            • 缩减同或(~^或者^~)
              • 缩减异或的操作结果求反
            💡
            唯一一个需要注意的点就是,操作数中只要存在x或者z,那么操作结果就是x。 另外这里还需要明确一个运算:~x=x,~z=x
             

            2.3.7 移位操作符

            Verilog支持4种移位操作符:逻辑左移(<<)、逻辑右移(>>)、算术左移(<<<)、算术右移(>>>)。
            • 移位操作符 (<<, >>, <<<, >>>)
              • 左侧操作数:被移位的数据。
              • 右侧操作数:移位的次数。
                • 总是被视为无符号数
                • 如果右侧操作数是x或z,则整个移位结果为x。
            • 逻辑移位(<<, >>
              • 对逻辑移位操作符,由于移位而腾空的位总是填0。
            • 算数移位(<<<,>>>
              • 左移腾空的位总是填0
              • 右移
                • 如果位于操作符左侧的操作数是无符号数,则腾空的位总是填0【无符号数的逻辑移位和算术移位是一样的】
                • 如果位于操作符左侧的操作数是有符号数,则腾空的位总是填符号位
             

            2.3.8 条件操作符

            条件操作符 (? :) 是 Verilog 中唯一的三元运算符,它提供了一种简洁的方式来实现“如果...那么...否则...”的逻辑。根据条件表达式的值从两个表达式中选择一个表达式。
            • 语法格式
              • 执行流程:
                  1. 先计算“条件表达式”。
                  1. 如果条件为真(非零),则返回“表达式1”的值。
                  1. 如果条件为假(零),则返回“表达式2”的值。
            • 示例
              • 先计算条件表达式marks > 18
              • 如果marks > 18为真,则student = grade_a
              • 如果marks > 18为假,则student = grade_b
             

            2.3.9 拼接和复制操作符

            拼接操作符 ({}) 用于将多个操作数(信号或常量)“拼接”成一个更宽的操作数。
            • 语法格式
              • 将各个操作数用{}扩起来,每个操作数之间用,隔开
              • 所有表达式从左到右依次拼接
              • 操作数类型可以是线网类型或者寄存器类型。
            • 示例
              • 关键特性
                • 位宽:拼接后的总位宽等于所有表达式位宽之和
                • 如果需要多次重复拼接同一个操作数,可以使用常数表示需要重复拼接的次数
                  • 通常会在需要赋值的操作数上再加上一层花括号。并且这里所说的常数,通常就是直接十进制表示的整数
               

              2.4 Verilog HDL的基本结构

              2.4.1 Verilog-HDL语法基础

              💡
              核心概念总结
              Verilog HDL是一种硬件描述语言,用于描述数字电路的结构和行为。它主要有三种建模方式:门级描述数据流级描述行为级描述。
              一个完整的 Verilog 模块通常包含以下几个部分:
              1. 模块声明 (module ... endmodule)
              1. 端口定义 (input, output, inout)
              1. 内部信号定义 (wire, reg)
              1. 功能描述 (门级实例化、数据流赋值、行为语句)
              • 注释
                • 多行注释:/*……*/
                • 单行注://
                  • //开始到该行结束。
              • 标识符
                • 定义:用来命名模块、信号、变量等的名字。
                • 可以包含英文字母、数字、下划线_$符号。
                • 首字符不能是数字
                • 大小写敏感
              • module与endmodule
                • 这是一对关键字,必须成对出现,用于定义一个模块的开始和结束。所有模块内的代码都必须写在这两者之间。
              • input定义输入信号变量
                • 用于声明模块的输入端口。
                • 输入信号通常是wire类型(默认),表示它是一个连线,其值由外部驱动。
                • 格式:input [位宽] 信号名;
                  • 例如:input A, B; (默认是1位)
                  • 或者:input [7:0] data_in; (8位输入)
              • output定义输出信号变量
                • 用于声明模块的输出端口。
                • 输出信号可以是wirereg类型。如果是组合逻辑输出,通常用wire;如果是时序逻辑(如寄存器)输出,则用reg
                • 格式: output [位宽] 信号名;
                  • 例如:output F;
              • 门级描述
                • 这是通过调用Verilo 内置的“门原语”来构建电路。
                • 语法: 门类型 实例名 (输出, 输入1, 输入2, ...);
                  • 顺序固定:必须是(输出, 输入1, 输入2, ...)
                  • 实例名:U1G1等是给这个门实例起的名字,方便调试和引用。
                • 内置门原语: 包括and, nand, or, nor, xor, xnor, buf (缓冲器)
                • 示例
                  • 这相当于在电路图中放置了一个两输入的与门,输入是 A 和 B,输出是 F。
              • 数据流描述
                • 这是通过连续赋值语句 (assign) 来描述组合逻辑。
                • 语法:assign 输出信号 = 表达式;
                  • 表达式中使用的是位运算符,而不是算术运算符。
                  • 位运算符:
                    • & : 位与 (AND)
                    • | : 位或 (OR)
                    • ~ : 位非 (NOT)
                    • ^ : 位异或 (XOR)
                    • ~^^~ : 位同或 (XNOR)
                  • 优先级: ~ (最高) > & > ^ > ~^ > | (最低)。复杂表达式建议加括号。
                • 示例
                • endmodule结尾
                  • 最后一行是endmodule,标志着模块定义的结束。这一行末尾没有分号;
                  • module行(首行)有“;”
                  • 模块内部的大多数语句(如input, output, assign, and等)末尾必须有分号 ;
                 

                2.4.2 模块的定义

                通用模板
                Verilog中有三种基本级别:门级、数据流级和行为级
                两种建模方式对比
                • 门级描述
                  • 特点
                    • 非常直观,直接对应物理电路图。
                    • 描述的是电路的结构
                    • 适合小规模、简单的组合逻辑。
                  • 优点:易于理解和仿真,与实际硬件对应关系最直接。
                  • 缺点:对于复杂电路,代码冗长,不易维护。
                • 数据流级描述
                  • 特点
                    • 描述的是信号之间的数据流动和逻辑关系
                    • 更抽象,更接近数学表达式。
                    • 代码简洁,易于阅读。
                  • 优点:代码量少,效率高,适合描述复杂的组合逻辑。
                  • 缺点:不直接反映电路结构,对于初学者可能不如门级直观。
                 

                2.4.2 模块的编写练习

                1.编写一个2输入或门模块
                • 使用门级描述
                • 使用数据流级描述
                  2.编写一个2输入异或门模块
                  • 同样用两种方法。
                     
                    notion image
                    💡
                    在Verilog-HDL中,属于内置门实例语句的有:and(与)、nand(与非)、or(或)、nor(或非)、xor(异或)、xnor(同或)、buf(缓冲器)。
                    notion image
                    💡
                    在这里用assign来描述电路的逻辑功能,位运算符“&”。位运算符的种类和功能有:&:AND;︳:OR;~:NOT;ˆ:XOR;~ˆ:XNOR
                    位运算的优先顺序如下:~ > & > ˆ > ~ˆ > |
                     
                     

                    2.5 Verilog HDL的行为级描述

                    • 行为级描述
                      • 行为级描述相当于软件设计过程中的流程图描述或算法描述,它抽象地表达电路功能的行为表现,而不是具体的实现手段和方法。
                      • 核心思想:你告诉Verilog“这个电路应该做什么”,而不是“它内部的门电路怎么连接”。
                      • 优势:非常适合描述复杂的时序逻辑电路(如状态机、计数器、CPU控制器等),代码简洁,易于理解和修改。
                    • 行为级描述的核心:过程块
                      • 行为级描述主要通过两种过程块来实现:
                        • initial过程块
                        • always过程块
                      • 重要前提:只有寄存器型数据(reg)能在过程块中被赋值
                        • wire(线网)类型的信号是“被动”的,它的值由驱动它的元件(如门、assign语句)决定。
                        • reg类型的信号是“主动”的,它的值可以由过程块中的语句主动改变,并且能保持住当前值,直到下一次被赋值。这正是时序逻辑(如触发器)所需要的特性。
                      • 所有的initialalways过程块在仿真开始的0时刻就会并发执行(同时启动)。

                    2.5.1 赋值操作

                    Verilog 有两种基本的赋值操作,它们的应用场景完全不同:
                    赋值类型
                    关键字
                    适用对象
                    特点
                    连续赋值
                    assign
                    wire (线网)
                    持续驱动,只要右边表达式变化,左边就立即更新。用于组合逻辑。
                    过程赋值
                    =<=
                    reg (寄存器)
                    在过程块内执行,受控制语句(如 if, case, always @)影响。用于时序逻辑和复杂组合逻辑。
                    • 连续赋值 (assign)
                      • 用途: 专门用于对wire类型的信号进行赋值。
                      • 特点: 它是一种“并行”的、持续的赋值。只要其右侧的表达式发生变化,左侧的wire 就会立即更新。
                      • 例子
                        • 这是一个三元条件运算符,如果enable为真,则mynet=data;否则等于 0。这是一个典型的组合逻辑。
                    • 过程赋值
                      • 用途: 专门用于对reg类型的信号进行赋值。
                      • 特点
                        • 把值放在寄存器中,过程赋值没有持续时间,相反,寄存器将保持赋值的值,直到发生下一次对变量的赋值。
                        • 过程赋值发生在过程块(always,initial,task和function)中,可以把它认为是触发器赋值(因为它的执行依赖于特定的事件(如时钟上升沿))。当执行到达过程块的赋值时,触发就发生。
                        • 受执行语句控制,事件控制、延迟控制、if语句、case语句和循环语句都能用来控制是否执行赋值操作。
                    💡
                    总结一下,就是线网类型的变量只能使用assign进行赋值,并且不能使用在过程块中;而寄存器类型的变量只能在过程块中赋值;
                    那么就是在使用数据流级进行描述的时候,就只能使用位操作符和assign连续赋值语句;但是在使用行为级进行描述时,既可以使用过程块和过程性赋值语句也可以在过程块外使用assign连续赋值语句。
                     

                    2.5.2 过程块结构的定义

                    • initial过程块
                      • 定义
                        • 特点:
                          • 在仿真时间0时刻开始执行
                          • 只执行一次,执行完毕后就“死亡”了。
                          • 用途:
                            • 给寄存器变量赋初始值
                            • 编写测试激励(Testbench),模拟输入信号的变化。
                      • always过程块
                        • 定义
                          • 特点:
                            • 在仿真时间0时刻开始执行
                            • 无限循环,反复执行。
                            • 它的执行不是无时无刻都在进行,而是只有当敏感事件表中的信号发生变化时才会被触发执行
                            • 用途: 描述时序逻辑(如D触发器、计数器)和复杂的组合逻辑
                          • 敏感事件表 (@(...))
                            • @(posedge clk):当clk的上升沿到来时触发。
                            • @(negedge clk):当clk的下降沿到来时触发。
                            • @(a or b or c):当ab,或c中任意一个信号发生变化时触发。(常用于组合逻辑)
                            • @(*):自动推断敏感列表,包含该always块内所有读取到的信号。(推荐用于组合逻辑,避免遗漏)
                        💡
                        需要注意的是,在格式上,initial和always关键字后面是没有分号的;而在执行时间上initial过程块和always过程块都是在0时刻开始并发执行的(assign连续赋值语句也在并发执行)

                        2.5.3 顺序语句与阻塞/非阻塞赋值

                        💡
                        可以理解为:begin-end就是程序设计语言中语句块的花括号。因此当always、initial、分支语句中所包含的语句超过一句的时候,就需要使用begin-end将语句包括起来了。
                        initialalways过程块内部,你可以使用类似软件编程的顺序结构分支结构循环结构
                        • 顺序结构 (begin-end)
                          • 使用begin ... end来将多条语句组织成一个语句块。
                          • begin-end块内的语句是按顺序执行的。
                        在顺序块中出现的语句都是过程性赋值语句,过程性赋值语句有两种类型:
                        类型
                        符号
                        执行方式
                        适用场景
                        关键区别
                        阻塞赋值
                        =
                        顺序执行。前一条语句必须执行完毕,才能执行下一条。
                        组合逻辑
                        适用于描述“先算A,再用A算B”的顺序逻辑。
                        非阻塞赋值
                        <=
                        并行执行。所有赋值操作“同时”安排在未来某个时刻完成,然后继续执行下一条语句。
                        时序逻辑
                        适用于描述“在同一时钟沿,多个寄存器同时更新”的情
                        非阻塞赋值即并不等到前式赋值完成后才执行下一句。而阻塞过程性赋值是一直等到前式被赋了新值后才执行下一句。
                        这段代码展示了阻塞赋值 (=)非阻塞赋值(<=)在结合延迟控制(#)时的行为差异。Z
                        notion image
                        notion image
                        💡
                        从上面这个例子就能看出,阻塞赋值一定要等到前面的赋值语句执行结束,因此如果使用绝对时间规定赋值语句的执行时间就不一定有意义了(因为绝对时间并不一定能被满足),因此是使用相对时间来指定赋值语句执行的时间;而非阻塞赋值由于赋值语句之间没有同步的要求,因此会直接使用绝对时间来指定赋值语句的执行时间。
                         

                        2.5.4 分支语句

                        这两者是 Verilog 中实现条件判断的主要手段,类似于软件编程中的if-elseswitch-case
                        • if_else 语句
                          • 语法
                            • 特点
                              • 按顺序检查条件,一旦某个条件为真,就执行对应的语句,然后跳出整个if-else 结构。
                              • 最后的else是可选的,用于处理所有条件都不满足的情况。
                              💡
                              这里的条件表达式需要进行连接的时候,需要使用逻辑操作符(与&&、或||、非!,与程序设计语言中的运算符是相同的),而不是位操作符(与&、或|、非~、异或^、同或~^/^~)。
                              在格式上还需要注意的是,if关键字和后面的条件表达式之间有一个空格
                           
                          • case语句
                            • 语法
                              • 特点
                                • case语句会将控制表达式的值与各个分支项表达式进行精确匹配
                                • 一旦找到匹配项,就执行对应的语句,然后跳出case结构。
                                • default分支是可选的,用于处理所有分支项都不匹配的情况。
                              • 适用场景: 当需要根据一个信号的不同取值来选择不同的操作时,如状态机、数据选择器等。它比 if-else 更清晰、更高效。
                            • 4选1数据选择器的两种实现
                              • 这是一个经典的组合逻辑电路,它的功能是根据地址信号A[1:0] 从四个输入 D0, D1, D2, D3 中选择一个输出到Y
                                • notion image
                                  notion image
                            实现一:使用case语句
                            注意: 这里使用的是阻塞赋值 =,因为这是一个组合逻辑电路,没有时钟,也不需要保持状态。
                            实现二:使用if_else语句
                             

                            2.6 习题

                            notion image

                            • 1. 硬件描述语言主要有哪两种?
                              • 答: 硬件描述语言(HDL)主要有两种:
                              • Verilog HDL: 语法风格类似 C 语言,简洁明了,应用非常广泛。
                              • VHDL (VHSIC Hardware Description Language): 语法风格类似 Ada 语言,结构严谨,类型系统更复杂,常用于军事和航空航天等高可靠性领域。

                            • 2. Verilog提供了哪几种描述方式?
                            答: Verilog 提供了三种基本的描述方式(建模级别):
                            1. 门级描述 (Gate-Level Modeling): 通过调用内置的逻辑门原语(如 and, or, not 等)来描述电路的物理结构。
                            1. 数据流级描述 (Dataflow-Level Modeling): 使用连续赋值语句 assign 和位运算符(如 &, |, ^ 等)来描述信号之间的逻辑关系。
                            1. 行为级描述 (Behavioral-Level Modeling): 使用过程块(initial, always)和高级语句(如 if-else, case, for 等)来描述电路的功能和行为,而不关心具体的实现细节。

                            • 3. Verilog HDL的数值有哪4类?
                            答: Verilog 中的数值(或称“四值逻辑”)有以下四种状态:
                            1. 0: 逻辑低电平。
                            1. 1: 逻辑高电平。
                            1. x (或 X): 未知状态。通常表示信号未被驱动、冲突或初始化前的状态。
                            1. z (或 Z): 高阻态。通常表示三态门处于关闭状态,输出端与电路断开连接。
                            这四个值可以用来模拟真实数字电路中的各种电气状态,对于仿真和调试非常重要。

                            • 4. Verilog HDL有两大类数据类型?
                            答: Verilog HDL 主要有两大类数据类型:
                            1. 线网型 (Net Type):
                                • 最常用的是 wire
                                • 代表电路中的物理连线,其值由驱动它的元件决定。
                                • 不能存储值,必须被持续驱动。
                                • assign 语句或门原语进行赋值。
                            1. 寄存器型 (Register Type):
                                • 最常用的是 reg
                                • 代表存储单元(如触发器、锁存器),能够保持其值。
                                • 只能在过程块(initial, always)中被赋值。
                                • 即使没有时钟,也可以在组合逻辑中使用 reg 类型(此时它只是一个变量,不代表真正的寄存器)。
                            关键区别: wire 是“被动”的,reg 是“主动”的。

                            • 5. Verilog的模块结构是什么?
                            答: 一个完整的 Verilog 模块结构通常包含以下几个部分:
                            • module ... endmodule: 定义模块的开始和结束。
                            • 端口列表: 定义模块与外界交互的接口。
                            • 内部信号: 声明模块内部使用的信号。
                            • 功能描述: 描述模块的具体逻辑功能。

                            • 6. Verilog HDL的行为级描述可以使用过程块结构进行描述,过程块结构有哪两种?
                            答: 行为级描述主要通过以下两种过程块结构进行:
                            1. initial 过程块:
                                • 在仿真时间0时刻开始执行,并且只执行一次
                                • 常用于给寄存器变量赋初值或编写测试激励(Testbench)。
                            1. always 过程块:
                                • 在仿真时间0时刻开始执行,并且无限循环
                                • 通过敏感事件表(如 @(posedge clk)@(*))来控制何时触发执行。
                                • 是描述时序逻辑和复杂组合逻辑的核心。

                            • 7. 在顺序块中出现的语句都是过程性赋值语句,过程性赋值语句有两种类型?请解释这两种类型。
                            答: 过程性赋值语句有两种类型:
                            1. 阻塞式赋值 (Blocking Assignment):
                                • 使用符号 =
                                • 特点: 赋值操作是顺序执行的。当前语句执行完毕(即左边变量被赋予新值)后,才会执行下一条语句。
                                • 适用场景: 主要用于描述组合逻辑
                                • 形象比喻: 就像排队,前一个人走完了,后一个人才能开始。
                            1. 非阻塞式赋值 (Non-blocking Assignment):
                                • 使用符号 <=
                                • 特点: 赋值操作是并发执行的。所有赋值语句会“同时”计算右边的表达式,并将结果安排在当前仿真时间点之后(通常是下一个时间步)更新到左边的变量上,然后立即继续执行下一条语句。
                                • 适用场景: 主要用于描述时序逻辑(如触发器、计数器)。
                                • 形象比喻: 就像多车道的十字路口,绿灯亮起时,所有方向的车都“同时”获得通行权。

                            • 8. 阻塞式赋值和非阻塞式赋值的区别?
                            答: 两者的主要区别在于执行顺序和赋值时机
                            特性
                            阻塞式赋值 (=)
                            非阻塞式赋值 (<=)
                            执行方式
                            顺序执行
                            并发执行
                            赋值时机
                            立即赋值
                            计划在未来某个时刻赋值
                            对后续语句的影响
                            必须等待当前语句执行完毕才能执行下一句
                            不等待,立即执行下一句
                            典型应用场景
                            组合逻辑
                            时序逻辑
                            核心原则: 在同一个 always 块中,应避免混合使用 = 和 <=,以免产生难以预料的综合结果。

                            • 9. 判断:CASE语句是最重要最常用的顺序语句?
                            答:错误。
                            • 理由: 虽然 case 语句在描述多路选择器、状态机等场景时非常常用和高效,但它并非“最重要最常用”的顺序语句。
                            • 最常用的顺序语句应该是if-else 语句,因为它更通用,几乎可以处理任何条件判断。
                            • case 语句的优势在于其结构清晰、易于阅读,特别适合于“多分支选择”的情况,但 if-else 的灵活性更高。
                            • 此外,for 循环、while 循环等也是重要的顺序语句。

                            • 10. 判断:标识符 “9moon” 是不合法的标识符?
                            答:正确。
                            • 理由: 根据 Verilog 的语法规则,标识符(如模块名、信号名)不能以数字开头
                            • “9moon” 以数字 9 开头,因此是一个不合法的标识符。
                            • 合法的标识符可以是:moon9, my_moon, _moon, Moon 等。

                            希望这份详细的解答能帮助你彻底掌握本章的知识点!祝你学习顺利!
                             
                            第一章:逻辑代数基础第三章:组合逻辑电路
                            Loading...