虽然这是一个浮躁的社会,充满着一夜暴富的神话,但是学习技术真的很少存在所谓的捷径。这么说吧,至少贫僧还没有那个本事,完成“三周精通某某某”的本领。如果有听众还要速成,某家不得不说您老这票错了。小本经营,概不退票啊。急于见到代码的“傻弟弟”们,实际上属于信心缺失的毛病。和不做系统、结构设计就直接上 coding 一样,根本不是“艺高人胆大”,而是信心缺失。第一讲,施主没见到 Verilog 代码,这一讲里面状况虽然有所改善,但是也好不到那里去:您老还是别妄图做能综合或者仿真的代码,这个最少要等到下一讲。
1. 标准层次,简单说明
关于 Verilog 语言的官方标准全称是《IEEE Std 1364-2001:IEEE Standard Verilog® Hardware Description Language》。其中包括 27 章以及 8 个附录,真正对于电路设计有用的内容大约 1/3 的样子。
Verilog 是一种硬件编程语言,它既是一种结构描述的语言也是一种行为描述的语言,也就是说对于同一功能的物理电路,我们可以用 Verilog 中提供的数字电路中较为形象的门级结构来描述电路,也可以用更为抽象的一些语句来描述电路,于是乎,我们根据抽象的程度不同,将不同的描述方式进行划分,得到以下几种不同抽象级别的描述方式,其中,从上至下,离真相越来越近,也就是说最形象的是开关级,其次是门级,依次类推,最抽象的是系统级。
系统级(System Level):用高级语言结构实现设计模块的外部性能的模型;
算法级(Algorithm Level):用高级语言结构实现设计算法的模型;
RTL 级(Register Transfer Level):描述数据在寄存器之间流动和如何处理这些数据的模型;
门级(Gate Level):描述逻辑门以及逻辑门之间的连接的模型;
开关级(Switch Level):描述器件中三极管和存储节点以及它们之间连接的模型。
换言之,不同级别的抽象,也就是说在不同抽象层次上用 Verilog 语言来描述一个物理电路,若从行为和功能的角度来描述某一电路,则为行为级描述,系统级、算法级和 RTL 级属于行为级描述方式;若从电路的结构来描述某一电路,则为结构级描述,门级和开关级属于结构级描述方式。在实际应用中,我们要根据需求对每个 Verilog 模型进行设计,选用不同抽象级别的描述,才能使我们的设计事半功倍。各个层次的关系如图 1 所示。每个层次都可以单独进行设计,这个和练“龙象般若功”不同,无需按照等级一层一层玩。
图 1 Verilog 逻辑层次及其与工程阶段的关系
很多初学者感觉学习 Verilog 语言是老虎吃天 ---- 无从下口呢?这是和 Verilog 的贪心不足密切相关的。从工程方法学的角度上说,设计和验证是完全不同的两个行当,很少有人能够做到“昆乱不当”的。然而,Verilog 标准里面,确实体现了完全的五味杂糅,认为的造成了混乱,这是所谓“困难”的源头。
本套数只会讲 RTL 的那些事儿,其他的内容请另找师傅吧,老衲不反对。
2. 四值变化,闪展腾挪
先看看 Verilog 语言的值域问题,这是大伙儿作战的主要阵地,不可不查也。
Verilog 语言是描述数字硬件电路的;而《数字电路》/《数字逻辑设计》教材里面,又说数字电路的理论基础是布尔代数;布尔这个就偏门了,帮大家查了一下下,都说这个是“二值”逻辑的。但是,为什么连维基百科里面会有如下说法?
“四值逻辑
逻辑值及其解释
0:逻辑低电平,条件为假
1:逻辑高电平,条件为真
z:高阻态,浮动
x:未知逻辑电平
信号强度(从强到弱)及其属性:
supply:驱动
strong:驱动
pull:驱动
large:存储
weak:驱动
medium:存储
small:存储
highz:高阻态
上面列出了 Verilog 采用的具有八种信号强度的四值逻辑(four-valued logic),数字电路中的信号可以用逻辑值、信号强度加以描述。当系统遇到信号之间的竞争时,需要考虑各组信号的状态和强度。如果驱动统一线网的信号强度不同,则输出结果是信号强度高的值;如果两个强度相同的信号之间连接到同一个线网,将会发生竞争,结果为不确定值 x。
值“0”和“1”不难理解,这个在数电里面,大伙儿接触的多了,无需详述。
在 Verilog 语言的仿真中间,存在这种模棱两可的可能,于是“x”这个第三者出现了,标记输入出现问题。未知逻辑电平“x”的主要出现情况有两个。第一个:上面介绍的情况,多出现于触发器等不满足采样要求(就是前面时序分析里面介绍的保持时间和建立时间),俗称为“采到边沿”;另一个:触发器等具有存储功能的器件未被赋初值(包括:FPGA 赋初值和复位赋初值),俗称为“无初值”。大多数时序电路的当前状态(也就是各个触发器的当前值的集合)是和电路本真以前的状态有关的,这个后面会多次看到。没有初始值,仿真软件会郁闷的,后面的值是什么完全没谱了。仿真总归比实际系统仁慈一些,于是输出的值是“x”而不是随机选择,这样容易观察到。在大多数仿真软件输出的波形里面,“x”会被标为醒目的红色,于是这种现象也就有了一个通俗的说法:“全国江山一片红”。
高阻态“z”与逻辑未知电平“x”是一个“语言造”的概念不同,它是在电路系统中真实存在的。
在总线连接的结构上。总线上挂有多个设备,设备与总线以高阻的形式连接。这样在设备不占用总线时自动释放总线,以方便其他设备获得总线的使用权。大部分单片机 I/O 使用时都可以设置为高阻输入。高阻输入可以认为输入电阻是无穷大的,认为 I/O 对前级影响极小,而且不产生电流(不衰减),而且在一定程度上也增加了芯片的抗电压冲击能力。
“二进制无符号数、有符号数的二进制编码”,应该属于数电教习提纲里面的“应教、应知、应会”的内容。且容鄙人偷个懒,抄一点教科书的内容。
“二进制是计算技术中广泛采用的一种数制。二进制的数据是用 0 和 1 两个数码来表示的数。它的基数为 2,进位规则是“逢二进一”,借位规则是“借一当二”,由 17 世纪德国数理哲学大师莱布尼兹发现。当前的计算机系统使用的基本上是二进制系统,数据在计算机中主要是以补码的形式存储的。计算机中的二进制则是一个非常微小的开关,用“开”来表示 1,“关”来表示 0。
一般来讲,若 b 是基底,我们在 b 进制系统中的数表示为 a1bk + a2bk-1 + a3bk-2 + ... + ak+1b0 的形式,并按次序写下数字 a1a2a3 ... ak+1。这些数字是 0 到 b-1 的自然数。
若一段文字(譬如这段文字)讨论多个基数,若有歧义时,基数(本身用十进制表示)用下标方式写在数的右边。除非有上下文说明,没有下标的数字视为十进制。 通过使用一点(小数点)来将数字分成两组,就可以用位置系统来表示小数。例如,基数 2 系统中,10.11 表示 1×21+ 0×20 +1×2-1 +1×2-2 = 2.75。
一般来讲,b 进制系统中的数有如下形式:
对于常用的二级制系统,b = 2。”
为了方便书写以及符合工程师的一般习惯,Verilog 语言里面提供了关于数字的多种进制的支持,表达的一般格式如图 2 所示。除了数值部分前面已经介绍以之外,其他部分的值域和含义见表 1。
图 2 Verilog 内常量的一般格式
表 1 Verilog 内常量表达中各个部分的含义
名称 |
符号 |
位宽 |
进制与编码部分 |
含义 |
常量的正负属性 |
常量占用的比特宽度 |
进制方式 |
值域 |
“+”:正数 “-”:负数 如果本部分不存在,则认为是正数 |
正整数 如果本部分不存在,则在赋值时根据目的变量位宽转化,例如 666(十进制 666),’h3F(十六进制 3F16),’033(八进制 338) |
“’d”/“’D”:十进制无符号数 “’b”/“’B”:二进制无符号数 “’o”/“’O”:八进制无符号数 “’h”/“’H”:十六进制无符号数 “’s|Sd”/“’ s|SD”:十进制符号数 “’ s|Sb”/“’ s|SB”:二进制符号数 “’ s|So”/“’ s|SO”:八进制符号数 “’ s|Sh”/“’ s|SH”:十六进制符号数 其中带“s”/“S”的表示已经用补码表示,否则由系统转化为补码形式 |
有时候,数值表示会很长,此时可以使用下划线“_”分割,参考例 1。
【例 1】常量及其分割
常量 |
含义 |
数值(转化为十进制) |
8’b1001_1000 |
8 比特二进制无符号数 |
152 = (1001, 1000)2 |
16’h11_32 |
16 比特十六进制无符号数 |
4402 = (11, 32)16 |
15’o32_111 |
15 比特八进制无符号数 |
13385 =(32, 111)8 |
3. 信号有名,万物之始
《论语》上曰:“子路曰:‘卫君待子而为政,子将奚先?’子曰:‘必也正名乎!’”,可见名字的重要性。一个数字逻辑系统那么复杂,不命名是不可能的,但是如何命名呢?这就告诉大家。
在电路设计中,变量是指一个包含部分已知或未知数字、信号或逻辑(即一个值)之信号节点,以及对应之符号名称。Verilog 是大小写敏感的。所谓标识别符就是用户为程序描述中的。也就是说:counter 和 Counter 会被作为两个不同的变量。为了保证不产生混淆,一般建议对于变量的首字母的大小写进行一定的规范,例如:模块内部变量采用小写字母开头,同时模块端口使用大写字母开头。
一些命名规范是作为 Verilog 语法在语言层面强制执行的。
首先,不能用关键词给变量命名。这个是非常一般的要求,不详细论述。
其次,Verilog 语法规定,标识符(包括这里的变量,还包含一些后面才会提到的其他内容)应该以字母、下划线(“_”)开头,中间可以包含字母、数字以及下划线和美元符号(“$”)的字符(串)。例 2.6 里面给出了一些合法与非法的变量命名。
【例 2】变量命名
合法 |
非法 |
Counter counter_statement timer3215 CLK_50M _50M_CLK if_all_0 get_all_$ |
1Counter(数字开头) 50M_CLK(数字开头) Timer32*1ns(含有非法字母“*”) reg_filter@Chap3(含有非法字母“@”) wire_parameter%32(含有非法字母“%”) if(使用关键词 if) $_my_love($开头,会和后面介绍的系统任务混淆) |
Verilog 所用到的所有变量都属于两个基本的类型:线网类型(net/wire)和寄存器类型(register)。一般的说法是:
“线网与我们实际使用的电线类似,它的数值一般只能通过连续赋值(continuous assignment),由赋值符右侧连接的驱动源决定。线网在初始化之前的值为 x(trireg 类型的线网是一个例外,它相当于能够储存电荷的电容器)。如果未连接驱动源,则该线网变量的当前数值为 z,即高阻态。线网类型的变量有以下几种:wire、tri、wor、trior、wand、triand、tri0、tri1、supply0、supply1、trireg,其中 wire 作为一般的电路连线使用最为普遍,而其他几种用于构建总线,即多个驱动源连接到一条线网的情况,或搭建电源、接地等。
寄存器与之不同,它可以保存当前的数值,直到另一个数值被赋值给它。在保持当前数值的过程中,不需要驱动源对它进行作用。如果未对寄存器变量赋值,它的初始值则为 x。Verilog 中所说的寄存器类型变量与真实的硬件寄存器是不同的,它是指一个储存数值的变量。如果要在一个过程(initial 过程或 always 过程)里对变量赋值,这个变量必须是寄存器类型的。寄存器类型的变量有以下几种:reg(普通寄存器)、integer(整数)、time(时间)、real(实数),其中 reg 作为一般的寄存器使用最为普遍。
此外,利用寄存器变量的数组,还可以对 ROM 进行建模。”
最初学习 Verilog 语言的时候,在下有一个粗陋的理解:wire 类型就是对应组合电路,reg 类型对应的则是时序电路。然后,就是范玮琪的《最初的梦想》里面唱的:“如果骄傲没被现实大海冷冷拍下……”。老衲败了,而且败得很惨。现在,鄙人可以举出很多很多反例来证明当年的无知。这个话题,后文书也会介绍的,先留个话头。现在,您老只要记住:这种说法是错的,大错特错。
4. 向量数组,多位表示
对于一个单个的多比特信号,需要引入“向量”的概念。
向量形式的数据是在硬件描述语言中十分重要。在 Verilog 中,标量的意思是只具有一比特位宽的变量,而向量表示具有多个比特位宽的变量。如果没有特别指明位宽,系统默认它为标量。
在真实的数字电路,例如将两个四位二进制数相加的进位加法器中,我们可以发现,其中一个数是通过四条电线(每条线表示四位中的某一位)连接到加法器上的。我们可以用一个向量来表示这个多位数,分别用这个向量的各个分量来表示“四条电线”,即四位中的某一位。这样做的好处是,可以方便地在 Verilog 代码的其他地方选择其中的一位(位选)或多位(域选)。当然,如果没有进行位选或域选,则这个多位数整体被选择。
【例 3】4 比特 wire 型向量的声明
wire [3:0] input_add; // 声明名为 input_add 的 4 位 wire 型向量
wire [4:1] input_add1; // 也是 4 位 wire 型向量,但是分量序号从 4 到 1
wire [0:3] input_add2; // 也是 4 位 wire 型向量,但是分量序号从 0 到 3
reg [3:0] output_add; // 声明名为 input_add 的 4 位 reg 型向量
reg [4:1] output_add1; // 也是 4 位 reg 型向量,但是分量序号从 4 到 1
reg [0:3] output_add2; // 也是 4 位 reg 型向量,但是分量序号从 0 到 3
上面的向量声明之后,我们就可以方便地选择其中的某几个分量进行操作。请注意用于域选的方括号的位置在向量名称之后,方括号内的数字为所需的位数。例如我们可以进行如例 4 的操作。
【例 4】对于向量个别位的操作
input_add [3] = 1'b1; // 将 1 赋值给 input_add 向量的第三位(最高位)
input_add [1:0] = 2'b01; // 将 0 和 1 分别赋值给 input_add 向量的第 1、0 位(最低两位)
对于 reg 型向量,在 Verilog 2001 中,在声明时候也可以定义初始值,见例 5。
【例 5】reg 型向量的声明与初始值
reg [7:0] counter = 8’h0; //8 比特计数器声明,同时定义初始值 0
Verilog 中的几种寄存器类型的数据,包括 reg、integer、time、real,以及由这几种数据构成的向量,都可以构成数组。声明数组时,方括号位于数组名的后面,括号内的第一个数字为第一个元素的序号,第二个数字为最后一个元素的序号,中间用冒号隔开。如果数组是由向量构成的,则数组的其中某个元素是向量。同样,出于习惯考虑,我们一般让数组第一个元素的序号为 0,后面元素的序号依次递增。此外,和 C 语言类似,用户可以声明多维数组。例 6 是一些数组声明的例子。最后说一下,二维数组是 Verilog 2001 的特性,估计是为了满足方兴未艾的图像处理的需求。
【例 6】数组的声明与引用
integer number [0:100]; // 声明一个有 101 个元素的整数数组
number [25] = 1234; // 将 1234 赋值给 25 号(第 26 个)元素
reg [7:0] my_input [65535:0]; // 声明一个有 65536 个元素的 8 位向量寄存器
my_input [97] = 8'b10110101; // 将 10110101 分别赋值给 97 号(第 2 个)元素的 7 至 0 位
reg my_reg [0:3][0:4]; // 声明一个具有 20 个元素的二维寄存器数组
my_reg [1][2] = 1'b1; // 将 1 赋值给上述二维数组的第 2 行、第 3 列元素
表示数组某个元素时,允许使用变量来表示元素的索引,但是表示一个向量的一位或者几位时,只允许使用数字来表示位的索引;此外,使用数组时一次只能对一个元素进行操作,而不能向向量那样同时对连续的几个位进行操作。
由于数组和向量的表示都使用了方括号,因此使用时需要注意这个变量或向量的名称在最初被声明为何种类型的数据。由于数组的索引和向量的索引都是用了方括号对“[ ]”,对于向量数组的引用采用先数组索引,后向量索引的顺序,这个需要特别强调。例 7 给出了一个同时索引数组和向量的情况,可供参考。
【例 7】向量数组的索引
my_input [65535][7:4] = 4'b1010; // 将一个 4 比特二进制数赋值给第 65536 个元素的高四位。
5. 模块声明,边界标定
现在遇到了一个关键的 Verilog 关键词:module,汉语翻译就是模块的意思。这个关键词 module 与 endmodule 配对,标记代码中的一个模块,也可以说是一个逻辑的“芯片”。一个抽象的简单模型如下:
module module_name ( ports_table );
ports_decription;
begin
module_behavior description;
end
endmodule;
其中,
“module_name”(模块名称)的起名,请遵守有关的命名规则。总得原则是,叫人看到这个名字就大约知道这个模块是什么用途的。有些人总是喜欢 U1、U2 之类的名称,这样是不好的。这样说吧,如果将来挨了批评什么的,乃至因此丢了饭碗,别找贫道拼命。
“ports_table”(输入输出端口 / 管脚列表),起到于罗列出芯片的管脚(pin)的作用。理论上说,一个有意义的模块,总是对于输入数据进行某种处理(也即功能部分)然后输出出去。所以呢,输入输出总是难免的。在 Verilog 2001 中在输入输出端口 / 管脚列表也可以完成是有关是输入输出端口 / 管脚列表定义。
“ports_decription”(输入输出端口 / 管脚定义),完成确定端口信号方向和位宽的作用。端口信号可以是 1 bit 的,也可以是多位位宽的。如上所述,这部分可能被输入输出端口 / 管脚列表收编。端口名称不能重复定义否则会引起混乱,这个不必细说,地球人都知道。
“module_behavior description”是模块真正的功能部分(这部分超出了本章的范围,在下将会在后文书给大伙儿详细介绍)。
最后值得注意的是 module 那句话后面也有分号“;”,这个很多人在最初做代码的时候容易忘掉。然后嘛,自然是语法错误,还比较难查。
对于如图 3 中的模块(这里还是抽象模型,不详细说明。工程中的一个实例就是半加器,当然也可以是其他单元),具有两个输入信号 in_1 和 in_2、两个输出信号 out1 和 out2。例 8 和例 9 就是两种不同的描述方法(请先忽略信号的类型,紧接着会唠叨得您老吐了为止的)。
图 3 抽象的双输入双输出端口
【例 8】具有端口定义部分的模块描述
module absctraction_module ( in_1, in_2, out_1, out_2 );
//Ports Description
//Inputs:
input in_1; //First input signal
input in_2; //Second input signal
//Outputs:
output reg out_1; //First output signal
output reg out_2; //Second output signal
//Module Operation
begin
module_behavior description;
end
endmodule;
【例 9】具有端口列表中完成定义的模块描述
module absctraction_module ( input in_1, input in_2, output reg out_1, output reg out_2 );
//Inputs:
// in_1: First input signal
//in_2: Second input signal
//Outputs:
// out_1: First output signal
// out_2: Second output signal
//Module Operation
begin
module_behavior description;
end
endmodule
这两个例子的有关模块定义是等效的,工程师可以根据自己的喜好选择。理论上,无需强行规定。如果有公司有规定,那是项目管理的需要而非语法的要求。
模块的例化主要问题是上层信号与模块端口的连接,这个有两个方法。第一种:按照模块端口列表的顺序,直接写上层信号。第二种,采用端口引用的方法,格式是:.port_name (signal_name),port_name 是模块端口的名称,signal_name 是上层信号的名称。如果采用第二种方法,姑且称为对点名,可以不考虑模块实现的端口列表的顺序。例 11 和例 12 分别用两种方法例化了例 10 中的模块,施主们参考。
【例 10】实数加法模块的抽象实现
module real_adder ( in_1, in_2, out_result );
//Ports Description
//Inputs:
input[7:0] in_1; //Operate number 1, 7 bits
input[7:0] in_2; // Operate number 2, 7 bits
//Outputs:
output reg[8:0] out_result; //Result = in_1 + in_2, 8 bits
//Module Operation
begin
module_behavior description; // Result = in_1 + in_2
end
endmodule
【例 11】复数加法模块:顺序引用连接(方法 1)
module real_adder ( real_1, real_2, image_1, image_2, real, image);
//Ports Description
//Inputs:
input[7:0] real_1; //Real part of operate number 1, 7 bits
input[7:0] real_2; // Real part of operate number 2, 7 bits
input[7:0] image_1; //Image part of operate number 1, 7 bits
input[7:0] image_2; //Image part of operate number 2, 7 bits
//Outputs:
output[8:0] real; //Real part of result, 8 bits
output[8:0] image; //Image part of result, 8 bits
//Module Implement
real_adder realpart_adder(real_1, real_2, real); //Real part operation
real_adder imagepart_adder(image_1, image_2, image); //Image part operation
//Module Operation
begin
module_behavior description; // Result = in_1 + in_2
end
endmodule
【例 12】复数加法模块:点名引用连接(方法 2)
module real_adder ( real_1, real_2, image_1, image_2, real, image);
//Ports Description
//Inputs:
input[7:0] real_1; //Real part of operate number 1, 7 bits
input[7:0] real_2; // Real part of operate number 2, 7 bits
input[7:0] image_1; //Image part of operate number 1, 7 bits
input[7:0] image_2; //Image part of operate number 2, 7 bits
//Outputs:
output[8:0] real; //Real part of result, 8 bits
output[8:0] image; //Image part of result, 8 bits
//Module Implement
real_adder realpart_adder(.in_1(real_1), .in_2(real_2), .out_result(real) ); //Real part operation
real_adder imagepart_adder(.out_result(image), .in_1(image_1), .in_2(image_2)); //Image part operation
//Module Operation
begin
module_behavior description; // Result = in_1 + in_2
end
endmodule
这正是:
“
圣贤也要求正名,老僧难免心不平。层次划分要正经,设计营运认分明。
四值常量皆有因,真实系统唯一零。随机应变变量名,连接有序模块心。
”
与非网原创内容,谢绝转载!
系列汇总: