HDL4SE:软件工程师学习Verilog语言

Posted 饶先宏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HDL4SE:软件工程师学习Verilog语言相关的知识,希望对你有一定的参考价值。

9 Verilog中的层次化结构

前面已经看到,Verilog支持对电路的层次化描述,具体的办法是通过模块中实例化其他模块,形成一个层次化的树状结构,树的根就是顶层模块,也就是一个verilog应用的主模块。FPGA的主模块对应的是FPGA芯片的外部I/O结构,一般是可以配置I/O属性的。ASIC的主模块则对应ASIC管芯的I/O结构。
子模块实例化时可以指定实例化参数,并通过端口与模块中的其他子模块或者线网连接,模块中的其他描述结构最终都编译为基本单元的实例化以及它们之间的连接。

9.1 module及module实例化

模块的定义以module开始,endmodule结束,也可以用macromodule开始,规范中没有给出它们之间的区别,只说由实现来定义,使用时一般可以不加区分地使用,当然如果有的开发工具有特别的说法,应该参考它的手册。
module前面可以有一个attribute列表,我们的HDL4SE的基本单元就是将基本单元的CLSID和软件库信息放在这里。module后面跟一个标识符,作为模块的名称,再后面是可选的实例化参数表,作为模块实例化时的参数表,然后是一个可选的端口表。一般而言端口表是需要出现的,没有任何端口的话,模块有用吗?(可能在仿真意义上有用,实际电路中不引出任何I/O信号的电路是没有意义的,当然电源模块之类除外)。后面就是一系列的模块内部元素,可以包括线网寄存器变量声明,持续性赋值,子模块实例化,always块,initial块等。
端口表定义时,可以只给出一个名字,此时端口的定义是不完整的,在module中还需要给出端口的属性。比如:

module test(a,b,c,d,e,f,g,h);
input [7:0] a; // 指定一个无符号的线网输入端口
input [7:0] b;
input signed [7:0] c;
input signed [7:0] d; // 指定带符号的线网输入端口
output [7:0] e; // 指定无符号线网输出端口
output [7:0] f;
output signed [7:0] g;
output signed [7:0] h; // 指定带符号线网输出端口
wire signed [7:0] b; // 端口b指定为带符号线网,此时宽度必须一致
wire [7:0] c; // 端口c按前面指定为带符号的
reg signed [7:0] f; // 端口f指定为带符号reg类型
reg [7:0] g; // 端口g指定为带符号reg
endmodule

子模块实例化时,一般应该给出实例化参数和端口连接表。这两个部分格式有点象,可以有两种方式给出,一种是按照顺序给出,不需要给出内部参数/端口的名字,按照顺序给出实例化参数/端口对应的实参的表达式即可。比如:

(* 
   HDL4SE="LCOM", 
   CLSID="D5152459-6798-49C8-8376-21EBE8A9EE3C",
   softmodule="hdl4se" 
*) 
module hdl4se_split4
    #(parameter INPUTWIDTH=32, 
      OUTPUTWIDTH0=8, OUTPUTFROM0=0, 
      OUTPUTWIDTH1=8, OUTPUTFROM1=8,
      OUTPUTWIDTH2=8, OUTPUTFROM2=16, 
      OUTPUTWIDTH3=8, OUTPUTFROM3=24
    )
    (
      input [INPUTWIDTH-1:0] wirein,
      output [OUTPUTWIDTH0-1:0] wireout0,
      output [OUTPUTWIDTH1-1:0] wireout1,
      output [OUTPUTWIDTH2-1:0] wireout2,
      output [OUTPUTWIDTH3-1:0] wireout3
    );
  wire [INPUTWIDTH-1:0] wirein;
  wire [OUTPUTWIDTH0-1:0] wireout0;
  wire [OUTPUTWIDTH1-1:0] wireout1;
  wire [OUTPUTWIDTH2-1:0] wireout2;
  wire [OUTPUTWIDTH3-1:0] wireout3;
  assign wireout0 = wirein[OUTPUTWIDTH0+OUTPUTFROM0-1:OUTPUTFROM0];
  assign wireout1 = wirein[OUTPUTWIDTH1+OUTPUTFROM1-1:OUTPUTFROM1];
  assign wireout2 = wirein[OUTPUTWIDTH2+OUTPUTFROM2-1:OUTPUTFROM2];
  assign wireout3 = wirein[OUTPUTWIDTH3+OUTPUTFROM3-1:OUTPUTFROM3];
