首先我们不开始讲Verilog HDL的语法,我们从Verilog HDL的程序结构出发。相信大家都看过芯片吧,它有个名字,有个外壳,外壳向外伸出有引脚(BGA封装的那种请不要乱搅和...),然后芯片它可以实现一定的功能。
Ok,知道这些之后,我们就来看看Verilog HDL的描述数字电路的程序结构吧。
在解释结构的时候,我拿芯片设计这个例子来打一个不恰当的比方。
VerilogHDL程序的大致结构就是这么一个形式,每一个模块的结构都是一致的只不过语句之间存在一些差别,每一部分的具体描述将在后面进行。
二、verilog程序结构的简单描述
OK,现在让我们现在开始了解一下一些语法和注意事项(注意,我这里不会把语法讲得很细很细,我主要是描述一些(我,或者初学者)易忘的,关键的语法和知识点),描述的顺序不一定按照上面的各个部分哟,我们先描述一些VerilogHDL程序必须的。
首先是模块说明:
module 和 endmodule ,这两个关键词成对出现,一般的内容都会囊括在这两个两个关键词之间。一个verilog(.v)文件可以有多个module ...endmodule,但是为了方便管理,建议只实现一个。
此外无论是能够综合成电路的verilog程序还是只是仿真的程序,都需要以模块的形式给出。
模块名:
①模块名的定义要符合标识符的定义,至于什么是标识符,以后会说的。此外也要注意书写的规范性。
端口说明:
①端口可以比方成芯片的输入输入管脚,它只有三种类型:输入,输出,双向;
②端口有一些附加属性,如数据类型、符号特性、位宽等;input端口只能是线网类型,output可以是寄存器类型也可以是线网类型,inout也只能是线网类型。至于线网类型和寄存器类型,在以后的数据类型中会介绍。
③当端口是总线类型时,可以简单地理解为端口有多位时,对应于同时描述芯片的多个管脚时,可以给端口加上位宽。即input wire [M:N] a;如果M>N,则为降序,a[M]为最高位,位宽为M-N+1位;如果M<N,则最高位位a[N]位,位宽位N-M+1位;
④端口的属性声明顺序可以是:
端口的输入输出 数据类型 有无符号 位宽 端口名称 ;
如:output reg signed [7:0] a;
注:数据类型没有写则默认为wire 型,有无符号没有写默认为无符号型;位宽没有写默认为1bit 。
⑤当书写完端口声明是,括号后面的‘;’千万不要忘记。
程序主体:
①前面我们提及到verilog的描述层次中有一个结构化描述,结构化的描述就是一点一点地例化(子模块、单元门)来实现系统/设计的功能。程序的主体可以是只例化一些门单元,但是verilog的这种纯结构化方式的显然是无法满足现代设计要求的,因此纯结构化方式已经被遗弃。
②程序的主体可以是行为描述,也就是通过一些verilog的行为语句来实现设计的要求/功能。行为描述主要有:控制流描述,过程描述,块语句,时序控制等。对于行为描述,有可综合的,有不可综合的,可综合的行为描述语句/语法就可以认为是RTL级描述时用到的,而不可综合的行为描述语句/语法可以认为是仿真/建模时用到的。
③下面大概说一下行为描述情况,也就是行为描述中无论是可综合还是不可综合都可以使用的情况,具体的可综合和不可综合的行为级描述语法在后面会描述。
控制流描述:以assign关键字开头的语句描述称为控制流描述,主要用来实现一些简单的组合逻辑。
块语句:包含在关键字begin...end 、fork ...join之间的语句称为块语句。
过程描述:由initial模块、always模块、function模块、task模块这四个模块实现的过程。
时序控制:延时控制、敏感信号控制语句等
④块语句可以有一个名字,写在begin/fork后面(如beign:adder_disc ),给它一个名字有什么好处呢?好处有两个,一个是可以在块内部定义寄存器变量(注意哦,只能寄存器变量,这个寄存器变量在块内部使用),另外一个就是可以用disable这个关键字来中断语句块的执行(具体怎么弄,请参看其他书籍,这里不详述)
⑤begin...end内的语句是串行的,这是从语法的结果上讲的,但是它是可以综合的,综合出来的实际电路是并行的,也就是实际电路中,各条语句之间并不全是串行的,这里需要建立一个概念,方便讲解以后的阻塞赋值和非阻塞赋值。而fork...join 内的语句是同时进行的,然而这是不可综合的。
⑥一个程序的主体中可以有多个initial模块、always模块、function模块、task模块。Function模块和task模块以后介绍。多个Initial模块、always模块之间是并行的,但是initial只执行一次,而always是反复执行。
initial一般是不可综合的,用在仿真当中;在进行仿真时,通常被用来描述测试模块的初始化、监视、波形生成等功能。
always用在可综合的描述当中,一般情况下由敏感列表触发(至于什么是敏感列表,敏感列表怎么用,请参考其他书籍,我后面也会做一些记载),也就是用在描述RTL级的描述当中,通常被用来对硬件的功能进行描述,可以实现组合逻辑和实现逻辑的功能。
上面没有给出initial格式和always格式只是提了一下简单应用。
⑦程序的主体大多可以由结构描述和行为描述构成
模块的例化:
①模块的例化主要是为实现总体的功能,比如说我要设计一块芯片,这个芯片的某个功能可以有某个芯片来完成,那我就可以调用这个芯片,例化就是调用这么一个概念。
②例化可以使用位置映射,也可使用名称映射,由于位置映射可读性太差,容易出错,所有建议使用名称映射。下面是名称映射的格式:
被调用(例化)的模块名字 自定义的例化模块名(
.被调用(例化)模块的端口名 (本模块中的线网变量名),
...
.被调用(例化)模块的端口名 (本模块中的线网变量名)
);
③悬空处理:
在模块例化时,如果被例化的模块输入管脚悬空,则该输入为高阻Z,相当于与外界隔绝,但是实际电路中...
如果将输出管脚悬空,则该输出管脚将被废弃掉。
④不同端口位宽处理
当模块例化端口和被例化的端口位宽不同时,端口通过无符号数的右对齐截断方式进行匹配。举个例子说:
参数定义与映射:
①参数定义就是parameter 那一部分了,也就是可以用一个标识符表示一个固定的参数,当然`define这种宏定义也可以实现这种功能,这样子提高程序的可维护性和可读性。
使用`define时语法为:`define A 2'b10 或者`define A 2.
一般把`define宏定义语句放在模块最前面,并且要注意,无论是在子模块还是顶层模块中,A 的值都代表2(调用时为 `A)(全局参数)。
使用parameter定义的语法为 parameter A = 2或者parameter A= 2’d2;
一般放在模块名字和模块端口列表之间:模块名 #(parameter A = XX,.....)(端口列表);使用parameter定义的参数只在当前模块中有用(局部参数)
localparam定义时,上层模块不能调用传递参数。
②参数传递:
参数传递就是在编译或者仿真的时候,进行维护时,对参数空闲重新复制而更改其值。传递的参数是子模块中定义的parameter,传递的方法有两种:
(1)使用’#’符号:在同一模块中使用’#’符号,参数赋值顺序必须与原始模块定义的顺序相同,并不是一定要给所以的参数赋值,但是不允许跳过任何一个参数,即使是保持不变的值也要写在相应的位置。说了这么一大坨,还是举个例子吧:
这样子,A_WITH就是x1,B_WITH不改变,但是要写在相应的位置。
(2)使用defparam关键字
直接上格式: