DDPP 第五章学习笔记

DDPP5 第五章 Verilog Hardware Description Language 的学习笔记

也就是初学 (System)Verilog 的笔记

本来想寒假学结果还是拖到了数字逻辑实验开始用 FPGA只不过手上有模块了再来学也挺好的

虽然从创建到发布拖了很久但其实大部分内容都是新建文件后一周内写的只是后来感觉学的东西已经差不多能应付上课了就一直咕着没把最后一点学完 & 写完

neovim 配置

用的是老师推荐的 Vivado 2019.2 WebPack而它自带的文本编辑器多少有点拉胯所以研究了一下配 neovim

最后选择的是 veridian + 通过 null-ls 使用 Vivado 的 xvlog一开始我还看 veridian 没在维护而且 star 少但它好歹最后更新是在 2021Vivado 还在用 2019先试了另外几个后来发现不说别的只有 veridian 支持 hover同时使用 xvlog 是觉得还是 Vivado 自带的 lint 比较靠谱

veridian 就是用 lspconfig记得同时装 verible 才能使用某些 feature在 lspconfig 里只需启用 veridian可以把 root_dir 设为 util.root_pattern('*.xpr', '.git') 来检测 Vivado 项目的根目录

null-ls 的配置如下因为 xvlog 实际上是一个不能禁用文件输出的 simulator采取了一些措施来防止它到处倒垃圾

local null_ls = require "null-ls"
local utils = require "null-ls.utils"
local helpers = require "null-ls.helpers"

local xvlog_sv = {
  name = "xvlog",
  method = null_ls.methods.DIAGNOSTICS,
  filetypes = { "systemverilog" },
  generator = null_ls.generator {
    command = "bwrap", -- only permit writing in /tmp
    args = {
      "--ro-bind", "/", "/",
      "--bind", "/tmp/xvlog", "/tmp/xvlog",
      "--dev", "/dev",
      "/home/ouuan/Xilinx/Vivado/2019.2/bin/xvlog",
      "--sv", "$FILENAME",
    },
    cwd = function(params)
      -- output in /tmp
      local dir = '/tmp/xvlog/' .. params.bufnr
      vim.fn.mkdir(dir, 'p')
      return dir
    end,
    to_temp_file = true,
    format = "line",
    check_exit_code = { 0, 1 },
    on_output = helpers.diagnostics.from_patterns {
      {
        pattern = '(.+): %[.+%] (.+) %[.+:(%d+)%]',
        groups = { "severity", "message", "row" },
        overrides = {
          severities = {
            ["ERROR"] = 1,
            ["WARNING"] = 2,
            ["INFO"] = 3,
          },
        },
      },
    },
  },
}

null_ls.setup {
  sources = {
    xvlog_sv,
  },
  root_dir = utils.root_pattern("*.xpr", ".git")
}

另外可以用 vim-xdc-syntax 添加管脚约束文件的高亮

Vivado 的 custom text editor 使用下面的脚本是针对 i3 配的给 konsole 设了 name 参数i3 设成 assign [instance="konsole"]instance 而非 class就可以不把这个 konsole 挪到其他 konsole 所在的 workspace并且可以在打开新文件时 focus 过去

#!/bin/bash

# vivado-nvim.sh "[file name]" [line number]

set -euo pipefail

unset LD_LIBRARY_PATH

NVIM_LISTEN_ADDRESS=/tmp/vivado-nvim.pipe

if [[ ! -e $NVIM_LISTEN_ADDRESS ]]; then
    exec konsole --name "vivado-nvim" -e nvim --listen $NVIM_LISTEN_ADDRESS "$1" "+$2"
else
    nvim --server $NVIM_LISTEN_ADDRESS --remote "$1"
    nvim --server $NVIM_LISTEN_ADDRESS --remote-send ":$2<CR>"
    i3-msg '[instance="vivado-nvim"] focus'
fi

基础语法

module

Verilog 以 module 为基本单位和前端的 component 有点类似

例子

module inhibit (
    input  in,
    input  invin,
    output out
);
  assign out = in & ~invin;
endmodule

convention 是每个文件只写一个 module

signal (net & variable)

一个 1-bit 的 signal 有四种取值01x未知z高阻抗

位运算&|~^~^/^~

