老衲第一次学习 Verilog 语言,基本就到前面几讲的程度,顶多加上了解`define 宏定义。于是对于能设计 IP 核的人,那是佩服的五体投地,如黄河泛滥一发不可收拾。直到 Verilog 2001 出了参数(parameter)和生成块(generate)功能,做 IP 核就成了人人可以掌握的技能了。对头,下面老僧就和施主们讲这些内容。


同样的代码 / 模块要简化不难,这就是所谓模块化的作用。但是类似的模块 ---- 例如同样是计数器,只是内部 D 触发器位宽的不同 ---- 要聚类就需要一些技巧了,因为电路是不支持结构灵活变化的。为了能够合并类似模块,Verilog 发展出了宏和参数这两个玩意。


最后呢,在 Verilog 2001 里面,设计了块生成的功能。这个功能使得用户可以更加简单的根据外部的输入,灵活的产生最佳的电路结构。


1. 参数定义,结构变化
参数的定义方法和传递方式见表 1 所示。定义有两种方法,传递也有两种方法,在加上参数列表的两种表示,又是一个“一题多解”。那种方式好?没标准答案,按照施主喜欢的方式来即可。但是一个项目里面,最好选择一种组合模式,方便其他人阅读。


表 1 参数的定于与传递


“parameter_list”为参数列表,参数之间用逗号“,”隔开;其中,“parameter_name”是用户定义的参数名称,建议采用容易阅读的命名方式;“parameter_initial_value”为该参数对应的初始值,在本模块的实例未被定义参数的时候使用。


“parameter_list_seperated”为独立于 module 定义之外的参数列表,需要在模块行为描述之前书写。它由若干个关键字“parameter”开头的参数定义构成;如果一个“parameter”带多个参数定义,则这些参数需要被逗号“,”隔离;每个“parameter”开头的定义,末尾用分号“;”表示结束。


“parameter_assignment_list”是参数赋值列表,由关键字“defparam”开头,后面是各个参数的赋值。对于每个参数,参数名称需要用“module_instance_index”指定是哪一个例化模块对应的参数;模块索引“module_instance_index”的格式是按照最高模块例化名称到最底层例化的顺序排列,模块例化名称之间用点“.”隔离;“parameter_value”是对应参数的值;参数赋值之间用逗号“,”隔离,末尾需要有分号“;”。


“parameter_assignment_list_sequence”是按照定义顺序的参数传递列表,其中各个对应参数值“parameter_value”的顺序必须和定义时候的顺序一致。


“parameter_assignment_list_named”是按照指定名称的参数传递列表,其中各个对应参数值“parameter_value”有前面带点“.”的参数名称“parameter_name”指定。所以在参数的排列顺序上可以任意。


当在实例化模块的同时传递参数的时候,请注意模块名称之后是参数传递列表,然后才是模块的实例名等,这个顺序不能搞错。参数传递列表被#(…)标记出来。


参数的值可以说明位宽,但是一般而言,参数的值是一个常数不需要定义位宽的。
代码中引用参数的时候,直接使用其名称,无需象宏定义那样用“`”开头。


例 2 给出了一个位宽和最大值参数化的计数器的例子,请参考。例子中,这个计数器完成到达最大值就清零的不停的计数功能。顶层模块调用了两个参数不同的计数器。

 


【例 2】位宽和最大值参数化的计数器
`define WIDTH_1 8
`define WIDTH_2 4
`define WIDTH_3 3
//Bit width for different sub modules

`define MAX_1 200
`define MAX_2 13
`define MAX_3 5
//Bit width for different sub modules

module top_counter_parameter
  (
    input clk, RST,
    output[`WIDTH_1 - 1:0] counter1,
    output[`WIDTH_2 - 1:0] counter2,
    output[`WIDTH_3 - 1:0] counter3      
  );
//Load other module(s)
counter_parameter C1(.clk(clk), .RST(RST), .counter(counter1));
counter_parameter C2(.clk(clk), .RST(RST), .counter(counter2));
    defparam  C2.WIDTH = `WIDTH_2, C2.MAX_VALUE = `MAX_2;
counter_parameter #(.WIDTH(`WIDTH_3), .MAX_VALUE(`MAX_3))
                  C3(.clk(clk), .RST(RST), .counter(counter3));
//Definition for Variables in the module

//Logic
endmodule
……
module counter_parameter
#(parameter WIDTH = 8,
//Bit width for output
MAX_VALUE = 200)
//Maximun value for counter)
  (
    input clk, RST,
    output reg[WIDTH - 1:0] counter
  );

 
//Load other module(s)

//Definition for Variables in the module

//Logic
always @(posedge clk or negedge RST)
begin
    if (!RST)
    begin
        counter <= 1'h0;
    end
    else if (counter < MAX_VALUE)
    begin
       counter <= counter + 1'h1;    
    end
    else
    begin
       counter <= 1'h0;    
    end
end
endmodule


参数型常数常用于定义延迟时间和变量宽度,在模块和实例引用时,可通过参数传递改变在被引用模块或实例中已定义的参数。参数在被综合的时候必须是常数值,这点要强调一下,变结构的电路是不可能被综合的。这个常数值可以是已知的常数,也可以通过常数之间的计算得到。


参数是给综合软件用的,所以在实现的电路里面不可能明显的看到参数的值。


除了参数,Verilog 2001 里面还定义了一个很多工程师都不知道干嘛用的本地参数“localparam”。本地参数的定义方法和参数一样,作用域也是相同的。这两类参数的区别在于,本地参数不同通过参数传递方式进行修改。

 


2. 生成有块,更加灵活
一般而言,生成块要和参数功能合作,完成动态产生电路的作用。当然这个功能也可以不动态产生电路,但是这样相当于用宝剑来做木匠活,不仅大材小用还不方便使用。


生成块的关键词是“generate”,英文产生的意思。一个生成块被
generate
operations
endgenerate
这样的框架包裹,其中“operations”是快生成的功能部分,用来描述实际有用的逻辑。生成块功能分为:条件、case 和循环三个类型,待贫僧一一道来。


条件嘛,莫过于就是“if…else if…”的样子了,看到这里的观众们应该可以耳熟能详了。但是注意,生成块的条件里面不是什么都可以装的,可以用于生成块的条件功能的内容仅限于:模块(例化)、UDP、Verilog 门原语、连续赋值,initial 块和 always 块等。这个要注意,否则报错是不可避免的。


生成块的语法结构,想必大伙儿也猜得出:
generate
if (condition)
    operation_1
else
    operation_2
endgenerate


其中,“condition”是逻辑表达式,是判决条件。当判决条件为真的时候,进行“operation_1”的操作;否则,进行“operation_2”的操作。


就和 if 与 case 的关系一般,生成块里面既然有 if 也少不得 case 来搭配。生成块的 case 的语法结构是:
generate
case (constant_express)
value_1: operation_1
value_2: operation_2
……
value_n: operation_n
default: operation_default
endgenerate

如何使用,不必啰嗦。生成块的 case 里面可以包含的内容和生成块 if 的一样的。强调一下,仅仅包括:模块(例化)、UDP、Verilog 门原语、连续赋值,initial 块和 always 块。


如何选择 if 和 case 的应用场景也是类似的情况,老衲也不多嘴。


看到上面的内容,施主们一定会产生轻敌的思想:生成块莫过如此,easy!兵法有云:“骄兵必败”,这个想法要不得。下面给大伙说说循环类型的生成块,这个很容易引起歧义,老衲需要细细讲解。


欲说循环生成,先要介绍生成索引变量“genvar”,其语法结构是:
genvar genvar_name_1, genvar_name_2, ……, genvar_name_n;
其中,“genvar_name”是不同循环索引变量的名称,要求符合 Verilog 对于变量名名的要求。这个变量是和循环生成共生的,看起来像是循环里面的循环变量。实际中,它比循环变量的应用范围专业,只用于循环生成的电路模块的“索引”。具体啥叫“索引”和“索引”啥,先买个关子,后面再说。


循环式生成块的语法结构是
generate
genvar genvar;

    for (genvar = start_value; end_condition; circle_express)
    begin:  instant_name
        operations
    end
endgenerate


其中, “start_vlue”、“end_condition”、“circle_expree”是和循环语句 for 是一样一样的含义。“operations”是每次循环的操作,这只能是变量声明、模块(例化)、UDP、Verilog 门原语、连续赋值,initial 块和 always 块这几个里面的一个或者几个。“genvar”就是前面说到的生成索引变量。最大的不同是操作一定要有“begin……end”括在内部,而且“begin”之后要有“:  instant_name”这个结构。冒号表示风格,不多说;“instant_name”就是所谓的生成索引的名字。换句话说,“instant_name”表示 for 内部实现的模块、变量的名称,以防止混淆。


这里说了这么许多,很多施主肯定已经迷糊了,下来给个例子十分必要。

 


看一个简单的例子,用循环生成做一个位宽参数化的加法链,如例 2 所示。由于最低比特是一个半加器,这个在代理里面特别处理了。特别说明一下,for 循环里面实现了若干个全加器,这些全加器的被命名为:full_adder[0].F,full_adder[1].F……这就是所谓的生成索引。


【例 2】位宽参数化的加法链(半加器外置)

代码

电路代码

综合软件内的代码

module  adder_line_generate #(parameter WIDTH = 8)

 (

    input[WIDTH - 1 :0] a0, a1,

    output[WIDTH :0] sum

  );

//Definition for Variables in the module

wire[WIDTH - 1:0]  c;

//Carried bits in the line

 

//Load other module(s)

half_adder HALF_ADDER(.a0(a0[0]), .a1(a1[0]),

          .s(sum[0]),.c1(c[0]));

//First bit: half adder

         

generate

//Other bits: full_adder

genvar loop;

begin

    for (loop = 1; loop < WIDTH; loop = loop + 1)

    begin: FULL_ADDER

        full_adder F(.a0(a0[loop]), .a1(a1[loop]), .c0(c[loop - 1]),

                 .s(sum[loop]), .c1(c[loop]) );

    end

end

endgenerate

 

//Logic

assign sum[WIDTH] = c[WIDTH-1];

//Carried bit for the result

endmodule

module  adder_line_generate #(parameter WIDTH = 8)

 (

    input[WIDTH - 1 :0] a0, a1,

    output[WIDTH :0] sum

  );

//Definition for Variables in the module

wire[WIDTH - 1:0]  c;

//Carried bits in the line

 

//Load other module(s)

half_adder HALF_ADDER(.a0(a0[0]), .a1(a1[0]),

          .s(sum[0]),.c1(c[0]));

//First bit: half adder

         

generate

//Other bits: full_adder

genvar loop;

begin

    for (loop = 1; loop < WIDTH; loop = loop + 1)

    begin: FULL_ADDER

        full_adder F(.a0(a0[loop]), .a1(a1[loop]), .c0(c[loop - 1]),

                 .s(sum[loop]), .c1(c[loop]) );

    end

end

endgenerate

 

//Logic

assign sum[WIDTH] = c[WIDTH-1];

//Carried bit for the result

endmodule

module  adder_line_generate #(parameter WIDTH = 8)

 (

    input[WIDTH - 1 :0] a0, a1,

    output[WIDTH :0] sum

  );

//Definition for Variables in the module

wire[WIDTH - 1:0]  c;

//Carried bits in the line

 

//Load other module(s)

half_adder HALF_ADDER(.a0(a0[0]), .a1(a1[0]),

          .s(sum[0]),.c1(c[0]));

//First bit: half adder

         

generate

//Other bits: full_adder

genvar loop;

begin

    for (loop = 1; loop < WIDTH; loop = loop + 1)

    begin: FULL_ADDER

        full_adder F(.a0(a0[loop]), .a1(a1[loop]), .c0(c[loop - 1]),

                 .s(sum[loop]), .c1(c[loop]) );

    end

end

endgenerate

 

//Logic

assign sum[WIDTH] = c[WIDTH-1];

//Carried bit for the result

endmodule


施主们就看到了生成块是可以嵌套的,这个一般的代码类似。还是一句顺口溜:嵌套用得好,写核难不倒。意思是说:生成块的嵌套用好了,自己写 IP 核就是探囊取物一般简单。这方面的资料相对较少,所以老僧就不厌其烦再给几个例子。


还是例 3 场景,实现位宽参数化的加法链。


【例 3】位宽参数化的加法链(全加器 / 半加器合并)
module  adder_line_generate #(parameter WIDTH = 8)
(
    input[WIDTH - 1 :0] a0, a1,
    output[WIDTH :0] sum
  );
//Definition for Variables in the module
         
generate
//Other bits: full adder
genvar loop;
begin
    for (loop = 0; loop < WIDTH; loop = loop + 1)
    begin: ADDER
       wire c;
       //Carried bit in the loop named ADDER[loop].c
       if (loop == 0)
       begin
           half_adder h(.a0(a0[loop]), .a1(a1[loop]),
                             .s(sum[loop]), .c1(c) );
       end
       else
       begin
           if (loop == WIDTH - 1)
           begin
                            full_adder F(.a0(a0[loop]), .a1(a1[loop]), .c0(ADDER[loop-1].c),
                                 .s(sum[loop]), .c1(sum[WIDTH]) );
            end
            else
            begin
                full_adder F(.a0(a0[loop]), .a1(a1[loop]), .c0(ADDER[loop-1].c),
                     .s(sum[loop]), .c1(c) );            
            end
        end
    end
end
endgenerate

//Logic

endmodule


生成块和 Verilog 语句里面对应的 if、case 和 for 很容易混淆,在最后贫僧在表 3 里面帮大家做了一个总结,请参考。


表 3 生成块和 Verilog 语句的区别

 

生成块:if 与 case

Verilog 语句的 if 与 case

功能

综合软件根据条件,判断选择的电路器件

直接产生电路

电路映射

不产生电路,隐式体现

产生电路,一般为选择器

内部可包含

模块(例化)、UDP、Verilog 门原语、连续赋值,initial 块和 always 块

Verilog 语句

条件表达式

参数化的常数表达式

常数表达式(不一定参数化)或者变量

可综合性要求

内部为模块(例化)、连续赋值和 / 或 always 块,并且内部语句可综合

内部语句可综合

 

生成块:for

Verilog 语句的 for

功能

综合软件循环次数,实现电路

综合软件循环次数,实现电路

电路映射

不产生电路,隐式体现

不产生电路,隐式体现

内部可包含

变量声明、模块(例化)、UDP、Verilog 门原语、连续赋值,initial 块和 always 块

其他可综合语句(不能实现模块例化等)

信号、模块数量

可参数化生成

规模固定[1]

循环变量类型

生成索引变量

一般变量

格式

必须由“begin …… end”括住,而且 begin 后面需要“:  instant_name”结构

如果是单独语句语法上可以没有“begin …… end”括住[2]

可综合性要求

  • 内部为变量声明、模块(例化)、连续赋值和 / 或 always 块,并且内部语句可综合
  • 循环次数为常数
  • 内部语句可综合
  • 循环次数为常数


[1]Verilog 语句 for 可以使得部分信号、模块的实例在实际中不被使用,但是理论上这些信号、模块依然被语言约束存在于电路中。当然好的综合软件会优化掉这些无用的信号、模块,但是这不是语法要求的。

[2]工程代码里面不建议这样的写法。


由此可见,虽然生成块和 Verilog 语句里面 if、case 和 for 的样子完全一样,但是应用场合却是大相径庭的。施主们一定要注意区别对待,用错了地方可是要闹笑话的。
这正是:



禹王量水号神针,大圣降妖棒一根。如今列位有福分,语言里面块生成。
模块选择参数能,数目大小循环认。灵活运用仙界登,不怕小核把乱生。

与非网原创内容,谢绝转载!

 

系列汇总:

之一:温故而知新:从电路里来,到 Verilog 里去!

之二:Verilog 编程无法一蹴而就,语言层次讲究“名正则言顺”

之三:数字逻辑不容小窥,电路门一统江湖

之四:Verilog 语言:还真的是人格分裂的语言

之五:Verilog 不难学,聊聊时序逻辑那些事儿

之六:数字电路设计:有理论、有电路、有代码“三位一体”

之七:熟读语言要素,不会编程也懂 verilog