一、Verilog数据类型 1.1 wire类型 wire类型用于表示硬件电路单元之间的物理连线。
未声明的信号类型默认为wire
wire不允许在always块内部定义和赋值,但可以在always块使用非阻塞赋值<=取其数值
常用在always块外部用assign连续赋值(硬件相连)
eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 module d_flip_flop( input wire clk, input wire rst_n, input wire D, output reg Q_reg, output reg Q_wire ); reg Q_0= 1'd0 ; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin Q_reg <= 1'd1 ; Q_0 <= 1'd0 ; end else begin Q_reg <= D; Q_0 <= !D; end end assign Q_wire = Q_0; endmodule
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
类型用的。
1 2 3 reg d = 4'b0001 ;real e = 3 .14 ;integer f = 3 e2;
1.3 数组(一维向量、二维向量(类矩阵)) 数组问题主要有:
一维数组操作(赋值、按位赋值、拼接)
二维数组操作(赋值、取值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 module xxx( input wire [7 :0 ] a, output reg [15 :0 ] b, output reg [7 :0 ] c ); reg [7 :0 ] d [3 :0 ]; wire [7 :0 ] d2_full; wire bit_d3_5;wire [7 :0 ] d0_shifted; always @(*) begin b = 16'b1111_1110_1101_1100 ; c = b[15 :8 ]; c[7 :1 ] = a[6 :0 ]; b = {a[7 :0 ] , b[15 :8 ]}; end always @(*) begin d[0 ] = 8'b0000_0001 ;d[1 ] = 8'b0000_0010 ;d[2 ] = 8'b0000_0011 ;d[3 ] = 8'b0000_0100 ; end assign d2_full = d[2 ]; assign bit_d3_5 = d[3 ][5 ]; assign d0_shifted = d[0 ] << 1 ; endmodule
1 2 3 4 5 6 7 initial begin d[0 ] = 8'b0000_0001 ; d[1 ] = 8'b0000_0010 ; d[2 ] = 8'b0000_0011 ; d[3 ] = 8'b0000_0100 ; end
1.4 parameter参数、localparam内部参数 parameter用于声明设计参数,是一个常量,不支持小数 。当代码多次使用同一个参数时(比如计时器的计数上限值),一个个修改起来较为麻烦,可以通过定义需要修改的变量为 parameter 参数类型,例化模块时直接设置 parameter 即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 module timer #( parameter time_count = 26'd24_999_999 , parameter width = 8 )( input wire clk, ... ); timer #( .time_count (26'd24_999_99 ) .width (8 ) ) timer_例化名( .clk (clk), ... );
此外还有另外一种用 defparam 进行参数设置的方法,例如例化同一模块使用不同的参数:
1 2 3 4 5 timer timer_例化名( .clk (clk), ... ) defparam timer_l.time_count = 26'd24_999_999 ;
localparam 用于在模块内部定义常量,其值在模块实例化时是固定的,无法被修改。用法同parameter 一致。但是与相比 parameter 而言 localparam 更严格,因为它只在定义的模块内部有效,不会影响外部模块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 module xxx( input wire clk, ... ); localparam time_count = 26'd24_999_999 ; localparam width = 8 ; reg [width-1 :0 ] counter; endmodule
二、运算符 2.1 算数运算符 加:+ 减:- 乘:* 除:/ 取余:%
取余运算两侧数据必须是整数数据。
2.2 赋值运算符(阻塞赋值与非阻塞赋值) 1 2 3 4 5 = : 阻塞赋值 <= : 非阻塞赋值 注意:所有阻塞赋值与非阻塞赋值左边必须是变量数据类型,可以是reg 、integer 、real 。右边可以是任何有效的表达式或信号 此外还有连续赋值(连线)assign :用于表示组合逻辑,左侧必须是wire 或tri ,右侧值变化,左侧数值立即更新。
2.3 比大小运算符 1 2 3 4 5 6 a < b :a小于b a > b :a大于b a == b :a等于b a != b :a不等于b a <= b :a小于等于b a >= b :a大于等于b
2.4 逻辑运算符 1 2 3 &&:逻辑与:a&&b,a和b同时为真时才为真,否则为假 ||:逻辑或:a||b,a和b同时为假时才为假,否则为真 ! :a为真时,!a为假
2.5 条件运算符 1 2 3 4 5 a = (条件) ? b : c; [eg: a = (b <= c) ? 1'b1 : 1'b0 ; 比较常用的条件运算符使用情况: assign a = (b) ? 4'b1 : 4'b0 ;
2.6 位运算符 1 2 3 4 5 ~ : 按位取反 & : 按位与 | : 按位或 ^ : 按位异或xor 两个输入值不同时,输出1 ,否则输出0 ,即同为0 ,异为1 ^~ : 按位同或xnor 两个输入值相同时,输出1 ,否则输出0 ,即同为1 ,异为0
2.7 移位运算符 1 2 3 4 5 6 reg [3 :0 ] a,c;reg [5 :0 ] b;a = 4'b1001 ; b = a<<2 ; c = a>>2 ; a = a<<2 ;
2.8 归约运算符(实现奇偶校验) 1 2 3 4 5 6 归约的操作数只有一个,实现该操作数所有位的运算,得到结果为一位 & : 归约与 a=1101 b = &a (b = 1 & 1 & 0 & 1 = 0 ) | : 归约或 a=1101 b = |a (b = 1 | 1 | 0 | 1 = 1 ) ^ : 归约异或 a=1101 b = ^a (b = 1 ^ 1 ^ 0 ^ 1 = 1 ) 从高位开始往低位异或 (归约异或可简单实现奇偶校验的逻辑)
三、语法与语句 3.1 always语句
组合逻辑:always@(*) 或 assign
时序逻辑:always@(posedge clk or negedge rst_n)
3.2 if语句 1 2 3 4 5 6 7 8 9 10 11 12 if (条件1 ) begin 执行1 ; end else if (条件2 ) begin 执行2 ; end else if (条件3 ) begin 执行3 ; end else begin 执行保护语句; end
3.3 case语句 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 case (sel) 2'd0 : begin dout = a; end 2'd1 : begin dout = b; end 2'd2 : begin dout = c; end 2'd3 : begin dout = d; end default : begin dout = 0 ; end endcase
3.4 for语句 1 2 3 4 5 6 7 8 9 10 11 12 13 integer i,j;需要先声明整型变量i,j ------------------------------------------普通循环 for (i=0 ; i<2 ; i=i+1 ) begin (循环变量赋初值; 执行条件; 循环变量自增/减) 循环体 end ------------------------------------------嵌套循环 for (j=0 ; j<2 ; j=j+1 ) begin for (i=0 ; i<2 ; i=i+1 ) begin 循环体 end end
注意:如果一个语句块有两个 for 循环(比如一个 if 语句 的 begin end内),需要使用不同的循环变量分开(如 i 和 j),但是如果两个for循环若分别在 if 和 else if 中就没问题:
1 2 3 4 5 6 7 8 9 10 11 12 if (条件1 ) begin integer i; for (i = 0 ; i < 4 ; i = i + 1 ) begin $display ("Block A: %d" , i); end end else if (条件2 ) begin integer i; for (i = 0 ; i < 4 ; i = i + 1 ) begin $display ("Block B: %d" , i); end end
3.5 typedef、enum、logic语句
这三个语句只能用于.sv文件,即SystemVerilog,.v文件用不了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 typedef :类型重定义关键字 eg: typedef enum logic [1 :0 ] { ... } state_t; state_t current_state, next_state; ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— enum :枚举类型 eg: enum logic [1 :0 ] { IDLE = 2'd0 , S1 = 2'd1 , S2 = 2'd2 } ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— logic :新型变量类型 eg: logic [1 :0 ] ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 综合起来: typedef enum logic [1 :0 ] { IDLE = 2'd0 , S1 = 2'd1 , S2 = 2'd2 } state_t; state_t current_state, next_state;
四、三段式状态机 4.1 状态机结构简介 状态机一般有三种编码方式:
一段式状态机 状态定义、状态转移、输出逻辑全部写在一个 always
块中,代码简单,但不利于维护和综合优化。
二段式状态机 状态转移与输出逻辑分开写,有两个 always
块,适合纯组合输出的场景。
三段式状态机 (推荐使用) 状态寄存、状态转移、输出逻辑三块分开 写,结构清晰,是最常用的写法,也是企业开发中最推荐的规范写法 。
4.2 三段式状态机思路 三段式状态机包含以下三部分:
第一段:状态寄存(时序逻辑) 负责状态的寄存,用于记录当前状态(current_state)。
第二段:状态转移逻辑(组合逻辑) 根据输入和当前状态决定下一状态(next_state)。
第三段:输出逻辑(组合逻辑 或 时序逻辑) 根据当前状态和输入生成对应输出。
4.3 三段式状态机 Verilog 示例 功能描述: 设计一个有限三段式状态机,输入 in
,当检测到连续两个 1
时输出 out=1
,否则输出 0
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 module fsm_detector ( input wire clk, input wire rst_n, input wire in, output reg out ); localparam IDLE = 2'd0 ; localparam S1 = 2'd1 ; localparam S2 = 2'd2 ; reg [1 :0 ] current_state, next_state; always @(posedge clk or negedge rst_n) begin if (!rst_n) current_state <= IDLE; else current_state <= next_state; end always @(*) begin case (current_state) IDLE: next_state = (in == 1'b1 ) ? S1 : IDLE; S1: next_state = (in == 1'b1 ) ? S2 : IDLE; S2: next_state = (in == 1'b1 ) ? S2 : IDLE; default : next_state = IDLE; endcase end always @(*) begin case (current_state) S2: out = 1'b1 ; default : out = 1'b0 ; endcase end endmodule
时钟周期
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
五、testbench仿真模块 5.1 语法问题 testbench之前会将之前所有代码看做成一个模块,这个模块里面会层层套很多模块,但是最终形成最大的那个模块会只有n个输入in,和m个输出out,testbench里,我们会将这n个输入都伺候好(一般使用reg来给赋值),并将m个输出都接出来(一般使用wire类型来接),最终通过仿真时序图来观察代码逻辑写的是否正确。
下面是testbench常用的架构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 `timescale 1ns / 1ps module tb_module_name(); 1 . 声明信号(wire /reg ) 2 . 实例化待测模块 3 . 产生时钟4 . 初始化输入信号与开始仿真initial begin $finish ; end endmodule
testbench区别于普通.v和.sv文件,常用tb_xxx.v
来命名,其也有一些特殊的语法,常用的有:
指令
功能说明
$display()
单次打印
$monitor()
自动追踪变量变化打印
$time
获取当前仿真时间(64位int型)
$finish
停止仿真
$stop
暂停仿真(可调试)
$dumpfile
/$dumpvars
波形输出(配合 GTKWave)
forever
=while(1) 会一直执行内部语句
$random
/$random %n
/{$random} %n
产生随机数/产生-n到n的随机数/产生0到n的随机数
$readmemh
/$readmemb
读取文件16进制数据/读取文件2进制数据 eg:$readmemh(“D:/mem.dat”,memory); 读.dat文件并放到reg型memory中
在这之后,还有一些常用的进阶仿真技巧语法:
自动打印状态值:
1 2 3 initial begin $monitor ("time=%0t | clk=%b rst_n=%b in=%b out=%b" , $time , clk, rst_n, in, out); end
文件输出波形:
1 2 3 4 initial begin $dumpfile ("waveform.vcd" ); $dumpvars (0 , tb_module_name); end
生成后使用命令打开:
for循环生成输入激励
1 2 3 4 5 6 7 8 9 10 11 12 13 14 integer i;reg [7 :0 ] input_seq = 8'b11001110 ;initial begin rst_n = 0 ; in = 0 ; #20 rst_n = 1 ; for (i = 7 ; i >= 0 ; i = i - 1 ) begin in = input_seq[i]; #10 ; end $finish ; end
两种生成50%占空比的时钟
1 2 3 4 5 6 7 8 9 10 11 12 13 第一种:使用forever 语句 initial begin begin clk_i=0 ; forever #(clk_period/2) clk=~clk; end end 第二种:使用always 块 initial begin clk=0 ; end always #(clk_period/2) clk=~clk;
使用 $readmemh
读取文件数据(激励文件驱动)
1 2 3 4 reg [7 :0 ] mem [0 :15 ];initial begin $readmemh ("input_data.txt" , mem); end
5.2 代码示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 `timescale 1ns / 1ps module tb_module_name; reg clk; reg rst_n; reg in; wire out; xxx u_xxx ( .clk (clk), .rst_n (rst_n), .in (in), .out (out) ); initial begin clk = 0 ; forever #5 clk = ~clk; end initial begin rst_n = 0 ; in = 0 ; #20 ; rst_n = 1 ; #10 in = 1 ; #10 in = 1 ; #10 in = 0 ; #10 in = 0 ; #10 in = 1 ; #10 in = 1 ; #10 in = 1 ; #10 in = 0 ; #10 ; $finish ; end initial begin $monitor ("Time: %0t | in=%b | out=%b" , $time , in, out); end initial begin $dumpfile ("waveform.vcd" ); $dumpvars (0 , tb_fsm_detector); end endmodule
5.3 注意事项
建议
原因
永远不要在 Testbench 中用 always @
写激励
会影响模拟流程,推荐全部写在 initial
中
分时序写激励,每段之间 #
模拟时间推进,需要 #
控制节拍
使用 $monitor
跟踪变量变化
方便调试输出
建议所有信号初值在仿真前显式赋值(都要尽量赋初值)
防止仿真中出现 x
状态
注意事项
`timescale 接时间单位/精度 ,#10即表示延时10个时间单位,支持计算形式:#(2*5)
仿真模块一般没有端口列表(input/output),通常输入信号为自定义为reg类型,而输出则定义为wire类型
模块例化 :仿真模块相当于顶层文件,将待仿真模块例化后再进行验证,例化名可自定义
六、xdc文件语法 6.1 时钟语法 1 2 3 create_clock -name <名称> -period <周期> [get_ports <端口名>] eg: create_clock -name sys_clk -period 10 [get_ports PL_CLK_P]
这句是时序分析的基础设置 ,告诉 Vivado 工具:PL_CLK_P这个端口是一个时钟源,且时钟频率为100MHz(周期10ns)。
只需对正向时钟(如 PL_CLK_P
)写一次 create_clock
,负向脚自动识别。
进阶篇(未完善):时序约束与时钟分析
时钟相关属性
create_generated_clock
set_clock_groups -asynchronous
set_clock_latency
, set_clock_uncertainty
输入/输出时序约束
set_input_delay
/ set_output_delay
set_input_jitter
, set_input_transition
对 IO 标准与板级延迟的建模
路径约束(路径排除/限定)
set_false_path
set_max_delay
/ set_min_delay
set_multicycle_path
6.2 绑定引脚 1 2 3 set_property PACKAGE_PIN <引脚名> [get_ports <端口名>] eg: set_property PACKAGE_PIN F10 [get_ports {LED[0 ]}]
绑定引脚是为了将 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 2 3 4 5 6 7 8 9 10 11 set_property IOSTANDARD <标准名> [get_ports <端口名>] eg1:单端接口 set_property IOSTANDARD LVCMOS18 [get_ports {LED[0 ]}] eg2:差分接口 set_property IOSTANDARD DIFF_SSTL12 [get_ports clk_p] set_property IOSTANDARD DIFF_SSTL12 [get_ports clk_n] eg3:同时约束多个端口 set_property IOSTANDARD LVCMOS18 [get_ports {LED[*]}]
名称
电压(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 3 eg: set_property PULLUP true [get_ports btn] set_property PULLDOWN true [get_ports rst_n]
PULLUP :给 IO 口内部接一个上拉电阻 (连接到高电平)**PULLDOWN :给 IO 口内部接一个下拉电阻 (连接到低电平) 一般在按键输入、复位信号会加PULLUP,将该引脚默认为高电平。
SLEW
1 2 3 eg: set_property SLEW FAST [get_ports {led[0 ]}] set_property SLEW SLOW [get_ports {data[*]}]
控制输出信号转换速率 ,一般是 FAST
或 SLOW
,低速信号用SLOW,高速信号用FAST。
DRIVE
1 2 eg: set_property DRIVE 12 [get_ports led[0 ]]
控制 IO 输出时能够提供的电流大小,常见值:2
, 4
, 6
, …, 24
mA,单位是mA。一般驱动较大电流例如LED,选12或16。驱动越大,功耗越高、噪声越大,不是越大越好 。某些 IOStandard(如 LVDS)不支持 DRIVE
,设置会报错。
OUTPUT_IMPEDANCE
1 2 eg: set_property OUTPUT_IMPEDANCE 50 [get_ports clk_out]
用于设置IO输出的特性阻抗 ,配合 PCB 匹配阻抗,减少反射,提高信号完整性。
适用于高速差分信号(如 TMDS、LVDS)等,大多数情况下可以忽略,由布线匹配完成。
DIFF_TERM
1 2 eg: set_property DIFF_TERM TRUE [get_ports clk_p]
在 FPGA 芯片内部的两个差分引脚之间插入一个 100Ω 匹配电阻 ,主要用于接收差分时钟、LVDS 信号等。
如果板上已经接了 100Ω 匹配电阻 ,就不要再设置 DIFF_TERM
。