Verilog HDL入门(4)

Verilog 基础语言4

结构建模与 generate 语句

结构建模

Verilog HDL 中的结构建模风格,结构建模风格用以下 3 种:

  • Gate 实例引用语句
  • UDP 实例引用语句(or、and、xor)
  • module 实例应用语句

模块

在 Verilog HDL 中,一个模块定义了一个基本单元,格式

1
2
3
module module_name(port_list);
Declarations_and_Statements
endmodule

端口列表(port_list)列出了该模块与外部模块进行通信的端口。

端口

模块的端口可以被声明为输入、输出、双向端口。端口的默认类型为线网类型(即 wire 类型),也可以明确地把端口声明为线网类型。在声明端口后,可在模块内将输出端口再次声明为 reg 变量类型。无论是在线网声明还是变量声明中,线网、变量必须与端口声明中指定的位宽一致。

端口声明还可以直接放在端口列表中而不放在模块内部,这样的端口声明风格称为模块端口声明风格,模块端口列表风格(端口声明放在模块内部)。

模块端口声明风格:

1
2
3
4
5
6
7
module (
input wire[3:0] prog_ctr,
output wire[1:0]instr_reg,
inout wire [15:0]next_addr
);

endmodule

端口在端口列表中被声明,那么就不能在模块内部重新声明。端口还可以被进一步限定为寄存器类型(reg)端口、有符号类型(signed)端口。

1
2
3
4
5
6
7
8
module remap_ctrl(
input[31:0]rpc_addr, //默认的类型是无符号线网类型
input signed[31:0]rpc_rdata, //有符号线网类型
output reg remap,ready, //无符号寄存器类型变量
output reg signed [31:0]rpc_wdata//rpc_wdata是有符号寄存器类型变量
);

endmodule

所有端口都是用逗号隔开,共用同一个声明的多个端口

参数端口

除了可以在模块内部声明参数外,还可以用端口列表的风格来声明参数。

1
2
3
4
5
6
module module_name 
#(parameter param1 = value1,param2 = value2,...,
parameter param3 = value3) //声明参数
(port_list);
...
endmodule

在参数声明中可以选择指定参数的位宽以及符号。

1
2
3
4
5
6
7
8
module sspm
#(parameter SIZE = 6,WIDTH = 8,
parameter [WIDTH-1:0]HOLD_VALUE = 56,
parameter DUMP_FILE = "dump.rpt")
(input chip_select,read_write,
output [WIDTH-1:0]data);
...
endmodule

若每一个实例引用语句中的参数都一样,换言之,参数值不是由实例引用语句来指定的,则参数应该被声明为局部参数。
否则参数既可以在模块内部声明,也可以在参数端口中声明。

模块实例引用

按照位置排列的连接、按照名称的连接

连接类型

标识符(变量、线网)、位选择、部分选择、上述类型的拼接、表达式(只适用于输入端口)

未连接的端口

实例引用语句中,若端口表达式的位置为空白,就将该端口指定为未连接的端口。例子:

1
2
3
4
5
6
df u0df(.q(qs),
.qb(), //输入端口
.cock(ck),
.qd() //输出端口
); //按照名称对应
df u1df(qs, ,ck,,); //按照位置对应

模块未连接输入端的值被设置为 z。模块未连接的输出端只是表示该输出端口没有被使用。

不同的端口位宽

在端口和端口表达式之间存在着一种隐含的连续赋值的关系。

因此当端口和端口表达式的位宽不一致时,会进行端口匹配,采用的位宽匹配规则与连续赋值时使用的规则相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module child(psa,ppy);
input [5:0]psa;
output [2:0]ppy;
...
endmodule

module top;
wire [1:2] bdl; //2位
wire [2:6] mpr; //5位

child u6child(
.psa(bdl),
.ppy(mpr)
);
endmodule

模块 child 的实例引用语句中,存在两个隐含的连续赋值。

psa 是输入端口,所以从端口表达式往端口进行赋值

1
assign psa = bdl;

ppy 是输出端口,所以从端口往端口表达式进行赋值

1
assign mpr = ppy;

bdl[2]->psa[0],bdl[1]->psa[1]。psa[5]、psa[4]、psa[3]、psa[2] 未连接,因此值为 z

mpr[6]<-ppy[0],mpr[5]<-ppy[1],mpr[4]<-ppy[2]

模块参数值

一个模块在另一个模块的内部被实例引用时,较高层次的模块能够改变较低层次模块参数的参数值。用以下两种途径可以改变模块实例的参数:

  1. 使用定义参数语句(defparam 语句)修改参数值
  2. 在模块实例引用中修改参数值

定义参数语句

定义参数语句的格式如下:

1
2
defparam 	hier_path1 = value1,
hier_path2 = value2,...;

在较低层次模块中的参数层次路径名可以使用如下这样一条语句清晰地加以设置(层次路径名),

1
2
3
4
5
6
7
8
9
module top (tpa,tpd,tps,tpc);
input tpa,tpd;
output tps,tpc;
defparam u5ha.XOR_DELAY = 5, //名为u5ha的半加器实例中的参数XOR_DELAY
u5ha.AND_DELAY = 2; //名为u5ha的半加器实例中的参数AND_DELAY

half_adder u5ha(tpa,tpb,tps,tpc);

endmodule
1
2
3
4
5
6
7
8
9
10
11
12
13
14
module top2(tp2p,tp2q,tp2cin,tp2sum.tp2cont);
input tp2p,tp2q,tp2cin;
output tp2sum,tp2cout;

defparam
u8fa.u1ha.XOR_DELAY = 2, //在名为u8fa的全加速器实例中引用的名为u1ha的半加器实例中的参数XOR_DELAY

u8fa.u1ha.AND_DELAY = 3, //名为u8fa的全加速器实例中引用的名为u1ha的半加器实例中的参数AND_DELAY

u8fa.OR_DELAY = 3; //名为u8fa的全加器实例中的参数OR_DELAY

full_adder u8fa(tp2p,tp2q,tp2cin,tp2sum,tp2cout);

endmodule

模块实例引用中修改参数值

模块实例本身就能指定新的参数值,模块实例引用语句可采用以下两种风格来指定实例中的参数值:

(1)按照位置来赋对应的参数值(位置关联):第 1 个值对应于模块中声明的第 1 个参数,第 2 个值对应于模块中声明的第 2 个参数,依此类推

(2)按照名称来赋对应的参数值(名称关联):参数的名称和值都被明确地标出。因此参数的顺序并不重要,从而可以按照任意顺序来传递参数。更重要的是,不是所有的参数值都必须明确地标出

下面功能一样

1
2
3
4
5
6
7
8
9
10
11
12
module top3(tp3a,tp3d,tp3s,tp3c);
input tp3a,tp3d;
output tp3s,tp3c;

half_adder #(5,2) u4ha(tp3a,tp3d,tp3s,tp3c);
//第1个值5赋给参数AND_DELAY,该参数是在模块half_adder中声明的第1个参数
//第2个值2赋给参数XOR_DELAY,该参数是在模块half_adder中声明的第2个参数
//这是通过位置来赋值

//下面是通过名称来赋参数值
half_adder #(.AND_DELAY(5),.XOR_DELAY(2)) u12ha(tp3a,tp3d,tp3s,tp3c);
endmodule
1
2
3
4
5
6
7
8
9
10
11
module top4(tp4p,tp4q,tp4cin,tp4sum.tp4cont);
input tp4p,tp4q,tp4cin;
output tp4sum,tp4cont;

defparam
u22fa.u1ha.XOR_DELAY = 2, //全加器实例,u22fa中引用半加器实例u1ha中的参数XOR_DELAY
u22fa.u1ha.AND_DELAY = 3; //全加器实例,u22fa中引用半加器实例u1ha中的参数AND_DELAY

full_adder #(3) u22fa(tp4p,tp4q,tp4cin,tp4sum.tp4cont);
//值3是参数OR_DELAY的新值
endmodule

按照位置来赋参数值时,模块实例引用语句中的参数值顺序必须与参数在被引用的底层模块中被声明的顺序一致。在模块 top3 中的半加器实例 u4ha 中,AND_DELAY 设置 5,XOR_DELAY 设置 2。

模块 top3 和 top4 的解释说明了用带参数值的模块实例引用语句只能将参数值向下传递一个层次(例如 OR_DELAY),但是用参数定义语句能够修改任意层次的参数值。

注意:在带参数值的模块实例引用语句中,指定参数值位置的标记符与门级实例引用语句中定义延时的位置标记符相似。

外部端口

模块定义中,端口列表举出了模块外部可见的端口

1
2
3
4
5
6
7
module scram_a(arb,ctrl,mem_blk,byte);
input [0:3] arb;
input ctrl;
input [8:0]mem_blk;
output [0:3]byte;
...
endmodule

arb、ctrl、mem_blk、byte 为这个模块的端口。这些端口同时也是外部端口,即在实例引用中,当采用按照名称

在模块定义的端口列表中,这两种风格可以混合使用,换言之,在模块定义中允许只有部分端口拥有外部端口名称。

如果模块端口是按位置对应关系进行连接的,则模块实例应用语句中不能使用外部端口名称。(端口名称不能与模块实例名相同)

内部端口名称不仅可以是标识符,也可以是下面类型的表达式之一:

位选择;部分选择;位选择、部分选择和标识符的拼接项。

下面举例说明:

1
2
3
4
5
6
7
8
9
10
11
module scram_c(
arb[0:2],ctrl,
{mem_blk[0],mem_blk[1]},byte[3]
);

input [0:3] arb;
input ctrl;
input [8:0]mem_blk;
output [0:3]byte;
...
endmodule

在 scram_c 模块的定义中,端口列表中包括部分选择(arb[0:2])、标识符(ctrl)、拼接项({mem_blk[0],mem_blk[1]})和位选择(byte[3])。在外部端口是位选择、部分选择、拼接项的情况下,不能隐式地指定外部端口的名称。因为若这样定义模块端口的话,在模块实例引用语句中,模块端口必须按位置对应关系才能进行互连,示例

1
2
3
scram_c u_scram_c(
cab[4:6],ram_ctl,mmy[1:0],tcb
);

这个实例引用语句中,端口按照位置对应关系来进行互连;因此,cab[4:6] 连接到 arb[0:2],ram_ctl 连接到 ctrl,mmy[1] 连接到 mem_blk[0],mmy[0] 连接到 mem_blk[1],tcb 连接到 byte[3]。arb[3]、mem_blk[8:2] 没有连接到任何信号,因此这些输入信号将被赋予默认值 Z

当内部端口信号与外部连接所需要的表示信号的位宽不完全一致时,若想要使用按名称对应关系连接,(内部端口并不所有信号需要与外部连接时,但是又要按名称对应关系连接,采用方式)则必须为模块内部的端口明确地指定外部端口名称。

如下

1
2
3
4
5
6
7
8
9
10
11
12
module scram_d(
.data(arb[0:2]),
.control(ctrl),
.mem_word(mem_blk[0],mem_blk[1]),
.addr(byte[3])
);
input [0:3]arb;
input ctrl;
input[0:8]mem_blk;
output [0:3]byte;
...
endmodule

在模块 scram_d 的实例引用语句中,端口既能够按位置对应关系,也能够按名称对应关系连接,但是不能混合使用。

按名称对应关系连接的实例引用语句。

1
2
3
4
scram_d u_scram_d(
.data(cab[4:6]),.control(ram_ctrl),
.mem_word(mmy[1:0]),.addr(tcb)
);

模块也可以只有外部端口而没有相应的内部端口。例如:

1
2
3
4
5
6
7
8
9
module scram_e(
.data ,.control(ctrl),
.mem_word({mem_blk[0],mem_blk[1]}),.addr()
);

input ctrl;
input [8:0]mem_blk;
...
endmodule

模块 scram_e 有两个外部端口 data、addr 没有与模块内部的任何信号相连。

一个内部端口是否能与多个外部端口相连?

Verilog HDL 允许一个内部端口与多个外部端口相连。例:

1
2
3
4
5
6
7
8
module fan_out(
.a(ctrl_in),.b(cond_out),.c(cond_out)
);
input ctrl_in;
output cond_out;

assign cond_out = ctrl_in;
endmodule

内部端口 cond_out 与两个外部端口 b 和 c 相连。因此在 b 和 c 上都将出现 cond_out 的值。

1
2
3
4
5
module ssp_ctrl(
.wdata({write_bus[15:12],write_bus[3:0]})
);
input [15:0]write_bus;
endmodule
1
2
3
4
5
module top_ssp_ctrl;
reg [7:0] ssp_select;

ssp_ctrl u_ssp_ctrl(.wdata(ssp_select));
endmodule

上面描述模块 ssp_ctrl 的外部端口名称是 wdata,而模块 top_ssp_ctrl 有选择性地把 ssp_select 的 8 个位依次连接到 write_bus[15:12] 和 write_bus[3:0] 上。

generate 语句

generate(产生,引起)语句允许细化时间(Elaboration - time)的选取或者某些语句的重复。这些语句可以包括模块实例引用语句、连续赋值语句、always 语句、initial 语句、门级实例引用语句等。

细化时间是指仿真开始前的一个阶段,此时所有的设计模块已经被链接到一起,并已完成层次引用。

generate 语句用关键字 generate … endgenerate 来定界。

在 generate 语句中可以出现以下 3 种语句:

(1)generate - loop 循环语句

(2)generate - case 分支语句

(3)generate - conditional 条件语句

generate 格式

1
2
3
4
5
6
generate
//generate 循环语句
//generate 条件语句
//generate 分支语句
//嵌套的 generate 语句
endgenerate

generate 循环语句

generate 循环语句主要功能是帮助设计者编写由多次重复所构造的复杂语句。这可借助于基于结构单元的 generate for 循环语句,在 Verilog 编译细化阶段,自动地生成由结构单元实例构成的复杂代码。

generate 循环语句被用于(Verilog 编译)细化阶段的语句复制,基本上允许对结构元素编写一个 for 循环。例:普通 N 位的异或门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module nbit_xor
(#(parameter SIZE = 16))
(
input [SIZE-1:0] a,b,
output [SIZE-1:0] y;
);

genvar gv_i;
generate
for(gv_i = 0;gv_i<SIZE;gv_i = gv_i +1)
begin: sblka
xor uxor(y[gv_i],a[gv_i],b[gv_i]);
end
endgenerate
endmodule

在细化期间,generate 循环语句将被扩展。对于循环变量的每一个值,for 循环语句的实体会被重复一次。例子细化后变成

1
2
3
xor sblka[0].uxor(y[0],a[0],b[0]);
xor sblka[1].uxor(y[1],a[1],b[1]);
...

通常,generate 循环语句的格式如下:

1
2
3
4
for(intitial_expression;final_expression;assignment)
begin: label
statements
end

在 for 循环语句内,循环变量需要被初始化并且在每一次循环时都需要被修改。使用的这种循环变量被称为 genvar 变量。这种变量必须用 genvar 声明语句来声明,并且只能够在 generate 循环语句中使用。genvar 变量用于初始化语句和 genvar 赋值语句中。

在模块 nbit_xor 中,genvar 变量是 gv_i。初始化时,将其赋值为 0,然后每一次循环后加 1。

generate 块需要标签,这个标签用来表示 generate 循环的实例名称。在模块 nbit_xor 中,generate 块的标签是 sblka。

实例 uxor 的层次路径是:

1
2
3
nbit_xor.sblka[0].uxor
nbit_xor.sblka[1].uxor
...

在 generate 语句中还可以有局部声明。例,如果我们在块 sblka 中包括了如下声明:

1
wire phy2;

那么将会出现线网变量 phy2 的 16 个实例,它们的层次引用名称为:

1
2
3
sblka[0].phy2
sblka[1].phy2
...

在细化期间,for 循环语句被扩展,即对于每一次循环,重复一次 for 语句的实体。

如果在 generate 语句内有多个语句,所有的语句都将被重复。尽管上例中的 generate 语句中只有一个门级实例引用语句,但是 generate 语句还可以包含过程性语句。

下面是一个波纹计数器的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
module ripple_counter
#(parameter BITS = 8)
(
input count_clk,nreset,
output[BITS-1:0]q
);

genvar gv_a;

jk_ff u0jk_ff(
.J(1'b1),.K(1'b1),.NRESET(nreset),
.CK(cond_out),.Q(q[0]),.NQ());

generate
for(gv_a = 1; gv_a < BITS; gv_a = gv_a +1'b1)
begin : gblk_a
jk_ff u1jk_ff(
.J(1'b1),.K(1'b1),.NRESET(nreset),
.CK(q[gv_a -1]),.Q(q[gv_a]),.NQ());
end
endgenerate
endmodule

genvar 变量是 gv_a。在 generate 语句外部实例引用了一个名为 u0jk_ff 的模块实例。在 generate 语句内部声明了另外的 BITS-1 个实例。

generate - conditional 条件语句

generate 条件语句允许在细化期间对语句进行条件选择。generate 条件语句的常见格式如下:

1
2
3
4
if(condition)
statements
[else
statements]

condition 必须是一个静态的条件,即在细化期间计算得出(这样一个条件表达式只能由常数和参数组成)。statements 可以是任何能够在模块出现的语句,例如,always 语句、模块实例引用语句和门级实例引用语句。

在细化期间,根据条件的值,选择相应的语句。注意:由于条件的值可能要取决于一个值从上层模块传递过来的参数,因此条件的值可能不能在细化期间被完全计算出来。

例是一个使用 generate 循环语句和 generate 条件语句的移位寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
module shift_register
#(parameter BITS = 8)
(input shift_in,clk,nreset,
output shift_out
);

wire [BITS-1:0]tq;
genvar gv_k;

generate
for(gv_k = 0 ; gv_k<BITS ; gv_k = gv_k+1'B1)
begin: gblk_x
if(gblk_x == BITS-1) begin
dflip_flop u0dff(
.d(shift_in),.nreset(nreset),.ck(clk),
.q(tq[gv_k])
);
end
else begin
if(gv_k == 0) begin
dflip_flop u1dff(
.d(tq[gv_k +1]),.nreset(nreset),.ck(clk),
.q(shift_out)
);
end
else begin
dflip_flop u2dff(
.d(tq[gv_k +1]),.nreset(nreset),.ck(clk),
.q(tq[gv_k])
);
end
end
end
endgenerate
endmodule

在细化期间,for 循环语句被扩展。如果 generate 语句的变量 gv_k 的值为 BITS-1,那么就完成了移位寄存器的第一个部分(串行输入进入了触发器的 d 输入端)。

如果 generate 语句的变量 gv_k 的值为 0,那么就完成了移位寄存器的最后一个部分(触发器的输出端输出到 shift_out)。

当 generate 语句的变量 gv_k 取其他的所有值时,则生成了移位寄存器的中间部分(触发器的 d 端连接到前一个触发器的 q 端)

在完成细化后(假设 BITS 的值为 8),可以得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
dflip_flop gblk_x[7].u0dff(
.d(shift_in),.nreset(nreset),.ck(clk),
.q(tq[7])
);

dflip_flop gblk_x[6].u2dff(
.d(tq[7]),.nreset(nreset),.ck(clk),
.q(tq[6])
);

dflip_flop gblk_x[5].u2dff(
.d(tq[6]),.nreset(nreset),.ck(clk),
.q(tq[5])
);

...

dflip_flop gblk_x[0].u1dff(
.d(tq[1]),.nreset(nreset),.ck(clk),
.q(shift_out)
);

用 generate 条件形成的 generate 语句的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
module adder
#(parameter SIZE = 4)
(input [SIZE-1 : 0]a,b,
output [SIZE-1 : 0]sum,
output carr_out
);

wire [SIZE-1 : 0] carry;
genvar gv_k;

generate
for(gv_k = 0; gv_k < SIZE; gv_k = gv_k + 1)
begin : gen_blk_adder
if(gv_k == 0) begin
half_adder u_ha(
.a(a[gv_k]),.b(b[gv_k]),.sum(sum[gv_k]),
.carr_out(carry[gv_k])
);
end
else begin
full_adder u_fa(
.a(a[gv_k]),.b(b[gv_k]),
.carry_in(carry[gv_k - 1]),.sum(sum[gv_k]),
.carry_out(carry[gv_k])
);
end
end
endgenerate
endmodule

generate - case 分支语句

generate 分支语句与 generate 条件语句类似,只不过 generate 分支语句是用分支语句来进行条件性选择。generate 分支语句的常见格式如下:

语句根据参数 IMPLEMENTATION_LEVEL 的值选择相应的加法操作行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module my_special_addr
#(parameter SIZE = 16,
parameter IMPLEMENTA TION_LEVEL = 0)
(input[SIZE-1:0] arg1,arg2,
output[SIZE-1:0]result
);

generate
case()
0:assign result = argl + arg2;

1:ripple_adder u_ra(.sum(result),.a(argl),.b(arg2));
2:cla u_cla(.sum(result),.a(arg1),.b(arg2));
3:fast_cla u_fcla(.sum(result),.a(argl),.b(arg2));
endcase
endgenerate
endmodule

default 分支是可选的。若 IMPLEMENTATION_LEVEL 的值大于 3,则这些分支选项中无论哪一条语句都不会被选中执行。

配置

库的概念,库是存储编译配置、UDP 和模块(可以统称为 cell(单元))的地方。库可以是逻辑库、符号库,包含许多已编辑好的源代码。

使用库声明语句可以声明带具体内容的库

1
library library_name "file_name(s)"

这条声明语句能把指定名的文件编译进名为 library_name 的逻辑库中。

库映像文件(library Map File)是由许多上述库声明语句组成的列表。用 include 语句可以在一个映像文件中包含另一个库映像文件。

1
include library_map_file;

下面举一个库映像文件的例子。

1
2
3
4
5
6
//文件:esoc_library:map
library lib_global "gbl/global_blocks.vg";
library lib_rtl "./*.vg";
library lib_work "/home/bond/IP/usb*.v";
include "../../global_lib_definitions";
library lib_gate "./syn/gate/*.vg";

库映像文件 esoc_library.map 包含了 4 个库声明语句和 1 个 include 语句。声明的 4 个库分别是 lib_global、lib_rtl、lib_work 和 lib_gate。文件 gbl/global_blocks.vg 被编译进 lib_global。在当前文件夹中所有扩展名为 *.vg 的文件都被编译进库 lib_rtl,以此类推,在文件名中指定的路径与库映像文件所在位置有关。

在剖析任何的源代码前,由 Verilog HDL 的剖析器读入库映像文件。可以拥有多个库映像文件,凡支持 Verilog HDL 的工具必须提供一种机制去支持读入多个库映像文件。

当源文件被编译时,每个源文件中的单元都被保存到库映像文件指定的相应库中去。例如,当文件 usb_host.v 被编译时,这个文件中所有的单元都被保存在库 lib_work 中。

现在回到配置这个话题,配置是指确定某实例与库中某个模块的绑定(Binding)关系。