第 4 章 Verilog HDL 基础知识

想深耕嵌入式?这个专辑值得收藏

MCU、FPGA、工控、传感器一站式学习,实战项目直接抄

4.1 Verilog概述

4.1.1 Verilog 简介

  • Verilog 是一种硬件描述语言(HDL),用于描述数字电路的结构(硬件连接方式)和行为(硬件工作逻辑)
  • Verilog可以从五个层次对电路进行描述:系统级、算法级、寄存器传输级(RTL级)、门级、开关级。日常设计中使用最多的是RTL级,因此Verilog代码也常被称为RTL代码

4.1.2 Verilog 与软件语言的的区别

  • 共同点:
    • 软件语言、Verilog 硬件描述语言,编译解析阶段都是串行:工具从上至下逐行做语法解析、构建语法结构;该串行只是代码处理方式,不决定运行逻辑
  • 核心差异:
    • 编译产物不同
      • Verilog:编译 / 综合后生成物理硬件电路,其执行(仿真 / 实际工作)逻辑为 并行启动、异步结束;
      • 高级软件语言:编译为 CPU 机器指令,,其执行逻辑为 “指令串行”(多核 / 多线程仅为 “分时叠加” 的伪并行)
  • 编译产物执行逻辑不同
    • Verilog 编译产物是 “真并行”:物理电路模块响应同一触发信号(如时钟沿)同时独立工作,因器件 / 布线 / 负载等物理延迟不同,必然 “异步结束”(硬件客观特性)
    • 高级软件语言编译产物是 “伪并行”:哪怕多核并行,每个核心内的指令仍严格串行,仅通过 “多个核心的串行叠加” 形成并行效果,本质是 “分时执行”

4.1.3 相关概念

  • 可综合:代码可以被综合工具(Vivado、Quartus)翻译成真实硬件电路(门、触发器、RAM、多路选择器等),最终能下载到 FPGA/ASIC 芯片里实际运行。
    - 只描述硬件结构、数据传输、时序控制,不能带仿真延时、打印、波形导出等虚拟行为;
    - 语法有严格限制,只能使用综合器支持的语句;
  • 不可综合:仅用于仿真器(ModelSim、VCS)做逻辑验证,无法转换成硬件电路,不能下载到 FPGA。
    - 这类语句用来产生激励、打印信息、观测波形、控制仿真启停,属于软件仿真行为,没有对应的物理器件。
    可综合代码:面向硬件,能上板运行;不可综合代码:面向仿真验证,仅电脑仿真用。
  • 仿真:借助专用仿真软件(ModelSim、VCS、Xcelium 等),在计算机软件环境里模拟硬件电路的输入激励、信号时序变化,按照 Verilog 描述的逻辑运行,观测电路输出波形与运行结果,提前验证硬件设计逻辑是否正确,不需要把代码烧写到 FPGA/ASIC 硬件芯片上。
    • 功能仿真(行为仿真):不包含器件、布线的物理传输延迟,只按照代码逻辑运行,理想状态下信号瞬间变化。
      • 作用:验证逻辑功能是否正确,检查加法器、状态机、接口时序逻辑有没有写错。
    • 时序仿真(门级仿真):加载芯片工艺延迟库,加入门延迟、走线延迟,真实还原硬件信号传输存在的时延、竞争毛刺、建立 / 保持时间违规、亚稳态等硬件真实现象
      • 作用:验证电路在实际硬件最高频率下能否稳定正常工作。

4.1.4 最简开发流程

  1. 编写Verilog代码(功能描述)
  2. 语法检查(编译,修复拼写/格式错误)
  3. 编写Testbench(测试代码,施加输入激励)
  4. 仿真验证(查看输出是否符合预期)
  5. 综合(将代码转换为门级电路网表)
  6. 布局布线(映射到FPGA/ASIC硬件资源)
  7. 下载验证(将程序烧录到硬件,测试实际效果)

4.1.5 工程代码规范

  • 缩进:统一用4个空格(避免Tab,不同编辑器Tab宽度不同);
  • 换行:每个语句独立一行,模块/always块前后空一行;
  • 空格:运算符两侧加空格(如a + b,而非a+b),逗号后加空格;

4.2 Verilog HDL 的语言要素

4.2.1 标识符

  • 标识符是模块、寄存器、端口、连线、示例和begin-end块等变量的名称,由字母、数字、下划线、$ 组成,必须以字母或下划线开头,标识符的字母区分大小写
  1. 命名建议(工程通用):
  • 时钟:clk_频率(如clk_50m、clk_1s);
  • 复位:rst_n(n表示低有效,统一风格);
  • 数据:方向+功能+位宽(如din_8bit、dout_16bit);
  • 使能:功能+en(如rx_en、tx_en)。
  1. 工程实际标识符:用英文标识符,EDA工具对中文支持不太友好

4.2.2 注释符

  • 单行注释:// 注释内容(用于单行说明,推荐);
  • 多行注释:/* 注释内容 */(用于模块/复杂逻辑说明);

工程实际注释:用英文注释,EDA工具对中文支持不太友好

4.2.3 空白符

  • 空白符包括空格符、制表符(Tab)、换行符、换页符、注释符,核心作用是分隔语法单元,编译器 / 综合器会忽略空白符,仅影响可读性,不影响逻辑;

4.2.4 转义标识符

  • 转义标识符是 Verilog 专为特殊命名需求设计的语法,允许使用原本不合法的字符(空格、特殊符号、关键字)作为标识符
    • 定义:以 \ 开头 + 空白符 结尾,结尾空白符是定义时的必须项
    • 引用:引用时需加转义符( \ ),结尾空白可省略(编译器自动识别)
    • 尽量避免使用转义标识符,转义标识符会降低代码可读性,优先用下划线替代空格或特殊符号

4.2.5 关键字

  • 关键词是Verilog HDL语言内部的专用词,都采用小写形式

作为语言本身的保留字,关键字有其特定和专有的语法作用,用户不能再对它们做新的定义

4.2.6 数值

  • 整数及其表示方法
    • 无位宽的十进制表示法:[±] 十进制数字,例如:-132
    • 位宽 + 进制的表示方法 【工程推荐】:[±] [位宽]'[s][进制符][数字]
      • [±] 和 s 配合声明有符号数(补码),必须同时存在,否则verilog 默认为无符号数(纯二进制数,即无符号位,所有位都是数值位)
        • + 和 s:声明有符号正数,最高位(符号位)为 0,其余位为数值位,例如:+8’sd118 表示 8 位有符号 +118,verilog默认转换为补码就是:01110110
        • - 和 s:声明有符号负数,最高位(符号位)为 1,,其余位为数值位,例如:-8’sd118 表示 8 位有符号 -118,verilog默认转换为补码就是:10001010

省略 s 时,[±] 符号无意义,规范写法中必须同时省略[±],即使写了编译器也会忽略,表示纯二进制数值(无符号数)。例如:8’d132 表示 8 位无符号十进制数 132,verilog默认转换为二进制就是 10000100 【verilog默认声明无符号数】

  • 位宽:表示数值的位数

    • 省略时默认 32 位
    • 实际位宽 大于 定义位宽,则最左边的位(高位)会被截断,导致数值错误
    • 实际位宽 小于 定义位宽,对于无符号数,高位(最左侧)补 0 ,对于有符号数(补码),高位(最左侧)补符号位(正数补0 ,负数补1)
  • 进制符:【 b/B:二进制 【建议使用】、d/D:十进制、h/H:十六进制【建议使用】、o/O:八进制】,进制符推荐小写
    - 数字:对应进制的合法值(超出合法值范围会报错)

    • verilog 中二进制合法值为: 0、1、x/X、z/Z、_、?
    • verilog 中十进制合法值为:0 - 9、_
    • verilog 中十六进制合法值为:0 - 9、a/A - f/F、x/X、z/Z、?、_
  1. 下划线( _ ) 是为了提高数值的可读性,在数值常量的任意位置(首字符除外)可以随意插入下划线( _ )
  2. 特殊值 x/z 是硬件描述的核心,用于表示未知 / 高阻态,x 或z在二进制中代表1位 x或z,在十六进制中代表 4位 x或z
  3. ? 是 case 语句的通配符(简化高阻表示),在 case 语句中 字符 ? 和 高组态(Z或z) 是等价的
  • n 位有符号数(补码)可以表示的数值范围为:−2n−1∼2n−1−1 -2^{n-1} \sim 2^{n-1} - 1 2n12n11
  • n 位无符号数可以表示的数值范围为:0∼2n−10 \sim 2^{n} - 102n1
  1. 无符号数是一种二进制数值(无符号位),有符号数(补码)是一种编码
  2. 有符号正数的原码、反码、补码规则
  • 原码:数值直接转二进制,最高位的左侧补符号位 0
  • 反码 = 原码
  • 补码 = 原码
  1. 有符号负数的原码、反码、补码规则
  • 原码:数值绝对值转二进制,最高位的左侧补符号位 1
  • 反码:符号位 1 保持不变,后面数值位全部按位取反(0 变 1,1 变 0)
  • 补码:负数反码 + 1
  • 实数及其表示【仿真使用】
    • 十进制表示法,例如:3.0
    • 科学计数表示法,例如:564.2e2的值为56420.0

实数转整数规定:通过 四舍五入 转换为最相近的整数,例如:-13.74 转换为整数是 -14

  • 字符串及其表示【仿真使用】
    • Verilog 中的字符串是由双引号(" ")括起来的字符序列,必须包含在一行中,不能多行书写。在表达式或赋值语句中作为操作数的字符串被看作ASCII值序列,即一个字符串中的每一个字符对应一个8位ASCII值(即每个字符占 1 字节,1字节= 8 bit),仅用于仿真调试(如打印信息、标注信号含义),不可综合(无对应硬件实体)
      • 表示形式:必须用双引号(" ")包裹,例如:“clk_div module”
      • 存储要求:字符串需存储在 reg型变量中 ,变量位宽 ≥ 字符串字符数 × 8
      • 字符限制:仅支持ASCII字符,不支持中文(EDA工具对中文字符串兼容性差,易乱码);
      • 拼接规则:字符串与变量拼接需通过display/display/display/monitor等系统函数实现,直接赋值仅支持纯字符串

字符串仅用于仿真调试,核心作用是提升日志可读性,代码中避免大量使用;打印时优先拼接关键变量(如时间、信号值),减少无意义的纯字符串输出。

4.3 Verilog HDL 对硬件电路建模的核心底层逻辑

Verilog HDL对硬件电路建模的核心底层逻辑——通过物理数据类型(连线/寄存器/存储器)、四值逻辑电平、信号强度三大维度,还原实际硬件的电气特性。

4.3.1 四值逻辑电平:抽象实际硬件的电气状态

  • Verilog HDL有四种基本的逻辑数值状态,用数字或字符表达数字电路中传送的逻辑状态和存储信息
    • 状态 0:代表 低电平、逻辑 0 或 假,在实际电路中代表硬件引脚或导线处于低电压(例如:0v)
    • 状态 1:代表 高电平、逻辑 1 或 真,在实际电路中代表硬件引脚或导线处于高电压(例如:3.3v或5v)
    • 状态 x/X:代表不确定或未知的逻辑状态,在实际电路中代表信号未初始化或驱动冲突或电平不确定
    • 状态 z/X:代表高阻态,在实际电路中代表引脚悬空或者三态门关闭(无驱动源)
  1. 软件语言只有 0/1 二值逻辑,而Verilog的 x/z 是对硬件电气特性的关键抽象——比如z直接对应三态门的高阻状态,x能在仿真中快速定位“未初始化信号”“驱动冲突”等硬件问题
  2. x、z 只是 Verilog 仿真阶段的抽象电平,仅仿真器能识别,真实硬件不存在这两种状态

4.3.2 信号强度

实际硬件中,多个驱动源可能同时驱动同一根导线,verilog通过信号强度来表示数字电路中不同强度的驱动源,用来解决不同驱动强度下的赋值冲突

  • 驱动强度标识:仿真中建模硬件冲突,解决多驱动源的赋值冲突(工程中99%场景只用这5个)
    • supply1/supply0:电源(VDD)/地(GND)级驱动(最强,不可覆盖),典型应用:建模电源/地对导线的驱动
    • strong1/strong0:逻辑门(与/或/非门)输出驱动,典型应用:建模普通逻辑门的输出
    • pull1/pull0:上拉/下拉电阻驱动,典型应用:建模I/O口的上拉/下拉
    • weak1/weak0:弱上拉/弱下拉电阻驱动,典型应用:建模弱上拉的总线(如I2C)
    • highz1/highz0【最常用】:高阻态(无驱动能力),典型应用:建模三态门关闭、引脚悬空
  • 电荷存储强度标识:建模硬件电容的电荷存储能力,无驱动优先级(几乎不用,了解即可)
    • large:大电容(存储电荷多),典型应用:仿真中建模高速电路的电容延迟
    • medium:中等电容,典型应用:普通数字电路的电容建模(极少用)
    • small:小电容(存储电荷少),典型应用:高频电路的寄生电容建模
  1. 只有驱动强度(supply/strong/pull/weak/highz)参与“多驱动源冲突判定”
  2. 驱动强度优先级(解决多源冲突的核心): supply > strong > pull > weak > highz
  3. 优先级覆盖:高优先级驱动强度完全覆盖低优先级(如:supply0 > strong1,最终信号为0);
  4. 同优先级冲突:同强度的0和1驱动同一根线,结果为x(未知,对应硬件总线竞争);
  5. highz无驱动能力:只要有非highz的驱动源,信号值由最强的非highz源决定;仅当所有驱动源都是highz时,信号才为:z
  6. 电荷强度不参与驱动冲突:large/medium/small 仅影响信号延迟,不改变最终逻辑值。
  7. 综合工具不识别任何信号强度:信号强度仅用于仿真(还原硬件电气特性)

4.4 常用的数据类型

4.4.1 wire 类型【工程里 99% 只用 wire】

  • 定义:wire 是 Verilog 最基础的线网类型,用来表示硬件中两根引脚之间的物理导线,负责传递电平信号,只能连续驱动,不能存储数值。
  • 语法声明:wire signed [高位:低位] 变量1,变量2,…,变量n;
  1. signed:是可选项,省略时默认声明的是无符号变量,有 signed 时声明的是有符号变量
  2. [高位:低位] :是可选项,用于声明变量的位宽,省略时默认位宽为 1 位,位宽=高位 - 低位 + 1
  • 驱动(赋值)方式:
    • 连续赋值 assign 驱动(最常用)
    • 子模块输出端口直接驱动
  1. 单个 wire 变量 只能有一个驱动源,多个 assign 同时驱动会出现逻辑冲突、仿真出现 x 未知态;
  2. 仿真初始阶段,若 wire 变量没有被任何驱动源驱动,默认值为高阻态 z

4.4.2 reg类型

  • 定义:reg 属于过程赋值变量,只能在 always、initial 过程块内部赋值,具备电平保持能力;不一定直接对应硬件寄存器,最终综合结果由所在逻辑场景决定。
  • 语法声明:reg signed [高位:低位] 变量1,变量2,…,变量n;
  1. signed:是可选项,省略时默认声明的是无符号变量,有 signed 时声明的是有符号变量
  2. [高位:低位] :是可选项,用于声明变量的位宽,省略时默认位宽为 1 位,位宽=高位 - 低位 + 1
  • 驱动(赋值)方式:
    • 阻塞赋值(=):只能写在过程块内部,适用于组合逻辑过程块,组合逻辑必须先给 reg 赋默认值,避免锁存器。
    • 非阻塞赋值(<=):只能写在过程块内部,适用于时序逻辑过程块
  1. 禁止使用 assign 连续赋值直接驱动 reg 变量
  2. reg 在仿真刚开始、还没有任何赋值操作时,默认值为 x(未知态)
  3. 单个 reg 变量允许在同一个过程块内多次赋值;多个不同过程块对同一个reg 赋值则属于多驱动冲突,仿真出现 x

4.4.3 信号的选取

n 比特信号有三种选择语法:整体选取、单比特位选择、连续区间段选择

整体选取

  • 语法:直接写信号名
wire [15:0] a, b;
wire [15:0] c;
assign c = a; // 整体选取,把a全部16位一次性赋值给c

单比特位选取 vec[idx]

  • 语法:向量名[下标]
  • 功能:只提取总线中单独 1 个比特,输出 1bit 信号。
  • 下标可以是任意合法数值或表达式,允许:
    • 数字常量:in[5]
    • 参数 localparam
    • 硬件动态信号
input [1023:0] in;
input [7:0] sel;
output [3:0] out;

assign out[0] = in[sel * 4 + 0];
assign out[1] = in[sel * 4 + 1];
assign out[2] = in[sel * 4 + 2];
assign out[3] = in[sel * 4 + 3];
// 逻辑:sel 运行时实时变化,每条语句单独选 1 根线,综合器自动生成多路选择器,语法完全合规。

连续区间段选择:vec[high : low]

  • 语法:向量名[高位下标 : 低位下标]
  • 功能:一次性截取一段连续多位比特,输出多位总线。
  • 要求:高位下标 > 低位下标
  • 高位下标 和 低位下标必须是编译期常量,不允许是动态硬件信号
    • 纯数字
    • localparam 本地参数
  • 区间切片遇到信号做索引时,可以转换为:把要截取的连续 W 位,拆成 W 个单独单 bit 变量索引,分别赋值给输出向量每一位
input [1023:0] in;
input [7:0] sel;
output [3:0] out;

assign out[0] = in[sel * 4 + 0];
assign out[1] = in[sel * 4 + 1];
assign out[2] = in[sel * 4 + 2];
assign out[3] = in[sel * 4 + 3];
// 逻辑:sel 运行时实时变化,每条语句单独选 1 根线,综合器自动生成多路选择器,语法完全合规。

4.4.4 存储器类型

  • 定义:存储器本质是一维 reg 数组,并非 Verilog 独立原生数据类型,由多个同位宽的 reg 有序排列组成,可综合为寄存器堆、分布式 RAM、Block RAM、ROM 等硬件存储结构,支持按地址寻址读写数据,具备硬件存储能力。
  • 语法声明:reg signed [高位:低位] 存储器名 [地址上限:地址下限];
  1. signed:是可选项,省略时默认声明的是无符号变量,有 signed 时声明的是有符号变量
  2. [高位:低位] :是可选项,用于声明变量的位宽,省略时默认位宽为 1 位,位宽=高位 - 低位 + 1
  3. [地址上限:地址下限]:表示声明了 地址上限 - 地址下限 + 1 个 位宽为 高位 - 低位 + 1 的寄存器
  4. 多个存储器分开声明
  • 赋值方式:
    • 仅支持按单个地址单元读写,不支持存储器整体直接赋值
    • 只能在过程块内对指定地址的寄存器赋值;
  1. 标准 Verilog 中二维 reg 数组不可综合,仅能用于仿真场景,无法映射为硬件存储器;
  2. 将二维 reg 数组转换为一维reg数组实现二维数组可综合,转换公式:一维总地址 = 当前行号 × 每行总列数 + 当前列号【即:把行列转换为一行】
  3. 存储器不能直接作为模块端口,需要借助普通 reg/wire 中转完成数据读写
  4. 单个过程块内,可以对存储器的同一地址多次赋值;多个不同过程块对存储器的同一地址多次赋值则属于多驱动冲突,仿真出现 x

4.4.5 抽象数据类型

Verilog HDL提供了以下几种抽象数据类型:整型(integer)、时间型(time)、实型(real)及参数型(parameter)。它们只是纯数学的抽象描述,不能够与实际的硬件电路相映射。主要用于辅助设计和验证。

  • 整型(integer)语法声明:integer 变量1,变量2,…,变量n;
  • 时间型(time)语法声明:time 变量1,变量2,…,变量n;
  • 实型(real)语法声明:real 变量1,变量2,…,变量n;
  • 参数型(parameter)语法声明:parameter 参数名1=表达式1,参数名2=表达式2,…,参数名n=表达式n;

4.5 运算符

4.5.1 混合运算规则

  • 混合运算:即 有符号数 与 无符号数同时一起参与运算
    • 只要任意一个操作数为无符号数,整个表达式按无符号规则运算,有符号操作数会被隐式转无符号数参与计算
  • 隐式转换:Verilog 默认的隐式转换不修改任何二进制位值,仅将有符号数的补码(物理存储形态)强制按 “无符号数(纯数值位,无编码规则)” 解释,导致运算结果完全错误,代码中要禁用隐式转换
  • 显式转换(仅改变 “位的解释规则”,不修改位值):
    • $signed(无符号数) 函数:让解析器将无符号数按照补码规则解析
    • $unsigned(有符号数)函数:让解析器将有符号数按照二进制规则解析

4.5.2 移位运算符

  • <<(逻辑左移):
    • 操作数:无符号数
    • 运算规则:无符号数向左移位,右侧空位补 0,位宽不够时会发生数据溢出,导致高位被截断
  • >>(逻辑右移):
    • 操作数:无符号数
    • 运算规则:无符号数向右移位,左侧空位补 0,右侧超出位宽的低位截断丢弃,不会发生数据溢出
  • <<<(算术左移):
    • 操作数:有符号数(补码)
    • 运算规则:补码所有位(含符号位)左移n位,低位补0,溢出高位丢弃,如果符号位发生改变就有溢出,符号位不变,则一定无溢出
  • >>>(算术右移):
    • 操作数:有符号数(补码)
    • 运算规则:有符号数向右移位(含符号位),左侧空位补符号位(正数补0,负数补1)

(一)逻辑左移和算术左移的区别

  1. 溢出本质相同:都是数值超出当前位宽的合法表示范围,高位被截断导致结果错误。
  2. 逻辑左移溢出:只有数值错误,不存在正负翻转;依靠最高位是否移出 1 判断溢出。
  3. 算术左移溢出:最典型特征是正负号发生翻转,可以通过移位前后符号位是否改变快速判定溢出

4.5.3 算术运算符

  • *(乘法):
    • 操作数:无符号数或有符号数;
    • 运算结果:无符号数相乘结果为无符号数,有符号数相乘结果为有符号数;无符号数乘有符号数结果为无符号数
  • /(除法):
    • 操作数:无符号数或有符号数;
    • 运算结果:无符号数相除结果为无符号数,有符号数相除结果为有符号数;无符号数除有符号数结果为无符号数
  • %(取模/取余):
    • 操作数:无符号数或有符号数;
    • 运算结果:无符号数取余结果为无符号数,有符号数取余结果为有符号数;无符号数除余有符号数结果为无符号数
  • +(加法):
    • 操作数:无符号数或有符号数;
    • 运算结果:无符号数相加结果为无符号数,有符号数相加结果为有符号数;无符号数加有符号数结果为无符号数
  • -(减法):
    • 操作数:无符号数或有符号数;
    • 运算结果:无符号数相减结果为无符号数,有符号数相减结果为有符号数;无符号数减有符号数结果为无符号数
  1. 无符号数 × 2ⁿ ,优先用逻辑左移(<<n)实现(可综合、资源少)
  2. 无符号数 ÷ 2ⁿ,优先用逻辑右移(>>n)实现(可综合、资源少)
  3. 非2幂次的乘除仅用于仿真,禁止写入硬件综合代码;
  4. 加法/乘法必须用足够位宽的变量接收结果,无符号数减法需先判断被减数 ≥ 减数,避免溢出导致结果错误;
  5. 工程中常用 2 的幂次位宽(8/16/32 位)
  6. 有符号数以补码表示,补码是一种编码,非二进制数,无符号数以二进制数表示,非编码

4.5.4 关系运算符

核心包含 8 种运算符:>(大于)、<(小于)、>=(大于等于)、<=(小于等于)、(等于)、(!=)不等于、(=)全等于、(!==)不全等于,统一规则如下:

  • 操作数:无符号数或有符号数
  • 运算结果:满足比较条件结果为 1’b1,不满足为 1’b0,操作数含x/z时结果为 1’bx;
  1. == :逐位比较,两个操作数所有比特不含x/z,并且所有比特均相等,结果为 1’b1,两个操作数所有比特不含x/z,并且所有比特存在不相等,结果为 1’b0,两个操作数任意比特存在x/z,结果为 1’bx,可以综合为同或门
  2. != :逐位比较,两个操作数所有比特不含x/z,并且所有比特均相等,结果为 1’b0,两个操作数所有比特不含x/z,并且所有比特存在不相等,结果为 1’b1,两个操作数任意比特存在x/z,结果为 1’bx,可以综合为异或门
  3. === :逐位比较,两个操作数所有比特(含x/z)均相等,结果为 1’b1,两个操作数所有比特(含x/z)存在不相等,结果为 1’b0,绝对不会产生1’bx,全等仅用于仿真,不可综合
  4. !== :逐位比较,两个操作数所有比特(含x/z)均相等,结果为 1’b0,两个操作数所有比特(含x/z)存在不相等,结果为 1’b1,绝对不会产生1’bx,不全等仅用于仿真,不可综合

4.5.5 位运算符

  • &(按位与):逐位进行与运算;
    • 规则:有 0 则结果为 0,全 1 则结果为 1;若某一位出现 x/z,该位结果为 x;
    • 可综合性:硬件由与门实现。
  • |(按位或):逐位进行或运算
    • 规则:有 1 则结果为 1,全 0 则结果为 0;若某一位出现 x/z,该位结果为 x;
    • 可综合性:硬件由或门实现。
  • ^(按位异或):逐位异或运算;
    • 规则:相同为 0,相异为 1;存在 x/z时该位结果为 x;
    • 可综合性:硬件由异或门实现。
  • ~/~(按位同或):逐位同或运算;
    • 规则:相同为 1,相异为 0;存在 x/z 时该位结果为 x;
    • 可综合性:硬件由同或门实现。
  • ~(按位取反)
    • 规则:对操作数每一位按位取反:0 变 1、1 变 0;输入为 x/z 时输出仍为 x/z ;
    • 可综合性:硬件由反相器(非门)实现。

4.5.6 缩位(归约)运算符

  • &(缩位与)
    • 规则:数据位中只要出现任意一位 0,结果为 1’b0; 所有比特全部为 1,结果为 1’b1; 只要任意一位包含 x/z,仿真结果为 1’bx
  • |(缩位或)
    • 规则:数据位中只要出现任意一位 1,结果为 1’b1; 所有比特全部为 0,结果为 1’b0; 只要任意一位包含 x/z,仿真结果为 1’bx
  • ^(缩位异或)
    • 规则:依次对所有比特异或,偶数个 1 结果 1’b0,奇数个 1 结果 1’b1;含 x/z 则结果 1’bx

4.5.7 逻辑运算符

  • &&(逻辑与):所有操作数为 非0数值 结果为 1,否则为 0,含x/z时结果为 x
  • ||(逻辑或):任意一个操作数为非0数值,结果为 1,否则为 0,含x/z时结果为 x
  • !(逻辑非):操作数数值非0 结果为 0、操作数数值为 0 为 结果 1,操作数含 x/z(未知/高阻)时结果为 x

组合逻辑中 用位运算替代逻辑运算,逻辑运算会被综合器拆解为位运算,直接用位运算可减少综合步骤;

4.5.8 条件运算符

  • 语法格式为:条件表达式 ? 表达式1 : 表达式2
  • 运算规则:条件表达式为真(1),执行表达式1、条件表达式为假(0),执行表达式2、条件表达式未知,结果为x
  1. 简单二选一逻辑优先用条件运算符,超过4选1建议用 case 语句(可读性更高,综合器优化效果更好);
  2. 表达式1和表达式2必须保证位宽、类型完全一致,否则综合器会自动补零/截断,导致结果错误

4.5.9 拼接和复制运算符

  • {}(拼接运算符):
    • 语法格式:{元素1,元素2,…}
    • 规则:从左至右依次拼接各个元素,组合为新的高位宽向量;所有元素必须显式带位宽
  • {{}}(复制运算符):
    • 语法格式:{num{矢量}}
    • 规则:将内部矢量连续复制 num 次,再拼接成宽位信号,重复次数 num 只能是常量,不能是变量,两组大括号均为必需项。

4.5.10 运算符的优先级

  • 工程中优先用括号明确运算顺序,减少对优先级的依赖,因为不同的综合开发工具在执行这些优先级时可能有微小的差别,因此在书写程序时 用括号来控制运算的优先级 ,以有效避免错误,同时增加程序的可读性

4.6 模块

Verilog里,所有设计都以模块(module)为单位。一个模块就是一个功能块

4.6.1 模块的基本结构

module 模块名 (端口声明);
    // 1. 内部信号定义 wire / reg / parameter
    // 2. 并行语句:assign、always、子模块例化
endmodule

4.6.2 模块端口声明

端口是模块和外界的接口

  • 端口声明语法:端口方向 端口数据类型 [高位:低位] 端口名
    • input (输入端口):只能从外部读入,模块内部不能赋值
    • output (输出端口):模块内部驱动(赋值),外部可以读取
    • inout(双向端口):既可以输入也可以输出,常用于总线
  1. 多个端口之间用逗号隔开
  2. 能用input 和 output 实现的尽量不要使用 inout
  3. 端口数据类型省略的话默认是 wire类型
  4. [高位:低位] 省略的话默认位宽为 1

4.6.3 模块端口的连接方式

举例:

module mod_a ( input in1, input in2, output out );
    // Module body
endmodule

在模块 top_module 中实例化 模块 mod_a
模块实例化语法:模块名 实例名(信号1,信号2,信号3,…);

  • 通过端口声明的位置连接【不推荐】:实例化模块时,端口会按照模块的声明从左到右进行连接
module  top_module(input a,input b,output out);

    mod_a by_position(a,b,out);
    // 把 a 连接到 mod_a 的 in1,把 b 连接到 mod_a 的 in2,把 out 连接到 mod_a 的 out,
endmodule
  • 通过端口名称连接【推荐】:实例化模块时,端口按照端口名称进行连接
module  top_module(input a,input b,output out);

    mod_a by_name(.out(out),.in1(a),.int2(b));
    // 把 a 连接到 mod_a 的 in1,把 b 连接到 mod_a 的 in2,把 out 连接到 mod_a 的 out,
endmodule
  1. 连接语法:.sub_port(top_wire)
  • .sub_port:子模块内部的端口名
  • top_wire:子模块外部的端口名或导线
  1. 实例化模块时输入端口必须连接,不能悬空
  • 底层模块的 input 端口如果不接顶层信号,会悬空,综合 / 仿真会出现不定态 x,逻辑错乱;
  1. 实例化模块时输出端口可以选择性连接
  • 如果你完全不用这个输出信号 → 可以不连线
    底层模块的 output 端口闲置时,允许悬空,不接顶层 wire,综合器会自动剪掉这部分逻辑,不会报错。
  • 如果你后续电路要用到该输出 → 必须连线
  1. 实例化模块时双向端口必须连接,不能悬空
  • 如果闲置,不能直接空着,需要显式赋值高阻 z,不然会出现总线冲突
  1. 子模块的输出端口,尽量通过内部导线连接顶层模块的输出端口
  2. 顶层模块的输入端口,不需要对输入信号处理的话,可以直连子模块输入端口,不需要内部导线

4.6.4 模块语句分类

  • 并行语句:写在模块顶层区域的语句,即写在 module 和 endmodule 之间的语句,并行执行,用于描述硬件并行电路,能在模块顶层直接写的语句只有并行语句
    • 信号定义语句:wire / reg
    • 连续赋值语句:assign
    • 过程块定义语句:always、initial
    • 模块实例化语句
  • 过程(顺序)语句:只能放在 always/initial 的 begin-end 内部,从上到下顺序执行,模拟电路触发后的赋值过程
    • 过程语句不能在模块顶层裸写,例如:if、case、for等这些过程语句,必须放进过程块里面

4.6.5 连续赋值(assign)

assign 是Verilog里最常用的语句之一。它用来描述组合逻辑,相当于硬件连线。

  • 基本语法:assign 信号 = 表达式;
  • 注意事项:
    • assign左边必须是wire类型,右边可以是任何表达式。
    • 多个assign语句是并行执行的,和书写顺序无关
    • 组合逻辑不能有反馈环路,除非你加了寄存器
    • 右侧表达式的值发生变化,左侧的信号立即变
    • 连续赋值语句不能出现在过程块中。

4.6.6 条件分支语句

if(条件表达式1) begin
    逻辑1;
end else if(条件表达式2) begin
    逻辑2;
....
end else if(条件表达式n) begin
    逻辑n;
end else begin
    兜底逻辑;
end

条件分支语句必须嵌套在过程块里面

4.6.7 case分支语句

case (判断表达式)
    常量1: begin  // 多条语句必须加 begin-end
        语句块1;
    end
    常量2: 语句块2; // 单条语句可省略 begin-end
    // ...
    常量n: 语句块n;
    default : 默认语句块; // 兜底分支,防止生成锁存器
endcase
  1. case分支语句必须嵌套在过程块里面
  2. case 开头,endcase 结尾
  3. 每个分支 常量: 后:
  • 仅 1 行语句:直接写语句,不用 begin/end;
  • 多条语句:必须 begin … end 包裹;
  1. 允许重复 / 重叠常量分支,从上到下匹配,先匹配到的优先执行;
  2. 组合逻辑中务必写 default,所有情况全覆盖,避免综合推断锁存器;
  3. 匹配完当前分支直接跳出 case语句

4.6.8 casez语句

  • 定义:casez 是 case 的变种分支判断语句,专门支持无关位匹配: 匹配模板里的 z 或 ? 代表无关位,该位置比特不管是 0/1,都视为匹配成功;只有 0/1 要求输入严格相等
  • 语法格式:
always @(*) begin // 组合逻辑必备
    casez (判断信号)
        匹配模板1 : 赋值语句;
        匹配模板2 : 赋值语句;
        ...
        default : 默认赋值;
    endcase
end
  • 信号会按书写顺序依次对比每一行模板,找到第一条匹配成功的分支就立刻执行,不再往下判断。
  • 无关位只作用于模板,输入信号不存在 z或?
  • casez:仅把模板中 z/? 当作无关位;模板里 x 视为严格 x,电路几乎不会出现 x,综合首选
  • 无关位只能写在匹配模板,不能写在 case 括号内的输入信号;
  • casex:模板中 x 和 z 全部当作无关位;仿真中容易出现未知态误匹配,工业界极少使用。

4.6.9 过程 for循环语句

integer i; // 循环变量,只能integer
for(初始赋值; 判断条件; 自增/自减) begin
    多条语句;
end
  1. 循环变量只能用 integer,必须放在过程块第一行或模块顶层
  2. 只能放在过程块内部,模块顶层不能单独写;
  3. 用于仿真时重复执行赋值语句,综合后不会生成多份硬件,只是简化代码;
  4. 循环边界必须是编译期可确定常数,不能是输入信号。
  5. 循环自增统一 i = i + 1,不推荐i++(部分老工具兼容性差);
  6. 循环条件统一用 <,边界清晰;
  7. 寄存器内部批量赋值只用过程 for

示例:

module test;
reg [7:0] a;
always @(*) begin
    integer i;
    a = 8'd0; // 先全部清零

    for(i=0; i<8; i=i+1) begin
        a[i] = 1'b1;
    end
end
endmodule

4.6.10 always 过程块

  • always块,用来描述时序逻辑、组合逻辑、锁存逻辑,内部可以用顺序语句(if/else、case、for、阻塞=、非阻塞<=)。
  • always 块会不断循环执行,需要敏感列表告诉编译器:什么信号变化时,重新执行一次块内代码。
  • 只要是在 always 内部定义的变量、常量,全部要写在块内所有赋值、if、for、case 语句的最前面
  • 语法格式:
always @(敏感信号列表) begin
    // 顺序语句
end
  • 敏感列表就是 always 块「关注」的信号列表。什么信号变了, always 块就要执行一次
  • always块里面需要赋值的变量必须是 reg 类型
    • reg 不一定是寄存器
      • always @(*) 里的 reg → 组合逻辑节点(由纯组合门驱动的信号导线),没有存储;
      • 时钟 always 里的 reg → 触发器,具备存储能力;
  • 敏感信号来源
    • 模块顶层输入信号(最常用)
    • 模块内部自定义信号

