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 最简开发流程
- 编写Verilog代码(功能描述)
- 语法检查(编译,修复拼写/格式错误)
- 编写Testbench(测试代码,施加输入激励)
- 仿真验证(查看输出是否符合预期)
- 综合(将代码转换为门级电路网表)
- 布局布线(映射到FPGA/ASIC硬件资源)
- 下载验证(将程序烧录到硬件,测试实际效果)
4.1.5 工程代码规范
- 缩进:统一用4个空格(避免Tab,不同编辑器Tab宽度不同);
- 换行:每个语句独立一行,模块/always块前后空一行;
- 空格:运算符两侧加空格(如a + b,而非a+b),逗号后加空格;
4.2 Verilog HDL 的语言要素
4.2.1 标识符
- 标识符是模块、寄存器、端口、连线、示例和begin-end块等变量的名称,由字母、数字、下划线、$ 组成,必须以字母或下划线开头,标识符的字母区分大小写
- 命名建议(工程通用):
- 时钟:clk_频率(如clk_50m、clk_1s);
- 复位:rst_n(n表示低有效,统一风格);
- 数据:方向+功能+位宽(如din_8bit、dout_16bit);
- 使能:功能+en(如rx_en、tx_en)。
- 工程实际标识符:用英文标识符,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 配合声明有符号数(补码),必须同时存在,否则verilog 默认为无符号数(纯二进制数,即无符号位,所有位都是数值位)
省略 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、?、_
- 下划线( _ ) 是为了提高数值的可读性,在数值常量的任意位置(首字符除外)可以随意插入下划线( _ )
- 特殊值 x/z 是硬件描述的核心,用于表示未知 / 高阻态,x 或z在二进制中代表1位 x或z,在十六进制中代表 4位 x或z
- ? 是 case 语句的通配符(简化高阻表示),在 case 语句中 字符 ? 和 高组态(Z或z) 是等价的
- n 位有符号数(补码)可以表示的数值范围为:−2n−1∼2n−1−1 -2^{n-1} \sim 2^{n-1} - 1 −2n−1∼2n−1−1
- n 位无符号数可以表示的数值范围为:0∼2n−10 \sim 2^{n} - 10∼2n−1
- 无符号数是一种二进制数值(无符号位),有符号数(补码)是一种编码
- 有符号正数的原码、反码、补码规则
- 原码:数值直接转二进制,最高位的左侧补符号位 0
- 反码 = 原码
- 补码 = 原码
- 有符号负数的原码、反码、补码规则
- 原码:数值绝对值转二进制,最高位的左侧补符号位 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等系统函数实现,直接赋值仅支持纯字符串
- Verilog 中的字符串是由双引号(" ")括起来的字符序列,必须包含在一行中,不能多行书写。在表达式或赋值语句中作为操作数的字符串被看作ASCII值序列,即一个字符串中的每一个字符对应一个8位ASCII值(即每个字符占 1 字节,1字节= 8 bit),仅用于仿真调试(如打印信息、标注信号含义),不可综合(无对应硬件实体)
字符串仅用于仿真调试,核心作用是提升日志可读性,代码中避免大量使用;打印时优先拼接关键变量(如时间、信号值),减少无意义的纯字符串输出。
4.3 Verilog HDL 对硬件电路建模的核心底层逻辑
Verilog HDL对硬件电路建模的核心底层逻辑——通过物理数据类型(连线/寄存器/存储器)、四值逻辑电平、信号强度三大维度,还原实际硬件的电气特性。
4.3.1 四值逻辑电平:抽象实际硬件的电气状态
- Verilog HDL有四种基本的逻辑数值状态,用数字或字符表达数字电路中传送的逻辑状态和存储信息
- 状态 0:代表 低电平、逻辑 0 或 假,在实际电路中代表硬件引脚或导线处于低电压(例如:0v)
- 状态 1:代表 高电平、逻辑 1 或 真,在实际电路中代表硬件引脚或导线处于高电压(例如:3.3v或5v)
- 状态 x/X:代表不确定或未知的逻辑状态,在实际电路中代表信号未初始化或驱动冲突或电平不确定
- 状态 z/X:代表高阻态,在实际电路中代表引脚悬空或者三态门关闭(无驱动源)
- 软件语言只有 0/1 二值逻辑,而Verilog的 x/z 是对硬件电气特性的关键抽象——比如z直接对应三态门的高阻状态,x能在仿真中快速定位“未初始化信号”“驱动冲突”等硬件问题
- 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:小电容(存储电荷少),典型应用:高频电路的寄生电容建模
- 只有驱动强度(supply/strong/pull/weak/highz)参与“多驱动源冲突判定”
- 驱动强度优先级(解决多源冲突的核心): supply > strong > pull > weak > highz
- 优先级覆盖:高优先级驱动强度完全覆盖低优先级(如:supply0 > strong1,最终信号为0);
- 同优先级冲突:同强度的0和1驱动同一根线,结果为x(未知,对应硬件总线竞争);
- highz无驱动能力:只要有非highz的驱动源,信号值由最强的非highz源决定;仅当所有驱动源都是highz时,信号才为:z
- 电荷强度不参与驱动冲突:large/medium/small 仅影响信号延迟,不改变最终逻辑值。
- 综合工具不识别任何信号强度:信号强度仅用于仿真(还原硬件电气特性)
4.4 常用的数据类型
4.4.1 wire 类型【工程里 99% 只用 wire】
- 定义:wire 是 Verilog 最基础的线网类型,用来表示硬件中两根引脚之间的物理导线,负责传递电平信号,只能连续驱动,不能存储数值。
- 语法声明:wire signed [高位:低位] 变量1,变量2,…,变量n;
- signed:是可选项,省略时默认声明的是无符号变量,有 signed 时声明的是有符号变量
- [高位:低位] :是可选项,用于声明变量的位宽,省略时默认位宽为 1 位,位宽=高位 - 低位 + 1
- 驱动(赋值)方式:
- 连续赋值 assign 驱动(最常用)
- 子模块输出端口直接驱动
- 单个 wire 变量 只能有一个驱动源,多个 assign 同时驱动会出现逻辑冲突、仿真出现 x 未知态;
- 仿真初始阶段,若 wire 变量没有被任何驱动源驱动,默认值为高阻态 z
4.4.2 reg类型
- 定义:reg 属于过程赋值变量,只能在 always、initial 过程块内部赋值,具备电平保持能力;不一定直接对应硬件寄存器,最终综合结果由所在逻辑场景决定。
- 语法声明:reg signed [高位:低位] 变量1,变量2,…,变量n;
- signed:是可选项,省略时默认声明的是无符号变量,有 signed 时声明的是有符号变量
- [高位:低位] :是可选项,用于声明变量的位宽,省略时默认位宽为 1 位,位宽=高位 - 低位 + 1
- 驱动(赋值)方式:
- 阻塞赋值(=):只能写在过程块内部,适用于组合逻辑过程块,组合逻辑必须先给 reg 赋默认值,避免锁存器。
- 非阻塞赋值(<=):只能写在过程块内部,适用于时序逻辑过程块
- 禁止使用 assign 连续赋值直接驱动 reg 变量
- reg 在仿真刚开始、还没有任何赋值操作时,默认值为 x(未知态)
- 单个 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 [高位:低位] 存储器名 [地址上限:地址下限];
- signed:是可选项,省略时默认声明的是无符号变量,有 signed 时声明的是有符号变量
- [高位:低位] :是可选项,用于声明变量的位宽,省略时默认位宽为 1 位,位宽=高位 - 低位 + 1
- [地址上限:地址下限]:表示声明了 地址上限 - 地址下限 + 1 个 位宽为 高位 - 低位 + 1 的寄存器
- 多个存储器分开声明
- 赋值方式:
- 仅支持按单个地址单元读写,不支持存储器整体直接赋值
- 只能在过程块内对指定地址的寄存器赋值;
- 标准 Verilog 中二维 reg 数组不可综合,仅能用于仿真场景,无法映射为硬件存储器;
- 将二维 reg 数组转换为一维reg数组实现二维数组可综合,转换公式:一维总地址 = 当前行号 × 每行总列数 + 当前列号【即:把行列转换为一行】
- 存储器不能直接作为模块端口,需要借助普通 reg/wire 中转完成数据读写
- 单个过程块内,可以对存储器的同一地址多次赋值;多个不同过程块对存储器的同一地址多次赋值则属于多驱动冲突,仿真出现 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 判断溢出。
- 算术左移溢出:最典型特征是正负号发生翻转,可以通过移位前后符号位是否改变快速判定溢出
4.5.3 算术运算符
- *(乘法):
- 操作数:无符号数或有符号数;
- 运算结果:无符号数相乘结果为无符号数,有符号数相乘结果为有符号数;无符号数乘有符号数结果为无符号数
- /(除法):
- 操作数:无符号数或有符号数;
- 运算结果:无符号数相除结果为无符号数,有符号数相除结果为有符号数;无符号数除有符号数结果为无符号数
- %(取模/取余):
- 操作数:无符号数或有符号数;
- 运算结果:无符号数取余结果为无符号数,有符号数取余结果为有符号数;无符号数除余有符号数结果为无符号数
- +(加法):
- 操作数:无符号数或有符号数;
- 运算结果:无符号数相加结果为无符号数,有符号数相加结果为有符号数;无符号数加有符号数结果为无符号数
- -(减法):
- 操作数:无符号数或有符号数;
- 运算结果:无符号数相减结果为无符号数,有符号数相减结果为有符号数;无符号数减有符号数结果为无符号数
- 无符号数 × 2ⁿ ,优先用逻辑左移(<<n)实现(可综合、资源少)
- 无符号数 ÷ 2ⁿ,优先用逻辑右移(>>n)实现(可综合、资源少)
- 非2幂次的乘除仅用于仿真,禁止写入硬件综合代码;
- 加法/乘法必须用足够位宽的变量接收结果,无符号数减法需先判断被减数 ≥ 减数,避免溢出导致结果错误;
- 工程中常用 2 的幂次位宽(8/16/32 位)
- 有符号数以补码表示,补码是一种编码,非二进制数,无符号数以二进制数表示,非编码
4.5.4 关系运算符
核心包含 8 种运算符:>(大于)、<(小于)、>=(大于等于)、<=(小于等于)、(等于)、(!=)不等于、(=)全等于、(!==)不全等于,统一规则如下:
- 操作数:无符号数或有符号数
- 运算结果:满足比较条件结果为 1’b1,不满足为 1’b0,操作数含x/z时结果为 1’bx;
- == :逐位比较,两个操作数所有比特不含x/z,并且所有比特均相等,结果为 1’b1,两个操作数所有比特不含x/z,并且所有比特存在不相等,结果为 1’b0,两个操作数任意比特存在x/z,结果为 1’bx,可以综合为同或门
- != :逐位比较,两个操作数所有比特不含x/z,并且所有比特均相等,结果为 1’b0,两个操作数所有比特不含x/z,并且所有比特存在不相等,结果为 1’b1,两个操作数任意比特存在x/z,结果为 1’bx,可以综合为异或门
- === :逐位比较,两个操作数所有比特(含x/z)均相等,结果为 1’b1,两个操作数所有比特(含x/z)存在不相等,结果为 1’b0,绝对不会产生1’bx,全等仅用于仿真,不可综合
- !== :逐位比较,两个操作数所有比特(含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
- 简单二选一逻辑优先用条件运算符,超过4选1建议用 case 语句(可读性更高,综合器优化效果更好);
- 表达式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(双向端口):既可以输入也可以输出,常用于总线
- 多个端口之间用逗号隔开
- 能用input 和 output 实现的尽量不要使用 inout
- 端口数据类型省略的话默认是 wire类型
- [高位:低位] 省略的话默认位宽为 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
- 连接语法:.sub_port(top_wire)
- .sub_port:子模块内部的端口名
- top_wire:子模块外部的端口名或导线
- 实例化模块时输入端口必须连接,不能悬空
- 底层模块的 input 端口如果不接顶层信号,会悬空,综合 / 仿真会出现不定态 x,逻辑错乱;
- 实例化模块时输出端口可以选择性连接
- 如果你完全不用这个输出信号 → 可以不连线
底层模块的 output 端口闲置时,允许悬空,不接顶层 wire,综合器会自动剪掉这部分逻辑,不会报错。- 如果你后续电路要用到该输出 → 必须连线
- 实例化模块时双向端口必须连接,不能悬空
- 如果闲置,不能直接空着,需要显式赋值高阻 z,不然会出现总线冲突
- 子模块的输出端口,尽量通过内部导线连接顶层模块的输出端口
- 顶层模块的输入端口,不需要对输入信号处理的话,可以直连子模块输入端口,不需要内部导线
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
- case分支语句必须嵌套在过程块里面
- case 开头,endcase 结尾
- 每个分支 常量: 后:
- 仅 1 行语句:直接写语句,不用 begin/end;
- 多条语句:必须 begin … end 包裹;
- 允许重复 / 重叠常量分支,从上到下匹配,先匹配到的优先执行;
- 组合逻辑中务必写 default,所有情况全覆盖,避免综合推断锁存器;
- 匹配完当前分支直接跳出 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
- 循环变量只能用 integer,必须放在过程块第一行或模块顶层
- 只能放在过程块内部,模块顶层不能单独写;
- 用于仿真时重复执行赋值语句,综合后不会生成多份硬件,只是简化代码;
- 循环边界必须是编译期可确定常数,不能是输入信号。
- 循环自增统一 i = i + 1,不推荐i++(部分老工具兼容性差);
- 循环条件统一用 <,边界清晰;
- 寄存器内部批量赋值只用过程 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 → 触发器,具备存储能力;
- reg 不一定是寄存器
- 敏感信号来源
- 模块顶层输入信号(最常用)
- 模块内部自定义信号
组合逻辑 always 块
- 核心特征:无时钟、无触发器,只生成纯门电路;输入变化输出立刻同步变化,无延迟。
- 敏感列表硬性规范:固定写 @(*),工具自动抓取块内所有读取的信号,不会漏信号,禁止手动罗列信号,容易漏信号产生锁存器。敏感列表里不包含时钟信号和复位信号
always @(*) begin
// 组合逻辑代码
end
- 赋值规则:内部只能使用阻塞赋值 =,禁止 <= 非阻塞赋值。
- 阻塞赋值 =:立刻赋值,阻塞后面语句,等当前赋值完成才往下走,时序逻辑 always块中禁止使用
- 综合性:always @(*) 中的 reg 变量 只是语法要求,综合后只是一根连接组合门电路的信号线,无存储、无记忆、无时序延迟
- 注意事项:
- 对于组合逻辑的 always 块,若敏感表不全 /if/case 分支不全,reg 变量存在未赋值分支,综合自动生成锁存器
- 组合逻辑产生锁存器的前提:存在没被赋值的分支
- 当 case 分支没有覆盖变量全部取值范围时,会生成锁存器,通过加 default,覆盖全部取值范围
- 当 if 分支没有覆盖变量全部取值范围时,会生成锁存器
- 组合逻辑产生锁存器的前提:存在没被赋值的分支
- 组合逻辑 always @(*) 里,输出信号最好先给默认值,并且要写在 if/case/for 前面,防止生成锁存器:
- 对于组合逻辑的 always 块,若敏感表不全 /if/case 分支不全,reg 变量存在未赋值分支,综合自动生成锁存器
- 电路延迟:纯组合逻辑,输入改变输出立即更新,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
混合电路代码规范
- 组合逻辑单独剥离(assign / always@(*) + =),时序寄存独立时钟块(always@(posedge clk) + <=),两层完全隔离,不混用、不嵌套
4.6.11 generate for 循环
genvar 循环变量名; // 必须genvar,不能integer
generate
for(初始化; 条件; 更新) begin : 循环块标签 // 标签不可省略
assign / 模块例化 / 寄存器定义
end
endgenerate
- genvar:专门给 generate 循环用的变量类型,综合工具识别用来复制硬件;
- 必须包裹generate/endgenerate,循环块必须加标签,综合工具区分每一份生成的硬件。
- 循环每一轮都会生成一份独立硬件(连线、寄存器、模块);
- 可放在模块任意位置 ;
- 循环上下限只能是常数,不能随输入变化。
- 位宽反转、多位总线、多通道电路一律用 generate for;
- 循环自增统一 i = i + 1,不推荐i++(部分老工具兼容性差);
- 循环条件统一用 <,边界清晰;
示例:
//多模块例化经典示例(生成 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 例化:把自己写的设计模块接入测试台,完成端口连线;
- 激励代码:产生时钟、复位、输入数据,驱动电路工作;
- 仿真辅助语句:导出波形、打印信息、自动判错、控制仿真启停。
- 一个完整测试台包含 4 部分:
- 关键特征
- 仅用于仿真,不能综合生成硬件电路;
- 自身没有输入、输出引脚;
- 包含激励,但不等同于激励,激励只是测试台其中一部分代码
- 测试台命名规范
- 测试台模块名通用规则:tb_待测模块名
- 测试台文件名通用规则:tb_待测模块名.v
- 测试台模块内子模块的实例名:u_模块名 / u_dut(万能,任何子模块都能用)
- 功能仿真测试台模块名:tb_func_模块名
- 时序仿真测试台模块名:tb_timing_模块名
- 随机激励测试台模块名:tb_rand_模块名
- 全部小写;
- 单词之间用下划线 _ 分隔
- 测试台模块必须以 tb_ 开头
- 测试台模块语句分类
- 激励语句:主动生成时钟、复位、输入数据等电平信号,驱动待测模块 DUT 运行。
- 时钟生成语句
- 复位赋值语句
- 输入数据 / 控制信号赋值语句
- 激励复用 task 任务(封装数据发送、复位流程)
- 非激励语句:只搭建仿真环境、观测输出、控制仿真流程,不会主动产生驱动 DUT 的信号
- 基础环境定义语句:时间尺度、测试台模块外壳定义、信号声明
- 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 类型
- 仅用于观测,不可赋值;
- reg 声明 :reg [位宽-1:0] 信号名;
第二步:DUT 模块例化
待测模块名 例化名(.待测模块端口1(测试台内部的信号1),待测模块端口2(测试台内部的信号2),...);
作用:将待测试模块接入测试台,仅完成物理接线,不产生任何电信号
第三步:编写全部激励语法(测试台核心功能)
- 激励是测试台里通过修改 reg 信号高低电平,持续给 DUT 输入时钟、复位、测试数据的全部代码,分为 3 种固定类型,全部写在框架内部。
- 时钟激励(时序电路必备):持续周期性方波,用 always 实现,属于永久运行激励:
- 语法:always #固定时长 信号 = ~信号;
- always 无限循环执行
- #固定时长:每等待该时间,执行一次翻转
- ~:按位取反,0 变 1、1 变 0,生成周期性方波;
- 前置要求:clk 信号声明时初始化reg clk = 1’b0;,上电默认低电平。
- always 块只能写时钟,不能放复位、数据赋值
- 必须使用 等号(=)赋值
- 示例代码:
- 语法:always #固定时长 信号 = ~信号;
- 时钟激励(时序电路必备):持续周期性方波,用 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 个完整时钟上升沿后,再执行后续代码
- initial 包裹一段仅执行 1 次的顺序代码,内部可组合 4 种子语法:#数字、赋值=、边沿等待@(posedge clk)、重复周期repeat(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 拍
- more 型(输出仅与现态有关)用组合逻辑
- 工程准则:能使用 Moore 型绝不使用 Mealy 型;必须使用 Mealy 型时,输出逻辑必须使用时序逻辑或者组合时序分离方案,消除毛刺,输出时序逻辑必须和状态机主时钟、复位完全同源
- Mealy 型采用组合时序分离方案
- 输出逻辑使用组合逻辑,但是不直接对模块外输出,必须经过一级D触发器再对外输出,D寄存器必须和状态机主时钟、复位完全同源;不经过D触发器对外输出可能会产生毛刺,有毛刺的信号,不能直接引出做外部控制信号,所以规范要求:Mealy 必须在输出口再加一级寄存器打拍
- 第三段组合逻辑的输出信号 → 接到新增寄存器D端 → 寄存器Q作为整个状态机最终对外输出
- Mealy 型输出逻辑采用时序逻辑输出时,使用现态作为输出会导致输出时序延迟一拍,使用次态作为输出时序不会延迟一拍,根据设计需求选择,即 输出可用 next_state(次态) 提前产生与状态变化同步的输出
4.8.7 FSM编写流程
- 明确系统需求,提取输入信号、输出信号
- 枚举全部工作状态,定义状态清单
- 绘制状态转移图 STD(State Transition Diagram)(不可省略),理清各状态的跳转条件【状态转移图比代码更容易发现逻辑漏洞】
- 列出状态转移真值表
- 选定 Moore/Mealy、状态编码方案,用 localparam 定义,FPGA推荐独热码
- 使用三段式 Verilog 编写代码
- 写出第一段:现态同步更新逻辑,异步复位赋初始状态。
- 写出第二段:组合逻辑描述次态转移,记得默认赋值和 default 分支。
- 写出第三段:根据需求选择组合输出或时序输出,为每个输出信号在每种状态下赋值,避免锁存器。
- 编写 Testbench,覆盖全部跳转分支、边界条件、非法状态
- 仿真验证;查看综合报告,检查 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 分支
- default分支必须写—— 哪怕你觉得所有状态都覆盖了
- 状态机卡死
- 独热编码天然存在风险:扰动导致多位同时为 1(非法状态),状态机卡死。 安全 FSM 设计:必须在次态逻辑增加判断,检测非法状态,强制跳回 IDLE 空闲态
- 出现毛刺
- Mealy 输出直接引出顶层,不打拍,出现毛刺
- Mealy型输出逻辑使用时序逻辑,输出值使用现态 ,造成时序延迟一拍,毛刺消除
- Mealy型输出逻辑使用时序逻辑,输出值使用次态 ,无时序延迟,毛刺消除
- Mealy型输出逻辑使用组合逻辑,组合逻辑的输出再经过一级D触发器引出顶层 ,造成时序延迟一拍,毛刺消除
- Mealy 输出直接引出顶层,不打拍,出现毛刺
- 状态位宽不足,无法容纳所有状态
4.9 verilog 参数化设计
-
parameter 写在 module 模块名 #(参数列表) (端口列表); 里面的参数列表里面**【推荐使用】**
- 可以被defparam修改,也可以例化时用#()修改【推荐使用】
-
parameter 未写在 module 模块名 #(参数列表) (端口列表); 里面的参数列表里面,而是写在模块内部
- Verilog‑1995:defparam 可以修改;Verilog‑2001/Vivado:defparam 修改无效
- 现代工程不推荐使用
parameter:可以在模块外部传参、可以例化时覆盖修改参数
- localparam:只能模块内部使用,外部不能修改,例化不能覆盖
重要规则:
- 想要外部修改参数,parameter必须放到 #() 里面
- localparam是模块私有常量,外部无法改动
- 现代代码例化优先用 #(.参数(值)),修改参数,舍弃 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
- 模块固定阈值、状态机编码,用localparam
- 需要外部配置,parameter 写在 #() 中,例化用 #(.参数名(参数值))
- 参数名尽量全部用大写字母
4.10 系统函数
- $bits(信号) :获取向量位宽

488

被折叠的 条评论
为什么被折叠?



