计算机技术学习札记

一些 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 不一定是真的寄存器

关于 regwire 的区别,也可参看 https://zhuanlan.zhihu.com/p/471539431

前面说了,Verilog 描述的是电路;具体地,它描述的是电路的「行为」,而不是电路本身。在 Verilog 语法中,reg 虽然被称为「寄存器」,但它们并不一定真的会被综合成实际的寄存器。事实上,regwire 的区别在于:

  • 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 块中驱动,不能用 caseif 这样的方便语法,只能用 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 bc 的变化是这样的:

很多时候,电路的表现与预期不一样,就是因为没有考虑寄存器采样的时机造成的。

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 仿真时不会记录所有的变量(regwire)。这使得产生波形后,如果发现自己关注的变量没有出现在波形里,添加它后需要重新进行仿真才能得到这个量的波形。通过 这个方法 可以让 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 将展示所有被测量值在触发前后数千个时钟周期内的波形。