第四章VerilogHDL设计技巧.docx
《第四章VerilogHDL设计技巧.docx》由会员分享,可在线阅读,更多相关《第四章VerilogHDL设计技巧.docx(69页珍藏版)》请在冰豆网上搜索。
![第四章VerilogHDL设计技巧.docx](https://file1.bdocx.com/fileroot1/2023-2/4/00eb862c-2dce-46ec-9230-d71e3010bef4/00eb862c-2dce-46ec-9230-d71e3010bef41.gif)
第四章VerilogHDL设计技巧
第四章、VerilogHDL设计技巧
本章通过一些简单的实例演示一下如何在veriloghdl中实现看似不可能实现的技巧,主要包括
1.双向端口的使用
2.PWM波形的产生
3.常见几种分频器的设计
4.巧用存储器定义语句实现存储器设计
5.基于存储器的DDS设计
6.有限状态机
本章讲述一些常用的verilog设计方法和技巧,可以加速实际的工程应用,提高效率。
双向端口
双向端口在应用过程中常常用到,如在进行和存储器的接口设计时。
由于存储器的数据线是双向信号,故FPGA的端口也必须用双向端口才能够连接。
VerilogHDL中的双向端口关键字为inout,如inoutdat,则表示dat为一个双向(既可以输入也可以输出)的端口。
在硬件中为了更好的处理端口,大多数情况下都是使用单向端口,即input或者output。
这样更容易对电路的逻辑进行描述。
双向端口也往往转换成两个单向的端口进行操作。
仔细分析一下,当一个端口作为双向端口时,实际上是分时的输入和输出,也就是说,当内部逻辑需要双向端口作为输入时,这时候是用双向的输入功能,反之,用输出功能。
假设”内部需要”为一个变量,暂且命名为dir,则,双向端口转化为两个单向端口的电路就可以如下图所示。
图中,实线框是整个的逻辑设计电路模块,io为此模块的双向端口。
虚线框为将双向端口转化为两个单向端口的子模块,两个三角形的电路为三态门,上面的三态门低电平使能,下面的三态门高电平使能。
当需要将io端作为输入时候,即dir为高(假设为高电平时使用io端的输入功能),上面的三态门被关闭,下面的三态门打开,于是信号的流向从io口经过下面的三态门由a端输出,那么这个时候其他逻辑部分就可以使用a端的信号进行运算处理;反之,当内部的其他逻辑需要输出信号至io双向端口时,可以置dir为低电平,上面的三态门被打开,下面的三态门被关闭,这样b端口的信号经由上面的三态门输出至io端。
这样,虚线框中的子模块就完成了双向端口转化为两个独立的单向端口(a和b)的功能。
代码如下:
代码注释:
(1)声明io端口为双向端口,一般情况下双向端口的类型声明为wire型,这里省略,默认为wire型。
(2)当dir为高电平时,io端口赋值给a端口,否则,a端为高阻态,也可以写成assigna=(dir)?
io:
1’bz;。
(3)当dir为低电平时,b端口赋值给io端口,否则,io端赋值成高阻态,也可以写成assignio=(!
dir)?
b:
1’bz;。
上面电路中用到了三态门,通常来说,三态门在fpga和cpld内部不推荐使用,而且一般仅限于fpga或者是cpld的引脚端才有,内部没有三态门。
如果在设计过程中大量采用三态门的话,会使芯片的功耗大幅增加,资源浪费严重。
仔细研究发现,可以在这里省略一个三态门。
新电路如下:
因为io的双向特性是分时使用的,所以在内部其他逻辑使用a端和b端的时候也是分时使用的,换句话说,当内其他逻辑部向b写的时候,无论a端是什么值对内部的其他电路时没有影响的,因为这个时候内部其他的逻辑不读取a端口;反过来,当内部需要读取a端的值的时候,a端的值必须来自于io端的值,故必须把b端和io端断开,即三态门呈现3态即可。
新的代码如下:
代码注释:
(1)直接赋值即可。
PWM波形的产生
脉冲宽度调制(PWM),是英文“PulseWidthModulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术,广泛应用在从测量、通信到功率控制与变换的许多领域中。
PWM波形本质上是宽度可以变化的脉冲。
见下图
图中,从A到C为一个脉冲的周期,AB之间电平为高电平,称作这个脉冲周期的宽度,PWM是指AB之间的高电平宽度可以调节。
若想用数字电路来实现pwm,不妨借鉴一下模拟电路中的pwm是如和生成的。
先产生锯齿波,然后利用锯齿波的电压和另一个门限电压做比较,当门限电压变化的时候,比较器输出的脉冲宽度就变化了。
示意图如下:
由上图比较得知,当门限电平电压升高时,比较器的输出的占空比宽度在减少,即达到了脉冲宽度调制的目的。
那么,在数字领域里,门限电平可以由一个任意的常量或者用端口输入来实现,怎么样去实现锯齿波呢?
仔细研究锯齿波,电压随着时间按照一定的比例上升(也就是增加),有点类似计数器的味道。
而实际上,如果将计数器的输出的数字量进行DA转换的话,看到的输出就是锯齿波。
这里以8位的计数器和门限宽度为例,设计一个宽度可调的脉冲输出。
代码注释:
(1)设置计数用的变量,位宽为8位。
和门限宽度一样。
(2)计数器计数,相当于模拟波形中的锯齿波
(3)比较器电路,当锯齿波值大于或等于门限时,pwm输出高电平,否则,输出低电平。
可想,当门限输入的值发生变化时,pwm输出的脉冲宽度一定随着变化。
使用QuartusII仿真如下图:
由上图可见,计数器的输出cntr呈现出的结果(数字结果的模拟显示)就是锯齿波,当门限thres为10的时候,pwm输出的脉宽很大,thres为128时,输出近似50%的脉冲(方波),thres为240时候,pwm的输出脉冲宽度很窄。
实现了pwm输出脉冲宽度受thres门限值的调制。
PWM在很多场合有重要的应用,例如,直流电机驱动,步进电机驱动、呼吸灯等。
分频器
计数器和分频器是VerilogHDL设计中最基础的时序电路的组成部分。
分频器指的是一个系统能够将输入的系统时钟信号转变成较低频率信号的输出。
例如:
4分频电路就是指输出的时钟频率是输入时钟频率的四分之一。
那么,如何实现呢?
仔细分析输出和输入的关系就可以得到答案。
先看下图:
这里有2个方波,上面的波形每个周期被虚竖线隔开,可以看出下面波形的周期为上面的波形的4倍。
如果说上面的波形为输入的时钟信号,下面的波形为分频器输出的信号,那么说这个输出就是输入的4分频。
仔细研究上图,可以看出,如果要输出4分频信号的话,可以对输入的信号进行模4计数(即0~3),当计数值为0和1时,将输出信号的电平置为低电平;当计数值为2和3的时候将输出信号的电平置为高电平。
由于计数是模4的,所以计数值就会在0~3之间不停的计数,而输出信号也会不停的输出低电平、高电平、低电平…,这样4分频的输出信号就实现了!
仿真图如下:
由仿真图可以看出,clko输出信号的频率的确为输入clki的频率的1/4。
为了能更加清楚地了解Verilog语言的特点,请把上面代码作如下修改,然后体会一下assign和always的区别。
用功能仿真验证一下结果。
并且与assign语句的仿真结果作比较,
在刚才的4分频中,输出信号时方波,如果说输出信号不是方波的话,但只要频率是输入的1/4,就称之为4分频。
如下图,波形2和波形3都是输入信号的4分频。
以下以波形2的输出为例,演示这种占空比为75%的分频输出。
同理也可以完成占空比为25%的4分频输出,代码请读者自己完成。
2分频
2分频和4分频一样,首先要构建一个模2计数器,当计数器值为0时,输出低电平;当计数器值为1时,输出高电平。
这样计数器每计2个数就可以得到一个周期的输出,故是2分频。
2分频的另外一种思想:
如果说输入信号每个信号上升沿到来的时候,都将输出端进行电平翻转一下(原来是低电平,现在变高电平;原来是高电平,现在变成低电平。
)这样,输出端若要得到一个周期就需要翻转2次,也就是2个输入时钟的上升沿,也就是输入2个周期得到一个周期,也即:
2分频。
代码如下:
通过波形仿真可以验证2分频的正确性。
这种二分频的方式用的比较频繁,也推荐大家在以后的应用中使用。
接下来进行三分频的学习。
根据前面的学习,可以得出,三分频可能有2中波形输出,见下图:
在上面的波形中,2中输出的占空比分别是33%和67%,没有50%的占空比输出波形了。
如果此时想得到50%的分频输出怎么办?
仔细研究上图,会发现,如果输出了50%的占空比波形的话,那么,该输出的边沿会处于输入时钟的上升沿和下降沿时刻。
看下图:
波形3为50%的方波,看图中虚线处,方波输出在输入时钟的下降沿和上升沿均有变化。
所以,这里要想到,如果使用下降沿做分频,效果如何呢?
为了便于比较,下图以33%占空比为例,分别用下降沿和上升沿进行分频。
图中,点虚线为一个周期,这个周期输出下降沿的分频信号,线虚线为上升沿分频的一个周期。
大家会发现,两个输出“错位”了半个输入时钟周期,按照50%的计算,输出脉冲宽度为1.5个输入时钟周期,如果说将两个33%的分频输出信号相“或”的话,输出不就是50%占空比了嘛!
3分频50%输出占空比的代码如下:
代码注释:
(1)cnt1为上升沿分频用的计数器值
(2)/*synthesiskeep*/作用为了防止QuartusII综合器对该节点进行优化,很多时候由于优化的作用,在电路内部节点可能被优化掉(不存在了),使用它,综合时候就会保留该节点。
这里为了能够在波形仿真的时候看到,故做此设置。
注意它的位置,在变量定义和“;”之间,如果写在分号的后面就无效了。
(3)这个always也可以写成always@(*)if(cnt1>0)o1<=0;elseo1<=1;或者always@(*)if((cnt1==1)||(cnt1==2))o1<=0;elseo1<=1;从逻辑角度上讲,只要有1个时钟周期的高电平输出即可。
(4)cnt2为下降沿分频用的计数器值。
不同的计数器要用不同的变量计数,因为在硬件描述语言中,变量对应着硬件,一个硬件只能有一个固定功能(逻辑上分时处理的除外)。
(5)同(3),可以有多种写法,读者可以进行尝试验证。
刚才使用了33%的占空比输出信号相或得到了50%的占空比输出,同理,使用67%的上升沿分频和下降沿分频也可以得到50%的输出,原理见下图:
使用两个输出信号的相“与”操作,可以得到50%的占空比输出。
代码请读者自行完成。
以上使用的是3分频输出50%的占空比输出,其实,同样的原理适用于所有的奇数分频。
因为奇数分频若想得到50%的占空比输出,则必然有半个周期的宽度脉冲出现(例如,5分频,50%占空比,则脉冲宽度为2.5个时钟周期),而上升沿和下降沿恰好就是半个周期的差别,因此,无论任何奇数,只要用此方法均可得到50%分频输出。
只是要注意,上升沿计数和下降沿计数要同一个时钟周期开始,这可以通过设计一个异步的复位端来实现,读者需细心揣摩。
半整数分频
所谓半整数分频指的是N.5分频,N为整数。
比如2.5分频。
如果输入频率为50Hz,则输出为20Hz。
在前面的整数分频中,都是以上升(或下降)沿作为计数的,所以每计数一个则为一个周期,不会出现.5的效果。
这里采用反推导方式来进行。
假设已经得到了分频后的结果。
根据刚才的推导,直接使用输入时钟去计数不可能得到半整数的分频效果。
所以必须使用另外的时钟来计数。
图中,由于没有明确指出分频后的占空比,图中仅给出半个周期的脉冲宽度,不过周期是固定的,就是2.5个输入时钟。
为了明确分频后的周期,图中以分频后输出信号的2分频给出波形的。
这里大胆进行一下尝试,如果说,用输出信号的2分频和输入时钟做“异或”操作,然后再用异或后的时钟作为分频时钟参与动作,会是什么样的结果呢?
先给出“异或”后的波形。
最后面一行是异或后的时钟信号。
可以看出时钟的周期在分频后的2个周期连接处发生了变化。
这里用它,代替原输入时钟进行计数和分频尝试。
代码说明:
(1)这儿使用新的时钟信号作为计数时钟。
(2)因verilog不支持小数2.5,采用整数3作为计数的模,经验证可以发现,如果写成整数2,则进行的是1.5分频。
(3)这里可以修改2.5分频输出脉冲的宽度,如果改为if(cntr<=1),则输出占空比为1.5/2.5。
可以通过实践验证。
仿真波形如下:
通过查看计数器的技术过程,可以看出,在计数器的值由2变到0的时候,实际的newclk是没有脉冲上升沿的。
因为这里采用了一个相互关联的连带性输出导致了该结果,可以看出,newclk间接地产生了clko,而clko又间接地影响了newclk的生成。
此处无法直接推导出。
有的文献上把这种方式称作脉冲扣除电路,它有一套成型的公式,根据这个公式,任何半整数分频都可以很容易设计出来。
N.5分频电路构成。
另一种形式的半整数分频
思想:
如果说能够在一个时钟周期内计数2个的话,半整数分频就可以转化成整数分频了。
例如2.5分频,在2.5个周期内能计5个数,则,可以在计数1个的时候输出高电平,其余值时输出低电平。
那么,怎么样才能在1个周期内计2个数呢?
有的人立刻想到,always@(posedgeclk,negedgeclk)cntr<=cntr+1;这种方式,这是错误的。
因为硬件描述语言是对硬件的描述,硬件上不存在的逻辑(即现实的触发器都只有1个输入时钟),即使语句语法没有错误,也是不能生成硬件的。
这里给出QuartusII对这种方式提出的错误报告,供大家参考。
Error(10239):
VerilogHDLAlwaysConstructerroratfreqdiv2dot5.v(23):
eventcontrolcannottestforbothpositiveandnegativeedgesofvariable"clk"。
那么怎样才能实现上升下降沿同时计数呢?
试着想一下,当上升沿计数后,紧接着下降沿到来,那么下降沿计数必须是在上升沿计数值的基础上进行加1操作,同理,再出现上升沿又必须在之前的下降沿计数基础上进行+1;或者换种说法,不管哪个沿计数,都必须在最终计数器的值上进行加1计数。
于是,这里需要设计多个计数器,上升沿计数器posecntr,下降沿计数器negcntr,和最终的计数器的值cntr。
以模5计数为例,这三者之间的关系是这样的,cntr在正常计数过程中应该是posecntr和negcntr中的最大值(谁大取谁);当计数值到达模值时候,再计数(无论是上升沿还是下降沿)应为0(假设做的是加计数,减计数另论),而不是谁大就取谁了。
然后在0的基础上继续计数。
双沿计数器的代码如下:
仿真波形如下:
可以看出,每个时钟的上升沿或者是下降沿cntr都进行了加1动作,是真正的双边沿计数器。
在上面代码上稍加修改,就得到了半整数分频的实现了。
代码注释:
(1)端口修改一下,分频器的输出端口clko加上去。
(2)声明cntr为内部变量,为了仿真能看到cntr的变化,加上keep属性,保留cntr节点不被优化掉。
(3)和上面的计数器内容完全相同。
(4)加入分频输出代码,这里,为了使分频后的结果接近50%,特意加了计数值条件的限制,如果对输出占空比没有要求的话,可以改成if((cntr==0)之类的条件就可以了。
仿真后的结果如下:
可以看出输出信号clko的周期为2.5个输入时钟周期。
小数分频:
所谓小数分频,就是输出频率和输入频率的比是小数,由于VerilogHDL的特点,这种情况下输出肯定不是均匀的,,只是从统计学角度上平均是小数。
例如:
3.1分频,也就是说,假设输入了3.1个脉冲个数,得到一个输出时钟周期。
很显然,这种方法是不可能均匀的,因为Verilog中所有的分频动作都是在上升沿或者是下降沿的时候进行的。
只能从统计学角度考虑,则输入了31个脉冲的话,输出端得到10个脉冲,也就是,先做2个10分频,再做一个11分频就可以了。
这里可以套用这个公式,假设要做M.N小数分频,可以做M分频和M+1分频的组合。
假设需要做m个M分频,n个M+1分频,则:
以3.1分频为例:
得出m=9,n=1,也就是做9次3分频,1次4分频。
需要说明的是,这9次3分频和1次4分频是分时工作的,也就是所,在进行3分频期间,4分频模块不要工作,反之亦然。
对于这类问题,最好的办法就是在一个always中进行,因为多个always之间是并行执行的,相互控制必然带来不必要的硬件动作和资源浪费。
由于是3分频和4分频是分时工作,可以采用一个计数器,另外,一共需要做10次分频,这个次数也需要存下来,这就是第二个计数器。
分别命名为cnt4和cnt10吧。
详细内容见代码:
代码说明:
(1)前面9次的3分频时间内做三分频,控制3分频的个数为9个(0~8)
(2)每个分频模块中都要分频输出,三分频最后输出高电平,也可以计数值为0时刻输出高电平,其余时刻输出低电平,这个地方和以前分频方式相同。
(3)每次分频之后要对分频的总个数进行加1,否则会一直停留在三分频里,4分频模块就不会工作了。
(4)当三分频结束时,进入4分频模块。
如果需要做多次的4分频,也要写成cnt>=?
&&cnt<=?
的形式。
这里只需要做一次,故可以省略写。
(5)和三分频一样,公用cnt4,因为是分时工作。
(6)这里写成了通用形式,和(4)原理一样。
可以直接写成cnt10<=0;因为就做一次4分频。
仿真波形如下:
从波形的输出上看,在cnt10计数值9之前都是3分频,输出较密一些。
当cnt10变为9时候就是一个4分频的输出,稍稀疏些。
在这种方式的分频,可以推广到所有的有理小数方法,最好是化简成最简分数形式,节约硬件寄存器资源。
如果是无理小数呢?
比如10/3,这个结果是3.3333…,无理小数是没有办法套用之前的公式的。
不过,只要能表示成分数形式的分频都可以采用下面这种方式。
小数分频的另一种形式
在之前的分频中,我门采用的计数器都是进行加1操作的,如果不是加1呢,比如说加3会是什么效果呢?
以下以模16的计数器演示一下
下面是仿真图
这里有两个光标,在光标中间的数据为一个大周期,之后便不停得循环。
且两个光标之间的数值由小到大的变化了3次,也就是说如果做成分频的话,会输出3个脉冲,而输入时钟的个数为16个,也就是说,如果做分频的话,分频的频率应为16/3分频。
也可以这样理解,当cntr计数器进行加1计数的时候,每计完一个周期输出一个脉冲的话就是16分频,那么每次加3,就是16/3分频。
那么,如何确定分频的输出呢?
在之前的分频中都是靠计数值来决定输出信号电平的高低,而在这种每次加3的计数过程中,每次计数的值都是不同的,如何确定呢?
第一种方式,从上面的仿真图上可以看出,当进入第二周的时候,开始的计数值一定比第一周值得最后的计数值小(如:
图中的2比15小),可以在这个时候输出一个脉冲。
实现代码如下:
代码说明:
(1)为了能知道前一次计数值的大小,定义一个cntr_dly用来保存。
Cntr为计数器。
(2)在时钟作用下,将cntr存入cntr_dly。
而在此同时cntr的值会变成cntr+3。
(3)如果出现后一次的计数值cntr小于cntr_dly,则cntr开始了下一次计数了,这是输出一个脉冲,即分频输出。
仿真波形如下:
注意看输出的脉冲第一个分频输出周期为6个时钟周期,第二个脉冲输出的周期为5个输入时钟周期,第三个脉冲输出周期为5个输入时钟周期。
第二种方式,注意观察计数的最大值,分别是13、14、15,即:
只要大于(15-3)的值都是模16内的最后的计数值修改判断输出部分的代码如下:
仿真波形如下:
可以看出,输出的波形没变,只是提前了一个时钟周期,因为这里没有比较,不需要将之前的计数值暂存。
第三种方式,也是最简单的方式,直接取计数器的最高位,即cntr[3],这个值可以理解成模16的中间值,且这种方式得到的输出近似的为方波。
代码如下:
仿真波形如下:
虽然波形的脉宽发生了变化,但个数没变。
因此,这个结果也是正确的。
分频系数大于1小于1.5的另一种分频方法
例如,4/3分频。
这种分频结果只能是在多个脉冲之间任意消除一个,像4/3分频,每4个脉冲中小曲一个脉冲。
当然,也可以采用前面的计数方式来实现,可以计4个数,前三个计数中,直接输出输入的时钟,最后计数的一个周期内不输出。
这样就可以实现4个脉冲输出3个了,即4/3分频。
下面是另一种方式的实现。
先看代码:
代码说明:
(1)和之前一样,设计一个暂存计数器最高位的变量
(2)将计数器的最高位延时一个时钟周期
(3)这个表达式的结果需要仔细分析cntr[2]的波形,请读者参照下面仿真图自行分析。
下图为4/3分频仿真结果:
从上图上看,clko每4个输入clki输出3个周期脉冲,达到了4/3分频的效果。
存储器的设计与使用
寄存器与存储器的定义
在VerilogHDL中,通常使用reg[m-1:
0]r来定义寄存器r。
这个寄存器r的位宽为m位.可以用下图来说明。
它占有m个位空间。
Reg类型通常用来定义寄存器,但也有例外的情况,例如前面讲过的与门电路可以使用reg定义输出端:
这里将c定义成reg型,而实际上生成与门电路的时候并没有寄存器的存在。
它只是从语法角度上满足always块的赋值特点而设计的,所以,寄存器类型的变量不一定生成寄存器。
于是在新的语法标准中,将reg类型重新命名为variable类型,并且同时将wire等连线型命名为net型。
同为variable类型的还有integer型,在32位的操作系统当中,它的作用相当于reg[31:
0],即32位的寄存器类型变量。
例如:
integeri;相当于reg[31:
0]i;。
对于多位的寄存器或者是连线型变量的定义,通常也称作向量的定义,它有高地位顺序的。
这里用[msb_constant_expression:
lsb_constant_expression]表示向量的通用形式,msb_constant_expression为最高位常量表达式,代表范围的左侧值,lsb_constant_expression为最低位常量表达式,代表范围的右侧值。
右侧表达式的值可以大于、等于、小于左侧表达式的值。
在前面的学习之中,大家知道,可以将向量中的部分位取出进行单独操作。
例如:
wire[3:
0]bus;
wireb;
assignb=bus[1];
这种操作称为位操作,如果取出多个位,称作域操作。
例如:
wire[3:
0]bus;
wire[1:
0]b;
assignb=bus[1:
0];//取bus的低两位域。
如果说,不允许在向量中进行位选择和域选择的话,可以使用scalared关键字来定义向量。
例如:
wirevectored[7:
0]bus;
那么,这个时候bus就不可以进行位操作和域操作了。
vectored关键字为标量类矢量。
例如上例bus就是标量类矢量。
默认情况下wire[7:
0]bus这个bus向量为矢量类矢量,它相当于wirescalared[7:
0]bus;,scalared为矢量类矢量的关键字。
在没有位宽的定义中,都称为标量。
如rega;,a就是一位标量。
总之,标量类矢量与矢量类标量按位或部分位(域)赋值的矢量称为标量类矢量,这相当于多个一位标量的集合,标量类矢量进行类型说明时,需要关键字scalared。
不能按位或域赋值的矢量称为矢量类矢量,在进行类型说明时,需要关键字vectored。
标量类矢量的说明可以缺省,就是说没有关键字scalared或vectored的矢量均将被解释成标量类矢量,这是使用最多的一类矢量。
由若干个位宽相同向量组合在一起,就可以构成存储器空间。
例如:
reg[7:
0]mymem[63: