Verilog HDL入门(3)

Verilog 基础语言3

数据流建模与行为级建模

数据流模型

连续赋值语句常用来建立数据流的行为模型;过程性赋值语句用来为时序电路建立行为模型。组合逻辑电路行为模型的最好方法是使用连续赋值语句。

连续赋值语句

连续赋值语句可以用来对线网进行赋值(不能用来对寄存器进行赋值),它的格式如下(简单格式):

1
assign LHS_target = RHS_expression;

举例:

1
2
3
4
5
//线网声明:
wire [3:0]frm_rdy,coh_rdy,hrd_tag;

//连续赋值语句
assign hrd_tag = coh_rdy & frm_rdy;

连续赋值语句被赋值目标是 hrd_tag;coh_rdy & frm_rdy,特别注意,连续赋值语句中一定有关键字 assign。

连续赋值语句在什么时候执行,只要右侧表达式中的操作数有事件发生(即操作数值改变)时,就会计算右侧表达式;若新的结果值与原来的值不同,则把新的结果值赋给左侧的被赋值目标。

若 coh_rdy 或 frm_rdy 发生了变化,就会计算右侧的表达式。若右侧表达式的值发生了变化,则把新计算出的值赋给线网 hrd_tag。

连续赋值的目标可以是类型:标量线网、向量线网、矩阵中的一个元素(该矩阵可以是标量线网类型的,也可以是向量线网类型的)、向量线网的某一位、向量线网的部分位、上述各种类型的拼接体。

标量线网如:wire a,b;

向量线网如:wire [3:0] a,b;

1
2
3
4
5
6
wire [3:0] a;
//4位宽的线网
//[3:0]指定了变量的位宽为4位,其中:
//3为最高有效位(MSB),对应二进制数的第3位;
//0为最低有效位(LSB),对应二进制数的第0位
//若未显式赋值,默认值为高阻态z(无驱动时)或不定态x(多驱动冲突时)

下面,被赋值的目标是一个标量线网和一个向量线网的拼接体

1
2
3
wire carry_out,carry_in;
wire [3:0] sum,a,b; //声明了三个4位宽的线网(wire)类型变量sum、a和b
assign {carry_out,sum} = a + b + carry_in;

a 和 b 的位宽是 4 位,所以加法运算能够产生最大为 5 位的结果。因此左侧表达式的宽度指定为 5 位(carry_out 1 位,sum 4 位),最终这个赋值语句将右侧表达式最右边(低)4 位的值赋给 sum,第五位(进位)的值赋值给 carry_out。

例子说明如何在一条连续赋值语句中进行多次赋值

1
2
3
4
assign 	mux_out = (select == 0)?Input_a:'bz,	//'bz 表示32位位宽
mux_out = (select == 1)?Input_b:'bz,
mux_out = (select == 2)?Input_c:'bz,
mux_out = (select == 3)?Input_d:'bz;

上面赋值语句是下面 4 条独立连续赋值语句的简化书写格式。

1
2
3
4
assign	mux_out = (select == 0)?Input_a:'bz;
assign mux_out = (select == 1)?Input_b:'bz;
assign mux_out = (select == 2)?Input_c:'bz;
assign mux_out = (select == 3)?Input_d:'bz;

例子:

1
2
3
4
5
wire dtag;
parameter SIZE = 7;
wire [SIZE:0]padded_dtag;

assign padded_dtag = {SIZE{1'b0},dtag};

若没有对连续赋值的目标类型进行声明,则将把它默认为标量线网。

1
assign mc_noburst = dma_lock;	//没有对mc_noburst经行声明,mc_noburst被默认为1位的线网。

线网声明赋值

连续赋值可以作为线网声明的一部分,这样赋值称为线网声明赋值。

1
2
3
4
5
6
7
wire [3:0]qmv_wr = 4'b0;
wire frm_wait = 'b1;

wire ictr_gt_qctr = ictr > qctr,
qctr_gt_ictr = qctr > ictr;

wire [(8*12-1):0]dbg_dump_rpt = "dbg_dump_rpt";

线网声明赋值不但声明了线网,还对声明的线网进行连续赋值。

线网声明赋值是:声明线网,然后编写连续赋值语句的一种简便形式。

1
2
3
4
wire wr_cycle;
assign wr_cycle = 'b1;
// 等价于线网声明赋值语句:
wire wr_cycle = 'b1;

不允许对同一线网进行多个线网声明赋值。若必须进行多个赋值,则必须使用连续赋值语句。

赋值延迟

1
assign #6 dbg_data = int_data|| peg_cntxt;

#6:上述赋值语句中指定的延时,指从右侧表达式中任一操作数的变化,到右侧表达式重新计算,再把计算结果赋给左侧的目标总共需要 6 个时间单位的延迟,若在时刻 5,int_data 值发生变化,则在时刻 5 重新计算赋值语句的右侧表达式,并在时刻 11(=5+6)把计算出的新值赋给 dbg_data。

以最后一次右边变化计算出的结果延时后,赋值给左边。

在右侧的值传递给左侧之前,右侧的值发生了变化,会怎么样?最后一次值的变化将起决定作用。

1
assign #4 peg_free = xbid_par;

延时期间右侧表达式发生的变化会被滤除(需要看书,才能理解此处)

在把右式的变化传播到左式之前,右式必须至少能够在该延迟期间保持右式值不变;在延迟期满,若右式的值发生了变化,则前面的值就不能传播到输出。

在每个延迟的声明中,总共有 3 个延迟值可以被指定:上升延迟值、下降延迟值、截止延迟值。

格式:

1
assign #(rise,fall,turn-off) LHS_target = RHS_expression;

三个延迟值为 0 时,如何在连续赋值语句中指定这些延迟:

1
2
3
4
5
6
7
8
9
10
11
//一个延迟参数
assign #4 biu_par = fe_par ||wsp_par; //上升延迟、下降延迟、截止延迟(既变化到Z的延迟)和变化到x的延时相同,都为4

//两个延迟参数
assign #(4,8)biu_par = rd_trg; //上升延迟4、下降延迟8、截止延迟和x的延迟相同,为4和8的最小值,4

//三个延迟参数
assign #(4,8,6)fe_arb = &fe_dbus; //上升延迟4、下降延迟8、截止延迟6 变化到x的延迟为4

//没有延迟参数
assign fe_dbus = rd_address[7:4]; //所有延迟为0

右侧表达式的值从非 0 向量变化到 0 向量,则使用下降延迟;右侧表达式的值变化到 z,则使用截止延迟,其余的情况都使用上升延迟。

线网延迟

延迟可以在线网声明中定义,

1
wire #5 mem_write;

该延迟指的是 mem_write 驱动源的值发生改变到线网 mem_write 本身的值发生改变的延迟。

1
assign #2 mem_write = chunk_valid & flop_valid;

假设 flop_valid 变化导致重新计算右侧表达式,结果和以前不同,则在 2 个时间单位后,重新赋值给 mem_write;但是 mem_write 的线网延迟,对线网 mem_write 的赋值发生在 7 个时间单位后。

首先赋值延迟起作用,然后加上线网延迟产生的作用;

线网声明赋值中,指定了延迟,这个延迟不是线网延迟,是赋值延迟。nc_data 进行的线网声明赋值,2 个时间单位指的是赋值延时,不是线网延迟(线网延迟,声明线网时不能有赋值)

1
wire [3:0] #2 nc_data = si_data-mem_wdata;

主/从触发器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module msdff_dataflow(
input wire d, clk,
output wire q,q_bar
);
wire not_clk, not_d,not_y,y,d1,d2,y_bar,y1,y2;
assign not_d =~d;
assign not_clk =~clk;
assign not_y = ~y;

assign d1 =~(d & clk);
assign d2 = ~(clk & not_d);



endmodule

行为级建模

过程性结构

两种语句是对设计进行行为级建模的主要结构 initial、always。模块中可以包含任意个 initial、always 语句,这些语句相互之间是并行执行。一条 initial、always 语句的执行会产生一个单独的控制流,所有的 initial、always 都是在 0 时刻开始并行的执行。

initial 语句只执行一次,在仿真开始时(0 时刻)执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
parameter SIZE = 1024;
reg [7:0] vld_ram[0:SIZE-1];
reg speed_reg;

initial
begin: seq_blk_a
integer index;

speed_reg = 0;

for(index = 0;index < SIZE; index = index + 1'b1)
vld_ram[index] = 0;

end

begin ... end 划定顺序块的界限,顺序块中的语句是顺序执行的

seq_blk_a 顺序块的标签,若过程中没有出现局部声明语句,不需要标签

always 语句

always 语句是反复执行的,与 initial 语句类似地方都是从 0 时刻开始执行

事件列表写成:@(a or b)、@(a,b)、@(*)

时序控制

延迟控制、事件控制

延迟控制

1
#2 tx_addr = rx_addr - 5;	//等待延迟,执行过程语句前等待2个单位延迟

延迟控制的延迟可以是任意表达式,不必限制为一个常量。

延迟表达式的值为 0,则称为显式零延迟。

1
#0;

显式零延迟触发一个等待,等待所有其他在当前仿真时刻要被执行的事件执行完毕后,才将其恢复,仿真事件不会前进

若延迟表达式的值为 x、z,等效于零延迟。若延迟表达式计算结果为负值,则将其二进制补码值作为延迟

若延迟值包含空格字符、表达式,需要用括号把延迟值括起来

事件控制

事件控制中,语句的执行是基于事件的:跳变沿敏感事件控制、电平敏感事件控制

跳变沿敏感事件控制 @(negedge rst_n)、@(posedge clk)

电平敏感事件控制 @cell_byte

1
@cell_byte per_frame= cell_byte; //当cell_byte上有事件发生时,才会执行赋值操作
负跳变正跳变
1 -> x0 -> x
1 -> z0 -> z
1 -> 00 -> 1
x -> 0x -> 1
z -> 0z -> 1

通过一种方式可以隐含地把相应的过程性语句中所有的变量和线网都包含在敏感事件列表中。

@* 表明相应的过程性语句对于其内部的任何值的变化都会敏感

1
2
always @ *
procedural_statement

@* 把过程性语句内部所有的变量都看作是敏感事件列表的一部分

1
2
3
4
5
always @* cpu_reg = master_rag + control_reg;
//隐含地把 master_rag 、control_reg 包含在敏感事件列表中。实际上是下面always语句简写格式
always
@(master_rag,control_reg)
cpu_reg = master_rag + control_reg;

@* 代表在相应块中的任何语句中使用了的变量和线网,还包括在赋值语句中的等号左边的表达式中的序号变量

1
always @* w[k] = ram&chip;

等价于

1
@(ram,chip,k)

电平敏感事件控制

电平敏感事件控制中,直到条件变为真后,过程性语句才执行

1
2
wait(condition)  //等到 condition 为真的时候,才执行下面的语句
procedural_statement

语句块(begin、end)

  1. 顺序语句块(begin、end):语句块中的语句按照给定顺序执行
  2. 并行语句块(fork、join):语句并行执行

语句块的标识符是可选的。若有标识符,可以在语句块内部声明局部变量,还可以被引用

顺序语句块

语句按顺序执行;定界符是 begin、end

顺序块语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
begin
[:block_id{declarations}]//{块id[声明]} 标识符
procedural_statement(s) //过程型语句 执行部分
end

begin //还可以这么写顺序块
mem_r = sms|mem;

@(negedge clk_sms)
update = mem_rd;
end

begin: lbl_seq_blok //lbl_seq_blok 标识符
reg[1:0] dmac;
dmac = 2'd1;
end

顺序块标记 lbl_seq_blok;

并行语句块

定界符是 fork、join;并行语句块中的语句是并行执行的

在并行语句块内的每条语句中指定的延迟值都是相对于语句块开始执行的时刻的;并行语句块中的最后一个行为(并不一定是最后一条语句)执行完成时,再继续执行这个并行块后面其他语句。在执行跳出语句块前必须执行完并行语句块内的所有语句。

就是将并行语句块中所有的语句并行执行完成后,再执行并行块后面的语句

1
2
3
4
fork
[:block_id{declarations}] //{块id[声明]} 标识符
procedural_statement(s)
join

顺序语句块和并行语句块可以混合使用,下面是两者不同之处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
always
begin :blk_seq_a
#4 pm_write = 5; //S1

fork:blk_par_a //S2
#6 pm_select = 7; //P1

begin :blk_seq_b //P2
wdog_rst = pm_enable; //S6
#5 wdog_intr = wdog_rst;//S7
end

#2 frc_sel = 3; //P3
#4 pm_itcr = 2; //P4
#8 itop = 4; //P5
join

#8 pm_sel = 3; //S3
#4 pcell_id = 52; //S4
#6 $stop; //S5
end

blk_seq_a 顺序语句块,并且块内所有语句(S1、S2、S3、S4、S5)都是按照顺序执行

过程性赋值

过程性赋值是在 initial、always 语句内进行的赋值,只能对变量类型赋值。

语句内部延迟

语句内部延迟之前计算的右式,然后进入延迟等待,最后把重新计算的值赋给左式的目标

重复事件控制的时序控制 格式:

1
repeat(expression) @(event_expression)

这种控制格式是利用一个或多个事件发生次数来指定延迟

1
hresult = repeat(2) @(negedge tclk) hw_data + hr_data;

即 hw_data + hr_data 的值,然后等待时钟 tclk 上出现 2 个负跳变沿,再把右边值赋给 hresult。

等价:

1
2
3
4
5
6
begin
temp = hw_data + hr_data;
@(negedge tclk);
@(negedge tclk);
hresult = temp;
end

阻塞性过程赋值

赋值操作符是 = 的过程性赋值称为阻塞性过程赋值

1
a = 52;

阻塞性过程赋值语句,当前语句必须执行完成才能执行下一条赋值语句。

非阻塞性过程赋值

非阻塞性过程赋值语句中,使用赋值符号 <=

特殊例子

1
2
3
4
5
initial
begin
wdog_intr <= 0;
wdog_intr <= 1;
end

在 initial 语句执行后,wdog_intr 的值为 1;Verilog HDL 标准规定了对同一个 reg 变量的非阻塞性赋值,按照赋值语句执行的顺序来执行;wdog_intr 先被赋值为 0,然后被赋值为 1

一个在 always 语句里读取一个变量(key_a)的值,又在另一条 always 语句里面对同一个变量进行赋值,并且两条 always 语句由同一个时钟沿来控制,怎么才能确定读操作发生在写操作之前?

答案是:用非阻塞赋值可以确保读操作发生在写操作之前。

1
2
3
4
5
always @(negedge clock)
reg_a <= data; //在此处reg_a 被赋值

always @(negedge clock)
reg_b <= reg_a;

何时使用哪种过程性赋值语句?(建议)

  • 对 always 语句块外用到的变量进行赋值时,使用非阻塞性赋值(<=)
  • 计算中间结果的时候,用阻塞性赋值(=)
1
2
3
4
5
6
7
8
always @(posedge clk_piol) begin
bdir_data = ^spi_rdata; //用阻塞性赋值计算中间结果
spi_wdata <= bdir_data + rst_cnt; //用非阻塞赋值对语句块外变量赋值
end

always @(spi_wdata) begin
spi_pariy <= ^ spi_wdata;
end

连续赋值(assign)与过程赋值的比较

过程赋值与连续赋值间的不同之处

过程赋值连续赋值
出现在 initial、always 语句中出现在模块(module)中
过程赋值语句的执行与其周围的其他语句是有关系的与其他语句并行执行;在右侧操作数的值发生变化时执行
驱动变量驱动线网
使用 =、<= 赋值符号使用 = 赋值符号
无 assign 关键词使用 assign 关键词

条件语句

1
2
3
4
5
6
7
8
9
if(condition_l) begin
procedural_statement_1
end
else if (condition_2) begin
procedural_statement_2
end
else begin
procedural_statement_3
end

case 语句

条件表达式和各分支项表达式都不必是常量表达式,在 case 语句中,x、z 值作为字符值进行比较。

case 语句的条件表达式和分支表达式的长度不同会出现什么情况呢?进行任何比较前,把 case 语句中所有的表达式的位宽都统一为这些表达式中最长的一个的位宽。

case 语句中的无关位

case 语句的两种其他形式:casez、casex,这些形式对 x、z 值使用了不同的解释。语法与 case 语句相同,关键字不同

casez 语句,在 casez 条件表达式和任意分支项表达式中的值为 z 的位都会被认为是无关位,即那个位被忽略(不进行比较)

casex 语句,值为 x 或 z 的位都会被认为是无关位。

casez 语句示例:

1
2
3
4
5
6
casez(intr_mask)
4'b1???:rtc_wdata[4] = 0;
4'b01??:rtc_wdata[3] = 0;
4'b001?:rtc_wdata[2] = 0;
4'b0001:rtc_wdata[1] = 0;
endcase

字符 ? 可以用来代替字符 Z,来表示无关位。

casez 语句:若 intr_mask 的第 1 位是 1(忽略 intr_mask 的其他位),则 rtc_wdata[4] = 0;

casez 语句:若 intr_mask 的第 1 位是 0,第 2 位是 1(忽略 intr_mask 的其他位),则 rtc_wdata[3] = 0;

依次类推

循环语句

有 4 类循环语句:forever、repeat、while、for 循环

forever 循环语句

循环语句格式:

1
2
forever
procedural_statement

此循环语句连续执行过程性语句。因此为了跳出这样的循环,可以在过程性语句内使用中止语句。同时,在过程语句中必须使用某些方式的时序控制,否则 forever 循环将在 0 延迟后永远循环下去。

forever 循环语句的示例:

1
2
3
4
5
initial begin
clk1hz = 0;
#5 forever
#10 clk1hz = ~clk1hz;
end

上面代码生成一个时钟波形,clk1hz 首先被初始化为 0,并一直保持为 0 到第 5 个单位时刻。此后每隔 10 个时间单位,clk1hz 反向一次。

repeat 循环语句 指定循环次数

语法格式

1
2
repeat (loop_count)	//循环次数
procedural_statement

按照指定的循环次数来执行过程性语句,循环计数表达式的值为 x、z,则循环的次数按照 0 处理

1
2
repeat(count)
sum = sum + 10;

循环语句与重复事件控制的不同

1
2
repeat(loop_count)			//循环语句
@(posedge clk_rtc) accum = accum + 1;

等待 clk_rtc 上出现正跳变沿,然后对 accum 进行加 1,循环执行 loop_count 次

1
accum = repeat(loop_count) @(posedge clk_rtc) accum + 1;	//重复事件控制

首先计算 accum + 1,随后等待在 clk_rtc 上出现 loop_count 次正跳变,最后赋值。

1
repeat(NUM_OF_TIMES) @(negedge zclk); //等待zclk上出现NUM_OF_TIMES下降沿,然后执行紧随在 repeat 语句之后的语句。

while 循环语句

格式:

1
2
while(condition)		// condition 状态状况
procedural_statement

while 和 C 语言相似,若条件表达式为 x、z,它也同样按照 0(假)来处理。

1
2
3
4
while(shift_by > 0) begin
acc = acc << 1;
shift_by = shift_by - 1;
end

for 循环语句

1
2
for(initial_assignment;condition;step_assignment)	//初始值 条件 步骤
procedural_statement

for 循环语句会重复执行过程性语句若干次。与 C 语言相似

1
2
3
4
5
6
7
8
9
10
11
12
13
integer k;

for(k = 0; k < MAX_RANGE ; k = k +1) begin
if(hold_data[k] == 0) begin
hold_data[k] = 1;
end
else if(hold_data[k] == 1) begin
hold_data[k] = 0;
end
else begin
$display("hold_data[k] is an x or a z")
end
end

过程性连续赋值

过程性连续赋值语句是一种能够在 always、initial 语句块中出现的语句。这种赋值方式可以改写(Override)所有其他语句对线网或者变量赋值。它允许赋值语句中的表达式被连续地驱动进入到变量或线网当中去。

过程连续赋值语句与连续赋值语句是有区别的,连续赋值语句只能出现在 initial、always 语句之外

过程性连续赋值语句有两种类型(推测成对出现):

  1. assign、deassign 过程性语句:对变量进行赋值
  2. force、release 过程性语句:虽然也可以用于对变量赋值,但主要用于对线网赋值。

assign、force 语句在某种意义上是“连续的”:就是当 assign、force 语句生效时,右式中的操作数的任何变化都会引起赋值语句的重新执行。过程性连续赋值的目标不能是变量的部分选择或位选择

assign、deassign 语句

assign 过程性语句可以改写所有的过程性赋值语句对变量进行的赋值。

deassign 过程性语句用来结束对变量的连续赋值。变量中的值一直保留到它被重新赋值为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
module d_flip_flop(d,clear,clock,q);
input d,clear,clock;
output reg q;

always @(clear)
if(!clear)
assign q = 0; //d对q无效
else
deassign q;

always @(negedge clock) q <= d;

endmodule

若 clear 变为 0,assign 过程性语句使 q 清零,而不考虑任何时钟沿时的情况,即 clock 中 d 对 q 没有影响。

若 clear 变为 1,deassign 语句被执行;这就使得连续性赋值被取消,以后 clock 就能够对 q 产生影响。

若 assign 语句应用于一个已经用 assign 进行赋值的变量,则先取消原来 assign 语句的赋值,然后再进行新的过程性连续赋值。

1
2
3
4
5
6
7
8
9
10
11
12
reg[3:0]load_ctr;
...
load_ctr = 0;
...
assign load_ctr = nibble^rtc_count;
...
assign load_ctr = 2; //先取消前面对load_ctr的assign 赋值,然后进行新的过程性连续赋值

...
deassign load_ctr; //load_ctr 一直保持值为2
...
assign load_ctr[2] = 1;/* 错误 :reg变量的位选择不能够作为过程性连续赋值的目标*/

第 2 个 assign 语句在进行下一次赋值前先使得前面的第一个 assign 语句无效。在 deassign 语句执行后,load_ctr 的值将一直保持为 2,直到出现另一个对该变量的过程性连续赋值。

assign 语句在某种意义上是“连续性”的;即在第 1 个 assign 语句执行后到第 2 个 assign 语句开始执行前,nibble、rtc_count 上的任何变化将使得第 1 个 assign 语句被重新计算。

force、release 语句

force、release 过程性语句与 assign、deassign 语句非常相似,不同之处是 force、release 过程语句不仅能够应用于线网,还能够应用于变量。

当 force 语句应用于变量时,变量的当前值被 force 语句中表达式的值覆盖;当 release 语句应用于变量时,变量中的当前值保持不变,除非对它进行过程性连续赋值(在 force 语句被执行时),这种情况下,连续赋值为变量确立一个新值。

当用 force 过程性语句对线网进行赋值时,该赋值语句将忽略线网所有的其他驱动源,直到对该线网执行 release 语句。

1
2
3
4
5
6
7
8
9
10
wire test_reset;
...
or #1(test_reset,penable,rtc_intr);

initial begin
force test_reset = penable&rtc_intr;
#5;
release test_reset;

end

force 语句的执行使得 test_reset 值(由 penable & rtc_intr 求得),改写来自于或门基元(原语)的值,直到执行 release 语句,才恢复由或门原语输出驱动 test_reset 重新生效。

当 force 语句有效的时候(前 5 个时间单位内),在 penable 和 rtc_intr 上的任何变化都会使赋值语句被重新执行。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
reg[2:0]pr_data;
...
pr_data = 2;
force pr_data = 1;
...
release pr_data; //pr_data 保持值1
...
assign pr_data = 5;
...
force pr_data = 3; //当用force过程性语句对线网进行赋值时,该赋值语句将忽略线网所有的其他驱动源
...
release pr_data ; //pr_data 的值变为 5
...
force pr_data[1:0] = 3; /* 错误:reg变量的部分选择不能被设置为过程性连续赋值的目标*/

对 pr_data 的第 1 次 release 使得 pr_data 的值保持为 1。因为在 force 语句生效时刻没有别的过程性连续赋值语句对该变量进行赋值。

在执行后一个 release 语句后,由于对 pr_data 的过程性连续赋值语句又开始生效,pr_data 被重新赋值为 5。

握手协议示例

always 语句可以用于描述交互进程的行为,如交互式有限状态机的行为。同一个模块内的语句可以通过所有 always 语句都可见的变量来进行相互通信。不建议使用在 always 内部声明 reg 变量在 always 语句之间传递信息。