第2章 VHDL语言编程基础
FPGA程序设计主要采用两种语言:VHDL和Verilog HDL。其中Verilog HDL比较简单,在NIOS项目里运用较多;而VHDL语言功能强大,学习起来有一定难度,在数字系统和通信系统设计中使用较多。本章将重点介绍FPGA硬件描述的主流语言VHDL。
2.1 FPGA系统的表示方法
FPGA数字系统可使用不同复杂度(系统级、电路板级、芯片级、门级)的电路网络在同一抽象程度上描述,有系统级、寄存器级、门级3种不同层次。
在传统可编程逻辑器件PLD的设计方式中,设计者在CAD工作站上以基本的电路组件来组织并构成一个完整的逻辑功能,这些最基本的电路组件如AND逻辑门、OR逻辑门或缓存器在CAD工作站的屏幕中以线条和图案来表示,每个组件都必须清楚定义输出/输入信号和功能。在硬件描述语言中,这种所谓的Symbol不存在,而以Entity的描述来代替它。
图2-1就是一个D型缓存器的Symbol和它所对应Entity描述。Entity在是构成所有设计的基础,设计者以Entity中的描述来定义一个设计中的所有输出/入信号。在一个阶层式(Hirearchical Design)的设计中,TOP-LEVEL设计整体是一个Entity,而往下每一阶层线路的描述,则包含在各个不同的LOWER-LEVEL的Entity中。
图2-1 DFR Entity
在上面这个例子中,R、D、和C这3个信号被定义为输入,而且只能表现出“0”和“1”两种逻辑态(BIT是定义在“0”和“1”两种值的Data Type)。而Q和XQ两个信号则被指定为输出,同样只有“0”和“1”双态。在Entity这个阶段中,并不定义内部线路的功能。
那么如何来表现一个Entity所含的功能?
在一个传统的阶层式设计中,任一阶层的Symbol,它的功能是以深藏在它下面的实际线络(Schematic)来表示的,而担任这项工作的即是Architecture描述。
在Architecture里有各式各样的Statement,这些Statement涵盖底层的门级GATE-LEVEL、次低层的RTL-LEVEL(Register Transfer Level)和顶层的行为级BEHAVIOR-LEVEL描述,可以很完整地表现一个设计的所有功能。
图2-2是一个四选一的MUX线路,包括Symbol、Schematic及Entity和Archite Cture描述。在图中的Archite Cture使用的是Condition Assignment,根据A和B两个信号的值不同,Q输出的值被指定为I0或I1或I2或I3。
图2-2 4MUX线路
基本上,在同一时间中只有一个程序代码被执行。但在真实FPGA硬件里,许多彼此不相关的信号变化或运算可以平行进行(Concurrent),为了能充份表现硬件线路的所有特性,允许Sequential和Concurrent两种不同的描述存在。描述硬件线路的方法不是唯一的,同样的线路往往有多种不同的表示法,如何选择最有效率的描述,是设计者极为重要的一项课题。
总的来说,FPGA设计一般采用自顶向下、由粗到细、逐步求精的方法。设计顶层是指系统的整体要求,底层是指具体的逻辑电路实现。自顶向下是将数字系统的整体逐步分解为各个子系统和模块,若子系统规模较大则进一步分解为更小的子系统和模块,层层分解,直至整个系统中各子模块关系合理、便于设计实现为止。
2.2 VHDL语言的特点
VHDL(VHSIC Hardware Description Language)是一种标准的硬件描述语言,中文意思就是超高速集成电路硬件描述语言。
利用VHDL进行系统行为级设计已成为FPGA与ASIC设计的主流。在电子产业界,无论ASIC设计人员,还是系统级设计人员,大都会通过学习VHDL来提高工作效率。由于VHDL所具有的通用性,它已成为可支持不同层次设计者需求的标准语言。使用VHDL,不仅可以快速地描述和综合FPGA设计,还可以提供如下所述的一些性能。
● 功能与灵活性:VHDL具有功能强大的语言结构,可用简洁明确的代码描述来进行复杂控制逻辑的设计。为了有效控制设计的实现,它还具有多层次的设计描述功能,支持设计库和可重复使用的元件生成,它支持阶层设计,且提供模块设计的创建。VHDL是一种设计、模拟、综合的标准硬件描述语言。
● 不依赖于器件的设计:VHDL允许设计者生成一个设计而并不需要首先选择一个用来实现设计的器件。对于同一个设计描述,可以采用多种不同器件结构来实现其功能。若需对设计进行资源利用和性能方面的优化,也并不是要求设计者非常熟悉器件结构才行。相反,可以集中精力从事设计构思(当然,这并不是说设计者可以忽略电路结构,诸如数据通路优化设计方面的需要)。
● 可移植性:VHDL的可移植能力(Portability)是允许设计者对需要综合的设计描述进行模拟,在综合前对一个数千门的设计描述进行模拟可以节约设计者可观的时间。在这时发现的设计上的瑕疵就能够在设计实现之前给予纠正。因为VHDL是一个标准语言,故VHDL的设计描述可以被不同的工具所支持。从一个模拟工具移植到另一个模拟工具、从一个综合工具移植到另一个综合工具、从一个工作平台移植到另一个工作平台去执行。这意味着同一个VHDL设计描述可以在不同的设计项目中采用。在某个EDA工具中构成的技术诀窍,在其他工具中同样可以采用。
● 性能评估的能力:非依赖器件的设计(Device-Independent Design)和可移植能力允许设计者可采用不同的器件结构和不同的综合工具来评估设计。在设计者开始设计之前,无须了解将采用何种器件,是CPLD还是FPGA。设计者可以进行一个完整的设计描述,并对其进行综合,生产选定的器件结构的逻辑功能。然后评估结果,选用最适合设计需求的器件。
● 上市时间快、成本低:VHDL语言的设计将大大提高数字单片化设计实现速度。VHDL语言使设计描述快捷、方便,促进设计的快速复制简便易行。同时,多种综合工具支持这种形式的设计。VHDL和可编程逻辑的组合作为一类强有力的现场集成设计方式,将为设计者的产品上市带来创记录的速度。
2.3 VHDL语言结构与要素
一个完整的VHDL语言程序通常包含实体(Entity)、构造体(Architecture)、配置(Configuration)、包集合(Package)和库(Library)5个部分。前4种是可以分别编译的源设计单元。实体用于描述设计的系统外部接口信号;构造体用于描述系统内部的结构和行为;包集合存放各设计模块都能共享的数据类型、常数和子程序等;配置用于从库中选取所需单元来组成系统设计的不同版本;库用于存放已经编译的实体、构造体、包集合和配置。库可由用户生成或由ASIC芯片制造商提供,以便于在设计中为大家所共享。
下面是一个表现VHDL语言程序结构的例子,从这个例子中可以大致看出用VHDL语言编程的特点及结构。其中最前面是使用的库文件的声明,形如:
LIBRARY _library_name; USE _library_name._package_name.ALL;
这一部分库文件是为包集合体使用而声明的。
在库文件声明后面是包集合体,包集合体包含声明部分和集合体实现部分。包集合体声明部分是“PACKAGE _package_name IS”与“END _package_name”之间的部分,其中可以包含信号定义、常数定义、数据类型、元件语句、函数定义和过程定义等。在包集合体声明后面是包集合体,即:
PACKAGE body _package_name IS ...... END _package_name;
包集合体给出了在声明中各项的具体实现细节。在包集合体之后还是一部分对所使用库的声明语句,这部分语句是给实体使用的,还声明了用户自定义库。
接下来是VHDL程序的主体部分:实体和结构体。实体定义了ASIC的输入/输出端口的名称和性质,它以“ENTITY _entity_name IS”开头,以“END _entity_name”结尾。结构体以“ARCHITECTURE a OF _entity_name IS”开头,以“END a”结束,ASIC的绝大部分功能都是在结构体中实现的。在结构体的最后部分是配置语句,如:
CONFIGURATION CONFIGURATION_NAME OF _entity_name IS ...... END CONFIGURATION_NAME;
设计者可以利用配置语句来选择不同的构造体,使其与要设计的实体相对应。
以下是一个完整的VHDL程序结构。
--库 LIBRARY _library_name; USE _library_name._package_name.ALL; ...... --包集合体 PACKAGE _package_name IS -- Type Declaration -- Subtype Declaration -- Constant Declaration -- Signal Declaration -- Component Declaration -- Function Declaration END _package_name; PACKAGE body _package_name IS ...... END _package_name; LIBRARY _library_name; USE _library_name._package_name.ALL; --调用包声明 ...... USE WORK._package_name.ALL ENTITY _entity_name IS --实体 GENERIC(_parameter_name : string := _default_value; _parameter_name : integer:= _default_value); PORT(_input_name, _input_name : INSTD_LOGIC; _input_vector_name: IN STD_LOGIC_VECTOR(high downto _low); _bidir_name, _bidir_name: INOUT STD_LOGIC; _output_name, _output_name: OUT STD_LOGIC); END _entity_name; ARCHITECTURE a OF _entity_name IS --构造体 SIGNAL _signal_name : STD_LOGIC; SIGNAL _signal_name : STD_LOGIC; BEGIN -- Process Statement -- Concurrent Procedure Call -- Concurrent Signal Assignment -- Conditional Signal Assignment -- Selected Signal Assignment -- Component Instantiation Statement -- Generate Statement END a; CONFIGURATION CONFIGURATION_NAME OF _entity_name IS ...... --配置 END CONFIGURATION_NAME;
以上的例子就是VHDL的基本结构,在文件存盘前,任一VHDL设计程序(代码)都必须给予正确的文件名。一般地,文件名可以由设计者任意给定,但具体取名最好与文件实体名相同;文件后缀扩展名必须是“.vhd”,如ADDER_F.vhd。但考虑到某些EDA软件的限制和VHDL程序的特点,即在元件(例如语句中的被调用文件)调用中,其元件名与文件名是等同的,因此建议,程序的存盘的文件名应该与该程序的实体名一致,如以上例子的文件名应该是:_entity_name.vhd(文件名不分大小写)。
2.3.1 实体说明
任何一个基本设计单元的实体ENTITY都具有如下的结构:
ENTITY <entity_name> IS Generic Declarations Port Declarations END <entity_name>;
一个基本设计单元的实体说明以“ENTITY <entity_name> IS”开始,以“END<entity_name>”结束。在VHDL中,大写或小写都不区分,故ENTITY等同与entity。
1. 类属参数说明
generic语句称为类属说明语句,该语句必须放在指定的端口说明之前,用于向模块传递参数,该语句语法为:
GENERIC(_parameter_name : string := _default_value; _parameter_name : integer:= _default_value);
其中_parameter_name是参数名,string和integer是参数类型,VHDL定义了很多参数类型,类属变量可以采用的参数类型受编译环境的限制,依编译器不同而不同。_default_value是初始化值,即传递给模型的参数。
下面是一个类属参数说明的例子,该例很明白地表示了generic语句的用法。
ENTITY <entity_name> IS Generic(constant tplh , tphl : time := 5ns --常量为5ns tphz, tplz : time := 3ns; default_value : integer := 1; cnt_dir : string := “up”); Port Declarations END <entity_name>;
这其中的常量说明CONSTANT是假设的,并且generic语句中无须写CONSTANT关键字。
2. 端口说明
端口说明是对基本设计实体(单元)与外部接口的描述,也可以说是对外部引脚信号的名称、数据类型和输入/输出类型的的描述。端口说明的语法结构为:
PORT(_input_name, _input_name: IN STD_LOGIC; _input_vector_name: IN STD_LOGIC_VECTOR(_high downto _low); _bidir_name, _bidir_name : INOUT STD_LOGIC; _output_name, _output_name: OUT STD_LOGIC);
其中_input_name和_output_name是端口名,即赋予每个外部引脚的名称。端口名可以由字母、数字和下画线组成,但只能由字母开头,下画线不能放在开头和结尾。
IN、INOUT、OUT是端口方向,即信号是输入还是输出。图2-3给出了VHDL端口方向的说明及其含义。
图2-3 VHDL端口模式
如图2-3所示,表明IN的信号只能输入,不能输出,而表明OUT的信号只能输出,不能输入,INOUT则既能输出,又能输入,BUFFER类似于OUT,但其信号可被ASIC内部利用,而OUT则不行。
OUT允许对应多个信号,而BUFFER只允许对应一个信号;当一个构造体用“BUFFER”说明输出端口时,与其连接的另一个构造体的端口也要用“BUFFER”说明,对于“OUT”则没有这种要求。
STD_LOGIC、STD_LOGIC_VECTOR是端口的数据类型。VHDL中有10数据类型,具体将在后面讲述。端口常用数据类型为BIT与BIT_VECTOR, BIT的取值只能是“0”或“1”, BIT_VECTOR是一组二进制值。如果一个端口是8位的总线,那么它就可以定义为8位的BIT_VECTOR。STD_LOGIC与STD_LOGIC_VECTOR是BIT与BIT_VECTOR的标准库文件的表示方法,两者是完全等效的,如果使用STD_LOGIC与STD_LOGIC_VECTOR,需要在程序的开始加入以下语句:
Library IEEE; Use ieee.std_logic_1164.all;
以便在编译VHDL语句时,从指定库的包集合寻找数据类型的定义。
下面一段程序是端口定义的使用例子:
ENTITY <entity_name> IS Generic Declarations Port ( signal clk : in bit; q : out bit); END <entity_name>;
该例定义了两个端口,一个输入端口clk是bit类型的,一个输出端口q是bit类型的。关键字signal在这里不是必需的。
2.3.2 构造体
构造体Architecture是一个基本设计单元的主体,它具体定义了设计单元的功能。构造体对其基本设计单元的输入/输出关系可以用3种方式进行描述,即行为描述(基本设计单元的数学模型描述)、结构描述(逻辑元件连接描述)和以上两种方法混合使用。不同的描述方式,只体现在描述语句上,而构造体的基本结构是完全一样的。
构造体必须跟在实体的后面,一个实体可以有多个构造体,构造体的具体结构如下:
ARCHITECTURE <identifier> OF _entity_name IS SIGNAL _signal_name : STD_LOGIC; SIGNAL _signal_name : STD_LOGIC; BEGIN -- Process Statement -- Concurrent Procedure Call -- Concurrent Signal Assignment -- Conditional Signal Assignment -- Selected Signal Assignment -- Component Instantiation Statement -- Generate Statement END <identifier>;
可以看到结构体以“ARCHITECTURE <identifier> OF _entity_name IS”语句开始,以“END <identifier>”语句结束。一般,结构体总是以RTL、BEHAV或STRUCTURE命名,这样的程序便于阅读与交流。
1.定义语句
定义语句位于ARCHITECTURE和BEGIN之间,用于对构造体的内部所使用的信号、常数、数据类型和函数进行定义,语法为:
-------信号类型 信号名称 :数据类型 :=初始值;--------- ARCHITECTURE <identifier> OF <entity_identifier> IS signal temp : integer := 1; constant load : boolean := true; BEGIN Process Statements Concurrent Procedural calls Concurrent Signal assignment Component instantiation statements Generate Statements END <architecture identifier> ;
信号定义需要有信号名和数据类型的说明,和端口不同的是,信号的定义不需要定义方向。在上例中定义了两个内部信号,第一个是整型信号,初始值为1,第二个信号是布尔型常量load,初始值为true。
2.并行处理语句
并行处理语句处于BEGIN和END之间,这些语句具体地描述了构造体的行为及其连接关系。由于并行语句是并行执行的,其书写可以不分顺序。
3.process语句
process语句是使用在Architecture中的并行处理语句的一种,一个结构体中可以有多个process进程语句,这些进程语句之间是并行执行的,但每一个进程语句内部的语句是顺序执行的,由这些process语句组成了Architecture的主体,进而实现了ASIC的功能。Process语句的语法如下:
_process_label: PROCESS (_signal_name, _signal_name, _signal_name) SIGNAL _variable_name : STD_LOGIC; VARIABLE _variable_name : STD_LOGIC; BEGIN -- Signal Assignment Statement -- Variable Assignment Statement -- Procedure Call Statement -- If Statement -- Case Statement -- Loop Statement END PROCESS _process_label;
在一个process语句内部可以定义内部信号,这些信号在process语句外是无法使用的,在process中的信号可以是SIGNAL,也可以是VARIABLE, SIGNAL可以应用在很多场合,而VARIABLE只能用在process、funtion和procedure语句中。
在Process关键字的后面的括号中有一个或几个信号量。这些信号量是process语句的输入信号,在VHDL中也称为敏感量,无论这些信号中哪一个发生变化,都会触发process语句。process语句启动后,将其内部语句顺序执行一遍,然后继续等待敏感量的变化,以触发下一次process语句。
一个Architecture中可以有多个process语句,这些process语句之间是并行执行的,而且它们可以一边互相通信,一边并行的同步执行,各进程的信号互为条件。下面是具有两个process语句的Architecture的例子。
LIBRARY ieee; USE ieee.std_logic_1164.all; ENTITY if_case IS PORT ( a, b, c, d : IN STD_LOGIC; sel : IN STD_LOGIC_VECTOR(1 DOWNTO 0); y, z : OUT STD_LOGIC); END if_case; ARCHITECTURE behav OF if_case IS BEGIN if_label: PROCESS(a, b, c, d, sel) --process for y BEGIN IF sel="00" THEN y <= a; ELSIF sel="01" THEN y <= b; ELSIF sel="10" THEN y <= c; ELSE y <= d; END IF; END PROCESS if_label; case_label: PROCESS(a, b, c, d, sel) --process for z BEGIN CASE sel IS WHEN "00" =>z <= a; WHEN "01" =>z <= b; WHEN "10" =>z <= c; WHEN "11" =>z <= d; WHEN OTHERS =>z <= '0' ; END CASE; END PROCESS case_label; END behav;
图2-4是这个程序的仿真,Entity中定义了a、b、c、d、sel、y、z这6个信号,a、b、c、d、sel是输入信号,y、z是输出信号,在Architecture中有两个process语句if_label和case_label,分别给y和z赋值。a、b、c、d、sel是两个process语句的敏感量,它们中的一个改变,将触发两个process语句。
在一个结构体中可以有多个进程语句,各进程语句之间并行执行,一个进程语句内部可以包括若干语句,这些语句是顺序执行的。
图2-4 if_case仿真图
2.3.3 VHDL语言要素
VHDL语言要素比较多,包括库、包集合、标识符、数据对象、数据类型、操作符等,下面一一叙述。
1.库
库是经编译后的数据的集合,它存放包集合定义、实体定义、构造体定义和配置定义。在VHDL语言中,库的说明总是放在设计单元的最前面。这样,在设计单元内的语句就可以使用库中的数据。由此可见,库的好处就在于可以使设计者共享已经编译过的设计结果。
使用库的语法为:
LIBRARY library_name; USE library_name.package_name.ITEM.name;
在IEEE库中包含以下4个包集合。
● std_logic_1164(标准逻辑类型和相应函数)。
● std_logic_arith(数学函数)。
● std_logic_signed(符号数学函数)。
● std_logic_unsigned(无符号数学函数)。
LIBRARY IEEE; USE IEEE. std_logic_1164.ALL; USE IEEE. std_logic_arith.ALL; USE IEEE. std_logic_signed.ALL; USE IEEE. std_logic_unsigned.ALL;
用户自定义的包集合都存放在WORK库中,其使用方法如下:
LIBRARY WORK; --WORK为用户定义包 USE WORK.<package name>.all;
库说明语句的作用范围从一个实体说明开始到它所属的构造体、配置为止。当一个源程序中出现两个以上的实体时,每个实体前都应有相应的库说明。
2.包集合
包集合用来定义在VHDL语言中所要用到的信号定义、常数定义、数据类型、元件语句、函数定义和过程定义等,它是一个可以编译的单元,也是库结构中的一个层次。如果要使用一个包集合,在程序的开始要加上如下语句:
USE LIBRAYR_NAME.PACKAGE_NAME.ALL;
其中“LIBRAYR_NAME”是所在的库文件名称,“PACKAGE_NAME”是包集合名称,而“USE”与“ALL”则是关键字,例如:
USE IEEE.STD_LOGIC_1164.ALL;
表示在VHDL程序中要使用IEEE库中名为STD_LOGIC_1164的包集合中的定义。
一个包集合由包集合定义和包集合体两部分组成,包集合定义中给出信号定义、常数定义、数据类型、元件语句、函数定义和过程定义等,而包集合体则给出各项的具体实现。包集合必须要有包集合定义,但包集合体不是必需的,包集合语法结构如下:
PACKAGE <package_name> IS --包集合1 Constant Declarations Type Declarations Signal Declarations Subprogram Declarations Component Declarations END <package_name> ; PACKAGE BODY <package_name> IS --包集合2 Constant Declarations Type Declarations Subprogram Body END <package_name>
包集合定义由“PACKAGE <package_name> IS”语句开始,由“END <package_name>”语句结束,“<package_name>”是包集合的名称。在包集合定义中可以声明程序中要用到的常量、信号、数据类型、元件和子程序定义等。
包集合体由“PACKAGE BODY <package_name> IS”开始,由“END <package_name>”语句结束,“<package_name>”是包集合的名称。在包集合体中具体定义在包集合定义中声明的子程序的具体实现,下面是一个具体的包集合的例子。
LIBRARY ieee; USE ieee.std_logic_1164.all; PACKAGE filt_cmp IS --包集合filt_cmp constant N1:integer:=24; COMPONENT acc port(xh : in std_logic_vector(10 downto 0); clk, first: in std_logic; yn : out std_logic_vector(11 downto 4)); END COMPONENT; FUNCTION compare (variable a , b : integer) RETURN boolean; END filt_cmp; PACKAGE BODY filt_cmp IS FUNCTION compare (variable a , b : integer) IS VARIABLE temp : boolean; Begin If a < b then temp := true ; Else temp := false ; end if; RETURN temp ; END compare ; END fily_cmp ;
在这个例子中定义了包集合“fily_cmp”,在包集合定义中声明了常量“N1”、元件(COMPONENT)“acc”和子函数“compare”,而在包集合体中则具体定义了子函数“compare”的实现。关于COMPONENT和子程序,将在后面介绍。
3.配置
配置语句描述层与层之间的连接关系及实体与结构之间的连接关系。设计者可以利用这种配置语句来选择不同的构造体,使其与要设计的实体相对应,也可以使元件与实体—结构体相对应。配置语句被广泛应用于仿真,它提供了一个可变的快速的交互设计的方式,但在综合时,该语句不被支持。配置语句的基本语法如下:
CONFIGURATION <identifier> OF <entity_name> IS FOR <architecture_name> END FOR; END <identifier>;
配置以“CONFIGURATION <identifier> OF <entity_name> IS”语句开始,以“END<identifier>”语句结束,“<identifier>”是配置名,“<entity_name>”是实体的名称,“<architecture_name>”是与实体相对应的结构体的名称,“CONFIGURATION”、“FOR”和“END”是关键字。下面是ENTITY、ARCHITECTURE、CONFIGURATION联合使用的例子。
LIBRARY ieee; USE ieee.std_logic_1164.all; ENTITY cmpl_sig IS PORT ( a, b, sel : IN bit; x, y, z : OUT bit; END cmpl_sig; ARCHITECTURE logic OF cmpl_sig IS BEGIN x <= (a AND NOT sel) OR (b AND sel); y <= a WHEN sel='0' ELSE b; WITH sel SELECT z <= a WHEN '0' , b WHEN '1' , '0' WHEN OTHERS; END logic; CONFIGURATION cmpl_sig_conf OF cmpl_sig ISFOR logic END FOR; --与结构体对应的配置 END cmpl_sig_conf;
在这个例子中,定义了实体“cmpl_sig”,它由a, b, sel这3个输入,x、y、z这3个输出,都是bit类型的。在结构体“logic”中给出了该电路的具体实现,BEGIN与END之间的3个语句是并行语句,具体用法后面详细讲述。最后由配置语句将结构体“logic”与实体“cmp1_sig”相对应。在该程序中只有一个实体与一个结构体,所以即使不用配置语句,“logic”与“cmp1_sig”也是一一对应的,这个例子说明了CONFIGURATION的用法。
4.标识符
在VHDL中对标志符(Identifiers)的规定有以下几点。
● 在标志符中允许的合法字符仅含26个英文大、小写字母及下画线。
● 标志符的第一个字母必须是英文字母。
● “_”不能是标志符的最后一个字符。在标志符中不能有两个相连的下画线。
● 在标志符中的英文字母不分大小写。
● 在VHDL程序中的说明文字开头一律为两个连续的“--”,注释可以在任意语句后面,也可以是独立行。
● 保留字(Reserved words)或关键字不能用做标志符。
在VHDL中的保留字和关键字如表2-1所示。
表2-1 VHDL关键字
5.数据对象
VHDL语言中的数据对象(Data Objects)主要包括以下3种:信号、变量和常数(Signal、Variable、Constant)。3种数据对象的含义和说明场合如表2-2所示。
表2-2 VHDL数据对象含义及说明场合
1)信号
信号定义的格式如下:
signal 信号名[,信号名…]:数据类型[:=表达式]; ------------示例--------------- signal sys_clk : bit :='0' ; signal ground : bit :='0' ;
信号包括输出/输入引脚信号及IC内部缓冲信号,有硬件电路与其相对应,故信号之间的传递有实际的附加延时。在实体中定义为外接输出/输入引脚信号,在architecture语句与begin语句之间定义为全局(Global)信号。信号与变量的形式几乎相同,只有一项重要的差异:信号可以用来存储或传递逻辑值,因此可被集成为存储器件或数据总线,而变量则不能。信号能在不同进程之间传递,变量则不能。信号赋值语句可定义为:
signal1 <= signal2 ;
2)变量
变量定义的格式如下:
--variable变量名[,变量名…]:数据类型[:=表达式]; -- VARIABLE x, y :INTEGER; VARIABLE count :INTEGER RANGE 0 TO 255 :=0;
变量并不对应到任何输出/输入引脚,而只是用来作为指针或存储程序中计算用的暂时值,故变量之间的传递是瞬时的,并无实际的附加延时。变量仅能出现在process, if_loop,Function等语句中,用于暂时运算,其值仅局部(Local)有效,要先指定给信号,才能将其最终值传出进程。
变量可在定义时赋初始值,也可在程序任何合法的地方重新赋值。变量的赋值语句为:
variable1 := variable2 ;
变量在赋值时不能产生附加延时,下式产生延时的方式是不合法的:tmp3:= tmp1 AFTER 10ns。
3)常数
常数定义的格式如下:
constant常数名[,常数名…]:数据类型:=表达式;
常数一旦被赋值,在整个程序中将不再改变,例如:
constant bus_width:integer:=8;
定义了bus_width为一个整型数常量,值为8。
4)计数指针
计数指针本身是一种整数类型,但不对应任何信号,只是在程序中用做计数器。故不需说明数据类型(正整数),如下面例子中的指针I。
for I in 7 downto 0 loop reg(I)<="00000000"; end loop;
但如果用于运算则应加以说明,例如:
process variable I:integer; begin I:=0; while I<8 loop reg(I)<="00000000"; I:=I+1; end loop; cnd process;
5)数据对象定义的例外
在VHDL中大部分的数据类型必须事先说明,才能使用,而且可以用“:=”运算符赋初值。但是有些数据例外,如下。
● IC器件的输出/输入引脚已隐含定义为信号,不用再说明。
● IC器件的参数值(Generics)已隐含定义为常数,不用再说明。
● 格式化函数(Function)参数必须是常数或信号,而且已在函数说明中隐含定义。
● 循环(Loop)或生成(Generate)语句开始时,其指针(Index)的数据类型已隐含定义了,而在语句结束后,则自动消失。
6)属性
属性用来表示信号的某种特性,例如:
signalA:std_logic_vector(3 to 12); --定义A A' left =3 --调用A的结果 A' right =12 A' high =12 A' low =3 A' length =10 A' range =3 to 12
“event”属性表示信号内涵改变,例如:
if(clk' event and clk=' l' )
该语句表示假如clk信号发生变化且现值为1,即clk发生上升沿触发,亦可改写成:
if rising_edge(clk)
6.数据类型
1)预定义的数据类型(Pre-defined data types)
● 位bit:在数字系统中,信号值通常用一个位来表示,位允许的数值为“0”或“1”。
● 布尔量boolean:一个布尔量只有true与false两种状态,“真”或“假”。布尔量不能进行算术运算,只能进行关系运算。例如,它可以在if语句中被测试,测试结果产生一个布尔量true或false。
● 位矢量bit_vector:位矢量由一串用双引号括起来的位组成,可以按递增或递减方式排列,用来表示总线的状态,如下。
signal a, b:bit_vector(0 to 3); signal c:bit_vector(3 downto 0) :="1100";
首先说明了a、b为4位数据总线。再以递减方式定义c(3)='1' , c(2)='1' , c(1)='0' , c(0)='0'。在VHDL中,单一位用单引号(' ')标明,而位矢量的常数则用双引号(" ")标明。若程序中赋值语句为c<=O"8",则表示c="001000",而O表示八进制。此时在定义信号c时,必为6位的位矢量,否则会出现错误。若程序中赋值语句为a<=X"34",则表示a="00110100",而X表示十六进制,此时在定义信号a时,必为8位的位矢量,否则会出现错误。
● 字符character:字符允许的数据内容为128个标准ASCII字符,字符量通常用单引号引起来,如’A'。一般情况下VHDL对大小写不敏感,但是对字符量中的大小写字符则认为是不一样的。
● 字符串string:字符串由一串字符构成,如下。
signal ksut:string(1 to 9):=Kung-Shan;
定义了字符串ksut=Kung-Shan,其中ksut(1)=K, ksut(9)=n。
● 标准逻辑(std_logic):STD_LOGIC数据类型定义如下。
TYPE STD_LOGIC IS (' U' , ' X' , '0' , '1' , ' Z' , ' W' , ' L' , ' H' , ' -' ) ;
以上定义的9种数据的含义是:' U’为未初始化;' X’为强未知;'0’为强逻辑0; '1’为强逻辑1; ' Z’为高阻态;' W’为弱未知的;' L’为弱逻辑0; ' H’为弱逻辑1; ' -’为忽略。它们较完整地概括了在数字系统中所有可能的数据表现形式。在实际IC集成时,只允许’0' 、'1' 、' Z'和’-'4种数据内容。'0’与’1’出现在程序中的任何地方,不受限制。' -’表示不关心输出,不能用在“if then else”和“case”语句中。一个矢量的最左位被认定为最高位(MSB)。在IEEEll64中定义’Z’为高阻输出。
在仿真和综合中,将信号或其他数据对象定义为STD_LOGIC数据类型是非常重要的,它可以使设计者精确地模拟一些未知的和具有高阻态的线路情况。对于综合器,高阻态’Z'和’-’忽略态(有的综合器对’X')可用于三态的描述。但就目前的综合器而言,STD_LOGIC型数据能够在数字器件中实现的只有其中的4种值,即’X'(或’-')、'0' 、'1’和’Z'。
● 标准逻辑矢量std_logic_vector:标准逻辑矢量跟位矢量几乎相同,只是数据内容多了’Z’与’-’两种。' Z’表示电路的高阻状态,仅能指定给最外层电路输出,并且必须对应到IC的实际引脚上,例如:
signal bus:std_logic_vector(7 downto 0); begin bus<="ZZZZZZZZ" ;
此段语句说明并指定输出总线bus全为高阻状态。' Z’可以指定给变量,而且可用在比较中,如:if a=' Z' then…。' Z’亦可以放在函数中调用。在使用标准逻辑和标准逻辑矢量的时候,一定要有相应的库声明。
● 整数integer:整数与数学中整数的定义相同,整数的表示范围为-2147483648~2147483647,即从-231~231-1。编译器将整数型操作数的位宽设为32位,只是在电路集成时,会去除不使用的位,如下。
signal count:integer range 0 to 1023;
此语句定义全局信号count的值为0~1023,编译器会自动将正整数值解码成对应的位矢量。又如:
count_A<=conv_std_logic_vector(counter,10);
此语句将整数counter转换成10位的位矢量count_A。不要把一个实数赋给一个整数变量,因为VHDL是一个强类型语言,它要求在赋值语句中的数据类型必须匹配。
2)用户自定义的数据类型(User-definable data types)
用户自定义的数据类型是以上述8种基本数据类型为基础而定义的数据类型。用户定义的数据类型的定义书写格式是:
TYPE数据类型名 {,数据类型名} 数据类型定义;
子类型(subtypes)通常用于一种数据对象的范围检查(Range Checking)或强制约束(Enforcing Constraints),可以是其他数据类型的一个部分集合。子类型定义的一般语法为:
SUBTYPE子类型名IS数据类型名 [范围]; subtype byte is std_logic_vector(7 downto 0);
定义byte是具有递减顺序的8位标准逻辑矢量的数据类型,各变量或信号的数据类型被指定为byte时,则其内容被限制为8位标准逻辑矢量。
Subtype single_digit is integer range 0 to 9;
定义子类型single_digit是1位整数,可能范围0~9,当变量或信号的数据类型被指定为single_digit时,则其数据内容可能是0、1、2、3、4、5、6、7、8、9中的任何一个数。
Type arith_op is (add, sub, mul, div); subtype add_op Is arith_op range add to sub; subtype mul_op is arith_op range mul to div;
首先定义为枚举式数据类型arith_op,它是一个四则运算符的集合。然后分别定义两组子类型add_op及mul_op, add_op包括add及sub两个运算符,mul_op包括mul及div两个运算符。
除上述外,子类型还常用于存储器阵列等的数组描述的场合。新构造的数据类型及子类型通常定义在包集合中,再由use语句装载到描述语句中。
3)复合数据类型(Composite data types)
复合数据类型是由其他数据类型的元素组合而成的,有两种复合数据类型:数组(array)及记录(record)。
● 数组array:数组是相同类型元素的集合,数组间可以互相从属(部分集合关系),数组可以是一维或多维的。其元素属于同一种数据类型,每一个元素都可以用数组指针标识出来。
数组定义的书写格式:
TYPE数据类型名IS ARRAY范围OF原数据类型名; type big_word is array(0 to 63) of std_logi;
定义数据类型big_word为一维的64位标准逻辑数组。
Type matrix_type is array(0 to 15,0 to 31) of std_logic;
定义类型matrix_type为16×32的二维标准逻辑数组。非限制性数组的指针范围是无限的,且为该类型的连续集合。
Type std_logic vector is array(integer range<>) of std_logic;
由此可知,标准位矢量是由标准位延伸定义而来的。新的数据类型可用array的形式定义延伸出来。
Type value_type is array(0 to 127) of integer;
定义数据类型value_type是含有128个整数的数组。
● 记录record:记录可将不同类型元素合成一种新的数据类型,记录中的元素其数据类型可以不是VHDL预先设定的,而是在程序中临时定义的。记录数据类型的定义格式为:
TYPE数据类型名IS RECORD 元素名:数据类型名; 元素名:数据类型名; … END RECORD; --示例-- type opcode is (add, sub, mul, div) type instruction is record operator:opcode; opl:integer; op2:integer; end record:
此段程序以枚举式数据类型定义opcode为一个四则运算符的集合。定义instruction为一种记录,表示—个单运算符及双操作数所组成的表达式,其中操作数的数据类型为整数,运算符为opcode四则运算符中的任何一个,于是可以指定变量instrl及instr2为这种类型的记录,如:
variable instrl, instr2:instruction; instrl.Operator:=add; instr2.Operator:=sub; instrl.opl:=instr2.op2;
定义记录instrl的运算符为加法add, instr2的运算符为减法sub,并将instr2的第2个操作数赋值给instrl的第1个操作数。
4)枚举式数据类型
枚举式数据类型是混合不同数据类型的元素,组合出特殊的数据类型,也可将相同类型元素以枚举方式组成一种新的数据类型。不同枚举可以包含相同元素,但调用时必须标明该枚举的名称,例如A' (blue)、B' (blue)。编译器先将集合中元素的位置顺序编码,再据此识别枚举式数据类型,并以集合涵盖的方式定义一种新的特殊数据类型,很适合用来定义状态机的所有状态,定义格式如下:
type名称is (值 [,值…]);
数据内容出现的顺序会影响关系运算符的运算结果。在括号中,由左至右,位置数依次为0、1、2…。
type arith_op is (add, sub, mul, div);
定义了一组四则运算符集合arith_op,集合内容按枚举方式逐项列出。这是一种使用方便的数据类型。
type states is (state0, state1, state2, state3);
定义了一个状态机states的所有可能状态:state0、statel、state2、state3。
5)数据类型转换
在VHDL中为了实现正确的代入操作,必须将要代入的数据进行类型转换。变换函数可以使用VHDL的包集合中定义的类型转化函数。
在“STD_LOGIC_1164”、“STD_LOGIC_ARITH”、“STD_LOGIC_UNSIGNED”包集合中提供了一些类型变换函数,使用时应先把相应的库声明和包集合使用语句写上,例如:
signal a:std_logic_vector(0 to 7); signal b:integer:=10; begin b<=CONV_INTEGER(a); --将位矢量a转换成整数b …
7.运算操作符
运算符(Operators)的种类包含数值运算、赋值(Assignment)与连接运算。数值运算以运算优先权的高低分为逻辑(Logical)、关系、加法,乘法及其他运算符。赋值(Assignment)运算符可以完成数据赋值操作,连接(Association)运算符可用来连接两种数据类型。IEEE 1076标准只支持+、-、×、/、=、>=及**,且其操作数只能是整数或位矢量,常见的运算符如表2-3所示。
表2-3 常见运算符及其功能
运算符(Operator)表示运算的方式,而操作数(Operand)则为运算的数据。可运算的操作数其值可由编译器决定,如指针数组元素、属性、函数调用与表达式。以下简述各种运算符的特性。
1)逻辑运算符
逻辑运算符只能用于bit、boolean及std_logic 3种类型的数据,而且运算符之间无优先权之分,仅能用括号来决定运算顺序,否则会产生错误,例如:
o<=b and c or d; --是不合法的 a<=(b and c) or d; --是正确的
当然也有例外,如果逻辑表达式中只有“and”、“or”、“not”运算符,那么改变运算顺序将不会导致逻辑的改变,而且所有逻辑运算符中,“not”优先级最高,例如:
a<=b AND c AND d AND e;
2)算术运算符
“+”, “-”及“&”3种运算符支持位及位矢量两种数据类型。“+”及“-”为传统整数加、减法,必须调用位算术程序包(bit arith package)才能运算。并置运算符“&”可用来连接两组字符、字符串、位、位矢量或标准逻辑矢量。假如两个操作数为位矢量,则运算结果为一个更大的位矢量,其长度是两个操作数长度之和。单一位可以看做长度为1的位矢量。
乘法运算符“*”可以处理两个整数或位矢量的乘法,其积的数据类型与操作数相同。“Abs”可以计算整数的绝对值。“**”可以求解整数的平方值。“+”、“-”运算符两边的操作数和代入的变量的位长应该相同,否则会出错。另外,“*”运算符两边的位长相加后的值和要代入的变量的位长不相同时,也会出现语法错误。
3)关系运算符
关系运算符用来判断两个具有相同数据类型操作数之间的相等、不相等及大小关系,其结果为布尔量。在关系运算符中,小于等于符“<=”和代入符“<=”是相同的,在读VHDL语言的语句时,应按照上下文的关系来判断此符号到底是关系符还是代入符。
4)赋值运算符
赋值运算符用来给信号赋值,可以出现在任何地方,例如:
signal vec1, vec2:bit_vector(0 to 3); vee2<=vecl; test<=(0=>'1' others=>'0' );
此为判别性的整集内涵赋值。test的第1位的值为1,而其他3位的值均为0。
如果要给变量赋值,只能在进程中来做。复合数据类型赋值可以给一组数据进行多值一次性赋值,可同时对一个变量或信号内的所有数据赋值。
Type opcode is (add, sub, mul, div); Type instruction is record operator:opcode; opl, op2:integer; end record; variable stop:instruction; begin stop<=(add,5,10);
此例同时将运算符add及两个操作数5、10赋给变量stop。
5)并置运算符
并置运算符“&”用于位的连接。例如,将4个位用并置运算符“&”连接起来就可以构成一个具有4位长度的位矢量。两个4位的位矢量用并置“&”连接起来就可以构成8位长度的位矢量,例如:
signal a:std_logid; signal b: std_logic_vector(3 downto 0); begin b<=a&a&a&a;
6)连接运算符
在调用集成电路组件时,必须使用连接的方式来指定该电路组件引脚与其对应的端口信号的连接通路。最常用方法是通过引脚图(Port Map)或属性图(Generic Map)连接。VHDL支持命名(Named)与定位(Positional)两种形式的连接。命名连接方式是采用“=>”将命名的电路实体的引脚直接指向定义实体的信号。“=>”符号总是指向实际的定义实体的信号,例如:
sto:dsrff port map(d=>data, s=>set, r=>rst, clk=>clk, q=>qout);
此例将D型触发器的所有引脚d、s、r、clk及q(这些字符为默队值,不能改变)指定连接到目前程序中所定义实体的信号端data、set、rst、clk及qout。d、s、r、clk及q出现的顺序是自由的。
定位连接的方式在实体调用语句中直接按照顺序将程序定义实体的端口信号定位列出,同时说明其输入特性及数据类型,而不需再列出调用实体的形式引脚名称,例如:
component jkff port (j, k, clk:in bit , q: out bit); endcomponent:
命名连接方式可以不用记忆jkff原先引脚的定义顺序,但仍然要知道其引脚名称:
jkl:jkff port map(j=>j_in, k=>k_in, clk=>clk, q=>q_out); jk2:jkff port map(j_in, k_in, clk, q_out); --两者功能相同
2.3.4 VHDL顺序语句与并发语句
1.顺序语句
顺序语句(Sequential Statements)只能出现在过程、进程及函数中,其中所有的命令语句是按顺序执行的,因此语句出现的顺序很重要。而整个流程、程序或函数本身则被视为一个整体、一个并行的语句。在VHDL中的顺序执行语句主要有变量赋值、条件语句、循环语句、选择语句和等待语句等,本节详细讲述这几种语句用法。
1)条件语句
条件语句(if)是根据所指定的条件来确定执行哪些语句的,其条件自上而下判断,第一个为真的条件后相应的顺序语句将被执行,如果所有的条件都是假的,那么else后的语句将被执行。其书写格式通常可以分为3种类型。
● if then语句的语法为:
IF <条件> THEN 顺序执行语句 END IF;
例如下面D触发器的VHDL描述:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity dff1 is port(clk :in std_logic; d:in std_logic; q:out std_logic); end dff1; architecture rtl of dff1 is begin process(clk) begin if(clk' event and clk='1' )then q<=d; --触发输出 end if; end process; end rtl;
这个例子是一个D触发器的RTL描述,在时钟的上升沿,输入数据被送到输出端,并被锁促。该例是一个典型的时钟上升沿使用方法,时钟下降沿的使用方法是:
if(clk' event and clk='0' )then
● if then else语句的语法为:
IF <条件> THEN 顺序执行语句 ELSE 顺序执行语句 END IF;
例:二选一电路,输入为a与b,输出为c,选择控制端为sel,如图2-5所示。
图2-5 二选一电路
library ieee; use ieee.std_logic_1164.all; entity mux2to1 is port(a, b, sel:in std_logic; --sel片选信号 c:out std_logic); end mux2to1; architecture rtl of mux2to1 is begin process(sel, a, b) begin if(sel='0' )then c<=a; --sel=0,选择a else c<=b; --否则选择b end if; end process; end rtl;
其仿真如图2-6所示。
图2-6 二选一电路仿真图
● if then elsif语句的语法为:
IF <条件1> THEN 顺序执行语句 ELSIF <条件2> THEN 顺序执行语句 ELSE 顺序执行语句 END IF;
例:三选一电路,当sela为高电平时输出a,当sela为低电平且selb为高电平时输出b,否则输出c,如图2-7所示。
图2-7 三选一电路
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity mux3to1 is port(a , c:in std_logic; b :in std_logic; sela, selb:in std_logic; --片选信号 q:out std_logic); end mux3to1; architecture rtl of mux3to1 is begin PROCESS(sela, selb, a, b, c) BEGIN IF sela='1' THEN --根据sela/b输出a/b/c q <= a; ELSIF selb='1' THEN q <= b; ELSE q <= c; END IF; END PROCESS; end rtl;
其仿真如图2-8所示。
图2-8 三选一电路仿真图
2)选择语句
case语句用来从许多不同的语句之中选择其一执行,CASE语句书写格式如下:
CASE _expression IS WHEN _constant_value => _statement; WHEN OTHERS => _statement; END CASE;
例:四选一电路,输入信号为a、b、c、d, sel为选择信号,输出为q,如图2-9所示。
图2-9 四选一电路
library ieee; use ieee.std_logic_1164.all; entity mux4to1 is port(a, b, c, d:in std_logic; sel:in std_logic_vector(1 downto 0); --2位的选择信号 q:out std_logic); end mux4to1; architecture rtl of mux4to1 is begin process(sel, a, b, c, d) BEGIN CASE sel IS --根据sel的值选择a/b/c/d WHEN "00" =>q <= a; WHEN "01" =>q <= b; WHEN "10" =>q <= c; WHEN OTHERS =>q <= d; END CASE; END PROCESS; end rtl;
其仿真如图2-10所示。
图2-10 四选一电路仿真图
3)循环、等待语句
loop语句与高级语言中的循环语句一样,使程序迭代执行。循环语句有for loop和while loop两种,loop语句采用next语句来跳出本次循环,用exit语句来结束循环。
● for loop语句的语法为:
_loop_label: FOR _index_variable IN _range LOOP _statement; END LOOP _loop_label;
例如8位奇偶校验电路的VHDL描述。
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity jioujiaoyan is port(a:in std_logic_vector(7 downto 0); q:out std_logic); --奇偶校验输出 end jioujiaoyan; architecture rtl of jioujiaoyan is begin PROCESS(a) variable tmp:std_logic; BEGIN tmp:='0' ; for i in a' high downto a' low loop tmp:=tmp xor a(i); --异或,按位对a进行奇偶校验 end loop; q<=tmp; END PROCESS; end rtl;
其仿真如图2-11所示。
图2-11 奇偶校验仿真图
● while loop语句的语法为:
_loop_label: WHILE _boolean_expression LOOP _statement; END LOOP _loop_label;
例如8位奇偶校验电路的VHDL描述,注意其与for loop语句的不同。
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; entity jioujiaoyan1 is port(a:in std_logic_vector(7 downto 0); q:out std_logic); --奇偶校验输出 end jioujiaoyan1; architecture rtl of jioujiaoyan1 is begin PROCESS(a) variable tmp:std_logic; BEGIN tmp:='0' ; i:='0' ; while (i < a' high ) loop --用While Loop语句实现奇偶校验 tmp:=tmp xor a(i); i:=i+1; end loop; q<=tmp; END PROCESS; end rtl;
● 等待语句:wait语句使程序产生等待,直到条件满足再继续执行。wait语句可以设置4种不同条件:无限等待、时间到、条件满足、敏感信号量变化,其书写格式为:
WAIT --无限等待 WAIT ON --敏感信号量变化 WAIT UNTIL --条件满足 WAIT FOR --时间到
例如:
WAIT UNTIL _clk_name = '1'
4)function语句
function语句与procedure语句的最大区别在于它是顺序语句,且一次只返回一个值。函数要先在包集合声明中声明,然后在包集合体中定义细节。
在VHDL语言中,函数语句的书写格式如下:
FUNCTION函数名(参数1,参数2, …) RETURN数据类型名IS 定义语句 BEGIN 顺序处理语句 RETURN返回变量名 END 函数名
在VHDL语言中,function语句中括号内的所有参数都是输入,参数或称输入信号。因此在括号内指定端口方向的“in”可以省略。function的输入值由调用者复制到输入参数中,若没有特别指定,则在function语句中按常数处理,例如加法器的VHDL描述。
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; package dp16 is function add(a:std_logic_vector; --在包集合里声明 b:std_logic_vector) return std_logic_vector; variable tmp:std_logic_vector(15 downto 0); begin tmp:=a+b; return tmp; end add; end dp16; --函数体结束 library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; use work.dp16.all; entity myrisc is port(instruction:in std_logic_vector(1 downto 0); a:in std_logic_vector(7 downto 0); b:in std_logic_vector(7 downto 0); c:out std_logic_vector(15 downto 0)); end myrisc; architecture behav of myrisc is begin process(instruction, a, b) begin case instruction is when "00"=> c<=add(a, b); --调用函数体 when others=>c<="0000000000000000"; end case; end process; end behav;
其仿真如图2-12所示。
图2-12 myrisc仿真图
2.并行语句
VHDL中的并行语句(Concurrent Statements)包括进程语句、并发信号代入语句、条件信号代入语句、选择信号代入语句、并发过程调用语句、块语句和元件例化语句等。本节将详述并发过程调用语句、块语句和元件例化语句。
1)进程、信号代入语句
● 进程语句:process语句在前面已经提及并多次用到,这里强调几点process语句的特点,在一个结构体中可以有多个process语句,这些语句是并行执行的;在进程结构中的所有语句都是顺序执行的;进程间通过信号传递来实现通信。
● 并发信号代入语句:前边已经讲述了信号代入语句的用法,需要注意的一点是独立的信号代入语句是并行语句,而在一个进程中的多条信号代入语句则是顺序执行语句。
● 条件信号代入语句:条件信号代入语句直接用于结构体中,根据不同条件将相应表达式代入信号量,语法为:
_label: _signal <=_expression when _boolean_expression else _expression when _boolean_expression else _expression;
when后面的表达式是条件,当相应的条件为真时,when前面的表达式被代入信号量,如果所有条件都不满足,则代入最后else后面的表达式,例如8-3编码器的描述。
library ieee; use ieee.std_logic_1164.all; entity coder8_3 is port(sel:in std_logic_vector(7 downto 0); q:out std_logic_vector( 2 downto 0)); end coder8_3; architecture behav of coder8_3 is begin q<="000" when sel="11111110"else--q根据条件sel代入 "001" when sel="11111101"else "010" when sel="11111011"else "011" when sel="11110111"else "100" when sel="11101111"else "101" when sel="11011111"else "110" when sel="10111111"else "111" when sel="01111111" else "XXX"; end behav;
其仿真如图2-13所示。
图2-13 8-3编码器仿真图
● 选择信号代入语句:选择信号代入语句类似case语句,它对表达式进行测试,当表达式取值不同时,将使不同的值代入信号量,语法为:
_label: WITH _expression SELECT _signal <= _expression WHEN _constant_value, _expression WHEN _constant_value, _expression WHEN _constant_value, _expression WHEN _constant_value;
when后面所跟的表达式是条件,当相应的条件为真时,when前面的表达式被带入信号量。最后一个when后面可以用others关键字,如果所有条件都不满足,则代入最后一个when前面的表达式,例如以下8-3编码器的VHDL描述。
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity coder8_3_1 is port(sel:in std_logic_vector(7 downto 0); q:out std_logic_vector( 2 downto 0)); end coder8_3_1; architecture behav of coder8_3_1 is begin with sel select q<="000" when "11111110" , --q根据sel选择代入 "001" when "11111101", "010" when "11111011", "011" when "11110111", "100" when "11101111", "101" when "11011111", "110" when "10111111", "111" when "01111111", "XXX" when others; end behav;
2)并发过程调用语句
并发过程调用语句可以出现在构造体中,而且是一种可以在进程之外执行的过程中调用语句。本节将详细讲述过程的结构和书写方法,以及过程调用语句的用法。
过程与函数一样需要先在包集合声明中声明,然后在包集合体中具体定义实现,过程语句的书写结构为:
PROCEDURE过程名(参数1,参数2, …)IS [定义语句];(变量等定义) BEGIN [顺序处理语句];(过程的语句) END过程名;
在procedure结构中,参数可以是输入也可以是输出。也就是说,过程中的输入/输出参数都应该列在过程名后的括号内。
并发过程调用语句可以出现在进程语句中,并发过程调用语句是一个完整的语句,在它的前面可以加标号。并发过程调用语句应该带有IN、OUT或者INOUT的参数,它们应列在过程名后的括号内。并发过程调用可以有多个返回值,但这些必须通过过程中所定义的输出参数带回。
例如以下的类型转换,该例仅说明procedure语句用法,并无实际功能。
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; package dp16 is procedure int_to_bits(int:in integer; bits:out std_logic_vector); end dp16; package body dp16 is procedure int_to_bits (int : in integer; bits:out std_logic_vector(31 downto 0)) is variable temp : integer; --包集合中声明 variable result : std_logic_vector(31 downto 0); begin temp:=int; result:=conv_std_logic_vector(temp,32); bits:=result; end int_to_bits; end dp16; --并发过程体结束 library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; use work.dp16.all; entity myrisc2 is port(instruction:in std_logic_vector(1 downto 0); a:in std_logic_vector(31 downto 0); c:out std_logic_vector(31 downto 0)); end myrisc2; architecture behav of myrisc2 is begin process(a) variable q:integer; --将位矢量a转换成整数类型给q variable b:std_logic_vector(31 downto 0); begin case instruction is when "00" =>q:=conv_integer(a); int_to_bits(q, b); --调用函数 c<=b; when others=>c<= "00000000000000000000000000000000"; end case; end process; end behav;
3)块语句
block语句是一个并发语句,而它所包含的一系列语句也是并发语句,而且块语句中的并发语句的执行与次序无关,block语句的书写格式一般为:
标号 :BLOCK 块头 {说明语句}; BEGIN {并发处理语句); ENDBLOCK标号名;
在这里,块头主要用于信号的映射及参数的定义,通常通过generic语句、generic map语句及port语句和port map语句来实现。
说明语句与构造体的说明语句相同,主要是对该块所要用到的客体加以说明,可说明的项目如下。
● USE子句。
● 子程序说明及子程序体。
● 类型说明。
● 常数说明。
● 信号说明。
● 元件说明。
例如3-8译码器与8-3编码器的混合ASIC的VHDL描述。
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity coder_decoder_8_3 is port(a:in std_logic_vector(7 downto 0); b:in std_logic_vector(2 downto 0); y:out std_logic_vector(7 downto 0); q:out std_logic_vector( 2 downto 0)); end coder_decoder_8_3; architecture behav of coder_decoder_8_3 is begin coder8_3:block --8-3编码器 begin q<="000" when a="11111110" else "001" when a="11111101"else "010" when a="11111011"else "011" when a="11110111"else "100" when a="11101111"else "101" when a="11011111"else "110" when a="10111111"else "111" when a="01111111"else "XXX"; --其他情况 end block coder8_3; decoder8_3:block begin y<="11111110"when b= "000"else "11111101"when b= "001"else "11111011"when b= "010"else "11110111"when b= "011"else "11101111"when b= "100"else "11011111"when b= "101"else "10111111"when b= "110"else "01111111"when b= "111" else "XXX"; --其他情况 end block decoder8_3; end behav;
其仿真如图2-14所示。
图2-14 编码/译码仿真图
4)元件例化语句(Component Instantiation)
在构造体的结构描述中,component语句是基本的描述语句。该语句指定了构造体中所调用的是哪一个现成的逻辑描述模块,component语句的语法为:
COMPONENT _component_name GENERIC(_parameter_name : string := _default_value; _parameter_name : integer := _default_value); PORT(_input_name, _input_name : IN STD_LOGIC; _bidir_name, _bidir_name: INOUT STD_LOGIC; _output_name, _output_name: OUT STD_LOGIC); END COMPONENT;
其中_component_name是逻辑描述模块的名称。
在结构体中引用已经存在的模块时,应该先用component语句声明所引用的元件,然后用COMPONENT_INSTANCE语句将元件的端口信号映射成高层设计电路中的信号。
COMPONENT_INSTANCE语法为:
_instance_name: _component_name GENERIC MAP(_parameter_name => _parameter_value , _parameter_name => _parameter_value) PORT MAP (_component_port => _connect_port, _component_port => _connect_port);
其中_instance_name为实例名,_component_name是引用的元件名,_component_port是引用的元件的端口名,_connect_port是实例的信号名称,也可简写为:
_instance_name: _component_name GENERIC MAP(_parameter_value , _parameter_value) PORT MAP(_connect_port, _connect_port);
2.3.5 描述方式
使用VHDL语言对硬件系统进行描述,可以采用3种不同风格的描述方式,即行为描述方式、RTL描述方式和结构描述方式。这3种描述方式从不同的角度对硬件系统进行行为和功能的描述。
1.行为描述
行为描述方式是对系统数学模型的描述,其抽象程度比寄存器传输描述方式和结构化描述方式更高。在行为描述方式的程序中大量采用算术运算、关系运算、惯性延时、传输延时等难于进行逻辑综合和不能进行逻辑综合的VHDL语句。一般来说,采用行为描述方式的VHDL语言程序主要用于系统数学模型的仿真或者系统工作原理的仿真。
在VHDL语言中存在一些专门用于描述系统行为的语句,它们是VHDL语言为什么能在高层次上对系统硬件进行行为描述的原因所在!这些语句与一般的高级语言的语句有较大差别。
行为描述方法只定义电路的功能,并不定义电路的结构,也没有具体实现硬件的目的,它只是为了综合的目的而使用的一种描述方法。
1)代入语句
代入语句是典型的行为描述语句。代入语句最普遍的格式为:
--信号量<=敏感信号量表达式;-- a<=b;
该语句的功能是a得到b的值。当该语句有效时,现行信号b的值将代入到信号a。只要b的值有一个新的变化,那么该语句将被执行。所以,b是该代入语句的一个敏感量。
2)延时语句
VHDL语句种的延时语句是典型的行为描述语句,在后面讲述的MAX+plusⅡ编译环境里面,延时语句并不报错,但该语句不能进行综合,所以在程序中出现该语句是无效的。
几乎所有的硬件电路都存在一定时间的延时,因此,硬件电路的设计人员为了逼真地仿真硬件电路的实际工作情况,在代入语句中总要加上惯性延时的说明,例如:
a<=b after 10ns ;
该语句的功能是a得到b的值,但有一个延时。当该语句有效时,现行信号b的值将在10ns后代入到信号a。
3)generic语句
generic语句常用于不同层次之间的信息传递。例如,在数据类型说明上用于位矢量长度、数组的位长及器件的延时时间等参数的传递。该语句所设计的数据除整数类型以外,如涉及其他类型的数据将不能进行逻辑综合。因此,该语句主要用于行为描述方式。使用generic语句易于使程序模块化和通用化。generic语句的语法为:
GENERIC(信号名:数据类型);
例如分频器(偶数分频)的VHDL描述:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity fredivn is generic (N:integer); port (clk:in std_logic; outclk:out std_logic); --分频后脉冲 end fredivn; architecture behav of fredivn is signal count:integer; begin process(clk) begin if(clk' event and clk='1' ) then --将clk信号N分频 if(count=N-1)then count<=0; else count<=count+1; if count<(integer(N/2)) then --产生分频后时钟 outclk<='0' ; else outclk<='1' ; end if; end if; end if; end process; end behav;
这个例子是一个分频器的例子,可以对输入时钟进行偶数分频,注意在实体声明中,generic语句中的参数N是分频参数,把频率分为几分之一就由这个参数决定。在其他对这个模块进行调用的程序中,只要把N赋予不同的参数值,就可以实现不同的分频。下面是对fredivn进行调用的例子。
package test_con is constant N1:integer:=16; --16分频 end test_con; use work.test_con.all; library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity frediv16 is port(clkin :in std_logic; clkout:out std_logic); end frediv16; architecture behav of frediv16 is component fredivn --调用分频函数 generic(N:positive); port (clk:in std_logic; outclk:out std_logic); END component; begin u1:fredivn --匹配端口、参数 generic map(N=>N1) port map(clkin, clk1); end behav;
在该例子中给N赋值16,实现了一个16分频的分频器。可以看到,通过generic语句可以实现同一模块的不同调用,非常方便。
其实VHDL语言的结构体的3种描述方法的区别不是特别明显,通常是混合使用的,就描述方法的侧重点不同,行为描述方法主要是描述电路的功能,并不涉及内部如何实现,因此类似此种描述方法的都可看成是行为描述方法。
2.RTL描述
RTL描述方式是一种明确规定寄存器描述的方法。由于受逻辑综合的限制,在采用RTL描述方式时,所使用的VHDL语言的语句有一定限制。在RTL描述方式中要么采用寄存器硬件的一一对应的直接描述,要么采用寄存器之间的功能描述。
RTL的英文全名是Register Transfer Level,它是一种可以综合的描述方法,综合过程是先把HDL翻译成电路的模式,然后再进行优化,最终达到一个门阶段的应用。
以下四选一电路实现的功能就是RTL描述的一个例子。它实现的如图2-15所示电路,输入信号为a、b、c、d, sel为选择信号,输出为q。
图2-15 四选一电路
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity mux4to1 is port(a, b, c, d:in std_logic; sel:in std_logic_vector(1 downto 0); --2位的选择信号 q:out std_logic); end mux4to1; architecture rtl of mux4to1 is begin PROCESS(sel, a, b, c, d) BEGIN CASE sel IS --根据sel的值选择a/b/c/d WHEN "00" =>q <= a; WHEN "01" =>q <= b; WHEN "10" =>q <= c; WHEN OTHERS =>q <= d; END CASE; END PROCESS; end rtl;
3.结构描述
所谓构造体的结构描述方式,就是在多层次的设计中,直接用门电路设计单元来构成一个复杂的逻辑电路的描述方法。结构描述方式最能提高设计效率,它可以将已有的设计成果,方便地用到新的设计中去。它主要是描述电路的功能和结构,它是由顶层模块对底层模块的调用来实现的,此描述方式实现的电路可以综合。
以下例子就是结构描述方式的一个例子。选择器常用于信号的切换,三选一电路可以用于3路信号的切换,根据信号选择端sel值不同,选择不同的输入信号进行输出。如图2-16所示的三选一电路,结合VHDL特点,使用component元件例化语句、port map语句,可以用2个二选一电路构成1个三选一电路。同理,可以用3个二选一电路构成1个四选一电路,4个二选一电路构成1个五选一电路……这是TOP DOWN设计方法的雏形。
图2-16 三选一电路
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity mux3to1_1 is port(a, c :in std_logic; b :in std_logic; sel1, sel2:in std_logic; --两选通信号 q:out std_logic); end mux3to1_1; architecture rtl of mux3to1_1 is signal d:std_logic; COMPONENT mux2to1 port(a, b:in std_logic; sel:in std_logic; c:out std_logic); END COMPONENT; begin u1: mux2to1 PORT MAP (a, b, sel1, d); --输出d口作为u2的输入 u2: mux2to1 PORT MAP (c, d, sel2, q); end rtl;
以上例子用了构造体的结构描述方式,即在多层次的设计中,高层次的设计模块调用低层次的设计模块,用简单的设计单元构成一个较复杂的逻辑电路。这种方法结构清晰,可以提高设计效率,将已有的设计成果方便地用到新的设计中去。
又如,某一个逻辑电路是由AND门、OR门和XOR门构成的,而AND门、OR门和XOR门的逻辑电路都已有现成的设计单元。那么,用这些现成的设计单元(AND的Entity、OR的Entity和XOR的Entity)经适当连接就可以构成新的设计电路的Entity。这样的描述,其结构非常清晰,且能做到与电路原理图中所画的器件一一对应。当然,如要用结构描述方式,则要求设计人员有较多的硬件设计知识。
以上所述就是VHDL语言种结构体描述的3种方式:行为描述方式、RTL描述方式(数据流描述方式)和结构描述方式,在实际应用中,往往是几种描述方式的组合应用,而不是单纯的使用一种描述方式,这是在使用中应该注意的。
实际的VHDL程序都是几种描述方式的混合应用,其目的是实现电路的功能和程序可以被综合。而且在某种描述方式中某些语句是不能综合的,如行为描述方式中的延时语句,虽可编译,但不能综合,需加以注意。
2.4 组合逻辑电路的VHDL实现
在FPGA数字系统中,组合逻辑电路主要负责信号传输。组合逻辑本质上是由逻辑门构成的,而逻辑层正是从逻辑门组合及连接角度去描述整个系统的。
2.4.1 简单门电路
简单的门电路如图2-17所示。
图2-17 常用逻辑门符号
这里仅给出二输入与门程序模块的描述:
--二输入与门程序模块-- library ieee; use ieee.std_logic_1164.all; entity myand1 is port(a, b:in std_logic; q :out std_logic); end myand1; architecture rtl of myand1 is begin q<=a and b; end rtl;
一个二输入与门为基础,进而生成一个多输入与门的模块。多输入与门的程序在实体定义中定义了一个常量sreg_width,只要改变该常量的大小,就可以改变与门输入信号数。
--多输入与门程序模块-- library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity myand is --n输入与门,n=sreg_width GENERIC (sreg_width:integer:=8); port(indata:in std_logic_vector(sreg_width-1 downto 0); q :out std_logic); end myand; architecture rtl of myand is signal z:std_logic_vector(sreg_width downto 0); component myand1 port(a, b:in std_logic; q :out std_logic); end component; begin z(sreg_width)<='1' ; g1:for i in sreg_width-1 downto 0 generate u1:myand1 port map(z(i+1), indata(i), z(i)); --由二输入与门构成多输入与门 end generate; q<=z(0); end rtl;
多输入与门程序模块仿真如图2-18所示。
图2-18 多输入与门仿真图
2.4.2 译码器、编码器和选择器
1.译码器
1)3-8译码器74LS138
译码器的逻辑功能是将每个输入信号的二进制代码译成对应的输出高、低电平信号。因此,译码是编码的反操作。74LS138是用TTL与非门构成的3-8译码器,它的逻辑框图如图2-19所示。
图2-19 74LS138逻辑框图
A、B、C是输入信号,对应的Y0~Y7是输出的译码信号,E1、E2、E3是3个控制端,当E1、E2为低电平,E3为高电平的时候,译码器处于工作状态。否则,译码器被禁止,所有的输出端被封锁在高电平中,这3个输入端也叫做“片选”输入端,利用片选的作用,可以将多片连接起来,这样可以扩展译码器的功能。
在前面的章节中已经给出过简单的3-8译码器的VHDL语言实现的例子。这里给出完整的程序,该电路的VHDL语言实现可以应用几种不同的语句实现,这里只给出用case语句实现的VHDL程序。
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity decoder_3_8 is port(a, b, c, e1, e2, e3:in std_logic; y:out std_logic_vector(7 downto 0)); end decoder_3_8; architecture rtl of decoder_3_8 is signal indata:std_logic_vector(2 downto 0); begin indata<=c&b&a; process(indata, e1, e2, e3) begin if(e3='1' and e1='0' and e2='0' )then--译码 case indata is when "000"=>y<="11111110"; when "001"=>y<="11111101"; when "010"=>y<="11111011"; when "011"=>y<="11110111"; when "100"=>y<="11101111"; when "101"=>y<="11011111"; when "110"=>y<="10111111"; when "111"=>y<="01111111"; when others=> y<="XXXXXXXX"; --其他情况 end case; else y<="11111111"; end if; end process; end rtl;
在程序的结构体中定义了一个内部信号indata,它是A、B、C 3个输入信号的并置。在process语句中,indata、e1、e2、e3是进程的敏感信号量,当其中一个信号变化的时候会引发进程。进程被启动后,首先判断片选信号e1、e2、e3的值,如果符合条件,则执行case语句,否则输出全为“1”。在case语句中,通过判断indata的值来决定输出赋予何值。3-8译码器还可以由不同的语句实现。
2)用两片74LS138构成的4-16译码器
4-16译码器可以由两片3-8译码器74LS138构成,其关键是利用好片选输入端。用两片74LS138构成4-16译码器的逻辑电路如图2-20所示。VHDL程序如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity decoder_4_16 is port(a1, b1, c1, d1, g2a1, g2b1:in std_logic; y1, y2:out std_logic_vector(7 downto 0)); end decoder_4_16; architecture rtl of decoder_4_16 is component decoder_3_8 port(a, b, c, e1, e2, e3:in std_logic; y:out std_logic_vector(7 downto 0)); end component; signal g2:std_logic; begin g2<=not d1; u1:decoder_3_8 port map(a1, b1, c1, d1, g2a1, g2b1, y1); --用两片3-8译码 u2:decoder_3_8 port map(a1, b1, c1, g2, g2a1, g2b1, y2); --构成4-16译码 end rtl;
图2-20 用两片74LS138构成的4-16译码器
由图2-20中可以看出两片74LS138的A、B、C连在一起,作为输入信号的低三位,而输入的高位信号D连在第一片74LS138的E1上,并经过一个非门后,连在第二个74LS138的E1上。这样,当D为低电平的时候第一片74LS138被选中,相应的引脚输出译码信号,而第二片74LS138的引脚均为高电平。当D为高电平时,第二片74LS138被选中,相应的引脚输出译码信号,而第一片74LS138的引脚输出为高电平,这样就用两片74LS138扩展成为一个4-16译码器。
图2-21是4-16译码电路的仿真。
图2-21 4-16译码器仿真图
由该程序可看出,用结构描述方式书写程序非常容易,这是VHDL语言的一大优点。
3)LED显示模块的译码电路
一个简单的LED共阴极显示模块的译码表,无须控制信号线,这种电路虽简单,但在实际应用中很实用。7段数码管LED常用的一般8字型,分为a、b、c、d、e、f、g、P段,其中P为小数点,共阴LED低电平有效,如图2-22所示。
图2-22 LED共阴极显示
--七段锁存译码驱动器,不带小数点,如需显示小数点-- --则输出结果或“10000000”-- library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity mc14495 is port(datain:in std_logic_vector(7 downto 0); dataout :out std_logic_vector(7 downto 0)); end mc14495; architecture behav of mc14495 is begin process(datain) begin case datain is when "00000000"=>dataout<="00111111"; --"0" when "00000001"=>dataout<="00000110"; --"1" when "00000010"=>dataout<="01011011"; --"2" when "00000011"=>dataout<="01001111"; --"3" when "00000100"=>dataout<="01100110"; --"4" when "00000101"=>dataout<="01101101"; --"5" when "00000110"=>dataout<="01111101"; --"6" when "00000111"=>dataout<="00000111"; --"7" when "00001000"=>dataout<="01111111"; --"8" when "00001001"=>dataout<="01101111"; --"9" when "00001010"=>dataout<="01110111"; --"A" when "00001011"=>dataout<="01111100"; --"B" when "00001100"=>dataout<="00111001"; --"C" when "00001101"=>dataout<="10011110"; --"D" when "00001110"=>dataout<="01111001"; --"E" when "00001111"=>dataout<="01110001"; --"F" when "01110011"=>dataout<="01110011"; --"P" when "00111110"=>dataout<="00111110"; --"U" when "00110001"=>dataout<="00110001"; --"1-" when "01101110"=>dataout<="01101110"; --"Y" when "01110110"=>dataout<="01110110"; --"H" when "00111000"=>dataout<="00111000"; --"L" when others=>dataout<="00000000"; --"灭" end case; end process; end behav;
图2-23是LED显示译码仿真。
图2-23 LED显示译码仿真图
2.编码器
1)8-3优先编码器74LS148
在优先编码器电路中,允许同时输入两个以上编码信号。不过在设计优先编码器时已经将所有的输入信号按优先顺序排了队,当几个输入信号同时出现时,只对其中优先权最高的一个进行编码,74LS148的逻辑电路图如图2-24所示。
图2-24 74LS148电路图
I0~I7是输入信号,S是片选信号,低电平有效,Ys和Yex用于扩展编码功能,Y0~Y2是输出信号,低电平有效。74LS148的VHDL语言实现如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity sn74ls148 is port(i:in std_logic_vector(7 downto 0); s:in std_logic; ys, yex:out std_logic; y:out std_logic_vector(2 downto 0)); end sn74ls148; architecture behav of sn74ls148 is begin ys<=NOT((NOT(s)) and ( i(0)) and ( i(1))and ( i(2))and ( i(3))and ( i(4)) and ( i(5))and ( i(6))and ( i(7))); yex<=NOT((NOT(s)) and ((NOT i(0)) or (NOT i(1)) or (NOT i(2)) or (NOT i(3)) or (NOT i(4)) or (NOT i(5)) or (NOT i(6)) or (NOT i(7)))); --扩展编码输出逻辑关系表达式 process(i, s) begin if(s='0' )then --片选 if(i(0)='0' )then y<="000"; --优先编码 elsif (i(1)='0' )then y<="001"; elsif (i(2)='0' )then y<="010"; elsif (i(3)='0' )then y<="011"; elsif (i(4)='0' )then y<="100"; elsif (i(5)='0' )then y<="101"; elsif (i(6)='0' )then y<="110"; elsif (i(7)='0' )then y<="111"; else y<="XXX"; --其他情况 end if; end if; end process; end behav;
图2-25是LED显示编码的仿真。
这段程序应该注意“if then else”语句的用法。因为要实现优先编码,故采用了“if then else”语句。当s='0’时,即芯片被选中的时候,输入信号I的变化会启动process语句,process语句中的语句是顺序执行的,所以“if then else”语句就一条一条地判断执行,直到某一条件为真时,输出相应的信号,否则输出为“XXX”。
图2-25 74LS148编码仿真图
2)16-4优先编码器
16-4优先编码器可以由两片8-3优先编码器74LS148扩展而来。扩展方法就是采用component语句调用前面已经编译好的8-3优先编码器74LS148模块,编程非常方便。
由两片8-3优先编码器74LS148扩展成16-4优先编码器的逻辑电路如图2-26所示。
图2-26 由两片74LS148扩展成的16-4优先编码器
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity coder16_4 is port(a:in std_logic_vector(15 downto 0); z:out std_logic_vector(3 downto 0)); --编码4位输出 end coder16_4 ; architecture rtl of coder16_4 is signal s_tmp, ys_tmp, yex_tmp, ys_tmp1, yex_tmp1 :std_logic; signal b, c:std_logic_vector(7 downto 0); signal d, e:std_logic_vector(2 downto 0); component sn74ls148 --8-3优先编码器74LS148 port(i:in std_logic_vector(7 downto 0); s:in std_logic; ys, yex:out std_logic; y:out std_logic_vector(2 downto 0)); end component; begin s_tmp<='0' ; b<=a(7)&a(6)&a(5)&a(4)&a(3)&a(2)&a(1)&a(0); c<=a(15)&a(14)&a(13)&a(12)&a(11)&a(10)&a(9)&a(8); u1:sn74ls148 port map(b, s_tmp, ys_tmp, yex_tmp, d); --利用扩展编码端口Ys/Yex u2:sn74ls148 port map(c, ys_tmp, ys_tmp1, yex_tmp1, e); --扩展成16-4优先编码 z(3)<=not ys_tmp; z(2)<=not(d(2) AND e(2)); z(1)<=not(d(1) AND e(1)); z(0)<=not(d(0) AND e(0)); --4路输出 end rtl;
图2-27是16-4编码器的仿真。
图2-27 16-4编码器仿真图
在本例中采用component语句实现了对底层模块SN74LS148的调用,在结构体中定义了一些内部信号,它们主要用于component语句中的端口映射。从这个例子可以看出,在VHDL语言中的模块化结构可以大大简化编程难度,从而缩短了编程周期。
3)BCD-7段译码器
为了能以十进制数码直观地显示数字系统的运行数据,在数字系统中广泛使用了7段数码管。为此就需要使用显示译码器将BCD代码译成数码管所需要的驱动信号,以便使数码管用十进制数字显示出BCD代码所表示的数值。
今以A3A2A1A0表示显示译码器的输入BCD代码,以Ya~Yg表示输出的7位二进制代码,并规定用“1”表示数码管中线段的点亮状态,用“0”表示线段的熄灭状态。则根据显示字型的要求就得到了表2-4的真值表。
表2-4 BCD-7段显示译码器的真值表
图2-28给出了BCD-7段显示译码器SN74LS48N的逻辑图。由图中可以看出,SN74LS48N有AI、BI、CI、DI 4个输入信号引脚,有A、B、C、D、E、F、G 7个输出端,输入与输出的关系如表2-1所示。LT是灯测试输入,当LT为低电平时,输出为全“1”,使数码管全部被点亮,平时应置LT为高电平。RBI是灭零输入信号,当输入为“0000”时,本应显示出零,但此时的零是多余的,如00013.700前后的零都是多余的,这时将“0”灭掉会使显示更醒目,此时如果RBI为“0”,则输出信号会是全“0”。BI/RBI称灭零输入/灭零输出端,是一个双向输入/输出口,当它作为输入时,如BI为“0”,则输出为全“0”,当它作为输出时,。
图2-28 SN74LS48N
SN74LS48N的VHDL程序实现如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity sn7448 is port(lt, rbi:std_logic; --lt灯测试输入 datain:in std_logic_vector(3 downto 0); rbo_bi:inout std_logic; dataout:out std_logic_vector(7 downto 0)); end sn7448; architecture behav of sn7448 is signal dataout_1:std_logic_vector(7 downto 0); begin rbo_bi<=not( datain(0) and datain(1) and datain(2) and datain(3) and and (not rbi)); --灭零输入/灭零输出 process(datain, lt, rbi, rbo_bi) begin if(lt='1' )then if(rbo_bi='1' )then case datain is when "0000"=>dataout_1<="00111111"; --译码"0" when "0001"=>dataout_1<="00000110"; --"1" when "0010"=>dataout_1<="01011011"; --"2" when "0011"=>dataout_1<="01001111"; --"3" when "0100"=>dataout_1<="01100110"; --"4" when "0101"=>dataout_1<="01101101"; --"5" when "0110"=>dataout_1<="01111101"; --"6" when "0111"=>dataout_1<="00000111"; --"7" when "1000"=>dataout_1<="01111111"; --"8" when "1001"=>dataout_1<="01101111"; --"9" when "1010"=>dataout_1<="01110111"; --"A" when "1011"=>dataout_1<="01111100"; --"B" when "1100"=>dataout_1<="00111001"; --"C" when "1101"=>dataout_1<="10011110"; --"D" when "1110"=>dataout_1<="01111001"; --"E" when "1111"=>dataout_1<="01110001"; --"F" when others=>dataout_1<="00000000"; --"灭" end case; if(rbi='0' and dataout_1="00111111")then dataout<="00000000"; else dataout<=dataout_1; end if; else dataout<="00000000"; end if; else dataout<="11111111"; end if; end process; end behav;
图2-29是BCD-7段译码器的仿真。
图2-29 BCD-7段译码器仿真图
其中if语句都是用来判断输入控制信号的状态,译码部分是用case语句实现的。因为RI/RBO引脚是一个输入/输出端口,故与它对应的端口rbo_bi的方向应该设置为INOUT。
3.选择器
在讲述顺序语句时,就提到了选择器。选择器结构比较简单,可以用两个与门、一个或门、一个非门连接而成。设输入信号为d0、d1、sel为信号选择端,当sel=0时选择d0, sel=1时选择d1,用VHDL描述有q<=d0 when sel=' o' else b。
--2选1电路用RTL描述对应的VHDL程序-- library ieee; use ieee.std_logic_1164.all; entity mux2_1 is port(d0, d1, sel:in bit; q:out bit); end mux2_1; --architecture beh of mux2_1 is --方法1 --begin -- q<=d0 when sel' o' else d1; --end beh; architecture lmq of mux2_1 is --方法2 begin dudu:process(d0, d1, sel) variable tmp1, tmp2, tmp3:bit; begin tmp1:=d0 and sel; tmp2:=d1 and (not sel); tmp3:=tmp1 or tmp2; q<=tmp3; end process; end lmq;
2.4.3 加法器、乘法器和除法器
1.加法器
1)半加器
半加器可用于最低位求和,并给出进位数。半加器h_adder逻辑功能如表2-5所示,其中co为进位。
表2-5 半加器h_adder逻辑功能真值表
--1位半加器描述-- LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; ENTITY h_adder IS PORT (a, b : IN STD_LOGIC; co, so : OUT STD_LOGIC); END ENTITY h_adder; ARCHITECTURE fh1 OF h_adder is SIGNAL abc : STD_LOGIC_VECTOR(1 DOWNTO 0) ; BEGIN abc <= a & b ; PROCESS(abc) BEGIN CASE abc IS WHEN "00" => so<='0' ; co<='0' ; WHEN "01" => so<='1' ; co<='0' ; WHEN "10" => so<='1' ; co<='0' ; WHEN "11" => so<='0' ; co<='1' ; WHEN OTHERS => NULL ; END CASE; END PROCESS; END ARCHITECTURE fh1 ;
2)1位全加器
全加器可以由两个半加器组成。
--1位二进制全加器顶层设计描述-- LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; ENTITY f_adder IS PORT (ain, bin, cin : IN STD_LOGIC; cout, sum : OUT STD_LOGIC ); END ENTITY f_adder; ARCHITECTURE fd1 OF f_adder IS COMPONENT h_adder PORT ( a, b : IN STD_LOGIC; co, so : OUT STD_LOGIC); END COMPONENT; COMPONENT or2a PORT (a, b : IN STD_LOGIC; c : OUT STD_LOGIC); END COMPONENT; SIGNAL d, e, f : STD_LOGIC; BEGIN u1 : h_adder PORT MAP(a=>ain, b=>bin, co=>d, so=>e); u2 : h_adder PORT MAP(a=>e, b=>cin, co=>f, so=>sum); u3 : or2a PORT MAP(a=>d, b=>f, c=>cout); END ARCHITECTURE fd1;
3)4位加法器
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity adder4bit is port(cin:in std_logic; a, b:in std_logic_vector(3 downto 0); s:out std_logic_vector(3 downto 0); cout:out std_logic); end adder4bit; architecture beh of adder4bit is signal sint:std_logic_vector(4 downto 0); signal aa, bb:std_logic_vector(4 downto 0); begin aa<='0' & a(3 downto 0); --4位加数矢量扩为5位,提供进位空间 bb<='0' & b(3 downto 0); sint<=aa+bb+cin; s(3 downto 0)<=sint(3 downto 0); cout<=sint(4); end beh;
图2-30是4位加法器的仿真。
图2-30 4位加法器仿真图
4)8位加法器
一个8位二进制加法器,可由两个并行的4位加法器级联而成。
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity adder8bit is port(cin:in std_logic; a, b:in std_logic_vector(7 downto 0); s:out std_logic_vector(7 downto 0); cout:out std_logic); end adder8bit; architecture beh2 of adder8bit is component adder4bit --调用4位加法器模块 port(cin:in std_logic; a, b:in std_logic_vector(3 downto 0); s:out std_logic_vector(3 downto 0); cout:out std_logic); end component; signal carry_out:std_logic; begin --匹配参数和端口 u1:adder4bit port map(cin=>cin, a=>a(3 downto 0), b=>b(3 downto 0), s=>s(3 downto 0), cout=>carry_out); u2:adder4bit port map(cin=>carry_out, a=>a(7 downto 4), b=>b(7 downto 4), s=>s(7 downto 4), cout=>cout); end beh2;
2.乘法器
1)普通的8位乘法器。
两位 N 位二进制的乘积用 X 与 A 的累加和表示,“手工计算”的方法就是=0k ),从中可以看出,只要ak不等于0,输入X就随着k的位置连续地变化,然后进行累加。如果ak等于0,相应地转换相加就可以忽略了。
对于一般的8位乘法器,乘法的执行分3个阶段完成。首先下载8位操作数并重置乘积寄存器。在第二阶段中,进行实际地串行-并行乘法运算。在第3步中,乘积被传输到输出寄存器重。
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; entity mul_8 is --接口 port(clk:in std_logic; x:in integer range -128 to 127; a:in std_logic_vector(7 downto 0); y:out integer range -32768 to 32767); end mul_8; architecture bit of mul_8 is type state_type is(s1, s2, s3); --自定义类型 signal state:state_type; begin beh:process --行为描述 variable p, t: integer range -32768 to 32767; variable count : integer range 0 to 7; begin wait until clk='1' ; case state is when s1=>state<=s2; --第1步初始化 count:=0; p:=0; --重置寄存器 t:=x; --移位 when s2=>if count=7 then --相乘累加 state<=s3; else if a(count)='1' then p:=p+1; end if; t:=t*2; count:=count+1; state<=s2; end if; when s3=>y<=p; --结果输出 state<=s1; end case; end process beh; end bit;
在该程序中,还使用了状态机state的VHDL描述。
2)16位乘法器
采用移位相加的方法实现一个16×16位乘法器。
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; entity mul16 is port (clk:in std_logic; a, b:in std_logic_vector(15 downto 0); q:out std_logic_vector(31 downto 0)); end mul16; architecture beh of mul16 is begin process (clk) variable tmp:std_logic_vector(31 downto 0); variable tout:std_logic_vector(31 downto 0); begin tout:="00000000000000000000000000000000"; if (clk' event and clk='1' ) then for i in 0 to 15 loop tmp:="00000000000000000000000000000000"; --初始化 if (b(i)='1' ) then --移位相加实现相乘 for j in 0 to 15 loop tmp(i+j):=a(j); end loop; end if; tout:=tmp+tout; end loop; end if; q<=tout; end process; end beh;
采用移位相加的方法16×16位乘法器。该程序编译指定芯片为EPF10K10TC144-3,打开mul16.rpt文件可知大体情况如表2-6所示,可见16位乘法器是比较占资源的。
表2-6 16位乘法器编译报告结果
3.除法器
基本的数学运算在VHDL中已经有相应的函数来实现,不用自己编写程序。但某些编译软件对一些算术运算不支持,它只支持除数是2的方次的除法,除数为其他数的除法则不支持,若要使用此种数学运算,则需自己编制程序。下面是一个除法器的例子,逻辑框图如图2-31所示。
该电路有两个输入信号,DIVIDENT和DIVIDOR,都是4位位矢量,DIVIDENT是被除数,DIVIDOR是除数。输出信号有RESULT、RESIDUAL、CARRYBIT 3个,RESULT是结果,RESIDUAL是余数,CARRYBIT是溢出标志位。
该电路功能比较简单,实现4位无符号整数的触发,可以进一步完善其功能。
图2-31 除法器逻辑框图
该电路的VHDL语言实现如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity mydivider is port(divident:in std_logic_vector(3 downto 0); --被除数 dividor:in std_logic_vector(3 downto 0); --除数 carrybit:out std_logic; --进位 result:out std_logic_vector(3 downto 0); --商 residual:out std_logic_vector(3 downto 0)); --余数 end mydivider; architecture behav of mydivider is begin process(divident, dividor) variable counter_1:integer; variable c, d, a, b, e, f, sig_1:std_logic_vector(3 downto 0); begin a:=divident; b:=dividor; e:=a; f:=b; counter_1:=0; if(b="0000")then c:="1111"; d:="1111"; carrybit<='1' ; else if(a<b)then --当a<b,则不需要除 c:="0000"; d:=a; carrybit<='0' ; else if(a="0000")then --0除以任何数都为0 c:="0000"; d:="0000"; else for i in 3 downto 0 loop -移位相除 if(f(3)='0' )then for j in 3 downto 1 loop --将除数移位到第一位不为零的位与被除数第一位不为零的数对齐 f(j):=f(j-1); end loop; f(0):='0' ; counter_1:=counter_1+1; end if; end loop; for i in 3 downto 0 loop --循环体 if(i>counter_1)then c(i):='0' ; elsif (e<f)then for j in 0 to 2 loop f(j):=f(j+1); end loop; f(3):='0' ; c(i):='0' ; else e:=e-f; c(i):='1' ; for j in 0 to 2 loop f(j):=f(j+1); --移位 end loop; f(3):='0' ; end if; end loop; d:=e; end if; carrybit<='0' ; end if; end if; result<=c; residual<=d; --得到商、余数 end process; end behav;
图2-32是除法器的仿真。
图2-32 除法器仿真图
对于该电路的程序实现主要是采用移位相减,即将除数移位到第一位不为零的位与被除数第一位不为零的数对齐,然后比较两数,若被除数比除数大,则两数相减,差放在被除数中,商的相应位置“1”;若被除数小于除数,则将除数左移一位,商的相应位置“0”;然后重复前边过程,直到除法过程结束。
2.4.4 三态门和总线缓冲器
在FPGA设计中,可以利用三态输出门电路还能实现数据的双向传输,利用总线缓冲器多路信号分时传递,而且它们的结构很简单。
1.三态门
三态门的三态是指输出端而言,除了0和1态,还有高阻态Z。三态门的VHDL描述如下:
LIBRARY IEEE; USE ieee.std_logic_1164.ALL; ENTITY prebus IS PORT(my_in : IN STD_LOGIC_VECTOR(7 DOWNTO 0); sel : IN STD_LOGIC; out : OUT STD_LOGIC_VECTOR(7 DOWNTO 0)); END prebus; ARCHITECTURE rtl OF prebus IS BEGIN out <= "ZZZZZZZZ"; WHEN (sel = '1' ) ELSE my_in; END rtl;
2.双向总线缓冲器
双向总线缓冲器主要是解决数据临时存储起来以防不必要的端到端延误。双向总线缓冲器的VHDL描述如下:
LIBRARY ieee; USE ieee.std_logic_1164.ALL; ENTITY bidir IS PORT(bidir : INOUT STD_LOGIC_VECTOR (7 DOWNTO 0); oe, clk : IN STD_LOGIC; inp : IN STD_LOGIC_VECTOR (7 DOWNTO 0); outp : OUT STD_LOGIC_VECTOR (7 DOWNTO 0)); END bidir; ARCHITECTURE rtl OF bidir IS SIGNAL a : STD_LOGIC_VECTOR (7 DOWNTO 0); --输入缓冲 SIGNAL b : STD_LOGIC_VECTOR (7 DOWNTO 0); --输出缓冲 BEGIN PROCESS(clk) BEGIN IF clk = '1' AND clk' EVENT THEN a <= inp; outp <= b; END IF; END PROCESS; PROCESS (oe, bidir) BEGIN IF( oe = '0' ) THEN bidir <= "ZZZZZZZZ"; b <= bidir; ELSE bidir <= a; b <= bidir; END IF; END PROCESS; END rtl;
2.5 时序逻辑电路的VHDL实现
在时序电路设计中主要介绍触发器、寄存器和计数器。执行时序电路基本存储元件的方式有3种:调用程序包中的寄存器组件、使用wait until语句及使用对时钟的边沿触发的进程,其中wait until语句不能综合。
2.5.1 锁存器、触发器
1.锁存器
学过数字电路的读者对D锁存器都不会陌生,最简单并最具代表性的时序电路是D触发器,它是现代可编程ASIC设计中最基本的时序元件和底层元件。D触发器的描述包含了VHDL对时序电路的最基本和典型的表达方式,同时也包含了在VHDL中许多最具特色的语言现象,其逻辑框图如图2-33所示。
图2-33 D锁存器
D锁存器有两个输入信号,一个是时钟,另一个是输入数据D,有一个输出引脚Q,该锁存器在时钟信号的上升沿将输入数据锁存,直到下一个时钟的上升沿才能改变。
该电路的VHDL语言实现如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity dff1 is port(clk, d:in std_logic; q:out std_logic); end dff1; architecture rtl of dff1 is begin process(clk) begin if((clk' event) and (clk='1' ) )then q<=d; --锁存 end if; end process; end rtl;
D锁存器的仿真如图2-34所示。
图2-34 D锁存器仿真图
在该程序中,对时钟上升沿的判断是通过if((clk' event) and (clk='1' ) )then语句实现的,如果要对下降沿判断,则需要用if((clk' event) and (clk='0' ) )then语句。
2.带异步置位、复位的D触发器
带异步置位、复位的D触发器是数字电路中常用的元件,其逻辑框图如图2-35所示,此触发器比D锁存器多了两个输入信号,其中SD是异步置位,CD是异步清零,其余引脚同D锁存器。
所谓异步与同步是相对于时钟来说的,异步就是指当此输入信号有效时,不管时钟是什么状态,也不用等待时钟信号的某一状态,直接起作用的一种信号称为异步。而同步则是指当信号有效后,必须等待时钟信号的某一状态到来才能执行相应功能。SD引脚为异步置位,就是当它的输入信号为低电平时,马上将输出信号Q置’1'。CD信号为异步清零,就是当它的输入信号是低电平时,不管时钟是何状态,马上将输出信号Q置’0'。因此,在程序中对这两个信号的判断,应该放在对时钟信号的判断之前进行。
图2-35 带异步置位、复位的D触发器
该电路的VHDL语句实现如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity dff2 is port(clk, cd, sd, d:in std_logic; q, notq:out std_logic); end dff2; architecture rtl of dff2 is begin process(clk, sd, cd) begin if(cd='0' )then --判断CD、SD是否有效 q<='0' ; notq<='1' ; elsif (sd='0' )then --异步置位SD的优先级低于异步清零CD q<='1' ; notq<='0' ; else if((clk' event) and (clk='1' ) )then q<=d; end if; end if; end process; end rtl;
带异步置位、复位的D触发器的仿真如图2-36所示。
图2-36 带异步置位、复位触发器仿真图
在程序中首先判断SD与CD的状态,若这两个输入信号有一个有效,则马上执行相应的功能,并且异步清零的优先级要高于异步置位的优先级。若此两个状态均无效则等待时钟的上升沿,在时钟的上升沿将输入数据锁存到输出。
2.5.2 寄存器
1.循环移位寄存器
在计算机内经常要用到循环移位寄存器,它根据程序的设定,将给定的数据循环移位,直到指定的位数,本例讲述一个循环左移寄存器。此循环移位寄存器只能向左移位,输入数据是din,位长为8位,输出数据是dout,位长为8位。s0~s2是移位位数,最多可移位7位。enb输入信号用于移位的使能,clk是输入时钟信号。循环移位寄存器的逻辑框图如图2-37所示。
图2-37 循环移位寄存器
该电路的VHDL语言源程序如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity bsr is port(din :in std_logic_vector(7 downto 0); s:in std_logic_vector(2 downto 0); --移位位数 clk, enb:in std_logic; --时钟输入、移位使能 wr:out std_logic; dout:out std_logic_vector(7 downto 0)); end bsr; architecture behav of bsr is signal counter:integer; begin process(clk) variable sc:integer; variable tmp, tmp1:std_logic; begin if(clk' event and clk='1' )then if(enb='0' )then counter<=0; wr<='1' ; elsif(counter<sc)then --end='1',移位 sc:=conv_integer(s); dout(0)<=din(7); for i in 7 downto 1 loop dout(i)<=din(i-1); end loop; counter<=counter+1; --记录移位次数 wr<='1' ; else wr<='0' ; end if; end if; end process; end behav;
该程序的进程由敏感信号量clk触发,当clk的上升沿来临的时候,进程被启动。然后程序判断enb的状态,如果enb是低电平,则把计数器清零,把wr信号置’1',否则就执行移位功能。
移位功能通过for语句来实现,一个时钟上升沿信号只能移位一位。若要移位多位,则需要多个时钟上升沿。在程序中用到了一个计数器counter,它的作用是记录移位几位,没有移位时被清零,当有移位时,每左移一位就加1,然后与输入的移位位数比较,如果小于程序设定的移位位数,则继续移位,否则将结束移位过程,并将wr信号置’0'。wr信号用于通知外部电路移位已经完成。
2.串行输入、串行输出移位寄存器
串行输入、串行输出移位寄存器的电路原理图如图2-38所示。它具有两个输入端:一个是数据输入端a和时钟输入端clk;一个是数据输出端b。图中所示的是八位的串行移位寄存器,在时钟信号的作用下,前级的数据向后移动。该8位移位寄存器由8个D触发器的描述就很容易写出8位移位寄存器的VHDL语言程序。
图2-38 串行输入/输出的8位移位寄存器
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity shift8 is port(a, clk1:in std_logic; b:out std_logic); --串行输出 end shift8; architecture rtl of shift8 is component dff1 --调用D触发器dff port(clk, d:in std_logic; q:out std_logic); end component; signal z:std_logic_vector(0 to 8); begin z(0)<=a; g1:for i in 0 to 7 generate dffx:dff1 port map(clk1, z(i), z(i+1)); --匹配参数 end generate; b<=z(8); end rtl;
该程序采用for generate语句来实现串行连接的8个D触发器。并且程序中用到了以前定义的D锁存器模块dff。
2.5.3 计数器
1.8位二进制计数器
计数器是一个典型的时序电路,分析计数器就能更好地了解时序电路的特性。计数器是数字电路中常用元件,下面是一个8位二进制计数器的例子,逻辑框图如图2-39所示。
图2-39 8位二进制计数器
该电路有一个时钟信号输入端,用于输入时钟信号。en是芯片使能信号,低电平有效。clr是同步清零端,当en为低电平,如果clr是低电平,那么在下一个时钟上升沿输出被清零。bcdwr信号是同步置数端,如果en为低电平,clr为高电平,这时如果bcdwr是低电平,当下一个时钟上升沿来临时,预置数被置入计数器。updn信号是加、减计数选择端,updn为’1’时是加计数,updn为’0’时是减计数。datain0~datain7是8位并行置数端,dataout0~dataout7是8位并行输出数据端。c是进位信号,当计数满时,产生进位信号,c平时是低电平,当dataout端输出为全’0’时,c为高电平。该电路的VHDL实现如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity count_1024 is port(clk, clr, en, updn, bcdwr:in std_logic; --时钟、清零、使能、加/减选择、同步 datain:in std_logic_vector(9 downto 0); c:out std_logic; q:out std_logic_vector (9 downto 0)); end count_1024; architecture rtl of count_1024 is signal count_10:std_logic_vector(9 downto 0); begin q(0)<=count_10(0); q(1)<=count_10(1); q(2)<=count_10(2); q(3)<=count_10(3); q(4)<=count_10(4); q(5)<=count_10(5); q(6)<=count_10(6); q(7)<=count_10(7); q(8)<=count_10(8); q(9)<=count_10(9); c<=count_10(0) and count_10(1) and count_10(2) and count_10(3) and count_10(4) and count_10(5) and count_10(6) and count_10(7) and count_10(8) and count_10(9); --进位 process(clk, clr, bcdwr) begin if clk' event and clk='1' then --开始计数 if en='0' then count_10<=count_10; elsif clr='1' then count_10<="0000000000"; elsif bcdwr='1' then count_10<=datain; --预置datain elsif updn='1' then count_10<=count_10+'1' ; else count_10<=count_10-'1' ; end if; end if; end process; end rtl;
1024计数器的仿真如图2-40所示。
图2-40 1024计数器仿真图
该程序采用一个内部计数信号count_10来计数。程序首先判断en的状态,若en为’0',则进一步判断clr的状态,若为’0',则清计数器,若为’1',则进一步判断bcdwr的状态。若bcdwr为’0',则对计数器置数,若为’1’则进一步判断updn的状态。若updn为高电平,则进行加计数,count_10加’1',否则进行减计数,count_10减’1'。输出端q等于count_10的状态,c等于count_10各位相与。
2.由两片8位二进制计数器组成的16位二进制计数器
若要实现更多位计数器,一种方法是将计数器的输入端口、输出端口增加位数,内部计数器也相应增加位数;另一种方法是用多片的8位二进制计数器连接扩展而来。下面例子是由两片8位二进制计数器扩展的16位二进制计数器,电路逻辑框图如图2-41所示。
用两片8位二进制计数器扩展成16位二进制计数器,也有很多方法,这里采用将第一片计数器的c进位信号连接到第二片计数器的时钟输入端,这样当第一片计数器计满溢出时,第二片计数器就计一个数。两片计数器的en、clr、bcdwr、updn可以连接到一起。如果要实现216以下任意计数的话,就要两片计数器的输出端进行相应的连接,OUTPUTLOGIC的输出端再接到bcdwr上,这样才能实现任意计数。
图2-41 由两片8位二进制计数器组成的16位二进制计数器
该电路的VHDL也是采用component语句,应用前面定义过的8位二进制计数器模块,源程序如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity counter_1m is port(clk, updn, bcdwr:in std_logic; d, e:out std_logic_vector(9 downto 0); c:out std_logic); --c进位信号连接到第二片计数器的时钟输入端 end counter_1m; architecture rtl of counter_1m is signal clr, en, c_tmp:std_logic; signal a:std_logic_vector(9 downto 0); component count_1024 port(clk, clr, en, updn, bcdwr:in std_logic; datain:in std_logic_vector(9 downto 0); c:out std_logic; q:out std_logic_vector (9 downto 0)); end component; begin a<="0000000000"; clr<='0' ; en<='1' ; u1:count_1024--由两片8位二进制计数器构成16进制计数器 port map(clk, clr, en, updn, bcdwr, a, c_tmp, d); u2:count_1024 port map(c_tmp, clr , en, updn , bcdwr, a, c, e); end rtl;
3.分钟计数器
六十进制计数器常用于时钟计数。六十进制计数,当个位计数到9(二进制为1001)10位进一,当10位计数到5(二进制为0101)清零重新计数,这就完成了六十进制计数功能。当然,可以不分为个位、十位计数,直接从0计数到59(二进制为111011),但前一种方法更有实用性,可直接用于时钟计数。
--------------六十进制计数器-------------------- library IEEE; use IEEE.std_logic_1164.all; use IEEE.std_logic_arith.all; use IEEE.std_logic_unsigned.all; entity counter60 is port(clk, clr:in std_logic; c:out std_logic; bcd1:out std_logic_vector(3 downto 0); --个位 bcd2:out std_logic_vector(3 downto 0)); --十位 end counter60; architecture rtl of counter60 is signal bcd1n:std_logic_vector(3 downto 0):="0000"; --个位 signal bcd2n:std_logic_vector(3 downto 0):="0000"; --十位 signal cn:std_logic:='1' ; begin bcd1<=bcd1n; bcd2<=bcd2n; c<=cn; process(clk) begin if(clr='0' ) then bcd1n<="0000"; else if(clk' event and clk='0' ) then if(bcd1n="1001") then --个位等于9,则进位 bcd1n<="0000"; else bcd1n<=bcd1n+1; end if; end if; end if; end process; process(clk) begin if(clr='0' ) then bcd2n<="0000"; else if(clk' event and clk='0' ) then if(bcd1n="1001") then if(bcd2n="0101") then --十位等于5,则进位 bcd2n<="0000"; else bcd2n<=bcd2n+1; end if; end if; end if; end if; end process; process(clk) begin if(clk' event and clk='0' ) then if(bcd1n="1001" and bcd2n="0101") then --等于59,进位 cn<='0' ; else cn<='1' ; end if; end if; end process; end rtl;
用Quartus Ⅱ进行原理图输入,还可以利用两片74160芯片(BCD码同步加法计数器),设计六十进制计数器。分别调用Libraries下的库元件74160、GND、NAND3、INPUT、OUTPUT,如图2-42所示。
原理图设计实现的功能与VHDL设计的功能和效果都是一致的,实现的都是六十进制的计数器。编译报告显示其fmax达到290.02MHz,关键路径延迟为2.967ns,使用了CPLD/FPGA总资源的1%LE和7%的管脚;相比之下,VHDL设计实例fmax为190.91MHz,关键路径延迟为4.324ns,使用了1%LE和11%的管脚。
图2-42 六十进制计数器的图形设计
仿真结果如图2-43所示。仿真结束后,可以用Floorplan进行管脚修改和绑定,然后把程序下载到FPGA板中验证结果。
图2-43 六十进制计数器仿真
4.分频器
在接口电路中,时钟信号的作用至关重要。一般FPGA的外部时钟信号可达几十MHz,但由于一些接口电路的特性所致,这样高频率的时钟不适合电路工作,所以应该引入时钟分频电路,产生频率适合接口电路工作的时钟信号,这样才能便于接口电路工作。
对于时钟分频可以有奇数分频与偶数分频,而且分频后时钟的占空比也是可变的,最简单的时钟分频就是对时钟进行计数,然后输出时钟信号等于计数器各位相与,这样得到的分频信号,其占空比很小,即时钟高电平只有一个外部时钟周期,但由于FPGA设计的对时钟跳变沿的判断的精确性,故这种分频方式也是可取的。
1)偶数分频
对时钟进行偶数分频,使占空比达到50%很简单,只要使用一个计数器,在计数器的前一半时间里,使输出电平为高电平,在计数的后一半时间里使输出的电平为低电平,这样分频出来的时钟信号就是占空比为50%的时钟信号,偶数分频的VHDL语言程序如下:
-------------even frequency division----------------- library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity fredivn is GENERIC (N:integer:=8); --8分频,偶数分频 port (clk:in std_logic; outclk:out std_logic); end fredivn; architecture rtl of fredivn is signal count:integer; begin process(clk) begin if(clk' event and clk='1' ) then if(count=N-1)then --计数周期 count<=0; else count<=count+1; if count<(integer(N/2)) then --产生分频脉冲,占空比达到50% outclk<='0' ; else outclk<='1' ; end if; end if; end if; end process; end rtl;
在该程序中端口参数N是分频数,由它来决定多少分频。在程序中预置该参数为8,即8分频,该程序的仿真波形如图2-44所示。
图2-44 8分频仿真图
由图中可以看出,输出时钟是对输入时钟的8分频,并且输出时钟的占空比是50%。
该程序的技术要点主要有两点。第一点是内部计数信号count的赋值语句,在程序中主要通过if语句来实现。该语句实现的功能是:当count小于N/2时,输出低电平,当count大于N/2时,输出高电平,因为N是偶数,所以占空比是50%,并且count循环计数。
if(count=N-1)then count<=0; else count<=count+1; if count<(integer(N/2)) then --产生分频脉冲 outclk<='0' ; else outclk<='1' ; end if; end if;
程序的另一个技术要点是使用了generic语句,在entity的定义语句中有:
generic (N:integer:=8);
这个语句定义了分频数N,并且由于运用了generic语句,故在其他程序调用的时候,可以任意更改分频数,使得对此模块的调用非常方便。
2)奇数分频
奇数分频相对于偶数分频来说,如果不要求占空比的话,难度是一样的,但是如果要使占空比为50%,则计数分频比偶数分频要复杂一些。
奇数分频与偶数分频思路是一样的,都是使输出信号在前一半计数时间里为低电平,在后一半计数时间里为高电平。如果要求占空比为50%,则要使用一些技巧。可以先对输入时钟的上升沿进行计数,然后让一个内部信号在前一半时间里为低电平,后一半时间里为高电平,同时对输入时钟的下降沿进行计数,让另一个内部信号在前一半时间里为高电平,在后一半时间里为低电平,然后让两个内部信号相与,则得到了半个时钟周期的一个高电平,再让这个信号与第一个内部信号相或,就得到了占空比为50%的输出时钟。
该电路的VHDL程序如下:
--------------odd frequency division---------------- library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; entity fredivn1 is GENERIC (N:integer:=7); --奇数分频 port (clk:in std_logic; outclk:out std_logic); end fredivn1; architecture rtl of fredivn1 is signal count1, count2:integer; signal q, outclk1, outclk2:std_logic; begin q<=outclk1 and outclk2; outclk<=q xor outclk1; process(clk) --上升沿进行计数 begin if(clk' event and clk='1' ) then if(count1=N-1)then count1<=0; else count1<=count1+1; if count1<(integer(N/2)) then --产生分频脉冲1 outclk1<='0' ; else outclk1<='1' ; end if; end if; end if; end process; process(clk) --下降沿进行计数 begin if(clk' event and clk='0' ) then if(count2=N-1)then count2<=0; else count2<=count2+1; if count2<(integer(N/2)) then --产生分频脉冲2 outclk2<='1' ; else outclk2<='0' ; end if; end if; end if; end process; end rtl;
该程序的仿真波形如图2-45所示。程序的技术要点就是如何保证占空比为50%。在此程序中用到了两个process,第一个进程是对上升沿进行计数,然后让outclk1在计数的前一半时间里为低电平,在计数的后一半时间为高电平。第二个进程对下降沿计数,在计数的前一半时间里让outclk2为高电平,在计数的后一半时间里让outclk为低电平,这样outclk1与outclk2就有了半个时钟周期的时间上重合的高电平,让outclk1与outclk2相与,结果赋给q, q就是一个只有半个时钟周期是高电平,其余时间都是低电平的信号,最后让q与outclk1相或,得到的波形正好是占空比为50%的分频时钟波形。
图2-45 奇数分频仿真图
在实体的定义部分都用到了generic语句,用这个语句,可以使程序的一些参数在其他模块调用时直接修改即可,这使得模块的重复利用非常方便。
2.6 状态机的VHDL实现
状态机是一种描述或处理数字控制系统的方法。一般将数字控制系统看做是一个系统黑箱,当有来自环境的输入刺激时,不但会改变黑箱(系统)现状,同时黑箱还会有输出反应,改变后的系统状态称为次态。这个系统可能是一台机器设备或一个电路,用来表现其内部详细动作的方式,称为状态机。
2.6.1 状态机的结构和功能
通常,状态机是控制单元的主体,它接收外部信号及数据单元产生的状态信息,产生控制信号序列。状态机设计的关键是如何把一个实际的时序逻辑关系抽象成一个时序逻辑函数,传统的电路图输入法通过直接设计寄存器组来实现各个状态之间的转换。
VHDL的结构非常适合编写状态机,而且编写方式并非唯一,不同编写方式会影响电路的集成。状态机的设计主要用到case when与if then else两种语句。case when用来指定并行的行为,而if then else用来设定优先度的编码逻辑。
状态机的状态转换图如图2-46所示。该状态机有Idle、Tap1、Tap2、Tap3和Tap4这5个状态。当reset信号为’1’时,状态机复位到Idle状态,当reset信号为’0’时,状态机正常工作。状态机的状态转换关系是:当前状态是Idle,在时钟上升沿来临时,如果输入信号nw为’1',则转入下一状态Tap1,否则仍停留在原状态Idle;如当前状态是Tap1,下一状态是Tap2;如当前状态是Tap2,下一状态是Tap3;如当前状态是Tap3,下一状态是Tap4;如当前状态是Tap4,输入信号nw为’0’时下一状态为Idle,输入信号为’1’时,下一状态为Tap1。
图2-46 状态机的状态转换图
分析状态机的特点如下。
● 对于状态机状态的描述一般用一个枚举数据类型,语句如下:
TYPE state_type IS (idle, tap1, tap2, tap3, tap4);
● 对于存储当前状态的对象一般是一个信号,即:
SIGNAL filter : state_type;
● 对于状态机下一个状态的判断一般是通过对时钟上升沿判断的if then else语句内嵌case when语句。
● 对于状态机的输出则可以用一个条件或选择信号声明语句,或者再用一个case语句来实现输出信号。
2.6.2 状态机的VHDL模型
1.一般状态机的VHDL实现
状态机是应用寄存器来实现的,故用if then else语句内嵌case when语句来实现最有效,结合图2-46状态机的特点有以下完整的VHDL描述。
LIBRARY ieee; USE ieee.std_logic_1164.all; USE ieee.std_logic_unsigned.all; USE ieee.std_logic_arith.all; ENTITY state_m2 IS PORT(clk, reset, nw : in std_logic; sel: out std_logic_vector(1 downto 0); nxt, first: out std_logic); END state_m2; ARCHITECTURE logic OF state_m2 IS TYPE state_type IS (idle, tap1, tap2, tap3, tap4); --5个状态 SIGNAL filter : state_type; BEGIN PROCESS (reset, clk) BEGIN IF reset = '1' THEN filter <= idle; ELSIF clk' event and clk = '1' THEN CASE filter IS WHEN idle =>IF nw = '1' THEN --判断各状态 filter <= tap1; END IF; WHEN tap1 =>filter <= tap2; WHEN tap2 =>filter <= tap3; WHEN tap3 =>filter <= tap4; WHEN tap4 =>IF nw = '1' THEN filter <= tap1; ELSE filter <= idle; --若都不是,则空闲状态 END IF; END CASE; END IF; END process; output: PROCESS(filter) BEGIN CASE filter IS WHEN idle =>nxt <= '0' ; --由各状态输出 first <= '0' ; WHEN tap1 =>sel <= "00"; first <= '1' ; WHEN tap2 =>sel <= "01"; first <= '0' ; WHEN tap3 =>sel <= "10"; WHEN tap4 =>sel <= "11"; nxt <= '1' ; END CASE; END PROCESS output; END logic;
该程序的仿真波形如图2-47所示。
VHDL状态机的设计一般由时序电路模块实现,以上例子给出了5个状态转换的状态机实现,采用状态机的实现无论是仿真和实际运行都非常可靠。在实际应用中,可以用此原理设计TCP状态机、SDRAM控制状态机、Binary/Gray状态机等。
图2-47 状态机仿真图
2.单进程Moore型有限状态机
通常将状态机分成以下三大类型。
● Moore状态机:次态=f(现状),输出=f(现状)。
● Mealy状态机:次态=f(现状,输入),输出=f(现状,输入)。
● 混合型状态机。
其中,Moore状态机的输出信号是直接由状态寄存器译码得到的。单进程Moore型有限状态机的VHDL描述如下:
LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; ENTITY MOORE1 IS PORT (DATAIN :IN STD_LOGIC_VECTOR(1 DOWNTO 0); CLK, RST : IN STD_LOGIC; Q : OUT STD_LOGIC_VECTOR(3 DOWNTO 0)); END MOORE1; ARCHITECTURE behav OF MOORE1 IS TYPE ST_TYPE IS (ST0, ST1, ST2, ST3, ST4); SIGNAL C_ST : ST_TYPE ; BEGIN PROCESS(CLK, RST) BEGIN IF RST ='1' THEN C_ST <= ST0 ; Q<= "0000" ; ELSIF CLK' EVENT AND CLK='1' THEN CASE C_ST IS WHEN ST0 => IF DATAIN ="10" THEN C_ST <= ST1 ; ELSE C_ST <= ST0 ; END IF; Q <= "1001" ; WHEN ST1 => IF DATAIN ="11" THEN C_ST <= ST2 ; ELSE C_ST <= ST1 ; END IF; Q <= "0101" ; WHEN ST2 => IF DATAIN ="01" THEN C_ST <= ST3 ; ELSE C_ST <= ST0 ; END IF; Q <= "1100" ; WHEN ST3 => IF DATAIN ="00" THEN C_ST <= ST4 ; ELSE C_ST <= ST2 ; END IF; Q <= "0010" ; WHEN ST4 => IF DATAIN ="11" THEN C_ST <= ST0 ; ELSE C_ST <= ST3 ; END IF; Q <= "1001" ; WHEN OTHERS => C_ST <= ST0; END CASE; END IF; END PROCESS; END behav;
该程序的仿真波形如图2-48所示。
图2-48 单进程Moore有限状态机仿真图
3.Mealy型有限状态机
Mealy状态机与Moore状态机不同的是,Mealy状态机是以现时的输入信号结合即将变成次态的现状编码成输出信号。Mealy型有限状态机VHDL描述如下:
LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; ENTITY MEALY1 IS PORT ( CLK , DATAIN, RESET : IN STD_LOGIC; Q : OUT STD_LOGIC_VECTOR(4 DOWNTO 0)); END MEALY1; ARCHITECTURE behav OF MEALY1 IS TYPE states IS (st0, st1, st2, st3, st4); SIGNAL STX : states ; BEGIN COMREG : PROCESS(CLK, RESET) BEGIN --决定转换状态的进程 IF RESET ='1' THEN STX <= ST0; ELSIF CLK' EVENT AND CLK = '1' THEN CASE STX IS WHEN st0 => IF DATAIN = '1' THEN STX <= st1; END IF; WHEN st1 => IF DATAIN = '0' THEN STX <= st2; END IF; WHEN st2 => IF DATAIN = '1' THEN STX <= st3; END IF; WHEN st3=> IF DATAIN = '0' THEN STX <= st4; END IF; WHEN st4=> IF DATAIN = '1' THEN STX <= st0; END IF; WHEN OTHERS => STX <= st0; END CASE ; END IF; END PROCESS COMREG ; COM1: PROCESS(STX, DATAIN) BEGIN --输出控制信号的进程 CASE STX IS WHEN st0 => IF DATAIN = '1' THEN Q <= "10000" ; ELSE Q<="01010" ; END IF ; WHEN st1 => IF DATAIN = '0' THEN Q <= "10111" ; ELSE Q<="10100" ; END IF ; WHEN st2 => IF DATAIN = '1' THEN Q <= "10101" ; ELSE Q<="10011" ; END IF ; WHEN st3=> IF DATAIN = '0' THEN Q <= "11011" ; ELSE Q<="01001" ; END IF ; WHEN st4=> IF DATAIN = '1' THEN Q <= "11101" ; ELSE Q<="01101" ; END IF ; WHEN OTHERS => Q<="00000" ; END CASE ; END PROCESS COM1 ; END behav;
以上程序的仿真波形如图2-49所示。
图2-49 Melay有限状态机仿真图
4.用状态机实现序列检测器
序列检测器可用于检测一组或多组由二进制码组成的脉冲序列信号,当序列检测器连续收到一组串行二进制码后,如果这组码与检测器中预先设置的码相同,则输出1,否则输出0。由于这种检测的关键在于收到的正确码必须是连续的,这就要求检测器必须记住前一次的正确码及正确序列,直到在连续的检测中所收到的每一位码都与预置数的对应码相同。在检测过程中,任何一位不相等都将回到初始状态重新开始检测,电路完成序列数“11100101”。当这一串序列数高位在前(左移)串行进入检测器后,若此数与预置的密码数相同,则输出"A",否则仍然输出"B"。
LIBRARY IEEE ; USE IEEE.STD_LOGIC_1164.ALL; ENTITY SCHK IS PORT( DIN, CLK, CLR : IN STD_LOGIC ; --串行输入数据位/工作时钟/复位信号 AB : OUT STD_LOGIC_VECTOR(3 DOWNTO 0)); --检测结果输出 END SCHK; ARCHITECTURE behav OF SCHK IS SIGNAL Q : INTEGER RANGE 0 TO 8 ; SIGNAL D : STD_LOGIC_VECTOR(7 DOWNTO 0); --8位待检测预置数 BEGIN D <= "11100101 " ; --8位待检测预置数 PROCESS( CLK, CLR ) BEGIN IF CLR = '1' THEN Q <= 0 ; ELSIF CLK' EVENT AND CLK='1' THEN --时钟到来时,判断并处理当前输入的位 CASE Q IS WHEN 0=> IF DIN = D(7) THEN Q <= 1 ; ELSE Q <= 0 ; END IF ; WHEN 1=> IF DIN = D(6) THEN Q <= 2 ; ELSE Q <= 0 ; END IF ; WHEN 2=> IF DIN = D(5) THEN Q <= 3 ; ELSE Q <= 0 ; END IF ; WHEN 3=> IF DIN = D(4) THEN Q <= 4 ; ELSE Q <= 0 ; END IF ; WHEN 4=> IF DIN = D(3) THEN Q <= 5 ; ELSE Q <= 0 ; END IF ; WHEN 5=> IF DIN = D(2) THEN Q <= 6 ; ELSE Q <= 0 ; END IF ; WHEN 6=> IF DIN = D(1) THEN Q <= 7 ; ELSE Q <= 0 ; END IF ; WHEN 7=> IF DIN = D(0) THEN Q <= 8 ; ELSE Q <= 0 ; END IF ; WHEN OTHERS => Q <= 0 ; END CASE ; END IF ; END PROCESS ; PROCESS( Q ) --检测结果判断输出 BEGIN IF Q = 8 THEN AB <= "1010" ; --序列数检测正确,输出 "A" ELSE AB <= "1011" ; --序列数检测错误,输出 "B" END IF ; END PROCESS ; END behav ;
程序的仿真波形如图2-50所示。
图2-50 序列检测器仿真图
5.用状态机对ADC0809的采样控制电路实现
ADC0809是CMOS的8位A/D转换器,片内有8路模拟开关,可控制8个模拟量中的一个进入转换器中。ADC0809的分辨率为8位,转换时间约100us,含锁存控制的8个多路开关,输出有三态缓冲器控制,单5V电源供电。
ADC0809主要控制信号说明如图2-51所示,START是转换启动信号,高电平有效;ALE是3位通道选择地址(ADDC、ADDB、ADDA)信号的锁存信号。当模拟量送至某一输入端(如IN1或IN2等),由3位地址信号选择,而地址信号由ALE锁存;EOC是转换情况状态信号(类似于AD574的STATUS),当启动转换约100us后,EOC产生一个负脉冲,以示转换结束;在EOC的上升沿后,若使输出使能信号OE为高电平,则控制打开三态缓冲器,把转换好的8位数据结果输至数据总线,至此ADC0809的一次转换结束了。
图2-51 ADC0809主要控制信号
--ADC0809的采样控制电路-- LIBRARY IEEE; USE IEEE.STD_LOGIC_1164.ALL; ENTITY ADCINT IS PORT ( D : IN STD_LOGIC_VECTOR(7 DOWNTO 0); --0809的8位转换数据 CLK , EOC : IN STD_LOGIC; --CLK是转换工作时钟 LOCK1, ALE, START, OE, ADDA : OUT STD_LOGIC; Q : OUT STD_LOGIC_VECTOR(7 DOWNTO 0) ); END ADCINT; ARCHITECTURE behav OF ADCINT IS TYPE states IS (st0, st1, st2, st3, st4, st5, st6) ; --定义各状态子类型 SIGNAL current_state, next_state: states :=st0 ; SIGNAL REGL : STD_LOGIC_VECTOR(7 DOWNTO 0); SIGNAL LOCK : STD_LOGIC; -- 转换后数据输出锁存时钟信号 BEGIN ADDA <= '1' ; LOCK1 <=LOCK; PRO: PROCESS(current_state, EOC) BEGIN --规定各状态转换方式 CASE current_state IS WHEN st0 => ALE<='0' ; START<='0' ; OE<='0' ; LOCK<='0' ; next_state <= st1; WHEN st1 => ALE<='1' ; START<='0' ; OE<='0' ; LOCK<='0' ; next_state <= st2; WHEN st2 => ALE<='0' ; START<='1' ; OE<='0' ; LOCK<='0' ; next_state <= st3; WHEN st3 => ALE<='0' ; START<='0' ; OE<='0' ; LOCK<='0' ; IF (EOC='1' ) THEN next_state <= st3; --测试EOC的下降沿 ELSE next_state <= st4; END IF ; WHEN st4=> ALE<='0' ; START<='0' ; OE<='0' ; LOCK<='0' ; IF (EOC='0' ) THEN next_state <= st4; --测试EOC的上升沿,等于1表明转换结束 ELSE next_state <= st5; --继续等待 END IF ; WHEN st5=> ALE<='0' ; START<='0' ; OE<='1' ; LOCK<='0' ; next_state <= st6; WHEN st6=> ALE<='0' ; START<='0' ; OE<='1' ; LOCK<='1' ; next_state <= st0; WHEN OTHERS => ALE<='0' ; START<='0' ; OE<='0' ; LOCK<='0' ; next_state <= st0; END CASE ; END PROCESS PRO ; PROCESS (CLK) BEGIN IF ( CLK' EVENT AND CLK='1' ) THEN current_state <= next_state; -- 在时钟上升沿,转换至下一状态 END IF; END PROCESS; -- 由信号current_state将当前状态值带出此进程,进入进程PRO PROCESS (LOCK) -- 此进程中,在LOCK的上升沿,将转换好的数据锁入 BEGIN IF LOCK='1' AND LOCK' EVENT THEN REGL <= D ; END IF; END PROCESS ; Q <= REGL; END behav;
程序的仿真波形如图2-52所示。
图2-52 ADC0809采样仿真图
2.7 存储器的VHDL实现
本节主要介绍存储器的VHDL实现。
2.7.1 ROM和RAM
存储器按类型分可以分为只读存储器ROM和随机存储器RAM,它们的功能有较大的区别,因此在描述上也有较大区别,但更多的是共同之处。
从应用的角度出发,各个公司的编译器都提供了或多或少的库文件,可以帮助减轻编程难度,并加快编程进度,这些模块符合工业标准,有伸缩性,应用非常方便。LPM(Library Parameterized Megafunction,可调参数元件)是Altera公司功能强大、性能良好的类似于IP Core的兆功能块LPM库。调用LPM部件lpm_rom和lpm_ram_dq,这也是编程者可以思考的一条途径,即如何充分利用现有资源,更快更好地编制程序。
如图2-53所示,嵌入式阵列块EAB是在输入、输出口上带有寄存器的RAM块,是由一系列的嵌入式RAM单元构成。
图2-53 EAB构成不同结构的RAM和ROM
1.ROM(只读存储器)
在Altera公司的编译软件中提供了LPM库文件,其中包括了很多常用元器件的VHDL程序包,编程者可以直接调用元件,从而减轻编程强度。
lpm_rom是LPM库中一个标准程序包文件,它的端口定义如下:
COMPONENT lpm_rom GENERIC (LPM_WIDTH: POSITIVE; --q宽度 LPM_TYPE: STRING := "LPM_ROM"; LPM_NUMWORDS: NATURAL := 0; --存储字的数量 LPM_FILE: STRING; --初始化文件.mif/.hex LPM_ADDRESS_CONTROL: STRING := "REGISTERED"; --地址端口是否注册 LPM_OUTDATA: STRING := "REGISTERED"; --q端口是否注册 LPM_HINT: STRING := "UNUSED"); PORT (address: IN STD_LOGIC_VECTOR(LPM_WIDTHAD-1 DOWNTO 0); --输入到存储器的地址 inclock: IN STD_LOGIC := '0' ; outclock: IN STD_LOGIC := '0' ; --输入/出寄存器时钟 memenab: IN STD_LOGIC := '1' ; q: OUT STD_LOGIC_VECTOR(LPM_WIDTH-1 DOWNTO 0)); END COMPONENT;
端口各信号描述如表2-7所示。
表2-7 lpm_rom端口信号说明
端口各参数描述如表2-8所示。
表2-8 lpm_rom端口参数说明
对于该模块的引用的VHDL语句如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; LIBRARY lpm; USE lpm.lpm_components.ALL; LIBRARY work; USE work.ram_constants.ALL; ENTITY rom256x8 IS PORT(memenab: IN STD_LOGIC ; address: IN STD_LOGIC_VECTOR (7 DOWNTO 0); data: OUT STD_LOGIC_VECTOR (7 DOWNTO 0)); we, inclock, outclock: IN STD_LOGIC; q: OUT STD_LOGIC_VECTOR (DATA_WIDTH -1 DOWNTO 0)); END rom256x8; ARCHITECTURE example OF rom256x8 IS signal inclock, outclock:std_logic; COMPONENT lpm_rom --声明ROM GENERIC (LPM_WIDTH: POSITIVE; LPM_TYPE: STRING := "LPM_ROM"; LPM_WIDTHAD: POSITIVE; LPM_NUMWORDS: NATURAL := 0; LPM_FILE: STRING; LPM_ADDRESS_CONTROL: STRING := "REGISTERED"; LPM_OUTDATA: STRING := "REGISTERED"; LPM_HINT: STRING := "UNUSED"); PORT (address: IN STD_LOGIC_VECTOR(LPM_WIDTHAD-1 DOWNTO 0); inclock: IN STD_LOGIC := '0' ; outclock: IN STD_LOGIC := '0' ; memenab: IN STD_LOGIC := '1' ; q: OUT STD_LOGIC_VECTOR(LPM_WIDTH-1 DOWNTO 0)); END COMPONENT; BEGIN inclock<='0' ; outclock<='0' ; inst_1: lpm_rom GENERIC --256*8bit MAP(8, "LPM_ROM",8,256, "inst_1.mif", "UNUSED", "UNUSED", "UNUSED") PORT MAP ( address , memenab, inclock, outclock, data ); END example;
在这个例子中不用时钟锁存和地址锁存,所以输入时钟和输出时钟都接到’0'。程序中要注意的一点是初始化文件的书写,初始化文件可以有两种类型,“*.mif”文件和“*.hex”文件,本例用到了“*.mif”文件。初始化文件名为“inst_1.mif”,内容如下:
DEPTH = 256; WIDTH = 8; ADDRESS_RADIX = HEX; DATA_RADIX = HEX; CONTENT BEGIN [0..FF] : 00; 1 : 4B 49 4D 4A 49 4E 53 54 55 44 49 4F; F : 4E 41 4E 4B 41 49 45 45; END ;
此文件定义了ROM初始化数据为全零,其中一部分空间存储了asc2码数据。调入ROM元件时(可用LPM_ROM或用MegaWizard Plug-In Manager调入)软件会问初始化文件的名字,若还没有做好这个文件,则可以先填一个文件名如test.mif或test.hex(test这个文件现在并不存在),完成设计后编译,再建立波形文件,初始化该Project中ROM内容的表格。当编辑初始化文件并输出成*.mif或*.hex文件,而且需要再次编译,才算完成。
2.RAM(随机存储器)
lpm_ram_dq是LPM库中的一个标准包文件,它的端口定义如下:
COMPONENT lpm_ram_dq GENERIC (LPM_WIDTH: POSITIVE; LPM_TYPE: STRING := "LPM_RAM_DQ"; LPM_WIDTHAD: POSITIVE; --address宽度 LPM_NUMWORDS: NATURAL : = 0; LPM_FILE: STRING := "UNUSED"; LPM_INDATA: STRING := "REGISTERED"; LPM_ADDRESS_CONTROL: STRING := "REGISTERED"; LPM_OUTDATA: STRING := "REGISTERED"; LPM_HINT: STRING := "UNUSED"); PORT (data: IN STD_LOGIC_VECTOR(LPM_WIDTH-1 DOWNTO 0); address: IN STD_LOGIC_VECTOR(LPM_WIDTHAD-1 DOWNTO 0); we: IN STD_LOGIC; inclock: IN STD_LOGIC := '0' ; outclock: IN STD_LOGIC := '0' ; q: OUT STD_LOGIC_VECTOR(LPM_WIDTH-1 DOWNTO 0)); END COMPONENT;
各端口信号说明如表2-9所示。
表2-9 lpm_ram_dq端口信号说明
各端口参数说明如表2-10所示。
表2-10 lpm_ram_dq端口参数说明
对于该模块的调用的VHDL程序如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_arith.all; use ieee.std_logic_unsigned.all; PACKAGE ram_constants IS constant DATA_WIDTH : INTEGER := 8; --数据总线宽度 constant ADDR_WIDTH : INTEGER := 8; --地址总线宽度 END ram_constants; LIBRARY ieee; USE ieee.std_logic_1164.ALL; LIBRARY lpm; USE lpm.lpm_components.ALL; LIBRARY work; USE work.ram_constants.ALL; ENTITY ram256x8 IS PORT(data: IN STD_LOGIC_VECTOR (DATA_WIDTH-1 DOWNTO 0); address: IN STD_LOGIC_VECTOR (ADDR_WIDTH-1 DOWNTO 0); we, inclock, outclock: IN STD_LOGIC; q: OUT STD_LOGIC_VECTOR (DATA_WIDTH -1 DOWNTO 0)); END ram256x8; ARCHITECTURE example OF ram256x8 IS BEGIN inst_1: lpm_ram_dq GENERIC MAP (lpm_widthad => ADDR_WIDTH, lpm_width => DATA_WIDTH) PORT MAP (data => data, address => address, we => we, inclock => inclock, outclock => outclock, q => q); END example;
2.7.2 FIFO
一个512×8bit的FIFO,电路结构如图2-54所示。
图2-54 FIFO电路结构
FIFO的原理及实现如下。
● FIFO是先进先出堆栈。程序中分配512(0~511)个单元,记为stack。有读写指针pr和pw, DataOut端口显示pr指向的内容即stack(pr)。当时钟上升沿到来若rd='0’并且堆栈不空则pr=pr+1,如果wr='0’并且堆栈不满则stack(pw)=DataIn, pw=pw+1。这里511加1是0,在程序中不用特别的处理,让其自动溢出即可。
● 堆栈空empty和堆栈满full(高电平有效)的设置方法。
full的设置方法:full变0的条件为pw = pr and wr='0' and rd='1' ; full变1的条件为full='0' and wr='1' and wr='0'。
empty的设置方法和实际的电路有关,可以分开两类:rdaddress输入端(即pr)有寄存器的和没有寄存器的,如图2-55所示。
图2-55 empty的设置方法分类
● 没寄存器的:empty变0的条件为pr+2=pw and rd='0' and wr='1';变1的条件为empty=0 and rd='1' and wr='0'。
● 有寄存器的:empty变0的条件为pr+1=pw and rd='0' and wr='1';变1的条件empty=0 and rd='1' and wr='0'。
FIFO的VHDL实现如下:
library ieee; use ieee.std_logic_1164.all; use ieee.std_logic_unsigned.all; entity fifo is port( DataIn :in std_logic_vector(7 downto 0); DataOut :out std_logic_vector(7 downto 0); clk :in std_logic; --rising is valid clr :in std_logic; --clear '0' is valid wr, rd :in std_logic; --write '0' is valid, read '0' is valid empty :out std_logic; --stack is empty, '1' is valid full :out std_logic ); --stack is full, '1' is valid end fifo; architecture beh of fifo is COMPONENT altdpram --使用MagaWizard Plug-in Manager GENERIC (WIDTH: NATURAL; WIDTHAD: NATURAL; INDATA_REG: STRING; WRADDRESS_REG : STRING; WRCONTROL_REG : STRING; RDADDRESS_REG : STRING; RDCONTROL_REG : STRING; OUTDATA_REG : STRING; INDATA_ACLR : STRING; WRADDRESS_ACLR : STRING; WRCONTROL_ACLR : STRING; RDADDRESS_ACLR : STRING; RDCONTROL_ACLR : STRING; OUTDATA_ACLR : STRING; LPM_HINT : STRING); PORT ( wren : IN STD_LOGIC ; inclock : IN STD_LOGIC ; q : OUT STD_LOGIC_VECTOR (7 DOWNTO 0); data : IN STD_LOGIC_VECTOR (7 DOWNTO 0); rdaddress: IN STD_LOGIC_VECTOR (8 DOWNTO 0); wraddress: IN STD_LOGIC_VECTOR (8 DOWNTO 0)); END COMPONENT; signal pw:std_logic_vector(9-1 downto 0); --指向下一个写单元 signal pr:std_logic_vector(9-1 downto 0); --指向当前读单元 signal sub_full, sub_empty:std_logic; --connect to full and empty signal wren:std_logic; --connect to dp_ram wren begin altdpram_component: altdpram GENERIC MAP ( --调用MagaWizard模块 WIDTH => 8, WIDTHAD => 9, INDATA_REG => "INCLOCK", WRADDRESS_REG => "INCLOCK", WRCONTROL_REG => "INCLOCK", RDADDRESS_REG => "UNREGISTERED", RDCONTROL_REG => "UNREGISTERED", OUTDATA_REG => "UNREGISTERED", INDATA_ACLR => "OFF", WRADDRESS_ACLR => "OFF", WRCONTROL_ACLR => "OFF", RDADDRESS_ACLR => "OFF", RDCONTROL_ACLR => "OFF", OUTDATA_ACLR => "OFF", LPM_HINT => "USE_EAB=ON") PORT MAP(wren => wren, --when wren is '0' disable write inclock => clk, data => DataIn, rdaddress => pr, wraddress => pw, q => DataOut); wren<=(not sub_full)and(not wr); --when not full and wr='0' 允许写 full<=sub_full; empty<=sub_empty; --------------- 读写过程--------------- process(clk, clr) begin if(clr = '0' )then pw<="000000000"; pr<="111111111"; elsif(clk' event and clk='1' )then if(sub_full='0' and wr='0' )then --写过程 pw<=pw+"000000001"; end if; if(sub_empty ='0' and rd='0' )then --读过程 pr<=pr+"000000001"; end if; end if; end process; --------------- 处理堆栈空、满--------------- process(clk, clr) variable Nextpr:std_logic_vector(9-1 downto 0); begin if(clr='0' )then sub_empty<='0' ; elsif(clk' event and clk='1' )then if(rd='0' and wr='1' )then --only read , no write Nextpr:=pr+"000000010"; if(Nextpr=pw)then sub_empty<='1' ; end if; elsif(wr='0' and rd='1' and sub_empty='1' )then --cancel empty sub_empty<='0' ; end if; end if; end process; process(clk, clr) begin if(clr='0' )then sub_full<='0' ; elsif(clk' event and clk='1' )then if(rd='1' and wr='0' )then --only write, no read if(pr=pw)then sub_full<='1' ; end if; elsif(wr='1' and rd='0' and sub_full='1' )then --cancel full sub_full<='0' ; end if; end if; end process; end beh;
最后可以使用器件EPF10K30ETC144-1,查看.rpt资源使用情况。
Total dedicated input pins used : 2/6 (33%) Total I/O pins used : 20/96 (20%) Total logic cells used : 66/1728 (3%) Total embedded cells used : 8/96 (8%) Total EABs used : 1/6 (16%) Average fan-in : 2.77/4 (69%) Total fan-in : 183/6912 (2%)
2.8 本章小结
本章对VHDL语言编程技术做了系统介绍,读者通过学习,可了解VHDL语言的结构、组成要素,熟悉常见组合逻辑电路、时序逻辑电路、状态机及存储器的实现过程,为后面的程序设计打下良好的语言基础。