0%

[CO Pre]Verilog 入门

这是北京航空航天大学计算机学院 2025 年计算机组成原理预习部分的 Verilog 部分。


IDE

本文使用 ISE 开发与仿真。


Verilog 语法

模块的定义方法

模块(module)是 Verilog HDL 的基本功能单元,它实际上代表了具有一定功能的电路实体。通俗来讲,其代表了电路中被导线连接的各个功能模块(子电路)。

以一个与门为例:
方法一:

1
2
3
4
5
6
7
module AndGate(
input i1,
input i2,
output o
);// 模块名定义、端口定义及IO说明
assign o = i1 & i2; // 模块功能定义
endmodule// 结束模块定义

方法二:

1
2
3
4
5
6
7
module AndGate(i1,i2,o); // 模块名定义及端口定义
input i1;
input i2; // 也可合并为一句: input i1,i2;
output o;
// 上为IO说明
assign o = i1 & i2; // 模块功能定义
endmodule // 结束模块定义

两种方法没有实质上的区别,只是形式上有所不同:方法 1 对方法 2 中的端口定义及 IO 说明进行了合并。
模块以 module 开始,endmodule 结束,中间包括模块名、端口定义、I/O 说明等部分。模块中的语句除了顺序执行的语句块以外都是并行的;输入输出端口若不特别说明类型及位宽,默认为 1 位 wire

常用数据类型

Wire 型

wire 型数据属于线网 nets 型数据,通常用于表示组合逻辑信号,可以将它类比为电路中的导线。它本身并不能存储数据,需要有输入才有输出(这里输入的专业术语叫驱动器),且输出随着输入的改变而即时改变。一般使用 assign 语句对 wire 型数据进行驱动(assign 语句将在下一节中进行讲解)。

wire 型的数据分为标量(1 位)和向量(多位)两种。可以在声明过程中使用范围指示器指明位数,如 wire [31:0] a;。冒号两侧分别代表最高有效位(MSB, Most Significant Bit)和最低有效位(LSB, Least Significant Bit)。在访问时,可以使用形如 a[7:4] 的方式取出 a 的第 7-4 位数据。

声明位宽时,如果写作 wire [0:31] a 也是可以的。此时高位被指定为第 0 位,低位被指定为第 31 位。为其赋值时 a = 32'h1234_5678 1 会实际存储在首位,而 8 会存储在末位,与正常相反。

信号定义好之后,不仅决定了位宽还决定了方向,例如定义为 [4:7]b 信号,四个管脚分别为 4,5,6,7,在使用中只能正向接,不能反向接。因此接 b[4:7] 是合法的,而 b[7:4] 是不合法的;同理接 c[8:11] 是合法的,接 c[11:8] 是不合法的。(“接”指赋值操作)

在 Verilog 里,信号的位宽可以通过 in[a:b] 这样的语法来取一部分:

1
assign out = in[7:4];//取 in 的第 7-4 位

这里的 ab 都必须是常量,不能包含变量。比如写成 assign out = in[m * 4 + 3:m * 4] 就会报错,因为 m 是一个变量。
但是,Verilog-2001/SystemVerilog 提供了一种叫 part-select with variable index 的写法:

1
2
assign out = in[start +: width];//从 start 位开始往高位取 width 位
assign out = in[start -: width];//从 start 位开始往低位取 width 位

这里的 start 则可以是变量,而 width 必须是常量。

Reg 型

reg 型是寄存器数据类型,具有存储功能。它也分为标量和向量,类似 wire 型,可以类比前面的教程。一般在 always 块内使用 reg 型变量(always 块将在本章后面提到),通过赋值语句来改变寄存器中的值。为了确定何时进行赋值,我们经常需要用到各种控制结构,包括 whileforswitch 等,这与 C 语言中的做法十分相似。

需要注意的是,reg 型变量不能使用 assign 赋值。而且,reg并不一定被综合成寄存器,它也可和 always 关键字配合(下一节会讲到),建模组合逻辑。

我们可以通过对 reg 型变量建立数组来对存储器建模,例如 reg [31:0] mem [0:1023];,其中前面的中括号内为位宽,后面的中括号内为存储器数量。这种写法在我们开始搭建CPU后会用到。
我们可以通过引用操作访问存储器型数据元素,类似于位选择操作,例如 mem[2] 就是访问 mem 中的第 3 个元素。

Verilog HDL 中没有多维数组。

数字字面量

Verilog 中的数字字面量可以按二进制(b 或 B)、八进制(o 或 O)、十六进制(h 或 H)、十进制(d 或 D)表示。

数字的完整表达为 <位宽>'<进制><值>,如 10'd100。省略位宽时采用默认位宽(与机器有关,一般为 32 位),省略进制时默认为十进制,值部分可以用下划线分开提高可读性,如 16'b1010_1011_1111_1010

Verilog 中除了普通的数字以外,还有两个特殊的值:xzx 为不定值,当某一二进制位的值不能确定时出现,变量的默认初始值为 xz 为高阻态,代表没有连接到有效输入上。对于位宽大于 1 的数据类型,xz 均可只在部分位上出现。

注意数字的位宽决定了数字的最大值。比如 3'd101 就是一个非法的数字,因为 3 位宽的数字最大值为 7。

Verilog 数字本身并不能添加负号,但写作类似于 -8'd5 这样的形式可以看作是对数字的运算,是合法的。

Integer 型

integer 数据类型一般为 32 位,与 C 语言中的 int 类似,默认为有符号数,在我们的实验中主要用于 for 循环(将在本章后面提到)。

Parameter 型

parameter 类型用于在编译时确认值的常量,通过形如 parameter 标识符 = 表达式; 的语句进行定义,如:parameter width = 8;。在实例化模块时,可通过参数传递改变在被引用模块实例中已定义的参数(模块的实例化将在后面的章节进行介绍)。parameter 虽然看起来可变,但它属于常量,在编译时会有一个确定的值。

parameter 可以用于在模块实例化时指定数据位宽等参数,便于在结构相似、位宽不同的模块之间实现代码复用。

组合逻辑建模常用语法

Assign 语句

assign 语句是连续赋值语句,是组合逻辑的建模利器,其作用是用一个信号来驱动另一个信号。如 assign a = b;,其中 awire 型(也可由位拼接得到,见运算符部分),b 是由数据和运算符组成的表达式。

assign 语句与 C 语言的赋值语句有所不同,这里“驱动”的含义类似于电路的连接,也就是说,a 的值时刻等于 b。这也解释了 assign a = a + 1; 这样的语句为什么是不合法的。由于这样的特性,assign 语句不能在 alwaysinitial 块中使用。

assign 语句经常与三目运算符配合使用建模组合逻辑。一般来说,assign 语句综合出来的电路是右侧表达式化简后所对应的逻辑门组合。

  • reg 类型不能被 assign 赋值。

  • 未被 assign 赋值(驱动)过的 wire 类型数据不能被赋给其他的 wire 类型数据。未被 assign 过的 wire 类型就好比什么都没连接的导线,它和其它导线连接是没有意义的。

  • 1 位的变量,不可以被两次 assign,多位的变量,每一位只能被一次 assign。比如 wire [3:0] output; assign output[1:0] = 2'b01; assign output[3:2] = 2'b10; 这是合法的。而 assign output = 4'b0; assign output[1:0] = 2'b01; 是不合法的,因为 0 位和 1 位被两次赋值。

运算符

这里只介绍和 C 语言有差异的运算符。

  • Verilog 中没有自增和自减运算符。

  • 操作数中有不定值 x 和高阻态 z 时,结果中也可能出现。

  • 逻辑右移 >> 和算术右移 >>>

    • 它们的区别主要在于前者在最高位补 0,而后者在最高位补符号位。
  • 相等比较运算符 =====、不等比较运算符 !=!==

    • ==!= 可能由于不定值 x 和高阻值 z 的出现导致结果为不定值 x,而 ===!== 的结果一定是确定的 0 或 1(xz 也参与比较)。
  • 阻塞赋值 = 和非阻塞赋值 <=

    • 不同于 assign 语句,这两种赋值方式被称为过程赋值,通常出现在 initialalways 块中,reg 型变量赋值。这种赋值类似 C 语言中的赋值,不同于 assign 语句,赋值仅会在一个时刻执行。由于 Verilog 描述硬件的特性,Verilog 程序内会有大量的并行,因而产生了这两种赋值方式。这两种赋值方式的详细区别会在之后的小节内介绍,这里暂时只需记住一点:为了写出正确、可综合的程序,在描述时序逻辑时要使用非阻塞式赋值 <=
  • 位拼接运算符 {}

    • 这个运算符可以将几个信号的某些位拼接起来,例如 {a, b[3:0], w, 3'b101};;可以简化重复的表达式,如 {4{w}} 等价于 {w,w,w,w};还可以嵌套,{b, {3{a, b}}} 等价于 {b, {a, b, a, b, a, b}},也就等价于 {b, a, b, a, b, a, b}
  • 缩减运算符

    • 运算符 &(与)、|(或)、^(异或)等作为单目运算符是对操作数的每一位汇总运算,如对于 reg[31:0] B; 中的 B 来说,&B 代表将 B每一位与起来得到的结果。

时序逻辑建模常用语法

Always 块

always 块有如下两种用法:

  • always 之后紧跟 @(...),其中括号内是敏感条件列表,表示当括号中的条件满足时,将会执行 always 之后紧跟的语句或顺序语句块(和 C 语言中的语句块类似,只是将大括号用 beginend 替换了)。这种用法主要用于建模时序逻辑
    例如:

1
2
3
4
always @(posedge clk)  // 表示在 clk 上升沿触发后面的语句块
begin
// 一些操作
end
  • always 之后紧跟 @ *@(*),则表示对其后紧跟的语句或语句块所有信号的变化敏感。这种用法主要用于与 reg 型数据和阻塞赋值配合,建模组合逻辑。

  • always 紧跟语句,则表示在该语句执行完毕之后立刻再次执行。这种用法主要配合后面提到的时间控制语句使用,来产生一些周期性的信号。

always 的敏感条件列表中,条件使用变量名称表示,例如 always @(a) 表示当变量 a 发生变化时执行之后的语句;若条件前加上 posedge 关键字,如 always @(posedge a),表示当 a 达到上升沿,即从 0 变为 1 时触发条件,下降沿不触发;加上 negedge 则是下降沿触发条件,上升沿不触发。每个条件使用逗号 , 或 or 隔开,只要有其中一个条件被触发,always 之后的语句都会被执行。

敏感条件是变量时,该变量只要变化就会触发执行,没有对高低电平的要求。

多个 always 块中对同一个变量进行赋值会导致无法综合。

Initial 块

initial 块后面紧跟的语句或顺序语句块在硬件仿真开始时就会运行,且仅会运行一次,一般用于对 reg 型变量的取值进行初始化。initial 块通常仅用于仿真,是不可综合的。下面的代码用于给寄存器 a 赋初始值 0:

1
2
3
4
5
reg a;

initial begin
a = 0;
end

wire 型数据不能在 alwaysinitial 块中赋值。
wire 类型本质上模拟硬件电路中的物理导线,它本身不具备存储功能,仅用于传递信号(从驱动源到接收端)。导线的特性是 “即时响应驱动源”—— 驱动源的信号变化会立即通过导线传递,没有时间延迟或状态保持。
always 块描述的是时序逻辑或组合逻辑的 “计算过程”,通常包含条件判断、状态跳转等逻辑,其赋值对象需要具备 “根据逻辑计算结果更新状态” 的能力。wire 作为导线,无法承载这种 “计算后更新” 的语义,因此不能作为 always 块的赋值目标。

语句块
块语句的作用是将多条语句合并成一组,使它们像一条语句那样。在使用上一节提到的各种控制语句或者要使用 always/initial 过程块时,如果要执行多条语句,就可以使用块语句,这就类似于 C 语言中大括号里的语句。块语句有两种:顺序块和并行块。顺序块的关键字是 begin - end,并行块的关键字是 fork - join,关键字位于块语句的起始位置和结束位置,相当于 C 语言中的左大括号和右大括号。块语句也可以嵌套。

  1. 顺序块中的语句是一条接一条按顺序执行的,只有前面的语句执行完成之后才能执行后面的语句,除非是带有内嵌延迟控制的非阻塞赋值语句。
  2. 如果语句包括延迟,那么延迟总是相对于前面那条语句执行完成的仿真时间的。

If 语句

Verilog 中 if 语句的语法和 C 语言基本相同,也有 else ifelse 这样的用法。但是,if 语句只能出现在顺序块中,其后的分支也只能是语句或顺序块。举例如下(下面的例子也使用了 always 建模组合逻辑):

1
2
3
4
5
6
7
8
always @ * begin
if (a > b) begin
out = a;
end
else begin
out = b;
end
end

Case 语句

Verilog 中的 case 语句与 C 语言的写法略有区别,详见下方的示例。case 语句同样只能出现在顺序块中,其中的分支也只能是语句或顺序块。与 C 语言不同,case 语句在分支执行结束后不会落入下一个分支,而会自动退出。举例如下:

1
2
3
4
5
6
7
8
9
10
11
always @(posedge clk) begin
case(data)
0: out <= 4;
1: out <= 5;
2: out <= 2;
3: begin
out <= 1;
end
default: ;
endcase
end

Verilog 中的 case 语句默认做的是全等比较,即所有位都相等(包括 xz)。上例中 data === 0out 才会赋值为 4。

For 语句

for 语句和 C 语言中的类似。

循环变量

integer 类型和 reg 类型的变量均可以作为循环变量,但 reg 型需要注意位宽的设置以免造成死循环,譬如:

以下代码会造成 Isim 崩溃。

1
2
3
4
5
6
7
reg [1:0] tmp;
initial begin
for (tmp = 2'h0; tmp <= 2'h3; tmp = tmp + 1) begin
$display("tmp = %d", tmp);
end
$display("Finished.")
end

这是因为 tmp 位宽为 2,最大只能到 2’h3,当 tmp 等于 2’h3 时,下一轮循环 tmp 溢出,回到 2’h0,如此往复导致死循环。

While 语句

while 语句和 C 语言中的类似。

在 Verilog 中所有的循环语句只能在 always 或 initial 块中使用。

模块实例化

对于一个已经存在的模块 Sample,以及其定义好的接口 input a, input b, output c,我们可以通过以下方法进行实例化:

1
2
3
4
5
wire x;
wire y;
wire z;
Sample sample_instance1 (x, y, z);//这里,x, y, z 会自动匹配 Sample 模块定义时的形参顺序。
Sample sample_instance2 (.b(x), .a(y), .c(z));//这里,.b, .a, .c 是为形参指定了匹配的实参。

非阻塞赋值和阻塞赋值

考察以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module blocked_and_non_blocked(
input clk,
input a,
output reg b_blocked,
output reg c_blocked,
output reg b_non_blocked,
output reg c_non_blocked
);

// 非阻塞赋值
always @(posedge clk) begin
b_non_blocked <= a;
c_non_blocked <= b_non_blocked;
end
// 阻塞赋值
always @(posedge clk) begin
b_blocked = a;
c_blocked = b_blocked;
end
endmodule

非阻塞赋值

clk 上升沿到来的时候,可以认为仿真器为 <= 右侧的变量做了一次“快照”,即存储了它们的值。然后将“快照”值赋给了 <= 左侧的变量。在上述代码中,b_non_blocked 值变为 a 的值,而 c_non_blocked 值变为原来的 b_non_blocked 值。

处在一个 always 块中的非阻塞赋值是在块结束时同时并发执行的。

阻塞赋值

阻塞赋值是顺序执行的。在 begin - end 顺序块中,前一句阻塞赋值完成后,后一句阻塞赋值才会开始。在上述代码中,上升沿到来时,b_blocked 值变为 a 的值,然后 c_blocked 值才变为新的 b_blocked 值,即 a 的值。

在时序逻辑中的阻塞赋值可能是不可综合的。

有符号数的处理

wire, reg 等类型的数据默认是无符号的。

若要声明该数据是有符号的,需要使用 $signed(),例如 $signed(a)

一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
module comparator(
input clk,
input reset,
input [3:0] a,
input [3:0] b,
output res
);

assign res = a >= b;

endmodule

我们编写一个 Testbench 来测试,令 ab 的初始值都为 1,100 ns 后令 b 为 -1。
我们期望看到 res 的值恒为 1,但是实际观测到 100 ns 后 res 的值变为 0。
这正是因为我们没有声明 b 是有符号数。Verilog 默认其为无符号数,当 b = -1 时,其补码为 4’b1111,会被认为是 15。
将比较代码修改为 assign res = $signed(a) > $signed(b);,程序即可达到预期结果。

值得一提的是,假如将比较代码修改为 assign res = a > $signed(b);,得到的结果也达不到预期效果。

在对无符号数和符号数同时操作时,Verilog 会自动地做数据类型匹配,将符号数向无符号数转化。因为在执行 a > $signed(b) 时,a 是无符号数,$signed(b) 是符号数,Verilog 默认向无符号类型转化,得到的结果仍是无符号数的比较结果。

关于符号数和无符号数的原理,您可参考这里。简单地概括,一个表达式,只要其子表达式中有任一表达式是无符号,则该表达式就是无符号的

一些注意事项

  • 对于移位运算符,其右侧的操作数总是被视为无符号数,并且不会对运算结果的符号性产生任何影响。结果的符号由运算符左侧的操作数和表达式的其余部分共同决定。

  • 对于三目运算符,其 ? 前的布尔表达式是自决定的表达式,不会对最外层表达式的符号造成影响。

  • 算术右移在左操作数无符号时高位仍然补 0,与逻辑右移效果相同。

  • 未指定位宽和进制的 0 的有无符号性是根据上下文决定的。而指定了位宽和进制的 0(比如 4’b0000)的符号是确定的。

宏定义

宏定义格式如下:

1
2
3
4
`define WORDSIZE 8
// 省略模块定义
reg[1:`WORDSIZE] data;
// 相当于定义 reg[1:8] data;

定义时,需要以反引号(`)开头。使用时,也需要加上反引号。


Verilog 例题

电梯调度

简介

一栋大楼有一部运行的电梯,你需要根据乘客请求和电梯状态来输出。

电梯的具体信息

  • 在最开始或者每次 reset 后,电梯默认初始楼层为一楼。保证在输入到来前先进行 reset。

  • 初始运行方向:每次 reset 后,电梯默认向上运行。

  • 运行范围:1 至 7 层。

  • 调度规则:

    • 如果当前周期没有乘客请求 (乘客请求详细信息见下文),则电梯将会按照当前的运行方向运动一层,并在下一周期更新楼层;如果位于边界楼层且运行方向越界,则反转运行方向并运动一层。
    • 如果当前周期有乘客请求,且乘客请求楼层与当前电梯所在楼层不相同,则电梯将会向乘客请求楼层方向移动一层,并在下一周期更新楼层,电梯运行方向也调整为向乘客请求楼层运行的方向 (若与原来方向一致则不用调整) 。
    • 如果当前周期有乘客请求,且乘客请求楼层与当前电梯所在楼层相同,则电梯将保持不动,并在下一周期维持楼层,电梯运行方向不改变。

乘客请求

  • 存在乘客请求的条件:

    • 若 from = 0 并且不存在未完成的乘客请求,则视为当前周期没有乘客请求。反之,如果 from != 0 或者+存在未完成的乘客请求++,则视为当前周期有乘客请求。
    • 若 from != 0,则视为当前周期有新到来的乘客请求。该乘客请求将会一直存在直到满足取消条件 ( 即使后续周期 from = 0 ) ,在此期间内视为:存在一个未完成的乘客请求。
    • 若 from = 0 并且存在一个未完成的乘客请求,则该乘客请求继续存在直至满足取消条件。
  • 取消乘客请求的条件:

    • 若当前周期的乘客请求楼层和电梯的当前所在楼层相同,则视为满足该请求的取消条件,在下一个周期开始时取消该请求。
    • 若当前周期有 reset 信号,则在下一周期取消该乘客请求。
  • 其他规则:输入数据保证在上一个乘客请求满足取消条件之前,不会输入下一个乘客请求。 也就是当 from != 0 时,当前一定不存在未完成的乘客请求。

输出要求

当乘客请求楼层和电梯的当前所在楼层相同时,输出 1 ;否则输出 0 。

样例

样例
如图,在 reset 后电梯默认从一楼向上运行。
在电梯运行到三楼时,输入了一楼的乘客请求,于是下个周期电梯向乘客请求楼层 (一楼) 方向运行,回到了二楼。然后再下个周期电梯到了一楼,此时电梯楼层和乘客请求楼层相同,因此输出 out 置为 1 ,同时下个周期电梯保留楼层和运行方向。再下个周期输入为 0,电梯掉头向上运行,同时取消上一个乘客请求。

当电梯继续运行到五楼时,输入了同层五楼的乘客请求,输出 out 立刻置为 1 ,同时下一周期电梯保留原楼层和运行方向,同时取消乘客请求。再下个周期输入为 0,电梯正常向上运行至六楼。

请认真阅读波形图,一切逻辑以波形图所示为准!

分析

本题适合用时序逻辑来解决。按照题意,在没有乘客请求的时候,电梯全自动运行,故首先我们需要解决这个问题。我们应当维护两个变量,它们分别是当前的楼层和电梯运行方向,记作 cur_floordirection。简单起见,由于楼层只有七层,可以用一个 3 位二进制数表示(001 至 111),方向则用 0 和 1 表示上与下,简略代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
initial begin//初始化在一楼,向上
...
end

always@(posedge clk) begin
if (reset == 1) begin//reset逻辑
...
end
else begin
if (direction == 0) begin//上
if (cur_floor == 3'b111) begin//顶楼
direction <= 1;
cur_floor <= cur_floor - 1;
end
else begin//未到顶楼
cur_floor <= cur_floor + 1;
end
end
else begin//下
...//类似逻辑
end
end
end

如果有乘客请求,其实有两种情况需要考虑:

  • from 信号出现的时候,电梯尚未到该楼层。

  • from 信号出现的时候,电梯已经到该楼层。

如果电梯还没到,则需要想办法保存下这个乘客请求的楼层号,然后及时调整电梯运行方向。当某个上升沿确认 cur_floor 等于 from 的楼层(当然这个 from 是一个副本,因为 from 输入可能只持续一个周期就归零了,接下来将把这个副本叫做 request)时,让下一个周期保持运行方向和楼层不变。这是样例中 30ns 时发出 1 楼请求的情况。可以看到,45ns 的上升沿电梯刚好到 1 楼,同时 request 等于 cur_floorout 置为 1,下一个周期(55ns - 65ns)电梯保持在 1 楼。

如果发出乘客请求时,电梯正好处于该楼层,则情况稍微会复杂些。按照样例的解释(一切逻辑以样例为准!),100ns 发出 5 楼的请求,此时时钟信号正处于下降沿,而 out立刻置为 1。很明显 out 的输出采用的组合逻辑的 assign 语句,一检测到 fromrequestcur_floor 相匹配就立刻置为 1(对于第一种情况,当然是检测 request 的信号,第二种则是 from)。再看电梯停留在 5 楼的时间。105ns 时电梯本准备往 6 楼去,而实际上停留在 5 楼,这就说明 105ns - 115ns 是电梯停留的一周期。也就是说,95ns - 105ns 发生了 cur_floorfrom 的判定,即在第二种情况下判定并不是发生在上升沿的,而是立即判定!

我们如果想要电梯停留在某一层一个周期,可以这样写:

1
2
3
4
if (condition) begin
cur_floor <= cur_floor;
direction <= direction;
end

无论怎么说,使用非阻塞赋值就决定了这一逻辑一定会在某个上升沿激活。对于第一种情况,我们可以大胆地让 cur_floor == request 作为条件,根据“乘客请求-其他规则”,request 尚未解决时不会有新的 from 出现。可以预见的是一定会在某个上升沿满足条件,然后下一个上升沿执行上述的等待逻辑。对于第二种情况,则大有不同了,接受 from 信号后将其赋给 request 必定也使用非阻塞赋值,那么 request 的赋值要等到 from 信号来临的下一个上升沿,即二者会出现一定的时间差。这对于 out 的输出,以及电梯的等待,都是不可接受的。具体到样例中,如果接到 from 信号后立刻赋给 request,那么 105ns 时 request 才会被置为 1。如果还用 cur_floor == request 作为条件,5 楼的等待时间将会延长到 115ns - 125ns 周期,这就错了。而且 outfromrequest 挂钩,如果在 request 已经被赋值(105ns),再用条件判断将其归零,也许要等到下一个上升沿(115ns),这样, out 就会从 100ns 一直激活到 115ns。

总的来说,面对第二种情况,等待逻辑的判定条件必须是 cur_floor == from,而且如果满足该条件,就要立刻发出 request 归零的命令,让 request 赶在下一个上升沿(105ns)就归零,相当于“反悔给 request 赋值”(这样做从波形上来看,request 不会被赋值,因为赋值和归零都是在上升沿以外的地方进行的),避免 out 激活时间过长。


Verilog 工程的设计开发调试

编写可综合代码

以下规则不适用于 Testbench。

  • 勿使用 Initial 块、勿为 Reg 型(寄存器)赋初值。

  • 一个寄存器只能在一个 always 块中赋值一次
    以下代码不可综合:

1
2
3
4
5
6
7
8
9
10
reg a;
wire b, c;

always @(posedge clk_1) begin
a <= b;
end

always @(posedge clk_2) begin
a <= c;
end

实际上,Reg 型一般会被综合为 D 触发器,只有一个时钟输入,而上述代码让该触发器处于两个时钟域中。

何谓赋值一次?如果使用 if / else / case 语句进行条件判断,在不同且互斥的情况下对同一个寄存器进行赋值,是完全合法的。而其他情况是不可被综合的。

  • 尽量避免综合后的奇怪故障

    1. 在时序逻辑中,永远使用非阻塞赋值(<=);在组合逻辑中,永远使用阻塞赋值(=);
    2. 每个组合逻辑运算结果仅在一个 always @(*) 中修改;
    3. always @(*) 中,为每个运算结果赋初值,避免 latch[1] 的产生。
    4. 使用位运算代替乘除法。

Verilog 代码规范

命名

  1. 信号名采用 snake_casePascalCase 或者 camelCase。全工程采用统一命名方式。

    • snake_case:变量名全小写,单词间以下划线连接。
    • PascalCase:首字母全大写。
    • camelCase:第一个字母小写,后续首字母大写。
  2. 低电平有效信号用 _n 后缀。

  3. 多路选择器标明规格。例如 4 选 1 的 32 位 MUX可记作 MUX4_1_32

  4. 对于状态机,各状态一定要命名,避免在代码中出现不知所云的数字。

组合逻辑的编写

  1. 一个信号只在一个 always 块中赋值。

  2. 组合逻辑用 always @(*) 块或者 assign

  3. 组合逻辑的 always 块只用阻塞赋值。

  4. 确保所有分支都赋值,否则出现锁存器[1:1]

时序逻辑

  1. 时序逻辑用 always @(posedge clock)

  2. 时序逻辑的 always 块只用非阻塞赋值。

  3. 通常情况下,不要用下降沿触发。

  4. 除了 always 敏感列表外,不要用时钟信号。

  5. 使用同步复位而非异步复位[2]

代码风格

  1. 单目运算符与变量间添加空格。

  2. 同一逻辑,但表达式复杂的语句,使用换行进行切割:

1
2
3
4
5
6
7
// GOOD
assign d = (op == 0) ? a + b :
(op == 1) ? a - b :
(op == 2) ? a & b :
a | b;
// BAD
assign d = (op == 0) ? a + b :(op == 1) ? a - b :(op == 2) ? a & b : a | b;
  1. 显式声明数字位宽。


  1. 如果没有初值,编译器会认为需要保持上一次该变量的值不变,这是符合锁存器(Latch)定义的。 ↩︎ ↩︎

  2. 同步复位(Synchronous Reset):复位信号的生效与时钟同步,只有时钟的有效沿到来时才生效。
    异步复位(Asynchronous Reset):复位信号的生效与时钟无关,只要复位信号有效,立即执行复位操作,无需等待时钟沿。 ↩︎

-------------本文结束 感谢您的时间-------------

欢迎关注我的其它发布渠道