第5章Verilog语句语法 Verilog HDL支持许多行为语句,使其成为结构化和行为性的语言,这些行为语句包括过程语句、块语句、赋值语句、条件语句、循环语句、编译指示语句等,如表5.1所示。 表5.1Verilog HDL的行为语句 类别语句可综合 过程语句 initial always√ 块语句 串行块beginend√ 并行块forkjoin 赋值语句 持续赋值assign√ 过程赋值=、<=√ 条件语句 ifelse√ case√ 循环语句 for√ repeat while forever 编译指示语句 `define√ `include `ifdef, `else, `endif√ 几乎所有的HDL语句都可用于仿真,但可综合的语句通常只是HDL语句的一个核心子集,不同综合器支持的HDL语句集通常有所不同。学习行为语句时,应该对语句的可综合性有所了解。目前,可综合的Verilog子集也在向标准化发展,已经推出的IEEE Std 1364[2].1—2002标准为Verilog语言的RTL级综合定义了一系列的建模准则。 编写HDL程序,就是在描述一个电路,每一段程序都对应着相应的硬件电路结构,应深入理解两者的关系。综合器可将HDL文本对应的硬件电路以图形的方式呈现出来,便于学习者建立HDL程序与硬件电路之间的对应关系。 5.1过程语句 Verilog语言中的多数过程模块都从属于always和initial两种过程语句。 在一个模块(module)中,使用always和initial语句的次数是不受限制的。always块内的语句是不断重复执行的; always过程语句是可综合的,在可综合的电路设计中广泛采用。initial语句常用于仿真中的初始化; initial过程块中的语句只执行一次。 5.1.1always过程语句 always过程语句使用模板如下: always @(<敏感信号列表sensitivity list>) begin //过程赋值 //if-else,case,casex,casez选择语句 //while,repeat,for循环 //task,function调用 end always过程语句通常带有触发条件。触发条件写在敏感信号表达式中,只有当触发条件满足时,其后的beginend块语句才能被执行。因此,此处首先讲解敏感信号列表(Sensitivity List)的含义,以及如何写敏感信号表达式。 1. 敏感信号列表 敏感信号列表,又称事件表达式或敏感信号表达式,即当该列表中变量的值改变时,就会引发块内语句的执行。因此,敏感信号列表中应列出影响块内取值的所有信号。有两个或两个以上信号时,它们之间用or连接。例如: @(a)//当信号a的值发生改变 @(a or b) //当信号a或信号b的值发生改变 @(posedge clock) //当clock的上升沿到来时 @(negedge clock) //当clock的下降沿到来时 @(posedge clk or negedge reset) //当clk的上升沿或reset信号的下降沿到来时 如例5.1中用case语句描述的4选1数据选择器,只要输入信号in0、in1、in2、in3,或选择信号sel中的任一个发生改变,输出就会改变,所以敏感信号列表写为 @ (in0 or in1 or in2 or in3 or sel) 【例5.1】用case语句描述的4选1数据选择器。 module mux4_1 (input in0,in1,in2,in3, input[1:0] sel, output reg out); always @(in0 or in1 or in2 or in3 or sel)//敏感信号列表 case(sel) 2'b00: out=in0; 2'b01: out=in1; 2'b10: out=in2; 2'b11: out=in3; default:out=2'bx; endcase endmodule 敏感信号分为边沿敏感型和电平敏感型两种。每个always过程最好只由一种类型的敏感信号来触发,避免将边沿敏感型和电平敏感型信号列在一起。例如下面的例子: always @(posedge clk or posedge clr) //两个敏感信号都是边沿敏感型 always @(A or B) //两个敏感信号都是电平敏感型 always @(posedge clk or clr) //不建议这样用,不宜将边沿敏感型和电平敏感型信号列在一起 2. posedge与negedge关键字 对于时序电路,事件通常是由时钟边沿触发的。为表达边沿这个概念,Verilog HDL提供了posedge和negedge两个关键字来描述。 【例5.2】同步置数、同步清零的计数器。 module count //模块声明采用Verilog-2001格式 (input load,clk,reset, input[7:0] data, output reg[7:0] out); always @ (posedge clk) //clk上升沿触发 begin if(!reset)out<=8'h00; //同步清零,低电平有效 else if(load)out<=data; //同步预置 else out<=out+1; //计数 end endmodule 在例5.2中,posedge clk表示时钟信号clk的上升沿作为触发条件,而negedge clk表示时钟信号clk的下降沿作为触发条件。 在例5.2中,没有将load、reset信号列入敏感信号列表,因此属于同步置数、同步清零,这两个信号要起作用,必须有时钟的上升沿到来。对于异步的清零/置数,如时钟信号为clk,clr为异步清零信号,则敏感信号列表应写为 always @(posedge clk or posedge clr) //clr信号上升沿到来时清零,故高电平清零有效 always @(posedge clk or negedge clr) //clr信号下降沿到来时清零,故低电平清零有效 若有其他异步控制信号,可按此方式加入。 注意: 块内的逻辑描述要与敏感信号列表中信号的有效电平一致。 例如,下面的描述是错误的。 always @(posedge clk or negedge clr)//低电平清零有效 begin if(clr) out<=0; //与敏感信号列表中低电平清零有效矛盾,应改为if(!clr) else out<=in; end 3. Verilog2001标准对敏感信号列表的新规定 Verilog2001标准对敏感信号列表做了新的规定。 (1) 敏感信号列表中可用逗号分隔敏感信号。 在Verilog2001中,可用逗号分隔敏感信号。例如: always @(a or b or cin) always @(posedge clk or negedge clr) 上面的语句按照Verilog2001标准可写为下面的形式。 always @(a,b,cin) //用逗号分隔信号 always @(posedge clk,negedge clr) (2) 在敏感信号列表中使用通配符*。 用always过程块描述组合逻辑时,应在敏感信号列表中列出所有的输入信号,在Verilog2001中,可用通配符*来表示包括该过程块中的所有信号变量。 例如,在Verilog1995中,一般这样写敏感信号列表: always @(a or b or cin) {cout,sum}=a+b+cin; 上面的敏感信号列表在Verilog2001中可表示为如下两种形式,这两种形式是等价的。 always @* //形式1 {cout,sum}=a+b+cin; always @(*) //形式2 {cout,sum}=a+b+cin; 4. 用always过程块实现较复杂的组合逻辑电路 always过程语句通常用来对寄存器类型的数据进行赋值,但always过程语句也可以用来设计组合逻辑。在有些情况下,使用assign实现组合逻辑电路会显得冗长且效率低下,而适当采用always过程语句来实现,能收到更好的效果。 例5.3是一个指令译码电路的例子。该例通过指令判断对输入数据执行相应的操作,包括加、减、求与、求或、求反。这是一个较为复杂的组合逻辑电路,如果采用assign语句描述,表达起来非常复杂。在本例中使用了电平敏感的always块,并采用case结构来进行分支判断,不但设计思想得到直观的体现,而且代码看起来整齐有序。 【例5.3】用always过程语句描述的简单算术逻辑单元。 `define add 3'd0 `define minus 3'd1 `define band 3'd2 `define bor 3'd3 `define bnot 3'd4 module alu(out,opcode,a,b); input[2:0] opcode; //操作码 input[7:0] a,b; //操作数 output reg[7:0] out; always@* //或写为always@(*) begin case(opcode) `add: out=a+b; //加操作 `minus: out=a-b; //减操作 `band: out=a&b; //按位与 `bor: out=a|b; //按位或 `bnot: out=~a; //按位取反 default:out=8'hx; //未收到指令时,输出任意态 endcase end endmodule 图5.1是例5.3的RTL综合结果,是由加法器、门电路、数据选择器等模块构成的。 图5.1例5.3的RTL综合结果 5.1.2initial过程语句 initial语句的使用格式如下: initial begin 语句1; 语句2; … end initial语句不带触发条件,initial过程中的块语句沿时间轴只执行一次。initial语句通常用于仿真模块中对激励向量的描述,或用于给寄存器变量赋初值,它是面向模拟仿真的过程语句,通常不能被逻辑综合工具支持。 下面举例说明initial语句的使用方法。如例5.4的测试模块中利用initial语句完成对测试变量a、b、c的赋值。 【例5.4】用initial过程语句对测试变量赋值。 `timescale 1ns/1ns module test; reg a,b,c; initialbegina=0;b=1;c=0; #50a=1;b=0; #50a=0;c=1; #50b=1; #50b=0;c=0; #50 $finish;end endmodule 例5.4对a、b、c的赋值相当于描述了如图5.2所示的波形。 图5.2例5.4所定义的波形 下面的代码用initial语句对memory存储器进行初始化,将其所有存储单元的初始值都置为0。 initial begin for(addr=0;addr<size;addr=addr+1) memory[addr]=0; //对memory存储器进行初始化 end 5.2块语句 块语句是由块标识符beginend或forkjoin界定的一组语句,当块语句只包含一条语句时,块标识符可以缺省。下面分别介绍串行块beginend和并行块forkjoin。 5.2.1串行块beginend beginend串行块中的语句按串行方式顺序执行。例如: begin regb=rega; regc=regb; end 由于beginend块内的语句顺序执行,最后将regb、regc的值都更新为rega的值,该beginend块执行完后,regb、regc的值是相同的。 在仿真时,beginend块中的每条语句前面的延时都是相对于前一条语句执行结束的相对时间。如例5.5,模块产生了一段周期为10个时间单位的信号波形。 【例5.5】用beginend串行块产生信号波形。 `timescale 10ns/1ns module wave1; parameter CYCLE=10; reg wave; initial beginwave=0; #(CYCLE/2)wave=1; #(CYCLE/2)wave=0; #(CYCLE/2)wave=1; #(CYCLE/2)wave=0; #(CYCLE/2)wave=1; #(CYCLE/2)$stop; end initial $monitor($time,,,"wave=%b",wave); endmodule 上面的程序用ModelSim编译仿真后,可得到一段周期为10个时间单位(100ns)的信号波形,如图5.3所示。 图5.3例5.5所描述的波形 5.2.2并行块forkjoin 并行块forkjoin中的所有语句是并发执行的。例如: fork regb=rega; regc=regb; join 由于forkjoin并行块中的语句是同时执行的,在上面的块语句执行完后,regb更新为rega的值,而regc的值更新为改变之前的regb的值,故执行完后,regb与regc的值是不同的。 在进行仿真时,forkjoin并行块中的每条语句前面的时延都是相对于该并行块的起始执行时间的。如要用forkjoin并行块产生一段与例5.5相同的信号波形,应该像例5.6这样标注时延。 【例5.6】用forkjoin并行块产生信号波形。 `timescale 10ns/1ns module wave2; parameter CYCLE=5; reg wave; initial forkwave=0; #(CYCLE)wave=1; #(2*CYCLE)wave=0; #(3*CYCLE)wave=1; #(4*CYCLE) wave=0; #(5*CYCLE) wave=1; #(6*CYCLE)$stop; join initial $monitor($time,,,"wave=%b",wave); endmodule 上面的程序用ModelSim编译仿真后,可得到与图5.3相同的信号波形。将例5.5和例5.6进行对比,可体会beginend串行块和forkjoin并行块的区别。 5.3赋值语句 5.3.1持续赋值与过程赋值 Verilog语言有持续赋值与过程赋值两种赋值方式。 1. 持续赋值语句 assign为持续赋值语句(Continuous Assignment),主要用于对wire型变量的赋值。例如: assign c=a&b; 在上面的赋值中,a、b、c皆为wire型变量,a和b信号的任何变化,都将随时反映到c上来。例5.7是用持续赋值方式定义的2选1多路选择器。 【例5.7】用持续赋值方式定义2选1多路选择器。 module mux2_1//模块声明采用Verilog-2001格式 (input a,b,sel, output out); assign out=(sel==0)?a:b; //持续赋值,如果sel为0,则out=a; 否则out=b endmodule 例5.8采用assign语句描述了一个基本RS触发器,图5.4是其综合结果。 【例5.8】基本RS触发器。 module rs_ff (input r,s, output q,qn); assign qn=~(r & q); assignq=~(s & qn); endmodule 图5.4基本RS触发器综合结果 例5.9采用持续赋值语句实现了对8位带符号二进制数的求补码运算; 图5.5是该例的综合结果,采用的是按位取反再加1的实现方法。 【例5.9】用持续赋值语句实现对8位带符号二进制数的求补码运算。 module buma ( input[7:0] ain, //8位二进制数 output[7:0] yout); //补码输出信号 assign yout=~ain+1; //求补 endmodule 图5.5求补电路综合结果 2. 过程赋值语句 过程赋值语句(Procedural Assignment)多用于对reg型变量进行赋值。过程赋值有非阻塞赋值和阻塞赋值两种方式。 (1) 非阻塞(non_blocking)赋值方式: 赋值符号为“<=”。例如: b<=a; 非阻塞赋值在整个过程块结束时才完成赋值操作,即b的值并不是立刻改变的。 (2) 阻塞(blocking)赋值方式: 赋值符号为“=”。例如: b=a; 阻塞赋值在该语句结束时就立即完成赋值操作,即b的值在该条语句结束后立刻改变。如果一个块语句中有多条阻塞赋值语句,那么在前面的赋值语句完成之前,后面的语句不能被执行,仿佛被阻塞了(blocking)一样,因此称为阻塞赋值方式。例5.10是用阻塞赋值方式定义的2选1多路选择器。 【例5.10】用阻塞赋值方式定义2选1多路选择器。 module mux2_1_block (input a,b,sel, output reg out); always @* beginif(sel==0) out=a; else out=b;end endmodule 5.3.2阻塞赋值与非阻塞赋值 阻塞赋值方式和非阻塞赋值方式的区别常给设计人员带来问题。为弄清非阻塞赋值与阻塞赋值的区别,可见例5.11。 【例5.11】非阻塞赋值与阻塞赋值。 //非阻塞赋值模块 module non_block (input clk,a, output reg c,b); always @(posedge clk) begin b<=a; c<=b; end endmodule //阻塞赋值模块 module block (input clk,a, output reg c,b); always @(posedge clk) begin b=a; c=b; end endmodule 将上面两段代码进行综合和仿真,可分别得到如图5.6和图5.7所示的波形。 图5.6非阻塞赋值的时序仿真波形 图5.7阻塞赋值的时序仿真波形 从图中可看出二者的区别: 对于非阻塞赋值,c的值落后b的值一个时钟周期,这是因为该always块中两条语句是同时执行的,因此每次执行完后,b的值得到更新,而c的值仍是上一时钟周期的b值。对于阻塞赋值,c的值和b的值相同,因为b的值是立即更新的,更新后又赋给了c,因此c与b的值相同。 综合后的电路分别如图5.8和图5.9所示。 图5.8非阻塞赋值综合结果 图5.9阻塞赋值综合结果 通过上面的讨论可以认为,在always过程块中,阻塞赋值可以理解为赋值语句是顺序执行的,而非阻塞赋值可以理解为赋值语句是并发执行的。为避免出错,在同一块内,最好不要将输出再作为输入使用。为使阻塞赋值方式完成与上述非阻塞赋值同样的功能,可采用两个always块来实现,如下面的代码所示。其中的两个always过程块是并发执行的。 module non_block(input clk,a, output reg c,b); always @(posedge clk) beginb=a;end always @(posedge clk) beginc=b;end endmodule 阻塞赋值与非阻塞赋值是学习Verilog语言的难点之一,这两种赋值方式将在后面进一步讲解。 5.4条件语句 条件语句有ifelse语句和case语句两种,都属于顺序语句,应放在always块内。下面分别介绍这两种语句。 5.4.1ifelse语句 if语句的格式与C语言中的ifelse语句格式类似,使用方法有以下几种。 (1)if(表达式) 语句1; //非完整性if语句 (2)if(表达式) 语句1; //二重选择的if语句 else 语句2; (3)if(表达式1)语句1; //多重选择的if语句 else if(表达式2)语句2; else if(表达式3) 语句3; … else if(表达式n) 语句n; else 语句n+1; 在上述方式中,“表达式”一般为逻辑表达式或关系表达式,也可能是1位的变量。系统对表达式的值进行判断,若为0、x、z,则按“假”处理; 若为1,则按“真”处理,执行指定语句。语句可以是单句,也可以是多句,多句时用beginend块语句括起来。if语句也可以多重嵌套,对于if语句的嵌套,若不清楚if和else的匹配,最好用beginend语句括起来。 下面举例说明if语句常用的几种使用方法。 1. 二重选择的if语句 首先判断条件是否成立,如果if语句中的条件成立,那么程序会执行语句1; 否则,程序执行语句2。如例5.12是用二重选择的if语句描述的三态非门。 【例5.12】二重选择if语句描述的三态非门。 module tri_not(x,oe,y); input x,oe; output reg y; always @(x,oe) beginif(!oe)y<=~x; elsey<=1'bZ; end endmodule 2. 多重选择的if语句 例5.13用多重选择if语句描述了一个1位二进制数比较器。 【例5.13】比较两个1位二进制数大小。 module compare1(a,b,less,equ,larg); input a,b; output reg less,equ,larg; always @(a,b) begin if(a>b) begin larg<=1'b1;equ<=1'b0;less<=1'b0;end else if(a==b) begin equ<=1'b1;larg<=1'b0;less<=1'b0;end else begin less<=1'b1;larg<=1'b0;equ<=1'b0;end end endmodule 3. 多重嵌套的if语句 if语句可以嵌套,多用于描述具有复杂控制功能的逻辑电路。 多重嵌套的if语句的格式如下。 if(条件1)语句1; if(条件2)语句2; ... 例5.14是用多重嵌套的if语句实现的模为60的8421 BCD码加法计数器。 【例5.14】模为60的8421 BCD码加法计数器。 module count60//模块声明采用Verilog-2001格式 ( input load,clk,reset, input[7:0] data, output reg[7:0] qout, output cout); always @(posedge clk) //时钟上升沿时计数 begin if(reset)qout<=0; //同步复位 else if(load) qout<=data; //同步置数 elsebegin if(qout[3:0]==9) //低位是否为9 begin qout[3:0]<=0; //回0 if(qout[7:4]==5)qout[7:4]<=0; //判断高位是否为5,若是,则回0 else qout[7:4]<=qout[7:4]+1; //高位不为5,则加1 end else qout[3:0]<=qout[3:0]+1; //低位不为9,则加1 end end assign cout=(qout==8'h59)?1:0; //产生进位输出信号 endmodule 5.4.2case语句 相对if语句只有两个分支而言,case语句是一种多分支语句,故case语句多用于多条件译码电路,如描述译码器、数据选择器、状态机及微处理器的指令译码等。case语句有case、casez、casex三种表示方式,这里分别予以说明。 1. case语句 case语句的使用格式如下: case (敏感表达式) 值1: 语句1;//case分支项 值2: 语句2; … 值n: 语句n; default: 语句n+1; endcase 当敏感表达式的值为1时,执行语句1; 值为2时,执行语句2; 以此类推; 若敏感表达式的值与上面列出的值都不符,则执行default后面的语句。若前面已列出了敏感表达式所有可能的取值,则default语句可以省略。 例5.15是一个用case语句描述的3人表决电路,其综合结果见图5.10,该电路由与门、或门实现。 【例5.15】用case语句描述3人表决电路。 module vote3//模块声明采用Verilog-2001格式 ( input a,b,c, output reg pass); always @(a,b,c) begin case({a,b,c}) //用case语句进行译码 3'b000,3'b001,3'b010,3'b100: pass=1'b0; //表决不通过 3'b011,3'b101,3'b110,3'b111: pass=1'b1; //表决通过 //注意多个选项间用逗号,连接 default: pass=1'b0; endcase end endmodule 图5.103人表决电路综合结果 下面的例子是用case语句编写的BCD码7段数码管译码电路,实现4位8421 BCD码到7段数码管显示译码的功能。7段数码管实际上是由7个长条形的发光二极管组成的(一般用a、b、c、d、e、f、g分别表示7个发光二极管),多用于显示字母、数字。图5.11是7段数码管的结构与共阴极、共阳极两种连接方式示意图。假定采用共阴极连接方式,用7段数码管显示0~9这10个数字,则相应的译码电路的Verilog描述如例5.16所示。 图5.117段数码管 【例5.16】BCD码7段数码管译码器。 module decode4_7 ( input D3,D2,D1,D0,//输入的4位BCD码 output reg a,b,c,d,e,f,g); always @* //使用通配符 begin case({D3,D2,D1,D0}) //用case语句进行译码 4'd0:{a,b,c,d,e,f,g}=7'b1111110; //显示0 4'd1:{a,b,c,d,e,f,g}=7'b0110000; //显示1 4'd2:{a,b,c,d,e,f,g}=7'b1101101; //显示2 4'd3:{a,b,c,d,e,f,g}=7'b1111001; //显示3 4'd4:{a,b,c,d,e,f,g}=7'b0110011; //显示4 4'd5:{a,b,c,d,e,f,g}=7'b1011011; //显示5 4'd6:{a,b,c,d,e,f,g}=7'b1011111; //显示6 4'd7:{a,b,c,d,e,f,g}=7'b1110000; //显示7 4'd8:{a,b,c,d,e,f,g}=7'b1111111; //显示8 4'd9:{a,b,c,d,e,f,g}=7'b1111011; //显示9 default:{a,b,c,d,e,f,g}=7'b1111110; //其他均显示0 endcase end endmodule 例5.17是用case语句描述的JK触发器。 【例5.17】用case语句描述下降沿触发的JK触发器。 module jk_ff (input clk,j,k, output reg q); always @(negedge clk) begin case({j,k}) 2'b00: q<=q; //保持 2'b01: q<=1'b0; //置0 2'b10: q<=1'b1; //置1 2'b11: q<=~q; //翻转 endcase end endmodule 从例5.17可以看出,用case语句描述实际上就是将模块的真值表描述出来,如果已知模块的真值表,不妨用case语句对其进行描述。 2. casez与casex语句 在case语句中,敏感表达式与值1~n的比较是一种全等比较,必须保证两者的对应位全等。casez与casex语句是case语句的两种变体,在casez语句中,如果分支表达式某些位的值为高阻z,那么对这些位的比较就不予考虑,因此只需关注其他位的比较结果。而在casex语句中,则把这种处理方式进一步扩展到对x的处理。也就是说,如果比较的双方有一方的某些位的值是x或z,那么这些位的比较就都不予考虑。 表5.2是case、casez和casex进行比较时的规则。 表5.2case、casez和casex语句的比较规则 case01xzcasez01xzcasex01xz 010000100101011 101001010110111 x0010x0011x1111 z0001z1111z1111 此外,还有另一种标识x或z的方式,即用表示无关值的符号“?”来表示。例如: case(a) 2'b1x:out=1;//只有a=1x,才有out=1 casez(a) 2'b1x:out=1;//如果a=1x、1z,则有out=1 casex(a) 2'b1x:out=1;//如果a=10、11、1x、1z等,都有out=1 casez(a) 3'b1??:out=1; //如果a=100、101、110、111或1xx、1zz等,都有out=1 3'b01?:out=1; //如果a=010、011、01x、01z,都有out=1 例5.18是一个采用casez语句描述并使用了符号“?”的数据选择器的例子。 【例5.18】用casez语句描述数据选择器。 module mux_casez (input a,b,c,d, input[3:0] select, output reg out); always @* begin casez(select) 4'b???1:out=a; 4'b??1?:out=b; 4'b?1??:out=c; 4'b1???:out=d;//不需再加default语句 endcase end endmodule 在使用条件语句时,应注意列出所有条件分支,否则,编译器认为条件不满足时,会引进一个触发器保持原值。这一点可用于设计时序电路,而在组合电路设计中,应避免这种隐含触发器的存在。当然,一般不可能列出所有分支,因为每一变量至少有4种取值: 0、1、z、x。为包含所有分支,可在if语句最后加上else语句; 在case语句的最后加上default语句。 例5.19是一个隐含锁存器的例子。 【例5.19】隐含锁存器。 module buried_ff (input b,a, output reg c); always @(a or b) begin if((b==1)&&(a==1))c=a&b; end endmodule 设计者原意是设计一个2输入与门,但因if语句中没有else语句,在对此语句逻辑综合时会默认else语句为“c=c; ”,即保 图5.12隐含锁存器 持不变,所以形成了一个隐含锁存器。该例的综合结果如图5.12所示。 在对例5.16仿真,出现a=1且b=1,c=1之后c的值会一直维持为1。为改正此错误,只需加上“else c=0;”语句即可,即 always @(a or b) beginif((b==1)&&(a==1)) c=a&b; else c=0; end 5.5循环语句 Verilog语言中存在4种类型的循环语句,用来控制语句的执行次数,分别如下。 (1) for: 有条件的循环语句。 (2) repeat: 连续执行一条语句n次。 (3) while: 执行一条语句直到某个条件不满足。 (4) forever: 连续地执行语句; 多用在initial块中,以生成时钟等周期性波形。 5.5.1for语句 for语句的使用格式如下(同C语言)。 for(循环变量赋初值; 循环结束条件; 循环变量增值) 执行语句; 下面通过7人表决器的例子说明for语句的使用: 通过一个循环语句统计赞成的人数,若超过4人赞成,则通过。用vote[7:1]表示7人的投票情况,1代表赞成,即vote[i]为1代表第i个人赞成,pass=1表示表决通过。 【例5.20】用for语句描述7人投票表决器。 module voter7 (input[7:1] vote, output reg pass); reg[2:0] sum; integer i; always @(vote) beginsum=0; for(i=1;i<=7;i=i+1)//for语句 if(vote[i]) sum=sum+1; if(sum[2]) pass=1; //若超过4人赞成,则pass=1 elsepass=0; end endmodule 例5.21中用for循环语句实现了两个8位二进制数的乘法操作。 【例5.21】用for语句实现两个8位二进制数相乘。 module mult_for//模块声明采用Verilog-2001格式 #(parameter SIZE=8) (input[SIZE:1] a,b, //操作数 output reg[2*SIZE:1] outcome); //结果 integer i; always @(a or b) beginoutcome<=0; for(i=1;i<=SIZE;i=i+1) //for语句 if(b[i]) outcome<=outcome+(a<<(i-1)); end endmodule 例5.22是一个用for循环语句生成奇校验位的例子。 【例5.22】用for循环语句生成奇校验位。 module parity_check (input[7:0] a, output reg y); integer i; always @(a) beginy=1'b1; //注意此处不能采用非阻塞赋值<= for(i=0;i<=7;i=i+1) //for语句 y=y ^ a[i];end //此处不能采用非阻塞赋值<= endmodule 在例5.22中,for循环语句执行1a[0]a[1]a[2]a[3]a[4]a[5]a[6]a[7]运算,综合后生成的RTL综合结果如图5.13所示。如果将变量y的初值改为0,则例5.22变为偶校验电路。 图5.13奇校验电路RTL综合结果 大多数综合器都支持for循环语句,在可综合的设计中,若需使用循环语句,应首先考虑用for语句实现。 5.5.2repeat、while、forever语句 1. repeat语句 repeat语句的使用格式如下。 repeat(循环次数表达式) begin 语句或语句块 end 例5.23利用repeat循环语句和移位运算符实现了两个8位二进制数的乘法。 【例5.23】用repeat实现两个8位二进制数的乘法。 module mult_repeat #(parameter SIZE=8) (input[SIZE:1] a,b, output reg[2*SIZE:1] result); reg[2*SIZE:1] temp_a; reg[SIZE:1] temp_b; always @(a or b) begin result=0; temp_a=a; temp_b=b; repeat(SIZE) //repeat语句,SIZE为循环次数 begin if(temp_b[1]) //如果temp_b的最低位为1,就执行下面的加法 result=result+temp_a; temp_a=temp_a<<1; //操作数a左移1位 temp_b=temp_b>>1; //操作数b右移1位 end end endmodule 2. while语句 while语句的使用格式如下。 while(循环执行条件表达式) begin 语句或语句块 end while语句在执行时,首先判断循环执行条件表达式是否为真,若为真,则执行后面的语句或语句块; 然后回头判断循环执行条件表达式是否为真,若为真,再执行一遍后面的语句,如此不断,直到条件表达式不为真。因此,在执行语句中必须有一条改变循环执行条件表达式值的语句。 例如在下面的代码中,利用while语句统计rega变量中1的个数。 begin : count1s reg [7:0] tempreg; count = 0; tempreg = rega; while (tempreg) begin if(tempreg[0]) count = count + 1; tempreg = tempreg >> 1; end end 下面的例子分别用while和repeat语句显示4个32位整数。 module loop1; integer i; initial//repeat循环 begin i=0; repeat(4) begin $display("i=%h",i);i=i+1; endend endmodule module loop2; integer i; initial//while循环 begini=0; while(i<4) begin $display("i=%h",i);i=i+1; endend endmodule 用ModelSim软件运行,其输出结果均为 i=00000001//i是32位整数 i=00000002 i=00000003 i=00000004 3. forever语句 forever语句的使用格式如下。 foreverbegin 语句或语句块 end forever循环语句连续不断地执行后面的语句或语句块,常用于产生周期性的波形。forever语句多用在initial语句中,若要用它进行模块描述,可用disable语句进行中断。 5.6编译指示语句 Verilog语言和C语言一样提供了编译指示功能。Verilog语言允许在程序中使用特殊的编译指示(Compiler Directive)语句,在编译时,通常先对这些指示语句进行预处理,然后再将预处理的结果和源程序一起进行编译。 指示语句以符号“`”开头,以区别于其他语句。Verilog HDL提供了十几条编译指示语句,如`define、`ifdef、`else、`endif、`restall等。比较常用的有`define、`include和`ifdef、`else、`endif。下面分别介绍这些常用语句。 1. 宏替换语句`define `define语句用于将一个简单的名字或标识符(或称为宏名)代替一个复杂的名字、字符串或表达式,其使用格式为 `define 宏名(标识符) 字符串 例如: `define sum ina+inb+inc 在上面的语句中,用宏名sum代替一个复杂的表达式ina+inb+inc,采用这样的定义后,程序中出现 assign out=`sum+ind;//等价于out=ina+inb+inc+ind; 再如: `define WORDSIZE 8 reg[`WORDSIZE:1] data;//相当于定义reg[8:1] data; 从上面的例子可以看出: (1) `define宏定义语句行末是没有分号的。 (2) 在引用已定义的宏名时,必须在宏名的前面加上符号“`”,以表示该名字是一个宏定义的名字。 (3) `define的作用范围是跨模块(module)的,可以是整个工程,就是说,在一个模块中定义的`define指令可以被其他模块调用,直到遇到`undef失效。所以用`define定义常量和参数时,一般习惯将定义语句放在模块外。与`define相比,用parameter定义的参数作用范围只限于本模块内,但上层模块例化下层模块时,可通过参数传递,重新定义下层模块中参数的值。 2. 文件包含语句`include `include是文件包含语句,它可将一个文件全部包含到另一个文件中。其格式为 `include "文件名" `include类似于C语言中的#include <filename.h>结构,后者用于将内含全局或公用定义的头文件包含在设计文件中; `include则用于指定包含任何其他文件的内容。被包含的文件既可以使用相对路径定义,也可以使用绝对路径定义; 如果没有路径信息,则默认在当前目录下搜寻要包含的文件。`include语句后加入的文件名称必须放在双引号中。 使用`include语句时应注意以下几点。 (1) 一个`include语句只能指定一个被包含的文件。如果需要包含多个文件,则需要使用多个`include语句进行包含,多个`include语句可以写在一行,但命令行中只可以出现空格和注释。例如: `include "file1.v"`include "file2.v" (2) `include语句可以出现在源程序的任何地方。被包含的文件若与包含文件不在同一个子目录下,必须指明其路径名。 (3) 文件包含允许多重包含,如文件1包含文件2,文件2又包含文件3,等等。 3. 条件编译语句`ifdef、`else、`endif 条件编译语句`ifdef、`else、`endif 可以指定仅对程序中的部分内容进行编译,这三个语句有以下两种使用形式。 (1) 第一种使用形式: `ifdef宏名 语句块 `endif 这种形式的意思是: 若宏名在程序中被定义过(用`define语句定义),则下面的语句块参与源文件的编译; 否则,该语句块将不参与源文件的编译。 (2) 第二种使用形式: `ifdef宏名 语句块1 `else语句块2 `endif 这种形式的意思是: 若宏名在程序中被定义过(用`define语句定义),则语句块1将被编译到源文件中; 否则,语句块2将被编译到源文件中。如例5.24所示。 【例5.24】条件编译举例。 module compile ( input a,b, output out); `ifdef add//宏名为add assign out=a+b; `elseassign out=a-b; `endif endmodule 在例5.24中,若在程序中定义了“`define add”,则执行“assign out=a+b;”操作,若没有该语句,则执行“assign out=a-b;”操作。 5.7任务与函数 任务和函数的关键字分别是task和function。利用任务和函数可以把一个大的程序模块分解成许多小的子模块,方便调试,并能使程序结构清晰。 5.7.1任务 任务(task)定义与调用的格式分别为 task <任务名>; //注意无端口列表 端口及数据类型声明语句; 其他语句; endtask 任务调用的格式为 <任务名>(端口1,端口2,…); 需要注意的是,任务调用时和定义时的端口变量应是一一对应的。例如,下面是一个定义任务的例子。 task test; input in1,in2; output out1,out2; #1 out1=in1&in2; #1 out2=in1|in2; endtask 当调用该任务时,可使用如下语句: test(data1,data2,code1,code2); 调用任务test时,变量data1和data2的值赋给in1和in2; 任务执行完成后,out1和out2的值则赋给了code1和code2。 在例5.25中,定义了一个完成两个操作数按位与操作的任务,然后在后面的算术逻辑单元的描述中,调用该任务完成与操作。 【例5.25】任务举例。 module alutask(code,a,b,c); input[1:0] code; input[3:0] a,b; output reg[4:0] c; task my_and;//任务定义,注意无端口列表 input[3:0] a,b; //a,b,out名称的作用域范围为task任务内部 output[4:0] out; integer i;begin for(i=3;i>=0;i=i-1) out[i]=a[i]&b[i]; //按位与 end endtask always@(code or a or b) begincase(code) 2'b00:my_and(a,b,c); /*调用任务my_and,需注意端口列表的顺序应与任务 定义时一致,这里的a,b,c分别对应任务定义中的a,b,out */ 2'b01:c=a|b; //或 2'b10:c=a-b; //相减 2'b11:c=a+b; //相加 endcase end endmodule 为检验其功能,编写例5.26的激励脚本并对其仿真。 【例5.26】激励脚本。 `timescale 100 ps/ 1 ps module alutask_vlg_tst(); parameter DELY=100; reg eachvec; reg [3:0] a;reg [3:0] b;reg [1:0] code; wire [4:0]c; alutask i1(.a(a),.b(b),.c(c),.code(code)); initialbegin code=4'd0;a=4'b0000;b=4'b1111; #DELY code=4'd0;a=4'b0111;b=4'b1101; #DELY code=4'd1;a=4'b0001;b=4'b0011; #DELY code=4'd2;a=4'b1001;b=4'b0011; #DELY code=4'd3;a=4'b0011;b=4'b0001; #DELY code=4'd3;a=4'b0111;b=4'b1001; $display("Running testbench"); end alwaysbegin @eachvec; end endmodule 用ModelSim运行例5.26的脚本,得到如图5.14所示的仿真波形。 图5.14例5.26的仿真波形 注意: 在使用任务时,应注意以下几点。 (1) 任务的定义与调用必须在一个module内。 (2) 定义任务时,没有端口名列表,但需紧接着进行输入/输出端口和数据类型的说明。 (3) 当任务被调用时,任务被激活。任务的调用与模块调用一样,通过任务名调用实现。调用时,需列出端口名列表,端口名的排序和类型必须与任务定义时相一致。 (4) 一个任务可以调用别的任务和函数,可以调用的任务和函数个数不受限制。 5.7.2函数 在Verilog模块中,如果多次用到重复的代码,则可以把这部分重复代码摘取出来,定义成函数(function)。在综合时,每调用一次函数,则复制或平铺(flatten)该电路一次,所以函数不宜过于复杂。 函数可以有一个或者多个输入,但只能返回一个值,通常在表达式中调用函数的返回值。函数的定义格式如下。 function<返回值位宽或类型说明> 函数名; 端口声明; 局部变量定义; 其他语句; endfunction <返回值位宽或类型说明>是一个可选项,如果默认,则返回值为1位寄存器类型的数据。 函数的调用是通过将函数作为表达式中的操作数来实现的。调用格式如下。 <函数名>(<表达式><表达式>); 例5.27用函数定义了一个83编码器,并使用assign语句调用了该函数。 【例5.27】用函数和case语句描述编码器(不含优先顺序)。 module code_83(din,dout); input[7:0] din; output[2:0] dout; function[2:0] code;//函数定义 input[7:0] din; //函数只有输入,输出为函数名本身 casex(din) 8'b1xxx_xxxx:code=3'h7; 8'b01xx_xxxx:code=3'h6; 8'b001x_xxxx:code=3'h5; 8'b0001_xxxx:code=3'h4; 8'b0000_1xxx:code=3'h3; 8'b0000_01xx:code=3'h2; 8'b0000_001x:code=3'h1; 8'b0000_000x:code=3'h0; default:code=3'hx; endcase endfunction assign dout=code(din); //函数调用 endmodule 与C语言相类似,Verilog语言使用函数以适应对不同操作数采取同一运算的操作。函数在综合时被转换成具有独立运算功能的电路,每调用一次函数相当于改变这部分电路的输入以得到相应的计算结果。 例5.28中定义了一个实现阶乘运算的函数factorial,该函数返回一个32位的寄存器类型的值。采用同步时钟触发运算的执行,每个clk时钟周期都会执行一次运算。 【例5.28】阶乘运算函数。 module funct(clk,n,result,reset); input reset,clk; input[3:0] n; output reg[31:0] result; always @(posedge clk)//在clk的上升沿时执行运算 begin if(!reset) result<=0; else begin result<=2*factorial(n); end //调用factorial函数 end function[31:0] factorial; //阶乘运算函数定义(注意无端口列表) input[3:0] opa; //函数只能定义输入端,输出端口为函数名本身 reg[3:0] i; beginfactorial=(opa>=4'b1)?1:0; for(i=2;i<=opa;i=i+1) //for语句若要综合,opa应赋具体的数值,如9 factorial=i*factorial; //阶乘运算 end endfunction endmodule 注意: 函数的定义中蕴涵了一个与函数同名的、函数内部的寄存器。在函数定义时,将函数返回值所使用的寄存器名称设为与函数同名的内部变量,因此函数名被赋予的值就是函数的返回值。 Verilog2001标准中定义了一种递归函数(Function Automatic),增加了一个关键字automatic,表示函数的迭代调用。如上面的阶乘运算,可采用递归函数来描述,如例5.29所示,通过函数自身的迭代调用,实现了32位无符号整数的阶乘运算(n!)。 比较例5.28与例5.29的异同,可体会函数与递归函数的区别。 【例5.29】阶乘递归函数。 module tryfact; function automatic integer factorial;//函数定义 input [31:0] operand; if(operand >= 2) factorial = factorial(operand-1) * operand; else factorial = 1; endfunction integer result; integer n; initial begin for(n = 0; n <= 7; n = n+1) begin result = factorial(n); //函数调用 $display("%0d factorial=%0d", n, result); endend endmodule // tryfact 例5.29的仿真结果如下: 0 factorial=1 1 factorial=1 2 factorial=2 3 factorial=6 4 factorial=24 5 factorial=120 6 factorial=720 7 factorial=5040 注意: 在使用函数时,应注意以下几点。 (1) 函数的定义与调用必须在一个module内。 (2) 函数只允许有输入变量且必须至少有一个输入变量,输出变量由函数名本身担任,如在例5.28中,函数名factorial就是输出变量,在调用该函数时,“result<=2*factorial(n);”自动将n的值赋给函数的输入变量opa,完成函数计算后,将结果通过factorial名字本身返回,作为一个操作数参与result表达式的计算。因此,在定义函数时,需声明函数名的数据类型和位宽。 (3) 定义函数时没有端口名列表,但调用函数时需列出端口名列表,端口名的排序和类型必须与定义时一致。这一点与任务相同。 (4) 函数可以出现在持续赋值assign的右端表达式中。 (5) 函数的使用与任务相比有更多的限制和约束。函数不能启动任务,而任务可以调用别的任务和函数,且调用任务和函数的个数不受限制。在函数中不能包含有任何时间控制语句。 表5.3对task与function进行了比较。 表5.3task与function的比较 比 较 项 目taskfunction 输入与输出可有任意个各种类型的参数至少有一个输入,不能将inout类型作为输出 调用任务只可在过程语句中调用,不能在连续赋值语句assign中调用函数可作为表达式中的一个操作数来调用,在过程赋值和连续赋值语句中均可以调用 定时事件控制(#,@和wait)任务可以包含定时和事件控制语句函数不能包含这些语句 调用其他任务和函数任务可调用其他任务和函数函数可调用其他函数,但不可以调用其他任务 返回值任务不向表达式返回值函数向调用它的表达式返回一个值 合理使用task和function会使程序显得结构清晰而简单,一般的综合器对task和function都是支持的,但也有的综合器不支持task。 5.8Verilog2001语言标准 Verilog语言处于不断的发展过程中,1995年IEEE将Verilog采纳为标准,推出Verilog1995标准(即IEEE 13641995); 2001年3月,IEEE又批准了Verilog2001标准(即IEEE 13642001)。 目前,几乎所有的综合器、仿真器都能很好地支持Verilog2001标准,Vivado软件支持的Verilog标准包括Verilog1995、Verilog2001和SystemVerilog。有必要较为深入地了解和学习Verilog2001标准。 5.8.1Verilog2001改进和增强的语法结构 Verilog2001标准对Verilog语言的改进和增强主要表现在以下三方面。 提高Verilog语言的行为级和RTL级建模的能力。 改进Verilog语言在深亚微米设计和IP建模方面的能力。 纠正和改进了Verilog1995标准中的错误和易产生歧义之处。 下面举例说明具体的改进。 1. ANSI C风格的模块声明 Verilog2001标准改进了端口的声明语句,使其更接近ANSI C语言的风格,可用于module、task和function。同时允许将端口声明和数据类型声明放在同一条语句中。例如,在Verilog1995标准中,可采用如下方式声明一个FIFO模块。 module fifo(in,clk,read,write,reset,out,full,empty); parameter MSB=3,DEPTH=4; input[MSB:0] in; input clk,read,write,reset; output[MSB:0] out; output full,empty; reg[MSB:0] out; reg full,empty; 上面的模块声明在Verilog2001标准中可以写成下面的形式。 module fifo_2001 #(parameter MSB=3,DEPTH=4)//参数定义,注意前面有"#" ( input[MSB:0] in, //端口声明和数据类型声明放在同一条语句中 input clk,read,write,reset, output reg[MSB:0] out, output reg full,empty); 例5.30的4位格雷码计数器的模块声明部分采用了Verilog2001格式。 【例5.30】4位格雷码计数器。 module graycount#(parameter WIDTH = 4) (output reg[WIDTH-1:0]graycount,//格雷码输出信号 input wireenable,clear,clk); //使能、清零、时钟信号 reg [WIDTH-1:0]bincount; always @ (posedge clk) if(clear) begin bincount<={WIDTH{1'b 0}} + 1; graycount <= {WIDTH{1'b 0}}; end else if(enable) begin bincount <=bincount + 1; graycount<={bincount[WIDTH-1], bincount[WIDTH-2:0] ^ bincount[WIDTH-1:1]}; end endmodule 4位格雷码计数器的行为级仿真波形如图5.15所示,其输出按照格雷码编码,相邻码字只有一个比特位不同。 图5.154位格雷码计数器的行为仿真波形 2. 逗号分隔的敏感信号列表 在Verilog1995标准中书写敏感信号列表时,通常用or来连接敏感信号。例如: always @(a or b or cin) {cout,sum}=a+b+cin; always @(posedge clk or negedge clr) if(!clr) q<=0;else q<=d; 在Verilog2001标准中可用逗号分隔敏感信号,上面的语句可写为如下形式。 always @(a, b, cin) //用逗号分隔信号 {cout,sum}=a+b+cin; always @(posedge clock,negedge clr) if(!clr) q<=0; elseq<=d; 3. 在组合逻辑敏感信号列表中使用通配符* 用always过程块描述组合逻辑时,应在敏感信号列表中列出所有的输入信号,在Verilog2001标准中可用通配符“*”来表示包含该过程块中的所有输入信号变量。 下面是在Verilog1995标准和Verilog2001标准中,4选1 MUX的敏感信号列表书写格式的对比: //Verilog-1995 always @(sel or a or b or c or d) case(sel) 2'b00:y=a; 2'b01:y=b; 2'b10:y=c; 2'b11:y=d; endcase //Verilog-2001 always @ *//通配符 case(sel) 2'b00:y=a; 2'b01:y=b; 2'b10:y=c; 2'b11:y=d; endcase 4. generate语句 Verilog2001标准新增了语句generate。generate循环可以产生一个对象(如module、primitive,或者variable、net、task、function、assign、initial和always)的多个例化,为可变尺度的设计提供便利。 generate语句一般和循环语句、条件语句(for、if、case)一起使用。为此,Verilog2001标准增加了4个关键字generate、endgenerate、genvar和localparam。genvar是一个新的数据类型,用在generate循环中的标尺变量必须定义为genvar型数据。还要注意的是,for循环的内容必须加begin和end(即使只有一条语句),且必须给begin和end块语句起个名字。 下面是一个用generate语句描述的行波进位加法器的例子,它采用了generate语句和for循环产生元件的例化和元件间的连接关系。 【例5.31】采用generate语句和for语句循环描述的4位行波进位加法器。 module add_ripple #(parameter SIZE=4) (input[SIZE-1:0] a,b, input cin, output[SIZE-1:0] sum, output cout); wire[SIZE:0] c; assign c[0]=cin; generate genvar i; for(i=0;i<SIZE;i=i+1) begin : add wire n1,n2,n3; xor g1(n1,a[i],b[i]); xor g2(sum[i],n1,c[i]); and g3(n2,a[i],b[i]); and g4(n3,n1,c[i]); or g5(c[i+1],n2,n3);end endgenerate assign cout=c[SIZE]; endmodule 对例5.31用Vivado软件综合,其RTL综合原理图如图5.16所示。 图5.164位行波进位加法器RTL综合原理图 下面的例子用generate语句描述一个可扩展的乘法器,当乘法器的a和b的位宽小于8时,生成CLA超前进位乘法器; 否则生成WALLACE树状乘法器。 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)) CLA_multiplier #(a_width, b_width) u1 (a, b, product); else WALLACE_multiplier #(a_width, b_width) u1 (a, b, product); endgenerate endmodule 5. 带符号的算术扩展 signed是Verilog1995标准中的保留字,未使用。在Verilog2001标准中,用signed来定义带符号的数据类型、端口、整数、函数等。 在Verilog2001标准中,对带符号的算术运算做了如下几点扩充。 (1) wire型和reg型变量可以声明为带符号(signed)变量。例如: wire signed[7:0] a,b; reg signed[15:0] data; output signed[15:0] sum; (2) 任何进制的整数都可以带符号; 参数也可以带符号。例如: 12'sh54f//一个12位的十六进制带符号整数54f parameter p0=2`sb00,p1=2`sb01; (3) 函数的返回值可以有符号。例如: function signed[31:0] alu; (4) 增加了算术移位操作符。 Verilog2001标准增加了算术移位操作符>>>和<<<,对于有符号数,执行算术移位操作时,用符号位填补移出的位,以保持数值的符号。例如,若定义有符号二进制数A = 8'sb10100011,则执行逻辑右移和算术右移后的结果如下。 A>>3;//逻辑右移后其值为8'b00010100 A>>>3; //算术右移后其值为8'b11110100 (5) 新增了系统函数$signed()和$unsigned(),可以将数值强制转换为带符号的值或不带符号的值。例如: reg[63:0]a;//定义a为无符号数据类型 always@(a)begin result1=a/2; //无符号运算 result2=$signed(a)/2; //a变为带符号数 end 6. 指数运算符** Verilog2001标准增加了指数运算符**,执行指数运算,一般使用的是底数为2的指数运算(2n)。例如: parameter WIDTH=16; parameter DEPTH=8; reg[WIDTH-1:0] data [0:(2**DEPTH)-1]; //定义了一个位宽为16位,28(256)个单元的存储器 7. 变量声明时进行赋值 Verilog2001标准规定可以在变量声明时对其赋初始值,所赋的值必须是常量,并且在下次赋值之前,变量都会保持该初始值不变。变量在声明时的赋值不适用于矩阵。 如下面的例子,在Verilog1995标准中需要先声明一个reg变量a,然后在initial块中为其赋值为4'h4; 而在Verilog2001标准中可直接在声明时赋值,两者是等效的。 //Verilog-1995 reg[3:0] a; initial a=4'h4; //Verilog-2001 reg[3:0] a=4'h4; 也可同时声明多个变量,为其中的一个或者几个变量赋值,例如: integer i=0, j, k=1; real r1=2.5, n300k=3E6; 在声明矩阵时,为其赋值是非法的,如下面的代码是非法的: reg [3:0] array [3:0]=0;//非法 8. 常数函数 Verilog2001标准增加了一类特殊的函数——常数函数,其定义和其他Verilog函数的定义相同,不同之处在于其赋值是在编译或详细描述(elaboration)时被确定的。 常数函数有助于创建可改变维数和规模的可重用模型。如下例定义了一个常数函数clogb2,该函数返回一个整数,可根据ram的深度(ram的单元数)来确定ram地址线的宽度。 module ram(address_bus, write, select, data); parameter SIZE = 1024; input [clogb2(SIZE)-1:0] address_bus; ... function integer clogb2 (input integer depth); begin for(clogb2=0; depth>0; clogb2=clogb2+1) depth = depth >> 1; end endfunction ... endmodule 注意: 常数函数只能调用常数函数,不能调用系统函数。常数函数内部用到的参数(parameter)必须在该常数函数被调用之前定义。 9. 向量的位选和域选 在Verilog1995标准中,可以从向量中取出一个或者若干相连比特,称为位选和域选,但被选择的部分必须是固定的。 Verilog2001标准对向量的部分选择进行了扩展,增加了索引的部分选择(indexed part selects)方式,其形式如下。 [base_expr +:width_expr] // 起始表达式正偏移位宽 [base_expr -:width_expr] // 起始表达式负偏移位宽 包括起始表达式(base_expr)和位宽(width_expr)。其中,位宽必须为常数,而起始表达式可以是变量; 偏移方向表示选择区间是表达式加上位宽(正偏移),还是表达式减去位宽(负偏移)。例如: reg [63:0] word; reg [3:0] byte_num;//取值范围: 0~7 wire [7:0] byteN = word[byte_num*8 +: 8]; 上例中,如果变量byte_num当前的值是4,则byteN = word[39:32],起始位为32(byte_num*8),终止位39由宽度和正偏移8确定。再如: reg[63:0] vector1;//小端(little-endian)次序 reg[0:63] ventor2; //大端(big-endian)次序 Byte=vector1[31-:8]; //Byte=vector1[31:24] Byte=vector1[24+:8]; //Byte=vector1[31:24] Byte=vector2[31-:8]; //Byte=vector2[24:31] Byte=vector2[24+:8]; //Byte=vector2[24:31] 10. 多维矩阵 Verilog1995标准中只允许一维的矩阵变量(即memory),Verilog2001标准对其进行了扩展,允许使用多维矩阵; 矩阵单元的数据类型也扩展至Variable型(如reg)和Net型(如wire)均可。例如: reg [7:0] array1 [0:255]; //一维矩阵,存储单元为reg型 wire [7:0] out1 = array1[address]; //一维矩阵,存储单元为wire型 wire [7:0] array3 [0:255][0:255][0:15]; //三维矩阵,存储单元为wire型 wire [7:0] out3 = array3[addr1][addr2][addr3]; //三维矩阵,存储单元为wire型 11. 矩阵的位选择和部分选择 在Verilog1995标准中,不允许直接访问矩阵的某一位或某几位,必须首先将整个矩阵单元转移到一个暂存变量中,再从暂存变量中访问。例如: reg[7:0]mem[0:1023];//存储器(一维矩阵) reg[7:0]temp; reg[3:0]vect; initial begintemp=mem[55]; vect=temp[3:0]; //合法 vect=mem[55][3:0]; //非法 end 而在Verilog2001标准中,可以直接访问矩阵的某个单元的一位或几位。例如: reg [31:0] array2 [0:255][0:15]; wire [7:0] out2 = array2[100][7][31:24]; //选择宽度为32位的二维矩阵中[100][7]单元的[31:24]字节 12. 模块实例化时的参数重载 当模块实例化时,其内部定义的参数(parameter)值是可以改变的(或称为参数重载)。在Verilog1995标准中有两种方法改变参数值: 一种是使用defparam语句显式地重载; 另一种就是模块实例化时使用“#”符号隐式地重载,重载的顺序必须与参数在原定义模块中声明的顺序相同,并且不能跳过任何参数。由于这种方法容易出错,而且代码的含义不易理解,所以Verilog2001标准增加了一种在线显式重载(inline explicit redefinition)参数的方式,这种方式允许在线参数值按照任意顺序排列。例如: module ram(...);//ram模块定义 parameter WIDTH = 8; parameter SIZE = 256; ... endmodule module my_chip (...); ... RAM ram1 (...); //ram模块例化1 defparam ram1.SIZE = 1023; //使用defparam语句显式地重新定义SIZE=1023 RAM #(8,1023) ram2 (...); //ram模块例化2 //使用#符号隐式地重载参数,注意参数的排列顺序 RAM #(.SIZE(1023)) ram3 (...);//ram模块例化3 //在线显式重载参数SIZE为1023 endmodule 13. register改为variable 在Verilog语言诞生后,一直用register这个词表示一种数据类型,但初学者很容易混淆register和硬件中的寄存器概念。而实际中,register数据类型的变量常被综合器映射为组合逻辑电路。 在Verilog2001标准中,将register一词改为了variable,以避免混淆。 14. 新增条件编译语句 Verilog1995标准支持条件编译语句`ifdef、`else、`endif,可以指定仅对程序中的部分内容进行编译。Verilog2001标准增加了条件编译语句`elsif和`ifndef。 15. 超过32位的自动宽度扩展 在Verilog1995标准中对超过32位的总线赋高阻时,如果不指定位宽,则只将低32位赋成高阻,高位补0。如果想将所有位都置为高阻,必须明确指定位宽。例如: //Verilog-1995标准中的超过32位的总线赋高阻: parameter WIDTH = 64; reg [WIDTH-1:0] data; data = 'bz; //赋值后,data='h00000000zzzzzzzz data = 64'bz; //赋值后,data='hzzzzzzzzzzzzzzzz Verilog2001标准改变了赋值扩展规则,将高阻z或不定态x赋给未指定位宽的信号时,可以自动扩展到信号的整个位宽范围。例如: //Verilog-2001标准中将高阻或不定态赋给未指定位宽的信号: parameter WIDTH = 64; reg [WIDTH-1:0] data; data ='bz;//赋值后,data='hzzzzzzzzzzzzzzzz 16. 可重入任务(Reentrant Task)和递归函数(Recursive Function) Verilog2001标准增加了一个关键字automatic,可用于任务和函数的定义中。 (1) 可重入任务: 任务本质上是静态的(Static Task),同时并发执行的多个任务共享存储区。若某个任务在模块中的多个地方被同时调用,则这两个任务对同一块地址空间进行操作,结果可能是错误的。Verilog2001标准中增加了关键字automatic,空间是动态分配的,使任务成为可重入的。若定义任务时使用了automatic,则定义一个可重入任务。这两种类型的任务所消耗的资源是不同的。 (2) 递归函数: 关键字automatic用于函数,表示函数的迭代调用。如在下面的例子中,通过函数自身的迭代调用,实现32位无符号整数的阶乘运算(n!)。 function automatic [63:0] factorial; input [31:0] n; if(n == 1)factorial = 1; else factorial = n * factorial(n-1);//迭代调用 endfunction 由于Verilog2001标准增加了关键字signed,所以函数的定义还可在automatic后面加上signed,返回有符号数。例如: function automatic signed [63:0] factorial; 17. 文件和行编译指示 Verilog编译和仿真工具需要不断地跟踪源代码的行号和文件名,Verilog可编程语言接口(PLI)可以取得并利用行号和源文件的信息,以标记运行中的错误。但是,如果Verilog代码经过其他工具的处理,源代码的行号和文件名可能丢失。故在Verilog2001标准中增加了`line,用来标定源代码的行号和文件名。 5.8.2属性及PLI接口 Verilog2001标准还在以下方面做了改进和增强。 1. 设计管理 Verilog1995标准将设计管理工作交给软件来承担,但各仿真工具的设计管理方法各不相同,不利于设计的共享。为了更好地在设计人员之间共享Verilog设计,并提高某个特定仿真的可重用性,Verilog2001标准加强了对设计内容的管理和配置。 Verilog2001标准中增加了配置块(Configuration Block),用它来指定每一个Verilog模块的版本及其源代码的位置。配置块位于模块定义之外,可以指定Verilog语言程序设计从顶层模块开始执行,找到在顶层模块中实例化的模块,进而确定其源代码的位置,照此顺序,直到确定整个设计的源程序。 Verilog2001标准中新增了关键字config和endconfig,还增加了关键字design、instance、cell、use和liblist,以供在配置块中使用。 下面的例子是一个简单的设计配置,test是一个测试模块(Test Bench),其中包含了设计模块myChip,myChip中又包含了其他实例化模块。 module test; ... myChip dut (...);/*设计模块实例化*/ ... endmodule module myChip(...); ... adder a1 (...); adder a2 (...); ... endmodule 配置块可以指定所有或个别实例化模块的源代码的位置。配置块位于模块定义之外,所以需要重新配置时,Verilog源代码可以不做任何修改。 在下面的配置块中,design语句指定了顶层模块及其源代码来源,rtlLib.top表示顶层模块的源代码来自rtlLib; default和liblist语句相配合指定了所有在顶层模块中实例化的模块均来自rtlLib库和gateLib库; 又使用instance语句具体指定了加法器实例a2的源程序来自门级库gateLib。 config cfg4 //给配置块命名 design rtlLib.top //指定从哪里找到顶层模块 default liblist rtlLib gateLib; //设置查找实例化模块的默认顺序 instance test.dut.a2 liblist gateLib; //明确指定模块例化使用哪一个库: a2来自门级库gateLib endconfig 下面的语句指定了RTL库和gateLib库模块的位置。 library rtlLib ./*.v; //RTL库模块的位置(位于当前目录下) library gateLib ./synth_out/*.v; //gateLib库模块的位置 2. 属性 属性用来向综合工具传递信息,以控制综合工具的行为和操作。属性包含在两个“*”之间,可用于对象的所有实例调用,也可只应用于某一个实例调用。与综合有关的属性语句如下。 (* synthesis, async_set_reset[="signal_name1,signal_name2,..."]*) (* synthesis, black_box[=<optional_value>] *) (* synthesis, combinational[=<optional_value>] *) (* synthesis, fsm_state[=<encoding_scheme>] *) (* synthesis, full_case[=<optional_value>] *) (* synthesis, implementation="<value>" *) (* synthesis, keep[=<optional_value>] *) (* synthesis, label="name" *) (* synthesis, logic_block[=<optional_value>] *) (* synthesis, op_sharing[=<optional_value>] *) (* synthesis, parallel_case[=<optional_value>] *) (* synthesis, ram_block[=<optional_value>] *) (* synthesis, rom_block[=<optional_value>] *) (* synthesis, sync_set_reset[="signal_name1,signal_name2,..."]*) (* synthesis, probe_port[=<optional_value>] *) Verilog没有定义标准的属性,属性的名字和数值由工具厂商或其他标准来定义,目前尚无统一的标准。 3. 增强的文件输入、输出操作 Verilog1995标准在文件的输入、输出操作方面功能非常有限。文件操作通常借助于Verilog PLI(编程语言接口),通过与C语言的文件输入、输出库的访问来处理,并且规定同时打开的I/O文件数目不能超过31个。 Verilog2001标准增加了新的系统任务和函数,为Verilog语言提供了强大的文件输入、输出操作,而不再需要使用PLI,并扩展了可以同时打开的文件数目至230个。这些新增的文件输入、输出系统任务和函数包括$ferror、$fgetc、$fgets、$fflush、$fread、$fscanf、$fseek、$fscanf、$ftel、$rewind和$ungetc; 还有读写字符串的系统任务,包括$sformat、$swrite、$swriteb、$swriteh、$swriteo和$sscanf,用于生成格式化的字符串或者从字符串中读取信息。 增加了命令行输入任务$test$plusargs和$value$plusargs。 4. VCD文件的扩展 VCD文件用于记录仿真过程中信号的变化,只记录在函数中指定的层次中相关的信号。信息的记录由VCD系统任务来完成。在Verilog1995标准中,只有一种类型的VCD文件,即四状态类型,这种类型的VCD文件只记录变量在0、1、x和z状态之间的变化,不记录信号强度信息。而在Verilog2001标准中增加了一种扩展类型的VCD文件,能够记录变量在所有状态之间的转换,同时记录信号强度信息。 扩展型VCD系统任务包括$dumpports、$dumpportsoff、$dumpportson、$dumpportsall、$dumpportslimit、$dumpportsfulsh和$vcdclose。 5. 提高了对SDF(标准延时文件)的支持 在Verilog1995标准中,specparam常数只能在specify块(指定块)中定义; Verilog2001标准允许在模块层级声明和使用specparam常数。Verilog2001标准基于最新的SDF标准(IEEE Std 1497—1999),提高了对SDF(Standard Delay File)的支持度。 6. 编程语言接口的改进 编程语言接口(Programming Language Interface,PLI)包括三个C语言功能库,分别是ACC、TF和VPI。Verilog2001标准清理和更正了旧的ACC和TF库中的许多定义,但并没有增加任何新的功能。Verilog2001标准对PLI的所有改进都体现在VPI库中,包括增加了6个VPI子程序: vpi_control()、vpi_get_data()、vpi_put_data()、vpi_get_userdata()、vpi_put_userdata()和vpi_flush(),为用户提供了更大的便利。现将这6个函数的功能简单介绍如下。 (1) vpi_control()的作用是传递用户给仿真器的指令。 (2) vpi_get_data()和vpi_put_data()相对应使用,从一次执行的save/restart位置获取数据。语法格式为vpi_get_data(id, dataLoc, numOfBytes)。 (3) vpi_put_data()的作用是将数据放到一次仿真的save/restart位置。其语法格式为vpi_put_data(id, dataLoc, numOfBytes)。其中,numOfBytes是个正整数,以字节为单位指定了要放置的数据的数目,dataLoc代表数据所在的位置,id代表vpi_get (vpiSaveRestartID, NULL)返回的save/restart ID。函数的返回值是数据的字节数,若出错,则返回0。 (4) vpi_get_userdata()和vpi_put_userdata()相对应使用。从系统任务或系统函数实例的存储位置读取用户数据。语法格式为vpi_get_userdata(obj)。 (5) vpi_put_userdata()将用户数据放置到系统任务/函数实例的存储位置。语法格式为vpi_put_userdata(obj, userdata)。其中,obj是指向系统任务或系统函数的句柄,userdata代表要和系统任务或系统函数相关联的用户数据。函数的返回值为1,出错时返回值为0。 (6) vpi_flush()的作用是将仿真器输出缓冲区和log文件输出缓冲区清空。 习题5 5.1阻塞赋值和非阻塞赋值有什么区别? 5.2用持续赋值语句描述一个4选1数据选择器。 5.3用行为语句设计一个8位计数器,每次在时钟的上升沿,计数器加1,当计数器溢出时,自动从零开始重新计数,计数器有同步复位端。 5.4设计一个4位移位寄存器。 5.5initial语句与always 语句的区别是什么? 5.6分别用任务和函数描述一个4选1多路选择器。 5.7总结任务和函数的区别。 5.8在Verilog中,哪些操作是并发执行的?哪些操作是顺序执行的? 5.9试编写求补码的Verilog程序,输入是带符号的8位二进制数。 5.10试编写两个4位二进制数相减的Verilog程序。 5.11有一个比较电路,当输入的一位8421BCD码大于4时,输出为1,否则为0,试编写出Verilog程序。 5.12用Verilog语言设计一个类似74138的译码器电路,用Vivado软件对设计文件进行综合,观察综合视图。 5.13用Verilog语言设计一个8位加法器,用Vivado软件进行综合和仿真。