首页 > 技术文章 > Verilog数据流描述

EXQSLoveForever 2021-10-04 16:59 原文

前言


当电路比较简单时,我们可以通过门电路的方式实现相应的功能,当电路规模变大时,如果仅使用门级描述依次完成所有逻辑门的实例化,建模工作就变得非常烦琐而且容易出错。这就要求设计者能够从更高的抽象层次对硬件电路进行描述建模。

数据流级描述便是抽象层次描述的一种。它从数据流动的角度来描述整个电路,所以大多数情况下它依然离不开基本的电路结构图或逻辑表达式。但是数据流语句的描述重点是数据如何在电路中“流动”,即数据的传输和变化情况﹐所以体现在描述语句中,重点是在整个电路从输入到输出的过程中,输入信号经过哪些处理或者运算,最终才能得到最后的输出信号。而这些数据的处理过程,就是通过等式右侧的由操作符和操作数组成的运算表达式获得的。

点击查看代码
//举例:

module logic_gates(oY,iA,iB,iC);
  output oY;
  input iA,iB,iC;
  
  assign oY = (iA&iB)|(iA&iC);

assign 语句


连续赋值语句(assign语句)是Verilog HDL数据流建模的基本语句,用于对线网进行赋值,在工程实践中经常用于组合逻辑电路。

  • 等价于门级描述,然而是从更高的抽象角度来对电路进行描述。

连续赋值语句必须以关键词assign开始,其语法如下:

assign [drive_strength] [delay] net_value = expression;

//drive_strength 为可选项,默认值为strong1和strong0
//表示“DRAM驱动强度”。这个参数用来控制内存数据总线  的信号强度,数值越高代表信号强度越高,增加信号强度可以提高超频的稳定性。

//delay 为可选项,用于指定赋值的延迟
//expression由操作符和操作数组成

举例:

点击查看代码
//连续赋值语句,out、i1、i2也是线网
assign out = i1&i2;

//向量网的赋值语句,addr、addr1、addr2是16位向量
assign addr[15:0] = addr1[15:0]^addr2[15:0];

//拼接操作。赋值操作符左侧是标量线网和向量线网的拼接
assign {c_out, sum[ 3:0]}= a[ 3:0] + b[3:0] + c_in;
  • (1)连续赋值语句等号左边的net_value必须是一个标量或向量线网,或者是标量或向量线网的拼接,而不能是向量或向量寄存器。
  • (2)连续赋值语句总是处于激活状态。只要任意一个操作数发生变化,表达式就会被立即重新计算,并且将结果赋给等号左边的线网。
  • (3)等号右边表达式expression的操作数可以是标量或向量的线网或寄存器,也可以是函数调用
  • (4)赋值延迟用于控制对线网赋予新值的时间,根据仿真时间单位进行说明。赋值延迟类似于门延迟,对于描述实际电路的时序是非常有用的。
  • (5)隐式连续赋值:除了首先声明然后对其进行连续赋值以外,Verilog还提供了另一种对线网赋值的简便方法,即在线网声明的同时对其进行赋值。

由于线网只能被声明一次,因此对线网的隐式声明赋值只能有一次。

比较:

//普通的连续赋值
wire out;
assign out = in1 & in2 ;


//使用隐式连续赋值实现与上面两条语句同样的功能
wire out = in1 & in2 ;

隐式线网声明:如果一个信号名被用在连续赋值语句的左侧,那么Verilog编译器认为该信号是一个隐式声明的线网。

如果线网被连接到模块的端口上,则Verilog编译器认为隐式声明线网的宽度等于模块端口的宽度。

例:

//连续赋值, out为线网类型
wire il, i2;
assign out =il& i2;
//out并未声明为线网,但Verilog 仿真器会推断出out是一个隐式声明的线网

连续赋值语句中的延迟用于控制任一操作数发生变化到语句左值被赋予新值之间的时间间隔。指定赋值延迟的方法有三种:普通赋值延迟、隐式赋值延迟和线网声明延迟。


普通赋值延迟


即在连续赋值语句中说明延迟值,延迟值位于关键词assign的后面

例:

assign #10 out = inl & in2;//连续赋值语句中的延迟

在上面的例子中,#表示时间延迟,如果inl和 in2中的任意一个发生变化,那么在计算表达式in1 & in2的新值并将新值赋给语句左值之前,会产生10个时间单位的延迟。如果在此10个时间单位期间,即左值获得新值之前, inl或in2的值再次发生变化,那么在计算表达式的新值时会取inl或in2的当前值。这种性质被称为惯性延迟。也就是说,脉冲宽度小于赋值延迟的输入变化不会对输出产生影响。


隐式连续赋值延迟


使用隐式连续赋值语句来说明对线网的赋值以及赋值延迟。隐式连续赋值等效于声明一个线网并且对其进行连续赋值。

例:

wire #10 out = in1 & in2;//隐式连续赋值延迟

线网延迟声明


