一些 Verilog 经验
可参见:Verilog、FPGA 和开发流程 —— 如果对「Verilog」「FPGA」是什么,以及自己要做什么还很迷茫,可以先看看这篇文章。
另可参见:《数字逻辑设计》理论课复习笔记(纯理论,无 Verilog 内容)。
Verilog 语言相关
我们写的是「电路」,不是「程序」
一个问题:C 语言和 Verilog 有什么区别?
事实上,这是一个无解的问题——C 语言是编程语言(PL),而 Verilog 是硬件描述语言(HDL)。C 语言编写的是「程序」,即告诉我们的计算机「第一件事做什么,第二件事做什么,然后做什么……」;而 Verilog 编写的是「电路」,即「这里有一个触发器,这里有一个逻辑门,我要把这几个地方用线连接起来」。
因此,在编写 Verilog 代码的时候,与其说是写程序,不如说是画电路图。为什么 Verilog 实现一个计数器要先 always @(posedge ...)
再让一个 reg
自增,而不能用 for
循环?那是因为电路里就没有能直接实现「for
循环」这样功能的结构;为什么 if
要和 else
配对?因为电路里你不走这条路,就必然得有另一个出口……我们所写的每一行 Verilog 代码,都会被综合器用来构建表达对应功能的电路结构。
reg
不一定是真的寄存器
关于 reg
和 wire
的区别,也可参看 https://zhuanlan.zhihu.com/p/471539431。
前面说了,Verilog 描述的是电路;具体地,它描述的是电路的「行为」,而不是电路本身。在 Verilog 语法中,reg
虽然被称为「寄存器」,但它们并不一定真的会被综合成实际的寄存器。事实上,reg
和 wire
的区别在于:
wire
描述的是「连线」,即,从一个地方连到另一个地方,实现值的传输。reg
描述的是「电路中暂时保留一个值的结构」,它的确可以是硬件上的「寄存器」,将值保留到下一个时钟边沿;但也可以只是一个线网连接处,作为某个复杂电路的中间结果。
具体被综合成什么电路结构,还要看整个电路的行为。
例如,下面的 Verilog 代码:
wire a;
wire b;
reg c;
always @(*) begin
c = a & b;
end
看上去,c
被声明为一个 reg
即所谓「寄存器」,但是因为所有与 c
有关的驱动电路都和时钟信号没有任何关系——它是在一个 always @(*)
即所谓「组合逻辑块」中驱动的,因此 c
实际综合时是线网。整个上面的电路等价于:
wire a;
wire b;
wire c = a & b;
最终综合为一个与门。实际综合出来的电路如下图:
这也可以解释为什么 三段式状态机 中,要写两个 reg
——当前状态的 state_current
和次态的 state_next
。由于 state_next
是在第二段用一个 always @(*)
驱动的,因此它并不是寄存器,只是一根线;我们用这根线去驱动 state_current
,使得它能在下一个时钟边沿到来时更新。如果你愿意,完全可以这么写三段状态机的第二段:
// reg [2:0] state_next; <- 不要用 reg 了,换 wire!
wire [2:0] state_next;
// 不要用 always 块了,直接上 assign!
assign state_next = (state_current == IDLE) ? 2'b00 :
(state_current == S1 ) ? 2'b10 :
...... // 略
那为什么不这么写呢?因为 wire
型变量不能在 always
块中驱动,不能用 case
或 if
这样的方便语法,只能用 assign
语句一次性驱动。这样会导致代码非常难读,因此我们选用「reg
+ always @(*)
+ 阻塞赋值」,而不选这样的「wire
+ assign
」。
寄存器「采样」到的都是旧值
对于真正的寄存器(reg
类型,并使用 always @(posedge ...)
驱动),它们会在每个时钟边沿到来时,去「采样」输入,然后用这个输入来更新自己。时间上,因为不能预知未来,因此所有寄存器采样的都是旧值,例如:
reg a, b, c, d;
always @(posedge clk) begin
a <= b;
b <= c;
c <= d;
end
当 d
发生变化后,a
b
和 c
的变化是这样的:
很多时候,电路的表现与预期不一样,就是因为没有考虑寄存器采样的时机造成的。
for
循环?
不要误会,Verilog 中的 for
循环是用来生成重复的电路结构的。例如,考虑下面的场景:现在有 100 根线 wire [99:0] in
,我们需要翻转它们(即,第 0 根变成第 99 根,第 1 根变成第 98 根……)并输出到 wire [99:0] out
。我们当然可以这样做,手动连接这 100 根线:
wire [99:0] in;
wire [99:0] out;
assign out[0] = in[99];
assign out[1] = in[98];
// ......
assign out[99] = in[0];
但是这样非常不优雅!有多少根线就要抄多少行代码。此时,借助 Verilog 的 for
循环,我们可以这样实现:
wire [99:0] in;
reg [99:0] out; // 为什么换成 reg 了?别误会,它仍然是 100 根线,原因在前面提到了
integer i; // 定义一个整型变量 i,它不依附任何电路结构
always @(*) begin
for (i = 0; i < 100; i = i + 1) begin
out[99 - i] = in[i];
end
end
上面的代码行使的功能,就和前面手动连 100 根线的代码是完全一样的。之所以 out
变成了 reg
,只是因为 for
循环必须在 always
块中使用,而如前面所言,又只有 reg
能在 always
块中驱动。这个 out
虽然是 reg
,但因为是组合逻辑驱动的,因此实际上是线网。这份 Verilog 代码综合成电路长这样:
从这个例子可以看出,Verilog 中的 for
循环是用来批量生成类似的电路结构(比如连 100 根线)用的,而不是具有任何「循环」功能的电路。for
循环使用的计数变量也不构成电路,例如上面的 i
。注意这个计数变量不能定义在任何 always
块内。同时 Verilog 没有 C 语言的 i++
简写法。
Vivado 相关
配置外部编辑器
由于 Vivado 内置的编辑器并不算好用,建议使用 Visual Studio Code 或者 VIM 或者其他自己用得顺手的编辑器。对于 Visual Studio Code,可以参考 这篇 文章配置插件和 Linter 以实现实时语法检查。
仿真所有的变量
默认情况下,Vivado 仿真时不会记录所有的变量(reg
和 wire
)。这使得产生波形后,如果发现自己关注的变量没有出现在波形里,添加它后需要重新进行仿真才能得到这个量的波形。通过 这个方法 可以让 Vivado 在一次仿真中就记录所有变量的值。
时钟 IP 核的选择
Xilinx 提供的时钟 IP 核有两种:锁相环(PLL,原理)和模式时钟管理器(MMCM,文档参考)。两种 IP 核使用时最直观的区别是稳定时间不一样(即 lock
信号出现的早晚不一样)。使用时可能需要注意和测试激励(testbench)的配合。
ILA 的使用
Xilinx 为其 FPGA 产品提供了片上逻辑分析仪(Integrated Logic Analyser, ILA)。ILA 可以用来抓取电路中的信号,实现电路在线调试。当遇到板上行为和仿真不一致时,可以考虑使用 ILA。
ILA 的使用细节见 Xilinx PG172。ILA 的使用方法大致是:ILA 作为一个 IP 核接入你的模块,在顶层模块中,将你想要测量的值连接到 ILA 上。综合上板后,设置一个 ILA 触发条件(某个量等于、大于、小于某个值),当触发条件到达时,ILA 将展示所有被测量值在触发前后数千个时钟周期内的波形。