基于FPGA的SOPC嵌入式系统设计与典型实例
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第2章 Verilog HDL语言编程基础

本章将对Verilog HDL编程基础知识进行详细讲述。首先简单介绍HDL语言的概念。

HDL(Hardware Description Language,硬件描述语言)主要用于从算法级、门级到开关级的多种抽象设计层次的数字系统建模。被建模的数字系统对象既可以是简单的门,也可以是完整的数字电子系统。硬件描述语言的主要功能是编写设计文件,建立电子系统行为级的仿真模型,然后利用高性能的计算机对用Verilog HDL或VHDL建模的复杂数字逻辑进行仿真,之后再对它进行自动综合以生成符合要求且在电路结构上可以实现的数字逻辑网表(Netlist),接着根据网表和适合某种工艺的器件自动生成具体电路,最后生成该工艺条件下具体电路的延时模型。仿真验证无误后用于制造 ASIC(Application Specific Integrated Circuit,专用集成电路)芯片或写入FPGA(现场可编程逻辑门阵列)和CPLD (复杂可编程逻辑器件)中。

在EDA领域,一般把用HDL语言建立的数字系统模型称为软核(Soft Core),把用HDL建模和综合后生成的网表称为固核(Hard Core)。重复利用这些模块可以缩短开发周期,提高产品开发成功率,并提高设计效率。

2.1 Verilog HDL语言特点

1.Verilog HDL简介

Verilog HDL 语言是一门标准硬件设计语言,它适合于电子系统设计的所有阶段。由于它容易被机器和人工阅读,因此它支持硬件设计的开发、验证、综合及测试,以及硬件设计数据的交流,便于维护、修改和最终硬件电路的获得。Verilog HDL 语言具有简单、直观和高效的特点。在各种设计工具(如仿真验证、时序分析、测试分析和综合)里面,它采用标准的文本格式,具有多层次的抽象。由于具有以上这些特点,Verilog HDL 语言已经被绝大多数的IC设计者所采用。

Verilog HDL 语言包含一套丰富的内建元件:逻辑门元件、用户自定义元件、开关级元件和连线逻辑元件。它也支持设备引脚到引脚延时和时序检测。Verilog HDL 通过定义两种数据类型(线网型和寄存器型)体现了混合的抽象层次。在Verilog HDL中有两种基本语句:连续赋值语句,在此表达式中寄存器和线网都可以连续驱动线网,实现基本的结构化建模;过程赋值语句,在此表达式中寄存器和线网都将运算结果存入寄存器,实现基本的行为建模。一个设计通常包含许多模块,每一模块都有一个输入/输出接口和该模块相应的功能描述。而该功能描述既可以采用结构化的描述方式,也可以采用行为化的描述方式,或者二者的结合。这些模块都被加工到一定的抽象级别,然后通过线网互连。

Verilog HDL语言可以通过编程语言接口程序(Programming Language Interface,PLI)和Verilog接口程序(Verilog Procedural Interface,VPI)来实现扩展。PLI/VPI是一个程序集,通过这些接口程序,外部函数可以访问包含在Verilog HDL描述中的信息,而且便于和仿真工具动态通信。PLI/VPI的应用具体包括连接Verilog HDL 仿真器和其他仿真器或者CAD系统,以及对调试任务、延时计算器和注释客户化。

给予Verilog HDL语言影响最为深刻的语言是HILO-2,它是由英国的Brunel大学开发的一门语言。HILO-2语言是该大学为英国国防部开发测试激励系统而产生的。HILO-2成功结合了门级和寄存器传输级抽象,并且支持仿真验证、时序分析、错误仿真和测试激励。

Verilog HDL语言最初是在1983年由Gateway Design Automationg公司为其模拟器产品开发的硬件建模语言,当时它只是一种专用语言。由于 GDA 公司的模拟器、仿真器产品的广泛使用,Verilog HDL 作为一种易于使用且实用的硬件描述语言逐渐为众多设计者所接受。1989年,GDA公司被Cadence公司并购。1990年,Cadence公司正式发布Verilog HDL语言,并成立了Open Verilog International(OVI)这一促进Verilog发展的国际性组织。1992年,OVI开始致力于推广Verilog OVI标准成为IEEE标准,并于1995年使Verilog HDL语言成为IEEE标准,称为IEEE Std1364-1995。在标准化完成之后,1364工作组开始从世界范围的1364用户那里寻找反馈信息以增强标准并做相应修订。经过5年的努力,即在2001年,一个更加完善的Verilog HDL标准,即IEEE Std1364-2001终于诞生了。

IEEE Std1364-2001成就包括:

● 巩固了IEEE Std1364-1995标准;

● Verilog生成语句;

● 多维阵列;

● 增强Verilog文件I/O;

● 标准化Verilog配置;

● 强化时序表达;

● 强化VPI程序。

Verilog HDL 语言仍然是一门不断发展的语言,有关其最新的发展动态,读者可以到Internet上浏览。以下是一些相关的网址简介:

http://comp.lang.verilog,这是一个关于Verilog的新闻组网站。这个新闻组不时地有一些关于建模或者Verilog工具的提示。

http://comp.cad.cadence,这也是一个新闻组网站,它有关于 Verilog-XL 和来自Cadence Design Systems,Inc.公司的一些工具的新闻发布。

http://comp.cad.synthesis,这是一个新闻组网站,有关于VHDL和Verilog综合工具的新闻发布。

此外,大多数的新闻组网站都会发布一个关于 Verilog 的 FAQ(Frequently Asked Questions),这个文档会经常更新,而且列出了当前关于Verilog的可利用的工具和出版物。

2.VHDL简介

VHDL 最初只是一个文档格式,用于进行项目之间的交流,它源于 1981 年美国国防部的VHSIC计划。1987年,由IEEE(Institute of Electrical and Electronics Engineers)将VHDL制定为标准。参考手册为IEEE VHDL语言参考手册标准草案1076/B版,于1987年被批准,称为IEEE 1076-1987。应当注意,起初VHDL只是作为系统规范的一个标准,而不是为设计而制定的。第二个版本是在1993年制定的,称为VHDL-93,增加了一些新的命令和属性。

虽然有“VHDL是一个4亿美元的错误”这样的说法,但VHDL毕竟是1995年以前唯一制定为标准的硬件描述语言,这是它不争的事实和优势;但同时它确实比较麻烦,而且其综合库至今也没有标准化,不能很好地描述门级(Gate Level)和晶体管开关级(Transistor Level)电路,也不具有模拟设计的描述能力,而且语言本身也不支持时序信息。因此,尽管得到了多方的支持,VHDL在硬件描述语言领域仍然没能占据主要地位。目前的看法是,对于特大型的系统级数字电路设计,VHDL是较为合适的。

实质上,在底层的VHDL设计环境是由Verilog HDL描述的器件库支持的。因此,它们之间的互操作性十分重要。目前,Verilog和VDHL的两个国际组织OVI、VI正在筹划这一工作,准备成立专门的工作组来协调VHDL和Verilog HDL语言的互操作性。OVI也支持不需要翻译、由VHDL到Verilog的自由表达。

Verilog HDL、VHDL和C这三种语言都属于高级程序语言,易于为人们所阅读和理解。前两种语言是目前最常用的硬件描述语言,同时也都是IEEE标准化的HDL语言,它们被广泛应用于数字电路系统逻辑设计领域,最终目标都是要实现硬件电路。而C语言可以说是目前世界上被最广泛采用的软件语言,它被广泛应用在各个不同的领域,已经发展出足够可靠的编译环境,而且自身语法完备。下面具体介绍这三种语言的区别与联系。

3.Verilog HDL与C语言比较

Verilog HDL源于C,两者的语法极为相似,但是Verilog HDL毕竟是一种硬件描述语言,要受到具体硬件电路的诸多限制,它们的区别如下。

(1)在Verilog HDL中不能使用C语言中很抽象的表示语法,如迭代表示法、指针(C语言最具特点的语法)、不确定的循环及动态声明等。

(2)C语言的理念是一行一行执行下去,顺序的语法;而Verilog HDL描述的是硬件,可以在同一时间内有很多硬件电路一起并行动作,这两者之间有冲突,糟糕的是,Verilog仿真器也是顺序执行的软件,在时序关系的处理上,会有思考上的死角。

(3)C语言的输入/输出函数丰富,而Verilog HDL能用的输入/输出函数很少,在程序修改过程中会遇到输入/输出的困难。

(4)C语言无时间延迟指定。

(5)C语言中函数的调用是唯一的,每一个都是相同的,可以无限制调用。而Verilog HDL对模块的每一次调用都必须赋予一个不同的别名,虽然调用的是同一模块,但不同的别名代表不同的模块,即生成了新的硬件电路模块。因此,Verilog HDL 中模块的调用次数受硬件电路资源的限制,不能无限制调用。这一点与C语言有较大区别。

(6)与C语言相比,Verilog HDL描述语法较死,限制很多,能用的判断语句有限。

(7)与C语言相比,Verilog HDL仿真速度慢,查错工具差,错误信息不完整。

(8)Verilog HDL 提供程序界面的仿真工具软件,通常都是价格昂贵,而且可靠性不明确。

(9)Verilog HDL中的延时语句只能用于仿真,不能被综合工具所综合。

(10)表2-1是常用C语言与Verilog HDL中相对应的关键字及控制描述。

表2-1 C语言与Verilog HDL关键字及控制描述对照表

(11)表2-2是C语言与Verilog HDL对应的运算符。

表2-2 C语言与Verilog HDL运算符对照表

4.Verilog HDL与VHDL比较

尽管Verilog HDL与VHDL两种语言都在努力争取能描述所有的硬件层次,但是任何事情都不是完美的,它们描述各层的能力各有千秋。Verilog HDL 善于描述更低层设计,包括结构级(门级和晶体管开关级)和物理级(器件、平面规划);而VHDL善于描述一些高层的设计,包括系统级(算法、数据通路、控制)和行为级(寄存器传输等)。

VHDL和Verilog HDL对比就像C和汇编对比。VHDL的语法描述更规范,高级语言特性较多,适合于大型的硬件逻辑设计。Verilog HDL 则更接近硬件,语法更灵活,适合于激励、仿真、硬件建模。

VHDL具有很多高级语言的特性:数据类型定义、常量定义、函数和过程定义。以上这些定义都可集中写在一个Package中(类似C的.H文件),在每个子模块中只需加一句话引用该Package即可。从总体设计上,这些特性使得模块化设计和TOP-DOWN设计可以比较方便地进行;从局部设计上,可以很方便地写出可读性很强的状态机结构。VHDL是正向逻辑设计的必然趋势,这方面 Verilog HDL 肯定是比不过的。现在已经有专门的VHDL的开发环境,如SUMMIT的Visual VHDL。你可以画状态机、流程图、原理图,由SUMMIT自动生成VHDL。

Verilog语法比VHDL更灵活。从语法核心上Verilog完全是一种事件触发的模式,它的描述能力实际是超过了硬件能实现的范围。比如 Verilog 语法可以很容易地描述一个多时钟的触发器。Verilog 非常适合于编写激励块和器件建模,这些工作很难用 VHDL 来完成。目前所有的半导体厂家的器件库都是用Verilog来描述的。SING OFF的平台也是基于Verilog的。VHDL的新标准里一直试图加入一些这方面的特性,但是很不成功。

目前EDA设计以综合为界,综合前的设计用VHDL,综合出的网表就是Verilog的了。布局布线、时延提取、带时仿真、测试设计都是基于Verilog的。和Verilog相关的步骤虽多,但是现在的设计方法是在VHDL设计阶段做较多约束,从综合开始就是机器自己去运行了。

目前,美国、日本和亚洲大多数的工程师在使用Verilog HDL,而在欧洲VHDL的用户要多于Verilog HDL,这可能与欧洲严谨的作风有关,因为VHDL是一种语法比较严格的语言。

表2-3是对两种HDL语言所作比较的一个简单汇总。

表2-3 VHDL与Verilog HDL比较

2.2 Verilog HDL程序的基本结构

本节将通过几个比较简单的设计实例,力图使读者迅速地从整体上把握Verilog HDL程序的基本结构和设计特点,达到快速入门的目的,为以后各章节的学习提供一个良好的开端。

2.2.1 模块

1.模块的概念和结构

和其他高级语言一样,Verilog HDL 语言也是模块化的,它以模块集合的形式来描述数字电路系统。模块(Module)是Verilog HDL语言的基本单元,它用于描述某个设计的功能或结构及其与其他模块通信的外部端口。一个模块可以是一个元件或者一个更低层设计模块的集合。典型的,元件被组合成一个个的模块,以提供通用的函数性,从而可以在整个系统设计的许多地方被重复调用。一个模块通过它的端口(输入/输出端口)为更高层的设计模块提供必要的函数性,但是又隐藏了其内部的具体实现。这样设计者在修改其模块的内部结构时不会对整个设计的其余部分造成影响。

模块的基本语法如下:

    Module<模块名>(<端口列表>)
        端口说明(input,output,inout)
        参数定义
        数据类型定义
        连续赋值语句(assign)
        过程块(initial和always)
            -行为描述语句
        低层模块实例
        任务和函数
        延时说明块
    endmodule

其中<模块名>是模块唯一的标识符;<端口列表>是由模块各个输入、输出和双向端口组成的一张端口列表,这些端口用来与其他模块进行通信;数据类型定义部分用来指定模块内用到的数据对象为寄存器型、存储器型还是连线型;过程块包括initial过程块和always过程块两种,行为描述语句只能出现在这两种过程块内;延时说明块用来对模块各个输入和输出端口间的路径延时进行说明。

下面以一个2选1多路选择器为例来说明模块。图2-1是该多路选择器的电路示意图。

图2-1 mux2_1模块电路示意图

用Verilog HDL语言实现上述电路的模块如下:

【例2-1】一个2选1多路选择器实例。

    1   module mux2_1(out,a,b,sel);     //端口定义
    2       output out;                 //输入/输出列表
    3        input a,b,sel;
    4
    5       not i5(sel_n,sel);          //结构描述
    6        and i6(sel_a,a,sel);
    7        and i7(sel_b,sel_n,b);
    8       or i8(out,sel_a,sel_b);
    9   endmodule

在Verilog HDL语言编辑、编译环境下,上例中的行号应当去掉,在此只是为了便于说明。下面将逐行解释上例每一条语句的含义。

第1行:声明模块名及其端口列表。

第2行:指定端口out的方向为输出(output),output是用于声明端口方向的一个Verilog关键字。

第3行:指定端口a、b、sel的方向为输入(input),同样input也是用于声明端口方向的一个Verilog关键字。

第5行:生成一个Verilog内建基本门级元件not的实例(也叫做模块的调用,在下一节中将做介绍,类似于C语言中的函数调用),该实例名为i5。第一个端口sel_n是输出端口,信号sel连接到该not元件的输入端口。

第6行和第7行:生成Verilog内建基本门级元件and的两个实例,实例名分别是i6和i7。

第8行:生成Verilog内建基本门级元件or的实例i8。

第9行:用关键字endmodule示意模块结束。

2.模块的描述方式

模块的描述方式又称为建模方式。Verilog既是一门行为化又是一门结构化的HDL语言。根据设计的需要,每个模块的内部可以分为4种抽象级别来进行描述。模块在外部环境中的表现都是同等的,而与其内部具体描述的抽象级别无关。因此模块的内部具体描述相对于外部环境来说是隐藏的。改变一个模块内部描述的抽象级别,可以不用对其外部环境做任何的改动。模块大致可以按以下4种抽象级别来进行描述。

(1)行为级或算法级的描述方式(行为级建模)

这是Verilog HDL最高抽象级别的描述方式。一个模块可以按照要求的设计算法来实现,而不用关心具体硬件实现的细节。在这种抽象级别描述方式上的设计非常类似C编程。行为描述是通过行为语句来实现的,行为功能可使用下述过程语句结构描述。

● initial语句:此语句只执行一次。

● always语句:此语句循环执行。

只有寄存器型数据能够在这两种语句中被赋值,寄存器型数据在被赋新值前保持原有值不变。所有的initial语句和always语句在零时刻并发执行。

下面以一个4bit的二进制行波计数器(带进位)为例来说明行为级的描述方式。图2-2是该计数器的电路示意图。

图2-2 4bit二进制行波计数器电路示意图

该计数器由4个T触发器级联而成,每一级T触发器的输出作为下一级T触发器的时钟输入。下例是用行为描述方式来实现计数器的一个例子。

【例2-2】用行为描述方式来实现行波计数器。

    module cnt_4bit (q,clear,clock);
        output  [3:0]  q;
        input clear,clock;

        reg  [3:0]  q;

        always @(posedge clear or negedge clock)
        begin
            if (clear)
                  q=4’d0;
            else
                q=(q+1) % 16;
        end
    endmodule

模块cnt_4bit的输出端口q是一个4bit的位矢量,代表4根输出端口线q[3]、q[2]、q[1]、q[0]。由于该输出端口要在 always 语句中被赋值,所以它被定义为 reg 型(寄存器型)数据。clear和clock是两个输入端口。always语句中包含一个或事件控制(紧跟在字符@后面的表达式),以及相关联的顺序过程(begin-end对)。此或事件控制的作用是当输入端口clear、clock上发生事件,即clear、clock中某一个值发生变化时,就执行下面的顺序过程。在顺序过程中的语句顺序执行,并在顺序过程执行结束后被挂起,程序指针返回到或事件控制语句,always语句再次等待clear、clock的值发生变化。这种机制与C++中的消息循环类似。

本例用非常抽象的方式描述了模块要实现的功能,而没有直接指明或者涉及实现该功能的硬件结构,即具体硬件电路的连接结构、逻辑门的组成结构、元件等。行为描述方式是Verilog这种高级硬件描述语言的一大特色,可以说没有行为级描述就没有Verilog,请读者在以后的学习中仔细体会。

(2)数据流描述方式(数据流级建模)

数据流描述方式,也称为RTL(寄存器传输级)描述方式。在这种描述方式下,设计者需要知道数据是如何在寄存器之间传输的,以及将被如何处理。数据流描述方式类似于布尔方程,它能够比较直观地表达底层逻辑行为。在 Verilog 中数据流描述方式主要用来描述组合逻辑,具体由连续赋值语句“assign”来实现。下面仍然以图 2-2 的 4bit 二进制行波计数器为例,根据自顶向下(Top-Down)的设计方法,用数据流描述方式来实现它。

【例2-3】用数据流描述方式来实现行波计数器。

第一步:设计顶层模块cnt_4bit_1,代码包含了4个T触发器模块T_ff实例(模块调用)。

    module cnt_4bit_1 (q,clear,clock);
        output  [3:0]  q;
        input clear,clock;

        T_ff  tff0(q[0], clear, clock);
        T_ff  tff1(q[1], clear, q[0]);
        T_ff  tff2(q[2], clear, q[1]);
        T_ff  tff3(q[3], clear, q[2]);
    endmodule

第二步:设计T_ff触发器模块,该模块内又包含了其下一层的D触发器模块edge_dff实例。

    module T_ff(q,clear,clock);
        output q;
        input clear,clock;

        edge_dff ff1(q, ,~q,clear,clock);
    endmodule

注意:在代码中用数据流操作符“~”对信号 q 取反,而不是用 Verilog 内建基本门级元件not,但是两者最终实现的功能完全相同。语句edge_dff ff1(q, ,~q,clear,clock)中的空格表示默认调用,但是“,”分隔符不能少。

第三步:运用数据流描述语句设计最底层模块负边沿触发D触发器edge_dff。图2-3是该D触发器的基本门级结构。

    module edge_dff(q,qbar,d, clear,clock);
        output q,qbar;
        input d,clear,clock;
        wire s,sbar,r,rbar,cbar;

    assign cbar=~clear;
    //输入锁存器;锁存器是电平敏感的。而一个边沿敏感的触发器需要使用3个RS锁存器来实现
    assign sbar=~(rbar&s),
        s=~(sbar&cbar&~clock),
            r=~(rbar&~clock&s),
            rbar=~(r&cbar&d);
    //输出锁存
    assign q=~(s&qbar),
            qbar=~(q&r&cbar);
    endmodule

图2-3 负边沿触发D触发器

在这一步的模块edge_dff中,s、sbar、r、rbar、cbar被定义为wire(连线)型数据, wire也是Verilog的关键字。在其后的代码中使用assign语句对模块的输入、输出端口和连线型数据之间的数据流传输关系进行了描述。这就是Verilog中的数据流描述方式。

(3)门级描述方式(门级建模)

在这种描述方式下,模块是按照逻辑门和它们之间的互连来实现的。在这种抽象级别下的设计与按照门级逻辑图来描述一个设计类似。具体地讲,门级描述方式就是指调用Verilog内建的基本门级元件来对硬件电路进行结构设计。这些基本的门级元件是一类特殊的模块,共有14种,分为4类,它们是由Verilog HDL语言自身提供的,不需要用户事先定义就可以在设计中被直接调用。

下面仍然以例2-3中的行波计数器为例,其第三步的模块edge_dff的设计可以采用门级描述方式来实现。根据图2-3的逻辑图,以门级描述方式来实现它,具体如下例所示。

【例2-4】用门级描述方式来实现负边沿触发D触发器。

    module edge_dff_1 (q,qbar,d, clear,clock);
        output q,qbar;
        input d,clear,clock;
        wire cbar,clkbar,sbar,s,r,rbar;

        not   N1(cbar,clear),
              N2(clkbar,clock);
        nand  NA1(sbar,rbar,s),
              NA2(s,sbar,cbar,clkbar),
              NA3(r,s,clkbar,rbar),
              NA4(rbar,r,cbar,d),
              NA5(q,s,qbar),
              NA6(qbar,q,cbar,r);
    endmodule

由本例与例2-3第三步对D触发器的两种不同风格的描述来看,数据流风格的描述就像是在列逻辑方程式,而门级描述就像是在画电路原理图,它们都非常直观地表达了D触发器的底层逻辑行为,它们最终实现的D触发器模块的逻辑功能完全相同。

(4)开关级描述方式(开关级建模)

开关级描述方式也称为晶体管级描述方式,是 Verilog 最低抽象级别的描述方式。在这种描述方式下,模块是按照开关级元件和存储节点以及它们之间的互连来实现的。具体来说,是指调用 Verilog 内建的基本开关级元件来对硬件电路进行结构设计。与门级元件类似,这些基本的开关级元件也是一类特殊的模块,共有12种,它们也是由Verilog HDL语言自身提供的,不需要用户事先定义就可以在设计中被直接调用。由于有了这一级别的描述方式,使得用户在MOS(Metal-Oxide Semiconductor,金属氧化物半导体)晶体管级进行设计成为可能。这一点也正是Verilog HDL语言的一大优点,而要在其他的HDL语言比如VHDL中进行晶体管级设计是非常困难的。然而,随着电路规模的日渐庞大(上百万门晶体管)和 EDA 工具的高速发展,在极少数情况下,设计者才可能选择在晶体管级进行电路设计。下面以一个简单的例子对开关级描述方式进行介绍。

尽管Verilog有内建的基本门级元件,比如nor门,但是现在我们可以使用CMOS(互补金属氧化物半导体)开关元件设计我们自己的 nor 门。nor 门的门级和开关级逻辑示意图如图2-4所示。

图2-4 nor门的门级和开关级逻辑示意图

【例2-5】用开关级元件描述方式设计nor门。

    module my_nor(out,a,b);
        output out;
        input a,b;
        wire c;
        supply1 pwr;
        supply0 gnd;

        pmos(c,pwr,b);
        pmos(out,c,a);
        nmos(out,gnd,a);
        nmos(out,gnd,b);
    endmodule

程序中的 supply1、supply0 是 Verilog 的关键字,分别定义电路的电源和地。pmos、nmos都是Verilog的基本开关级元件。

(5)描述方式总结

以上介绍了 Verilog 的4 种抽象级别的描述方式。事实上,设计者可以在一个设计中混合使用这4种描述方式。如果一个设计包含4个模块,那么每一个模块可以分别用不同抽象级别的描述方式来实现。从设计的成熟性上考虑,大多数模块都可以转化为门级描述来实现。

通常,描述方式越抽象,设计的灵活性和技术独立性也越强。而越靠近开关级的描述方式,对技术的依赖性也越强,设计本身也就越不灵活,一个小改动就可能导致整个设计的大改变。这一点与用C语言和汇编语言编程相似:用更高级的语言(比如C)进行编程更容易,而且也更容易移植到不同的机器上;相反,如果用汇编语言编程,那么程序是针对某种特定的机器,要移植到其他机器上则很困难。

对于数据流级描述方式(RTL 级描述方式),从本质上说,它是对线网型变量的行为进行了描述,因此本书将这种数据流描述方式归类于行为级描述方式。对于门级和开关级描述方式,由于它们都是根据电路的硬件结构特点进行设计的,因此本书将这两种描述方式归类于结构级描述方式。至此,Verilog语言分为3大类描述方式,即行为级描述方式、结构级描述方式以及它们之间的混合描述方式。本书后续章节将结合具体的 Verilog 程序语句对行为级描述方式和结构级描述方式做详细介绍。

3.设计的仿真与测试

一个设计一旦完成就应当对它进行测试。通过编写激励块,输入激励信号,然后检测结果,可以检测一个设计功能的正确性。将激励块和设计块分离开来是设计者应该养成的一个好习惯。通常测试块也被称为测试凳(Test Bench),应用不同的测试凳可以对一个设计块进行全方位的测试。激励信号的应用方式大致被分为两种。

第一种,在激励块内调用设计块,并且直接驱动设计块的信号。在图2-5中,激励块成为顶层模块,对输入信号clk和reset进行操作,检测并显示输出信号q。

图2-5 激励块调用设计块示意图

第二种,在顶层的假模块内同时调用激励块和设计块,激励块和设计块仅通过接口相互作用,如图2-6所示。激励块驱动信号d_clk和d_reset,这两个信号与设计块的两个端口clk和reset分别相连。它也检测并显示输出信号c_q,该信号与设计块的输出端口q相连。顶层假模块的功能仅仅是为了调用设计块和激励块。

图2-6 顶层假模块调用设计块和激励块示意图

下面我们将对例2-2的行波计数器进行仿真测试。将例2-2设计模块程序重写如下。

    module cnt_4bit (q,clear,clock);
        output  [3:0]  q;
        input clear,clock;

        reg  [3:0]  q;

        always @(posedge clear or negedge clock)
        begin
            if (clear)
                q=4’d0;
            else
                q=(q+1) % 16;
        end
    endmodule

现在我们必须写出激励块以检测行波计数器功能是否正确。在此,我们必须控制信号clk和reset以检测行波计数器的计数功能和异步复位机制是否都正确。我们将使用如图2-7所示的波形来检测设计块。图中显示了输入信号clk、reset以及4bit的输出信号q。时钟信号clk的时钟周期为10个时间单位;复位信号reset从0到15保持为高,然后变低,直到195再次变高,至205后变低;输出信号q的范围是从0到15。

图2-7 激励和输出波形示意图

下面我们就准备写出激励块,生成如图2-7所示的波形。我们将使用如图2-5所示的激励方式。在此不要过多考虑Verilog的语法,将精力集中在激励块是如何调用设计块上。

【例2-6】编写激励块。

    module stimulus1;
    reg clk;
    reg reset;
    wire [3:0] q;
    cnt_4bit  r1(q,reset,clk);        //调用设计块cnt_4bit生成实例r1
    //控制信号clk以驱动设计块,时钟周期设为10个时间单位
    initial
        clk=1’b0;                   //设置clk到0
    always
        #5 clk=~clk;                //clk每隔5个时间单位反转一次
    //控制复位信号reset以驱动设计块,0~15为高,15~195为低,195~205为高,然后变低
    initial
    begin
        reset=1’b1;
        #15 reset=1’b0;
        #180 reset=1’b1;
        #10 reset=1’b0;
        #20 $finish;
    end
    //监视输出
    initial
        $monitor($time,"output q=%d",q);
    endmodule

下面我们就可以运行仿真器,以检测设计块功能的正确性。仿真的输出结果如图 2-8所示。

图2-8 仿真输出结果示意图

关于模块的仿真与测试,本书后面的章节将会对其进行更加详细的介绍。

2.2.2 模块调用

在上一节对模块概念和结构的介绍举例中已经多次运用了模块的调用。所谓模块的调用,是指从模块模板生成实际的电路结构对象的操作,这样的电路结构对象被称为模块实例,模块调用也被称为实例化。每一个实例都有它自己的名字、变量、参数和I/O接口。一个 Verilog 模块可以被任意多个其他模块调用。模块调用和函数调用非常相似,但是在本质上又有很大差别:一个模块代表拥有特定功能的一个电路块,每当一个模块在其他模块内被调用一次,被调用模块所表示的电路结构就会在调用模块代表的电路内部被复制一次(即生成被调用模块的一个实例);但是模块调用不能像函数调用一样具有“退出调用”的操作,因为硬件电路结构不会随着时间而发生变化,被复制的电路块将一直存在下去。

需要注意的是,在 Verilog 中,模块不能被嵌套定义,即在一个模块的定义内不能包含另一个模块的定义;但是却可以包含其他模块的拷贝,即调用其他模块的实例。模块的定义和模块的实例是两个不同的概念,在一个设计当中,只有通过模块调用(实例化)才能使用一个模块。

模块调用语句的基本格式如下:

<模块名> <参数值列表> <实例名> (<端口连接表>);

在上面的格式中:

● <模块名>是指模块定义时为被调用模块指定的模块名。

● <参数值列表>是可选项,它是由一些参数值组成的一张有序列表,这些参数值将被传递给被调用模块实例内的各个参数。

● <实例名>是为模块调用后所生成的模块实例所取的一个名字,它是模块实例的唯一标志。在某一模块内可以对同一模块多次调用,但是每次调用生成的模块实例名不能重复。实例名和模块名的区别是,模块名表示不同的模块,即用来区分电路单元的不同种类;而实例名则表示不同的模块实例,用来区分电路系统中的不同硬件电路单元。

● <端口连接表>是由外部信号端子组成的一张有序列表,这些外部信号端子代表着与模块实例各端口相连的外部信号。所以<端口连接表>指明了模块实例端口与外部电路的连接情况。

在例2-1中,语句:

    not i5(sel_n,sel);
    and i6(sel_a,a,sel);
    and i7(sel_b,sel_n,b);
    or i8(out,sel_a,sel_b);

以及在例2-3中,语句:

    T_ff  tff0(q[0],clear,clock);
    T_ff  tff1(q[1], clear, q[0]);
    T_ff  tff2(q[2], clear, q[1]);
    T_ff  tff3(q[3], clear, q[2]);

都是模块调用的一些例子。关于模块调用的具体细节,将在本书后面的第4章做详细介绍。

与其他高级语言一样,Verilog HDL语言也具有自身固有的语法格式。下面将对Verilog的语法基础知识进行详细说明。希望通过对本章的学习,读者能够对Verilog HDL有初步的了解。

2.3 程序格式

Verilog HDL程序是由模块构成的,每个模块的内容都包含在module和endmodule两个语句之间。每个模块都要进行端口定义、端口声明。Verilog HDL程序的书写与C语言类似,一行可以写多条语句,也可以一条语句分成多行书写,每条语句以分号结束, endmodule 语句后可不加分号。每个文件内可以包含多个模块定义,但只能有一个顶层模块。每个Verilog HDL程序源文件都以.v作为文件扩展名。

2.4 注释与间隔符

1.注释

Verilog HDL有两种方式引入注释。其一是单行注释,起始于双斜杠“//”,结束于新的一行的开始。其二是多行注释,或者叫做块注释,以符号单斜杠星号“/*”作为开始标志,以星号单斜杠“*/”作为结束标志。块注释不能嵌套。单行注释符号“//”在块注释语句内并无特定含义。

2.间隔符

间隔符包括空格(\b)、tab(\t)、换行符(\n)及换页符。若间隔符并非出现在字符串中,则该间隔符应当被忽略。间隔符除起到分隔的作用外,在必要的地方插入相应的空格或换行符,可以使文本错落有致,方便用户阅读与修改。

2.5 数值

Verilog HDL中的数值可以取下面的4类值。

● 0:逻辑0或假状态。

● 1:逻辑1或真状态。

● x(X):未知状态,x对大小写不敏感。

● z(Z):高阻状态,z对大小写不敏感。

Verilog HDL中有两类数值常量:整型常量和实型常量。

1.整型常量

整型常量有两种表示方法:

(1)简单的十进制格式

这种格式是由0~9的数字串组成的十进制数,可以在数值前面加上符号“+”或“-”来表示数的正或负。

(2)指定位宽的基数格式

这种格式由三部分组成:<size> <’base_format> <number>

<size>:指定数的二进制位宽的参数。该参数应该是一个非零的无符号十进制常量。例如,两位十六进制数的位宽为8,因为一位十六进制数要求位宽为4。

<’base_format>:单引号(’)是指定位宽格式表示法的固有字符,不能省略。base_format是用于指定数的基数格式(进制格式)的一个字母,对大小写不敏感。在 base_format 之前(即单引号(’)之后)可以加上字母s(S),表示该数为有符号数。合法的基数格式字母有d(D)、h(H)、o(O)、b(B),分别对应于十进制、十六进制、八进制、二进制。单引号(’)和base_format之间不能有空格。

<number>:是一个无符号的数,由相应基数格式的数字串组成。十六进制数字 a~f对大小写也是不敏感的。

当位宽小于数值的实际位数时,相应的高位部分被省略;当位宽大于数值的实际位数,且数值的最高位是0或1时,相应的高位部分补0或1;当位宽大于数值的实际位数,且数值的最高位是x或z时,相应的高位部分补x或z;如果未指定位宽(即<size>参数默认),那么默认的位宽至少是32位。

二进制的一个x或z表示一位处于x或z,八进制的一个x或z表示3位二进制都处于x或z,十六进制的一个x或z表示4位二进制位都处于x或z。此外,Verilog HDL中的z可以用问号(?)来代替,在十六进制、八进制、二进制中的一位“?”分别表示4位z、3位z、1位z。问号(?)也用在用户自定义基元的状态表中。

简单的十进制格式表示的数被作为有符号整数处理;而在基数格式表示的数中,如果在base_format之前有字母s(S),那么该数被作为有符号数处理,否则作为无符号数处理。在基数格式表示法中,可以在 size 之前加上“+”或“-”表示数的正或负,但是不能在base_format和number之间加“+”或“-”,因为这违背了Verilog HDL的语法规则。值得一提的是,在Verilog HDL中负数都按二进制补码形式存储和处理。

下划线符号(_)除了不能放在数值的首位以外,可以放在整型和实型内任何地方。它们对数值没有任何影响,在编译时会被忽略,只是为了将长的数值分段,以提高可读性。

下面举例说明整型常量的表示。

【例2-7】未指定位宽的整型常量表示。

    659 // 简单十进制格式
    'h 837FF // 默认位宽十六进制数
    'o7460 //默认位宽八进制数
    4af // 非法的整数表示(十六进制要求格式 'h)

【例2-8】指定位宽的整型常量表示。

    4'b1001 //一个4位二进制数
    5 'D 3 // 一个5位十进制数
    3'b01x// 一个3位二进制数,最低位状态未知
    12'hx //一个12位十六进制数,状态未知
    16'hz //一个16位十六进制数,状态为高阻

【例2-9】有符号整型常量的表示。

    8 'd -6 // 非法格式,base_format和number之间不能有“+”或“-”
    -8 'd 6 //定义正整数6的补码,占8位,相当于-(8'd 6)
    4 'shf //表示4位二进制数'1111', 作为1的补码数或者'-1',相当于-4'h 1
    -4 'sd15 // 相当于 -(-4'd 1)或者'0001'

【例2-10】自动左填充。

    reg [11:0] a, b, c, d;
    initial begin
        a='h x; // 生成 xxx
        b='h 3x; //生成03x
        c='h z3; //生成zz3
        d='h 0z3; //生成0z3
    end
    reg [84:0] e, f, g;
        e='h5; //生成{82{1'b0},3'b101}
        f='hx; //生成{85{1'hx}}
        g='hz; //生成{85{1'hz}}

【例2-11】使用下划线(_)。

    27_195_000
    16'b0011_0101_0001_1111
    32 'h 12ab_f001

2.实型常量

Verilog HDL中的实型常量可以用十进制和科学计数法两种格式来表示。如果采用十进制格式,则小数点的两边至少都有一个数字,否则为非法表示。下面举例说明实型常量的表示。

【例2-12】实型常量的表示。

    1.2                 //十进制
    0.1                 //十进制
    2394.26331          //十进制
    1.2E12              //科学计数法,指数符号E对大小写不敏感
    1.30e-2             //科学计数法
    0.1e-0              //科学计数法
    23E10               //科学计数法
    29E-2               //科学计数法
    236.123_763_e-12    //科学计数法,下划线被忽略
    .12                 //非法表示
    9.                  //非法表示
    4.E3                //非法表示
    .2e-7               //非法表示

3.两种常量的转换

实型常量可以通过对小数四舍五入,转换为最靠近的整型常量,而不是直接将小数舍弃,从而得到整数。当一个实型常量被赋给一个整型变量时,一种隐式的转换就发生了。例如,实数35.7和35.5转换为整数都得到36,而实数35.2转换为整数得到35。实数-1.5转换为整数得到-2;实数1.5转换为整数得到2。

2.6 字符串

字符串是用双引号(“”)括起来的字符序列,它必须包含在一行内,不能分成多行书写。如果字符串被用做 Verilog 表达式或赋值语句的操作数,则字符串被看做无符号整数序列。每个整数代表一个8位的ASCII字符值,即每个整数对应字符串中的一个字符。

1.字符串变量声明

字符串变量是寄存器类型(参见后面章节)的变量,该字符串变量的位数要大于等于字符串的最大长度。

【例2-13】字符串变量的声明。

存储一个12字符的字符串"Hello world!"需要一个8*12=96位的寄存器变量。

    reg [8*12:1] stringvar;
    initial
    begin
        stringvar="Hello world!";
    end

2.字符串操作

可以使用Verilog HDL的操作符对字符串进行操作,被操作的值是一个8位ASCII值序列。在操作过程中,如果声明的字符串变量位数大于字符串实际长度,则在赋值操作后,字符串变量的左端(即高位)补0。这一点与非字符串值的赋值操作是一致的。如果声明的字符串变量位数小于字符串实际长度,那么字符串的左端被截去,这些高位字符就丢失了。

【例2-14】字符串操作。

    module string_test;
        reg [8*14:1] stringvar;
    initial
    begin
        stringvar="Hello world";
        $display("%s is stored as %h", stringvar,stringvar);
        stringvar={stringvar,"!!!"};
        $display("%s is stored as %h", stringvar,stringvar);
    end
    endmodule

输出结果为:

    Hello world is stored as 00000048656c6c6f20776f726c64
    Hello world!!! is stored as 48656c6c6f20776f726c64212121

3.特殊字符

在某些字符之前加上一个引导性的字符(转义字符),这样的字符只能用于字符串中。表2-4列出了这些特殊字符的表示和意义。

表2-4 特殊字符的表示和意义

2.7 标识符

标识符是赋予一个对象唯一的名字,通过标识符该对象就可以被引用。标识符分为简单标识符、转义标识符、生成标识符、关键字。

1.简单标识符

简单标识符是由字母、数字、货币符号($)、下划线(_)构成的任意序列。简单标识符的第一个符号不能是数字或$符号。简单标识符对大小写敏感。下例是一个简单标识符定义的例子。

【例2-15】简单标识符定义。

    shiftreg_a
    busa_index
    error_condition
    merge_ab
    _bus3
    n$657

注意:具体实现一个标识符的时候,应当设定一个标识符的最大长度限制,该最大长度不能小于 1024 个字符。如果一个标识符的长度超过了设定的实现长度限制,则会有错误报告产生。

2.转义标识符

转义标识符以反斜杠(\)作为起始,以空白(空格、Tab、换行)作为结束。在转义标识符里面可以包含任意可印刷ASCII字符(十进制值为33~126,对应十六进制值为21~7E)。转义标识符的起始标志反斜杠(\)和结束的空白都不作为标识符的一部分来处理,因此转义标识符 \cpu3被作为非转义标识符cpu3处理。以下是一个转义标识符定义的例子。

【例2-16】转义标识符定义。

    \busa+index
    \-clock
    \***error-condition***
    \net1/\net2
    \{a,b}
    \a*(b+c)

3.生成标识符

生成标识符是由生成循环语句(generate loop)产生的,是一种特殊的标识符,它可以被用做层次名。一个生成标识符是由生成块(generate block)语句产生的,并以数字字符串作为尾缀。生成标识符在层次名里面被用做节点名。

4.关键字

关键字是Verilog HDL预定义的非转义标识符,用于定义Verilog HDL语言的结构。转义标志符后面跟的关键字不会被当做关键字处理。所有的关键字都用小写字母定义。

2.8 系统任务和函数

为了便于设计者对仿真过程进行控制,以及对仿真结果进行分析,Verilog HDL 提供了大量的系统功能调用,大致可以分为两类:一种是任务型的功能调用,称为系统任务;另一种是函数型的功能调用,称为系统函数。Verilog HDL的系统任务和系统函数是以字符$开头的标识符,它们的区别主要有两点:系统任务可以没有返回值或者有多个返回值,而系统函数只有一个返回值;系统任务可以带有延迟,而系统函数不允许延迟,在0时刻执行。

系统任务与系统函数已内置于Verilog HDL中,用户可以随意调用。而且用户可以根据自己的需要,基于Verilog仿真系统提供的PLI(Programming Language Interface)编程接口,编制特殊的系统任务和系统函数。根据系统任务和系统函数实现的功能不同,可将其分成以下几类:

● 标准输出任务;

● 文件管理任务;

● 仿真控制任务;

● 时间函数;

● 其他。

1.标准输出任务

在Verilog HDL中有两种主要的标准输出任务:$display和$write。这两种标准输出任务的格式相同,区别在于$display 任务在将特定信息输出到标准输出设备时,具有自动换行的功能,而$write不带有行结束符。下面以$display任务为例进行说明。

$display可以用来输出字符串、表达式及变量值,其语法格式与C语言中的printf函数相同,可表示如下:

    $display(<format_specifiers>,signal,signal,… …);

其中<format_specifiers>用来指定输出格式。表2-5给出了各种不同的输出格式。

表2-5 输出格式符说明

【例2-17】$display任务。

    $display("display a message");
    输出为:display a message
    $display($time); //目前的仿真时间
    输出为:420
    $display("The number is %b",number);
    输出为:The number is 0101

在Verilog HDL中除了$display和$write这两种主要的标准输出任务外,还有以下几种标准输出任务。

● $displayb和$writeb(输出二进制数)。

● $displayo和$writeo(输出八进制数)。

● $displayh和$writeh(输出十六进制数)。

2.文件管理任务

(1)打开文件

在Verilog HDL中利用关键字$fopen来打开文件,其语法格式如下:

    <file_handle>=$fopen("<file_name>");

<file_name>指定被打开的文件名及其路径,如果路径与文件名正确,则返回一个 32位的句柄描述符<file_handle>,且其中只有一位为高电平,否则返回出错信息。因为标准输出具有自己的最低位设置,所以当第一次使用$fopen 时,返回的 32 位句柄描述符中将次低位设置为高电平。每一次调用$fopen都会返回一个新的句柄,且高电平依次左移。下面举例说明。

【例2-18】打开文件。

    integer handleA,handleB;//定义两个32位整数
    Initial
    begin
        handleA=$fopen("myfile.out");
      // handleA=0000_0000__0000_0000_0000_0000_0000_0010
    handleB=$fopen("anotherfile.out");
      // handleB=0000_0000__0000_0000_0000_0000_0000_0100
    end

(2)输出到文件

Verilog HDL中用来将信息输出到文件的系统任务有$fdisplay、$fwrite、$fmonitor。它们具有如下相同的语法格式:

    <task_name>(<file_handles>,<format_specifiers>);

其中<task_name>是上述三种系统任务中的一种。<file_handles>是文件句柄描述符,与打开文件所不同的是,可以对句柄进行多位设置(见下例)。<format_specifiers>用来指定输出格式,格式说明符可参照表2-5。

【例2-19】输出到文件。

    //利用例2-18的句柄
    integer channelsA;
    initial

    begin
        channelsA=handleA|1;
        $fdisplay(channelsA, "hello");
    end

本例中的handleA为例2-18的句柄输出。

(3)关闭文件

系统函数$fclose能够用于关闭文件,其语法格式如下:

    $fclose(<file_handle>);

当使用多个文件时,为了提高速度,可以将一些不再使用的文件关闭。一旦某个文件关闭,则不能再向它写入信息,但其他文件可以使用该文件的句柄。

(4)从文件中读出数据到存储器

Verilog HDL中有两个系统任务$readmemb和$readmemh,它们能够把一个数据文件中的数据内容读入到一个指定的存储器中。这两个系统任务的区别在于,前者要求以二进制数据格式存放数据文件,而后者要求以十六进制数据格式存放数据文件。它们具有相同的语法格式:

    <task_name>(<file_name>,<register_array>,<start>,<end>);

其中<task_name>用来指定系统任务,可取上述任务中的一个。<file_name>是读出数据的文件名。<register_array>为要读入数据的存储器。<start>和<end>分别为存储器的起始地址和结束地址。现有一数据文件mem.dat,可以通过下面的例子将其读入到存储器中。

【例2-20】从文件中读出数据到存储器。

    module testmemory;
      reg[7:0] memory[9:0];
      integer index;
    initial
    begin
            $readmemb("mem.dat",memory);
            for(index=0;index<10;index=index+1)
            $display("memory[%d]=%b",index[4:0],memory[index]);
    end
    endmodule

3.仿真控制任务

(1)仿真控制任务

Verilog HDL中有3种仿真监控任务:$monitor、$monitoron、$monitoroff。

$monitor的语法格式为:

    $monitor(<format_specifiers>,signal,signal,……);

可以看出$monitor 与$display 的语法格式相同,因而$monitor 的参数说明可参照$display 的参数说明。该任务可用来连续监控指定的信号参数,如果发现其中的任一信号发生变化,则系统按照调用$monitor时规定的格式,在时间步结束时显示整个信号表。

可以通过系统任务$monitoron打开监控任务,通过系统任务$monitoroff关闭监控任务。

【例2-21】系统任务$monitor。

    module mytest;
        integer a,b;
        initial
        begin
            a=2;
            b=4;
            forever
            begin
                #5 a=a+b;
                #5 b=a+1;
            end
        end
        initial #40 $finish;
        initial
        begin
            $monitor($time,"a=%d,b=%d", a,b ) ;
        end
    endmodule

输出结果为:

    #         0 a=     2, b=     4
    #         5 a=     6, b=     4
    #        10 a=     6, b=     7
    #        15 a=    13, b=     7
    #        20 a=    13, b=    14
    #        25 a=    27, b=    14
    #        30 a=    27, b=    28
    #        35 a=    55, b=    28

(2)仿真结束任务

Verilog HDL中有两个系统任务可以用来结束仿真:$finish和$stop。

$finish 用来终止仿真器的运行,结束仿真过程返回到操作系统。$stop 暂时挂起仿真器,进入Verilog界面,可以通过输入相应命令使仿真继续运行。

【例2-22】仿真结束任务。

    initial
    begin
        clock=1'b0;
        ……            //需要完成的任务
        #200 $stop;     //暂停仿真并进入交互方式
        #500 $finish;   //结束仿真任务
    end

4.时间函数

(1)时间标度函数

Verilog HDL提供了两种时间标度函数:$timeformat和$printtimescale。

$timeformat用于控制%t格式如何显示时间信息,语法格式为:

    $timeformat(<unit>,<precision>,<suffix>,<min_field_width>);

其中<unit>用于指定时间单位,其取值范围为0~-15,各值所代表的时间单位如表2-6所示。

表2-6 <unit>的取值所代表的时间单位

<precision>指定所要显示时间信息的精度,<suffix>是诸如“ms”、“ns”之类的字符,<min_field_width>说明时间信息的最小字符数。

【例2-23】 $timeformat。

    $timeformat(-9,2,"ns",10);
    $display("current simulation time is:%t",$time);

$printtimescale的语法格式为:

    $printtimescale(module_hierarchical_name);

该函数给出指定模块的时间单位与精度。如果没有指定参数,则用于输出包含该任务调用的所有模块的时间单位与精度。

(2)时间显示函数

时间显示函数包括:

● $time,返回64位的整数,指定当前的仿真时间。

● $stime,返回32位的仿真时间。

● $realtime,以实数形式返回当前的仿真时间。

5.其他

(1)随机函数

当进行模块测试时,常常需要提供随机脉冲序列,该功能可以通过系统函数$random来实现,它的语法格式为:

    $random%<number>;

其中<number>为一大于0的整数,用来指定所产生随机数的范围,即-<number>+1到<number>-1。

【例2-24】系统函数$random。

    Reg[7:0] random_data;
    always
        #100 random_data=$random%50;

(2)转换函数

有时需要将整数转换成实数,或将实数转换成整数,或者用向量形式来表示实数等。Verilog HDL提供了许多转换函数可以方便实现上述功能。

● $rtio,通过截断小数部分,将实数转换成整数。

● $itor,将整数转换成实数。

● $realtobits,将实数转换成64位的实数向量表示。

● $bitstoreal,将位模式转换为实数。

2.9 编译指令

同C语言中的编译预处理指令一样,Verilog HDL也提供了大量编译指令。通过这些编译指令,EDA工具开发商使得用他们的工具解释Verilog HDL模型变得相当容易。值得注意的几点是:

● 所有的Verilog编译指令都以符号(')开头,它也叫做重音符号。

● Verilog 编译指令的末尾不需要加分号(;)。

● Verilog 编译指令对所有被编译的文件都有效,直到有另外的编译指令取代它,或者是整个编译结束。

Verilog HDL提供了以下大致9种编译指令。

1.单元模块定义指令

单元模块定义指令包括两条指令:'celldefine和'endcelldefine。

这两条指令用于将模块定义为单元模块,一般是成对出现的,后者用于结束定义。单元模块(cell)应用于某些 PLI 程序中。在一个单一源文件描述中,可以出现多对这样的指令对。'celldefine和'endcelldefine指令对可以出现在源文件描述中的任何地方,但推荐的做法是将这些语句放在模块定义的外面。

注意:指令'resetall(后面将会详述)包含了'endcelldefine指令的功能。

2.默认线网类型声明指令

默认线网类型声明指令'default_nettype 为隐含线网类型的模块指定线网类型。它只能放在模块定义的外面。如果一个源文件描述内只有一条'default_nettype 指令,那么该指令指定的线网类型对其后面的所有源程序都有效。当然Verilog HDL允许在一个源文件描述内有多条'default_nettype 指令,此时每一段源程序的默认线网类型受其前面最近的'default_nettype指令控制。

'default_nettype指令的语法格式为:

    'default_nettype <net_type>

参数<net_type>为线网类型 wire、tri、tri0、wand、triand、wor、trior、ireg、none 中的一种。

如果源程序没有使用'default_nettype指令或者使用了'resetall指令,那么隐含的线网类型默认为wire。如果<net_type>选择none,那么所有的线网类型都需要显示声明,如果没有显示声明则会有错误产生。

3.宏编译指令

宏编译指令包括两条指令:'define和'undef。

一个文本宏替换可以非常方便地代替经常使用的一个文本块。例如,在整篇源程序描述的多个地方如果频繁使用某一数字常量,而要对该数字常量进行修改是比较烦琐的,但是如果使用文本宏代替该常量,那么只需要在一个地方对文本宏进行修改就可以了,非常方便。一个文本宏不会受指令'resetall影响。

(1)'define指令(宏定义指令)

'define指令生成一个文本宏(macro)。该指令既可以放在模块定义内部,也可以放在模块定义之外。如果已经定义了一个文本宏,那么在它的宏名(macro_name)之前加上重音符号('),就可以在源程序中引用该文本宏。编译器编译时将会自动用相应文本块代替字符串'macro_name。Verilog HDL中的所有编译指令都被看做预定义的宏名,要将一个编译指令重新定义为一个宏名是非法的。一个文本宏定义可以带有参数,这样就允许为每一个单独的应用定制文本宏。文本宏定义的语法格式如下:

    'define  < text_macro_name>  <macro_text>

● < text_macro_name>为宏名,其语法格式为:

    text_macro_identifier [< list_of_formal_arguments > ]

● text_macro_identifier为宏标识符,要求是简单标识符,< list_of_formal_arguments >为形参列表。一旦一个宏名被定义,它就可以在源程序的任何地方被使用,没有范围限制。

● <macro_text>为宏文本,可以是与宏名同行的任意指定文本。如果指定的文本超过一行,那么新的一行需要以反斜杠(\)作为起始。这样反斜杠后面的文本也将作为宏文本的一部分,参与宏替换。否则,如果新的一行不是以反斜杠开始,那么该行的文本就不属于宏文本,而宏文本在上一行就结束了。反斜杠并不参与宏替换,编译时,它将被忽略。如果宏文本包含了一个单行注释语句(以“//”开始的注释语句),那么该注释语句并不属于替换文本,编译时不参与替换。宏文本可以是空白,在这种情况下,文本宏被定义为空,当这样的宏被使用时,没有文本参与替换。

当形式参数被用来定义文本宏时,该形参的作用范围将贯穿整个宏文本。宏文本中的形式参数可以按与标识符同样的方式来使用。

文本宏调用的语法格式如下:

    'text_macro_identifier [< list_of_actual_arguments > ]

注意:与宏定义不同的是,在text_macro_identifier之前加上了('),而且参数列表由< list_of_formal_arguments >改为< list_of_actual_arguments >,即由形参列表改为实参列表。这样在编译过后,如果宏文本带有参数,那么所有的形参都被相应的实参值所代替。

宏文本不能作为下列词法标记的一部分进行宏替换,也就是不能用宏文本分割下列词法标记。

● 注释(Comments);

● 数值(Numbers);

● 字符串(Strings);

● 标识符(Identifiers);

● 关键字(Keywords);

● 操作符(Operators)。

下面举例说明宏指令'define。

【例2-25】宏指令'define。

    'define wordsize 8
    reg [1:'wordsize] data;
    //define a nand with variable delay
    'define var_nand(dly) nand #dly
    'var_nand(2) g121 (q21, n10, n11);
    'var_nand(5) g122 (q22, n10, n11);

下面是非法的宏指令使用:

    'define first_half "start of string
    $display('first_half end of string");

下面有几点是需要读者注意的。

● 编译时每一个实参是照字面意义逐字取代形参的。因此当一个表达式被用做实参时,该表达式将按照它的完整形式取代形参。如果一个形参在宏文本里被多次使用,将会导致该形参对应的表达式实参被多次计算。下面是表达式实参应用的例子。

【例2-26】表达式实参。

    'define my_square(x) ((x)*(x))
    n='my_square(a+b) ;

进行宏替换后,相应的宏将扩展为:

    n=((a+b)*(a+b));

在此,表达式a+b将被计算两次。

如果将宏定义修改为:

    'define my_square(x) (x*x)
    n='my_square(a+b) ;

进行宏替换后,相应的宏将扩展为:

    n =(a+b*a+b);

由此得到的宏替换可能与用户需要得到的结果大相径庭。因此当使用表达式作为实参时,为避免产生歧义,最好是在宏定义的宏文本中为形参加上括号“()”。

● “define”是一个编译指令关键字,但它并不属于标准关键字集,因此Verilog HDL源程序中的标准标识符可以使用编译指令关键字同样的名字(当然并不推荐读者这样做)。下面的几个问题值得读者考虑:

➢ 宏名不能取与编译指令关键字相同的名字。

➢ 宏名可以被再用做普通标识符。例如signal_name与'signal_name是不同的。

➢ 文本宏可以被重复定义。如果在源文本中对同一宏名进行了多次定义,编译时如遇到该宏名,将会使用最近一次定义的宏文本做宏替换。

● 与C函数一样,Verilog HDL允许宏嵌套,即在一个宏定义的宏文本中可以包含其他的文本宏。但是该被包含的宏只有在包含它的宏被调用后才会进行宏替换,而不是在包含它的宏被定义的时候进行宏替换。宏不允许递归调用,即宏文本内不能包含它自己本身。

(2)'undef指令(取消宏定义指令)

指令'undef 用来取消先前定义的宏。如果先前并没使用指令'define 进行宏定义,那么现在使用'undef指令将会导致一个警告。'undef指令的语法格式如下:

    'undef text_macro_identifier

一个取消了的宏没有值,就像它未被定义一样。

4.条件编译指令

条件编译指令包括'ifdef、'else、'elsif、'endif、'ifndef。这些指令用来控制Verilog HDL的源程序代码行是否参与编译。'ifdef指令用来检测一个宏名是否被定义过。如果宏名已经被定义过,那么跟在'ifdef指令后的代码行就被包含参与编译;如果一个宏名未曾被定义,而且存在一个'else指令,那么'else指令后面的代码行将参与编译。'ifndef指令也是用来检测宏名是否被定义过。如果宏名未曾被定义过,那么跟在'ifndef指令后的代码行就被包含参与编译;如果一个宏名已经被定义过,而且存在一个'else指令,那么'else指令后面的代码行将参与编译。

如果存在'elsif 指令(注意不是'else),编译器将会检测宏名是否被定义过。如果宏名已经被定义,那么跟在'elsif后面的代码行将参与编译。'elsif指令等效于'else 'ifdef ... 'endif指令序列,但是它不需要一条成对出现的'endif指令。这条指令之前必须有一条'ifdef或者'ifndef指令。

以上这些指令可以出现在源程序中的任何地方。以下几种情况可能需要使用条件编译指令。

● 选择一个模块不同的描述法,比如行为级、结构级或者开关级描述;

● 选择不同的时序或结构信息;

● 为一个仿真选择不同的激励。

条件编译指令有如下的语法格式。

'ifdef指令:

    'ifdef text_macro_identifier
    ifdef_group_of_lines
    { 'elsif text_macro_identifier elsif_group_of_lines }
    ['else else_group_of_lines ]
    'endif

'ifndef指令:

    'ifndef text_macro_identifier
    ifndef_group_of_lines
    { 'elsif text_macro_identifier elsif_group_of_lines }
    ['else else_group_of_lines ]
    'endif

text_macro_identifier是Verilog HDL的简单标识符。ifdef_group_of_lines、elsif_group_of_lines、else_group_of_lines、ifndef_group_of_lines都是Verilog HDL源程序描述语句。'else和'elsif编译指令及跟在它们后面的源程序描述语句都是可选项。此外,'ifdef、'ifndef、'else、'elsif和'endif这些指令还允许嵌套使用。下面是一些条件编译指令使用的例子。

【例2-27】'ifdef指令的简单使用。

    module and_op (a, b, c);
        output a;
        input b, c;
        'ifdef behavioral
            wire a=b & c;
        'else
            and a1 (a,b,c);
        'endif
    endmodule

【例2-28】条件编译指令嵌套。

    module test1(out);
        output out;
        'define wow
        'define nest_one
        'define second_nest
        'define nest_two
        'ifdef wow
            initial $display("wow is defined");
            'ifdef nest_one
                initial $display("nest_one is defined");
                    'ifdef nest_two
                        initial $display("nest_two is defined");
                    'else
                        initial $display("nest_two is not defined");
                'endif
            'else
                    initial $display("nest_one is not defined");
            'endif
        'else
            initial $display("wow is not defined");
            'ifdef second_nest
                initial $display("nest_two is defined");
            'else
                initial $display("nest_two is not defined");
            'endif
        'endif
    endmodule

仿真结果为:

    # wow is defined
    # nest_one is defined
    # nest_two is defined

【例2-29】条件编译指令链的嵌套。

    module test;
        'ifdef first_block
            'ifndef second_nest
                initial $display("first_block is defined");
            'else
                initial $display("first_block and second_nest defined");
            'endif
        'elsif second_block
            initial $display("second_block defined, first_block is not");
        'else
            'ifndef last_result
                initial $display("first_block, second_block, last_result not
defined.");
            'elsif real_last
            initial
                $display("first_block, second_block not defined,last_result
and real_last defined.");
            'else
                initial $display("Only last_result defined!");
            'endif
        'endif
    endmodule

仿真输出结果为:

    # first_block, second_block, last_result not defined.

5.文件包含指令

Verilog HDL中的文件包含指令'include与C语言中的预编译指令#include类似,在编译时,将其他文件中的源程序的完整内容插入当前的源文件。这样做的结果也就相当于将其他文件中的源程序内容复制到当前文件中出现指令'include的地方。'include编译指令可以将一些全局通用的定义或任务包含进文件中,而不用为每一个文件都编写一段重复的代码(指这些全局通用的定义或任务)。

使用'include指令的好处是:

● 提供了一个完整的配置管理;

● 改善了Verilog HDL源程序的组织结构;

● 便于Verilog HDL源程序的维护。

'include指令语法格式如下:

    'include  "file_name"

'include指令可以放在Verilog HDL源程序中任何地方。file_name是被包含进当前源文件的其他文件名。该文件名可以是完全的或者相对的路径名。只有空白或者注释可以出现在'include指令的同一行。一个被包含的文件内部也可以使用'include指令包含其他的文件,这就叫做包含嵌套,嵌套的层数是有限制的,这样的限制层数最小为15。

下例是文件包含的例子。

【例2-30】文件包含指令。

    //文件fileA.v的内容
    'define WORDSIZE 8
    function[WORDSIZE-1:0] mul1(input1,input2);
    ……
    endfunction
    //文件fileB.v的内容
    module fileB(in1,in2,out);
        'include "fileA.v"
        wire [2*WORDSIZE-1:0] temp;
        assign temp=mul1(in1,in2);
        ……
    endmodule

6.复位编译指令'resetall

在编译过程中一旦遇到'resetall复位编译指令,所有的编译指令都被设置到默认值。这在编译一个特定的源文件,只要求激活某些指令时是非常有用的。

推荐的做法是把'resetall指令放在每个源文件的开始,而让需要的编译指令紧随其后。

7.行号编译指令'line

行号编译指令'line用来重置当前文件的当前行号和文件名。如果当前的文件已经被修改(如添加或缩减代码行),那么指令'line 就可以用来映射原始文件中的位置。一旦指定了新的行号和文件名,编译器就可以根据新的行号和文件名查阅到原始源文件中的位置。例如,源代码调试中的错误信息能够提示用户实际的原始行号。行号编译指令'line的语法格式如下:

    'line  number  "file_name"  level

行号编译指令'line可以放在Verilog HDL源程序中任何地方。参数number(行号)是指令'line下一行代码的新行号。参数file_name(文件名)是文件的新名字,文件名可以是一个完全或者相对路径名。参数level指示进入一个被包含文件(level值为1),退出一个被包含文件(level值为2),或者这两者都不是(level值为0)。行号编译指令'line的结果并不受指令'resetall影响。当编译器处理文件的其余部分或新文件时,每读一行,行号应当递加 1,文件名也应当更新为当前被处理的文件名。当开始读 include 包含文件时,应当保存当前的行号和文件名,以便在读完include包含文件时恢复行号和文件名。被更新的行号和文件名信息可以通过PLI程序接口访问。当然库搜索机制并不受'line编译指令的影响。

8.时间标度指令'timescale

时间标度指令'timescale用于指定其后模块的时间单位和时间精度。时间单位是诸如仿真和延迟时间值的度量单位。在同一个设计中,有可能要使用不同时间单位的模块,而下列的时间标度编译指令是非常有用的。

● 编译指令'timescale在设计中用于指定模块的时间单位和精度;

● 系统任务$printtimescale显示模块的时间单位和精度;

● 系统函数$time、$realtime,以及系统任务$timeformat和%t格式规范用于指定时间信息如何被汇报。

时间标度指令'timescale 对跟随其后的所有模块都生效,直到有另一条指令'timescale出现为止。如果没有使用指令'timescale 或者已经被'resetall 指令复位,那么时间单位和精度就由仿真器指定。如果某些模块使用指令'timescale,而另外一些模块不使用,则会有错误发生。指令'timescale的语法格式如下:

    'timescale  time_unit / time_precision

时间单位参数time_unit是指定时间和延迟的度量单位。

时间精度参数 time_precision 指定延迟时间值被用于仿真之前如何舍入。在设计中具有最小时间精度参数的'timescale指令,决定仿真的时间单位精度。需要注意的是时间精度参数的精确度不能小于时间单位参数的精确度,也就是参数 time_precision 不能指定一个比参数time_unit更长的时间单位。

时间单位参数和时间精度参数都由整数和度量单位两部分构成。整数指定值的数量级大小,合法的整数只能取1、10或100。度量单位由字符串构成,合法的字符串有s、ms、us、ns、ps或fs,它们代表的时间度量单位分别是秒、毫秒、微秒、纳秒、皮秒或飞秒。

下面举例说明指令'timescale的用法。

【例2-31】时间标度指令'timescale。

    'timescale 10 ns / 1 ns
    module test;
        reg set;
        parameter d=1.55;
        initial
        begin
            #d set=0;
            #d set=1;
        end
    endmodule

指令'timescale 10 ns / 1 ns指定了模块的测试时间单位为10ns,时间精度为1ns。因此模块内的时间值都是10ns的倍数,对小于1ns的部分进行四舍五入。根据时间精度(1ns)将参数d的值四舍五入,即由1.55变为1.6。存储在参数d中的时间延迟值也就被换算成16ns(1.6×10 ns)。所以程序仿真运行的结果是在仿真时刻16ns处,给寄存器变量set赋值0,在仿真时刻32ns处给set赋值1。无论时间标度是否生效,参数d的值都保持不变。

9.驱动编译指令'unconnected_drive和'nounconnected_drive

所有出现在指令'unconnected_drive和'nounconnected_drive之间的未连接的模块输入端口都被上拉到电源或者下拉到地而非标准的默认值。

指令'unconnected_drive带有参数,可在pull1(上拉)和pull0(下拉)两者之间任选一个。'nounconnected_drive不带参数,其作用是取消'unconnected_drive的定义。这两条指令应当成对出现,而且只能放在模块声明之外。一般是将'unconnected_drive放在模块声明之前,而将'nounconnected_drive放在模块声明之后。

2.10 数据类型

在Verilog HDL中,数据类型被设计用来表示数字硬件电路中数据的存储和传输。

2.10.1 线网(Net)和变量(Variable)

在Verilog HDL中,根据赋值和对值的保持方式不同,可将数据类型主要分为两大类:线网(Net)和变量(Variable)。这两类数据也代表了不同的硬件结构。

1.线网(Net)声明

线网数据类型体现了结构实体(比如门级元件)之间的物理连接关系。除了 trireg net,其他的线网类型都不能保存值。相反,线网类型数据的值是由它的驱动器(比如一个连续赋值或者是一个门级元件)的值决定的。如果没有任何驱动器连接到线网上,那么它的值就为高阻(z)。只有一种情况例外,那就是当线网为trireg net型时,它将保持先前的驱动值。Verilog HDL禁止对已经声明过的线网、变量或参数再次声明。线网(Net)声明的语法格式如下:

    net_declaration::=
        net_type [ signed ] [ delay ] list_of_net_identifiers ;
        |  net_type  [  drive_strength  ]  [  signed  ]  [  delay  ]  list_of_
net_decl_assignments ;
        | net_type [ vectored | scalared ] [ signed ] range [ delay ] list_of_
net_identifiers ;
        | net_type [ drive_strength ] [ vectored | scalared ] [ signed ] range
[ delay ] list_of_net_decl_assignments ;
        | trireg [ charge_strength ] [ signed ] [ delay ] list_of_net_identifiers ;
        |  trireg  [  drive_strength  ]  [  signed  ]  [  delay  ]  list_of_
net_decl_assignments ;
        | trireg [ charge_strength ] [ vectored | scalared ] [ signed ] range
[ delay ] list_of_net_identifiers ;
        | trireg [ drive_strength ] [ vectored | scalared ] [ signed ] range
[ delay ] list_of_net_decl_assignments ;
    net_type::=
        supply0 | supply1| tri | triand | trior | tri0 | tri1| wire | wand | wor
    drive_strength ::=
        ( strength0 , strength1 )| ( strength1 , strength0 )| ( strength0 ,
highz1 )| ( strength1 , highz0 )
        | ( highz0 , strength1 )| ( highz1 , strength0 )
    strength0 ::=
        supply0 | strong0 | pull0 | weak0
    strength1 ::=
        supply1 | strong1 | pull1 | weak1
    charge_strength ::=
        ( small ) | ( medium ) | ( large )
    delay ::=
        # delay_value | # ( delay_value [ , delay_value [ , delay_value ] ] )
    delay_value ::=
        unsigned_number|     parameter_identifier|     specparam_identifier|
mintypmax_expression
    list_of_net_decl_assignments ::=
        net_decl_assignment { , net_decl_assignment }
    list_of_net_identifiers ::=
        net_identifier [ dimension { dimension }]{ , net_identifier [ dimension
{ dimension }] }
    net_decl_assignment ::=
        net_identifier=expression
    dimension ::=
        [ dimension_constant_expression : dimension_constant_expression ]
    range ::=
        [ msb_constant_expression : lsb_constant_expression ]

针对上述声明做以下几点说明:

(1)符号“::=”表示某项的定义;方括号“[]”表示该项为可选项;花括号“{}”表示列表中的同类项;符号“|”表示该项的另外一种定义形式。

(2)net_type表示线网型数据的类型。

(3)delay指定仿真延迟时间。

(4)drive_strength指定线网型数据赋值时的驱动强度。

(5)strength0可以取关键字supply0、strong0、pull0、weak0中的任何一个;strength1可以取关键字supply1、strong1、pull1、weak1中的任何一个。

(6)charge_strength用来指定所保持电荷容量的大小,可以取关键字small、medium、large中的任一个。

(7)dimension用来指定数组的维度。

(8)range 用来指定数据为标量或矢量。若该项默认,表示数据类型为 1 位的标量;反之,由该项指定数据的矢量形式。

有关线网声明的关键词较多,在后续的章节将会详细讲述。

2.变量(Variable)声明

变量是数据存储元件的抽象。从一次赋值到下一次赋值之前,变量应当保持一个值不变。程序中的赋值语句将触发存储在数据元件中的值改变。对于reg、time和integer这些变量型数据类型,它们的初始值应当是未知(x)。对于real和realtime变量型数据类型,默认的初始值是 0.0。如果使用变量声明赋值语句,那么变量将采用这个声明赋值语句所赋的值作为初值,这与initial结构中对变量的赋值等效。

(1)整型变量声明

整型变量常用于对循环控制变量的说明,在算术运算中被视为二进制补码形式的有符号数。整型数据与 32 位的寄存器型数据在实际意义上相同,只是寄存器型数据被当作无符号数来处理。整型变量的声明格式如下:

    integer_declaration ::=
        integer list_of_variable_identifiers ;
    list_of_variable_identifiers ::=
        variable_type { , variable_type }
    variable_type ::=
        variable_identifier [=constant_expression ]
        | variable_identifier dimension{ dimension }
    dimension ::=
        [ dimension_constant_expression : dimension_constant_expression ]

(2)实型变量声明

实型数据在机器码表示法中是浮点型数值,可用于对延迟时间的计算。实型变量声明的格式如下:

    real_declaration ::=
        real list_of_real_identifiers ;
    list_of_real_identifiers ::=
        real_type { , real_type }
    real_type ::=
        real_identifier [=constant_expression ]
        | real_identifier dimension { dimension }

(3)实时时间型变量声明

实时时间型变量声明的格式如下:

    realtime_declaration ::=
        realtime list_of_real_identifiers ;

(4)寄存器型变量声明

寄存器型变量对应的是具有状态保持作用的硬件电路,如触发器、锁存器等。寄存器型变量与线网型数据的区别主要在于:寄存器型变量保持最后一次的赋值,而线网型数据需要有连续的驱动。寄存器型变量声明的格式如下:

    reg_declaration ::=
        reg [ signed ] [ range ] list_of_variable_identifiers ;
    range ::=
        [ msb_constant_expression : lsb_constant_expression ]

(5)时间型变量声明

时间型变量与整型变量类似,只是它是 64 位的无符号数。时间型变量主要用于对仿真时间的存储与计算处理,常与系统函数$time一起使用。时间型变量声明的格式如下:

    time_declaration ::=
        time list_of_variable_identifiers ;

有关变量(Variable)声明,在后续的章节将会详细讲述。

2.10.2 标量(Scalar)与矢量(Vector)

在一个net或reg型声明中,如果没有指定范围,就被看做是1bit位宽,也就是通常所说的标量(Scalar)。通过指定范围来声明多位的net或reg型数据,则称为矢量(Vector)。

下面着重讲述矢量(Vector)。

1.矢量说明

矢量范围由常量表达式来说明。[msb_constant_expression(最高位常量表达式):lsb_constant_expression(最低位常量表达式)]。

msb_constant_expression(最高位常量表达式)代表范围的左侧值,lsb_constant_expression(最低位常量表达式)代表范围的右侧值。右侧表达式的值可以大于、等于、小于左侧表达式的值。

net和reg型矢量遵循以2为模(2n)的乘幂算术运算法则,此处的n值是矢量的位宽。net和reg型矢量如果没有被声明为有符号量或者连接到一个已声明为有符号的数据端口,那么该矢量被隐含当作无符号的量。

下例是有关矢量说明的例子。

【例2-32】矢量说明。

    wand w; //标量线网型(wand)
    tri [15:0] busa; // 三态16位总线
    trireg (small) storeit; // 一个小强度(small)的电荷存储节点
    reg a; // 一个标量寄存器型变量
    reg[3:0] v; // 一个4位的矢量寄存器变量,由v[3]、v[2]、v[1]、v[0]构成
    reg signed [3:0] signed_reg; //一个4位的有符号矢量型寄存器变量,其取值范围为-8到7
    reg [-1:4] b; // 一个6位的矢量型寄存器变量
    wire w1, w2; // 声明两个net(wire型)数据
    reg [4:0] x, y, z; // 声明3个5位的矢量寄存器型变量

2.矢量线网型数据的可访问性

vectored 和 scalared 是矢量线网型或矢量寄存器型数据声明中的可选择关键字。如果这些关键字被使用,那么矢量的某些操作就会受约束。如果使用关键字 vectored,那么矢量的位选择或部分位选择以及强度指定就被禁止,而PLI就会认为数据对象未被展开。如果使用关键字scalared,那么矢量的位或部分位选择就被允许,PLI认为数据对象将被展开。下例是使用关键字vectored和scalared的例子。

【例2-33】关键字vectored和scalared。

    tri scalared [63:0] bus64; //一个将被展开的数据总线
    tri vectored [31:0] data; //一个未被展开的数据总线

2.10.3 线网(Net)数据类型

1.强度(strength)

在一个线网型数据类型声明中,可以指定两类强度:电荷量强度(charge strength)和驱动强度(drive strength)。

● charge strength:只有trireg线网类型的声明中,才可以使用该强度。

● drive strength:只有在一个线网型数据的声明语句中对数据对象进行了连续赋值,才可使用该强度。

门级元件的声明只能指定drive strength。

(1)电荷量强度(charge strength)

一个trireg线网型数据用于模拟电荷存储。电荷量强度可由下面的关键字来指定电容量的相对大小:

● small

● medium

● large

默认的电荷强度为medium。

一个trireg线网型数据能够模拟一个电荷存储节点,该节点的电荷量将随时间而逐渐衰减。对于一个 trireg 线网型数据在仿真时,其电荷衰减时间应当指定为延迟时间。下例说明charge strength的使用。

【例2-34】charge strength的使用。

    trireg a; //声明一个trireg型线网数据,其电荷强度为medium
    trireg (large) #(0,0,50) cap1 ; //声明一个trireg型线网数据,其电荷强度为large,
                                  //电荷衰减,时间为50个时间单位
    trireg (small)signed [3:0] cap2 ; //一个4位有符号trireg型线网矢量数据声明,
                                      //电荷强度为small

(2)驱动强度(drive strength)

在一个线网型数据的声明语句中如果对数据对象进行了连续赋值,就可为声明的数据对象指定驱动强度。

2.线网型数据的隐式声明

如果没有显式声明,那么在以下情况中,一个默认线网型数据类型就被指定。

● 在一个端口表达式的声明中,如果没有对端口的数据类型进行显式说明,那么默认的端口数据类型就为wire型,且默认的wire型矢量的位宽与矢量型端口声明的位宽相同。

● 在基本元件例化、模块例化的端口列表中,如果先前没有对端口的数据类型进行显示说明,那么默认的端口数据类型为线网型标量。

● 如果一个标识符出现在连续赋值语句的左侧,而该标识符先前未曾被声明,那么该标识符的数据类型就被隐式声明为线网型标量。

3.线网型数据的初始化

线网型数据的默认初始化值为 z。带有驱动的线网型数据应当为它们的驱动输出指定默认值。trireg线网型数据是一个例外。它的默认初始化值为x,而且在声明语句中应当为其指定电荷量强度(small、medium或large)。

4.线网型数据的类型

线网型数据的类型如表2-7所示。

表2-7 线网型数据类型

(1)wire和tri

wire和tri都用于连接电路元件。wire和tri有相同的语法和功能。wire型数据可以被单个的门级元件或连续赋值所驱动。tri型数据可以被多个驱动源所驱动。

一个wire型数据或者一个tri型数据被多个相同强度的驱动源驱动时所导致的逻辑冲突是产生一个未知量(x)。表2-8是wire型数据和tri型数据被多个驱动源驱动时的真值表。此处假定两个驱动源的强度相同。

表2-8 线网型数据的wire和tri型的真值表

(2)wand、triand、wor和trior

wand、triand、wor和trior都用于连线型逻辑结构建模。当有多个驱动源驱动wand和triand型数据时,将产生线与结构。如果驱动源中任一个为0,那么线网型数据的值将为0。同样,当有多个驱动源驱动wor和trior型数据时,将产生线或结构。如果驱动源中任一个为1,那么线网型数据的值将为1。wand和triand有相同的语法和功能,而wor和trior也有相同的语法和功能。表2-9和表2-10分别表示具有多重驱动源驱动时wand和triand,以及wor和trior的真值表。此处都假定两个驱动源的强度相同。

表2-9 线网型数据的wand和triand型的真值表

表2-10 线网型数据的wor和trior型的真值表

(3)trireg

trireg线网型数据用于存储一个值以及电荷存储节点建模。一个trireg线网型数据可以处于驱动和电容性两种状态之一。

● 驱动状态:当至少被一个驱动源驱动时,trireg线网型数据有一个值(1、0或x)。判决值被导入trireg型数据,也就是trireg型线网的驱动值。

● 电容性状态:如果所有驱动源都处于高阻状态(z),trireg 线网型数据则保持它最后的驱动值。高阻值不会从驱动源导入trireg线网型数据。

根据trireg线网型数据声明语句中的指定,trireg线网型数据处于电容性状态时其电荷量强度可以是small、medium或large。同样,trireg线网型数据处于驱动状态时,根据驱动源的强度,其驱动强度可以是supply、strong、pull或weak。图2-9是包含一个trireg线网型数据(驱动强度为medium)、驱动源和仿真结果的示意图。

针对图2-9做两点说明:

● 在仿真时刻0,wire a和wire b值都为1,开关管nmos1和nmos2都导通,同时被wire c互连,一个强度为strong的值1从与门传送到trireg d。

● 在仿真时刻10,wire a值变为0,nmos1关断了与门和wire c的连接,从而wire c的值变为高阻(z)。wire b保持为1,nmos2继续导通,因此wire c保持和trireg d的连接。但是高阻值(z)不会从wire c传送到trireg d。相反,trireg d进入电容性状态,存储它最后的驱动值1,该驱动值1的强度为medium。

图2-9 一个trireg线网型数据的仿真值和它的驱动源

① 电容性线网

一个电容性线网是两个或更多个trireg之间的互连线网。如果电容性线网中的每一个trireg都处于电容性状态,那么在trireg之间可以互相传递逻辑和强度值。图2-10是一个电容性线网的示意图,在该线网中,某些trireg的逻辑值改变了其他同等或更小强度trireg的逻辑值。线网型数据trireg_la电荷量强度为large,trireg_me1、trireg_me2电荷量强度为medium,trireg_sm电荷量强度为small。仿真结果按事件发生的顺序排列如下:

图2-10 电容性线网的仿真结果

● 在仿真时刻0,wire a和wire b值为1,wire c驱动值1到trireg_la和trireg_sm;wire d驱动值1到trireg_me1和 trireg_me2。

● 在仿真时刻10,wire b的值变为0,将trireg_sm和trireg_me2与它们的驱动源断开,这两个trireg进入电容性状态,并且保持它们最后的驱动值1。

● 在仿真时刻20,wire c驱动值0到trireg_la。

● 在仿真时刻30,wire d驱动值0到trireg_me1。

● 在仿真时刻40,wire a的值变为0,将trireg_la和trireg_me1与它们的驱动源断开,这两个trireg进入电容性状态,并且保持它们最后的驱动值0。

● 在仿真时刻50,wire b的值变为1,将trireg_sm和trireg_la连接,这两个trireg有不同的电荷量强度并且保存不同的值。这个连接将导致电荷量强度更小的trireg保存电荷量强度更大的trireg的值,从而使trireg_sm保存值0。wire b值由0变为1也将trireg_me1和trireg_me2连接,这两个trireg有相同的电荷量强度而保存不同的值。该连接将导致trireg_me1和trireg_me2的值都变为未知(x)。

在电容性线网中,电荷量强度将会从较大强度的trireg传递到较小强度的trireg。图2-11显示了一个电容性网络和它的仿真结果。在该图中,trireg_la电荷量强度为large, trireg_sm电荷量强度为small,仿真结果如下:

图2-11 电荷共享的仿真结果

● 在仿真时刻0,wire a、wire b和wire c值为1,wire a驱动值1(驱动强度为strong)到trireg_la和trireg_sm。

● 在仿真时刻10,wire b的值变为0,将trireg_la和trireg_sm与wire a断开,trireg_la和trireg_sm都进入电容性状态,因为它们仍然通过tranif1_2保持连接,所以两个trireg共享trireg_la的电荷量强度large。

● 在仿真时刻20,wire c变为0,断开trireg_la和trireg_sm,trireg_sm不再共享trireg_la的电荷量强度large,而是保持一个small的电荷量强度。

● 在仿真时刻30,wire c变为1,连接trireg_la和trireg_sm,从而它们应当共享电荷量强度large。

● 在仿真时刻40,wire c再次变为0,断开trireg_la和trireg_sm。trireg_sm不再共享trireg_la的电荷量强度large,而是保持一个small的电荷量强度。

② 理想电容性状态和电荷衰减

一个trireg线网可能不确定保持它的值,或者电荷量会随着时间而衰减。电荷量衰减的仿真时间是在trireg线网声明语句中的延迟时间中指定的。

(4)tri0和tri1

tri0和tri1用于带有上拉或下拉电阻的设备建模。当没有驱动源驱动一个tri0线网时,该线网值为 0;同样,没有驱动源驱动一个 tri1 线网时,该线网值为 1。线网值的驱动强度都为pull。tri0等效于这样一个wire型线网:有一个强度为pull的0值连续驱动该wire。同样,tri1等效于这样一个wire型线网:有一个强度为pull的1值连续驱动该wire。关于tri0和tri1的真值表分别如表2-11和表2-12所示。

表2-11 tri0真值表

表2-12 tri1真值表

(5)supply0和supply1

supply0和supply1线网用于电路中的电源建模。它们的驱动强度都为supply。

2.10.4 变量(Variable)数据类型

1.寄存器型变量

在Verilog HDL中,寄存器型(reg)变量的赋值是由过程赋值语句实现的。由于寄存器型变量能够在两次赋值操作之间保持一个值不变,因此它可以用于硬件寄存器建模。边沿敏感(如触发器)和电平敏感(如RS和透明锁存器)的存储元件都可以用寄存器型变量来建模。当然寄存器型变量也不一定就用来描述存储元件,它也可以描述组合逻辑。

2.整型、实型、时间型和实时时间型变量

关于寄存器型(reg)、整型(integer)、实型(real)、时间型(time)和实时时间型(realtime)变量的声明语法格式已在2.10.1节中详细说明。整型变量是一个处理数值的通用变量,但它并不对应任何硬件寄存器。时间型变量用于存储和处理仿真时间值,因为在仿真时往往需要检测时序和对程序进行调试。时间型变量通常是和系统函数$time 联合使用。整型和时间型变量的赋值方式与寄存器型变量的赋值方式相同。即对这几种变量的赋值都应当采用过程赋值语句。时间型变量与一个64位的寄存器矢量(最低位为0)等效,都属于无符号量;应当使用无符号算术运算对它们进行操作。相反,整型变量被作为一个有符号数,当然在形式上与32位的寄存器矢量相同,其最低位为0;对整型变量应当使用二进制补码形式的算术运算操作。实型变量可以用在与整型或时间型变量同样的地方,只是以下几点限制是需要读者注意的:

● 并不是所有的Verilog HDL操作符都可以用于实型变量。至于哪些操作符可以用于实数或实型变量,请参见后面的章节。

● 实型变量在声明时不能指定范围。

● 实型变量有一个默认的初始化值0。

实时时间型变量与实型变量在声明时意义相同,可以在使用时互换。

下例是这几种变量的声明举例。

【例2-35】变量的声明。

    integer a;          //整型变量
    time last_chng;     //时间型变量
    real float ;        //实型变量
    realtime rtime ;    //实时时间变量

(1)操作符和实数

对实数和实型变量进行逻辑或关系运算得到的结果将是一个一位的标量值。并不是所有的Verilog HDL操作符都可以在表达式中对实数和实型变量进行操作。在下列情况下,实型常量和实型变量是被禁止的:

● 边沿描述符(posedge、negedge)不能应用于实型变量。

● 变量被声明为实型时,不能使用位选择或部分位选择。

● 对矢量进行位选择或部分位选择时,索引表达式不能为实数。

(2)实数和整数的转换

实数转换为整数应当对实数的小数部分进行四舍五入,而不是直接将小数部分去掉。当实数被赋给一个整型变量时,将产生一个隐式的类型转换。如果实数的小数部分恰好是0.5,那么经过四舍五入,它应当成为1。当一个表达式被赋给实型变量时,也将产生一个隐式转换,在转换时,线网或变量为x或z的各个位都将被看做0。

2.10.5 数组(Array)类型

一个线网型数组或变量型数组的声明定义了每一个数组元素的数据类型,要么是标量要么是矢量,即数组的类型就是数组元素的类型。数组可以将已声明过类型的元素组合成多维的数据对象。数组声明时,应当在声明的数组标识符后面指定元素的地址范围。每一个维度代表一个地址范围。数组可以是一维数组(一个地址范围),也可以是多维数组(多重地址范围)。数组的索引表达式应当是常量表达式,该常量表达式的值应当是正整数、负整数或者 0。一个数组元素可以通过一条单独的赋值语句被赋值,但是整个数组或数组的一部分不能用一条单独的赋值语句被赋值。整个数组或数组的一部分也不能为一个表达式赋值。要给一个数组元素赋值,需要为该数组元素指定索引。数组索引可以是一个表达式,这就为数组元素的选择提供了一种机制,即依靠对该数组索引表达式中其他的线网数据或变量值的运算结果来定位数组元素。例如一个程序计数寄存器变量可以用做RAM(随机存取存储器)的数组索引。具体实现的时候,数组元素的数目是被限制的,但该数目最小应为16777216 (224)。

1.线网型数组

线网型数组主要用于模块例化(模块调用)时的端口映射。线网型数组的每一个元素都可按线网标量或线网矢量相同的方式使用。

2.寄存器与变量型数组

数组元素的类型可以是变量(reg、integer、time、real、realtime)中的任一种。

3.存储器

如果一个一维数组的元素类型为reg型,那么这样的一维数组也称为存储器。存储器可用于ROM(只读存储器)、RAM(随机存取存储器)和寄存器组建模。数组中的每一个寄存器也叫做元素或字,并且是通过单一的索引来寻址的。一个n位的寄存器可以通过一条单独的赋值语句被赋值,但是整个存储器不能通过这样的一条语句被赋值。为了对存储器的某个字赋值,需要为该字指定数组索引。该索引也可以是一个表达式,该表达式中含有其他的变量或线网数据,通过对该表达式的运算,得到一个结果值,从而定位存储器的字。

4.数组举例

(1)数组声明

【例2-36】数组声明。

    reg [7:0] mema[0:255];      //声明一个存储器 mema,该存储器有256个8位的寄存器
                                //索引是从0到255
    reg arrayb[7:0][0:255];     //声明一个二维数组arrayb,
                                //该数组的元素是一位的寄存器
    wire w_array[7:0][5:0];     //声明一个wire型的二维数组
    integer inta[1:64];         //声明一个整型数组,该数组包含64个整型变量
    time chng_hist[1:1000];     //声明一个时间型数组,该数组包含1000个时间型变量
    integer t_index;

(2)数组元素的赋值

【例2-37】数组元素的赋值。

本例的赋值语句是以例2-36中的数组声明作为前提的。

    mema=0;                    // 非法的赋值语句—试图对整个数组赋值
    arrayb[1]=0;               // 非法的赋值语句—试图对数组元素
                                 // [1][0]..[1][255] 赋值
    arrayb[1][12:31]=0;        // 非法的赋值语句— 试图对数组元素
                                 // [1][12]..[1][31] 赋值
    mema[1]=0;                 // 对数组mema的第2个元素赋值0
    arrayb[1][0]=0;            // 通过索引[1][0]对数组arrayb的元素
                                 // [1][0]赋值0
    inta[4]=33559;             // 对数组的元素[4]赋一个十进制值
    chng_hist[t_index]=$time;  // 根据整数索引t_index寻址,将当前的仿真
                                 // 时间值赋值到数组元素

(3)存储器的区别

本例说明一个包含n个1位寄存器的存储器与一个n位寄存器矢量的区别。

【例2-38】存储器的区别。

    reg [1:n] rega;     //一个n位的寄存器矢量
    reg mema [1:n];     //一个存储器,含有n个元素,每个元素为1位的寄存器标量

2.10.6 参数

Verilog HDL 中的参数既不属于变量类型也不属于线网类型范畴。参数不是变量,而是常量。有两类参数:模块参数和延时参数。为一个已经声明为线网、变量或参数的标识符重新声明一个名字是违反语法规则的操作。两种类型的参数都可以为其指定范围,如果没有指定范围,默认情况下的模块参数和延时参数位宽都应该能够容纳常量值。

1.模块参数

模块参数声明的语法格式如下:

    parameter_declaration ::=
        parameter [ signed ] [ range ] list_of_param_assignments ;
        | parameter integer list_of_param_assignments ;
        | parameter real list_of_param_assignments ;
        | parameter realtime list_of_param_assignments ;
        | parameter time list_of_param_assignments ;
    list_of_param_assignments ::=
        param_assignment { , param_assignment }
    param_assignment ::=
        parameter_identifier=constant_expression
    range ::=
        [ msb_constant_expression : lsb_constant_expression ]

针对上述语法格式做以下几点说明:

(1)list_of_param_assignments 应当是一个用逗号(,)分隔的赋值语句列表,在赋值语句的右侧应当是一个常量表达式;也就是说该表达式只能包含常数和先前定义过的参数。

(2)list_of_param_assignments可以出现在模块中描述语句的位置,或者模块声明时模块端口参数列表的位置。如果参数赋值语句出现在模块端口参数列表中,那么这样的参数就成为局部参数,不能用任何方法修改它。

(3)模块参数表示常量,因此在运行时修改它的值是非法的。然而,模块参数可以在编译时被修改为不同于声明时所赋的值,这就允许模块的调用客户化。模块参数可以使用defparam语句修改,或者在模块调用语句中被修改。模块参数通常用于指定变量的时间延迟或者位宽。

(4)可以为模块参数指定类型和范围。这需要遵守下述规则:

● 如果模块参数的声明中没有指定参数的数据类型和范围,那么该参数的默认数据类型和范围将是该参数最终所赋值的数据类型和范围。

● 如果参数的声明中只指定了范围而没指定数据类型,那么该参数的范围将是声明中所指定的范围,而数据类型为无符号型。即参数的范围和数据类型不受所赋值的范围和数据类型的影响。

● 如果参数声明中指定了数据类型而没指定范围,那么该参数的范围将是最终所赋值的数据范围。

● 如果参数声明中指定的数据类型为有符号型,而且也指定了范围,那么该参数的符号和范围不受所赋值数据的符号和范围的影响。

● 如果参数声明中没有指定范围,并且要么指定参数的类型为有符号型,或者没有指定数据类型,而且该参数最终所赋值指定了位宽,那么该参数默认的位宽等于最终所赋值的位宽。

● 如果参数声明中没有指定数据范围,并且要么指定参数的类型为有符号型,要么没指定数据类型,而且该参数最终所赋值没有指定位宽,那么该参数默认的数据范围位宽至少为32位。

至于实数和整数的转换规则在2.10.4节中已有阐述。

下面是参数的声明举例。

【例2-39】参数声明。

    parameter msb=7;                  // 定义msb为常量7
    parameter e=25, f=9;            // 定义两个常量数值
    parameter r=5.7;                  // 定义r为实数参数
    parameter byte_size=8,
    byte_mask=byte_size - 1;
    parameter average_delay=(r + f) / 2;
    parameter signed [3:0] mux_selector=0;
    parameter real r1=3.5e17;
    parameter p1=13'h7e;
    parameter [31:0] dec_const=1'b1;  // 将数值转换为32位的矢量
    parameter newconst=3'h4;          // 隐含参数范围[2:0]
    parameter newconst=4;             // 隐含参数范围[31:0]

2.局部参数

局部参数与模块参数大致相同,唯一不同的是局部参数值不能直接由defparam语句或者由模块调用语句所修改。局部参数可由包含其他参数的常量表达式所赋值,而该所包含参数可由defparam语句或者由模块调用语句所修改。局部参数声明的语法格式如下:

    local_parameter_declaration ::=
        localparam [ signed ] [ range ] list_of_param_assignments ;
        | localparam integer list_of_param_assignments ;
        | localparam real list_of_param_assignments ;
        | localparam realtime list_of_param_assignments ;
        | localparam time list_of_param_assignments ;

该语法格式中的range和list_of_param_assignments已在模块参数声明语法格式中阐述。

3.延时参数

延时参数是一类特殊的参数类型,由关键字specparam声明,它仅用于时序和延迟值。延时参数除了不能出现在为其他参数赋值的表达式中或者声明语句的范围指定部分外,它可以出现在任意表达式中。在最初的Verilog HDL语言版本中延时参数仅能出现在specify语句块中,现在延时参数既可出现在 specify 语句块中又可出现在模块的主体语句内。出现在 specify 语句块之外的延时参数在被引用之前应当先对其声明。延时参数可以被任意常量表达式赋值,延时参数也可以用做其后另一延时参数声明中常量赋值表达式的一部分;与此相反的是模块参数声明中常量赋值表达式不能包括任何的延时参数。与模块参数不同,声明了的延时参数在程序中不能再使用任何语句修改它,但是却可以通过SDF(标准延迟格式)标注对它进行修改。表2-13是关于模块参数和延时参数的区别总结。

表2-13 延时参数和模块参数的区别

延时参数声明的语法格式如下:

    specparam_declaration ::=
        specparam [ range ] list_of_specparam_assignments ;
    list_of_specparam_assignments ::=
        specparam_assignment { , specparam_assignment }
    specparam_assignment ::=
        specparam_identifier=constant_mintypmax_expression
        | pulse_control_specparam
    pulse_control_specparam ::=
        PATHPULSE$=( reject_limit_value [ , error_limit_value ] ) ;
        |  PATHPULSE$specify_input_terminal_descriptor$specify_output_terminal
_descriptor
       =( reject_limit_value [ , error_limit_value ] ) ;
    error_limit_value ::=
        limit_value
    reject_limit_value ::=
        limit_value
    limit_value ::=
        constant_mintypmax_expression
    range ::=
        [ msb_constant_expression : lsb_constant_expression ]

按照如下规则可以为延时参数指定一个数据范围。

● 如果在延时参数声明语句中没有指定数据范围,那么将最终为参数所赋值的数据范围默认为该延时参数的数据范围。

● 如果在延时参数声明语句中指定了数据范围,则参数的数据范围不受为其所赋值的数据范围的影响。

【例2-40】延时参数声明。

    //在specify语句块中延时参数声明
    specify
        specparam tRise_clk_q=150, tFall_clk_q=200;
        specparam tRise_control=40, tFall_control=50;
    endspecify
    //在模块内的延时参数声明
    module RAM16GEN (DOUT, DIN, ADR, WE, CE)
        specparam dhold=1.0;
        specparam ddly=1.0;
        parameter width=1;
        parameter regsize=dhold + 1.0; // 非法的参数声明语句——不能将延时参数
                                        // 赋值给模块参数
    endmodule

2.10.7 名字空间

在Verilog HDL中有7类名字空间,其中两类为全局名字空间,其余5类为局部名字空间。

1.全局名字空间

全局名字空间包括定义和文本宏。

(1)定义名字空间包括所有 module(模块)、marcomodule(宏模块)、primitive(基本元件)的定义。一旦某个名字被用于定义一个模块、宏模块或基本元件,那么它将不能再用于声明其他的模块、宏模块或基本元件,也就是这个名字在定义名字空间具有唯一性。

(2)文本宏名字空间也是全局的。由于文本宏名由重音符号(')引导,因此它与别的名字空间有明显的区别。文本宏名的定义逐行出现在设计单元源程序中,它可以被重复定义,也就是同一宏名后面的定义将覆盖其先前的定义。

2.局部名字空间

局部名字空间包括:block(语句块)、module(模块)、port(端口)、specify block(延时说明块)和attribute(属性)。一旦某个名字在这5个名字空间中的任意一个空间内被定义,它就不能在该空间中被重复定义,即具有唯一性。

(1)语句块名字空间包括:语句块名、函数名、任务名、参数名、事件名和变量类型声明。其中变量类型声明包括reg、integer、time、real和realtime声明,这在前面的章节介绍过。

(2)模块名字空间包括:函数名、任务名、例化名(模块调用名)、参数名、事件名和线网类型声明与变量类型声明。其中线网类型声明包括wire、wor、wand、tri、trior、triand、tri0、tri1、trireg、supply0和 supply1,这在前面的章节已经介绍过。

(3)端口名字空间用于连接两个不同名字空间中的数据对象。连接可以是单向的(input或output)或双向的(inout)。端口名字空间是模块名字空间与语句块名字空间的交集。从本质上说,端口名字空间规定了不同空间中两个名字的连接类型。端口的类型声明包括input、output、inout。在端口名字空间中定义的端口名可以在模块名字空间中被再次引用,这只需要在模块名字空间中声明一个与端口名同名的变量或wire型数据就可以了。

(4)延时说明块名字空间将在specify块结构中介绍。

(5)属性是由符号(* *)包括的语言结构。属性名只能在属性名字空间中被定义和使用,而其他任何名字都不能在属性名字空间中被定义。

2.11 表达式

表达式是将操作符和操作数联合起来使用的一种Verilog HDL语言结构,通过运算得到一个结果,从语义上说,这个结果值也可以看做操作数的函数值。一个合法的操作数,例如一个线网型矢量的位选择,尽管没有操作符参与,但是它也被看做表达式。表达式可以用在Verilog HDL语句中任何一个需要值的地方。

某些语句结构要求表达式是一个常量表达式。常量表达式中的操作数只能是常数数值、参数、参数的某一位、参数的部分位、常量函数调用,而且操作符只能使用表 2-14中定义的操作符。

标量表达式的是一个对标量求值的表达式。如果标量表达式对矢量求值,那么所求结果的最低位被用做标量结果值。

表达式的操作数可以是下述类型之一:

● 常量数值(包括实数);

● 线网型数据;

● 变量类型;

● 线网矢量的某一位;

● 线网矢量的某几位;

● reg、integer和time型数据的某一位;

● reg、integer和time型数据的某几位;

● 数组元素;

● 用户函数或系统函数调用,其返回值是上述类型中的任意一个。

2.11.1 操作符

Verilog HDL中的操作符符号与C语言中的类似。表2-14中列出了这些操作符。

表2-14 Verilog HDL中的操作符及功能说明

1.实型操作数的操作符

表2-15列出了适用于实型操作数的合法操作符,而其他操作符对于实型操作数来说则被认为是非法的。

表2-15 实型操作数的合法操作符

对实型操作数使用逻辑或关系操作符得到的结果将是一个标量值。表2-16列出了关于实型操作数的非法操作符。

表2-16 实型操作数的非法操作符

2.操作符的优先级

表2-17是Verilog HDL中的操作符优先级列表。

表2-17 操作符的优先级

在表2-17中,按照箭头指示的方向,操作符的优先级从高到低降序排列,同一行的操作符有相同的优先级。例如,*、/、%有相同的优先级,二元运算符+、-也有相同的优先级,而*、/、%的优先级高于二元运算符+、-。

结合性指出了表达式中具有相同优先级的操作符求值的先后顺序。除了条件操作符(?:),其余操作符都具有左结合性,而条件操作符具有右结合性。例如在下式中由于加(+)号和减(-)号,具有相同优先级,而它们的结合性为左结合,所以求值的顺序是这样的:式中B首先被加到A,得到结果A+B;然后再从结果A+B中减去C。

    A+B-C;

当操作符处于不同优先级时,更高优先级的操作符应当被首先结合。例如下式中,除(/)号的优先级高于加(+)号,而求值的顺序是这样的:首先计算 B 除以 C,得到结果B/C;然后再将此结果加到A。

    A+B/C;

圆括号可以改变操作符优先级。如下式,求值顺序是:首先计算A+B,得到一个结果值;然后再将此结果值除以 C。式中加(+)号的优先级变得高于除(/)号,这都是因为加上了圆括号的缘故。

    (A+B)/C;

3.表达式中整数数值的使用

整数数值可以用做表达式中的操作数。一个整数数值可以按下述方式表示:

● 简单的十进制格式(如12);

● 未指定位宽,基数格式(如’d12,’sd12);

● 指定位宽,基数格式(如16’d12,16’sd12)。

在表达式中一个十进制格式的负整数与基数格式的负整数在被机器编译后是不同的。一个十进制格式的负整数在表达式中被编译成有符号的二进制补码格式,一个无符号基数格式的负整数在表达式中被编译成无符号数。

【例2-41】负整数在表达式中的区别。

本例用4种方式表达-12除以3,以说明负整数在表达式中的区别。

    integer IntA;
    IntA=-12 / 3;         // 结果为-4。
    IntA=-'d 12 / 3;      // 结果为1431655761。
    IntA=-'sd 12 / 3;     // 结果为-4。
    IntA=-4'sd 12 / 3;    // -4'sd12是4位的负数,等于1100, 也就是-4。
                            //-(-4)=4,因此最终结果为1。

请读者注意,-12 和-’d12 被求值后有相同的二进制格式,即都是二进制补码格式,但是在表达式中,-’d12失去了它的负号,变成无符号数。

4.表达式的求值顺序

求一个表达式的值时,操作符应当遵照前述“操作符的优先级”中的结合性规则。然而如果表达式的最终值可以被提前决定,那么就没有必要对整个表达式进行求值。这也叫做短路表达式的求值,如下例所示。

【例2-42】短路表达式求值。

    reg regA, regB, regC, result ;
    result=regA & (regB | regC) ;

如果事先知道regA的值为0,那么整个表达式的值就被确定为0,而不必对后面的regB| regC再做计算。

5.算术运算操作符

二元的算术运算操作符由表2-18给出。

表2-18 二元算术运算操作符

整数除法的结果应当截去小数部分。对于除法或取余操作符,如果第二个操作数为0,那么整个的结果值应当为未知(x)。在取余操作中,如果第一个操作数恰好能被第二个操作数整除,那么最终的结果(即取得的余数)为 0;取余操作结果值的符号采用与第一个操作数相同的符号。在乘方运算中,如果有任意一个操作数为实数、整数或者有符号数,那么最终的结果应当为实数;如果两个操作数都为无符号数,那么结果为无符号数;如果第一个操作数为0第二个操作数为非正数,或者第一个操作数是负数第二个为非整数值,那么乘方运算的结果为未知(x)。

一元算术运算操作符的优先级高于二元算术运算操作符。一元算术运算操作符由表2-19给出。

表2-19 一元算术运算操作符

对于算术运算操作符,如果任何一个操作数的某一位的值为未知(x)或者高阻(z),那么最终的结果值为未知(x)。

表2-20是关于取余操作的例子。

【例2-43】取余操作。

表2-20 取余操作举例

6.含有寄存器型或整型数据的算术运算表达式

寄存器型数据如果没有显式声明为有符号数,则应当被看做无符号数值。而整型变量则被看做有符号数。有符号的值在机器中以二进制补码形式表示。有符号数和无符号数之间的转换在二进制表示形式上相同,只是在机器编译后才有所改变。表2-21中列出了算术运算操作符是如何编译每一种数据类型的。

表2-21 算术运算操作符对数据类型的编译

下例说明如何在表达式中使用整型和寄存器型数据。

【例2-44】在表达式中使用整型和寄存器型数据。

    integer intA;
    reg [15:0] regA;
    reg signed [15:0] regS;
    intA=-4'd12;
    regA=intA / 3;    //表达式结果为-4,intA 为整型数据,其中regA 为65532
    regA=-4'd12;      // regA 为 65524
    intA=regA / 3;    //表达式结果为21841,其中regA 为寄存器型数据
    intA=-4'd12 / 3;  //表达式结果为1431655761,其中-4'd12是一个32位的寄存器型数据
    regA=-12 / 3;     //表达式结果为-4,其中-12 是一个整型数据,regA 为65532
    regS=-12 / 3;     //表达式结果为-4,其中regS是一个有符号的寄存器型数据
    regS=-4'sd12 / 3; //表达式结果为1,其中-4'sd12实际上等于4,按照整数除法的规则
                        //有4/3==1

7.关系操作符

表2-22中列出了关系操作符。

表2-22 关系操作符

使用关系操作符的表达式结果为标量值,如果指定的关系为“假”,则结果为值 0,反之,指定的关系为“真”,则结果为值1。如果关系操作符两端的任意一个操作数包含一个未知(x)或者高阻(z)值,则表达式的结果为1位的未知(x)值。

如果两个操作数有不同的位宽,其中一个或两个操作数为无符号数,那么应当在位宽较小的操作数的高位补0,将其位宽扩展到较大位宽的操作数位宽。

所有的关系操作符有相同的优先级,但关系操作符的优先级低于算术运算操作符。

下面举例说明这个优先级规则。

【例2-45】关系操作符与算术运算操作符的优先级。

    a < foo - 1         ①
    a < (foo - 1)       ②
    foo - (1 < a)       ③
    foo - 1 < a         ④

表达式①等同于表达式②,但表达式③不同于表达式④。在表达式③中,首先对关系表达式1 < a求值,得到0或1,然后再将该值从foo中减去。而在表达式④中,首先是对foo减1,将得到的值再与a比较。

如果关系表达式中的两个操作数都是有符号的整数(整型数据、有符号寄存器数据或者十进制格式整数),那么表达式就应当被看做是两个有符号数的比较。如果关系表达式中的任意一个操作数为实型数据,那么另外一个操作数就应当首先被转换成实型数据,然后表达式就被看做是两个实型数据之间的比较。除以上两种情况外,表达式都被作为无符号数之间的比较。

8.相等操作符

相等操作符的优先级比关系操作符更低。表2-23中列出了相等操作符。

表2-23 相等操作符

这4个相等操作符有相同的优先级。相等操作符在比较两个操作数时是按位比较的,如果两个操作数的位宽不等,那么较小位宽的操作数在高位补 0。同关系操作符一样,如果比较关系为“假”,则表达式结果为值0,反之,比较关系为“真”,则结果为值1。

对于逻辑相等和不等操作符(==和!=),如果在操作数中有未知(x)或高阻(z)位,则比较关系不明确,比较的结果为 1 位的未知(x)值。对于全等或非全等操作符(===和!==),操作数中的未知(x)或高阻(z)位也应当纳入比较,从而比较的结果是一个明确的值,要么为0要么为1。

9.逻辑操作符

逻辑表达式的结果值可以是 0(定义为假)、1(定义为真)或者 x(逻辑关系不明确未知)。对于二元的逻辑操作符&&(逻辑与)和||(逻辑或),前者比后者有更高的优先级,但两者都比关系操作符和相等操作符的优先级低。对于单目的逻辑操作符!(逻辑非),它有最高的优先级。下面是逻辑操作符的应用举例。

【例2-46】逻辑操作符的应用。

(1)假定寄存器变量alpha保持整数值237,而beta保持值0:

    regA=alpha && beta;   // regA 被设置为0
    regB=alpha || beta;   // regB 被设置为1

(2)下面的表达式对三个子表达式进行逻辑与操作,而不需要任何的圆括号:

    a < size-1 && b != c && index != lastone

但是出于可读性考虑,加上圆括号可以使求值的优先级非常清晰,如下可这样重写上面的这个表达式:

    (a < size-1) && (b != c) && (index != lastone)

(3)逻辑非操作符(!)通常用在下面的结构中:

    if (!inword)

在某些情况下,使用逻辑非符号比使用相等操作符更能加深人的理解,比如下面的结构使用的是相等操作符,但是它实际上与上式是等效的。

    if (inword == 0)

10.按位操作符

按位操作符是将一个操作数中的一位与另一操作数中的对应位进行计算,从而得到结果的一位。这样逐位处理,直到完成操作数所有位的计算。如果两个操作数的位宽不等,那么较短的操作数应当在高位补0。表2-24至表2-28是按位运算结果的真值表。

表2-24 按位与操作结果真值表

表2-25 按位或操作结果真值表

表2-26 按位异或操作结果真值表

表2-27 按位异或非操作结果真值表

表2-28 单目取反操作结果真值表

11.规约运算符

单目规约运算符是在一个单独的操作数上按位进行运算,最终得到一个一位的结果值。对于规约与、规约或、规约异或操作,首先是将操作数的第一位与第二位进行相应的位运算。第二步及后续的步骤是,将上一步的一位结果值与操作数的下一位进行相应的位运算,以此类推,直到计算完操作数所有的位得到最终的结果值。对于规约与非、规约或非、规约异或非,只需要将规约与、规约或、规约异或操作的结果值分别求反即可。表2-29至表2-31是规约运算中位运算的真值表。

表2-29 规约与操作位运算真值表

表2-30 规约或操作位运算真值表

表2-31 规约异或操作位运算真值表

表2-32显示了规约运算符应用在不同操作数上的结果。

表2-32 规约运算操作结果

12.移位操作符

有两种类型的移位操作符:逻辑移位操作符<<和>>、算术移位操作符<<<和>>>。左移操作符<<和<<<应当将它们左边的操作数按照右边操作数指定的位数左移相应的位数,空闲位补 0。同理,右移操作符>>和>>>应当将它们左边的操作数按照右边操作数指定的位数右移相应的位数。逻辑右移后的空闲位补 0。而算术右移后的空闲位分两种情况进行处理:如果结果的类型为无符号型,则空闲位补 0;如果结果的类型为有符号型,则空闲位按照左边操作数的最高位(即符号位)值进行补充。如果操作符右边的操作数有一个未知(x)或高阻(z)值,则移位结果为未知(x)。结果的符号由左边操作数的符号和表达式的其余部分决定。

下面举例说明移位操作符的使用。

【例2-47】移位操作符的使用。

    module shift;
        reg [3:0] start, result;
        initial
        begin
        start=1;
        result=(start << 2);  //result的最终结果为二进制值0100
        end
    endmodule

    module ashift;
        reg signed [3:0] start, result;
        initial
        begin
        start=4'b1000;
        result=(start >>> 2); // result的最终结果为二进制值1110
        end
    endmodule

13.条件操作符

条件操作符也叫三目操作符,具有右结合性,带有三个操作数。其语法格式如下:

    conditional_expression ::=
        expression1 ? { attribute_instance } expression2 : expression3
    expression1 ::=
        expression
    expression2 ::=
        expression
    expression3 ::=
        expression

在条件表达式中,首先对表达式1进行求值。

(1)如果表达式1的值为“假”(0),那么接下来就求表达式3的值,并将表达式3的值作为最终条件表达式的值。

(2)如果表达式1的值为“真”(1),那么表达式2的值将被求出,并作为最终条件表达式的值。

(3)如果表达式1的值不明确(为x或z),那么表达式2和表达式3都应当被求值,然后将它们的结果根据表2-33按位计算,从而得到最终的条件表达式值;当然有一种情况例外,那就是表达式2和表达式3中任意一个结果为实型,则最终的条件表达式值为0。如果表达式2和表达式3的结果值位宽不同,则需要将位宽较短的操作数扩展到较长位宽操作数的位宽,即在其高位补0。

表2-33 条件不明确情况下条件表达式的结果

【例2-48】条件操作符的使用。

本例用一个三态输出总线说明条件操作符的普通用法。

    wire [15:0] busa=drive_busa ? data : 16'bz;

当drive_busa值为1时,由data驱动busa;如果drive_busa值为未知(x),则由一个未知值驱动busa;否则,busa不被驱动。

14.连接与复制操作符

连接操作符是将两个或更多表达式连接起来合并成一个表达式的操作符,用花括号{}将被连接的表达式括起来,括号中各表达式用逗号“,”分隔开。除了非定长的常量,任何表达式都可以进行连接运算。这是因为连接操作中每一个操作数都需要计算完整的连接位宽。下面举例说明连接操作符的使用。

【例2-49】连接操作符的使用。

    module concatTest;
      reg a;
      reg[1:0] b;
      reg[5:0] c;
      initial
      begin
            a=1'b1;
            b=2'b00;
            c=6'b101001;
            $displayb({a,b});       //产生一个三位数3'b100
            $displayb({c[5:3],a});  //产生一个四位数4'b1011
      end
    endmodule

连接符号的另外一种应用形式是复制操作。将一个表达式放入双重花括号“{{ }}”中,而复制因子放在第一层括号中,用来指定复制的次数。该复制符为复制一个常量或变量提供一种简便记法,如下例所示。

【例2-50】复制操作符的使用。

    module replicTest;
        reg a;
        reg[1:0] b;
        reg[5:0] c;
        initial
        begin
        a=1'b1;
        b=2'b00;
        $displayb({4{a}});   //结果为1111
        c={4{a}};
        $displayb(c);        //结果为001111
        end
    endmodule

2.11.2 操作数

在表达式中可以指定几类操作数。最简单的操作数类型是引用一个线网或变量型数据的完整形式,也就是给定的线网或变量名。在这种情况下,组成线网或变量值的所有位都被用做操作数。如果线网矢量、寄存器矢量、整型变量或时间型变量的某一位被引用,那么这样的操作数就称为位选择型操作数。如果线网矢量、寄存器矢量、整型变量或时间型变量的某几个连续位被引用,那么这样的操作数就称为部分位选择型操作数。一个存储器字也可被引用做操作数。操作数的合并(用连接操作符将几个操作数连接起来)也被看做是一个操作数。此外,一个函数的调用也被认为是操作数。

1.矢量的位选择和部分位选择的寻址

位选择是指从线网矢量、寄存器矢量、整型变量或时间型变量中选取一个特定位。这个特定位可以用表达式来寻址。如果位选择超出了寻址范围或者被选择的位的值是x或z,那么该位的引用返回值为 x。一个实型或实时时间型变量的位选择或部分位选择声明都是非法操作。

部分位选择中的几个连续位也可以被寻址。有两种形式的部分位选择:一是常量形式,二是索引形式。

常量形式的部分位选择语法格式如下:

    vect[msb_expr:lsb_expr]

方括号中两个表达式都是常量表达式。表达式msb_expr表示地址高位,表达式lsb_expr表示地址低位。如果部分位选择超出了寻址范围或者被选择的部分位的值为x或z,那么引用的返回值为x。

索引形式的部分位选择语法格式如下:

    reg [15:0] big_vect;
    reg [0:15] little_vect;
    big_vect[lsb_base_expr +: width_expr]
    little_vect[msb_base_expr +: width_expr]
    big_vect[msb_base_expr -: width_expr]
    little_vect[lsb_base_expr -: width_expr]

表达式 width_expr 是一个常量表达式,它不受运行时参数赋值的影响。表达式lsb_base_expr和msb_base_expr在运行时可以改变。前两个部分位选择中位地址起始于地址基址,并且逐渐增加,被选择的位的数目等于表达式width_expr的值。后两个部分位选择中位地址起始于地址基址,并且逐渐减少,被选择位的数目等于表达式width_expr的值。

部分位选择中超出寻址范围的位或者是被选择的位本身值为x或z,那么从这些位读出的值为 x;而如果对部分位进行写操作时,只对寻址范围内的位起作用,超出寻址范围的位不受影响。

下面举例说明位选择。

【例2-51】索引形式的位选择。

本例中,索引index将对矢量acc中的某一位进行寻址。

    reg [15:0] acc;
    reg [2:17] acc;
    acc[index]

【例2-52】索引形式的部分位选择。

    reg [31:0] big_vect;
    reg [0:31] little_vect;
    reg [63:0] dword;
    integer sel;
    initial begin
    if ( big_vect[0 +:8] == big_vect[7 : 0]) begin  end
    if (little_vect[0 +:8] == little_vect[0 : 7]) begin  end
    if ( big_vect[15 -:8] == big_vect[15 : 8]) begin  end
    if (little_vect[15 -:8] == little_vect[8 :15]) begin  end
    if (sel >0 && sel < 8)
    dword[8*sel +:8]=big_vect[7:0]; // 替换被选择的字节

下面举例说明位寻址中需要遵循的一些原则。

【例2-53】位寻址原则。

    reg [7:0] vect;
    vect=4;   //用二进制形式00000100对矢量vect赋值,最高位是第7位,最低位是第0位

● 如果addr值为2,那么vect[addr] 返回值为1。

● 如果addr值超出寻址范围,那么vect[addr] 返回值为x。

● 如果addr值为0、1或者3到7,则vect[addr] 返回值为0。

● vect[3:0]引用返回位的值为0100。

● vect[5:1]引用返回位的值为00010。

● vect[返回值为x的表达式]返回值为x。

● vect[返回值为z的表达式]返回值为x。

● 如果addr的任何一位为x或z,那么addr的值为x。

请读者注意,部分位选择时,如果索引值为x或z,则编译时可能报错;如果位选择或部分位选择超出了声明的寻址范围,则编译时也可能报错。

2.数组和存储器寻址

数组和存储器(一维寄存器数组)声明已经在前面的章节讨论过,现在将讨论数组的寻址。

存储器寻址的语法格式如下:

    mem_name[addr_expr]

地址表达式addr_expr可以是任意表达式,因此存储器可以被间接寻址,如下所示:

    mem_name[mem_name[3]]

在上式中,mem_name[3]是一个地址表达式,它表示存储器mem_name中地址为3的字。而该字的值又成为存储器mem_name的地址索引。与位选择一样,存储器声明中给定的寻址范围,决定了存储器地址表达式的作用范围。如果地址索引表达式超出了寻址范围或者地址索引中有任何的位值为x或z,则被引用的值将为x。

多维数组(比如二维或三维)的寻址语法格式如下:

    twod_array[addr_expr][addr_expr]                    //二维数组寻址
    threed_array[addr_expr][addr_expr][addr_expr]      //三维数组寻址

地址表达式addr_expr可以是任意表达式。

为了表示数组元素的位选择或部分位选择,需要首先选中数组元素,即需要为数组的每一维度指定一个地址。然后接下来的位选择或部分位选择与前面讲过的方式相同。

下面举例说明多维数组的位选择或部分位选择。

【例2-54】多维数组位寻址。

    twod_array[14][1][3:0]      //寻址一个二维数组字的低4位
    twod_array[1][3][6]         //寻址一个二维数组字的第6位
    twod_array[1][3][sel]       //使用索引变量寻址二维数组字中的某一位
    threed_array[14][1][3:0]    //对三维数组非法的位寻址

3.字符串

字符串操作数被看做由8位的ASCII代码构成的常量数字序列,每一个8位的ASCII字符对应一个数字。任何Verilog HDL操作符都可以处理字符串操作数。如果一个数值变量声明的位宽大于赋值值的位宽,那么赋值操作完成后,该变量的左端(高位)应当补0。对于字符串变量的赋值操作与此雷同。下面举例说明。

【例2-55】字符串变量的赋值。

    module string_test;
      reg [8*14:1] stringvar; //声明一个字符串变量,可以容纳14个ASCII字符
      initial
      begin
            stringvar="Hello world";
            $display("%s is stored as %h", stringvar, stringvar);
            stringvar={stringvar,"!!!"};
            $display("%s is stored as %h", stringvar, stringvar);
      end
    endmodule

仿真结果为:

      Hello world is stored as 00000048656c6c6f20776f726c64
    Hello world!!! is stored as 48656c6c6f20776f726c64212121

可见由于字符串"Hello world"只有11个字符,而声明的字符串变量stringvar可以容纳14个字符,所以,将在变量stringvar的高位补0(由于一个ASCII字符对应两位十六进制数字,所以在此补上6个十六进制数字0,对应三个ASCII字符)。而对于字符串的合并{stringvar,"!!!"}刚好为14个字符,所以赋值完成后不对stringvar的高位补0。

(1)字符串操作

对字符串通常有复制、合并、比较这些操作。复制是通过简单的赋值操作实现的。而合并是由连接操作符来实现。比较是由相等操作符来实现。当用寄存器矢量来处理字符串时,寄存器矢量的位数至少为8*n(n为ASCII字符的个数)位。

(2)字符串值的补充和潜在问题

当将字符串赋值给变量时,如果字符串的位宽小于变量的位宽,则应当在变量的左端(高位)补0。这种补0可能会影响比较和合并操作的结果。因为比较和合并操作符并不区分补充的0值和原始的字符串字符’\0’(ASCII码为0)。下面将举例说明这种潜在的问题。

【例2-56】字符串补0潜在问题。

    reg [8*10:1] s1, s2;
    initial
    begin
        s1="Hello";
        s2=" world!";
        if ({s1,s2} == "Hello world!")
        $display("strings are equal");
    end

本例中的比较操作返回结果值为“假”。因为在将字符串赋值给变量时,对变量的左端进行了补0,赋值完成后,变量s1和s2的结果如下:

    s1=000000000048656c6c6f
    s2=00000020776f726c6421

而合并操作的结果为:

    {s1,s2}=000000000048656c6c6f00000020776f726c6421

因为字符串"Hello world!"不包含0值补充,比较的结果将为0(即为“假”),这可用下面的示意图加以说明,如图2-12所示。

(3)空字符串处理

空字符串“”被看做等效于ASCII空字符“\0”(其值为0),而与字符串“0”不同。

图2-12 字符串补0潜在问题

2.11.3 延迟表达式

在Verilog HDL中,延迟表达式的格式为用圆括号括起来的三个表达式,这三个表达式之间用冒号(:)分隔开。三个表达式依次代表最小、典型、最大延迟时间值。具体的延迟表达式语法格式如下:

    constant_expression ::=
        constant_primary
        | unary_operator { attribute_instance } constant_primary
        | constant_expression binary_operator { attribute_instance } constant_expression
        | constant_expression ? { attribute_instance } constant_expression :
constant_expression
        | string
    constant_mintypmax_expression ::=
        constant_expression
        | constant_expression : constant_expression : constant_expression
    expression ::=
        primary
        | unary_operator { attribute_instance } primary
        | expression binary_operator { attribute_instance } expression
        | conditional_expression
        | string
    mintypmax_expression ::=
        expression
        | expression : expression : expression
    constant_primary ::
        constant_concatenation
        | constant_function_call
        | ( constant_mintypmax_expression )
        | constant_multiple_concatenation
        | genvar_identifier
        | number
        | parameter_identifier
        | specparam_identifier
    primary ::=
        number
        | hierarchical_identifier
        | hierarchical_identifier [ expression ] { [ expression ] }
        | hierarchical_identifier [ expression ] { [ expression ] } [ range_expression ]
        | hierarchical_identifier [ range_expression ]
        | concatenation
        | multiple_concatenation
        | function_call
        | system_function_call
        | constant_function_call
        | ( mintypmax_expression )

下面举例说明延迟表达式的用法。

【例2-57】延迟表达式的使用。

    (a:b:c) + (d:e:f)   //最小延迟值为a+d的和,典型延迟值为b+e的和,最大延迟值为
                        //c+f的和
    val - (32'd 50: 32'd 75: 32'd 100)

2.11.4 表达式的位宽

为了对表达式求值时得到可靠的结果,控制表达式的位宽是非常重要的。在某些情况下采取最简单的解决办法,比如如果两个16位的寄存器矢量的位排序方式和操作被指定,那么结果就是一个 16 位的值。然而在某些情况下,究竟有多少位参与表达式求值或者结果有多少位,并不是显而易见的。例如两个16位操作数之间的算术加法,是应该使用16位求值还是该使用 17 位(允许进位位溢出)求值?答案依靠被建模设备的类型以及那个设备是否处理进位位溢出来决定。Verilog HDL利用操作数的位宽来决定有多少位参与表达式的求值。

1.表达式位宽规则

控制表达式位宽的规则已经公式化,因此在大多数实际情况下,都有一个简单的解决方法。表达式位宽是由包含在表达式内的操作数和表达式所处的环境决定的。自主表达式的位宽由它自身单独决定,比如延迟表达式。环境决定型表达式的位宽由该表达式自己的位宽和它所处的环境(实际上该表达式本身又是另外一个表达式的一部分)来决定,比如一个赋值操作中右侧表达式的位宽由它自己的位宽和赋值符左侧的位宽来决定。表 2-34中说明了表达式的形式如何决定表达式结果的位宽。在表2-34中,i、j、k都表示单操作数的表达式,而L(i)代表表达式i的位宽,op代表操作符。

表2-34 表达式位宽规则

2.表达式位宽问题举例

在表达式求值过程中,中间结果应当采用具有最大位宽操作数(如果是在赋值语句中,也包括赋值符的左侧)的位宽。在表达式求值过程中要注意避免丢失数据的重要位。下面举例说明。

【例2-58】表达式位宽问题举例1。

    reg [15:0] a, b, answer; // 声明三个16位寄存器矢量
    answer=(a + b) >> 1;   //不能正确达到目的

上式的目的是a与b相加(可能导致溢出),然后再右移1位,以保存进位位到一个16位的寄存器answer中。这样问题就来了:因为表达式中所有操作数都是16位位宽,因此表达式(a + b)产生的中间结果也仅仅是16位位宽,这样在执行右移1位操作之前,就已经丢失了进位位。解决办法是使表达式(a + b)按照至少17位位宽来求值。例如,在表达式(a + b)中加上整数值0,则表达式求值时将按照整数的位宽来执行,如下所示。

    answer=(a + b + 0) >> 1; //能够正确达到目的

【例2-59】表达式位宽问题举例2。

    module bitlength();
        reg [3:0] a,b,c;
        reg [4:0] d;
        initial
        begin
            a=9;
            b=8;
            c=1;
            $display("answer=%b", c ? (a&b) : d);
        end
    endmodule

仿真结果为:

    answer=01000

对于表达式(a&b),应当属于环境决定型表达式,它本身的位宽为4,但它又是条件表达式的一部分,所以应当采用最大位宽操作数d的位宽5作为自己的实际位宽。

3.自主表达式举例

【例2-60】自主表达式举例。

    module self_determine;
      reg [3:0] a;
      reg [5:0] b;
      reg [15:0] c;
      initial
      begin
            a=4'hf;
            b=6'ha;
            $display("a*b=%x",a*b);     //表达式a*b为自主表达式
            c={a**b};                 //由于操作符号{}的存在
            $display("a**b=%x", c);     //表达式a**b成为自主表达式
            c=a**b;                   //表达式a**b位宽由c决定
            $display("c=%x", c);
      end
    endmodule

本例的仿真输出结果如下:

    a*b=16      //由于表达式a*b的位宽为6,所以相乘结果96被截短为16
    a**b=0021  //表达式{a**b}位宽为16(c的位宽)。由于a**b是自主表达式,它的位宽
                 //为6,所以其值为21,经过赋值操作后,在表达式左边补0,从而得到最
                 //终结果0021
    c=ac61       //表达式a**b位宽为16

2.11.5 有符号表达式

为了得到可靠的结果,控制表达式的符号是非常重要的。除了后面概括的规则,两个系统函数$signed( )和$unsigned( )也应当用于处理表达式的符号类型。这两个函数对输入表达式求值,然后返回一个同输入表达式位宽相同、大小相同的值,但是返回值的符号类型由系统函数决定。

● $signed:返回值为有符号型。

● $unsigned:返回值为无符号型。

下面举例说明。

【例2-61】系统函数$signed( )和$unsigned( )的应用。

    reg [3:0] regA;
    reg signed [3:0] regS;
    regA=$unsigned(-4);    // regA=4'b1100
    regS=$signed(4'b1100); // regS=-4

1.表达式符号类型规则

● 表达式的符号类型仅仅依靠操作数,与LHS(左侧)值无关。

● 简单十进制格式数值是有符号数。

● 基数格式数值是无符号数,除非符号(s)用于基数说明符(例如4'sd12)。

● 无论操作数是何类型,其位选择结果为无符号型。

● 无论操作数是何类型,其部分位选择结果为无符号型,即使部分位选择指定了一个完整的矢量。

【例2-62】部分位选择结果的符号类型。

    reg [15:0] a;
    reg signed [7:0] b;
    initial
    a=b[7:0];     // 表达式b[7:0]为无符号型,因此在a的高位用0补充

● 无论操作数是何类型,连接(或复制)操作的结果为无符号型。

● 无论操作数是何类型,比较操作的结果(1或0)为无符号型。

● 通过类型强制转换为整型的实数为有符号型。

● 任何自主操作数的符号和位宽由操作数自己决定,而独立于表达式的其余部分。

● 对于非自主操作数遵循下面的规则;

➢ 如果有任何操作数为实型,则结果为实型;

➢ 如果有任何操作数为无符号型,则结果为无符号型,而不论是什么操作符;

➢ 如果所有操作数为有符号型,则结果为有符号型,而不论是什么操作符。

2.表达式求值步骤

● 根据前述表达式位宽决定规则,确定表达式位宽。

● 根据表达式符号规则,确定表达式符号。

● 将表达式中每一操作数(自主操作数除外)的符号类型强制转换为表达式的符号类型。

● 将表达式中每一操作数(自主操作数除外)的位宽扩展到表达式位宽。当且仅当操作数为有符号型(在符号类型强制转换之后)时,对其进行符号扩展。

3.对赋值表达式求值的步骤

● 根据赋值表达式位宽决定规则,确定LHS(左侧)位宽。

● 如果有必要,对LHS(左侧)位宽进行扩展。当且仅当LHS为有符号型时,对其进行符号扩展。

4.在有符号表达式中如何处理x和z

如果一个有符号操作数的位宽被扩展到更大的位宽,而其符号位值为 x,那么结果值的空闲位应当用x进行补充;如果其符号位值为z,那么结果值的空闲位应当用z进行补充。如果一个有符号值的任意一位为x或z,那么包含该值的任何非逻辑性操作都将产生一个完整的结果值x,而该结果的符号类型与表达式的符号类型一致。

2.12 本章小结

本章主要介绍了Verilog HDL语言编程基础知识,包括:Verilog HDL语言特点、Verilog HDL程序基本结构、数值、字符串、标识符、编译指令、数据类型、表达式等内容。读者通过学习,可以熟悉Verilog HDL程序的基本结构和设计特点。由于本章涉及了许多概念,因此分清各个概念也是学习的一个重点。