跳转到内容

可编程逻辑/Verilog RTL 编码规范

来自维基教科书,开放的书籍,开放的世界

Verilog RTL 编码规范

[编辑 | 编辑源代码]

Verilog RTL 是 Verilog 语言的子集,用于描述实际硬件。RTL 代码可以综合成 ASIC 门或 FPGA 单元。因此,它必须符合非常严格的规则。

Verilog RTL 如何与实际硬件结构相关联

[编辑 | 编辑源代码]

当 RTL 设计人员查看 Verilog RTL 代码行时,他们看到的不是代码行,而是诸如门、多路复用器、加法器和触发器之类的结构,以及它们是如何连接在一起的。例如

always @(posedge clk) begin
  if(cond)
    res <= a;
  else
    res <= b;
end

在上面的代码中,RTL 设计人员会立即看到一个触发器(res),其输入由一个多路复用器驱动(因为存在 if 语句)。多路复用器的选择引脚是信号 cond,多路复用器的输入是 a 和 b。

要无缝地在 Verilog RTL 和门结构之间转换需要练习,但这是成为一名优秀 RTL 设计人员的关键技能。

Verilog RTL 编码风格

[编辑 | 编辑源代码]

Always 块内的赋值

[编辑 | 编辑源代码]

如果在 always 块内对信号进行赋值,则应为该 always 块的每个可能路径对信号进行赋值。如果情况并非如此,当 always 块执行时,仿真将愉快地保留信号的先前值。这种行为在实际门中是不可能的,除非使用锁存器来保留值,这通常不是预期效果。因此,请确保在所有分支中对所有信号进行赋值。在早期对信号赋予默认值(使用非阻塞赋值)并稍后覆盖它们是可以的。Verilog 清楚地规定,只有后面的赋值会生效。

以下是不正确代码的示例

always @(b or c or e or f or cond)
  if(cond)
    a <= b + c; // forgot to assign d in that case
  else
    d <= e + f; // forgot to assign a in that case
end

阻塞与非阻塞赋值

[编辑 | 编辑源代码]

虽然不是严格要求,但良好的编码风格是仅对在 always 块之外使用的所有信号使用阻塞赋值,而仅对在单个 always 块内完全评估并且不在任何其他地方使用的临时信号使用非阻塞赋值。

考虑以下代码,我们希望根据条件对 a+1 或 b+1 的总和进行赋值。我们不想推断出两个加法器,而是推断出一个多路复用器和一个加法器。

reg [N-1:0] sum;

always @(posedge clk)
  if(cond)
    sum <= a + 1'b1;
  else
    sum <= b + 1'b1;
end

使用在 always 块之外的赋值语句的临时信号(tmp)可以实现我们的目标

reg [N-1:0] sum;
wire [N-1:0] tmp;

assign tmp = cond ? a : b;

always @(posedge clk)
begin
  sum <= tmp + 1'b1;
end

在 RTL 设计实践中,最好不要在单个 always 块内混合阻塞和非阻塞语句。临时信号可以在 always 块之外或使用阻塞赋值的单独 always 块中计算。应该不存在它们没有被赋值但被使用的路径。请注意,在替代 RTL 代码中,tmp 需要声明为一个 reg,因为它是在过程中(在 'always' 块内)赋值的。

reg [N-1:0] sum;
reg [N-1:0] tmp;

always @*
   tmp = cond ? a : b;

always @(posedge clk)
begin
  sum <= tmp + 1'b1;
end

多个驱动器

[编辑 | 编辑源代码]

您不能从多个 always 块驱动同一个信号,这会导致多个驱动器尝试写入同一个信号,这是不可能的(除非您显式地生成一个三态总线)。因此,信号只能由一个 always 块驱动。以下代码是错误的,因为信号 foo 由两个不同的 always 块驱动

always @(posedge clk) begin
  if(reset)
    foo <= 1'b0;
end

always @(posedge clk) begin
  if(count)
    foo <= a;
end


Initial 语句和复位

[编辑 | 编辑源代码]

Initial 语句在综合过程中会被忽略,它们不会转化成任何逻辑结构。因此,RTL 代码不应包含 initial 语句。

通常,所有芯片都有一些复位信号或序列。使用芯片的复位序列来复位您想要的值,而不是在 initial 块内复位值。

always @(posedge clk) begin
  if(reset) begin
    // Reset all state
    sig1 <= 1'b0;
    sig2 <= 1'b0;
  end else begin
    // Design behavior
  end
end

不完整的敏感列表

[编辑 | 编辑源代码]

组合 always 块的敏感列表必须包含该块中使用的所有输入。如果您忘记了敏感列表中的某些信号,综合将静默地假定它们已包含在内并生成门。但是,Verilog 模拟器将尊重您不完整的敏感列表,并且模拟行为将不正确。这意味着,根据通过模拟您认为有效的行为实际上是错误的(一旦您综合了您的设计)。

这是一个不正确代码的示例:您能发现敏感列表中缺少哪个信号吗?

always @(b or c or e or f)
  if(cond)
    a = b + c;
  else
    a = e + f;
end

答案:cond。

最佳的 RTL 方法是使用新的 Verilog 2001 选项 @*,因为这在编写方面会快得多,并且会自动包含所有输入。

always @*
  if(cond)
    a = b + c;
  else
    a = e + f;
end

为了在综合过程中生成实际的门,所有循环都将被展开。这也意味着迭代可以一劳永逸地确定,它们不能依赖于动态值。循环范围可以是常量、参数或 `define,但不能基于信号的表达式。

编写一个推断出太多逻辑的非常大的循环是一个常见的错误。只需几行 Verilog 代码,就可以意外地生成数千个门甚至更多。

信号命名规范

[编辑 | 编辑源代码]

命名规范有助于理解设计。它们不是 Verilog 作为 RTL 子集的一部分的严格要求,但强烈建议使用它们。

对于流水线设计,习惯上将信号名称的后缀添加它们所属的流水线阶段。例如,在一个典型的 4 阶段 CPU 流水线(F、D、E、W)中,valid_W 是阶段 W 的有效信号。它比 valid_E 延迟一个周期。这在查看波形时很有帮助:要么确保将所有属于同一流水线阶段的信号对齐,要么根据名称中指示的流水线阶段在心理上进行周期偏移。它还有助于确保计算 W 阶段信号的表达式都来自前面的流水线阶段(E)。

常量宽度

[编辑 | 编辑源代码]

当您使用常量时,Verilog 会将常量的宽度扩展到 32 位。在以下示例中,当编译器生成逻辑时,它将首先将常量 1 扩展到 32 位的值 1。然后,它将该值截断为 6 位,以添加到 bar。这通常会生成一个截断警告。

wire [5:0] foo;
wire [5:0] bar;

assign foo = bar + 1;

另一种方法是使常量的大小等于 bar 的大小。这将消除截断警告,但您仍然会收到一个大小警告,因为加法结果是 7 位,而不是 6 位(如 foo 所声明的),因此存储在 foo 中的结果被截断。根据实际设计,这可能是完全可以接受的,但编译器正在提醒设计人员确保如果结果大于其被赋值到的位置,您的设计可以正常工作。

wire [5:0] foo;
wire [5:0] bar;

assign foo = bar + 6'd1;
华夏公益教科书