verilog笔记

逻辑值

1、0、z(高阻态,无驱动)、x(未知逻辑电平,实际不存在)

实际电路中,只存在:1、0、z,还有亚稳态不稳定状态

参数定义

parameter 可以在顶层文件中对模块中参数修改

localparam 只能模块内部使用

换算二进制后位宽的总长度可有可无,Verilog 会为常量自动匹配合适的位宽。当总位宽大于实际位宽,则自动在左边补 0;总位宽小于实际位宽,则自动截断左边超出的位数

如果直接写参数 100,则表示 32 位的十进制数 100

阻塞赋值与非阻塞赋值

赋值方式特点
阻塞赋值 =顺序赋值
非阻塞赋值 <=同时赋值

时序逻辑(always @(posedge clk))用非阻塞赋值 <=,组合逻辑(always @(*))用阻塞赋值 =,混用会导致仿真与综合结果不一致。

乘和除一般不宜使用,消耗资源大。有乘法器时使用乘法器,除法通过别的方法计算。求余一般用在测试文件中

归约运算符与按位运算符

& 可以作为一元运算符(仅有一个参与运算的量),也可以作为二元运算符(有两个参与运算的量)

一元运算

&m 表示归约与,将 m 中所有 bit 相与,最后结果为 1 bit

二元运算

按位与 m&n,m 中每一位与 n 中每一位相与。在运算时候要保证 m 和 n 的比特数相等,最后的结果和 m(n)的比特数相同

~& ^ ~^ | ~| 与上面同理

逻辑运算符

&& 与 C 语言相同,逻辑与运算符结果只有真或假

||(逻辑或) ==(逻辑相等) !=(逻辑不等)

关系运算符

< > <= >=

移位运算符

<< 左移运算符 >> 右移运算符

位拼接符号

拼接是一堆花括号加逗号组成 {,},不同数据之间用 , 隔开。8bit 的 a、3bit 的 b、5bit 的 c 顺序拼接成 16 位的 d

1
d = {a,b,c}

条件运算符

?: 和 C 语言中三目运算符相同

优先级

归约运算符 > 算术运算符 > 移位运算符 > 关系运算符 > == 和 != > 按位运算符 > && 和 || > 条件运算符

总体:一元运算符 > 二元运算符 > 三元运算符

使用括号添加优先级

if - else 语句

分支语句

case casex casez

case 分支

系统函数

有一些任务和函数,完成一些特殊的功能,称为系统任务和系统函数。这些函数大多数只能在 Testbench 仿真中使用

`timescale 1ns/1ns //时间尺度预编译指令,时间单位/时间精度

时间单位和时间精度由值 1、10 和 100 以及单位 s、ms、us、ns、ps 和 fs 组成。

  • 时间单位:定义仿真过程所有与时间相关量的单位。仿真中使用 #数字 表示延时相应时间单位的时间,例 #10 表示延时 10 个单位的时间,即 10ns。
  • 时间精度:决定时间相关量的精度及仿真显示的最小刻度。

`timescale 1ns/10ps 精度 0.01,#10.11 表示延时 10110ps。

下面这种写法就是错误的,因为时间单位不能比时间精度小。

1
`timescale 100ps/1ns //错误

主要的函数有如下这些,在支持 Verilog 语法的编辑器中都会显示为高亮关键字

1
2
3
4
5
6
7
8
9
$display //打印信息,自动换行
$write //打印信息
$strobe//打印信息,自动换行,最后执行 放在代码最后的地方执行
$monitor//监测变量 打印的变量任何一个变量修改,都会重新执行该语句
$stop//暂停仿真
$finish//结束仿真
$time//时间函数
$random//随机函数
$readmemb//读文件函数 $readmemb读取二进制文件 $readmemh 读取十六进制文件下面我们单独介绍它们的功能,并在 Modelsim 的 Transcript 界面中打印这些信息。
1
2
$display() //自动换行
$display("%b+%b=%d",a,b,c);//格式 "%b+%b=%d" 为格式控制,未指定时默认十进制
格式符输出形式
%h 或 %H以十六进制的形式输出
%d 或 %D以十进制的形式输出
%o 或 %O以八进制的形式输出
%b 或 %B以二进制的形式输出

initial 只执行一次

文件读取函数 $readmemb

$readmemb:用于读二进制文件函数。

$readmemh:用于读十六进制文件函数。

1
2
$readmemb("<数据文件名>",<存贮器名>);  // 读二进制文件
$readmemh("<数据文件名>",<存贮器名>); // 读十六进制文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
`timescale 1ns/1ns
module tb_test()

integer i;
reg [7:0] a [20:0]; //数据宽度7 表示该存储器包含21个存储单,地址范围从0到20

initial begin
$readmemb("dffd.txt",a);
for(i = 0; i<=20 ;i=i+1) begin
#10;
$write("%s",a[i])
end
end
endmodule

存储器定义 reg [7:0] a [20:0];

1
reg [7:0] a [20:0];
  • reg [7:0] 表示定义 8 位宽的寄存器类型数据,每位索引范围为 7(最高位)到 0(最低位)
  • a [20:0] 表示定义深度为 21 的数组,索引范围为 20 到 0
  • 整体含义:定义了一个包含 21 个存储单元的存储器,每个单元为 8 位寄存器,总存储容量为 21×8 位

实际应用

该定义合法且常见于硬件描述中,可用于建模 RAM、ROM 等存储结构

示例初始化方式:

1
reg [7:0] a [20:0] = '{21{8'h00}}; // 全部初始化为0

注意事项

  • 存储器索引通常按降序定义(如 [20:0] 而非 [0:20]),符合 Verilog 规范
  • 每个存储单元需通过完整下标访问(如 a[15] 表示第 16 个单元的 8 位数据)

该定义可直接用于实际设计文件(如 .v 文件),综合工具会将其映射为实际硬件存储单元

1
2
3
4
5
case (sel)
1'b0: ;
1'b1: ;
default: ;
endcase

双向 IO 使用

1
2
3
4
5
inout io;
reg [7:0]out; 输出的数据

wire oe;//控制使能信号
assign io = oe?out[0]:1'bz;// out[0]取位

右移循环

1
2
3
4
5
6
7
8
reg [7:0]shift_a;

always@(posedge clk) begin
shift_a <={shift_a[0],shift_a[7:1]};
// shift_a[7] shift_a[6] shift_a[5] shift_a[4] shift_a[3] shift_a[2] shift_a[1] shift_a[0]
// shift_a[0] shift_a[7] shift_a[6] shift_a[5] shift_a[4] shift_a[3] shift_a[2] shift_a[1]
//上面的逻辑每一个时钟,就向右移动一位,如上面注释所示
end

左移循环

1
2
3
4
5
6
7
8
reg [7:0]shift_a;

always@(posedge clk) begin
shift_a <={shift_a[6:0],shift_a[7]};
// shift_a[7] shift_a[6] shift_a[5] shift_a[4] shift_a[3] shift_a[2] shift_a[1] shift_a[0]
// shift_a[6] shift_a[5] shift_a[4] shift_a[3] shift_a[2] shift_a[1] shift_a[0] shift_a[7]
//上面的逻辑每一个时钟,就向左移动一位,如上面注释所示
end

位拼接

1
2
3
4
5
6
7
8
wire [3:0]x;
wire [3:0]y;
wire [31:0]n;

assign n = {y,7{x}};//等效 有些编译器无法通过
assign n = {y,x,x,x,x,x,x,x};

assign n = {y,x};//假如 y=a x=b, n= 32'hab

除法器

注意:FPGA 内部一般没有硬件除法器,/ 运算会综合成大量加减法和移位逻辑,资源消耗大、时序差。设计时应尽量避免除法;若除数是 2 的 N 次方,改用右移实现(a/8 即 a>>3)。