signal 有两大类netvariable

  • net 表示线路一般是 wire还可以是 supply0supply1我用 supply1 的时候出现了神秘的问题没细究反正用恒为 1 的 logic 也差不多
  • variable 用于 procedural statement 中不一定对应到物理上的线路可以是 reg 或者 integer其中 reg 就是变量的意思名字取得不太好表示单个 bit 或者 vector与基于 flip-flop 的寄存器无关integer 是有符号整型一般不用来存储数据或信号而是用于 for 循环之类的地方

input 只能是 net而 output 可以是 net 或 reg不写 wire / reg 时默认是 wire

在 SystemVerilog 中推荐用 logic 来代替 reg

数字字面量 & parameter

直接写十进制数会得到一个 signed number

可以指定位数和进制1'b0 是 1 bit 的 04'ha 是 4 bit 的 10十六进制的 A8'b01x0z1x1 的一些 bit 是未知 / 高阻抗如果后面写的值的位数超过前面指定的位数高位会被扔掉如果少了则会高位补零或者补 xz如果最高位是 xz这样得到的会是一个 unsigned vector可以加上 s 得到 signed vector4'sb1101

parameter 用来设置带默认值的参数parameter SIZE = 32, MSB = SIZE - 1, LSB = 0parameter ESC = 7'h1b parameter 一般用作常量而在 instance statement 中可以被修改

vector & 算术运算

多个 bit 可以组成一个 vectornetregparameter 都可以是 vector

vector 的下标可以是左边MSBreg [7:0] byte1, byte2也可以是右边LSBreg [1:16] bus起止的下标都可以指定左边的下标对应字面量中左边的 bitbyte1[7]byte1 最左边的 bitbus[16]bus 最右边的 bitbus[1:8]bus[9:16]bus 的左右两边读取越界会读到 x写入会忽略越界的部分

{} 用来连接 vector例如 {2'b10, 2'b01} 等于 4'b1001{2{byte1}, 2{byte2}} 等于 {byte1, byte1, byte2, byte2}

vector 可以按位进行位运算在二元运算中短的会高位补零后进行运算

二元位运算符也有一元的版本表示将 vector 内所有 bit 运算在一起得到 1-bit 的结果例如 &byte11'b1 表示 byte1 的所有 bit 都是 1

vector 之间进行赋值时会截低位或高位补零

vector 之间可以进行算术+-*/%**<<>><<<算术左移>>>算术右移

算术的高位和低位基于左右而与下标大小无关

除法和取模在某些情况下可能不 synthesizable除非除数是 2 的次幂synthesizable 时也可能会生成除法器的电路而非常昂贵

逻辑移位得到 unsigned算术移位保持原来的 signed/unsigned算术右移高位补符号位二元算术中只要有一个是 unsigned 就会将另一个转成 unsigned 再计算signal 可以声明为 signedreg signed [15:0] a

array

array 是相同类型的一列东西一列 regintegerwirearray 也可以指定下标范围而与 vector 相反声明 array 时框放在右边例如 reg [7:0] byte1, mem1[0:255] 表示一个 8-bit reg 和一个由 256 个 8-bit reg 组成的 array

array 可以嵌套为高维数组而访问只能访问单个下标不能像 vector 一样一下访问一个区间总之除了能指定下标范围都和 C 的数组差不多

逻辑运算

x / z 或者全 0 的 vector或者 1'b0是 false不含 x / z 且含 1 的 vector或者 1'b1是 truefalse 的值是 1'b0true 的值是 1'b1

逻辑运算符和比较运算符和 C 是一样的比较时如果一侧是 unsigned 则会按 unsigned 比较比较运算在电路中可能需要比较器尤其是两侧都不是常量时所以可能是昂贵的

三目运算符 ?: 和 C 是一样的

在 test bench 中x / z 的值在比较时结果为 x用在条件判断时即为 false可以使用 === / !== 来逐位比较x === x, z === z但它们不能用在 synthesizable module 中

compiler directives

`include`define和 C 是一样的

model

structural model

可以使用 instance statement 来写 structural model说白了就是将其他 module 实例化并连线

built-in gate 有

  • andnandornorxorxnor接受任意个输入
  • bufnot接受单个输入
  • bufif0bufif1notif0notif1三态门接受一个 data input 和一个 enable inputdata input 在前enable input 在后if 表示 enable 的 active level

使用 built-in gate 的例子built-in gate 的 port 没有名字只能通过顺序指定output 在前

module inhibit (
    input  in,
    input  invin,
    output out
);
  wire notinvin;
  not U1 (notinvin, invin);
  and U2 (out, in, notinvin);
endmodule

使用其他 module 的例子可以指定每个 port 的名字从而不依赖于顺序

module silly_xor (
    input  in1,
    input in2,
    output out
);
  wire inh1, inh2, notinh2, notout;
  inhibit U1 (
      .out(inh1),
      .in(in1),
      .invin(in2)
  );
  inhibit U2 (
      .out(inh2),
      .in(in2),
      .invin(in1)
  );
  not U3 (notinh2, inh2);
  inhibit U4 (
      .out(notout),
      .in(notinh2),
      .invin(inh1)
  );
  not U5 (out, notout);
endmodule

修改 parameter 的例子

module maj #(
    WID = 1
) (
    output [WID-1:0] out,
    input  [WID-1:0] i0,
    input  [WID-1:0] i1,
    input  [WID-1:0] i2
);
  assign out = i0 & i1 | i0 & i2 | i1 & i2;
endmodule

然后就可以 maj #(8) U1 (.out(W), .i0(X), .i1(Y), .i2(Z)) 或者 maj #(.WID(8)) U1 (.out(W), .i0(X), .i1(Y), .i2(Z))

不指定 parameter 时会使用其默认值parameter 只能在 instance statement 也就是 structural model 中被修改在其他类型的 model 中只能使用默认值

可以使用 generate block作用类似于 v-forv-if里面可以用 for循环变量需要是 genvarifcase被判断的要是 parameter例如

genvar i;
generate
  for (i = 0; i < N; i = i + 1) begin
    half_adder u0 (a[i], b[i], sum[i], cout[i]);
  end
endgenerate

dataflow model

可以使用 continuous-assignment statement 来写 dataflow model也就是 assign例如

module is_prime (
    input [3:0] N,
    output F
);
  assign F = N[3] ? (N[0] & (N[1] ^ N[2])) : (N[0] | (~N[2] & N[1]));
endmodule

behavioral model

always & begin-end block

always statement 用来执行一句 procedural statementbegin-end block 用来将若干 procedural statement 合成一句begin-end block 里的语句是顺序执行的always 是和 module 中的其他语句一起并行执行的

在 SystemVerilog 中推荐使用 always_combalways_ffalways_latch 来代替 always分别用于组合逻辑ff 和 latch但一般不会特意去写 latchalways_comb 会检查条件判断语句没有漏情况避免意外生成 latchalways_ff 需要加形如 always_ff @(posedge clk) 的 sensitivity list

如果 begin-end 里有 local logic 则需要给 block 起名字begin 的后面写上 : name才能在 simulation 之类的地方看到可读的变量名

module alarm_circuit (
    input panic,
    input enable,
    input exiting,
    input window,
    input door,
    input garage,
    output logic alarm
);
  always_comb begin : blk
    logic secure;
    secure = window & door & garage;
    alarm  = panic | (enable & ~exiting & ~secure);
  end
endmodule

赋值

procedural statement 中有两种赋值blocking 的 = 和 non-blocking 的 <== 表示立即赋值<= 会将赋值推迟至整个 always 的结尾右侧表达式的计算是立刻进行的从而在 always 剩下的部分中左侧变量的值依然是赋值前的

赋值最好遵循下面的规则

  • 在组合逻辑中只使用 =
  • 在时序逻辑中只使用 <=
  • 不要在同一个 block 中混合使用两种赋值
  • 不要在不同的 always 中对同一个变量赋值

if-else 语句

和 C 的语法是一样的唯一的不同就是大括号变成 begin - end

case 语句

module prime (
    input [3:0] n,
    output logic f
);
  always_comb
    case (n)
      4'd2, 4'd3, 4'd5, 4'd7, 4'd11, 4'd13: f = 1;
      default: f = 0;
    endcase
endmodule

虽然并非必须但一般来说选项应当是不重复且指定了宽度的字面量

即使选项覆盖了所有可能加上一个 default 可以在 simulation 中正确处理带 x 的值

casez 是允许使用通配符 ? 的匹配例如选项可以是 4'b10??

循环语句

有很多种循环语句但推荐使用的只有一种就是 for (integer i = 0; i <= 7; i = i + 1)

function 和 task

function 和 task 是一段可复用的 procedural statement写起来和 module 差不多需要定义在 module 内部可以通过 `include 来在不同 module 中复用

function 有单个返回值可以在函数名的前面给返回值设置类型也可以省略默认类型是 1-bit但不能有 output / inout port而是以 function 自身的名字作为返回值的名字代码中需要对这个函数名进行赋值function 中不能设置延时

module sillier_xor (
    input in1,
    input in2,
    output logic out
);
  function inhibit(input in, input inv_in);
    inhibit = in & ~inv_in;
  endfunction

  always_comb begin
    logic inh1, inh2;
    inh1 = inhibit(in1, in2);
    inh2 = inhibit(in2, in1);
    out  = ~inhibit(~inh2, inh1);
  end
endmodule

task 没有返回值但可以有 output / inout port用于 simulation 时可以设置延时

module lock_sim;

  ...

  task clock();
    #500;
    clk = 1;
    #500;
    clk = 0;
  endtask

  ...

endmodule

有一些内置的 task 和 function用于 simulation

  • $writeprintf 差不多
  • $display$write 的基础上多个换行
  • $monitor每次信号发生改变时都输出后指定的 monitor 会覆盖之前的
  • $monitoroff / $monitoron
  • $fflushflush 输出
  • $time输出当前的 simulated time
  • $random返回一个随机数接受种子作为参数不指定种子的话初次运行的种子是固定的
  • $stop停止模拟如果传参 (1) 则会显示当前的 simulated time 和代码位置

timescale & 指定延迟

`timescale 1ns / 10ps 指定以 1ns 作为延时的单位以 10ps 作为模拟的精度

assign 时可以指定延迟assign #5 a = b & c;

procedural model 中可以用 delay statement (#500;) 来暂停

simulation (test bench)

语法上和 synthesizable module 没有区别只不过有个 initial就是只执行一遍的 always一般不用于 synthesizable module写起来一般是一个没有输入输出有一个被测试 module 的实例有一个 initial block 的 module看看例子就行

懒得改 DDPP 上的代码了直接复制一份数字逻辑实验的代码

`timescale 1ns / 1ps

module lock_sim;
  logic clk = 0;
  logic rst = 0;
  logic mode = 1;
  logic [3:0] digit_input = 0;
  wire unlocked, incorrect, alert;

  lock #(
      .ADMIN_PASSWORD(16'ha73f)
  ) lock_inst (
      .clk(clk),
      .rst(rst),
      .mode(mode),
      .digit_input(digit_input),
      .unlocked(unlocked),
      .incorrect(incorrect),
      .alert(alert)
  );

  task clock();
    #500;
    clk = 1;
    #500;
    clk = 0;
  endtask

  task reset();
    rst = 1;
    #500;
    rst = 0;
    #500;
  endtask

  task input_password(input [15:0] password);
    reset();
    digit_input = password[15:12];
    clock();
    digit_input = password[11:8];
    clock();
    digit_input = password[7:4];
    clock();
    digit_input = password[3:0];
    clock();
  endtask

  initial begin
    input_password(16'ha73f);  // admin password
    input_password(16'h1234);  // incorrect

    mode = 0;
    input_password(16'h1234);  // set password

    mode = 1;
    input_password(16'h4321);  // incorrect
    input_password(16'h1234);  // correct
    input_password(16'ha73f);  // admin password

    // incorrect three times
    input_password(16'h4321);
    input_password(16'h4321);
    input_password(16'h4321);

    input_password(16'h1234);  // locked

    mode = 0;
    input_password(16'h4321);  // cannot set password

    mode = 1;
    input_password(16'h1234);  // locked
    input_password(16'h4321);  // incorrect
    input_password(16'ha73f);  // admin password
    input_password(16'h1234);  // correct
    input_password(16'h4321);  // incorrect
  end
endmodule

通过使用 $display可以更直观地输出结果从而不需要对着波形图看或者自动检查结果是否正确在出错时输出

有时可以通过文件读写来更方便地编写测试数据检查输出结果

有关 synthesize 的一些注意事项

  • 长串的 ifelse ifelse if……可能导致电路也有一长串使用 case 可能会更优
  • 组合逻辑中的循环可能会创建同一套电路的多个副本如果要用同一个电路可能需要改写成时序逻辑
  • 根据具体使用的 tool有些 language feature 是不能被 synthesize 的
  • 为了更好的 synthesize 结果而需要把代码写成什么样需要依具体使用的 tool 而定