Verilog语法整理
一、Verilog数据类型
1.1 wire类型
wire类型用于表示硬件电路单元之间的物理连线。
- 未声明的信号类型默认为wire
1 | 比如下面的这种情况,默认定义为wire型 |
- wire不允许在always块内部定义和赋值,但可以在always块使用非阻塞赋值<=取其数值
- 常用在always块外部用assign连续赋值(硬件相连)
eg:
1 | module d_flip_flop( //以D触发器为例,输出信号有reg,也有wire,以说明其赋值操作的区别 |
1.2 reg类型、integer整数、real实数
verilog中数值表达形式为:(位宽)’(进制)(数值) 比如【4’d1】。
reg类型的变量只能在always或initial中赋值,因此给reg变量赋初值一般在if(!rst_n) begin里面,或者在定义的时候直接赋值,reg [5:0] LED = 6'b000001;reg类型不能在assign语句中赋值,因为那是给wire类型用的。
| b | 二进制 |
|---|---|
| o | 八进制 |
| d | 十进制 |
| h | 十六进制 |
1 | reg d = 4'b0001; |
这里有一个比较重要的问题:给a赋值15的时候为什么尽量不要用
a<=15;来赋值,而是使用a<=5'd15;。可以看下面的示例↓
1 | wire [3:0] a; |
1.3 数组(一维向量、二维向量(类矩阵))
数组问题主要有:
- 一维数组操作(赋值、按位赋值、拼接)
- 二维数组操作(赋值、取值)
1 | module xxx( |
1 | // ========== 二维数组初始化(initial 块) ========== |
1.4 parameter参数、localparam内部参数
parameter用于声明设计参数,是一个常量,不支持小数。当代码多次使用同一个参数时(比如计时器的计数上限值),一个个修改起来较为麻烦,可以通过定义需要修改的变量为 parameter 参数类型,例化模块时直接设置 parameter 即可。
1 | //子模块里面 |
此外还有另外一种用 defparam 进行参数设置的方法,例如例化同一模块使用不同的参数:
1 | timer timer_例化名( //先不例化,等后面用defparam例化 |
localparam 用于在模块内部定义常量,其值在模块实例化时是固定的,无法被修改。用法同parameter 一致。但是与相比 parameter 而言 localparam 更严格,因为它只在定义的模块内部有效,不会影响外部模块。
1 | module xxx( |
1.5 genvar变量
genvar i是Verilog和System Verilog中用于generate语句块的专用变量声明。
生命周期:只在generate内部生效,只在Elaborate(展开)过程起作用。
完整的设计周期包括:Elaborate→Synthesis→Implementation→generate bitstream
Elaborate之后,genvar变量就消失了
eg:
1 | genvar i; |
以上代码Elaborate之后会变成:
1 | IIR u_IIR0(.a(a[0])); |
从此以后,genvar变量就消失了。
那么为什么要用genvar变量呢?
genvar的唯一作用就是:在编译期批量生成可重复的硬件结构,使10行代码可以生成成百上千个真实的并行电路。如果不用genvar:代码行数多不易读,任何端口名改一次,都要同步改很多遍,其中里面如果有一个端口有错误,代码就无法运行。
因此
genvar不是时间循环,而是空间复制
1.6 Verilog 属性(attribute)声明
(* ... *) 是 属性语法
在 Verilog / SystemVerilog 中:
1 | (* attribute_name = value *) |
表示给后面的对象加一个“工具属性”,这个工具属性仅服务于综合、实现、调试,不服务于仿真。
常见的一些属性如下:
1 | (* MARK_DEBUG="true" *) reg state; |
| 属性 | 作用 |
|---|---|
MARK_DEBUG |
保留 + 可被 ILA 探针 |
DONT_TOUCH |
禁止综合器改动 |
MARK_DEBUG="true" 的作用:强制把这个信号保留下来,并允许被 ILA / Debug Core 观察。也就是告诉 Vivado:这个信号是“我想调试的”,防止综合优化把它删掉、合并掉
二、运算符
2.1 算数运算符
加:+ 减:- 乘:* 除:/ 取余:%
取余运算两侧数据必须是整数数据。
2.2 赋值运算符(阻塞赋值与非阻塞赋值)
1 | = : 阻塞赋值 //前面赋值语句会阻塞后面的语句,按照顺序运行 |
2.3 比大小运算符
1 | a < b :a小于b |
2.4 逻辑运算符
1 | &&:逻辑与:a&&b,a和b同时为真时才为真,否则为假 |
2.5 条件运算符
1 | a = (条件) ? b : c; //条件为真:a=b,条件为假:a=c |
2.6 位运算符
1 | ~ : 按位取反 //a=1001 ~a=0110 |
2.7 移位运算符
1 | reg [3:0] a,c; |
2.8 归约运算符(实现奇偶校验)
1 | 归约的操作数只有一个,实现该操作数所有位的运算,得到结果为一位 |
三、语法与语句
3.1 always语句
- 组合逻辑:always@(*) 或 assign
- 时序逻辑:always@(posedge clk or negedge rst_n)
3.2 if语句
1 | if (条件1) begin |
3.3 case语句
1 | case(sel) |
3.4 for语句
1 | integer i,j;需要先声明整型变量i,j |
注意:如果一个语句块有两个 for 循环(比如一个 if 语句 的 begin end内),需要使用不同的循环变量分开(如 i 和 j),但是如果两个for循环若分别在 if 和 else if 中就没问题:
1 | if (条件1) begin |
3.5 typedef、enum、logic语句
- 这三个语句只能用于.sv文件,即SystemVerilog,.v文件用不了
1 | typedef:类型重定义关键字 //作用:用于给一个类型取一个别名。 |
3.6 generate if语句
条件调试逻辑generate if :
在开发过程中,很多调试信号仅在验证阶段使用,最终综合时并不需要。这时,我们可以通过 generate if 语句控制 ILA 的插入,做到**“调试时打开,发布时关闭”**。
注意!! generate if 是在综合前进行判断的静态条件语句。它依赖的通常是 parameter 等常量,综合工具只会根据条件选择其中一个分支进行综合,另一个分支不会生成逻辑电路。
举个例子:如果我写了两套互斥的逻辑 代码1 和 代码2,用 generate if 来选择哪套逻辑启用,最终综合时只会保留符合条件的那一套,不会像运行时 if 那样生成两套电路。因此,generate if 是控制代码结构的工具,而不是条件执行逻辑,它不会带来额外的电路资源浪费。
应用eg:
1 | module Clk_Divider# |
上述代码的核心思想是:仅在 DEBUG_ENABLE 为 1 时实例化 ILA IP,从而避免在正式交付中引入多余逻辑。该方式适用于 RTL 层面灵活插入调试器,并通过参数或宏开关控制启用状态。
四、三段式状态机
4.1 状态机结构简介
状态机Finite State Machine(有限状态机),一般有三种编码方式:
- 一段式状态机
状态定义、状态转移、输出逻辑全部写在一个always块中,代码简单,但不利于维护和综合优化。 - 二段式状态机
状态转移与输出逻辑分开写,有两个always块,适合纯组合输出的场景。 - 三段式状态机(推荐使用)
状态寄存、状态转移、输出逻辑三块分开写,结构清晰,是最常用的写法,也是企业开发中最推荐的规范写法。
4.2 三段式状态机思路
三段式状态机包含以下三部分:
- 第一段:状态寄存(时序逻辑)
负责状态的寄存,用于记录当前状态(current_state)。 - 第二段:状态转移逻辑(组合逻辑)
根据输入和当前状态决定下一状态(next_state)。 - 第三段:输出逻辑(组合逻辑 或 时序逻辑)
根据当前状态和输入生成对应输出。
4.3 三段式状态机 Verilog 示例
功能描述:
设计一个有限三段式状态机,输入 in,当检测到连续两个 1 时输出 out=1,否则输出 0。
- 子模块定义:
fsm_detector.v
1 | module fsm_detector ( |
| 时钟周期 | Current State | Input in |
Next State | Output out |
|---|---|---|---|---|
| 1 | IDLE | 1 | S1 | 0 |
| 2 | S1 | 1 | S2 | 0 |
| 3 | S2 | 0 | IDLE | 1 |
| 4 | IDLE | 0 | IDLE | 0 |
| 5 | IDLE | 1 | S1 | 0 |
| 6 | S1 | 1 | S2 | 0 |
| 7 | S2 | 1 | S2 | 1 |
| 8 | S2 | 0 | IDLE | 1 |

4.4 全都要用三段式状态机吗?
先说结论:并不是
情况1:代码逻辑极其简单
这种情况下可以直接用简单逻辑实现,根本不需要加状态机
情况2:代码逻辑较为复杂(eg:uart的tx与rx逻辑)
这种情况下可以选择多个三段式结合、三段式+两段式、三段式+零碎语句来实现,比如uart.v中的tx使用一个三段式状态机,rx使用一个三段式状态机,其余的零碎的计时器或分频器占用一个状态机或一个always块或直接再写一个.v底层模块,专门用来实现。
情况3:代码逻辑极其复杂(eg:spi的tx与rx逻辑)
这种情况下尽量选择多模块设计,tx与rx分别分开写成两个.v文件,其余模块也分别使用别的.v文件实现,最终使用top作为顶层,依次例化连接,或者也可以直接封装为IP核,接线即可。
五、testbench仿真模块
5.1 语法问题
testbench之前会将之前所有代码看做成一个模块,这个模块里面会层层套很多模块,但是最终形成最大的那个模块会只有n个输入in,和m个输出out,testbench里,我们会将这n个输入都伺候好(一般使用reg来给赋值),并将m个输出都接出来(一般使用wire类型来接),最终通过仿真时序图来观察代码逻辑写的是否正确。
下面是testbench常用的架构:
1 | //timescale 时间单位/精度 |
testbench区别于普通.v和.sv文件,常用tb_xxx.v来命名,其也有一些特殊的语法,常用的有:
| 指令 | 功能说明 |
|---|---|
$display() |
单次打印 |
$monitor() |
自动追踪变量变化打印 |
$time |
获取当前仿真时间(64位int型) |
$finish |
停止仿真 |
$stop |
暂停仿真(可调试) |
$dumpfile/$dumpvars |
波形输出(配合 GTKWave) |
forever |
=while(1) 会一直执行内部语句、注意:一个initial块只能有一个forever |
$random/$random %n/{$random} %n |
产生随机数/产生-n到n的随机数/产生0到n的随机数 |
$readmemh/$readmemb |
读取文件16进制数据/读取文件2进制数据 eg: |
| 在这之后,还有一些常用的进阶仿真技巧语法: |
- 自动打印状态值:
1 | initial begin |
- 文件输出波形:
1 | initial begin |
生成后使用命令打开:
1 | gtkwave waveform.vcd |
- for循环生成输入激励
1 | integer i; |
- 两种生成50%占空比的时钟
1 | 第一种:使用forever语句 |
- 使用
$readmemh读取文件数据(激励文件驱动)
1 | reg [7:0] mem [0:15]; |
5.2 代码示例
1 | //timescale 时间单位/精度 |
5.3 注意事项
| 建议 | 原因 |
|---|---|
永远不要在 Testbench 中用 always @ 写激励 |
会影响模拟流程,推荐全部写在 initial 中 |
分时序写激励,每段之间 # |
模拟时间推进,需要 # 控制节拍 |
使用 $monitor 跟踪变量变化 |
方便调试输出 |
| 建议所有信号初值在仿真前显式赋值(都要尽量赋初值) | 防止仿真中出现 x 状态 |
| 注意事项 |
|---|
| `timescale 接时间单位/精度,#10即表示延时10个时间单位,支持计算形式:#(2*5) |
| 仿真模块一般没有端口列表(input/output),通常输入信号为自定义为reg类型,而输出则定义为wire类型 |
| 模块例化:仿真模块相当于顶层文件,将待仿真模块例化后再进行验证,例化名可自定义 |
六、xdc文件语法
6.1 时钟语法
1 | create_clock -name <名称> -period <周期> [get_ports <端口名>] |
这句是时序分析的基础设置,告诉 Vivado 工具:PL_CLK_P这个端口是一个时钟源,且时钟频率为100MHz(周期10ns)。
只需对正向时钟(如 PL_CLK_P)写一次 create_clock,负向脚自动识别。
进阶篇(未完善):时序约束与时钟分析
- 时钟相关属性
create_generated_clockset_clock_groups -asynchronousset_clock_latency,set_clock_uncertainty
- 输入/输出时序约束
set_input_delay/set_output_delayset_input_jitter,set_input_transition- 对 IO 标准与板级延迟的建模
- 路径约束(路径排除/限定)
set_false_pathset_max_delay/set_min_delayset_multicycle_path
6.2 绑定引脚
1 | set_property PACKAGE_PIN <引脚名> [get_ports <端口名>] |
绑定引脚是为了将 Verilog 中定义的逻辑端口(v语言定义的)绑定到实际芯片封装上的物理管脚,这是使信号真正进入/输出芯片的关键步骤,没绑定=信号进不来/出不去。
| 命令名 | 作用对象 | 使用阶段 | 常见用途 | 示例 |
|---|---|---|---|---|
get_ports |
顶层设计的端口(Verilog 中的 input/output) | 综合前(前端约束) | 引脚绑定、IO 电压、时钟创建等 | get_ports clk |
get_cells |
设计中的逻辑单元(触发器、模块实例) | 综合后(后端约束) | 锁定资源、设置属性 | get_cells u_led_reg |
get_nets |
网络连接(连线) | 综合后 | 分析路径、添加布线或时序约束 | get_nets clk_net |
6.3 IO电平标准定义
1 | set_property IOSTANDARD <标准名> [get_ports <端口名>] |
| 名称 | 电压(V) | 差分 | 用途说明 |
|---|---|---|---|
| LVCMOS18 | 1.8 | 否 | 常用于普通 GPIO,常见于 1.8V 供电系统 |
| LVCMOS33 | 3.3 | 否 | 通用低速 IO,比如按钮、LED |
| LVCMOS25 | 2.5 | 否 | 适用于 2.5V 供电外设 |
| SSTL15 | 1.5 | 否 | DDR 控制器,单端 DDR 信号 |
| SSTL18 | 1.8 | 否 | DDR2 控制器,非差分使用 |
| SSTL15_II | 1.5 | 否 | 类似 SSTL15,带双边驱动 |
| HSTL_I | 1.5 | 否 | 高频信号通信 |
| HSTL_II | 1.5 | 否 | 高速 SDR/DDR 信号 |
| DIFF_SSTL12 | 1.2 | 是 | DDR3/DDR4 差分时钟/命令信号 |
| DIFF_HSTL_I | 1.5 | 是 | 差分高速 IO |
| DIFF_HSTL_II | 1.5 | 是 | DDR 或高性能 ADC 差分信号 |
| LVDS | ~1.2 | 是 | 低压差分信号,适合高速数据传输(如 ADC/DAC) |
| SUBLVDS | ~1.2 | 是 | 亚 LVDS,功耗更低,常用于图像传感器 |
| TMDS_33 | 3.3 | 是 | HDMI/DVI 数字视频信号 |
| MIPI_DPHY_DCI | 1.2 | 是 | MIPI CSI/DSI 差分接口,用于摄像头、屏幕 |
6.4 IO口控制
| 属性名 | 功能 | 常用取值 | 建议用法 |
|---|---|---|---|
| PULLUP / PULLDOWN | 上/下拉电阻 | true / false | 输入悬空或按键使用 |
| SLEW | 信号边沿速率 | FAST / SLOW | 高速信号用 FAST,排线建议 SLOW |
| DRIVE | 驱动能力(电流) | 2~24(mA) | 驱动 LED 或高阻输入 |
| OUTPUT_IMPEDANCE | 输出阻抗匹配 | 50 / 75 等 | 高速信号布线匹配 |
| DIFF_TERM | 差分终端匹配电阻 | TRUE / FALSE | 差分输入时使用 |
PULLUP / PULLDOWN
1
2
3eg:
set_property PULLUP true [get_ports btn]
set_property PULLDOWN true [get_ports rst_n]PULLUP:给 IO 口内部接一个上拉电阻(连接到高电平)**
PULLDOWN:给 IO 口内部接一个下拉电阻(连接到低电平)
一般在按键输入、复位信号会加PULLUP,将该引脚默认为高电平。SLEW
1
2
3eg:
set_property SLEW FAST [get_ports {led[0]}]
set_property SLEW SLOW [get_ports {data[*]}]控制输出信号转换速率,一般是
FAST或SLOW,低速信号用SLOW,高速信号用FAST。DRIVE
1
2eg:
set_property DRIVE 12 [get_ports led[0]]控制 IO 输出时能够提供的电流大小,常见值:
2,4,6, …,24mA,单位是mA。一般驱动较大电流例如LED,选12或16。驱动越大,功耗越高、噪声越大,不是越大越好。某些 IOStandard(如 LVDS)不支持DRIVE,设置会报错。OUTPUT_IMPEDANCE
1
2eg:
set_property OUTPUT_IMPEDANCE 50 [get_ports clk_out]用于设置IO输出的特性阻抗,配合 PCB 匹配阻抗,减少反射,提高信号完整性。
适用于高速差分信号(如 TMDS、LVDS)等,大多数情况下可以忽略,由布线匹配完成。
DIFF_TERM
1
2eg:
set_property DIFF_TERM TRUE [get_ports clk_p]在 FPGA 芯片内部的两个差分引脚之间插入一个 100Ω 匹配电阻,主要用于接收差分时钟、LVDS 信号等。
如果板上已经接了 100Ω 匹配电阻,就不要再设置
DIFF_TERM。