endmodule

是我们的HDL4SE基本单元hdl4se_split4的模块定义,当然实际实现时这个模块是用c语言的LCOM对象实现的。
实例化时可以用按照顺序给出参数或端口。比如:

hdl4se_split4
     #(32,1,0,1,1,1,2,1,3)
  bReadData_wButton012(
      bReadData,
      wButton0Pressed,
      wButton1Pressed,
      wButton2Pressed,
      wnouse
    );

也可以按照名称给出参数或端口连接:

hdl4se_split4
     #(.INPUTWIDTH(32),
       .OUTPUTWIDTH0(1), .OUTPUTFROM0(0), 
       .OUTPUTWIDTH1(1), .OUTPUTFROM1(1),
       .OUTPUTWIDTH2(1), .OUTPUTFROM2(2), 
       .OUTPUTWIDTH3(1), .OUTPUTFROM3(3)
     ) 
  bReadData_wButton012(
      .wirein(bReadData),
      .wireout0(wButton0Pressed),
      .wireout1(wButton1Pressed),
      .wireout2(wButton2Pressed),
      .wireout3(wnouse)
    );

按照顺序给出时,中间不能缺某个参数或连接,按照名称给出时,顺序可以不按声明的给,如果缺了某个参数,就取模块定义时给的默认值,如果缺了某个端口连接表达式,则表示该端口没有连接(如果是输入端口,表示该端口处于一种悬空状态,输入值是不确定的,这与高阻态有点类似了)。
同一个表(参数表或端口连接表)中只能选择一种方式,按顺序或按名字不能混着用。

9.2 实例化参数

模块在定义时可以定义参数表,每个参数还可以给出默认值,如果实例化时不给出参数的值,则取默认值。参数可以在模块定义开始的参数表中定义,也可以在模块中用parameter开始的语句定义。
模块内部还可以定义局部参数,用localparam开始,局部参数的初始化可以初始化为常数表达式。所谓的常数表达式,是指表达式中可以出现常数值或参数或其他局部参数,当然局部参数不能相互引用。
局部参数和参数的区别在于,参数可以在实例化时重新指定,局部参数则不行,局部参数更像一种内部为了方便使用的表达式,但是局部参数可以出现在常数表达式中。
实例化时子模块的参数有两种办法指定:一种是在实例化参数表中指定,另一种是在模块中用defparam语句指定。如果对同一个参数用了两种指定方法,则以defparam语句的优先。因此模块实例中的参数取值的优先顺序是:defparam指定的表达式,实例化参数表中指定的参数,模块定义中的默认参数。如果模块中定义参数时没有指定类型或位宽,则以指定的为准,如果定义时指定了类型或位宽,则外部指定参数转换为内部的类型和位宽。比如:

module foo(a,b);
 real r1,r2;
 parameter [2:0] A = 3'h2;
 parameter B = 3'h2;
 initial begin
 r1 = A;
 r2 = B;
 $display("r1 is %f r2 is %f",r1,r2);
 end
endmodule // foo
module bar;
 wire a,b;
 defparam f1.A = 5.1415;
 defparam f1.B = 5.1415;
 foo f1(a,b);
endmodule // bar

模块foo中的参数A是指定了位宽的,因此defparam f1.A=5.1415在实例f1中5.1415转换为整数5,然后低两位2‘b01赋予实例f1的参数A。参数B没有指定类型和位宽,于是f1中B就取为实数5.1415。
注意使用defparam时,可以跨层对参数进行设置,也就是可以对子模块的子模块的参数进行设置。
模块内部指定参数的默认值时,可以指定为包括其他参数的一个表达式,然而如果外部重新指定该参数时,就不受这个表达式影响了。(如果要定义外部不能重新指定的参数,可以用局部参数),比如:

parameter
 word_size = 32,
 memory_size = word_size * 4096

定义了两个参数,如果外部指定了参数word_size,没有指定memory_size,则memory_size根据word_size计算出来,如果外面指定了memory_size参数,则以外面的参数为准。

9.3 实例化时端口连接的规则

模块实例化时,模块实例的端口通过端口连接表与外部连接,前面介绍过可以通过顺序连接表或者命名连接表两种方式进行链接。连接时遵守下面的规则:

  1. 实数表达式不能直接与端口连接,如果实数表达式需要连接到端口上,则需要用 r e a l t o b i t s 进 行 转 换 , 可 以 用 realtobits进行转换,可以用 realtobitsbitstoreal转换回来。HDL4SE中不支持实数,一般认为实数是不可综合的。
  2. input和inout类型的端口只能是线网类型,不能是reg类型。
  3. 端口连接到表达式被视为一个持续性赋值,实例化时子模块的inout和output端口只能连接到变量(reg等),线网,或者线网中的某一位(必须由常数表达式指定),或者线网中的一个部分(必须由常数表达式指定),或者上述类型中的连接。
  4. 如果线网连接的端口都是类型uwire,应该在线网无法合并时进行警告。
  5. 如果线网连接的端口是不同类型的线网,则应该按规则进行合并,按照合并后的类型实现功能。HDl4SE不支持除了wire之外的线网类型,因此这里不多介绍,感兴趣的可以参考IEEE. 1364-2005的12.3.10。
    6.signed无法通过端口连接传递,如果类型,位宽不一致,则应该转换为模块内部定义的类型或位宽。

9.4 生成结构

verilog语言中定义了一种生成结构的语言要素,能够根据条件或者循环生成一系列可生成的结构。所谓可生成的结构,就是除了端口声明,参数声明,specify块和specparam声明以外的各种模块内能够出现的语言结构。生成结构提供一种可以根据实例化参数来调整模块内部结构的能力。它有可能让一些重复的结构更加简洁地表达出来。
有两种生成结构,循环和条件,循环生成结构可以让一个生成块在一个模块中多次实例化,条件生成结构包括if-结构和case结构,可以根据条件生成需要的结构。
值得注意的是,生成结构的语法要素是在模型建立过程中进行的,也就是说在综合到目标平台时,生成结构中的结构其实是已经确定的,因此生成结构可以是RTL可综合的。这要求其中的循环控制参数和条件控制参数都必须是常数表达式,就是只包括常数或参数的表达式。这其中还要求循环必须在有限次内终止,循环中用到的循环变量要用genvar特别声明,不能用通用的变量声明。
verilog中可以用generate和endgenerate定义一个所谓的generate区,当然其实也可以不用定义generate区,规范中没有区分用不用generate区的区别。如果使用的话,generate和endgenerate必须配对使用,而且不能嵌套使用。–这么麻烦又没什么用,那还不如不用算了。

9.4.1 循环生成结构

循环生成结构用类似于for循环语句的方式定义,区别在于循环索引变量必须用genvar定义。循环生成结构可以嵌套。注意这个for循环中的初始部分和后面的修改部分必须对genvar声明的变量进行赋值,在初始部分不能出现对循环变量的引用。在循环生成结构的内部,可以使用一个隐形的局部参数,这个参数与循环变量同名。注意嵌套的循环生成结构不能用同名的循环控制变量。另外,由于是局部参数,因此在循环生成结构中不允许对循环控制变量进行赋值。这样做确保循环控制参数的修改只在for语句中进行。
循环生成结构中如果有多个项目,需要用begin end括起来,这个根for循环后面只能有一个语句是一致的。用begin和end括起来的块可以进行命名。
循环生成结构的例子:

module ripple_adder(co, sum, a, b, ci); 
 parameter SIZE = 4; 
 output [SIZE-1:0] sum; 
 output co; 
 input [SIZE-1:0] a, b; 
 input ci; 
 wire [SIZE :0] c; 
 wire [SIZE-1:0] t [1:3]; 
 genvar i; 
 assign c[0] = ci; 
 for(i=0; i<SIZE; i=i+1) begin
 	assign t[1][i] = a[i] ^ b[i];
 	assign sum[i] = t[1][i] ^ c[i];
 	assign t[2][i] = a[i] & b[i];
 	assign t[3][i] = t[1][i] & c[i];
 	assign c[i+1] = t[2][i] | t[3][i];
 end
 assign co = c[SIZE]; 
endmodule

这段代码生成了一个所谓的行波进位加法器(ripple carry adder),它可以在实例化时指定加法器的位宽。如果不使用生成结构,则不同位宽的加法器必须重新写一遍代码,定义不同的module来实现。
当然这样的写法其实也就是为了简洁,有点像软件工程师追求的一样的事情用一段代码来写,其实最终生成的电路是一样的,实例化后该生成多少逻辑还是生成多少逻辑了。

9.4.2 条件生成结构

条件生成结构有两种形式,一种是if-条件生成结构,一种是case-条件生成结构。例如:

module multiplier(a,b,product); 
parameter a_width = 8, b_width = 8; 
localparam product_width = a_width+b_width; 
input [a_width-1:0] a; 
input [b_width-1:0] b; 
output [product_width-1:0] product; 
generate
 if((a_width < 8) || (b_width < 8)) begin
 	CLA_multiplier #(a_width,b_width) u1(a, b, product); 
 end
 else begin
 	WALLACE_multiplier #(a_width,b_width) u1(a, b, product); 
 end
endgenerate
// The hierarchical instance name is mult.u1
endmodule

这段代码定义了一个可以设置宽度的乘法器,根据输入的宽度选择不同的乘法器模块进行实例化。这样做可以将乘法器接口和模块统一起来,外部使用的时候通过一个统一的模块形式使用,内部则根据操作数不同的位宽选择不同的乘法器。
如果选择情况比较多,可以用case-条件生成结构来表达:

generate
 case (WIDTH)
 1: begin: adder // 1-bit adder implementation
 adder_1bit x1(co, sum, a, b, ci); 
 end
 2: begin: adder // 2-bit adder implementation
 adder_2bit x1(co, sum, a, b, ci); 
 end
 default: 
 begin: adder // others - carry look-ahead adder
 adder_cla #(WIDTH) x1(co, sum, a, b, ci); 
 end
 endcase
// The hierarchical instance name is adder.x1 
endgenerate

9.5 分层访问

在verilog语言中,允许命名过的语法结构跨层访问。事实上,每一个标识符都有一个对应的唯一的多级路径名,这个多级的层次命名包括模块名一级下面定义的各种项目,比如任务,命名语句块,以及其中声明的各种名字。这种多级的层次化命名是树状结构,有点像文件系统中的树状目录结构一样,对应每个文件都有一个全路径名。其中每个实例化模块,生成块实例,任务,函数,命名的begin/end块和fork/join块都定义了一个层级。其中的每个声明从顶层模型开始都有一个唯一的全路径名对应。
跟文件系统有点不同的是,verilog中的全路径名是各个层级名字之间用小数点隔开,而不是文件系统中用斜杠隔开。比如在下面的代码中:

module mod (in); 
input in; 
always @(posedge in) begin : keep 
	reg hold; 
	hold = in; 
end
endmodule

module cct (stim1, stim2);
input stim1, stim2;
// instantiate mod
mod amod(stim1), bmod(stim2);
endmodule

module wave;
reg stim1, stim2;
cct a(stim1, stim2); // instantiate cct
initial begin :wave1
	#100 fork :innerwave
		reg hold;
	join
	#150 begin
	stim1 = 0;
	end
end
endmodule

其中的名称构成了下面的一棵树状结构:

wave a wave1 amod bmod innerwave keepa keepb keep 以上是关于HDL4SE:软件工程师学习Verilog语言的主要内容,如果未能解决你的问题,请参考以下文章

HDL4SE:软件工程师学习Verilog语言(十四)

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言

HDL4SE:软件工程师学习Verilog语言