FPGA之道(32)Verilog基本程序框架

举报
李锐博恩 发表于 2021/07/15 01:12:57 2021/07/15
【摘要】 文章目录 前言Verilog基本程序框架Verilog基本程序框架模板模块接口部分模块命名部分参数定义部分端口列表部分 模块实现部分声明部分语句部分 Verilog基本程序框架范例Verilog注释语法单行注释段落注释 前言 这篇博客摘自《FPGA之道》上对于Verilog程序框架的描述,采用与VHDL语法描述同样的方式,例:VHDL基本程序...

前言

这篇博客摘自《FPGA之道》上对于Verilog程序框架的描述,采用与VHDL语法描述同样的方式,例:VHDL基本程序框架
由于先学习的Verilog,所以,对于Verilog部分,就当做复习与回顾了。
强迫自己仔细看一遍过往的东西肯定也会有所收获。

Verilog基本程序框架

在介绍详细的Verilog语法之前,让我们先来了解一下Verilog代码的基本程序框架,这样可以让我们先对Verilog程序设计有一个整体的概念把握,进而在后续的Verilog语法学习中做到有的放矢。阅读本节时请着眼于大体,而不要过分去苛求细节语法,细节的语法介绍将在后续的小节中慢慢展开。

Verilog基本程序框架模板

Verilog基本程序框架的模板可以概括如下:

module <module_name> 
#(parameter <parameter_name> = <value>, <other parameters>) 
(
<mode> <type> <range> <port_name>,
<other ports>
);

<type> <data_name>;
<other data type definitions>

<module_name>
#(.<parameter_name> (<paremeter_value>),
<other parameters>
)
<instance_name>(
.<port_name> (data_name),
<other ports>
); 
<other instances>;

always @ (<sensitive_list>)
begin
	<statements>;
end
<other always blocks>;

<continuous assignments>;

endmodule

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

从Verilog基本程序框架模板中,我们可以看出它只有一个大的部分,即module,称之为模块。而module中主要包括两大部分,即module的接口部分与module的实现部分。这两部分就好比我们电路板上的芯片一样,接口部分负责描述芯片的管脚信息,而实现部分负责描述芯片具体的功能。 下面我们来分别介绍。

模块接口部分

模块的接口部分主要描述模块的外貌,主要是模块的输入和输出等的端口信息。对应的语句如下:

module <module_name> 
#(parameter <parameter_name> = <value>, <other parameters>) 
(
<mode> <type> <range> <port_name>,
<other ports>
);

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

可见,模块接口部分可以细分为三个部分,即模块命名部分,参数声明部分和端口列表部分。分别介绍如下:

模块命名部分

模块命名部分很简单,语法关键字就是module,然后空格后跟随一个模块名字即可。

参数定义部分

参数定义部分是一个可选结构,语法关键字parameter,如果模块有需要定义的一些参数,可以紧随模块命名部分之后进行声明。Parameter的意义有两个方面,一是增加代码的可重用性;一是定义代码中的一些常量,增强代码可读性和方便代码修改。

端口列表部分

端口列表部分,顾名思义,即是对输入、输出等接口的描述,它没有专门的语法,仅仅是以小括号括起来的端口列表表示,位于模块接口的最后一个部分。Verilog中的端口共有3种:输入端口、输出端口以及双向端口,定义时分别对应语法如下:

input <range> <port_name>,
output <type> <range> <port_name>,
inout <range> <port_name>,

  
 
  • 1
  • 2
  • 3

当有多个端口具有同样的范围、方向和类型时,也可以使用如下语法:
<port_name_list>,
其中input就是输入端口的意思,没什么需要特别说明的。下面主要介绍一下output与inout类型的端口。
从output端口的定义来看,它多了一个语法,简单来说是这样的,默认的端口类型都是线网类型,而有时候我们输出的数据是一个寄存器类型的,那么就必须在output后加reg关键字,例如
output reg a;
否则按照Verilog的语法,会因为数据类型不匹配而报错。

inout端口是双向端口,即既可以利用它发送数据也可以利用它接收数据,主要在处理总线数据时使用,一般不用于FPGA的内部逻辑。
最后,我们发现所有的端口声明中都有一个选项,在Verilog中,声明一个端口或者变量的时候需要明确指定其位宽,例如,如果我们需要声明一个位宽32位的双向总线,我们可以写成如下形式:
inout [31:0] bus;
当然也可以写成:
inout [0:31] bus;
不过通常情况下还是建议从大到小进行范围定义,因为这样与数的2进制表示是一致的,方便使用和操作

模块实现部分

从端口定义结束到endmodule关键字之间为模块实现部分。模块实现部分按照功能特性可分为两部分——声明部分和语句部分,分别介绍如下。

声明部分

与任何程序语言一样,除非实现特别简单的功能,否则总少不了对中间变量的需求,因此,当我们在Verilog中需要使用一些中间变量来作为数据或运算结果的暂存时,就可以在这里声明。
Verilog的声明部分特别灵活,除了声明变量以外,它还有很多种变化的写法,简介如下:

  • 变化一:声明输入输出端口。在Verilog的早期版本中,端口列表中是不出现类似input或者output的关键字的,而仅仅出现端口的名称,这个时候,端口的方向等等参数就需要在声明部分写明。例如:
	module portDefine(a,b);
input [1:0] a;
output reg b;
……
endmodule

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 变化二:分开声明端口的方向和类型。上述例子还可以写成如下形式:
module portDefine(a,b);
input [1:0] a;
output b;
reg b;
……
endmodule

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 变化三:声明参数。在模块接口部分我们讲过一个参数定义部分,那么其实参数的定义也可以放在声明部分中。但是注意,此时的参数由于声明在端口列表之后,所以也就不能被端口列表中使用。例如:
module portDefine(
input [WIDTH-1 : 0] a, //这是注释不是代码,这样写会报错!
output b
);
parameter WIDTH = 8;
wire [WIDTH-1 : 0] t;
……
endmodule

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 变化四:声明可应用到端口的参数。为了解决变化三中不能对端口列表进行参数使用的问题,需要利用变化一,代码修改如下:
module portDefine(a,b);
parameter WIDTH = 8;
input [WIDTH-1 : 0] a;
output b;
……
endmodule

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当然了,这里的参数声明并不仅仅限于parameter关键字。
最后,虽然声明部分写法非常灵活,但是建议大家在写该部分的时候还是不要太随意,选准一种风格,这样既可以提高代码的可读性,又可以减少出错的概率。

语句部分

模块中的语句部分是整个模块功能的核心,位于其中的语句都是并发执行的,即与语句的书写顺序无关。根据Verilog基本程序框架模板,我们可以看出这些并行语句主要包含三种形式:实例化语句、程序段和连续赋值语句。分别介绍如下。

  • 实例化语句。在Verilog中,可以通过将一些功能简单的模块聚合在一起,从而形成一个功能比较复杂的模块,然后再将这些功能比较复杂的模块聚合在一起,进而形成一个功能更复杂的模块,如此这般,便可以做出一个功能非常强大的FPGA设计。那么如果换个角度来看,其实这就相当于一个根模块,调用了一些功能复杂的模块,而这些功能复杂的模块又各自调用了一些功能较复杂的模块,而这些功能较复杂的模块又各自调用了一些功能简单的模块,从而实现了一个功能非常强大的设计。那么,这种在一个模块中调用另一个模块的过程就是实例化的过程。实例化语句没有专门的语法关键字,只需要知道已有模块的名字和端口列表,再为实例起一个名字即可。写法如下:
<module_name> <instance_name>(
.<port_name> (data_name),
<other ports>
);

  
 
  • 1
  • 2
  • 3
  • 4
  • 在一个FPGA项目中,有一个默认的work库,所有的Verilog代码所描述的module都会被自动添加到work库当中,当然了,这些module必须是不能重名的。 而实例化语句就是一个Verilog代码从work库中选择相应的模块来进行实例的方法。它对我们能够实现复杂的层次化设计至关重要!实例化语法中最为重要的一步就是端口的映射,这其实也是赋值的一种表现形式。通过这一步,可以将被调用的子模块的端口和调用其的父模块中的变量进行连接,从而将子模块的功能融入到父模块中去。
    对比Verilog基本程序框架模板,我们可以发现,实例化语句还有一个参数映射环节。这要追朔到前面模块接口中讲的参数定义部分。为了增加一个模块的可重用性,我们往往需要参数化这个模块,最简单的例子就是对其信号的位宽进行参数化,这样我们在实例化该模块的时候,就可以通过修改该参数的值,来灵活的控制我们需要的数据位宽。需要说明的一点是,实例化时,我们不光可以修改模块接口中的参数定义部分定义的参数值,也可以修改写在模块实现中的声明部分的参数值。
  • 程序块。程序块是verilog中最常用的一种语句结构,利用程序块,我们可以很方便的按照程序的功能来实现对代码的聚类。程序块中最常用的一种就是always程序块,always程序块会反复不断的执行,通常,我们都会在它内部用串行语句来进行描述,从而实现一些复杂的功能。所谓的串行描述,即语句的执行顺序和它们的位置息息相关,写在前面的语句先执行,然后才能轮到它后面的语句执行。注意,此处的串行语句只是用来描述一些并行语句不太方便描述的功能的,例如时序逻辑,而真正的硬件执行都是并行的,因为硬件在工作的时候离不开电流和电压,而电压和电流是存在于整个回路中的,不会因为各个模块排列的先后顺序而导致哪里先有电流,哪里后有电流。
	always的语法如下:
always @ (<sensitive_list>)
begin
	<statements>;
end

  
 
  • 1
  • 2
  • 3
  • 4
  • 5

这其中需要说明的一点就是<sensitive_list>,在Verilog语言中,<sensitive_list>共有三种表现形式,从而也对应了三种最基本的always类型。
类型一,纯组合逻辑always。这个always里面只描述组合逻辑,<sensitive_list>一般应该包含该always所有的输入信号。如何判断always的输入信号呢?一般来说,always中满足以下三个条件的信号即是一个always的输入信号:
条件一:出现在赋值等号右边的信号;
条件二:出现在条件判断中的信号;
条件三:没有出现在赋值等号左边的信号。
条件三的作用是剔除掉那些在条件一、二中被错误选择出来的中间信号。不过上述三点其实是写给看代码的人参考的,因为在现实中,写代码的人不可能不清楚自己的always到底使用了哪些输入信号。
类型二:纯同步时序逻辑always。这个always只描述同步时序逻辑,<sensitive_list>应该只包含该同步时序逻辑的时钟信号即可。
类型三:具有异步复位的时序逻辑always。这个always只描述具有异步复位的时序逻辑,<sensitive_list>应该只包含异步复位信号和相应时序逻辑的时钟信号。
具体程序设计时,可以根据情况灵活选择always的三个“纯种”的基本类型来解决问题,不推荐使用“杂种”的always。

  • 连续赋值语句。连续赋值语句是针对线网变量的一种赋值语句,线网变量一般对应到FPGA中的一段连线,而连线的值是会随着它的驱动源的变化而不停变化的,因此也就称之为连续赋值语句。顾名思义,连续赋值语句肯定是一种描述组合逻辑的语句。以下是连续赋值语句的一个例子:
wire a, b;
	assign a = b;
	其实,同样的功能也可以用always语句来描述,即
	reg a;
	wire b;
	always@(b)
		a = b; //只有一句语句的时候可以省略begin和end

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

由此可见,上述连续赋值语句其实就相当于只有一条代码的纯组合逻辑always,只不过由于always对输出变量类型的要求,我们需要将a该为reg类型。
最后,需要说明一点,那就是并不是所有的语句都能直接写在module的语句部分的,例如if…else…语句,它只能依附于always等程序块。

Verilog基本程序框架范例

在本节的最后,让我们结合一个范例,来复习一下本节的内容。范例共包含两个文件,cellAnd.v和cellNand.v。
cellAnd.v的代码如下:

module cellAnd
#(parameter WIDTH = 8)
(
input [WIDTH-1 : 0] a,
input [WIDTH-1 : 0] b,
output [WIDTH-1 : 0] c 
);
assign c = a & b;
endmodule

cellNand.v的代码如下:
module cellNand(
input [3:0] a,
input [3:0] b,
output reg [3:0] nc1,
output [3:0] nc2
);

wire [3:0] c;
reg [3:0] tmp;

cellAnd #(.WIDTH(4))
		m0(.a (a), .b (b), .c (c)); assign nc2 = ~ c; always@(a, b)
begin
	tmp <= a & b;
	nc1 <= ~ tmp;
end endmodule

  
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

这个范例其实实现了对两个4bits向量a和b按位做与非逻辑运算的功能,nc1与nc2其实功能上是一摸一样的,只不过给大家演示一下不同的实现方法罢了。

Verilog注释语法

在【共同语言篇->硬件描述语言->HDL简史->Verilog HDL发展史】小节,我们介绍过Verilog语言的创造者当时从C语言中获得了不少灵感,这其中就包括借鉴了C语言的注释语法。因此,Verilog的注释语法有两种,分别是单行注释与段落注释。

单行注释

单行注释语法如下:
//
即两个连续的斜横杠组成了Verilog的注释符号”//”,凡是写在这之后的并且与注释符号同在一行的任何字符,都会被自动忽略,即成为注释。例如:
a = b & c; // this is an AND gate
或者
// this is an AND gate
a = b & c;
通过连续的使用单行注释符,也可以完成多行注释,例如:
// this
// is a
// multi-line comments !

段落注释

段落注释的语法如下:
/*

/
即符号”/
”表示段落注释的起始,符号”/”表示段落注释的结束,写在这两个符号之间的所有内容,都被视为注释。例如:
/
this
is a
multi-line comments ! /
可见,段落注释可以更加方便的实现多行注释的功能。但是请注意,段落注释中,段落也可以只有一行的内容,例如:
/
this is a comment */
除此以外,段落注释中的段落甚至还可以是非整行的内容,这是单行注释所无法完成的功能,例如:
wire v,/short for Verilog/ vhd/short for VHDL/;

文章来源: reborn.blog.csdn.net,作者:李锐博恩,版权归原作者所有,如需转载,请联系作者。

原文链接:reborn.blog.csdn.net/article/details/104300316

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。