Verilog 允许在声明线网的时候指定一个延迟,这样对该线网的任何赋值都会被推迟指定的时间。

例:

wire #10 out;//线网延迟声明
assign out = in1 & in2;

与下面的语句等价:

wire out;
assign #10 out = in1 & in2;

操作符


Verilog HDI的操作符有很多种,按其功能大致可以分为逻辑操作符、位操作符、算术操作符、关系操作符,移位操作符,拼接操作符﹑缩减操作符、条件操作符。如果按其处理操作数的个数可以分为单目操作符,双目操作符和三目操作符。


操作符优先级



操作符分类和使用举例:



操作数


在操作数中首先要介绍的是数字,数字并不是数据类型中的某一种,但可以使用数字对数据类型进行赋值。

基本格式如下:

<位宽>'<进制><数值>

eg:
2'b10//位宽2,二进制,值为10
2'd10//位宽2,十进制,值为10

Verilog HDL中支持4种进制形式:二进制,八进制、十进制和十六进制,分别用b,o,d ,h来表示(不区分大小写)。

数值部分指在相应进制下的数值。位宽表示了一个数字包含几位信息,指明了数字的精确位数,这个位数是以该数字转化为二进制后所具有的宽度来表示的

eg:
2'b01
4'd11

一般在包含位宽和进制的时候,有以下几种情况

  • 当位宽大于数值宽度时,如果数值部分是确切的数值,缺少的部分采用补零原则;

  • 当位宽小于数值宽度时,采用低位对其直接截取的方式,保留位宽中定义的宽度。

  • 在数值部分出现不定态x和高阻态z时,x和z也会根据进制的不同被扩展为不同的宽度。

在八进制中一个x相当于三位的二进制数xxx,在十六进制中就变为四位的二进制数xxxx。

  • 在数值的首位为x和z时,如果出现了位宽大于数值宽度的情况,则缺少的位分别按x或z补齐。

如果格式中缺少了位宽或进制,则会有其他等效方法

  • 如果数字中只包含进制和数值部分,则位宽采用默认宽度﹐主要取决于所使用机器的系统宽度和仿真器所支持的宽度,一般为32位。

  • 如果仅有数值部分,则在默认宽度的基础上默认进制为十进制。

数字也可以表示负数,在数字前直接添加负号即可,此时表示的是当前负数的二进制补码,负号不可以放在数值部分

-4'd6


数据类型


线网类型


线网(net)表示硬件单元之间的连接。就像在真实的电路中一样,线网由其连接器件的输出端连续驱动,包括wire,wand,wor,tri,triand, trior 以及 trireg 等,其中, wire类型的线网声明最为常用。

wire这个术语和net经常互换使用。如果没有显式地说明为向量,则默认线网的位宽为1。

线网的默认值为z(trireg类型的线网例外,其默认值为x),这就是为什么如果仿真失败后,所看到的线全部为z的原因

线网的值由其驱动源确定,如果没有驱动源,则线网的值为z。

例如:

wire a; // 声明a是wire(连线)类型,默认值为z
wire d=1'b0;  //连线d在声明时,d被赋值为逻辑值0

寄存器类型


用来表示存储元件,它保持原有的数值,直到被改写。

不要将这里的寄存器与实际电路中由边沿触发的触发器构成的硬件寄存器混淆

在Verilog 中,术语register仅仅意味着一个保存数值的变量。

与线网不同,寄存器不需要驱动源,而且也不像硬件寄存器那样需要时钟信号。

在仿真过程中的任意时刻,寄存器的值都可以通过赋值来改变。

寄存器数据类型一般通过使用关键字reg来声明,默认值为x。

例如:

reg reset;         //声明能保持数值的寄存器变量reset;
reset = 1'b1;      //把reset 初始化为1,使数字电路复位
#100 reset = 1'b0; //经过100个时间单位后,reset置逻辑0

寄存器也可以声明为带符号(Signed)类型的变量,这样的寄存器就可以用于带符号的算术运算。

reg signed [31:0] Q;  //声明一个带符号的寄存器向量

向量


线网和寄存器类型的数据均可以声明为向量(位宽大于1)。

如果在声明中没有指定位宽,则默认为标量(l 位)。

例如:

wire a;          //标量线网变量,默认宽度
wire [7:0] bus;  //8位长总线
reg s;           //标量寄存器,默认宽度
reg [0:40] addr; //向量寄存器,41位宽虚拟地址

向量通过[high: low]或[low: high]进行说明,方括号中左边的数总是代表向量的最高有效位。

向量addr的最高有效位是它的第0位,bus 最高有效位位第8位

  • 向量域选择:对于上面例子中声明的向量,可以指定它的某一位或若干个相邻位。
例如:

bus[7];   //选择bus的第7位
bus[2:0]; //选择bus的最低3位