组合逻辑 always 块

  • 核心特征:无时钟、无触发器,只生成纯门电路;输入变化输出立刻同步变化,无延迟。
  • 敏感列表硬性规范:固定写 @(*),工具自动抓取块内所有读取的信号,不会漏信号,禁止手动罗列信号,容易漏信号产生锁存器。敏感列表里不包含时钟信号和复位信号
always @(*) begin
    // 组合逻辑代码
end
  • 赋值规则:内部只能使用阻塞赋值 =,禁止 <= 非阻塞赋值。
    • 阻塞赋值 =:立刻赋值,阻塞后面语句,等当前赋值完成才往下走,时序逻辑 always块中禁止使用
  • 综合性:always @(*) 中的 reg 变量 只是语法要求,综合后只是一根连接组合门电路的信号线,无存储、无记忆、无时序延迟
  • 注意事项:
    • 对于组合逻辑的 always 块,若敏感表不全 /if/case 分支不全,reg 变量存在未赋值分支,综合自动生成锁存器
      • 组合逻辑产生锁存器的前提:存在没被赋值的分支
        • 当 case 分支没有覆盖变量全部取值范围时,会生成锁存器,通过加 default,覆盖全部取值范围
        • 当 if 分支没有覆盖变量全部取值范围时,会生成锁存器
    • 组合逻辑 always @(*) 里,输出信号最好先给默认值,并且要写在 if/case/for 前面,防止生成锁存器:
  • 电路延迟:纯组合逻辑,输入改变输出立即更新,0 时钟延迟。
  • 标准模板(带 if 防止锁存器)
module comb_always(
    input a, b, en,
    output reg out
);
always @(*) begin
    out = 1'b0; // 默认赋值,全覆盖,杜绝锁存器
    if(en) begin
        out = a & b;
    end
end
endmodule

时序逻辑 always 块

同步复位 时序逻辑 always 块
  • 核心特征:带 D 触发器,输出延迟;复位仅在时钟上升沿生效,无时钟沿时复位不起作用。
    • 综合性:综合生成普通 D 触发器,复位逻辑由前端组合逻辑实现;
    • 复位仅在时钟上升沿生效,复位电平需维持至少 1 个时钟周期;
    • 触发条件:只有时钟上升沿到来时,才执行一次块内代码;
    • 1 个 always=1 级触发器,延迟 1 拍;多个独立 always 级联可实现 N 拍延迟。
    • if 判断复位必须写在最前面,优先级高于数据通路。
      • 敏感列表变化(只是触发进入代码),if(复位信号) 才是做决策(到底清零还是存数据),同步、异步都离不开这个判断。
  • 敏感列表硬性规范:只写时钟 posedge clk,复位信号不写入敏感列表
// 同步复位,复位靠 if 判断实现
always @(posedge clk) begin
end
  • 赋值规则:内部赋值只能使用非阻塞赋值(<=)
    • 非阻塞赋值 <=:所有右边表达式并行采样,块结束统一更新左边,组合逻辑 always块中禁止使用
  • 模板 1:高电平同步复位 rst
module sync_high_rst(
    input clk,
    input rst,    // rst=1 复位有效
    input din,
    output reg q
);
always @(posedge clk) begin
    if(rst) begin
        q <= 1'b0;
    end else begin
        q <= din;
    end
end
endmodule
  • 模板 2:低电平同步复位 rst_n(工程常用)
module sync_low_rst(
    input clk,
    input rst_n,  // rst_n=0 复位有效
    input din,
    output reg q
);
always @(posedge clk) begin
    if(!rst_n) begin
        q <= 1'b0;
    end else begin
        q <= din;
    end
end
endmodule
  • 级联案例:同步复位,延迟 2 个时钟节拍
    两级独立 always 逐级传递,输出 q2 相对 din 延迟 2 拍
module sync_delay_2clk(
    input clk,
    input rst,
    input din,
    output reg q1, // 延迟1拍
    output reg q2  // 延迟2拍
);
// 第一级FF,延迟1拍
always @(posedge clk) begin
    if(rst)
        q1 <= 1'b0;
    else
        q1 <= din;
end
// 第二级FF,接收q1,再多延迟1拍,合计2拍
always @(posedge clk) begin
    if(rst)
        q2 <= 1'b0;
    else
        q2 <= q1;
end
endmodule
异步复位时序 always 块
  • 核心特征:带专用异步复位 D 触发器;复位不依赖时钟,只要复位有效电平到来,立刻清零输出。
  • 综合带专用异步复位端口的 D 触发器;
  • 复位不依赖时钟,只要复位有效电平出现,立刻清零输出;
  • 触发条件:clk 上升沿 或 复位边沿跳变,任意一个都会执行块内代码;
  • 1 个 always=1 级触发器,延迟 1 拍;多个独立 always 级联可实现 N 拍延迟
  • if 判断复位必须写在最前面,优先级高于数据通路。
    • 敏感列表变化(只是触发进入代码),if(复位信号) 才是做决策(到底清零还是存数据),同步、异步都离不开这个判断。
  • 敏感列表硬性规范:敏感列表必须同时包含时钟上升沿 + 复位跳变沿,or 分隔,缺一不可
  • 赋值规则:内部赋值只能使用非阻塞赋值(<=)
    • 非阻塞赋值 <=:所有右边表达式并行采样,块结束统一更新左边,组合逻辑 always块中禁止使用
// 异步复位
always @(posedge clk or negedge rst_n) begin
end
  • 模板 1:低电平异步复位 rst_n(行业主流)
    敏感列表:posedge clk or negedge rst_n
module async_low_rst(
    input clk,
    input rst_n,  // rst_n下降沿触发复位,低电平有效
    input din,
    output reg q
);
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        q <= 1'b0;
    end else begin
        q <= din;
    end
end
endmodule
  • 模板 2:高电平异步复位 rst
    敏感列表:posedge clk or posedge rst
module async_high_rst(
    input clk,
    input rst,    // rst上升沿触发复位,高电平有效
    input din,
    output reg q
);
always @(posedge clk or posedge rst) begin
    if(rst) begin
        q <= 1'b0;
    end else begin
        q <= din;
    end
end
endmodule
  • 级联案例:异步低电平复位,延迟 2 个时钟节拍
module async_delay_2clk(
    input clk,
    input rst_n,
    input din,
    output reg q1, // 延迟1拍
    output reg q2  // 延迟2拍
);
// 第一级寄存器
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        q1 <= 1'b0;
    else
        q1 <= din;
end
// 第二级寄存器,输入取自q1,总延迟2拍
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        q2 <= 1'b0;
    else
        q2 <= q1;
end
endmodule

混合电路代码规范

  1. 组合逻辑单独剥离(assign / always@(*) + =),时序寄存独立时钟块(always@(posedge clk) + <=),两层完全隔离,不混用、不嵌套

4.6.11 generate for 循环

genvar 循环变量名; // 必须genvar,不能integer
generate
    for(初始化; 条件; 更新) begin : 循环块标签 // 标签不可省略
        assign / 模块例化 / 寄存器定义
    end
endgenerate
  1. genvar:专门给 generate 循环用的变量类型,综合工具识别用来复制硬件;
  2. 必须包裹generate/endgenerate,循环块必须加标签,综合工具区分每一份生成的硬件。
  3. 循环每一轮都会生成一份独立硬件(连线、寄存器、模块);
  4. 可放在模块任意位置 ;
  5. 循环上下限只能是常数,不能随输入变化。
  6. 位宽反转、多位总线、多通道电路一律用 generate for;
  7. 循环自增统一 i = i + 1,不推荐i++(部分老工具兼容性差);
  8. 循环条件统一用 <,边界清晰;

示例:

//多模块例化经典示例(生成 8 个相同子模块)
genvar k;
generate
for(k=0;k<8;k=k+1) begin : mod_loop
    sub_module u_sub( .din(din[k]), .dout(dout[k]) );
end
endgenerate

4.7 仿真验证与Testbench编写

4.7.1 为什么要做仿真验证

  • 硬件流片、焊板子成本极高,写错会直接报废;
  • 仿真:在电脑上模拟电路通电运行,提前查出逻辑错误、时序错误;
  • 验证:对比电路输出和预期结果,确认设计功能完全符合需求。

4.7.2 功能仿真

  • 不考虑芯片延迟、走线延迟,只检查逻辑对错
    • 优点:速度快,快速排查功能 bug
    • 适用:模块刚写完,初步验证逻辑

4.7.3 时序仿真

  • 导入芯片工艺延迟、布线延时,模拟真实硬件时序。
    • 检查建立时间、保持时间、竞争冒险;
    • 适用:设计定稿、即将流片前。

4.7.4 验证分类

  • 人工验证:导出波形,肉眼对照波形判断输出是否正确,适合简单小模块;
  • 自动验证(工业主流):TB 内写判断逻辑,自动比对输出与预期值,出错直接打印报错,大批量测试无需人工看波形

