3.3.1 双向端口的使用和仿真
双向端口顾名思义既可以作为输入端口接收数据,也可以作为输出端口发出数据,对数据的操作是双向的。比如某个设计需要一个16位的数据输入口和一个16位的数据输出口,并且数据输入和输出不会同时发生。如果分别设计数据输入口和输出口,就需要32根数据线;而用双向端口来设计,只需要16根数据线,这样就节省了16根数据线引脚。本节给出FPGA中双向端口的设计原理和方法,以及仿真和初始化双向端口的方法。
1. 双向端口的实现原理
双向端口是通过控制三态门来实现的,其典型结构如图3-23所示。当z=0时,上面输出的管子开通,此时数据可以从上面的管子中输出,这时双向端口就作为输出口;当z=1时,上面的管子被置为高阻态,数据不能从上面的管子输出,此时数据只可以从下面的管子由外向内输入,这时的双向端口是输入口。

图3-25 双向端口的硬件结构
2. 双向端口的Verilog实现
根据Verilog HDL语法,I/ O端口可以分成三类:输入端口input;输出端口output以及双向端口inout。output端口信号可定义成寄存器型变量,并在always块内可以被赋值使用,而inout型双向端口信号不能被定义成reg型变量,因此只能采用assign赋值语句,不能在always块内使用,这一点与VHDL中双向端口的使用方法不同。
双向端口的语法为:
inout a; wire z, b;
//当控制信号z为1时,开通三态门,a为输入端口;当z为0时,三态门为高阻,
//a为输出端口
例如,assign a = (z) ? b : 8'bz;
例3-16 使用Verilog实现一个位宽为16比特的数据选择器,其结构如图3-24所示,当控制信号 时,将输入数据从双向端口输出;当控制信号 时,将双向端口数据从输出端口输出。

图3-2616比特数据选择器的示意图
module bidirec_data(clk,z,din,dinout,dout);
input clk; //时钟
input z; //控制信号
input [15:0] din; //输入时钟
inout [15:0] dinout; //双向端口
output [15:0] dout; //输出时钟
reg [15:0] dout;
reg [15:0] din_t;
assign dinout=(!z) ? din_t : 16'bz; //完成双向赋值
always@(posedge clk) begin
if (!z)
din_t <= din;
else
dout <= dinout;
end
endmodule
3. 双向端口的仿真
当双向端口作为输出口时,我们不需要对它进行初始化,而只需要开通三态门。当双向端口作为输入口时,需要对它进行初始化赋值,并关闭三态门。对双向端口的初始化赋值,需要使用wire型的数据。此外可以通过force命令来对双向端口输入赋值。
例3-17 用Verilog完成例3-16的测试,并给出相应的测试结果。
module test_bidata;
// The input signals
reg clk;
reg z;
reg [15:0]din;
// The output signals
wire [15:0] dout;
wire [15:0] dinout;
integer i;
bidirec_data uut(
.din(din),
. z z),
. clk(clk),
. dout(dout),
. dinout(dinout));
always #10 clk= ~clk;
initial begin
z=1;
clk=0;
din = 0;
force dinout=20;
#200 for (i=0;i<10;i=i+1)
#20 force dinout = dinout - 1;
end
always #20 din = din + 1;
endmodule
上述程序经过ModelSim SE仿真,得到如图3-27所示的仿真结果。从中可以看到该数据选择器的功能是正确的。

图3-27 数据选择器的局部仿真结果示意图
3.3.2 阻塞赋值与非阻塞赋值
在对Verilog HDL程序进行仿真和综合的过程中经常会遇到由于“=”和“<=”的使用不当而产生不易察觉的问题,从而使仿真和综合的结果不一致,这是由于设计者对阻塞与非阻塞过程赋值的功能和执行过程没有深刻理解所造成的。本节将详细地阐述阻塞与非阻塞过程赋值的功能和执行过程,并通过一些具体的例子来分析它们之间的差异。
1. 功能定义
在硬件中,过程赋值语句表示用赋值语句右端表达式所推导出的逻辑来驱动该赋值语句左边表达式的变量。过程赋值语句只能出现在always语句和initial语句中。有两种过程赋值语句:
-
阻塞赋值(blocking assignments)
阻塞赋值由符号“=”来完成,“阻塞赋值”由其赋值操作行为而得名:“阻塞”即是说在当前的赋值完成前阻塞其他类型的赋值任务,但是如果右端表达式中含有延时语句,则在延时没结束前不会阻塞其他赋值任务。
-
非阻塞赋值(nonblocking assignments)
非阻塞赋值由符号“<=”来完成,“非阻塞赋值”也由其赋值操作行为而得名:在一个时间步(time step)的开始估计右端表达式的值,并在这个时间步(time step)结束时用等式右边的值更新取代左端表达式。在估算右端表达式和更新左端表达式的中间时间段,其他的对左端表达式的非阻塞赋值可以被执行。即“非阻塞赋值”从估计右端开始并不阻碍执行其他的赋值任务。
2.组合逻辑中的阻塞与非阻塞
首先,给出一个例子来说明两种赋值语句的不同。
例3-18 使用Verilog给出阻塞赋值的实例
module ex1 ( out, a, b, c, d);
input a, b, c, d;
output out;
reg t1, t2, out;
always @ (a or b or c or d) begin
t1 = a & b; //t1 < = a & b;
t2 = c & d; //t2 < = c & d;
out = t1 | t2; //out < = t1 | t2;
end
endmodule
经过ModelSim 6.2b仿真后,得到的仿真结果如图3-26所示。

图3-28阻塞赋值的仿真结果
从例子中可以看出:如果a,b,c,d 的值发生如下变化:a,b,c,d 都从0→1,则采用阻塞赋值语句(always语句中采用“//”前的语句)所得的结果是:t1变为1,t2变为1, out变为1;而采用非阻塞赋值语句(always语句中采用“//”后的语句)所得的结果是:t1 变为1,t2变为1,out仍为0;从阻塞与非阻塞赋值语句的执行过程来看就是:阻塞赋值是一步完成,而且一条语句执行的同时会阻止其他阻塞赋值语句的执行,所以执行“out = t1 | t2;”语句时所用的t1,t2的值是更新过的值;非阻塞赋值是两步完成的,而且一条语句的执行不会阻止其他非阻塞赋值语句的同时执行其右端的估值,此时t1、t2都没有被更新,使用的都是未更新的旧值。然后才执行左端的更新,所以out的值不变。
由例3-18可见“=”和“<=”对逻辑的不同影响,很显然此例要实现的是两个与门然后或门的一个组合逻辑电路,而使用“<=”的电路并不能满足要求。如果读者仍希望利用非阻塞赋值,那么通过在触发事件表中增加变量,也可以满足功能。即对于上例,如果仍利用“<=”,可以把@(a or b or c or d ) 改为@ (a or b or c or d or t1 or t2), 这样每当t1,t2发生变化时,always语句就会被重新计算,并最终得到正确的out值,但是这样会降低仿真器性能。由本例可以发现非阻塞赋值存在以下两个问题:
-
非阻塞赋值不反映逻辑流;
-
需要将所有赋值对象都列入事件表中。
如果用阻塞式赋值就很容易避免这两个问题, 所以一个好的编程风格就是:对组合逻辑建模采用阻塞式赋值。
3.时序逻辑中的阻塞与非阻塞
首先来看一个用阻塞赋值语句实现的简单时序逻辑——D触发器。
例3-19 使用Verilog实现D触发器
module ex2 (clk, d, q1, q2)
input clk, d;
output q1, q2;
reg q1, q2;
always @ (posedge clk) begin
q1 = d;
q2 = q1;
end
endmodule
例3-19综合后能得到所期望的逻辑电路吗?答案是否定的。根据阻塞赋值语句的执行过程可以得到执行后的结果是q1 = d;q2 = d,即实际只会综合出一个寄存器,而不是所期望的两个,那如何才能得到所需要的电路呢?如果把always块中的两个赋值语句的次序颠倒后再进行分析:先把q1的值赋于q2,然后再把d赋于q1,这样q1,q2的值就不再都是d了,满足了设计的要求。
如果用非阻塞赋值来实现,就会发现不论两条语句的次序如何都能满足要求。如果把寄存器从2个变为3,4,…,n 个,语句的次序就会更多,不同的次序对阻塞赋值会有不同的结果,但非阻塞赋值语句的结果都是一样的,所以一个好的编程风格就是:对时序逻辑建模采用非阻塞式赋值。
通过实践表明,遵循以下好的编码风格可以大大减少设计中的错误和提高设计效率:
- 对组合逻辑建模采用阻塞式赋值。
- 对时序逻辑建模采用非阻塞式赋值。
- 用多个always块分别对组合和时序逻辑建模。
- 尽量不要在同一个always 块里面混合使用“阻塞赋值”和“非阻塞赋值”;如果在同一个always块里面既为组合逻辑又为时序逻辑建模,应使用“非阻塞赋值”。
3.3.3 输入值不确定的组合逻辑电路
有些电路,尽管它的某些输入是不确定值,但其输出却是个确定值。这种情况的最简单例子是,与门的一个输入是不确定值x,另一个输入却是0。在这种情况下,Verilog HDL可以识别出门的输出一定是0。此外,还有更复杂的例子,如实际中的2选1选择器,如果选择器的两个输入都是0,而输入的控制信号是x,那么不管控制信号是什么,输出确定是0。但在这种情况下,Verilog HDL却不能认识到这种情况,相反会把x值传播到输出口。所以,这就需要设计者自行设计一个电路,使其在所有条件下都能展示出期望行为的选择器。
在Verilog HDL中,有两种不同的原因可能导致信号值为x。第一种原因是,有两个不同的信号源用相同的强度驱使同一个节点,并试图驱动成不同的逻辑值,这一般是由设计错误造成的。第二种原因是信号值没有初始化。所以在设计组合逻辑时,需要将不确定的输入转化成确定输入,然后再完成组合逻辑。这种结构如下图3-29所示:
![]()
图3-29 输入的不确定值转换成确定值
例3-20 一个转换不确定输入的模块
module x2one (in, out);
input in;
output out;
assign out = (in==1) ? 1: 0;
endmodule
3.3.4 数学运算中的扩位与截位操作
1.扩位操作
在定点计算中,经过加法和乘法运算后,输出结果的位宽会增加。但如果继续使用和输入操作数同等位宽的数来表示结果,就会丢失有用的比特信息,造成输出结果错误。
在有限字长的情况下,若两个 位的数相加,其结果就是 位;若两个 位的数相乘,其结果就是 位。(???这里错误)
例3-21 展示4比特加法运算中的扩位现象。
下面从有符号数和无符号数两类情况来说明:
-
如无符号数4'b1111 + 4'b1111 = 5' ,但是由于加数是4位,在Verilog语言中只保留低4位,就会得到4'b1111 + 4'b1111 = 4' 的结果,这样就会造成计算错误。
-
对于有符号数4'b0101和4'b0111分别对应着+5和+7,二者相加后本应为+12,即5'b01100。但由于位宽限制,如不扩位,只能保留低4位,即4'b1100,对应着-4,造成严重的计算错误。类似的错误还会造成负数相加变成正数。
从上面可以看出,对于数学运算需要考虑位宽效率,否则会造成严重的计算错误。
2.截位操作
在有限字长的情况下,若两个 位的数相加,其结果就是 位;若两个 位的数相乘,其结果就是 位。但在实际的操作过程中,考虑到资源的问题,不能任由相加、相乘操作来增加操作数的位宽,必须进行截断。例如,两个16位数相乘后,其结果为32位,如再和一个16位数相乘,结果就变为48位,这样下去,用不了几个乘法操作就会使操作数的位宽剧增,所占用的硬件资源也会很多。因此,需要将乘积结果进行截位,寄存在 位的寄存器中。
截取是按照定点仿真的结果来定的,下面依次给出加法操作、加保护截取操作和移位操作的书写规范。
-
加法实现规范,扩展符号位后相加。
reg[12:0] Adder_Out;
reg[11:0] Adder_In1,Adder_In2;
Adder_Out <= {Adder_In1[11],Adder_In1} + {Adder_In2[11],Adder_In2};
-
对于截取乘法的结果,需要加溢出保护的截取规范。例如要截取12比特输出的第6位到第2位,其实现代码为:
if((addRakeOut[11:6] == 0) || (addRakeOut[11:6] == 63))
tmptraffic <= addRakeOut[6:2];
else
tmptraffic <= (addRakeOut[11] == 1) ? 16 : 15;
或者:
if((addRakeOut[11:6] == 6'b000000) || (addRakeOut[11:6] == 6'b111111))
tmptraffic <= addRakeOut[6:2];
else
tmptraffic <= (addRakeOut[11] == 1) ? 5’b10000 : 5’b01111;
-
最后给出常用的移位操作范例:
reg[15:0] Data;
//左移一位
if((Data[15:14] == 2'b00) || (Data[15:14] == 2'b11))
Data <= {Data[14:0],1'b0};
else
Data <= (Data[15]) ? 16'b1000_0000_0000_0000 : 16'b0111_1111_1111_1111;
//右移一位
Data <= {Data[15],Data[15:1]};
3.3.5 利用块RAM来实现数据延迟
块RAM是Xilinx FPGA的一类核心资源,不占用任何逻辑资源。在设计中,合理利用块RAM能节省大量的逻辑资源,并且能保证时序,简化时序设计。块RAM作为一种存储单元,可以将其封装为FIFO、移位寄存器以及延迟器。ISE中提供了基于块RAM以及分布式RAM的FIFO、移位寄存器等模块的IP core,可满足用户参数化配置。因此,本节主要讲述基于RAM的数据延迟器。
利用块RAM实现数据延迟器的思路就是:按照一定规律排列写地址,将数据依次写入,在读出时,将读地址比写地址延迟N个空间,即可实现数据延迟。由于块RAM可以工作在芯片所支持的最高频率下,其工作时钟一般可达到数据流的几倍甚至几十倍,因此只要块RAM的容量满足,其可用于多路数据的延迟。例3-22给出了块RAM实现两路数据延迟的实现。
例3-22 利用块RAM实现a、b两路数据的延迟,其中a、b两路数据的位宽都为32比特,速率都为61.44Mbps,要求a路延迟16个数据时钟周期,b路延迟8个数据时钟周期。
module bram_delay(clk_122p88MHz, a, b, a_delay, b_delay);
input clk_122p88MHz;
input [31:0] a;
input [31:0] b;
output [31:0] a_delay;
output [31:0] b_delay;
reg [31:0] a_delay;
reg [31:0] b_delay;
wire [5:0] addra, addrb;
wire [31:0] douta, doutb;
reg [5:0] addra1 = 0;
reg [5:0] addra2 = 0;
reg [5:0] addrb1 = 32;
reg [5:0] addrb2 = 32;
reg wea = 0;
reg web = 0;
reg flag = 0;always @(posedge clk_122p88MHz) begin
flag <= !flag;
if(flag == 1'b1) begina_delay <= a_delay;
b_delay <= b_delay;
wea <= 1'b1;
web <= 1'b1;
addra2 <= addra2;
addrb2 <= addrb2;
if(addra1 == 31)
addra1 <= 0;
else
addra1 <= addra1 + 1'b1;
if(addrb1 == 63)
addrb1 <= 32;
else
addrb1 <= addrb1 + 1'b1;end
else beginwea <= 1'b0;
web <= 1'b0;
a_delay <= douta;
b_delay <= doutb;
addra1 <= addra1;
addrb1 <= addrb1;
if(addra1 <=15)
addra2 <= addra1 + 16;
else
addra2 <= addra1 - 16;
if(addrb1 <=39)
addrb2 <= addrb1 + 24;
else
addrb2 <= addrb1 - 8;end
end
assign addra = !flag ? addra1 : addra2;
assign addrb = !flag ? addrb1 : addrb2;
bram_16 bram_16(.clka(clk_122p88MHz),
.dina(a),
.addra(addra),
.wea(wea),
.douta(douta),
.clkb(clk_122p88MHz),
.dinb(b),
.addrb(addrb),
.web(wea),
.doutb(doutb));
endmodule
上述程序经过Synplify Pro综合后,其RTL结构如图3-30所示。其中块RAM的IP Core作为黑盒子被综合。

图3-30 数据延迟电路的RTL结构图
在ModelSim 6.2b中完成仿真,其结果如图3-31所示,从中可以看出上例成功利用块RAM完成了两路数据的延迟。

图3-31 数据延迟电路的仿真结果示意图
3.3.6 测试向量的生成
Verilog HDL还可以用来描述变化的测试信号。描述测试信号的变化和测试过程的模块叫做测试平台(Testbench),它可以对任何一个Verilog/VHDL模块进行动态的全面测试。通过测试被测试模块的输出信号是否符合要求,可以测试和验证逻辑系统的设计和结构正确与否,并发现问题及时修改。
下面给出测试模块的代码编写风格。
timescale 1ns / 1ps
module cmult_v;
// 输入信号向量
reg clk;
reg [15:0] ar, ai, br ,bi ;
//输出信号向量
wire [31:0] qr, qi;
// 实例化待测的模块单元 (UUT)
cmultip uut (
.clk(clk), .ar(ar), .ai(ai), .qr(qr), .br(br), .bi(bi), .qi(qi)
);
initial begin
// 初始化输入向量
clk = 0; ar = 0; ai = 0; br = 0; bi = 0;
#100; //等待100ns后,全局reset信号有效
ar = 20; ai = 10; br = 10; bi = 10;
end
always #5 clk = ~clk;
always # 10 ar = ar + 1;
always # 10 ai = ai + 1;
always # 10 br = br + 1;
always # 10 bi = bi + 1;
endmodule
在测试模块中,测试向量的产生是测试问题中的一个重要部分,只有测试向量产生的完备,分析测试结果才有意义。如果有方法产生出期望的结果,可以用Verilog或者其他工具自动地比较期望值和实际值。如果没有简易的方法产生期望的结果,那么明智地选择测试向量,可以简化仿真的结果。当然,测试向量的产生是个在繁琐中追求特殊的情况。所以需要根据实际情况来选择测试向量。
在本书中我们引入先进的Verilog验证方法,即应用MATLAB软件辅助电路设计,并进行电路功能验证。即用MATLAB首先完成浮点运算的设计,由输入得到相应的输出,然后再将其量化,最后将量化后的定点输入导入仿真软件,完成仿真,最后再将仿真软件的输入和MATLAB的量化输出作对比,并统计结果。这样,只需要几个操作,便可以完成模块的功能仿真。


