3.2 数字
3.2.1数字表示方式
在Verilog中的数字表示方式,最常用的格式是:<</span>位宽>’<</span>基数><</span>数值>,如,4’b1011。
位宽:描述常量所含位数的十进制整数,是可选项。4’b1011中的4就是位宽。通俗理解就是4根线。如果没有这一项,可以从常量的值推断出。例如’b1011可知位宽是4;’b10010可推断出位宽为5。
基数:表示数值是多少进制。可以是b,B,d,D,o,O,h或者H,分别表示二进制、十进制、八进制和十六进制。如果没有此项,则缺省默认为十进制数。
例如,二进制的4’b1011,可以写成十进制的4’d11,也可以写成十六进制的4’hb,或者八进制的4’o13,还可以不写基数直接写成11。不管怎么样,只要二进数相同,写成十进制、八进制和十六进制,都是同样的数字。
数值:是由基数所决定的表示常量真实值的一串ASCII码。如果基数定义为 b或B,数值可以是0,1,x,X,z或Z。如果基数定义为 o或O,数值还可以是2,3,4,5,6,7。如果基数定义为 h或H,数值还可以是8,9,a,b,c,d,e,f,A,B,C,D,E,F。对于基数为d或者D的情况,数值符可以是任意的十进制数:0到9。但不可以是x或z。
例如,4’b12是错误的,因为b表示二进制,数值只能是0、1、x或者z,不有是2。
32’h12等同于32’h00000012,也就是数值未写完整时,高位补0。
3.2.2二进制是基础
在数字电路中,如果芯片A给芯片B传递数据,例如传递0或者1信息。可以将芯片A和芯片B,通过一个管脚进行相连。然后由芯片A控制该管脚为高电平或者低电平,通过高低电平来表示0和1。例如,芯片B检测到该管脚为低电平时,表示收到0;当芯片B检测到该管脚为高电平时,表示收到1。
如果用低电平表示收到1,用高电平表示收到0,这可不可以呢?当然可以,只要芯片A和芯片B事先协定。芯片A要发数字1时,会将该管脚置为低电平。芯片B检测到该管脚为低电平,知道收到了数字1,通信完成。
一个管脚拥有高低电平两种状态,可以分别表示数字0和1两种情况。如果芯片A要发数字0、1、2、3给芯片B,那该怎么办呢?
可以让芯片A和芯片B连接两根管脚,即两条线,a和b。当两条线都为低电平时,表示发送数字0;当a为高电平,b为低电平时,表示发送数字1;当a为低电平,b为高电平时,表示发送数字2;当两条线都是高电平时,表示数字3。
按照同样的道理,芯片A要发送数据4,5,6,7给芯片B的时候,只要再添加一条线就可以了。三根线一共有8种状态,可以表示8个数字。
综上所述,我们可能通过线的不同电平状态,表示不同的含义。有多少个不同状态,就可以表示多少个数字。
如果芯片A要发送+1,-1,0,+2等数字给芯片B,这里有正负了,那又该如何表示呢?参考前面的思想,线的高低电平表示的含义,是由芯片双方向事先约定好的。既然是这样,那么我们拿一根线出来,例如低电平表示正数,高电平表示负数。
上面就是三根线,我们用线c表示正负,0表示正数,1表示负数。用线a和线b表示数值。
3’b111,可以解释为十进制数7,也可以解释为有符号数原码“-3”,也可以解释为有符号数补码“-1”,这取决于工程师对二进制数的定义。只要这个定义不影响到电路之间的通信那就绝对不会有问题。
所以,数字中的“0”和“1”不仅可以表示含义,也可以表示其他意义,如正负符号等。同样的道理,
在数字电路中,二进制数是其他如八进制、十进制、十六进制、有符号数、无符号数、小数等的根本。在FPGA设计中,不清楚小数、有符号数的计算方法,最根本的原因是不清楚这些数据所对应的二进制值。只要理解了它所对应的二进制值,很多问题都可以解决。
例如,有初学者经常问,FPGA中如何实现小数计算,如“0.5+0.25”这个功能。
首先,众所周知的,0.5+0.25的结果为0.75。
其次,我们可以考虑,0.5、0.25和0.75用二进制该如何表示?这取决于工程师的做法,因为这种表示方法有很多种,例如定点小数,浮点小数,甚至如前面所讨论,用几根线自行来定义,只要能正常通信,那就绝对没有问题。
假设,某工程师用三根线,自行定义了二进制值所表示的小数值。
为了说明二进制值的意义是可以随便定义的,我特意将数字顺序打乱。当然,有读者可能说为什么只有这几种小数呢?这是因为我假定本系统就只有这几种数字,如果想表示更多数字,那就增加线就行了。
有了上面定义之后,要实现“0.5+0.25”就很容易了,其实就是3’b001和3’b100“相加”,期望得到3’b010。如果我们直接使用3’b001 + 3’b100,结果为“101”了,不是想要的结果。那怎么办呢?可以这么写:
当然,这是其中一种写法。总之,只要能实现所对应的功能,结果正确就可以。
有读者问,按上面的表格0.1+0.8应该为0.9,但上面没有0.9的表示。这个其实是设计者这个表格定义有缺陷,或者设计者认为不会出现这个情况吧。总之,笔者要表达的是,只要定义好所对应的二进制数,很多功能是很容易设计的。
当然,实际的工程中,我们通常会遵守约定成俗的做法,没必要自己搞得另类。例如下面是常用的定点小数的定义。
现在要实现0+0.5=0.5,也就是3’b000和3’b100相加,期望能得到3’b100。我们发现直接用二进制3’b000+3’b100就可以得到3’b100。
要实现0.125+0.75=0.8725,也就是3’b001和3’b110相加,期望能得到3’b111。我们发现直接用二进制3’b001+3’b110就可以得到3’b111。
要0.5+0.75=1.25,这个1.25已经超出了表示范围,要不就增加信号位宽,要不只能表示小数位。如果只是表示小数位,那结果就是0.25。也就是3’b100和3’b110相加,期望得到3’b010。我们发现3’b100 + 3’b110 = 4’b1010,用3位表示就是3’b010,也就是0.25了。
综上所述,对于定点小数的计算很简单,就是直接相加。
3.2.3不定态
前面讲过,数字电路只有高电平和低电平,分别表示1和0。但代码中经常能看到x和z,如1’bx,1’bz。那么这个x和z是什么电平呢?答案是没有实际的电平来对应。这个x和z是更多地用来表示设计者的意图或者用于仿真目的,告诉仿真器和综合器怎么解释这段代码。
X态,称之为不定态,设计者常用于判断条件,用于告诉综合工具,设计者不关心它的电平是多少,是0还是1都可以。
上面的例子,条件是din==4’b10x0,这个条件等价于din==4’b1000||din==4’b1010,其中“||”是“或”符号。
明德扬则建议,直接写成din==4’b1000||din==4’b1010,好于写成“din==4’b10x0”,直接简单明了。
仿真的时候,有些信号产生了不定态,那么设计者就要认真分析,这个不定态是不是应该的。如果真的不关心它是0还是1,那么可以不解决。但明德扬建议,所有信号都不应该处于不定态,是0还是1,写清楚,不要给设计添加“思考”的麻烦。
3.2.4高阻态
Z态,一般称之为高阻态,表示设计者不驱动这个信号(既不给0也不给1),通常用于三态门接口当中。
上图就是三态总线的应用案例。图中的连接总线对于CPU和FPGA来说,既当作输入又当作输出,是双向接口。一般的硬件电路中,会将该线接上一个上拉电阻(弱上拉)或下拉电阻(弱下拉)。
当CPU和FPGA都不驱动该总线时,A点保持为高电平。当FPGA不驱动该总线,CPU驱动该总线时,A点的值就由CPU决定。当CPU不驱动该总线,FPGA驱动该总线时,A点的值就由FPGA决定。FPGA和CPU不能同时驱动该总线,否则A的电平就不确定了。通常FPGA和CPU何时驱动总线,是按协议事先协商好的。
上图是典型的I2C的时序。I2C的总线SDA就是一个三态信号。I2C协议已规定好上面的时间中,哪段时间是由主设备驱动,哪段时间是由从设备驱动,双方都要遵守协议,不能存在同时驱动的情况。
那么FPGA在设计中,是如何做到“不驱动”这一行为呢?这是因为FPGA内部有三态门。
三态门是一个硬件,上图是它的典型结构。三态门有四个接口,例如上图中的写使能wr_en、写数据wr_data、读数据rd_data和与外面器件相连的三态信号data。
注意写使能信号,当该信号有效时,三态门会将wr_data的值赋给三态线data,此时data的值由wr_data决定,当wr_data为0时,data值就为0;当wr_data为1时,data值就为1。
当写使能信号无效时,则不管wr_data值是多少,都不会对外面的data值有影响,也就是不驱动。
那么在Verilog中,是通过如下两行代码来描述这一功能的。
综合器看到这两行代码,就知道要综合成三态门了。
这个高阻z的作用就在于这里。而且注意到,硬件上用三态线是为了减少管脚,而在FPGA内部没有必要减少连线,所以使用三态信号是没有意义的。
也就是说,明德扬的设计建议,FPGA内部不要使用高阻态“z”,没有必要给自己添加“思考”的麻烦。当然,使用了也不会报错,也能实现功能。
总结一点,高阻态“z”是表示“不驱动总线”这个行为,实际上数字电路就是高电平或者低电平,不存在其他电平的情况。
3.2 数据类型
Verilog HDL的信号类型有很多种,但主要包括两种数据类型:线网类型(net type) 和寄存器类型(reg type)。明德扬的设计,也是只会使用这两个类型。
3.2.1线网类型wire
线网类型用于对结构化器件之间的物理连线的建模。如器件的管脚,内部器件如与门的输出等。以上面的加法器为例,输入信号A,B是由外部器件所驱动,异或门X1的输出S1是与异或门X2输入脚相连的物理连接线,它由异或门X1所驱动。
由于线网类型代表的是物理连接线,因此它不存贮逻辑值。必须由器件所驱动。通常由assign 进行赋值。如 assign A = B ^ C;
wire 类型定义语法如下:
wire [msb: lsb] wire1, wire2, . . .,wireN;
Ø msb 和lsb 定义了范围,表示了位宽。例如[7:0]是8位位宽,也就是可以表示成8’b0至8’b1111_1111;
Ø msb和lsb必须为常数值;
Ø 如果没有定义范围,缺省值为1位;
Ø 信号没有定义数据类型时,缺省为wire 类型。
Ø 对数组类型,请按降序方式,如[7:0] ;不要写成[0:7]。
wire [3:0] Sat; // S a t 为4 位线型信号
wire Cnt; //1 位线型信号
wire [0:31] Kisp, Pisp, Lisp ;// Kisp, Pisp, Lisp 都是32位的线型信号,不建议这样定义。
3.3.2寄存器类型reg
reg 是最常用的寄存器类型,寄存器类型通常用于对存储单元的描述,如D型触发器、ROM 等。存储器类型的信号当在某种触发机制下分配了一个值,在分配下一个值之时保留原值。但必须注意的是,reg 类型的变量,不一定是存储单元,如在always 语句中进行描述的必须用reg 类型的变量。
reg 类型定义语法如下:
reg [msb: lsb] reg1, reg2, . . . r e g N;
Ø msb 和lsb 定义了范围,表示了位宽。例如[7:0]是8位位宽,也就是可以表示成8’b0至8’b1111_1111;
Ø msb和lsb必须为常数值;
Ø 如果没有定义范围,缺省值为1位;
Ø 信号没有定义数据类型时,缺省为wire 类型,不是reg型。
Ø 对数组类型,请按降序方式,如[7:0] ;不要写成[0:7]。
例如:
reg [3:0] Sat; // S a t 为4 位寄存器。
reg Cnt; //1 位寄存器。
reg [1:32] Kisp, Pisp, Lisp ;
3.3.3Wire和reg定义的场合区分
Reg型信号不一定生成寄存器。那么什么时候使用wire类型,什么时候用reg类型,明德扬总结出一套方法:在本模块中,使用always设计的信号都定义为reg型;其他都用wire型。
上面代码中,cnt1是用always设计的,所以要用reg型。Add_cnt1和end_cnt不是由always产生的,所以定义为wire型。
上面代码中,x是用always设计的,所以要定义为reg型。注意,实际的电路中,x不是寄存器,但我们仍然定义为reg型。
上面是例化的代码,其中df是例化模块的输出。由于df不是由always产生的,而是例化产生的,所以要定义成wire型。
注:(本博客连载的内容将出版成图书,并将录制视频,免费公开学习,欢迎大家留意。本连载前面是基础部分,与一般教材无异,后面是项目实践,是本连载的特色。如果你有一定的基础(能看懂verilog代码即可),那么可跳过前面部分,直接学习后面的项目实践。
项目实践将有16个,从基础的闪烁灯开始,到最后是信号处理的项目,如信号发生器、FIR滤波器、插值滤波器和AD采集等。
本连载学习效果:不难看能懂代码,还能知道每一行代码怎么写,怎么设计
|