4.7.5 Testbench(测试台)

  • 测试台(Testbench)是一段独立的 Verilog 仿真顶层模块,无对外输入输出端口;它专门用于实例化待测设计模块(DUT),生成激励信号驱动 DUT 输入引脚,同时观测、采集 DUT 输出信号,完成电路功能仿真与验证的完整仿真环境
    • 一个完整测试台包含 4 部分:
      • 信号声明:reg(生成激励)、wire(接收模块输出);
      • DUT 例化:把自己写的设计模块接入测试台,完成端口连线;
      • 激励代码:产生时钟、复位、输入数据,驱动电路工作;
      • 仿真辅助语句:导出波形、打印信息、自动判错、控制仿真启停。
  • 关键特征
    • 仅用于仿真,不能综合生成硬件电路;
    • 自身没有输入、输出引脚;
    • 包含激励,但不等同于激励,激励只是测试台其中一部分代码
  • 测试台命名规范
    • 测试台模块名通用规则:tb_待测模块名
    • 测试台文件名通用规则:tb_待测模块名.v
    • 测试台模块内子模块的实例名:u_模块名 / u_dut(万能,任何子模块都能用)
    • 功能仿真测试台模块名:tb_func_模块名
    • 时序仿真测试台模块名:tb_timing_模块名
    • 随机激励测试台模块名:tb_rand_模块名
  • 全部小写;
  • 单词之间用下划线 _ 分隔
  • 测试台模块必须以 tb_ 开头
  • 测试台模块语句分类
    • 激励语句:主动生成时钟、复位、输入数据等电平信号,驱动待测模块 DUT 运行。
      • 时钟生成语句
      • 复位赋值语句
      • 输入数据 / 控制信号赋值语句
      • 激励复用 task 任务(封装数据发送、复位流程)
    • 非激励语句:只搭建仿真环境、观测输出、控制仿真流程,不会主动产生驱动 DUT 的信号
      • 基础环境定义语句:时间尺度、测试台模块外壳定义、信号声明
      • DUT 例化连线语句:把待测模块接入测试台,完成端口信号连接:
      • 波形与打印观测语句:导出波形、打印信息、报错校验
      • 仿真控制语句:控制仿真启停

只要代码在主动改变 reg 信号高低电平 → 激励语句
只做定义、接线、看波形、打印、启停仿真,不改变输入电平 → 非激励语句

4.7.6 Testbench(测试台)分类

  • 按复杂度与激励实现方式划分
    • 基础固定激励测试台:手动写死时钟、复位、固定几组输入数据;无随机、无自动校验;依靠肉眼看波形判断结果
    • Task 封装复用型测试台:把复位、发数据等重复激励封装成 task,代码复用,批量发送多组固定测试向量。
    • 自动比对测试台:在 task 测试台基础上增加判断逻辑,自动对比输出与预期值,出错打印$error,无需全程人工看波形。
    • 随机激励测试台:使用$random生成随机输入,覆盖边界、极端场景,搭配自动判错、文件读写。
    • UVM 标准化测试台:工业级通用验证框架,分层结构,支持寄存器模型、覆盖率收集、复杂总线 IP 验证。
  • 按仿真用途(是否包含器件延迟)划分
    • 功能仿真测试台:不含芯片工艺、布线延时,只验证逻辑功能对错;仅关注信号逻辑时序,不检查时序违例
    • 时序仿真测试台:导入标准延时文件,包含门延迟、走线延迟;专门检查建立时间、保持时间、竞争冒险;芯片流片前最终验证使用。
  • 按激励数据来源划分
    • 内部激励测试台:时钟、复位、输入数据全部直接写在 TB 代码内
    • 文件驱动测试台:测试向量存放在外部文本文件,TB 通过文件读取函数加载激励;适合上万组大批量测试用例。

4.7.7 测试台(Testbench)编写流程

前置知识

待测设计模块(DUT)
  • 定义:用 verilog 编写的硬件逻辑模块(如:计数器、加法器、分频器等),仅包含功能逻辑与输入输出端口;
  • 核心特征:DUT 自身不会生成任何电信号,没有内置时钟、复位、输入数据;仅预留引脚(输入端口)等待外部给电平;
  • 局限:单独编译 DUT 无法仿真,缺少驱动信号,电路不会运行,必须搭配测试台。
测试台(Testbench)
  • 定义:独立顶层仿真专用module,无任何输入、输出端口;专门实例化 DUT,生成激励信号驱动 DUT 输入引脚,采集观测 DUT 输出信号,完成电路仿真验证的完整虚拟环境;
  • 核心特征:测试台整体包含两大部分代码:激励语句、非激励语句;激励只是测试台内部的一部分代码
DUT 仅有硬件逻辑,无驱动信号,不能独立仿真;测试台是配套完整仿真环境,提供激励与观测能力,是DUT仿真运行的必要载体
两类信号声明
  • reg:寄存器型信号,能主动赋值,用来做激励,DUT 所有 input 输入端口必须使用reg 类型
  • wire:连线型信号,只能被动接收电平,不能主动赋值, DUT 所有 output 输出端口必须使用 wire 类型

赋值规则:测试台内所有激励(reg 信号)统一使用 = 赋值

两大仿真执行块
  • 初始块(initial)
    • 定义:仅仿真上电瞬间从头到尾完整执行 1 次,执行完毕不再重复运行,用于复位、输入数据、仿真控制等
    • 特点:内部支持延时#、时钟等待@(posedge clk)、赋值等
  • 循环块(always)
    • 定义:内部代码无限循环往复执行,仿真全程不间断
    • 特点:仅用来生成连续时钟,禁止放复位、数据输入代码
延时符号
  • 语法:#数字
  • 含义:等待对应时长后,再执行下一行代码;
  • 生效前提:文件开头必须写`timescale,否则延时完全失效,所有信号瞬间跳变,无时间间隔。

第一步:声明时间尺度 + 搭建测试台模块外壳 + 信号声明

  • 时间尺度声明:`timescale 时间单位/时间精度
    • 语句位置:必须放在 tb_待测模块名.v 文件的第一行
    • 作用:统一规定本文件内所有 #数字 的计时基准,没有这条语句,所有 #数字 延时功能全部失效
      • 时间单位:规定了 #数字 的单位
        • 可选单位有:s(秒)、ms(毫秒)、us(微秒)、ns(纳秒)、ps(皮秒)
        • 示例:timescale 1ns/1ns,时间单位是 1ns ,那么 #5 = 5ns,即延迟5纳秒
      • 时间精度:仿真器能够识别的最小时间刻度,所有带小数的延时会自动四舍五入到精度的整数倍,用于控制延时小数精度,精度越小,仿真分辨率越高
        • 硬性规定:时间精度必须小于或等于时间单位

通用模版:`timescale 1ns / 1ns

  • 测试台模块
`timescale 1ns / 1ns
module tb_待测模块名;
//内部写信号、例化、激励代码
endmodule
  • 一个文件内仅写 1 个测试台模块
  • 命名规范:统一 tb_待测模块名,全小写,下划线分隔;
  • 测试台模块无任何输入输出端口
  • 信号声明
    • reg 声明 :reg [位宽-1:0] 信号名;
      • 位宽省略默认 1 位
      • DUT 所有 input端口 必须定义为 reg 类型
      • 可在声明时直接初始化,例如:reg clk = 1’b0;
      • 只能使用等号 (=) 赋值
    • wire 声明:wire [位宽-1:0] 信号名;
      • 位宽省略默认 1 位
      • DUT 所有 output端口 必须定义为 wire 类型
      • 仅用于观测,不可赋值;

第二步:DUT 模块例化

待测模块名 例化名(.待测模块端口1(测试台内部的信号1),待测模块端口2(测试台内部的信号2),...);

作用:将待测试模块接入测试台,仅完成物理接线,不产生任何电信号

第三步:编写全部激励语法(测试台核心功能)

  • 激励是测试台里通过修改 reg 信号高低电平,持续给 DUT 输入时钟、复位、测试数据的全部代码,分为 3 种固定类型,全部写在框架内部。
    • 时钟激励(时序电路必备):持续周期性方波,用 always 实现,属于永久运行激励:
      • 语法:always #固定时长 信号 = ~信号;
        • always 无限循环执行
        • #固定时长:每等待该时间,执行一次翻转
        • ~:按位取反,0 变 1、1 变 0,生成周期性方波;
        • 前置要求:clk 信号声明时初始化reg clk = 1’b0;,上电默认低电平。
        • always 块只能写时钟,不能放复位、数据赋值
        • 必须使用 等号(=)赋值
      • 示例代码:
