1.3时序优化
时序一般是指一个设计的时钟速度,一个设计中的任何两个时序单元之间最大延时将决定这个设计的最大时钟速度。理想的时钟速度是存在于底层的抽象,而不是本书别处讨论的速度面积平衡中的速度。因为时钟速度通常和那些拓扑结构并不直接相关,尽管那些结构中的速度面积平衡必然会影响到时序。举例来说,我们在详细了解具体实现细节之前,不可能知道流水线拓扑结构是否一定会比迭代运行的更快。最高速度或者说最大频率可以由下面的公式来直接定义,这就是著名的最大频率等式(忽略时钟抖动):
Fmax=1Tclk-q+Tlogic+Trouting+Tsetup-Tskew (1.1)
公式中,Fmax就是时钟的最大频率;Tclk-q是时钟到达到数据到达寄存器Q端的时间;Tlogic是触发器之间逻辑的传播延时;Trouting是触发器之间的布线延时;Tsetup是下一个时钟沿到来前数据必须到达寄存器D端的最小时间(即建立时间);Tskew时钟到启动触发器和锁存触发器之间的传播延时。
了解了时钟最大频率的公式以及各项意义,下一节将给大家阐述各种用于提高时序性能的方法和平衡手段。
1.3.1在组合逻辑中插入寄存器优化时序
第一个用于优化时序的策略是在关键路径中插入寄存器。这种技巧一般多用于高度流水的设计中,因为在这种设计中额外插入寄存器增加的时钟周期延迟并不会违反整个设计的规范要求,从而不会影响到设计的总体功能性实现。(笔者注:这些额外插入的寄存器在保持吞吐量不变的情况下改善了设计的时序性能)。
举例来说,我们先假定下述代码实现的FIR(有限脉冲响应)滤波器并没有达到时序要求:
module fir(
output [7:0] Y,
input [7:0] A,B,C,X,
input clk,
input validsample);
reg [7:0] X1,X2,Y;
always@(posedge clk)
if (validsample) begin
X1<=X;
X2<=X1;
Y<=A*X+B*X1+C*X2;
end
endmodule
上述代码结构上,所有乘/加操作在一个时钟周期之内完成,如图1-4所示。
图1-4:包含长路径的MAC
换句话说,包含一个乘法器和一个加法器的关键路径延时大于一个时钟周期的最小要求。如果我们假设设计延迟没有固定在一个时钟周期,那么我们可以通过在乘法器之间插入额外的寄存器来使设计插入流水。第一级非常简单,直接在乘法器和加法器之间插入一级流水,代码如下所示:
module fir(
output [7:0] Y,
input [7:0] A,B,C,X,
input clk,
input validsample);
reg [7:0] X1,X2,Y;
reg [7:0] prod1,prod2,prod3;
always @(posedge clk) begin
if (validsample) begin
X1<=X;
X2<=X1;
prod1<=A*X;
prod2<=B*X1;
prod3<=C*X2;
end
Y<=prod1+prod2+prod3;
end
endmodule
代码经过上述修改之后,加法器被一级流水将其和乘法器分离,如图1-5所示。
图1-5:插入流水寄存器的MAC
相对来说乘法操作非常适合流水,因为乘法运算易于被分开成多级运算。所以通过额外插入寄存器可以将乘法分开成多级流水实现。所以我们的结论是在关键路径上插入寄存器可以将关键路径分成两个更小的路径,这样就可以改善了关键路径的时序。
1.3.2使用并行结构来优化时序
第二个可以提高时序性能的策略是重新组织关键路径逻辑结构,使用并行结构代替之前的结构。这个技巧应该是多用于那些由一系列串行逻辑实现的功能模块,而且这些串行逻辑能被打开并可以通过平行逻辑结构实现。举例来说,假设前面我们讨论的三次方设计实例并没有符合设计的时序要求。那么我们可以通过创建并行结构来将乘法器分成多个部分的独立乘法操作,然后来重新编译设计。比如,一个8-bit二进制乘法就可以分拆成两个4-bit的二进制乘法:
X = {A,B}
这里A是最高4-bit,而B是最低4-bit。而在我们上述的三次方例子中,被乘数等于乘数,所以此例中的乘法操作可以被重新组织成:
X*X = {A,B} * {A,B} = {(A*A),(2*A*B),(B*B)}
这样重新组织一下后,就给我们减少了一系列4-bit的乘法以及这些乘法结果的合并。上述重组后的二进制乘法可以由下述代码来实现:
module power3(
output [7:0] XPower,
input [7:0] X,
input clk);
reg [7:0] XPower1;
//partialproductregisters
reg [3:0] XPower2_ppAA,XPower2_ppAB,XPower2_ppBB;
reg [3:0] XPower3_ppAA,XPower3_ppAB,XPower3_ppBB;
reg [7:0] X1,X2;
wire [7:0] XPower2;
//nibblesforpartialproducts(AisMSnibble,BisLS
nibble)
wire [3:0] XPower1_A=XPower1[7:4];
wire [3:0] XPower1_B=XPower1[3:0];
wire [3:0] X1_A=X1[7:4];
wire [3:0] X1_B=X1[3:0];
wire [3:0] XPower2_A=XPower2[7:4];
wire [3:0] XPower2_B=XPower2[3:0];
wire [3:0] X2_A=X2[7:4];
wire [3:0] X2_B=X2[3:0];
//assemblepartialproducts
assign XPower2=(XPower2_ppAA<<8)+(2*XPower2_ppAB<<4)+XPower2_ppBB;
assignXPower=(XPower3_ppAA<<8)+(2*XPower3_ppAB<<4)+XPower3_ppBB;
always @(posedge clk) begin
//Pipelinestage1
X1<=X;
XPower1<=X;
//Pipelinestage2
X2<=X1;
//createpartialproducts
XPower2_ppAA<=XPower1_A*X1_A;
XPower2_ppAB<=XPower1_A*X1_B;
XPower2_ppBB<=XPower1_B*X1_B;
//Pipelinestage3
//createpartialproducts
XPower3_ppAA<=XPower2_A*X2_A;
XPower3_ppAB<=XPower2_A*X2_B;
XPower3_ppBB<=XPower2_B*X2_B;
end
endmodule
上述代码没有考虑任何数据溢出问题,但是它已足以表现我们要讨论的问题。我们看到单个乘法器已经被分开成几个很小的功能模块,这些小的乘法功能模块可以独立地并行运行,如图1-6所示。
图1-6:乘法器分成多个独立的模块
那些分开可以独立并行执行的乘法模块,那么该设计的最大延时就被减少到这些独立执行的小模块中的最大延时。有时候我们也称这种技巧为retiming。