时序逻辑

连续赋值

1
2
3
4
5
6
7
8
9
input a;
input b;

output c;

assign c = a+b; //如果使用该方法写,c需要定义为 wire 线网类型

always@(a,b) //等效always@(a or b)
c = a+b; //如果使用该方法写,c需要定义为 reg 类型

只要是使用 always 中输出的结果,都需要定义 reg 类型。

1
2
3
4
5
6
7
8
module add2(a,b,c);
input a;
input b;
output reg [1:0]c;

always@(a or b)
c = a + b;
endmodule

所有敏感信号 always@(*)

1
2
3
4
5
6
7
8
9
10
11
12
13
module
assign d = sel1?a:sel2?b:c;

always@(*) begin
case({sel1,sel2})
2'b00: d = c;
2'b01: d = b;
2'b10: d = a;
2'b11: d = a;
endcase
end

endmodule

前级仿真

功能仿真

设置仿真工具

在 Quartus 中,通过 Assignments > Settings > Simulation 选择仿真工具(如 ModelSim),并确保不勾选 Run gate-level simulation(否则会触发后仿真流程)。

配置测试文件

在 Native Link 选项卡中添加测试文件(如 testbench.v),并指定顶层模块名称需与 RTL 代码中的模块名一致。

执行仿真

通过 Tools > Run RTL Simulation 或 Tools > Run Functional Simulation 启动前仿真,此时仅验证逻辑功能,无时序延迟。

前仿真:基于 RTL 代码,速度快,用于功能验证。

后级仿真

生成网表文件

完成布局布线后,Quartus 会自动生成网表文件(.vo)和时序文件(.sdo),这些文件将用于后仿真。

启用时序仿真

在 Settings > Simulation 中勾选 Run gate-level simulation,并确保已加载正确的器件库(如 altera_ver)。

配置延时文件

在 ModelSim 中,需将生成的 .sdo 时序文件与网表文件一同加载,以反映实际布线延迟。

后仿真:基于网表文件,包含布线延迟,用于时序验证。

生成网表文件(.vo)和时序文件(.sdo)

参数 parameter

模块

1
2
3
4
5
6
module led(clk,rst,out);

parameter cnt_max = 49999;//计数值
....

endmodule

例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module led_top();

reg clk;
reg rst;
wire led_out1,led_out2,led_out3;

//产生了不同的输出频率 #(.cnt_max = 3999)设置模块中的参数
led #(.cnt_max = 3999) LED1(.clk(clk),.rst(rst),.out(led_out1));
led LED2(.clk(clk),.rst(rst),.out(led_out2));
led #(.cnt_max = 69999) LED3(.clk(clk),.rst(rst),.out(led_out3));

defparam LED2.cnt_max = 69999; //修改LED2中的cnt_max的值

endmodule

可以通过 parameter 设置实例模块产生不同频率的输出,一个模块多场景应用,也可以使用端口来设置值

parameter 具体可以查看《Verilog HDL 入门 (4)》;用于模块内部的参数定义,可通过模块例化时的 #() 语法修改值

defparam 具体可以查看《Verilog HDL 入门 (4)》;用于在例化后通过层次化路径强制修改已定义的 parameter 值

冲突与优先级

若同一参数被 #() 和 defparam 同时修改,defparam 的赋值会最终生效(因其在编译后执行)。

但此用法易导致代码可读性差,且部分综合工具可能不支持 defparam。

设计建议

优先使用 #() 语法在例化时传递参数,避免混用 defparam

defparam 通常仅用于仿真调试或遗留代码维护

注意

1. 设计一个功能,分成控制模块与数据处理模块编写,然后通过顶层模块联合在一起

串口

收数据通过采样,在一定时间内通过采样次数去比较是高或低电平,最低采样次数大于 16 次,找中间点数据分析。