`timescale 1ns / 1ns
module tb_待测模块名;
    reg clk = 1'b0;
    // always #固定时长 信号 = ~信号;
    always #5 clk = ~clk; //周期10ns时钟
endmodule
  • 复位 + 数据输入激励:initial 块综合语法
    • initial 包裹一段仅执行 1 次的顺序代码,内部可组合 4 种子语法:#数字、赋值=、边沿等待@(posedge clk)、重复周期repeat(N)
      • #数字 :表示暂停仿真指定时长,时间到再执行下一行
      • 时钟上升沿等待(@(posedge clk) ):程序阻塞卡住,直到时钟出现上升沿(0 跳 1),才继续向下执行;保证数据与时钟严格同步,避免时序错位。
      • repeat (数值 N ) @(posedge clk):连续等待 N 个完整时钟上升沿后,再执行后续代码

复位信号一般有两种:异步复位和同步复位。仿真中我推荐用异步复位
复位释放时,最好避开时钟的上升沿,建议用 @(negedge clk) 来同步释放复位

  • 示例代码
initial begin
    //复位激励
    rst_n = 1'b0; //复位有效,初始化电路
    #100;         //保持复位100ns
    rst_n = 1'b1; //释放复位,电路正常工作
    
    //数据输入激励
    @(posedge clk);       //等第一个时钟上升沿再发数据
    din = 4'd3;
    repeat(2) @(posedge clk); //等待2个时钟周期
    din = 4'd8;
end

不写@(posedge clk) 直接赋值数据:数据和时钟不同步,出现建立时间违例

第四步:仿真辅助控制语法

  • 写在测试台模块顶层里面,不会产生输入信号,仅用于观测、启停仿真:
    • $dumpfile(“文件名.vcd”) :指定仿真波形保存的文件名称,后缀固定.vcd;无该语句不会生成波形文件
    • $dumpvars (0, 测试台模块名) :记录测试台内部全部信号线的电平变化;参数 0 代表完整层级记录
    • $finish:仿真运行到本行时,直接关闭仿真器,结束运行,必须加延时,保证电路完整跑完测试流程
    • $display(“文字”, 信号名):执行到代码行才打印,只打印 1 次;
    • monitor("打印格式字符串",信号1,信号2,...):全局监控,写一次全程生效;只要参数列表里任意信号跳变,自动打印,不用循环包裹。同一仿真只能生效一组monitor("打印格式字符串", 信号1, 信号2, ...):全局监控,写一次全程生效;只要参数列表里任意信号跳变,自动打印,不用循环包裹。同一仿真只能生效一组monitor("打印格式字符串",信号1,信号2,...):全局监控,写一次全程生效;只要参数列表里任意信号跳变,自动打印,不用循环包裹。同一仿真只能生效一组monitor,多次调用会覆盖前一次监控配置。建议 写在 initial 块最开头
      • 字符串占位符:%d(十进制)、%h(十六进制)、%b(二进制)、%t(时间)、%m(层次路径)
    • $error(“提示文字”):输出报错信息,用于自动比对输出与预期值。

所有仿真辅助控制语句全部写在 initial 块内

完整成品测试台(框架 + 激励 + 辅助语句一体)

//第一步:时间尺度全局声明
`timescale 1ns / 1ns
//第一步:测试台外壳模块定义
module tb_counter;
//第一步:信号声明 reg激励、wire观测
reg clk = 1'b0;
reg rst_n;
reg [3:0] din;
wire [3:0] cnt;

//第二步:DUT模块例化接线
counter u_dut(
    .clk(clk),
    .rst_n(rst_n),
    .din(din),
    .cnt(cnt)
);

//第三步激励1:时钟always语法
always #5 clk = ~clk;

//第三步激励2 + 第四步仿真辅助语句 initial块
initial begin
    //第四步:波形存储辅助语法
    $dumpfile("wave.vcd");
    $dumpvars(0, tb_counter);
    
    //第三步:复位激励
    rst_n = 1'b0;
    #100;
    rst_n = 1'b1;
    
    //第三步:数据输入激励
    @(posedge clk);
    din = 4'd3;
    repeat(2) @(posedge clk);
    din = 4'd8;
    
    //第四步:仿真终止辅助语法
    #300 $finish;
end
endmodule

4.7.8 简单自检

什么叫自检?就是让Testbench自己判断结果对不对,最简单的自检方式,就是在initial 块里用 if 语句比较

比较时用 !== 而不是 !=。因为 !== 会检查X态和Z态,而 != 会把X当成不关心

4.8 有限状态机(FSM)

4.8.1 有限状态机的概念

  • 有限状态机是同步 / 异步时序逻辑电路的标准化建模结构,用于描述具有顺序步骤、状态跳转、条件输出的控制逻辑; 电路内部存在一组寄存器用来保存当前状态,根据外部输入、现态,通过组合逻辑算出下一状态,同时产生控制输出; 因为整体电路的工作状态总数是有限个,因此称为有限状态机

4.8.2 有限状态机根据状态切换时机分类

同步有限状态机(最常用, 工业主流)

  • 全局统一时钟 clk,所有状态寄存器仅在时钟沿更新状态;
  • 状态跳转、输出变化只发生在时钟上升 / 下降沿
  • 时序可控、无竞争冒险、MCU 控制器、通信协议全部使用同步 FSM
  • 结构:时序逻辑 + 组合逻辑 + 输出逻辑

FSM 是一种行为建模方法,工程中99%场景只用同步FSM

异步有限状态机(无全局时钟,了解即可)

  • 无统一时钟,寄存器由输入信号跳变直接触发
  • 状态随时可变,竞争、毛刺严重,时序分析困难;现代数字设计基本淘汰
  • 判断是同步还是异步状态机,只看一件事:切换状态要不要等统一时钟
    • 要等时钟 → 同步 FSM
    • 没有时钟、输入一变就换状态 → 异步 FSM
  • 复位分同步 / 异步复位,不能用来区分同步 / 异步 FSM

4.8.3 有限状态机根据输出逻辑分类

Moore 型(工程最常用,三段式 FSM 首选)

  • 输出仅由当前状态(现态)决定,与输入无关
  • 输出稳定、无毛刺、时序干净;

Mealy 型

  • 输出由当前状态 (现态)+ 输入 共同决定
  • 输入变化会立刻改变输出,易产生组合毛刺;

4.8.4 状态码

  • 状态本质是一组寄存器,不同编码方式占用资源、时序、抗干扰能力不同

二进制编码 Binary

  • 资源最少,触发器消耗最小
  • 缺点:状态跳转经常多位同时翻转 → 竞争冒险
  • 适用:ASIC 设计、状态数量极多、触发器资源紧张场景

格雷码 Gray Code

  • 相邻状态仅有 1 位不同,降低竞争冒险。
  • 例如:4 状态:00,01,11,10
  • 缺点:译码逻辑复杂,FPGA 很少使用。

独热编码 One-Hot(FPGA 最主流)

  • 每个状态独立占用 1 个触发器,任何时刻仅有 1 位为 1
  • 例如:4 状态:0001、0010、0100、1000
  • 位数 = 状态总数
  • 状态判断极其简单,不需要多路比较
  • 组合译码路径短,时序压力小,更容易跑到高频
  • 消耗的触发器比较多

FPGA 内部触发器资源非常充裕,工业项目控制器默认 One-Hot
FPGA项目一般用独热码,因为LUT+FF的结构很适合。ASIC项目如果状态数超过16,我会考虑二进制编码。格雷码——跨时钟域同步时必用,其他场景看心情
状态编码选型要权衡——没有银弹,看项目需求

独冷编码 One-Cold

  • 只有 1bit 为 0,其余全 1,极少使用

4.8.5 状态定义语法

方式 1:localparam【✅工程首选,推荐】

//独热编码
localparam IDLE    = 4'b0001;
localparam S_START = 4'b0010;
localparam S_RUN   = 4'b0100;
localparam S_DONE  = 4'b1000;
  • 作用域仅限当前 module,不能外部传递参数
  • 不可在实例化时改写
  • 推荐用于状态常量定义

方式 2:parameter

parameter IDLE    = 4'b0001;
parameter S_START = 4'b0010;
  • 特性:模块实例化可以重写;不建议用来定义状态,容易被意外覆盖。