//如果写成bus[0:2]是非法的,因为高位应该写在范围说明的左侧。
  • 可变的向量域选择:除了用常量指定向量域以外,Verilog HDL还允许指定可变的向量域选择,设计者可以通过for循环来动态地选取向量的各个域。
下面是动态域选择的两个专用操作符。
[<starting_bit >+ : width]   //从起始位开始递增,位宽为width
[<starting_bit >- : width]  //从起始位开始递减,位宽为width
起始位可以是一个变量,但是位宽必须是一个常量。

整数


整数是一种通用的寄存器数据类型,用于对数量进行操作,使用关键字integer进行声明。

虽然可以使用reg类型的寄存器变量作为通用的变量,但声明一个整数类型的变量来完成计数等功能显然更为方便。

整数的默认位宽为主机的字的位数,与具体实现有关,但最小应为32位。

声明为reg类型的寄存器变量为无符号数,而整数类型的变量则为有符号数。

举例:

integer counter;     //一般用途的变量,用作计数器
counter = 0;         //把0存储到寄存器中

实数


实常量和实数寄存器数据类型使用关键字real来声明,可以用十进制或科学记数法(例如3e6代表3 000 000)来表示。

实数声明不能带有范围,其默认值为0。

如果将一个实数赋给一个整数,那么实数将会被取整为最接近的整数。


时间寄存器


仿真是按照仿真时间进行的,Verilog使用一个特殊的时间寄存器数据类型来保存仿真时间。

时间变量通过使用关键字time来声明,其宽度与具体实现有关,最小为64位。

通过调用系统函数$time可以得到当前的仿真时间。

time save_sim_time;        //定义时间类型的变量save_sim_time
save_sim_time = $time;    //记录当前的仿真时间

仿真的时间单位为s,和真实时间的表示方法相同,但真实事件和仿真时间的对应由用户定义完成。


数组


在Verilog 中允许声明reg,integer,time,real,realtime 及其向量类型的数组,对数组的维数没有限制,即可以声明任意维数的数组。

线网数组也可用于连接实例的端口,数组中的每个元素都可以作为一个标量或向量,以同样的方式来使用,形如<数组名>[<下标>]

对于多维数组来讲,用户需要说明其每一维的索引。

reg向量与reg类型的数组在定义上的区别

reg向量的定义方式为reg [31:0] xl,reg数组的定义为reg sz[31:0]

不要将数组和线网或寄存器向量混淆起来

向量是一个单独的元件,它的位宽为n;
数组由多个元件组成,其中的每个元件的位宽为n或1


存储器


在数字电路仿真中,人们常常需要对寄存器文件,RAM和ROM建模。

在Verilog 中,使用寄存器的一维数组来表示存储器

数组的每个元素称为一个元素或一个字,由一个数组索引来指定,每个字的位宽为1位或多位。

注意,n个1位寄存器和一个n位寄存器是不同的。

如果需要访问存储器中的一个特定的字,则可以通过将字的地址作为数组的下标来完成。

举例:

reg mem1bit[0:1023]; //1kb(1位)存储器
reg [7:0] membyte [0:1023];//1kb(8位)存储器
membyte[511] = 0;   //将membyte存储器第511处所存的内容清零

参数


Verilog 允许使用关键字Parameter在模块内定义常数。

参数代表常数,不能像变量那样赋值,但是每个模块实例的参数值可以在编译阶段被重载。

通过参数重载使得用户可以对模块实例进行定制。

除此之外,还可以对参数的类型和范围进行定义。

通过使用参数,用户可以更加灵活地对模块进行说明。

用户不但可以根据参数来定义模块,还可以方便地通过参数值重定义来改变模块的行为:通过模块实例化或使用defparam语句改变参数值。

在参数定义时需要注意避免使用硬编码:

Verilog中的局部参数使用关键字localparam来定义,其作用等同于参数,区别在于它的值不能改变,不能通过参数重载语句(defparam)或通过有序参数列表或命名参数赋值来直接修改

例如,状态机的状态编码是不能被修改的,为了避免被意外地更改,应当将其定义为局部参数。

举例:

localparam state1 = 4'b001,
           state2 = 4'b001,
           state3 = 4'b001,
           state4 = 4'b001;

字符串


保存在reg类型的变量中,每个字符占用8位(一个字节),因此寄存器变量的宽度应足够大,以保证容纳全部字符。

  • 如果寄存器变量的宽度大于字符串的大小(位),则Verilog使用0来填充左边的空余位;

  • 如果寄存器变量的宽度小于字符串的大小(位),则Verilog 截去字符串最左边的位。

因此,在声明保存字符串的reg变量时,其位宽应当比字符串的位长稍大

举例:

reg [8*18:1] string1_1;//声明变量string_1,其宽度为18个字节
initial 
string_1 = "Hello world!";//字符串可以存储在变量中

有一些特殊字符在显示字符串时具有特定的意义,例如换行符、制表符和显示参数的值。

如果需要在字符串中显示这些特殊的字符,则必须加前缀转义字符

推荐阅读