> 文章列表 > Verilog Tutorial(9)任务Task与函数Function的使用

Verilog Tutorial(9)任务Task与函数Function的使用

Verilog Tutorial(9)任务Task与函数Function的使用

写在前面

在自己准备写verilog教程之前,参考了许多资料----FPGA Tutorial网站的这套verilog教程即是其一。这套教程写得不错,只是没有中文,在下只好斗胆翻译过来(加了自己的理解)分享给大家。

这是网站原文:https://fpgatutorial.com/verilog/

这是系列导航:Verilog教程系列文章导航


本文将讨论如何在 verilog 中使用任务task与函数function。总的来说,task和function也被称为子程序(subprograms ),因为它们允许设计者编写可复用的verilog 代码

与大多数编程语言一样,设计者应该尽可能编写可复用性强的 verilog 代码,这能够有效地减少未来项目的开发时间----可以更轻松地将代码从一个项目移植到另一个项目。

任务task与函数function之间有两个主要的区别。编写函数时,它会执行计算并返回单个值。相反,任务则会执行许多顺序语句且不返回值,但任务可以有无限数量的输出。函数会立即执行,并且不能包含任何时间控制语句,例如delay、posedge 或wait等语句;任务则可以包含时间控制语句。

函数Function

在 verilog 中,函数是一个子程序,它可以由一个或多个输入值,运行一些计算并返回一个输出值。

在设计中的多个地方使用的相同代码的这一操作可以使用函数来替代,通过使用一个函数而不是在多个地方重复相同的代码,可以使代码更易于维护。

下面的代码片段展示了 verilog 中函数的一般语法。

//第一种方法
function <return_type> <name> (input <arguments>);// Declaration of local variablesbegin// function codeend
endfunction//第二种方法
function <return_type> <name>;(input <arguments>);// Declaration of local variablesbegin// function codeend
endfunction

每个函数都必须有一个名称,如上例中的 <name> 所示。

设计者可以将输入声明与函数声明写在同一行(方法1),或者输入声明也可以作为函数体的一部分。用来声明输入参数的方法对函数的性能没有影响。

将输入声明与函数声明写在同一行时,也可以省略 begin 和 end 关键字。

在上面的示例中, <arguments> 被用来来声明函数的输入。

<return_type> 被用来声明函数返回值的数据类型,如果在函数声明中排除这部分,那么该函数将默认返回一个 1 位的值。

当返回一个值时,通过给函数名赋值的方式来实现。下面的代码片段展示了如何简单地将输入返回给函数。

function integer easy_example (input integer a);easy_example = a;
endfunction

函数使用规则

尽管函数通常很简单,但在编写 verilog 函数时必须遵循一些基本规则。

函数一个最重要的规则是它们不能包含任何时间控制语句,例如delay、posedge 或wait等语句。当设计者想要编写一个带有时间控制的子程序时,应该改用任务。

因此设计者也无法从函数内调用任务,相反却可以从函数内调用另一个函数。

由于函数会立即执行,所以只能在函数中使用阻塞赋值。

在编写函数的时候,可以声明和使用局部变量,这意味着可以在函数中声明不能在声明它的函数之外访问的变量。除此之外,还可以在函数中访问所有全局变量。

例如,如果在一个模块中声明一个函数,那么该函数可以访问和修改该模块中声明的所有变量。

下表总结了在 verilog 中使用函数的规则。

在 Verilog 中使用函数的规则

函数可以有一个或多个输入参数

函数只能返回一个值

不能从函数中调用任务,可以从函数中调用其他函数

函数内不能使用非阻塞赋值

局部变量可以在函数内部声明和使用

可以从函数内部访问和修改全局变量

如果不指定返回类型,该函数将返回一个bit

函数示例

为了更好地演示如何使用 verilog 函数,请看一个基本示例:编写一个函数,它接受 2 个输入参数并返回它们的和。

除了使用integer类型作为输入参数和返回类型外,还必须使用加法运算符来计算输入的总和。

下面的代码片段展示了此示例函数在 verilog 中的实现。可以使用两种方法来声明 verilog 函数,这两种方法都显示在下面的代码中。

//方法1
function integer addition (input integer in_a, in_b);addition = in_a + in_b;
endfunction//方法2
function integer addition;input integer in_a;input integer in_b;beginaddition = in_a + in_b;end
endfunction

调用函数

当设计者想在verilog 设计的另一部分中使用函数时,就必须调用它。用于执行此操作的方法类似于其他编程语言。

调用一个函数时,设计者用与声明它们的顺序相同的顺序将参数传递给函数,这被称为位置关联(positional association),这意味着声明参数的顺序非常重要。

下面的代码片段展示了如何使用位置关联的方法来调用加法示例函数。

在下面的示例中,in_a 将映射到 a 参数,in_b 将映射到 b。

//函数调用
func_out = addition(a, b);

Automatic 函数

设计者还可以使用 verilog automatic 关键字将函数声明为可重入的(reentrant)。automatic 关键字是在Verilog-2001标准中被引入的,这意味着不能在使用Verilog-1995标准时编写可重入函数。

将函数声明为可重入时,函数内的变量和参数是动态分配的。相反,普通函数的内部变量和参数是静态分配的。

编写一个普通函数时,所有用于执行函数处理的内存都只分配一次。这个过程在计算机科学中被称为静态内存分配。因此,仿真软件必须完整地执行该功能,然后才能再次使用该功能。这也意味着函数使用的内存永远不会被释放(deallocated)。因此,存储在此内存中的任何值将在函数调用之间保持它们的值。

相反,使用 automatic 关键字的函数会在调用函数时分配内存。一旦函数完成,内存就会被释放。此过程在计算机科学中称为自动或动态内存分配。因此,仿真软件可以实现automatic函数的多个实例。

设计者可以使用automatic关键字在verilog中编写递归函数(recursive functions),这意味着可以创建调用自身来执行计算的函数。

例如,递归函数的一个常见用例是计算给定数字的阶乘。

下面的代码片段展示了如何使用 automatic 关键字在 verilog 中编写递归函数。

function automatic integer factorial (input integer a);beginif (a > 1) beginfactorial = a * factorial(a - 1);endelse beginfactorial = 1;endend
endfunction

任务Task

就像函数一样,任务可以用来替代在整个设计中重复使用的一小段代码。任务可以有任意数量的输入,也可以生成任意数量的输出。这与只能返回单个值的函数形成对比。

与函数不同,在任务中还可以使用时间控制语句,例如 wait、posedge 或 delays (#)。因此,在任务中同时使用阻塞赋值和非阻塞赋值。这些特性意味着任务最适合用于实现在设计中重复多次的简单代码片段。

设计者可以创建由特定文件中的所有模块共享的全局任务,为此只需在文件中的模块声明之外编写任务代码。

下面的代码片段展示了任务的一般语法。

//方法1
task <name> (<io_list>);begin//实现任务的代码end
endtask//方法2
task <name>;<io_list>begin//实现任务的代码end
endtask

与函数一样,任务也可以通过两种方式声明,但两种方式的效果是相同的。

每个任务都必须有一个名称,如上面的 <name> 所示。

当编写在任务主体中声明输入和输出的任务时,还必须使用 begin 和 end 关键字。但是,当对输入和输出使用内联声明(inline declaration)时,可以省略 begin 和 end 关键字。

在编写任务时,可以声明和使用局部变量,这意味着可以在任务中创建变量,这些变量不能在声明它的任务之外访问。除此之外也可以访问任务中的所有全局变量。

与函数不同,任务中可以调用任务和函数。

任务示例

让考虑一个简单的示例,以更好地演示如何编写任务。为此将编写一个可用于生成脉冲的基本任务。调用任务就可以指定脉冲的长度。

为此,需要一个输入(确定脉冲的长度)和一个用于生成脉冲的输出。

下面的 verilog 代码展示了使用两种不同风格的任务来实现这个例子。

//方法1
task pulse_generate(input time pulse_length, output pulse);pulse = 1'b1#pulse_timepulse = 1'b0;
endtask//方法2
task pulse_generate;input time pulse_length;output pulse;beginpulse = 1'b1;#pulse_timepulse = 1'b0;end
endtask

虽然这个示例很简单,但在这里看到如何在任务中使用时延运算符 (#)。如果在一个函数中编写这段代码,那么肯定会编译错误。

从这个例子中也可以看出,这时并没有像处理函数那样有返回值。相反,必须声明在任务声明中使用的任何输出。编写任务时可以包含和驱动任意数量的输出。

调用任务

调用任务的方法类似于用于调用函数的方法。但是有一个重要区别----调用任务时,不能像使用函数那样将其用作表达式的一部分。相反,应该将任务调用视为将代码块打包到设计中的一种简便方法。

与函数一样,调用任务时使用位置关联方法将参数传递给任务,这意味着将参数传递给任务的顺序与在编写任务代码时声明它们的顺序要相同。

下面的代码片展示了如何使用位置关联方法来调用之前的 pulse_generate 任务。在这种情况下,pulse_length 输入映射到 pulse_time 变量,脉冲输出映射到 pulse_out 变量。

generate_pulse(pulse_time, pulse_out);

Automatic任务

设计者还可以使用 verilog automatic 关键字将任务声明为可重入的(reentrant)。automatic 关键字是在Verilog-2001标准中被引入的,这意味着不能在使用Verilog-1995标准时编写可重入任务。

使用 automatic 关键字意味着仿真工具将使用动态内存分配。与函数一样,任务默认使用静态内存分配,这意味着仿真工具只能运行任务的一个实例。相反,使用 automatic 关键字的任务会在任务被调用时分配内存。一旦任务完成,内存就会被释放。

请看一个基本示例来展示automatic任务的使用以及它们与普通任务的区别。这个例子将使用一个简单的任务,将局部变量的值增加给定的数量,然后在模拟工具中多次运行它,以查看局部变量在使用automatic任务和普通任务时的区别。

下面的代码展示了如何编写一个静态任务来实现这个例子。

task increment(input integer incr);integer i = 1;i = i + incr;$display("Result of increment = %0d", i);
endtask//运行3次
initial beginincrement(1);increment(2);increment(3);
end

在仿真工具中运行此代码会产生以下输出:

Result of increment = 2
Result of increment = 4
Result of increment = 7

从这里可以看出,局部变量 i 的值是静态的,并且存储在一个单独的内存位置。因此,i 的值是不变的(persistent),并且在任务调用之间保持它的值。

调用任务时,正在增加已经存储在给定内存位置的值。

下面的代码片段显示了相同的任务,只是这次使用了 automatic 关键字。

task automatic increment(input integer incr);integer i = 1;i = i + incr;$display("Result of increment = %0d", i);
endtask//运行3次
initial beginincrement(1);increment(2);increment(3);
end

仿真工具中运行此代码会产生以下输出:

Result of increment = 2
Result of increment = 3
Result of increment = 4

从这里可以看到局部变量 i 是如何变成动态的,其在调用任务时创建。在它被创建之后,它被赋值为 1。当任务完成运行时,动态分配的内存被释放,局部变量不再存在。


  • 📣您有任何问题,都可以在评论区和我交流📃

  • 📣本文由 孤独的单刀 原创,首发于CSDN平台🐵,博客主页:wuzhikai.blog.csdn.net

  • 📣您的支持是我持续创作的最大动力!如果本文对您有帮助,还请多多点赞👍、评论💬和收藏