方式 3:`define 宏定义(❌不推荐)

`define IDLE    4'b0001
`define S_START 4'b0010
  • 缺陷:全局宏,极易命名冲突,作用域污染,尽量杜绝。

状态名工程命名规范

  • 统一前缀 ST_ / S_ 区分状态常量;
  • 全大写(大小写有别),和普通信号区分;
  • 语义化,一眼看懂状态功能:
    • 空闲:IDLE / ST_IDLE
    • 启动:S_START / ST_START
    • 运行:S_RUN / ST_WORK
    • 等待计数:S_COUNT / ST_DELAY
    • 结束完成:S_DONE / ST_FINISH
    • 错误状态:S_ERR / ST_ERROR

4.8.6 同步有限状态机的三段式结构

  • 标准三段式(FPGA 最常用):
    • 第一段:时序逻辑:描述状态寄存器更新(现态→次态)
      • 作用:时钟沿把次态载入现态
      • 赋值符号:时序逻辑内部只能使用非阻塞赋值 <=,绝对不能用=;
      • 逻辑功能:本段只做状态存储,不写任何跳转判断、不写输出赋值;
      • 复位分支放在 if 最顶层,优先级最高。
      • always 块内被赋值的变量不能是 wire 类型,必须声明为 reg 类型
      • 示例一:异步复位(FPGA 最常用)
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        curr_state <= IDLE; //复位跳回空闲态 
        end else begin
        curr_state <= next_state;
        end
    end
- 示例二:同步复位(高速设计常用)
always @(posedge clk) begin
    if(!rst_n) begin
        curr_state <= IDLE;
        end else begin
        curr_state <= next_state;
        end
    end

这一段的唯一任务是在时钟沿将次态值赋给现态,并处理异步复位

  • 第二段:组合逻辑:描述次态转移条件(次态跳转逻辑)
    • always @(*):自动敏感列表,会捕获块内所有输入信号,不要手动罗列信号 ;
    • 逻辑功能:根据当前状态 和 输入,计算下一周期要进入的状态(次态)
    • 开头默认赋值 next_state = state
    • 组合块内部只能用阻塞赋值 = ,禁止<=;
    • default分支必不可少
always @(*) begin
    next_state = curr_state; //默认:保持当前状态【防Latch核心代码】
    case(curr_state)
        IDLE: begin
            if(start)
                next_state = S_START;
        end
        S_START: begin
            next_state = S_RUN;
        end
        S_RUN: begin
            if(finish)
                next_state = S_DONE;
        end
        S_DONE: begin
            next_state = IDLE;
        end
        default: next_state = IDLE; //捕获非法状态,防止卡死
    endcase
end

这一段根据当前状态(现态)和输入,决定下一状态的值,是纯组合逻辑。为避免生成锁存器(latch),务必保证 next_state(次态) 在所有条件下都被赋值

  • 第三段:输出逻辑:描述输出状态
    • more 型(输出仅与现态有关)用组合逻辑
      • 多输出场景下,每个输出必须先赋默认值,避免锁存
      • 输出和现态同步变化,不会滞后 1 拍
      • 组合输出不依赖次态,只读取现态;
      • 组合逻辑 always 块内被赋值的变量不能是 wire 类型,必须声明为 reg 类型
      • 输出优先选用 wire 信号,即不选过程块
      • 第三段只看现态,组合输出毛刺极少,工程上不需要额外加寄存器
    • mealy 型(输出与现态和输入有关)必须用时序逻辑(寄存输出,消除组合逻辑毛刺,缺点是输出会延迟 1 拍
  1. 工程准则:能使用 Moore 型绝不使用 Mealy 型;必须使用 Mealy 型时,输出逻辑必须使用时序逻辑或者组合时序分离方案,消除毛刺,输出时序逻辑必须和状态机主时钟、复位完全同源
  2. Mealy 型采用组合时序分离方案
  • 输出逻辑使用组合逻辑,但是不直接对模块外输出,必须经过一级D触发器再对外输出,D寄存器必须和状态机主时钟、复位完全同源;不经过D触发器对外输出可能会产生毛刺,有毛刺的信号,不能直接引出做外部控制信号,所以规范要求:Mealy 必须在输出口再加一级寄存器打拍
  • 第三段组合逻辑的输出信号 → 接到新增寄存器D端 → 寄存器Q作为整个状态机最终对外输出
  1. Mealy 型输出逻辑采用时序逻辑输出时,使用现态作为输出会导致输出时序延迟一拍,使用次态作为输出时序不会延迟一拍,根据设计需求选择,即 输出可用 next_state(次态) 提前产生与状态变化同步的输出

4.8.7 FSM编写流程

  1. 明确系统需求,提取输入信号、输出信号
  2. 枚举全部工作状态,定义状态清单
  3. 绘制状态转移图 STD(State Transition Diagram)(不可省略),理清各状态的跳转条件【状态转移图比代码更容易发现逻辑漏洞】
  4. 列出状态转移真值表
  5. 选定 Moore/Mealy、状态编码方案,用 localparam 定义,FPGA推荐独热码
  6. 使用三段式 Verilog 编写代码
  • 写出第一段:现态同步更新逻辑,异步复位赋初始状态。
  • 写出第二段:组合逻辑描述次态转移,记得默认赋值和 default 分支。
  • 写出第三段:根据需求选择组合输出或时序输出,为每个输出信号在每种状态下赋值,避免锁存器。
  1. 编写 Testbench,覆盖全部跳转分支、边界条件、非法状态
  2. 仿真验证;查看综合报告,检查 Latch、时序警告

4.8.8 状态机高频报错点

  • case /casex/casez 区别
    • case:严格匹配 0/1,不匹配 x/z;✅状态机标准使用 case
    • casez:把 z、? 视为无关值;常用于总线译码,禁止用于状态机
    • casex:把 x、z 全部当成无关;❌严禁在可综合状态机使用,仿真与综合行为不一致

状态机只用普通 case

  • full_case /parallel_case 综合指令(谨慎使用)
    • full_case:告诉综合器所有状态都覆盖,不生成锁存器 ❌工程不建议手动加!规范写法:代码写 default 替代
    • parallel_case:告知 case 分支互斥

现代综合工具不推荐依赖综合指令,靠代码结构保证

  • 赋值符号混用
    • 时序逻辑:always@(posedge clk) 必须使用 非阻塞赋值 <=
    • 组合逻辑:always @(*) 必须使用阻塞赋值 =
  • 信号类型错误
    • always 过程块里面需要被赋值的变量必须是 reg 类型
  • 敏感列表不全
    • 敏感列表使用 always @(*) ,自动感知块内所有用到的信号
    • 同步复位使用 always@(posedge clk) (高速设计常用)
    • 异步复位(复位低电平有效)使用 always @(posedge clk or negedge rst_n) (FPGA 最常用)
    • 异步复位(复位高电平有效)使用 always @(posedge clk or posedge rst) (FPGA 最常用)
  • 组合逻辑缺少默认赋值
    • 组合逻辑 always@(*) 内部所有变量最好先赋默认值
    • 组合逻辑必须给所有分支赋值,否则综合出锁存器
      • 例如:首行必须给 next_state (次态)默认赋值,防止生成 Latch 锁存器
  • 缺少 default 分支
    1. default分支必须写—— 哪怕你觉得所有状态都覆盖了
  • 状态机卡死
    • 独热编码天然存在风险:扰动导致多位同时为 1(非法状态),状态机卡死。 安全 FSM 设计:必须在次态逻辑增加判断,检测非法状态,强制跳回 IDLE 空闲态
  • 出现毛刺
    • Mealy 输出直接引出顶层,不打拍,出现毛刺
      • Mealy型输出逻辑使用时序逻辑,输出值使用现态 ,造成时序延迟一拍,毛刺消除
      • Mealy型输出逻辑使用时序逻辑,输出值使用次态 ,无时序延迟,毛刺消除
      • Mealy型输出逻辑使用组合逻辑,组合逻辑的输出再经过一级D触发器引出顶层 ,造成时序延迟一拍,毛刺消除
  • 状态位宽不足,无法容纳所有状态

4.9 verilog 参数化设计

  • parameter 写在 module 模块名 #(参数列表) (端口列表); 里面的参数列表里面**【推荐使用】**

    • 可以被defparam修改,也可以例化时用#()修改【推荐使用】
  • parameter 未写在 module 模块名 #(参数列表) (端口列表); 里面的参数列表里面,而是写在模块内部

    • Verilog‑1995:defparam 可以修改;Verilog‑2001/Vivado:defparam 修改无效
    • 现代工程不推荐使用

parameter:可以在模块外部传参、可以例化时覆盖修改参数

  • localparam:只能模块内部使用外部不能修改,例化不能覆盖

重要规则:

  1. 想要外部修改参数,parameter必须放到 #() 里面
  2. localparam是模块私有常量,外部无法改动
  3. 现代代码例化优先用 #(.参数(值)),修改参数,舍弃 defparam

案例 1:parameter 放在 #()【推荐写法】

module cnt #(
    parameter MAX = 10   // #()中的parameter,外部可改
)(
    input clk,rst_n,
    output reg [3:0] q
);
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) q <= 0;
    else if(q == MAX) q <= 0;
    else q <= q + 1;
end
endmodule

例化方式 1(推荐)

// 模块名 #(.参数名1(参数值1),....,.参数名n(参数值n)) 例化名(端口连接);
cnt #(.MAX(20)) u_cnt( .clk(clk), .rst_n(rst_n), .q(q) );

例化方式 2(defparam,了解即可,不推荐使用)

cnt u_cnt( .clk(clk), .rst_n(rst_n), .q(q) );
// defparam 例化名.参数名n = 参数值n;
// 参数名尽量全部用大写字母
defparam u_cnt.MAX = 20;

案例 2:parameter 写模块内部 【不推荐】

module cnt(
    input clk,rst_n,
    output reg [3:0] q
);
parameter MAX = 10; //写在模块体内,不在#()
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) q <= 0;
    else if(q == MAX) q <= 0;
    else q <= q + 1;
end
endmodule

例化方式

cnt u_cnt( .clk(clk), .rst_n(rst_n), .q(q) );
defparam u_cnt.MAX = 20;

Verilog‑2001/Vivado下 MAX 依旧等于 10,defparam 修改失败
Verilog‑1995下 MAX 等于 20,defparam 可以修改【不推荐使用】

案例 3:localparam,模块私有常量(最常用)

module led(
    input clk,rst_n,
    output reg led_out
);
localparam CNT_MAX = 25'd24_999_999; //本模块固定,外部不能改
reg [24:0] cnt;

always @(posedge clk or negedge rst_n) begin
    if(!rst_n) cnt <= 0;
    else if(cnt == CNT_MAX) cnt <= 0;
    else cnt <= cnt + 1;
end
endmodule

❌不能用 defparam 或者 #() 修改CNT_MAX

  1. 模块固定阈值、状态机编码,用localparam
  2. 需要外部配置,parameter 写在 #() 中,例化用 #(.参数名(参数值))
  3. 参数名尽量全部用大写字母

4.10 系统函数

  • $bits(信号) :获取向量位宽

想深耕嵌入式?这个专辑值得收藏

MCU、FPGA、工控、传感器一站式学习,实战项目直接抄

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值