Verilog 编程笔记

前言

这是我在大学修习逻辑与计算机设计基础、计算机组成和计算机体系结构三门硬件课的过程中积累的 Verilog 笔记。

Verilog 是一门主要用于逻辑电路设计的硬件描述语言。语言标准主要有 Verilog-1995Verilog-2001 两个版本,建议在创建工程时选择 Verilog-2001 标准以支持更多实用的语法。

虽然 Verilog 的语法与 C 相似,但是二者是面向各自的目标硬件设计的。Verilog 是一门面向逻辑电路的具体实现而设计的语言(正如 C 在某种程度上是面向汇编等底层实现的),因此写作 Verilog 时不可以将 C 等编程语言的思维方式代入,而是要始终清晰地思考正在编写的代码将能够综合成怎样的逻辑电路实现——如果可以,那么大多能够写出在字面和实现上都优雅的代码;如果不行,那么综合时大概也会报错或消耗大量资源,此时则应该考虑调整思路。

手册

Verilog 的标准文档 IEEE 1364-2001 在网络上可以找到下载,但相比之下,这份标准还是稍显冗长或不够友好。而在标准文档之外,Xilinx ISE 的 XST 综合器也提供了实现文档,并且其中包含了许多 Verilog 语法的实用描述,因此可以作为一本更加友好的手册进行查阅。

XST User Guide for Virtex-6, Spartan-6, and 7 Series Devices

以及这里还有一份简单的非官方 Verilog-2001 手册:

Verilog HDL Quick Reference Guide

指南

在本文草稿的同时,我的某位同学也完成了一份 Verilog 指南。指南十分详尽而实用,因此希望读者先行阅读。

Verilog General Guide

重复(Replication)

在 Verilog 中,你可以使用 {COUNT{singal}} 将指定的信号重复数次后连接。

例如,想要写一个 32 位 0/1 相见的字面量,不需要写出 32'b01010101010101010101010101010101,而是写 {16{2'b01}} 即可简洁而直观地完成。

另一个例子是 16 位到 32 位的符号扩展,也可以用 {16{signal[15]}, signal[15:0]} 来完成。

参数定义:parameter, localparam, `define

Verilog 提供了三种定义“常量”的方式。

parameter 是可以由模块外部改变的参数。除了在模块内部定义的语法,我更加推荐采用 Verilog-2001 中在模块接口声明中定义的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module module_name
#(
parameter parameter_name = parameter_default_value
) (
...
)

...
endmodule

module_name #(
.parameter_name(parameter_value)
) (
...
);

localparam 是模块内部定义的参数,语法如 localparam localparam_name = localparam_value

`define 是 Verilog 的宏定义,与 C 宏的文本替换相似。宏除了用于定义常量,还可以用来简化代码的编写,例如:

1
2
`define packed_wires = {wire_1, wire_2, wire_3}
packed_wires = 3'b101

变基部分选择(Indexed part-select)

可以使用 wire_name[index_wire_name] 的方式来实现一位宽度的多路选择器。相应地,Verilog-2001 也提供了 wire_name[WIDTH * index_wire_name +: WIDTH] 的方式来实现多位宽度的多路选择器。详情可以参考这个 StackOverflow 问题:

What is this operator called as “+:” in verilog - Electrical Engineering Stack Exchange

然而这个语法的综合有一些怪异的地方,有时会导致综合成移位器而非多路选择器,消耗较多的资源,使用时需要留意一下综合报告。

对表达式进行选择

在 Verilog 中,无法直接对 wire_name + 1'b1 这样的表达式选择某些位,但可以其实通过加上花括号的方式进行选择,例如 {wire_name + 1'b1}[3:0]。这个形式可以用于显式地截短运算结果并且不触发警告。

然而这个 trick 对于变基部分选择无效。以及似乎在某些旧型号的硬件上 XST 无法识别这个语法。

代码简化

可以采用 generatetaskfunction 简化代码逻辑的编写,详细的使用方法在 Verilog General Guide 中已经说明,故不再赘述。

信号宽度警告

信号宽度警告是一种十分有用的警告,因为许多低级错误都是由于错误地(或忘记)指定信号宽度导致的。请务必仔细检查综合报告。

由于裸写的非零十进制整数字面量默认为 32 位,容易增加多余的警告,因此建议在书写字面量时总是使用 2'b10'd16'h 这样的前缀形式。

至于对信号进行递增并且在溢出后自动归零,可以采用 reg_name = reg_name + 1'b1 的形式,综合器不会将此判断为溢出而发出警告。

组合逻辑

在 Verilog 中组合逻辑可以通过在 always @* 块中使用 ifswitch 和阻塞赋值 = 实现,也可以直接使用 assign?: 这个条件运算符实现。

我个人的建议是避免使用 always 块实现组合逻辑,因为在用 ifswitch 实现逻辑时,很容易忘记书写某些情况下的默认值,导致综合结果为 FF/Latch 的时序电路而非组合电路,并且这件事情很难在综合报告中发现。

我目前采取的实践是仅将 always 块用于实现时序电路,而使用条件运算符实现组合逻辑。

顺带提及,always @* 用于实现组合电路,应该只使用阻塞赋值 =always @(posedge clock) 用于实现时序电路,应该只使用非阻塞赋值 <=;两者混用依然可以成功综合,但是意义不明,十分容易出错。

Lint

对于长时间调试难以找出的错误,可以尝试使用 Lint 工具对代码进行静态检查,它比 XST 综合器的检查更加严格,也常常能指出代码中埋藏的许多低级错误(我曾经用它在十分钟内找出了一个调试了一整天未能找出的 Bug)。

Verilator 是一个免费的 Verilog 模拟器,其中也包含了 Lint 功能。安装 verilator后,执行 verilator --lint-only top.v 即可开始检查。

其他

综合过程中输出的 Simulation mismatch 警告不能无视,否则可能会导致不稳定的综合结果和奇怪的错误。

结语

除了某些语法检查不够严格,Verilog 其实是一门设计得较为直觉的语言,语法结构大多可以映射到电路实现,有着 C 一般的简洁和直观。如果遵循一定的编码规范和最佳实践,使用硬件而非软件的思维进行编码,还是可以较为简单地达成目标的。