HDL4SE:软件工程师学习Verilog语言
Posted 饶先宏
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HDL4SE:软件工程师学习Verilog语言相关的知识,希望对你有一定的参考价值。
3 数据类型与程序结构
上一次介绍了verilog语言中的词法结构,并给出了verilog词法的形式描述文件,可以通过flex工具生成词法分析程序。运行该程序,我们可以逐个读取源代码中的单词。当然,词法分析之前还有一个预处理过程,后面会给出预处理过程的实现代码。
学习一种计算机语言,我们在搞定单词表后,下一步关心的一个是底层的语言要素,就是这种语言描述什么样的数据类型和数据结构,如何描述,同时也关心这种语言的总体组织是什么样的,如何运行起来。软件工程师的第一个任务就是很得意地写个Hello, World!的程序,表示这种语言,我入门了,然后再苦苦折磨自己,陷入到语言的各种细节和各种技巧中去。
本节就介绍verilog语言中的数据类型和数结构,以及这种语言的程序结构,主程序是什么样子的,各个子程序如何调用,参数如何传输等等。后面还接着介绍如何用软件来描述它们,以便构造一个模拟运行平台。
3.1 从一个例子开始
让我们从一个例子开始,学习计算机语言是一个很奇怪的事情,按说把语言的语法手册和规范手册完整看一遍,应该是最有效的把,但是这样做往往记住了很多语言规则,还是不会编程序,最后连语法都忘记了。最好的学习办法就是做个实际的例子,然后通过看语法规范来补漏。
想象一下你拿到一个FPGA开发板,板上有一个FPGA,还有十个按钮,十个数码管(七段数字笔划加一个小数点),当然还有晶振电路之类来提供时钟信号和复位信号。开发板上已经将FPGA与按钮和数码管通过一个32位读写总线连接在一起。这个总线包括读写信号,读写地址,读写数据,写的时候还有一个字节使能,支持只写其中某几个字节。
按钮是否按下的信息可以由FPGA读某个地址,比如0xF0000000,返回的低10位值就是10个按钮是否按下的状态。数码管的控制也是每个数码管给一个地址,比如从地址0xF0000010到0xF0000019,每个字节地址对应一个数码管,往地址上写一个八位数,每一位代表该地址对应的数码管的对应段是否点亮,比如要显示一个0,就写个8’b00111111,要显示一个5,则写入8’b01101101,分别对应数码管中DPGFEDCBA八个显示单元的控制电平。
十位数码管:
我们要做的第一个Verilog应用就是做一个FPGA程序,来控制数码管上显示的内容。其中包括一个计数器,对时钟进行计数,控制数码管将计数值显示在数码管上。
计数器一开始是不动作的,在外部按第0个键时对计数器的值进行清零,按第1个键时停止计数,按第2个键开始计数,开始计数时计数值从当前值开始(如果多个键同时按下,则以序号小的为准)。
我们一下就引出几个问题:1.我们的Verilog应用要如何与FPGA板连接在一起,2.计数器如何设计,如何对时钟进行计数。3.计数器值到数码管的控制码之间的对应关系,4.按键状态以及计数值复位,当然还有更加基础的,计数值如何存储,所谓外部的数据如何读写。我们后面一个一个来解决。
3.2 Verilog程序的主程序入口
我们写c语言代码时,会写一个所谓的主函数:
int main(int argc, char * argv[])
{
printf("Hello, World!\\n");
return 0;
}
那么Verilog程序的主程序如何写呢?对应到c语言的函数,Verilog中是模块,主函数对应的就是与FPGA对外连接的所有I/O管脚的一个主模块。每个Verilog应用程序必须有一个主模块,主模块也称为顶层模块(top module)。前面描述的FPGA的主模块写成如下的verilog代码:
module main(wClk, nwReset,
wWrite, bWriteAddr,
bWiteData, bWriteMask,
wRead,
bReadAddr, bReadData);
input wClk, nwReset;
output wWrite;
output [31:0] bWriteAddr;
output [31:0] bWriteData;
output [3:0] bWriteMask;
output wRead;
output [31:0] bReadAddr;
input [31:0] bReadData;
endmodule
这段代码定义了一个所谓的模块,并引入一种简单的命名规则,名字以w,b,nw开始,分别表示一位高电平有效的信号,多位信号组合,和一位低电平有效的信号,后面是表示这个名称的英文,每个单词第一个字母大写,如果是缩写,就可以全部大写,或者把缩写当英文单词用,良好的命名规则是良好代码的基础。
模块定义从关键字module开始,后面跟着用户自己给模块取的名字,这个名字可以是前面词法中讲的simident或者escident。这里取main也不是主模块的意思,实际上每个FPGA开发工具或者ASIC的开发工具都能够让用户选择一个模块作为主模块。模块名称后面是一对括号括住的模块端口表,这里给出端口名字。所谓端口,可以理解位c语言函数中的参数,注意顺序是有意义的,有点象c语言中的函数原型定义了,只是类型不在这里定义。后面跟个分号,这样就声明了一个模块。紧接着声明各个端口的属性,包括输入输出,宽度等信息,用input表示这个端口时外部往模块中传信号,用output表示这个端口是模块往外部传信号,用inout表示这是双向传信号的端口。如果不给出宽度,则表示端口是一个一位信号,给宽度的办法是用一对方括号,中间有一对用:分开的整数常数。模块最后以一个endmodule结束。这是一个什么也不做的模块,后面我们会慢慢增加内容,完成前面说的功能。
这个main模块对外的接口有时钟信号wClk, 复位信号nwReset,这是从FPGA输入到主模块的。模块输出一组写信号:wWrite,bWriteAddr, bWriteData, bWriteMask。几个信号之间的关系及完成的功能是,wWrite为1时,往bWriteAddr地址上写一个32位值bWriteData,bWriteMask是写的时候的字节使能,4位对应32位中的四个八位,为1时表示写入,为0时表示不变。
该模块还提供了一组读信号,wRead, bReadAddr, bReadData。其功能是,wRead为1时,同时给出bReadAddr,下一个时钟周期bReadData就是bReadAddr地址上的值。
这个模块将是我们以后的模拟器的主模块,每个在我们的模拟器上运行的verilog应用程序都必须写这样原型的主模块。
在FPGA应用中,主模块描述的输入输出信号就是FPGA上各个管脚上的信号。FPGA开发工具有一个pin assignment的工具,就是将主模块的输入输出信号跟硬件管脚一一对应起来,并配置管脚上的输入输出特性,驱动特性,电压电流等令软件工程师看了头大的特性,幸好这辈子不是数字工程师啊,否则不被折腾到半死。
如果是ASIC设计,顶层模块的信号也同样对应到芯片管芯的输入输出脚信号,这个可能不是外部封装的管脚,毕竟从晶元到封装还有一次基板映射。当然ASIC开发平台中也有类似于pin assignment的动作。
PS.数字电路工程师看到这些个诸如应用程序,编译,主模块,主程序入口之类的叫法是不是很不爽啊,为了让软件工程师理解,先忍忍吧,名字而已,叫着叫着就习惯了。
3.3 数据类型
软件工程师写完主函数后,接着写实现功能的若干函数,然后再主函数中调用,完成需要的功能。我们先来写个计数器模块。
module counter
#(parameter WIDTH=4,
MAXVALUE=9, RESETVALUE=0)
(input wClk, nwReset, wCounterIt,
output [WIDTH-1:0] bCouter,
output wCounterOverflow);
endmodule
这个模块定义方式跟前面不一样了,在模块名称与端口表之间有个所谓的参数表,我们希望定义一个可以通用的计数器,也就是外部可以指定计数器的位数,以及计数的最大值,到达这个值之后就输出wCounterOverflow,计数值从头开始,还有计数的复位值,也就是复位信号有效时的初始化值以及到达最大值后的从头开始的值。参数表以#开始,被一对括号括住,中间是多个参数,每个参数格式可以只给个名称,也可以是 名称=常数表达式 的格式,参数之间用逗号隔开,一个模块可以有多个参数表。如果外面使用模块时不给出参数,则参数采用常数表达式作为默认值。注意,参数在模块内可以按照常数对待,因为它在编译时的值是已知的。模块参数化的设计有点象c++中的模板,模板也是参数化的设计方式。参数还可以在模块中定义,后面的章节中会讲到verilog中如何定义模块参数及其他类型参数。考虑到在端口表中就可能用到参数(比如端口信号宽度),所以将参数表紧接着模块名称后面放是合理的,类似下面c++的模板函数定义,这个放得更前,放在函数返回类型和名称前面去了:
template <class T> void swap(T& a, T& b){}
后面的端口表也有变化,可以在端口表中指定端口的类型和宽度了,这个是不是有点象早期的c语言 到c99的变化,软件工程师细品一下。
这里还是定义了一个空的模块,输入wClk和nwReset,在每个时钟周期进行计数,nwReset有效时则进行复位。输入的wConterIt信号则表示每个周期是否要计数,输出bCounter是当前的计数值,wCouterOverflow则表示计数值是否达到指定的最大值,这个信号可以作为下一级计数器wCounterIt的输入,这样多个计数器就可以级联起来用了。
计数器内部必须记住状态,就是当前的计数值,这个计数值在c语言中用个整型变量来表示,在verilog中如何表示呢,我们先暂停一下编程的冲动,来看看verilog中的数据类型和数据结构,然后接着编这个模块。
3.3.1 数据的取值
前面也提到过,verilog语言中,数据是以位为基本单位的,每一位的状态有四种:0,1,x,z。其中0和1是标准的布尔量,它们参与运算按照布尔代数的规则做就是了。x表示状态不确定,或者不关心,无所谓,它参与计算,结果总是x。z表示没有值,在一根没有连接任何输入信号的电缆上量信号,结果是噪声,是没有意义的,所以一旦参与计算,跟x效果相同,它存在的意义主要是描述线缆的状态。
3.3.2 线网
我们前面讲过,所谓电路,其实就是用线缆连接在一起的一些电路单元。其中电路单元我们用module来表示,在本节后面再详细介绍,线缆在verilog中则用所谓线网这种数据类型来表达。
线网有如下几种:supply0,supply1,tri,triand,trior,tri0,tri1,uwire,wire,wand,wor。线网变量声明时要给出它的类型,可选择给出延时,驱动强度(上下拉,弱上下拉,强上下拉),充电强度(小中大),还可以给出一个向量化的范围,表示这是一根多根电缆捆在一起的多芯线缆,最后是一系列标识符,表示线网的名称。
比如下面的声明:
wire a;//一位wire线缆
tried [15:0] address,data;//两根16芯线缆
作为软件工程师,我们不关心电缆的物理属性,这已经涉及到开关级描述了,不属于RTL范畴。比如什么驱动强度,什么充电强度,什么延时,阻抗匹配啊,是不是虚焊之类,软件工程师总是假定连上的就是好的,因此在后面的模拟器中不对这些属性进行模拟。
这些属性主要是影响两根电缆连接在一起后,上面的信号是如何的,比如一个是强上拉,一个是弱下拉,连在一起应该是上拉的效果,一个是强驱动,一个是弱驱动,连在一起结果是强驱动的输出有效。一根连接到多个输出的电缆,如果同时有多个输出电缆上的信号就会很复杂,如果只有一个输出,其他都是高阻态(不输出),线缆上的值就是这个输出的值。后面会给出线网的软件模型。关于线网的详细特征,这里就不再详细讲,以后我们只支持一种线网wire,如果verilog中声明了其他线网,编译器就报个警告,然后放一边不管。
3.3.3 变量与寄存器
除了线网之外,verilog支持integer,real,realtime,reg,和time数据类型,相应的声明方式就是类型后面跟一系列标识符。
对于reg类型,还可以声明是否是带符号的,以及位宽度(范围),比如:
reg r;//一位寄存器
reg [-1:4] b;//6位寄存器
reg signed [3:0] sreg,breg;//两个4位寄存器
integer i; //一个整数
verilog规定线网和reg的位宽可以由实现来给定最大值,但是这个最大值不得小于65536。
实际上,在RTL可综合描述中,一般都不支持除了reg之外的变量类型。所以后面的模拟器中我们只考虑reg类型的变量,其他类型要么不支持,要么就转换成reg。
所谓RTL可综合,就是编译器要能够把描述编译成一个对应的电路,其实是不是可综合(可编译成电路),只要关注两个问题,一个是描述的电路能否在时钟周期内给出稳定有效的信号,一个是因为电路时并发运行的,因此不能包括某种按顺序执行的隐形假定在里边,也就是c语言中可能的顺序,跳转,循环,分支等概念都必须严格审查,必须保证能够生成顺序无关的执行单元才行。这点目前看比较抽象,我们后面会随时解释。
3.3.4 数组
verilog语言中支持声明线网和变量的数组,声明办法是在标识符后面加数组的下标范围。数组的最大大小也由实现来指定,但是不得少于2^24,16 777 216。比如:
reg xx[11:0];//12个一位信号数组
reg [31:0] regs[127:0];128个32位寄存器
数组可以是多维的:
wire w_array[7:0][4:0];//二维数组。
对数组的操作,只能对其中的某个成员进行操作,因为verilog没有类似c中指针的类型,所以无法同时操作多个数组成员。
然而对一个变量中的多位线网或寄存器操作是允许的,比如声明
reg[31:0] areg;
那么areg[12:7]是一个合法的操作数也是一个合法的赋值对象。这点后面的verilog表达式与赋值介绍中会进行更详细的介绍。
3.3.5 计数器模块的实现
我们接着来看计数器模块的实现。注意verilog没有全局变量的概念,因为主模块也就是顶层模块已经是应用程序能访问的最外层结构了,从内部已经不能直接访问顶层模块外面了,这个不像c语言,在main函数之外还可以定义东西,每个函数内部可以访问全局的数据。verilog中每个模块只能引用自己定义的变量。变量定义在模块的端口描述到endmodule之间,为了更好地看到代码的解释,我们把代码拆成一段一段地放出来,中间夹杂着解释。
module counter
#(parameter WIDTH=4, MAXVALUE=9, RESETVALUE=0)
(input wClk, nwReset, wCounterIt,
output [WIDTH-1:0] bCouter,
output wCounterOverflow);
下面声明一个WIDTH宽度的寄存器用来保存计数器的值:
reg [WIDTH-1:0] bCurrentCounter;
下面声明一个一位寄存器来保存计数器是否溢出,作为时序电路模块的输出,从寄存器输出是一个比较好的选择,可以使用该模的模块的压力,毕竟每个组合电路本质上都是从寄存器到寄存器之间必须建立稳定信号的,我们的模拟器模拟的FPGA保证每个输入信号都是用寄存器输出的,内部逻辑不用缓存到寄存器中即可用于组合电路中:
reg wOverflow;
定义两个与输出端口同名同宽度的线网,这样声明之后,对这些线网的操作等价于对端口的操作:
wire [WIDTH-1:0] bCounter;
wire wCounterOverflow;
将输出线网与输出寄存器连在一起,当然这里也可以直接将输出定义为寄存器,不需要用线网重新中转,不过内部不直接操作输出端口,也是个好习惯的,对端口,还是要敬而远之,尽可能少操作它们。这种赋值方式称为持续性赋值,后面会讲到。编译后的电路上就是将线网与某个寄存器或者线网连接在一起。
assign bCounter = bCurrentCounter;
assign wCounterOverflow = wOverflow;
下面这个语句称为always块,一个模块中可以有多个always块,格式是always开始,后面跟个所谓的事件描述,比如时间延迟时间,时钟上沿事件等,然后跟一个语句(当个居于或者begin end括起来的复合语句, begin end对应到c语言即是{ },个人还是比较喜欢{ } 一些。意思是一个死循环,等到事件发生时就执行后面的语句,执行完后再等这个事件,这里的事件是@(posedge wClk,表示在时钟信号wClk的上升沿,就是从低电平到高电平转换的时刻:
always @(posedge wClk) begin
if (~nwReset) begin
如果复位信号(低电平)有效,则给出寄存器的初始值,这里用所谓非阻塞赋值,表示被赋值的对象是一个时钟沿锁存的寄存器,就是在时钟沿上把后面的表达式表示的组合电路的采样值锁存到寄存器中。这样的描述编译后生成一个寄存器写的动作,寄存器也确实能够记住状态了。有关表达式与赋值的详细情况,后面的章节中会继续介绍。
这里有个比较费解的问题,软件工程师一开始会有点难以接受,就是这个非阻塞赋值其实并没有让寄存器的输出变成表达式的值,也就是说如果这个时刻访问寄存器,得到的值是上个周期的值。这是因为在时钟上沿,表达式的值还没有开始形成,注意组合电路是有延迟的,在时钟上沿,每个寄存器锁存了值后,表达式(组合电路)才能在寄存器锁存完成后的某个时刻(寄存器输出信号建立时间)得到寄存器的输出,作为表达式的输入,然后表达式表示的组合电路经过某个建立时间之后,才能给出有效的值,在下个时钟上沿到达之前所有组合电路会稳定,因此组合电路在时钟上沿时的值其实时根据上个时钟周期寄存器输出的值计算出来的。
bCurrentCounter <= RESETVALUE;
wOverflow <= 1’b0;
end else begin
/*复位信号无效的情况,开始计数操作 */
if (wCounterIt) begin
if (bCurrentCounter == MAXVALUE) begin
到达计数器最大值,则重新设置为复位值,这里参数设置时要注意计数器宽度能表达的值包括最大值,否则计数永远到不了最大值,这样就无法达到设计要求了
bCurrentCounter <= RESETVALUE;
wOverflow <= 1’b1;
end else begin
计数器增加一,注意到这是所谓的非阻塞赋值,也就是赋值符号右边的表达式中bCurrentCounter是上个周期锁存后输出的值,赋予的值将在下个周期时钟信号上沿到达之前才会更新
bCurrentCounter <= bCurrentCounter + 1;
wOverflow <= 1’b0;
end
end
end
end
endmodule
代码拆成一段段的,太难看了,本来想把中间的说明用注释方式写出来的,但是写在代码中CSDN不能指定自动换行(是我没学会么?CSDN高手请留言指教),这样看的时侯得滑动着看注释,也挺难受的,可以尝试着手工换行,把每行搞得短一点,但是到底多宽,还是不好确定,长了手机用户不满意,短了电脑用户不满意。
这里还是忍不住把前面的代码收集在一起,没办法,看着代码不好看,很不舒服,是不是有点强迫症啊,反正又不是写网络小说,不算凑字数吧:
module counter
#(parameter WIDTH=4, MAXVALUE=9, RESETVALUE=0)
(input wClk, nwReset, wCounterIt,
output [WIDTH-1:0] bCouter,
output wCounterOverflow);
/*WIDTH宽度的寄存器用来保存计数器的值*/
reg [WIDTH-1:0] bCurrentCounter;
/*定义一个寄存器来表示计数器是否溢出*/
reg wOverflow;
wire [WIDTH-1:0] bCounter;
wire wCounterOverflow;
/*输出线网直接连接在寄存器上*/
assign bCounter = bCurrentCounter;
assign wCounterOverflow = wOverflow;
always @(posedge wClk) begin
if (~nwReset) begin /*复位处理*/
bCurrentCounter <= RESETVALUE;
wOverflow <= 1’b0;
end else begin
/*复位信号无效的情况,开始计数操作 */
if (wCounterIt) begin
if (bCurrentCounter == MAXVALUE) begin
bCurrentCounter <= RESETVALUE;
wOverflow <= 1’b1;
end else begin
bCurrentCounter <= bCurrentCounter + 1;
wOverflow <= 1’b0;
end
end /*wCounterIt*/
end /*nwReset*/
end /*always*/
endmodule
这个看着舒服多了。有关表达式与赋值,always块的更多的内容,后面会做更详细的介绍,这里先囫囵吞枣好了,不理解也不要纠结,你只要确定的一件事情是,我们完成了计数器模块。
3.4 模块与程序结构
前面提前演示了module的定义,verilog中的程序结构以模块为基础,所有模块的声明和定义都在同一个层次(不会在一个模块中定义另一个模块,这个跟c语言不会在一个函数中定义另一个函数类似,在pascal语言中是可以在一个过程中定义子过程的),这里先看看模块定义的详细语法说明,劳逸结合一下,然后再接着讨论我们的verilog应用。
3.4.1 模块
按照IEEE 1364-2005,模块的定义语法(BNF格式)如下:
/*A.1.2 这里表示在IEEE 1364-2005中的章节号,以便对照学习*/
module_declaration ::=
{ attribute_instance } module_keyword module_identifier
[ module_parameter_port_list ] list_of_ports ;
{ module_item } endmodule
| { attribute_instance } module_keyword module_identifier
[ module_parameter_port_list ] [ list_of_port_declarations ] ;
{ non_port_module_item } endmodule
module_keyword ::= module | macromodule
其中的attribute_instance定义如下:
/*A.9.1*/
attribute_instance ::= (* attr_spec { , attr_spec } *)
attr_spec ::= attr_name [ = constant_expression ]
attr_name ::= identifier
为每个需要额外信息的对象提供附加信息,主要是为编译器或者仿真工具提供一些附加信息,BNF语法中用花括号括住,表示可以给一组或者多组属性,当然也可以不给出。1364规范中没有定义具体的附加信息,只是给出了如果要传达附加信息,应该遵循的格式。这样,如果你工作在某个FPGA开发平台或者ASIC开发平台上,可能要了解对应开发平台上定义的这些附加信息。可以理解为这些附加信息其实是为每个开发平台提供了一些verilog语言扩展的可能。看来开发平台很强势啊,verilog规范制定者都必须为他们预留语言扩展的办法才行,当然也许是几个开发平台参与verilog语言规范制定妥协下来的结果。这种做法可能会降低verilog代码的通用性,但是在性能或者其他方面得到提升,也可以用来实现一些特别的功能。比如我们用LCOM框架实现模拟器时,可以定义某些模块是模拟器预定义模块,这些模块甚至不是用verilog写的,而是用c语言直接写的。为了能够在用户的verilog代码中使用这些预定义的模块,我们可以写一个空的模块,使用指定的名称,并用attribute_instance的方式给出模块实现的LCOM CLSID编号,以便编译器连接该模块时,就可以根据CLSID直接生成LCOM对象。这在FPGA开发平台中也是经常用的,比如预定义的RAM,FIFO或者DSP单元,都是在库里有个空的模块,然后连接的时候换成相应的FPGA或ASIC工艺库中的单元。
后面的参数表是可选项,定义如下:
/* A.1.3 */
module_parameter_port_list ::= # ( parameter_declaration
{ , parameter_declaration } )
/* A.2.1.1*/
parameter_declaration ::= parameter [ signed ] [ range ]
list_of_param_assignments
| parameter parameter_type list_of_param_assignments
在后面是端口表
list_of_ports ::= ( port { , port } )
list_of_port_declarations ::= ( port_declaration { ,
port_declaration } )
| ( ) port ::= [ port_expression ]
| . port_identifier ( [ port_expression ] )
port_expression ::= port_reference | { port_reference {
, port_reference } }
port_reference ::= port_identifier [ [
constant_range_expression ] ]
port_declaration ::= {attribute_instance} inout_declaration
| {attribute_instance} input_declaration
| {attribute_instance} output_declaration
后面的module item才是组成module实现的主体,我们先看看包括些什么内容,后面章节中会进行详细介绍:
/*A.1.4*/
module_item ::= port_declaration ; |
non_port_module_item
module_or_generate_item ::=
{ attribute_instance } module_or_generate_item_declaration
| { attribute_instance } local_parameter_declaration ;
| { attribute_instance } parameter_override
| { attribute_instance } continuous_assign
| { attribute_instance } gate_instantiation
|{ attribute_instance } udp_instantiation
|{ attribute_instance } module_instantiation
| { attribute_instance }initial_construct
| { attribute_instance } always_construct
| { attribute_instance } loop_generate_construct
| { attribute_instance } conditional_generate_construct
non_port_module_item ::= module_or_generate_item
| generate_region
| specify_block
| { attribute_instance } parameter_declaration ;
所有的这些语法结构最终目标都是用合适的方法描述电路单元,以及单元之间的连接。我们会在后面的描述中做详细介绍,并特别关注每种语法结构能否编译成实际的电路,如果能够编译成实际的电路,讨论如何编译成实际的电路。
3.4.2 模块实例化
verilog语言中一个模块中可以使用另外一个模块的实例,因此verilog 中的程序结构实际上是一个树状的层次结构。这点与c++中的类有点像,一个类中可以声明另一个类的实例作为成员变量,形成一个类的层次结构。
声明模块实例的具体办法是,在前面的module_item中,使用module_instantiation类型的item。具体的格式是先是一个模块名称,然后是可选的实例化参数表,实例名称,以及端口连接表。子模块的输入输出可以与模块中声明的线网或寄存器连接在一起,如果子模块带参数,可以为实例指定参数。具体的语法如下:
/* A.4.1 Module instantiation*/
module_instantiation ::= module_identifier [ parameter_value_assignment ]
module_instance { ,module_instance } ;
parameter_value_assignment ::= # ( list_of_parameter_assignments )
list_of_parameter_assignments ::= ordered_parameter_assignment
{ , ordered_parameter_assignment }
| named_parameter_assignment { , named_parameter_assignment }
ordered_parameter_assignment ::= expression
named_parameter_assignment ::= . parameter_identifier ( [ mintypmax_expression ] )
module_instance ::= name_of_module_instance ( [ list_of_port_connections ] )
name_of_module_instance ::= module_instance_identifier [ range ]
list_of_port_connections ::= ordered_port_connection { , ordered_port_connection }
| named_port_connection { , named_port_connection }
ordered_port_connection ::= { attribute_instance } [ expression ]
named_port_connection ::= { attribute_instance } . port_identifier ( [ expression ] )
比如,我们下面的代码在main模块中声明10个counter模块的实例,对应10个计数器,十个计数器是级联在一起的,对时钟信号构成10个十进制的计数器。请仔细看代码中的注释,会结合代码给出较为详细的解释。为了照顾手机上的阅读体验,这里尝试手工进行了宽度限制,不过这样做会影响电脑上的阅读体验。
module main(wClk, nwReset,
wWrite, bWriteAddr, bWiteData, bWriteMask,
wRead, bReadAddr, bReadData);
input wClk, nwReset;
output wWrite;
output [31:0] bWriteAddr;
output [31:0] bWriteData;
output [3:0] bWriteMask;
output wRead;
output [31:0] bReadAddr;
input [31:0] bReadData;
/*声明与读端口的名称一样的线网,可以将读端口连接
在线网上*/
wire [31:0] bReadAddr;
wire [31:0]bReadData;
wire wRead;
wire wButton0Pressed;
wire wButton1Pressed;
wire wButton2Pressed;
/*我们一直在读按键的状态*/
assign wRead = 1’b1;
assign bReadAddr = 32’hF000_0000;
/*将bReadData[0]…bReadData[2]换个有
物理意义的名字的线网,这么做纯粹是为了
增加代码的可读性,也是为了如果硬件上修
改了三个button的位置或者信号来源,整个
代码只要改这几行就够了,后面的计数器修改
代码不用修改,编译器会处理这种别名式的命
名和赋值,不会影响最终生成的电路 */
assign wButton0Pressed = bReadData[0];
assign wButton1Pressed = bReadData[1];
assign wButton2Pressed = bReadData[2];
/*声明十个线网,作为本级计数器是否计数
的输入,第0级根据按键状态生成,其他的
由前级的溢出输出生成*/
assign wCounterin0 = wCounterIt;
wire wCountin0, wCountin1, wCountin2,
wCountin3, wCountin4, wCountin5,
wCountin6, wCountin7, wCountin8,
wCountin9;
/*实例化参数和端口按照模块定义时的顺序
匹配,此时必须保证顺序正确,而且不能缺
少中间的某个参数,可以缺少最后的几个,
有点象c++中的参数默认值*/
counter #(4,9,0) counter0(wClk, nwCounterReset,
wCounterin0, bCounter0, wCounterin1);
counter #(4,9,0) counter1(wClk, nwCounterReset,
wCounterin1, bCounter1, wCounterin2);
counter #(4,9,0) counter2(wClk, nwCounterReset,
wCounterin2, bCounter2, wCounterin3);
counter #(4,9,0) counter3(wClk, nwCounterReset,
wCounterin3, bCounter3, wCounterin4);
counter #(4,9,0) counter4(wClk, nwCounterReset,
wCounterin4, bCounter4, wCounterin5);
counter #(4,9,0) counter5(wClk, nwCounterReset,
wCounterin5, bCounter5, wCounterin6);
counter #(4,9,0) counter6(wClk, nwCounterReset,
wCounterin6, bCounter6, wCounterin7);
counter #(4,9,0) counter7(wClk, nwCounterReset,
wCounterin7, bCounter7, wCounterin8);
/*不给出参数表的,使用模型定义时指定的
默认参数,如果模型没有指定默认参数,这
里必须给出参数*/
counter counter8(wClk, nwCounterReset,
wCounterin8, bCounter8, wCounterin9);
/* 实例counter9的声明演示了另外一种声
明方式,实例化参数可以给出名字, 这样可
以不按照顺序,并可以只设置其中几个,没
有设置的用默认参数。端口匹配也可以用名
字匹配,这样代码可读性要好,并且有些端
口可以不连接到线网中,比如counter9的溢
出信号,外部不用,就不必要连接到外面的
线网上了。如果是输入端口,建议都应该连
接一个线网,以确保模块中能够得到正确的
值 */
counter #(RESETVALUE=0, WIDTH=4) counter9(
.wClk(wClk), .nwReset(nwCounterReset),
.wCounteit(wCounterin9), .bCounter(bCounter9),
.wConteroverflow以上是关于HDL4SE:软件工程师学习Verilog语言的主要内容,如果未能解决你的问题,请参考以下文章