硬件描述语言(HDL)大致功用

硬件描述语言和逻辑电路之间的关系类似于c语言和机器代码。c语言通过编译器编译成机器码,而硬件描述语言通过硬件描述语言综合器变成硬件电路网表文件

硬件描述语言和高级语言的一大区别是硬件描述语言的基本运行逻辑是并行,而高级语言是顺序执行

在HDL中,只有一部分是可以综合的,称为可综合的HDL。另外一部分主要用于仿真验证(例如等几秒后再进行下一步)

综合器大致工作过程为:

  • 将RTL描述通过EDA工具翻译成未经过优化的设计约束
  • 然后进行逻辑优化。去除冗余逻辑
  • 之后根据工艺库实现逻辑电路及其布线。工艺库由中芯国际和台积电等公司提供

语法基础

框架:

module mux2(input logic D0, D1, sel, output logic y);
logic a, b;
assign a = D0 & (~sel);
assign b = D0 & sel;
assign y = a | b;
endmodule

  • module endmodule: 定义了一个模块,注意module后面有一个;,而endmodule后面没有
  • input/output/inout [类型] [位宽] 端口名1, 端口名2: 这是模块的输入和输出。端口定义还可以有其他一些形式,如:
module mux2(input D0, output logic y);
logic D0, y;


module mux2(D0, y);
input logic D0;
output logic y;

推荐使用框架中的那种
  • 内部变量定义: 类型 [位宽] 变量名1, 变量名2: 这和端口定义基本类似,除了没有input和output

变量、常量、运算符

注释和c语言相同,也是///*

常量

常量的格式为: <+/-><位宽>’<进制><数值>

  • +/-: 表示正负, EDA工具默认使用无符号数,也就是说加了+号和不加+号是不同的含义
  • 位宽: 指的数据最多有多少个bit
  • ': 位宽后面的标识
  • 进制: b或B是二进制, o或O是八进制, d或D是十进制, h或H是十六进制.如果没有给出进制,默认为有符号数
  • 数值:数值间可以加下划线方便阅读。例如:1100_0101

例如:

-4'b0011: 首先转换成补码,为1101,然后是4位,所以结果就是1101
-4'd10: 转换成补码为10110(因为是负数,最高位一定是1),之后取4位,结果为0110。和我们想要得到的结果不同

数据类型

大体上可以分为变量和线网两种类型。变量只能有一个驱动源,线网必须是多驱动源(也就是可能有多个输入碰到一起),线网通常被解释为连线,而变量既可以被解释为连线,也可以被解释为寄存器。

线网类型主要有wire和tri。其中wire因为也是单驱动源,已经被废弃(端口信号可能是线网类型)。tri可以用于定义多驱动源信号。

变量

  • logic: 格式为:logic <位宽> <signed> 信号名1, 信号名2, …, 信号名n.logic可以表示4中状态,0,1,x,z。

默认logic位宽是1

例如:

logic [16 : 0] addrbus, databus;   	//定义了两个(无符号)的16位向量信号
logic [16 : 0] signed addrbus; // 定义了两个(有符号)的16位向量信号
logic a, b; //定义了两个1位标量信号,相当于logic [0 : 0] a, b;

logic可以进行域选(即任意把一些位赋给另一些位)

logic temp = z;
logic[7 : 0] out, in;
assign out[7 : 4] = in[3 : 0];
assign out[3] = in[2 : 2];

  • bit: 和logic类似,但是他只有0,1两种取值,下面几种变量也都只有两种取值
  • byte: 定义8位信号,但是它一次就赋8位,不能进行域选
  • shortint: 定义16位信号,和byte类型
  • int: 定义32位信号

运算符

大致和c语言相同,但是有一些还是略有区别。

缩减运算

例如:

logic [7 : 0] a;
assign &a;

等同于 a[0] & a[1] & a[2] & a[3] & a[4] & a[5] & a[6] & a[7];(不全是1结果就是0)

assign ^a;

^是半加法器,也就是1的个数为偶数时结果为0,为奇数时结果为1.

拼接和复制

SystemVerilog允许将多个数据进行组合,合成为一个数据

格式:{ 信号1[n1 : m1],信号1[n2 : m2], …,信号n[nn : mn] }

并且这些信号必须是确定位数,因此不可以出现未确定位数的常数,如1。

例如:

logic [1 : 0] B, C;	 	// 假设B = 2’b00,C = 2’b11,
logic [3: 0] Y;
Y = {B, C}; // 正确,结果Y = 4’b0011
Y = {B[0], C[1], 2’b10}; // 正确,结果Y = 4’b0110,注意,常数的位宽不能缺省
Y = {B, 5}; // 错误,因为常数5的位宽不确定
Y = {2{C}}; // 正确,结果Y = 4’b1111
Y = {B[0], C[1], {2{B[1]}}}; // 正确,结果Y = 4’b0100

