老衲第一次学习 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
//Other bits: full_adder
begin: FULL_ADDER full_adder F(.a0(a0[loop]), .a1(a1[loop]), .c0(c[loop - 1]), .s(sum[loop]), .c1(c[loop]) );
//Logic assign sum[WIDTH] = c[WIDTH-1]; //Carried bit for the result endmodule |
//Definition for Variables in the module
//Carried bits in the line //Load other module(s)
//First bit: half adder
generate //Other bits: full_adder genvar loop; begin for (loop = 1; loop < WIDTH; loop = loop + 1) begin: FULL_ADDER
end end endgenerate //Logic
//Carried bit for the result
|
施主们就看到了生成块是可以嵌套的,这个一般的代码类似。还是一句顺口溜:嵌套用得好,写核难不倒。意思是说:生成块的嵌套用好了,自己写 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] |
可综合性要求 |
|
|
[1]Verilog 语句 for 可以使得部分信号、模块的实例在实际中不被使用,但是理论上这些信号、模块依然被语言约束存在于电路中。当然好的综合软件会优化掉这些无用的信号、模块,但是这不是语法要求的。
[2]工程代码里面不建议这样的写法。
由此可见,虽然生成块和 Verilog 语句里面 if、case 和 for 的样子完全一样,但是应用场合却是大相径庭的。施主们一定要注意区别对待,用错了地方可是要闹笑话的。
这正是:
“
禹王量水号神针,大圣降妖棒一根。如今列位有福分,语言里面块生成。
模块选择参数能,数目大小循环认。灵活运用仙界登,不怕小核把乱生。
”
与非网原创内容,谢绝转载!
系列汇总: