SV学习(2)——过程语句、函数和任务
1. 过程语句
1.1. 硬件过程块
在SV中首先需要清楚哪些语句应该被放置于硬件世界,哪些程序应该被放置于软件世界;
为了区分硬件世界和软件世界,先引申出一个概念域(scope)。为了区分硬件设计、软件设计,我们将定义的软件变量或者例化的硬件其所在的空间称之为域;
module / endmodule,interface / endinterface可以被视为硬件世界,program / endprogram和class / endclass可以被视为软件世界。
always是为了描述硬件的行为,而在使用时需要注意哪些使用方式是时序电路描述,哪种使用方式事组合电路描述;
always中的 @(event …) 敏感列表是为了模拟硬件信号的触发行为,同学们需要正确对标硬件行为和always过程块描述。需要理解硬件行为的核心要素有哪些?
所以说,always过程块是用来描述硬件时序电路和组合电路的正确打开方式,因此只可以在module或者interface中使用;
always具备描述硬件电路行为的核心要素,你认为下列哪些事正确使用always的方式?
- 由时钟驱动
- 由其他非时钟信号驱动
- 不同always语句块之间是并行执行的
- 可以在always中初始化变量
always中,可以对对应的信号做复位,初始化变量是一个软件的概念,通常在定义变量的时候或者在initial中进行初始化。
initial从名字也看得出来,与always在执行路径上有明显区别,即initial非常符合软件的执行方式,即只执行一次;
initial和always一样,无法被延迟执行,即在仿真一开始它们都会同时执行,而不同initial和always之间的执行顺序上是没有顺序可言的,因此不应该将它们在代码中的前后顺序与它们的执行顺序画上等号;
initial从其执行路径的属性来看,它不应该存在于硬件设计代码中,它本身不可综合,对于描述电路没有任何帮助;
initial就是为了测试而生的,由于测试需要按照时间顺序的习惯,即软件方式来完成,所以initial便可以实现这一要求;
在Verilog时代,所有的测试语句都可以被放置在initial中,为了便于统一管理测试顺序,建议将有关测试语句都放置在同一个initial过程块中;
initial过程块可以在module、interface和program中使用;
对于过程块的书写方式,请记住用begin…end将其作用域“包”住,这一建议同样适用于稍后提到的控制语句、循环语句等;
1.2. 赋值语句
SV比verilog多了自加和自减操作符,允许变量自身可以将当前值做递增或递减。
assign优先级高于普通过程赋值语句,所以处于连续赋值状态的寄存器变量将忽略普通过程赋值语句对它的过程赋值操作,其逻辑状态仍然由过程连续赋值语句表达式做决定。使用assign对寄存器型变量赋值之后,这个值将一直保持在这个寄存器上,直至遇上deassign。
// assign_example
module test_assign ( );
wire [ 9: 0] net_data [10: 0];
initial begin
logic [ 9: 0] data [10: 0] ;
logic [ 9: 0] var_data ;
data[0] <= 'x; // 非阻塞赋值
data[1] = 'z; // 阻塞赋值
#5 data[2] = 7;
#5 data[2]++; // 变量自加
#5 data[2]--; // 变量自减
#5 assign var_data = data[2]; // 连续赋值语句将7赋值给var_data
#5 var_data = 4; // 不会改变var_data地数值
#5 deassign var_data; // 去除连续连续赋值地作用
#5 var_data = 5; // var_data的值改变了
#5 force var_data = data[1];
#5 var_data = 8;
#5 release var_data;
#5 var_data = 9;
#5 force net_data[1] = 10'h00;
#5 release net_data[1];
end
endmodule
1.3. 控制语句
1.3.1. for循环
SV允许在循环内产生一个本地变量,其他并行循环不会偶然地影响这个循环控制变量
for (int i = 0, j = 0; i < 256; i++, j++)
...
1.3.2. while循循环
举例,计算向量中1的个数
// while_example
module test_while ( );
logic [ 3: 0] count;
logic [ 7: 0] temp;
initial begin
count = 0;
temp = 8'b0011_1100;
#10
while (temp) begin
if (temp[0] == 1'b1)
count ++;
// temp = temp >> 1;
temp = {1'b0, temp[7:1]};
end
$display ("%d",count);
end
endmodule
1.3.3. do-while循环
逐位打印字符串
// dowhile_example
module test_dowhile ( );
// int map [string]; // 关联数组,索引为string型
bit [ 7: 0] map [string];
// map["hello"] = 1;
// map["sad"] = 2;
// map["world"] = 3;
// // 对关联数组的初始化放在initial外部会有报错
// // near "[": syntax error, unexpected '[', expecting IDENTIFIER or TYPE_IDENTIFIER
string s;
initial begin
map["hello"] = 1;
map["sad"] = 2;
map["world"] = 3;
s = "hello!";
if (map.first(s))
do
$display ("%8s: %2d", s, map[s]);
while (map.next(s));
end
endmodule
1.3.4. repeat循环
通过加法和移位实现乘法器
// repeat_example
module test_repeat ( );
parameter size = 8;
parameter longsize = 16;
logic [size: 1] opa = 3, opb = 6;
logic [longsize: 1] result ;
initial begin
logic [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
$display ("The result is %d", result);
end
endmodule
1.3.5. foreach循环
foreach循环语句中指定数组后,程序会逐个遍历数组成员。
foreach ( <array name> [< loop variable>] ) <statement>
它的自变量可以是一个指定的任意类型数组(固定尺寸的 / 动态的及联合数组),然后紧跟着一个包围在方括号内的循环变量的列表。每一个循环变量对应于数组的某一维度。 foreach结构类似于一个repeat循环,它使用数组返回替代一个表达式来指定重复次数。
// foreach_example
module test_foreach ( );
string words [2] = '{"hello", "world"};
int prod [ 1: 8][ 1: 3];
initial begin
// int j;
foreach (words[j])
$display (j, words[j]);
foreach (prod[k ,m])
prod[k][m] = k * m;
end
endmodule
循环变量的数目必须匹配数组变量的维数。空循环变量可以用来表示在对应的数组维数上没有迭代,并且处于尾部的连续空循环变量可以被忽略。循环变量是自动的、只读的,并且它们的作用范围对于循环来讲是本地的。每一个循环变量的类型被隐含的声明成与数组索引一致的类型。
2. 函数和任务
Verilog中要求函数必须有返回值,并且返回值必须被使用,例如用到赋值语句中;SV的限制稍微放宽了;
2.1. 函数function
- 可以在参数列表中指定输入参数(input)、输出参数(output)、输入输出参数(inout)或者引用参数(ref);
- 可以返回数值或者不返回数值(void);
function int double (input a);
return 2*a;
endfunction
initial begin
$display ("double of %0d is %0d", 10, doublr(10));
end
function属性:
- 默认的数据类型是logic,例如 input [ 7: 0] addr;
- 数组可以作为形式参数传递;
- function可以返回或者不返回结果,如果返回即需要用关键词return,如果不返回则应该在声明function时采用void function ();
- 只有数据变量可以在形式参数列表中被声明为ref类型,而线网类型则不能被声明为ref类型;
- 在使用ref时,有时候为了保护数据对象只被读取不被写入,可以通过const的方式来限定ref声明的参数;
- 在声明参数时,可以给入默认数值,例如 input [ 7: 0] addr = 0,同时在调用时如果省略该参数的传递,那么默认值即会被传递给function;
2.2. 任务 task
任务相比于函数要更加灵活,且以下不同点:
- task无法通过return返回结果,因此只能通过output、inout或者ref的参数来返回;
- task内可以置入耗时语句,而function则不能。常见的耗时语句包括@event、wait event、# delay等;
function是立即返回
task my_task (output logic [31: 0] x, input logic y);
...
endtask
通过上面的比较,我们对function和task建议的使用方法是:
- 对于初学者傻瓜式用法即全部采用task来定义方法,因为它可以内置常用的耗时语句;
- 对于有经验的使用者,请今后对这两种方法类型加以区别,在非耗时方法定义时使用function,在内置耗时语句时使用task。这么做的好处是在遇到了这两种方法定义时,就可以知道function只能运用于纯粹的数字或者逻辑运算,而task则可能会被运用于需要耗时的信号采样或者驱动场景;
- 如果要调用function,则使用function和task均可对其调用;而如果要调用task,我们建议使用task来调用,这是一位内如果被调用的task内置有耗时语句,则外部调用它的方法类型必须为task;
出个题目:
typedef struct {
bit [ 1: 0] cmd;
bit [ 1: 0] addr;
bit [31: 0] data;
} trans;
function automatic void op_copy (trans t, trans s);
t = s;
endfunction
initial begin
trans s;
trans t;
s.cmd = 'h1;
s.addr = 'h10;
s.data = 'h100;
op_copy (t, s);
t.cmd = 'h2;
end
上述代码,变量t中的三个成员{cmd, addr, data}最后数值为多少?
- {'h1, 'h10, 'h100}
- {'h2, 'h0, 'h0}
- {'h2, 'h10, 'h100}
- {'h0, 'h0, 'h0}
B:函数端口中,没有声明端口方向,默认都是输入,s和t都是input
2.3. 子程序参数
Verilog中,要求定义某些参数两次,方向、类型,任务名后不能出现输入、输出端口列表,
task my_task;
output [31: 0] x;
reg [31: 0] x;
input y;
...
endtask
SV中,任务、函数声明的形式参数可以定义在圆括号中,参数缺省方向是input,缺省类型是logic
task my_task (output int x, input logic y);
...
endtask
每一个形式参数可以选择的方向属性:
input:在开始的时候复制值
output:在结束的时候复制值
inout:在开始的时候复制,在结束的时候输出
ref:传递引用(句柄或者指针)
Verilog对于参数只有传值的方式:在子程序调用的时候,类型为input或者inout的实际参数的数值被复制到本地变量,在子程序退出的时候,类型为output或者inout的实际参数的数值被更新;
SV提供两种方式来为函数和任务传递参数:值传递和引用传递。引用传递(reference)像是C++的句柄或者指针,也就是变量的入口地址。此时,参数的类型应定义为ref
通过引用将数组传递到子程序中,
function void ary_sum (const ref int ary []);
// 引用传递,相对于指针传递
// 加const防止改变传递进来的变量的值
// 也就是只能读取变量,而不能写变量
int sum = 0;
for (int i = 0; i < ary.size; i++)
sum += ary[i];
$display ("The sum of the arrays is %0d", sum);
endfunction
通过传值的方式来传递数组参数,这样数组将被整体复制,这样消耗一定的内存和操作时间。而使用ref传递,只是获取该数组的入口地址(句柄 / 指针),操作速度快,减少内存使用。
const关键字可以防止一个函数或者任务改变一个通过ref类型传递的变量。这对于大型的数据结构通过ref传递,避免了整个结构被复制,而且在函数内部也不会被改变。一旦使用了const ref编译器就会检查子程序是否修改了该数组,当试图改变数组值时,编译器报错
在子程序修改ref参数变量的时候,其变化对于外部是立即可见的,在几个程序并行执行而想通过简单方法传递信息的情况下很有用,在并发执行线程中很有用。
下面的例子,bus.enable有效,初始化块中的thread2块马上就可以获取来自存储器的数据,而不用等到bus_read任务完成总线上的数据处理后返回,这可能需要若干个时钟周期。由于参数data是以ref方式引用传递的,所以只要任务里的data一有变化,@data语句就会触发,
// ref_example
module test_ref ( );
logic [31: 0] addr;
logic [31: 0] data;
initial fork
bus_read (addr, data);
begin
@ data;
$diaplay ("data is %0d", data);
end
join
task automatic bus_read (input logic [31: 0] addr, ref logic [31: 0] data);
// request bus and drive address
bus.request = 1'b1;
@ (posedge bus.grant) bus.addr= addr;
// wait for data from memory
@(posedge bus.enable) data = bus.data;
// release bus and wait for grant
bus.request = 1'b0;
@(negedge bus.grant);
endtask
endmodule
参数可以通过名字或者位置来传递。任务和函数的参数还可以指定缺省值,这就是使得调用任务或函数的时候可以不用传递参数。
function void print_sum (ref int a [ ]);
...
endfunction
function void calculate ( );
...
print_sum (a);
endfunction
initial begin
calculate ( );
end
若在后来需要修改print_sum函数参加参数,指定数组的开始地址和结束地址,
function void print_sum (ref int a [ ], input int start, input int last);
修改过函数后,原程序中热任何一个调用print_sum( )的代码,就需要对应改成print_sum (a1, b1, c2); 否则缺少了参数,程序无法正常运行。如采用参数默认值的方法,就不需要做改动了,
function void print_sum (ref int a [ ], input int start = 0, input int last = a.size() - 1);
下例是采用名字进行参数传递,
task many (input int a = 1, b = 2, c = 3, d = 4);
$display ("%0d, %0d, %0d, %0d", a, b, c, d);
endtask
initial begin
many(6, 7, 8, 9); // 6 7 8 9 指定所有值
many(); // 1 2 3 4 使用缺省值
many(.c(5)); // 1 2 5 4 只指定c
many( , 6, .d(8)); // 1 6 3 8 混合方式
end
建议用ref追踪外部变量的变化,如果添加const ref就表示这个数是只读的,不可以再修改还能追踪到实时的值
2.4. 子程序返回
在Verilog中,当任务、函数运行到endtask、endfunction的时候退出,由于任务中可以有延迟语句、敏感事件控制语句等事件控制语句,任务定义结构内可以出现disable终止语句,这条语句将中断正在执行的任务。在任务被中断后,程序流程将返回到调用任务的地方继续向下执行。而在SV中,可以使用return语句在任务体或函数体的任意一点退出子程序。
在Verilog中函数必须有返回值,返回值是用过对函数名赋值来完成的;而SV允许将函数声明成void类型,它没有返回值,其他函数就像Verilog一样,可以通过为函数名赋值来返回一个值,或者使用return语句实现,
// verilog
function [15: 0] my_func1 (input [ 7: 0] x, y);
my_func1 = x * y - 1; // 返回值赋值给函数名字
endfunction
// SV
function [15: 0] my_func2 (input [ 7: 0] x, y);
return x * y - 1; // 使用return语句指定返回值
endfunction
SV中,任务和函数都可以用return返回
2.5. 自动存储
Verilog-1995 中,所有对象都是静态分配的,如果在测试程序里的多个地方调用同一个任务,由于任务里的局部变量会使用静态存储区,所以不同的线程之间会窜用这些局部变量;
Verilog-2001 通过使用automatic关键字,将任务、函数和模块声明为自动存储模式,这样,仿真器就能够对其所有形式的参数和内部变量使用堆栈的形式来存储。
automatic使函数和任务的重入(reentry)成为可能,
function automatic [63: 0] factorial;
input [31: 0] n;
if (n == 1)
factorial = 1;
else
factorial = n * factorial(-1);
endfunction
在SV中,子程序仍然默认使用静态存储方式,对于所有的模块(module block)和程序快(program block)也一样;对于声明在class中的子程序和变量默认使动态存储的。SV还允许在一个静态任务中将特定的形式参数和本地变量声明成自动的(automatic),也允许在一个自动任务内将特定的形式参数和本地变量声明成静态的(static)。
3. 变量声明周期
- 在SV中,我们将数据的生命周期分为动态(automatic)和静态(static);
- 局部变量的生命周期同其所在域共存亡,例如function/task中的临时变量,在其方法调用结束后,临时变量的声明也将终结,所以它们是动态生命周期;
- 全部变量即便随这程序执行开始到结束一直存在,例如module中的变量默认情况下全部为全局变量,用户也可以理解为module中的变量由于在模拟硬件信号,所以它们是静态生命周期;
- 如果数据变量被声明为automatic,那么在进入该进程/方法后,automatic变量会被创建,而在离开该进程/方法后,automatic变量会被销毁。而static变量在仿真开始时即会被创建,而在进程/方法执行过程中,自身不会被销毁,而且可以被多个进程和方法所共享;
// life_cycle_test
module life_cycle_test ;
function automatic int auto_cnt (input a);
int cnt = 0;
cnt = cnt + a;
return cnt;
endfunction
function static int static_cnt (input a);
static int cnt = 0;
cnt = cnt + a;
return cnt;
endfunction
function int def_cnt (input a);
static int cnt = 0;
cnt = cnt + a;
return cnt;
endfunction
initial begin
$display ("@1 auto_cnt = %0d", auto_cnt(1));
$display ("@2 auto_cnt = %0d", auto_cnt(1));
$display ("@1 static_cnt = %0d", static_cnt(1));
$display ("@2 static_cnt = %0d", static_cnt(1));
$display ("@1 def_cnt = %0d", def_cnt(1));
$display ("@2 def_cnt = %0d", def_cnt(1));
end
endmodule
automatic方法,内部的所有变量也是automatic,即伴随automatic方法的生命周期建立和销毁;
static方法,内部所有变量都是static类型,第一次调用的时候,声明static int cnt = 0; 计数器加1,返回1,第二次调用的时候,cnt是1,静态变量不会再次初始化,计数器加1,返回2;
对于automatic或者static方法,用户可以对其内部定义的变量做单个声明,使其类型被显式声明为automatic或者static;
对于static变量,用户在声明变量时应该同时对其做初始化,而初始化只会伴随它的生命周期发生一次,并不会随着方法被调用被多次初始化
def_cnt ( ),默认函数是static的,里面的变量也都是静态变量;
一般情况,我们调用函数都期待是auto_cnt ( )的样子,这时候就需要automatic关键字了
在module、program、interface、task和function之外声明的变量拥有静态的生命周期,即存在于整个仿真阶段,这同C定义的静态变量一致;
在module、interface和program内部声明,且在task、process或者function外部声明的变量也是static变量,且作用域在该块内;
在module、program和interface中定义的task、function默认都是static类型;
在过程块(task、function、process)定义的变量均跟随它的作用域,即过程块的类型。如果过程块为static,则它们也默认为static,反之亦然。这些变量也可以由用户显式声明为automatic或者static;
为了使得在过程块中声明的变量有统一默认的生命周期,可以在定义module、interface、package或者program时,通过限定词automatic或者static来区分。对于上述程序默认的生命周期类型为static;
下列域中的变量哪些默认是static即静态变量?
- module中声明的变量
- module中initial过程块中声明的变量
- 在module中定义的function声明的变量
- 在module中定义的task声明的变量
4. 补充
定义这个task,为什么不用声明端口和类型?
在定义任务的时候,默认是input logic的;
clk是直接在module里面的定义的变量,这是全局变量,作用域是整个模块,可以直接在task里使用
静态变量?生命周期?全局变量?作用域?