最后两个中出现了 数字加{C}的方式,这是将这个信号进行复制,格式为:{n{A}}

行为建模

持续性赋值语句的建模

持续性赋值语句即使用assign的赋值语句,它是并行的,因此哪条语句在前哪条语句在后没有关系。它的使用方法如下:

assign <#延迟量> 信号名 = 表达式,延迟量以纳秒为单位

例如:assign #5 out2 = ~(A&B)

持续性赋值语句的含义是只要右侧变量发生变化,则立刻把该值赋给左侧变量,对应到电路就是输入输出的关系。此外延迟量仅用于仿真,不能被综合

基于过程块的建模

过程块内部的基本逻辑是顺序执行,每条语句之间有逻辑上的前后顺序(但是实际综合的时候可能会影响顺序)

它由关键字initial或always定义,通过begin…end(相当于c语言中的{…})包围代码块。initial主要用于仿真验证。因此这里之间always

always表示总是,也就是这个代码块处于无限循环当中,只要条件改变就会导致变化(always包围的代码块和其他assign也是并行的)。

always可以看成c语言中的代码块,它的参数就是外面的输入和它所影响的输出。一旦外面对应数值发生改变里面也会立刻对应改变

always_comb(描述组合逻辑)

模板:

      always_comb   begin
过程赋值语句;
高级语言结构,如ifelse, for
end

例如
module mux2 (input logic D0, D1, sel,
output logic out);
always_comb begin
// 高级语言结构if…else
if (sel == 0)
// 过程赋值语句
out = D0;
else
out = D1;
end
endmodule

在过程赋值语句中,=表示阻塞性赋值,也就是这条语句没有完成不进行下一步。并且左边必须是变量类型,不能是线网类型,这一点可能会导致一些错误

module adder (input a, b, cin, output [1 : 0] out); 
logic half_sum, half_carry;
always_comb begin
half_sum = a ^ b ^ cin; // 正确
half_carry = a & b | a & ~b & cin | ~a & b & cin; // 正确
out = {half_carry, half_sum};//错误
end
endmodule

最后一个错误是因为out并没有赋予它类型,因此它是默认的wire类型,是一种线网类型。

=号只在逻辑上是阻塞,实际上不一定是串行。

module mux2 (input logic D0, D1, sel, output logic y)
logic a, b;
always_comb begin
a = D0 & ~sel; // 阻塞赋值
b = D1 & sel; // 阻塞赋值
y = a | b; // 阻塞赋值
end
endmodule
这是一个多路选择器,它的两个端口是 同时进行选择的,也就是说a和b是同时进行赋值的,但是这里使用阻塞性语句也可以综合成电路

分支结构

SystemVerilog也支持if-else语句和case语句。如果其中有多行代码要使用begin-end
包围,尤其是case在c语言中不需要而在这里需要。

例如:

module mux2 (input  logic D0, D1, sel, 
output logic y);
always_comb begin
// 高级语言结构if…else
if (sel == 0)
y = D0;
else
y = D1;
end
endmodule

module mux2 (input logic D0, D1, s,
output logic y);
always_comb begin
case (s)
1’b0: y = D0;
1’b1: y = D1;
endcase
end
endmodule

注意: if语句和case语句都尽量考虑到所有情况,否则将综合处带有锁存器的时序电路,效率大为降低。

因此对于不完整分支,将剩下项补全,多余的项的内容可以随意设置。

在case语句中,可以使用?表示无关项,也就是该项即可以取零又可以取1.例如:

module dec2to4(input logic EN, input logic [1 : 0] A, output logic [3 : 0] Y);
always_comb begin
casez({EN, A})
3’b100: Y = 4’b0001;
3’b101: Y = 4’b0010;
3’b110: Y = 4’b0100;
3’b111: Y = 4’b1000;
3’b0??: Y = 4’b0000;
endcase
end
endmodule

循环结构

  • for语句:其用法是for (循环变量初始化; 条件表达式; 修改循环变量表达式)语句块
  • repeat语句:一种预先指定循环次数的循环语句,其用法是repeat (循环次数表达式) 语句块
  • while语句:其用法是while (条件表达式) 语句块
  • forever语句:一种无限循环语句,其用法是forever 语句块

在这四种语句中,只有for语句是可综合的

例如:

module loop_demo(input logic [2 : 0] din, 
output logic [1 : 0] out);
int num_bits
always_comb begin
int i;
num_bits = 0;
for (i = 0; i < 3; i++) begin
if (din[i] == 1)
num_bits = numbits + 1;
else num_bits = numbits;
end
assign out = num_bits;
end
endmodule

模块实例化

创建模块之后可以在其他模块之中被调用,调用格式为:

模块名 实例化名 (信息列表)

  • 信息列表也就是参数列表,他有两种书写形式
    • 位置关联: 每个参数都依照模块定义时的接口进行填入
    • 名称关联: 这种方法就可以不按顺序了。它的格式为.端口号(信号), .端口号
      是模块端口的名字,而信号这传入的参数的名字
    • 位置关联和名称关联一次只可以使用一种,不可以部分位置关联部分名称关联

例如:

mux2 (input logic D0, D1, sel,
                        output logic y);
always_comb begin
if (sel == 0) out = D0;
else out = D1;
end
endmodule
mux2 finalmux (low, high, sel[1], y);//位置关联
mux2 beginmux(.y(y), .D0(low), .D1(high), .sel(sel[1]));

其中mux2是这个模块的名字,也就相当于类名,finalmux是实例化后的名字,注意实例化后就立刻开始执行,不需要写额外的代码。

模块和assign或者always语句是同一级别的,他们之间都是并行的

此外系统一般还会提供一些非常基础的模块,如and, or等,这些模块可以不写实例化名字

参数化建模

参数化模块

在SystemVerilog HDL中,可以使用parameter来标识一个常量,除此之外他还可以用作传入模块的一个预定义变量

例如:

module mux2
#(parameter WIDTH = 8)
(input logic [WIDTH-1:0] D0, D1,
input logic s,
output logic [WIDTH-1:0] y);

assign y = s? D0 : D1;

endmodule

module mux4_32
(input logic [31:0] D0, D1, D2, D3,
input logic [1:0] s,
output logic [31:0] y);
logic [31:0] low, hi;
mux2 #(32) lowmux(D0, D1, s[0], low);
mux2 #(32) himux(D2, D3, s[0], hi);
mux2 #(32) outmux(low, hi, s[1], y);
endmodule

注意后面的mux2 #(32).这就是把#(parameter WIDTH=8)中的WIDTH进行修改,并用更改后的WIDTH当做模块的参数,和c++中的模板类类似。

此外还可以把parameter写入内部,当做一个常量使用

module mux2 (input D0, D1, s,  
output y);

parameter WIDTH = 8;
logic [WIDTH-1:0] D0, D1, y;
logic s;
assign y = s? D0 : D1;

endmodule

但是引入常量还可以使用`define语句,它可以用`include导入

`define WIDTH 8   //结尾不需要“;”
`define ENABLE 1
`define INIT 16’h000


`includedefine.sv”
module mux2
(input logic [WIDTH-1:0] D0, D1,
input logic s,
output logic [WIDTH-1:0] y);

assign y = s? D0 : D1;

endmodule

generate语句

generate通常和for,if,case配合使用,它可以自动生成代码

module andN #(parameter WIDTH = 4) (input logic [WIDTH – 1 : 0] a, output logic y);
genvar i;
logic [WIDTH – 1 : 0] x;

generate
assign x[0] = a[0];
for (i = 1; i < WIDTH; i = i+1) begin : forloop
assign x[i] = a[i] & x[i - 1];
end
endgenerate
assign y = x[WIDTH - 1];
endmodule

注意想要使用generate,在for语句中必须写上begin和end,并且begin后面还要加上forloop.并且循环变量的类型是genvar.

测试

例如:

`timescale 1ns/1ns		// 预编译指令,定义时间单位和时间精度
module sillyfunction_tb ( ); // 测试程序没有输入/输出端口
logic a, b, c, y;
sillyfunction dut (.a(a), .b(b), .c(c), .y(y)); // 实例化待测模块
initial begin // 给出激励信号
a = 0; b = 0; c = 0; #10;
c = 1; #10;
b = 1; c = 0; #10;
c = 1; #10;
a = 1; b = 0; c = 0; #10;
c = 1; #10;
b = 1; c = 0; #10;
c = 1; #50;
$finish
end
initial begin // 输出结果,否则只产生波形
$monitor ($time, “a = %a, b = %b, c = %c, y = %y, a, b, c, y);
end
endmodule

initial语句

initial语句广泛应用于测试程序之中,它的特点就是所有语句都只会执行一次。

但是需要注意的是多个initial语句时并发执行,如果有某一时刻两个initial语句对用一个变量进行赋值那么就会产生错误。

通过文件给予激励

我们可以把输入输出放到一个文件之中,然后程序从文件中获得激励信号并进行比对。

module sillyfunction_tb ( );
logic a, b, c, y;
logic [2 : 0] stim [7 : 0]; // 声明一个logic类型的数组
int i;
sillyfunction dut (.a(a), .b(b), .c(c), .y(y));
initial begin
$readmemb (“testvector.txt”, stim); // 将所有激励读入数组stim
for (i = 0; i < 8; i = i + 1) begin
{a, b, c} = stim[i]; #10; // 依次测试各个激励
end
end
initial begin
$monitor ($time, “a = %a, b = %b, c = %c, y = %y, a, b, c, y);
end
endmodule

输入文件格式

0000_0000
0110_0001 0011_0010
// 地址3~255没有定义
@100 // hex
1111_1100
//地址257~1022没有定义
@3FF
1110_0010
  • 可以使用_进行分隔,便于观察
  • 使用空格或者换行来区分单个数据
  • 使用@+地址来表示之后从第几位开始,注意这里不是第几个bit,而是第几个数据

系统任务(函数)

名称 作用
$time 显示仿真时间,也就是第几十纳秒
$stime 返回32位时间
$realtime 显示实数的时间(有小数)
$display $display (“显示格式控制符”, <输出变量(信号)列表>);,例如$display ($time, " a = %b b = %b c = %b y = %b", a, b, c, y);``%
$moniter 和display作用类似,都是输出到控制台,不同的是display只有运行到display语句才会执行,而moniter只要显示的量改变就会执行
$finish / $finish(n) 中断仿真, n是中断仿真的时间
$stop 和finish用法相同
$readmemb 从文件中读取二进制数据,$readmemb (“数据文件名”, 数组(存储器)名, <起始地址>, <结束地址>);
$readmemb 从文件中读取十六进制数据
$fopen $fopen(“文件名“, “操作模式“); 打开文件,操作模式有w,w+, a, a+。fopen会返回一个int型值,可以被fclose用来关闭文件
$fclose 关闭文件

打开文件将控制台输入导入文件示例

int MCD;
MCD = $fopen(“文件名“, “操作模式“);
$fdisplay( MCD, “显示格式控制符”, <输出变量(信号)列表>);
$fmonitor( MCD, “显示格式控制符”, <输出变量(信号)列表>);
$fclose( MCD);

display的%后面的参数可以为

%h %o %d %b %c %s %t %m
16进制 8 10 2 ASCII 字符串 时间 模块名

算术电路

加法器

半加法器

半加法器很容易想到使用异或操作,但是半加法器也有一个进位的输出,它的真值表为:

A B $C_{out}$ S
0 0 0 0
0 1 0 1
1 0 0 1
1 1 1 0
S = A ^ B
C_out = A & B //只有两个都是1才会进位

全加器

全加器相比于半加器还把上一个加法器来的进位考虑进去了

它的表达式为:

S = A ^ B ^ C
C_out = A & B + (A ^ B) & C_in;
A & B表示a和b同时为1时产生进位。
A ^ B表示A,B有一个为1时结果为1,与上C_in就表示A和B中有一个为1并且C_in为1就进位。

其实(A | B) & C_in也可以,但是前面已经有一个A ^ B了,在电路中我们可以直接拉一根导线然后和C_in相与,这样就少了一个或的器件

行波进位加法器

这是一种最为朴素的多位加法器,就是上一个加法器计算完成之后将进位给下一个加法器,直到最后一个加法器计算完成。

如图是一个四位的行波进位加法器,每个方框代表一个加法器,而红色部分代表关键路径

先行进位加法器

先行进位比行波进位更为快速,但消耗的硬件资源更多。

公式推导

可以把S = A & B 看成 G
把(A ^ B) 看成P
于是 C_out = G + P C_in;

通过上面我们可以看到,先行进位可以通过计算G和P快速得到进位。

这是先行进位加法器的电路图。例如把32位的加法器分成8组,每组四个加法器。

  • 一旦开始通电,则每组的G和P都可以并行计算得到,总共需要的时间为$t{pg}$(这是计算$G_0$,$P_0$,$G_1$,$P_1$…所需要的时间,也就是一个与门或者是或门所需要的时间。还有$t{pg_block}$,这段时间是$G_{3:0}$所需要的时间,也就是6个与或门所需要的时间
  • 一旦上一个先行进位计算完成,就可以传输到下一个先行进位计算器,到最后一个先行进位计算器所需要的时间为$t{pg} + t{pg_block} + 7 * t{and_or}$.前面两个是第一段计算G和P的时间,因为是同步计算,所以第一组行波进位器计算完G和P的时候其他的也计算完了。其他的只需要等待进位到来就可以计算。而$t{and_or}$是$C{in}到C{out}$那一段。
  • 最后一个先行进位计算完成之后,给最后一组四个加法器进行计算,加法器内部使用的是行波进位,我们可以把行波进位时间设为$t_{FA}$

因此总时间为$t{pg} + t{pgblock} + 7 * t{andor} + t{FA}$

移位器

移位器也就是左移右移操作。

图中是右移移位,它使用了4个多路选择器进行选。如果控制信号是00,那么不需要移位,4个选择器的00分别接对应输出位的输入。如果是01,那么最高位是0,Y3的01位也就是0.

乘法器

乘法器可以使用移位和加法实现。

$A_3 , A_2 , A_1 , A_0$是4个二进制位,$A_0 B_0$是两个二进制位相与