HDL4SE:软件工程师学习Verilog语言(七)

7 行为模型

前面一节读起来是不是有点虎头蛇尾的感觉啊,表达式介绍了一大堆,赋值却草草收场。其实verilog语言中赋值语句与所谓的行为模型相关,本节我们将继续介绍赋值过程,补上上一节留下的遗憾。
行为模型,英文是Behavioral modeling,是指verilog中对数字系统比较高层次的描述方法,然而我们更关心其中顺序,并发,分支,循环等控制结构的描述方式。
实际上verilog语言非常强悍,往下层走甚至可以描述到电路的晶体管实现这个层次,可以描述物理实现参数。然而这些不是我们关注的目标,因此IEEE 1364-2005中对太底层的描述,比如门级和开关级的模型(Gate- and switch-level modeling),用户自定义原语( User-defined primitives -UDPs,用来自定义特殊物理实现的特别的门电路或开关电路,或者是芯片设计中全定制的基本单元)等,这里就跳过去了,HDL4SE软件也将对这样的描述视而不见,不支持其功能,感兴趣的可以直接看规范中相关的章节。
verilog语言往上层,可以支持一种抽象的所谓行为级描述,就是直接描述信号的生成顺序,以及一些灵活的控制逻辑。这种描述不关心电路如何实现,不以前面的寄存器加组合逻辑为中心的所谓RTL描述为目标,而是提供在项目早期设计过程中一种概念性的设计描述,它更关注大模块的逻辑顺序关系和模块间的同步关系。这种描述很多情况下并不能生成实际的电路,然而在设计过程中仍然是比较重要的一环。
行为模型的一些描述方法,还是可以用在RTL描述中的,我们在介绍中会仔细加以区分。有些行为级描述我们的HDL4SE不支持,有些我们还是支持的,没有它们,做数字电路设计也不行。
前面我们知道,一个verilog的model中,开始是<属性表> module <参数表> <接口表>;然后是各种module_item,最后是一个endmodule结束,module_item中可以出现参数,局部参数,接口描述,但是一个接口的属性只能声明一次,在整个module中不能修改,这个跟参数不一样。除此之外,还能出现变量声明,变量的持续性赋值,其他module实例化等操作。当然还有我们不支持的UDP和门级和开关级的描述。对于门级网表(verilog汇编)来说,这些已经够用了。然而这些看着总不象在编程序,没有传统编程意义上的顺序,分支,循环等控制结构。

7.1 initial块和always块

这些程序性的控制结构,就在所谓的initial 块和always块里,它们用来操作其他module_item声明的变量以及端口和参数。initial块的定义很简单,就是关键字initial之后跟一个语句,always块的定义就是always后面跟一个语句。当然后面会看到,语句之前可以声明时序控制。如果语句比较复杂,一般使用一个复合语句(参见7.2)。
比如:

module behave;
reg [1:0] a, b;
reg clk;
initial begin
	a = 'b1;
	b = 'b0;
	clk = 1'b0;
end

always #10 clk = ~clk;

always begin
	#50 a = ~a;
end

always begin
	#100 b = ~b;
end
endmodule

一个module中可以有多个initial块和多个always块, 所有的initial块和always块都是在模拟开始时开始同时并发运行的,initial块只运行一次,always块则反复运行,好像在一个不退出的循环中似的。其中的#用来表示时间延迟,这样initial块对clk赋予一个初始值0,后面always #10 clk=~clk;这个语句每过10个时钟单位将clk反转一次,生成的就是周期为20个时钟周期的方波信号,没有initial和always块的配合,周期性方波信号都无法生成了。
其实在硬件本质上就是反复执行的,因此initial块定义的只执行一次的操作反而没法实现了,一般的ASIC开发平台和FPGA开发平台在生成电路时都不考虑initial块。initial块的功能似乎仅仅用来做行为级仿真用了。而且initial块和always块其中的#开始的延时,实际上硬件也无法简单地通过综合来实现,因此这样的描述也只是为行为级模拟来用。

7.2 复合语句

initial块,always块包括task和function中逻辑往往不只包括一个语句,但是语法上都只能包括一个语句,因此verilog中有类似于c语言中的复合语句的结构,它把一组程序语句合并在一起,构成一个语法上跟单个语句等价的语句块。这种块语句分两种,一种叫顺序块,也称为称为begin/end块,它的语句包含在关键字begin 和end之间,其中的语句逻辑上是按照出现顺序执行的,一种叫并行块,也称为fork/join块,它的语句包括在关键字fork和join之间,其中的语句是并发执行的。两种块内都可以出现其他语句类型,包括两种复合语句。

7.2.1 顺序块

顺序块用begin开始,end结束,它的特征是:

  • 其中的语句执行时按照顺序进行的,一条语句执行完再执行下一条语句
  • 每条语句的延迟时相对于上条语句结束时间而言的,也就意味着块语句的总延迟是所有语句延迟的和
  • 最后一条语句执行完成表示该块语句执行完成
    顺序块可以取个名字,名字就跟在begin后面,用个冒号隔开。带名字的块可以由其他语句引用。顺序块开始的地方可以声明 reg,integer,time,real,realtime,event,local_parameter和parameter等类型的变量作为局部变量,声明完成后再跟着块内的语句。块内语句可以直接使用这些变量。下面例子中的语句类型我们稍后再详细说明。
    例如:
begin
	areg = breg;
	creg = areg; // 按顺序执行,creg 中存放 breg的值
end

包括带延时控制的语句

begin
	areg = breg; 
	@(posedge clock) creg = areg; // 赋值在时钟信号clock的上沿到达时完成
end

带参数声明的块语句,来生成一个按照一定时间间隔变化的信号

begin 
	parameter d = 50; // d 时间间隔
	reg [7:0] r; 
	#d r = 'h35;
	#d r = 'hE2;
	#d r = 'h00;
	#d r = 'hF7;
	#d -> end_wave; //产生一个信号叫end_wave 
end

7.2.2 并行块

并行块用fork开始,join结束,它的特征是:

  • 其中的语句是并发执行的
  • 语句的延迟相对块的开始,这样整个块的延时就是延时最长的语句的延时
  • 可以为其中的赋值语句提供时间控制
  • 最后执行完的的语句执行完成则表示块语句执行完成
    并行块也可以取个名字,名字就跟在begin后面,用个冒号隔开。带名字的块可以由其他语句引用。并行块开始的地方可以声明 reg,integer,time,real,realtime,event,local_parameter和parameter等类型的变量,声明完成后再跟着块内的语句。块内语句可以直接使用这些变量。
    比如:
fork
   #50 r = 'h35;
   #100 r = 'hE2;
   #150 r = 'h00;
   #200 r = 'hF7;
   #250 -> end_wave;
join

与前面顺序块中最后一个例子的效果一样。
并行块中的语句可以任意顺序出现,不影响结果,比如,前面的语句等价于:

fork
   #250 -> end_wave;
   #150 r = 'h00;
   #50 r = 'h35;
   #200 r = 'hF7;
   #100 r = 'hE2;
join

块语句可以嵌套,这样可以实现部分并发部分顺序的效果,比如:

begin
	fork
		@Aevent;
		@Bevent;
	join
	areg = breg;
end

中Aevent和Bevent可以任意顺序发生,两个事件都发生后,再执行后面的赋值语句。如果是这样:

begin
	begin
		@Aevent;
		@Bevent;
	end
	areg = breg;
end

那就要先等到Aevent发生,然后再等到Bevent发生,才执行赋值语句。

7.3 程序性赋值

前一节中我们介绍了持续性赋值,持续性赋值只能给线网赋值,它对应的电路其实就是把线网连接到表达式的结果上,表达式结果一发生变化,线网上的值就有变化。持续性赋值对一个线网可以多次赋值,多次赋值时可以根据选通特征设置表达式结果为高阻,这样线网上同时实际只由一个表达式的结果来驱动。程序性赋值只对线网类型以外的变量赋值,赋值语句在程序结构中出现。程序性赋值分两种,一种是带强制顺序的所谓阻塞赋值,执行时按照出现先后顺序执行,另一种时非阻塞赋值,执行时与后面的语句并发运行,对执行顺序不进行阻塞。
程序性赋值语句由左值,赋值符号和表达式组成,其中左值是赋值的对象,包括以下几种:

  • 用 reg, integer, real, realtime或time 类型声明的变量
  • reg, integer或time数据类型的位选择,对其中的某一位进行赋值,其他位不变
  • reg, integer或time数据类型的部分位选择,部分位选择的时候,选择的上下限必须是常数,对其中选中的部分位进行赋值,其他的位不变
  • 内存字:内存变量的单个字,所谓内存变量,就是reg的数组
  • 上述各种左值的连接或嵌套连接,赋值只对连接中的位进行,其他位不做变化。
    其中阻塞赋值的基本格式是variable_lvalue = [ delay_or_event_control ] expression ,左值前面已经描述过了,延时和事件控制语句后面会专门描述。阻塞赋值出现在语句块中时,是按照顺序执行的,在顺序块中上个语句执行完后开始执行,执行完成的时机靠延迟或事件控制机制决定。如果在并发块中出现,则以块开始为开始执行,不对后面出现的语句构成阻塞。
    非阻塞赋值的基本格式是variable_lvalue <= [ delay_or_event_control ] expression,注意赋值符号有不同了。非阻塞赋值的执行时不阻塞所在块的下一个语句,即使在顺序块中它跟后面的语句也是并行执行的。比如:
module evaluates2 (out);
output out;
reg a, b, c;
initial begin
a = 0;
b = 1;
c = 0;
end
always c = #5 ~c;
always @(posedge c) begin
a <= b; 
b <= a; 
end
endmodule

这段代码中,initial中带了一个顺序块,这个initial块在程序启动时开始运行,把a,b,c分别设置为0, 1, 0,always c = #5 ~c;这个只有一个阻塞赋值语句的always块每隔5个时间单位就反转一次c的值,生成一个周期为10个时间单位的方波信号。最后的一个always块则在c信号的每次上升沿执行一次顺序块中的语句,由于两个都是非阻塞的,两个语句是同时执行的,于是效果是a和b的值互换,形成这个结果原因是在每个c信号的上沿,a和b两个寄存器都同时锁定对方的输出值,由于锁定和输出之间总有个信号建立时间,因此a和b锁存了对方前面的值,也就是互相交换了值。
从电路实现的角度看,initial块实际上是无法实现的,因为电路都是不停循环运行的,因此要想实现只执行一次的功能块,实现上不能用initial块来做,第二个always c=#5~c;中间包括了一个延时,其实也是不可实现的,c信号本身没有用非阻塞赋值,事实上并不是一个真正的寄存器,寄存器锁存必须在时钟信号上下沿处才行。
因此真正编译时,因为不是持续性赋值,所以赋值的左值必须不能是线网类型的变量。但是对阻塞赋值,实际上左值因为不是在始终信号上下沿进行赋值,所以其实跟一个线网类似。
非阻塞赋值出现在一个在时钟上下沿作为循环起始的一个always块中,左值构成一个寄存器,在语句执行的时候完成对表达式的值的锁存。因此特别值得注意的是,由于寄存器锁存必须在时钟信号的上下沿,尽管都声明为reg,但是只有在always @(posedge/negedge 信号)的语句中被非阻塞赋值语句赋值的左值,才是寄存器的一部分,其他的都被视为线网。
尽管IEEE. 1364-2005中规定非阻塞赋值在其他条件下也能完成赋值,但是这种情况下的代码编译出来后并不能真正成为寄存器,也只能作为类似于线网的变量。在实际的编译中,阻塞赋值生成组合逻辑电路,其中的延时和控制无法在电路中实现,另外,组合电路中不能有圈的存在,因此,如果去掉延时和事件控制,运行结果与语句顺序无关的顺序块,才是可综合成电路的RTL描述:

module test;
reg [3:0] a, b, c;
always #10 begin
   a = 5;
   b = a + 3;
   c = a + b;
end
endmodule

module test;
reg [3:0] a, b, c;
always #10 begin
   b = a + 3;
   c = a + b;
   a = 5;
end
endmodule

编译后生成的电路都是两个常数单元和两个加法器的组合电路:
HDL4SE:软件工程师学习Verilog语言(七)_第1张图片
这是前面的描述的仿真波形图
HDL4SE:软件工程师学习Verilog语言(七)_第2张图片

这是后面的描述的仿真波形图。
HDL4SE:软件工程师学习Verilog语言(七)_第3张图片
a, b, c 的值中间变化可能不一样,最终的结果都是一样的,结果与顺序无关的描述才是可以综合成实际电路的。这个里边,还有个#10的延时控制,但是这个已经不重要了,因为实际的电路中总是不断在运行的,也就是电路中不管如何都有个延时,无非是多点少点。组合电路只要不成圈,结果就与顺序无关,综合(编译)的时候就只要把每个赋值语句中的表达式生成组合电路连接起来就是了。这就像是搭面包板似的,先搭a,还是先搭b或c,只要连接关系一样,结果不会有差别。
非阻塞赋值电路描述是可以成圈的,它们被综合成带寄存器的时序电路,如果没有时间延迟和事件控制描述,也满足可综合RTL的要求。
比如:

module test;
reg wClk;
reg [15:0] bData;
reg nwReset;
initial begin
   wClk = 0;
   nwReset = 1b'1;
   #10
   nwReset = 1'b0;
   #20
   nwReset = 1'b1;
end
always #5 wClk = ~wClk;
always @(posedge wClk) 
if (!nwReset)
	bData <= 0;
else
    bData <= bData + 1;
endmodule   

这个描述在实际的应用中,wClk和nwReset都是外面给出的信号,这里仅仅为仿真方便。最后的always语句编译出来的电路是:

HDL4SE:软件工程师学习Verilog语言(七)_第4张图片
仿真结果如下:
HDL4SE:软件工程师学习Verilog语言(七)_第5张图片

一般而言,为确保能够编译成正确的电路,在时钟沿事件触发的always语句中,应该都用非阻塞赋值,这样赋值的左值都被编译为寄存器,在其他事件触发的always语句中,应该都使用阻塞赋值,其实这个时候与模块中的持续赋值已经没有多大差别了。

7.4 程序性持续赋值

除了阻塞赋值和非阻塞赋值之外,还有一种可以在程序块中使用的持续性赋值,称为程序性持续赋值。这种赋值语句与模块中的持续性赋值有点像,但是表现力更强,基本格式是:
以assign, deassign, force, release等关键字开头, 后面跟变量或线网左值,然后是等号,右边是一个表达式。既然是持续性的,赋值就没有什么延迟控制和事件控制。既然是程序性的,就可以在不同的条件下为一个左值赋予不同的表达式。比如:

module dff (q, d, clear, preset, clock);
output q;
input d, clear, preset, clock;
reg q;
always @(clear or preset)
if (!clear)
	assign q = 0;
else if (!preset)
	assign q = 1;
else
	deassign q;
always @(posedge clock)
	q = d;
endmodule

其中,assign是声明赋值, deassign则取消持续性赋值,后面采用always中的阻塞赋值。此时的assign与模块中的持续性赋值不同,这里的持续性赋值会取消前面assign过的持续性赋值,模块中的持续性赋值则可以多次对同一个左值进行持续性赋值,可以达到一种总线的效果。注意,assign和deassign只能对非线网对象进行,如果要对线网对象也进行程序性持续赋值操作,可以用force和release,assign 和force的区别似乎也就是这点差别了。对于可以综合成电路的描述而言,他们的区别不大,一般而言有持续性赋值,阻塞赋值和非阻塞赋值足够用了。

7.5 分支语句

分支语句与c语言中基本一样,由条件表达式和true语句和false语句构成,分支语句执行时,先对表达式进行求值,如果结果为真,则执行true分支,否则执行false分支,当然false分支可以空缺。verilog中还定义了一种称为if else if 的结构,其实就是if else结构的扩展。if else语句的最简单的编译方法是用条件表达式的结果作为选择信号,把true和flase块的结果作为输入进行选择,得到分支语句的输出。对每个在分支语句中出现的左值都进行这样的选择后,通过赋值语句进行连接。
如果一个左值在某个分支中没有出现在赋值语句中,此时按照分支语句的逻辑,相应的条件下这个左值应该维持原来的值。对时钟沿触发的always块中的非阻塞赋值语句,这个没有什么问题,只要在选择器的对应分支输入端输入寄存器的输出值即可。然而对其他事件触发的always块中的阻塞赋值而言,这个就不行了,如果把变量的值当做对应分支的输入,再对该左值进行赋值,就会造成组合逻辑循环,编译器一般的做法是把分支语句之前对该左值的赋值作为缺省值参与选择,避免使用左值本身参与选择。如果分支语句之前也没有对这个左值进行过赋值,此时应该判断为无法编译为组合电路,报告对应的错误信息出来提请用户修改。一般的开发工具要求在一个块中的阻塞赋值的左值必须在各种情况都进行过赋值。一个简单的办法是不管三七二十一,在always语句开头都给每个事实上是线网的寄存器变量赋予一个默认值,这样避免某个分支中没有赋值的问题。
由于分支语句可能嵌套,这样如果一个左值在嵌套的分支语句中的不同分支层次中都赋过值,就必须将每个分支的条件组合到一起,形成一个类似于case语句的结构,在不同的条件下为同一个左值赋予不同的表达式。
FPGA开发工具和ASIC的综合工具在形成网表时,事实上将每一位组合电路的输出都将分支语句中的条件表达式和分支赋值表达式做了结合。if © a=exprt else a=exprf,这样的语句被编译为a=(c & exprt) | (~c & exprt);嵌套的分支语句也进行复合语句操作,比如

if (c1) 
  a = expr1 
else begin
    if (c2)
       a = expr2;
    else 
       a = expr3
  end

可能被编译为 a=(c1 & expr1) | (~c1 & (c2 & expr2 | ~c2 & expr3)),到后面讨论编译器实现的时候我们再来详细看看如何在我们的编译器中实现分支语句中的赋值。

7.6 Case语句

verilog中的case语句语法跟c语言中有点不一样,它以一个case, casez或casex开始,以一个endcase结束,中间是所谓的case_item,每个case_item开始是一个表达式表后面跟一个冒号,后面在跟一个语句,case_item也可以以default开始跟语句的形式。与c语言中的switch语句相比,有两点不同,一个是形式,switch语句以switch开始,后面是通常的语句序列,语句序列中可以出现case 常数表达式:语句;的形式的语句,可以出现break,可以出现以default开始的语句。另一个是c语言中的case分支中只能出现常数表达式,但是verilog中的case分支中可以出现多个常数表达式,表示case条件表达式等于这些常数表达式其中一个,就运行后面的语句。当然语句也可以为空。
case语句同样存在分支语句中出现的赋值语句的左值可能没有默认值的情况,这对非阻塞赋值到一个寄存器是没有问题的,但是对阻塞赋值语句赋值到线网而言,就没法实现了,一般建议要么在case_item中将case表达式中出现的所有值都列出来,要么干脆放一个default开始的case_item,里边对所有涉及的阻塞赋值左值进行赋值,确保每个阻塞赋值的左值都有值。下面是一个例子:

reg [15:0] rega;
reg [9:0] result;
case (rega)
16'd0: result = 10'b0111111111;
16'd1: result = 10'b1011111111;
16'd2: result = 10'b1101111111;
16'd3: result = 10'b1110111111;
16'd4: result = 10'b1111011111;
16'd5: result = 10'b1111101111;
16'd6: result = 10'b1111110111;
16'd7: result = 10'b1111111011;
16'd8: result = 10'b1111111101;
16'd9: result = 10'b1111111110;
default result = 'bx;
endcase

case_item中的条件表达式中可以出现x,z,?之类的值,比如:

case (select[1:2])
2'b00: result = 0;
2'b01: result = flaga;
2'b0x,
2'b0z: result = flaga ? 'bx : 0;
2'b10: result = flagb;
2'bx0,
2'bz0: result = flagb ? 'bx : 0;
default result = 'bx;
endcase

此时对case,casez和casex开始的case语句,处理是不一样的。对case语句,必须完全一致才行,对casez和casex,如果出现在case_item中的条件表达式中,出现?值意味着对应的不做比较,直接放过去,比如:

reg [7:0] ir;
casez (ir)
8'b1???????: instruction1(ir);
8'b01??????: instruction2(ir);
8'b00010???: instruction3(ir);
8'b000001??: instruction4(ir);
endcase

ir[7]如果为1,不管其他各个位取什么值,都进入instruction1分支。
ir[7:6]如果为01,则不管其他各个位取什么值,都进入instruction2分支。
再比如:

reg [7:0] r, mask;
mask = 8'bx0x0x0x0;
casex (r ^ mask)
8'b001100xx: stat1;
8'b1100xx00: stat2;
8'b00xx0011: stat3;
8'bxx010100: stat4;
endcase

这里边,对r = 8’b01100110,按上一节的运算规则表,r^mask=8’bx1x0x1x0,此时比较的时候,取值x的位不参加不参加比较,于是能与8’b1100xx00匹配,与执行stat2。
在编译为电路时,可以跟分支语句同样的处理方式,同时注意生成比较操作的时候区分case,casez和casex的情况即可,其中的z,x,?只在casez和casex中用来判断对应的位是否参与比较即可。

7.7 循环语句

verilog中有四种循环语句,forever, repeat, while , for语句,各种循环语句比较如下:

类型 格式 含义
forever forever statement 不断重复执行statement
repeat repeat(expr) statement 重复expr指定的次数执行statement
while whle(expr) statement 在expr结果非零时不断执行statement
for for(
variable_assignment ;
expression ;
variable_assignment )
statement
开始执行第一个variagle_assignment,
然后对expression求值,在值为真的情况下执行statement,
然后再执行后面的variable_assignment。
然后再进入对expression求值比较的过程,知道求值结果为0时退出。

例如:

parameter size = 8, longsize = 16;
reg [size:1] opa, opb;
reg [longsize:1] result;
begin : mult
	reg [longsize:1] shift_opa, shift_opb;
	shift_opa = opa;
	shift_opb = opb;
	result = 0;
	repeat (size) begin
		if (shift_opb[1])
			result = result + shift_opa;
		shift_opa = shift_opa << 1;
		shift_opb = shift_opb >> 1;
	end
end

这段代码展示了用repeat语句用移位加法实现了一个乘法器。不过这段代码有没法消除的顺序执行特征,而且组合逻辑中有圈,因此无法用实际的电路实现。

reg [7:0] a;
reg [7:0] b;
integer i;
for (i = 0;i<8;i=i+1) begin
  a[i] = b[i] ^ i[0];
end

这段代码看着似乎也无法用电路实现,然而可以将循环展开成下面的代码:

reg [7:0] a;
reg [7:0] b;
begin
	a[0] = b[0] ^ 1'b0;
	a[1] = b[1] ^ 1'b1;
	a[2] = b[2] ^ 1'b0;
	a[3] = b[3] ^ 1'b1;
	a[4] = b[4] ^ 1'b0;
	a[5] = b[5] ^ 1'b1;
	a[6] = b[6] ^ 1'b0;
	a[7] = b[7] ^ 1'b1;
end

这就可以编译成实际的电路了。可以看出,循环展开还是比较复杂的,要实现循环展开不是那么容易。
从上面可以看出,真正能够综合的循环语句其实比较少,因此一般的RTL描述中很少用循环语句,要用也是能够展开的比如for循环,或者是repeat循环。

7.8 程序性结构中的时序控制

verilog的程序性结构中,可以有两种方式控制语句执行的时序,一种是时间延迟,另一种是事件。用#delay_value表示一个延迟,其中delay_value是一个整数,实数或者是一个参数,还有一种表达方法是#(expr1:expr2:expr3),给出三个延时,表示最小,典型和最大延时。事件用@开始来表示事件,事件有几种表达方式,一种是@后面跟一个事件标识符,该标识符可以是子块中定义的事件,这样可以是一个用小数点隔开的标识符号串,表示从当前块开始的子块下面的标识符,还有一种是@后面用括号括住的一系列变量名(线网或寄存器),这些变量名每一个都代表该变量变化的事件,变量之间可以用or分开,也可以用逗号隔开,表示的都是一个意思,就是这些事件中有一个发生。如果变量是一位的,前面还可以加posedge或negedge,来指定变化具体是上升沿还是下降沿,就算时序满足运行条件。时序控制可以用repeat循环进行重复。比如:

@r rega = regb; // r的值变化时开始执行
@(posedge clock) rega = regb; // clock信号的上升沿开始执行
forever @(negedge clock) rega = regb; // 每一个clock的下降沿执行语句

实际上,在一个always块带阻塞赋值语句时,可以在always后面的事件控制中指定在什么事件变化时对always中的阻塞赋值语句进行求值并赋值。一般情况下应该把所有的右端表达式中出现的变量都列出来,比如:

always @(a or b or c or d or tmp1 or tmp2) begin 
	tmp1 = a & b; 
	tmp2 = c & d;
	y = tmp1 | tmp2;
end
/* 或者*/
always @(a , b , c , d , tmp1 , tmp2) begin 
	tmp1 = a & b; 
	tmp2 = c & d;
	y = tmp1 | tmp2;
end

这样,在a, b, c, d, tmp1或tmp2变化时,这个always块后面的顺序块会执行。
想把这种全部依赖的输入变化作为事件时,一个简化办法是:

always @(*) begin 
	tmp1 = a & b; 
	tmp2 = c & d;
	y = tmp1 | tmp2;
end
/* 或者 */
always @* begin 
	tmp1 = a & b; 
	tmp2 = c & d;
	y = tmp1 | tmp2;
end

事实上,不把所有依赖的变量列出来的always块中做阻塞赋值,或者是列了其他无关的变量做的的阻塞赋值,电路上是没法实现的,电路中只要输入变化了,输出就会跟着变化,输入不变,输出也不会变化。因此,可综合的RTL描述中,总是要求把依赖的变量变化事件列出来,有时候变量多了,很容易没有列全,造成歧义,因此推荐用@或@()这种表达方式,避免这些小的问题。
时序控制可以放在always后面,也可以放在块中的每条语句之前,还可以放在赋值语句的赋值符号与表达式之间。放在always语句的always关键字之后,可以控制always后面的语句的执行条件,比如always @(a or b) statement,表示在a或b变化时执行statement。always @(posedge clk) statement表示在clk信号的上沿时执行statement。always # 10 statement表示等待10个时间单位执行statement。
时序控制如果放在赋值符号与表达式之间,表示先做表达式计算,然后进行延时或者等待事件,然后才赋值。比如:

a = #5 b;
/* 与下面的语句等价 */
begin
	temp = b;
	#5 a = temp;
end

a = repeat(3) @(posedge clk) b;
/* 与下面语句等价 */
begin
	temp = b;
	@(posedge clk);
	@(posedge clk);
	@(posedge clk) a = temp; 
end

除了用@表示变化,还可以用wait(expr)来等待expr的值非零这样的事件。比如:
begin
wait (!enable) #10 a = b;
#10 c = d;
end
就是等待enable从1变化到0时,延时10个时间单位,然后执行a=b,在延时10个时间单位,然后执行c=d;
可以声明专门的event变量,用来做多个并行块之间的同步,具体的办法是在关键字event后面带一串标识符,就声明了一系列命名事件,可以带数组声明。
在语句块中可以用->事件名称的方式来触发事件的产生,其他语句用@事件名称来等待该事件。

7.9 任务和函数

verilog中有类似于c语言中的函数的表达方式,来把一些公共的多个地方会用到的操作集中在一起实现,这样达到“做同样的事情运行同一段代码”的目标,有利于程序的维护,增加代码的可读性。verilog中类似的结构分两种,分别是任务task和函数function,它们之间的差别在于:

  • function总是运行在调用时刻,其中没有时序控制,task中可以包含时序控制的描述
  • function中不能启动一个task,task则可以启动其他的task和调用其他function
  • function至少要有一个input类型的参数,并且不能有output或inout类型的参数,task则可以有任意多个任意类型的参数,当然也可以没有参数。
  • function返回一个值,task不返回任何值
  • 调用函数时,没有任何延迟会返回一个值,函数可以调用自己,实现递归调用。启动一个task后,则要等到task结束才会返回,task内部也可以启动其他task,形成递归。
    并行的语句中可以同时启动多个task,理论上对启动task的个数不做限制。
    task和function中可以访问module中在声明task或function之前已经声明变量,task和function。然而它内部的命名空间中名字如果跟外面声明的重复,则引用的是内部声明的名字。

7.9.1 任务的声明及启动

task作为一个module_item声明在module中,具体格式如下:

task_declaration ::=
  	task [ automatic ] task_identifier ;
	{
      task_item_declaration } 
	statement_or_null 
	endtask
| 	task [ automatic ] task_identifier ( [ task_port_list ] ) ;
	{
      block_item_declaration } 
	statement_or_null 
	endtask 

task就像一个module一样,可以声明input, output, inout类型的端口,声明reg, integer, time, real, realtime, event变量,也可以声明参数和局部参数。task在声明端口和变量后,可以跟着一个语句,然后是endtask结束声明。
在启动task的地方,直接写task的名称,并给端口赋予实参表达式(inout和output必须赋予能当左值的表达式),就启动了一个task。例如:

task my_task; 
input a, b; 
inout c; 
output d, e; 
begin
	/* 其他的语句 */
	c = foo1; /* 修改返回的参数,此时相当于对inout和output的实参进行赋值 */
	d = foo2;
	e = foo3;
end
endtask
/* 或者用下面的等价形式 */
task my_task (input a, b, inout c, output d, e); 
begin
	/* 其他的语句 */
	c = foo1; 
	d = foo2;
	e = foo3;
end
endtask

使用my_task (v, w, x, y, z);的形式就启动任务my_task。一个稍微完整的例子,完成跟前面7.2中例子等价的任务:

module test;
task initwck(input v);
  wClk = v;
endtask

reg wClk;
reg [15:0] bData;
reg nwReset;

initial begin
   initwck(0);
   nwReset = 1'b1;
   #10
   nwReset = 1'b0;
   #20
   nwReset = 1'b1;
end
always #5 wClk = ~wClk;
always @(posedge wClk) 
if (!nwReset)
	bData <= 0;
else
  bData <= bData + 1;
endmodule   

下面的例子用task演示红绿灯控制

module traffic_lights; 
reg clock, red, amber, green;
parameter on = 1, off = 0, red_tics = 350, 
amber_tics = 30, green_tics = 200;
// initialize colors. 
initial red = off; 
initial amber = off; 
initial green = off;
always begin // sequence to control the lights. 
	red = on; // turn red light on 
	light(red, red_tics); // and wait.
	green = on; // turn green light on 
	light(green, green_tics); // and wait.
	amber = on; // turn amber light on 
	light(amber, amber_tics); // and wait. 
end
// task to wait for 'tics' positive edge clocks 
// before turning 'color' light off. 
task light; 
	output color; 
	input [31:0] tics; 
	begin
		repeat (tics) @ (posedge clock); 
		color = off; // turn light off. 
	end
endtask

always begin // waveform for the clock. 
	#100 clock = 0; 
	#100 clock = 1; 
end
endmodule // traffic_lights

下面是仿真波形:
HDL4SE:软件工程师学习Verilog语言(七)_第6张图片
task声明默认是static的,可以声明为automatic类型的,如果一个module实例中启动了一个task的多个实例,那么对static类型的task,其中的局部变量多个task实例是共享的,此时外部可以通过task名称加变量名访问到内部的局部变量,如果声明是automatic类型的,则在启动task时,会临时分配一段存储空间用来存储task内部声明的变量,此时外部就无法访问了,因为也无法知道访问的是哪个task实例。当然,如果在不同的module实例中,即使是static类型,也有独立的存储空间。

7.9.2 用disable来停止一个命名的块语句和任务

verilog中提供了diable语句,用来终止一个命名的块语句(复合语句)或者已经启动的task,办法是:
disable 块语句名称或task名称;
可以通过中间用小数点隔开的模块实例名序列来跨层次访问子模块的块语句或任务。
语句块中也可以终止自己,类似于c语言中的return 或者break功能。比如:

begin : assign_rega
	rega = regb;
	if (regb != 0)
		disable assign_rega; /* 此时停止assign_rega块的执行,直接跳到end */
	regc = rega; 
end
/* 同样的用法可以在task中使用: */
task proc_a;
begin
...
...
	if (a == 0)
		disable proc_a; // return if true
...
...
end
endtask

再看稍微更加复杂点的例子,启动一个名为action的task,reset事件发生时则停止整个event_expr,包括正在执行中的action。

fork
	begin : event_expr
		@ev1;
		repeat (3) @trig;
		#d action (areg, breg);
	end
	@reset disable event_expr;
join

下面这个例子,每隔250个时间单位将q设置为0。当retrig事件发生时,设置块被终止执行,q被设置为1并保持下去。

always begin : monostable
	#250 q = 0;
end
always @retrig begin
	disable monostable;
	q = 1;
end

7.9.3 函数的声明及调用

函数的声明与task稍有差别:

function_declaration ::=
	function [ automatic ] [ function_range_or_type ] 
	function_identifier ;
	function_item_declaration {
      function_item_declaration } 
	function_statement 
endfunction
| 	function [ automatic ] [ function_range_or_type ] 
	function_identifier ( function_port_list ) ;
	{
      block_item_declaration } 
	function_statement 
endfunction

function可以声明一个返回值类型,如果不声明,则返回一个一位的值,可以声明返回类型为real, time, realtime, integer,不指定类型则返回类似reg的值。返回值可以跟一个宽度说明。在function中对函数名称进行赋值就表示设置返回值。函数必须跟一个输入参数。例如:

function [7:0] getbyte; 
input [15:0] address; 
begin
	. . .
	getbyte = result_expression;
end
endfunction
/* 下面的代码是等价的 */
function [7:0] getbyte (input [15:0] address); 
begin
	. . .
	getbyte = result_expression;
end
endfunction

调用函数时,用这种格式:word = control ? {getbyte(msbyte), getbyte(lsbyte)}:0;即可,语法上函数调用的地方相当于一个函数返回值类型的表达式。
函数的使用相比任务有更多的限制,具体如下:

  • 函数中不能包括任何时序控制的语句,就是带#,@,wait的语句
  • 函数中不能启动任务
  • 函数必须包含至少一个输入参数
  • 函数中不能有output或inout的参数
  • 函数中不能有非阻塞赋值和程序持续性赋值
  • 函数中不能触发任何事件

函数中可以调用自己,构成递归调用。例如:

module tryfact;
// define the function
function automatic integer factorial;
	input [31:0] operand;
	integer i;
	if (operand >= 2) 
		factorial = factorial (operand - 1) * operand;
	else
		factorial = 1;
endfunction
// test the function
integer result;
integer n;
initial begin
	for (n = 0; n <= 7; n = n+1) begin
		result = factorial(n);
		$display("%0d factorial=%0d", n, result);
	end
end
endmodule // tryfact

仿真时输出:

0 factorial=1
1 factorial=1
2 factorial=2
3 factorial=6
4 factorial=24
5 factorial=120
6 factorial=720
7 factorial=5040

7.9.4 常数函数

再很多常数出现的地方,可以使用函数调用,来简化比较复杂的计算,此时能调用的函数称为常数函数。常数函数有如下限制:

  • 不能有跨层的访问
  • 如果调用函数,只能调用本module中声明的常数函数
  • 可以调用能够在constant_expression出现的系统函数,不能调用其他系统函数
  • 常数函数中的系统任务被忽略
  • 在调用常数函数时所有的参数必须已经定义
  • 除了参数和函数名外的标识符都必须在当前函数中定义
  • 如果有任何参数直接或间接受defparam语句影响,结果不确定,此时可以产生一个错误,常数函数可以返回一个立即值。
  • 不能在一个generate块中定义常数函数
  • 不能自己调用自己
    例如:
module ram_model (address, write, chip_select, data);
	parameter data_width = 8;
 	parameter ram_depth = 256;
 	localparam addr_width = clogb2(ram_depth);
 	input [addr_width - 1:0] address;
 	input write, chip_select;
 	inout [data_width - 1:0] data;
 	//define the clogb2 function
 	function integer clogb2;
 		input [31:0] value;
 		begin
 			value = value - 1;
 			for (clogb2 = 0; value > 0; clogb2 = clogb2 + 1)
 				value = value >> 1;
 		end
 	endfunction
 	reg [data_width - 1:0] data_store[0:ram_depth - 1]; 
 	//the rest of the ram model 
 endmodule

用下面的参数实例化ran_model时,
ram_model #(32,421) ram_a0(a_addr,a_wr,a_cs,a_data);
局部参数addr_width就是使用常数函数clogb2计算出来的。

7.10 小结

行为级描述是verilog的一个重要的功能,该功能让verilog可以从非常高的层次对系统进行建模,不需要一下子将模型写到RTL级别,可以作为系统的概念级设计阶段的一种描述方式。
遗憾的是,行为级描述往往不是可以综合的,也就是说它实际不能直接转换成实际的电路,FPGA开发工具和ASIC开发工具一般都不支持行为模型来进行设计,然后直接综合成电路。所以事实上感觉verilog跨度非常大,行为级描述,RTL描述,门级描述甚至开关级描述虽然可以共存在一中语言语法中,其实只能用于不同的场合。还不如定义成三个不同配置的子语言可能还清晰一些。
后面我们会更加关注语言要素如何编译成电路,HDL4SE中对不能综合成电路的描述直接就忽略过去,可能会出个合乎verilog语法但是不支持的警告信息。

7.11 进展报告

这段时间的改进如下:

  • 调整IEEE.1364-2005的语法,使之能够满足LALR(1)的要求(bison要求的),配合软件实现来进行语法合规性检查。
  • 编译器已经能够接受HDL4SE的基本单元库,软件发布到0.0.7版本。
  • 模拟器设计了波形输出接口如下:
typedef struct sIHDL4SEWaveOutput {
     
   OBJECT_INTERFACE
   int (*AddSignal)(HOBJECT object, const char *unitname, const char *signalname);
   int (*SetTopModule)(HOBJECT object, HOBJECT topmodule);
   int (*ClkTick)(HOBJECT object, unsigned long long clktick);
   int (*StartRecord)(HOBJECT object);
   int (*StopRecord)(HOBJECT object);
}IHDL4SEWaveOutput;
  • 实现了一个vcd文件对象提供该接口,可以在模拟过程中输出vcd格式的波形文件。vcd文件可以用开源的gtkwave来查看,也可以用其他EDA工具来查看。下面是一个gtlwave查看前面例子生成的vcd文件的截图,看着比用Excel效果好多了啊:
    HDL4SE:软件工程师学习Verilog语言(七)_第7张图片

  • 可以考虑基于这个接口做实时的波形查看器,为此发布一个项目如下:

名称 在线的HDL4SE波形查看器
难度 软件或电子专业本科/硕士毕业设计,景嘉微二级/三级软件工程师
目标 做一个能够支持HDL4SE模拟器环境下的在线波形查看器,比如实现一个IWaveOutput接口,
能够在模拟的每个时钟周期接收到模拟器送过来的vcd数据,进行动态显示,并能存成vcd文件。
要求跨平台,保证将来能够在全国产化计算机上运行。
关键词 可以参考gtkwave,参考目前的vcdfile实现。
联系方式 [email protected]

下一步将用verilog做几个例子,然后转入RISC-V的实现。软件方面的规划第一步是实现门级网表到目标代码(c语言以基本单元库支持)的汇编级程序。也进一步完善模拟接口,完善bitnumber对象的实现。

【请参考】
1.HDL4SE:软件工程师学习Verilog语言(六)
2.HDL4SE:软件工程师学习Verilog语言(五)
3.HDL4SE:软件工程师学习Verilog语言(四)
4.HDL4SE:软件工程师学习Verilog语言(三)
5.HDL4SE:软件工程师学习Verilog语言(二)
6.HDL4SE:软件工程师学习Verilog语言(一)
7.LCOM:轻量级组件对象模型
8.LCOM:带数据的接口
9.工具下载:在64位windows下的bison 3.7和flex 2.6.4
10.git: verilog-parser开源项目
11.git: HDL4SE项目
12.git: LCOM项目
13.git: GLFW项目

你可能感兴趣的:(笔记,编程语言,verilog,c++)