大家好,我是数字小熊饼干,一个练习时长两年半的ic打工人。我在两年前通过自学跨行社招加入了IC行业。现在我打算将这两年的工作经验和当初面试时最常问的一些问题进行总结,并通过汇总成文章的形式进行输出,相信无论你是在职的还是已经还准备入行,看过之后都会有有一些收获,如果看完后喜欢的话就请关注我吧~谢谢~
在前面的文章中,我们对时序进行了探讨,了解到了什么是时序违例,在本篇文章我们来讲讲该对时序违例进行修复的几种方法。
一、插入寄存器
插入寄存器是解决时序违例最简单有效的方法,我们只需要根据时序报告找到那些时序紧张的关键路径,并在关键路径中间插入寄存器即可对时序进行优化,例如下面的代码:
module fir(
input wire clk,
input wire [7:0] A,
input wire [7:0] B,
input wire [7:0] C,
input wire [7:0] X,
input wire validsample,
output reg [7:0] Y
);
reg [7:0] X1,X2;
always @(posedge clk) begin
if (validsample) begin
X1 <= X;
X2 <= X1;
Y <= A*X + B*X1 + C*X2;
end
end
endmodule
上面的代码中,所有的乘法和加法操作都发生在一个周期内,因此很容易形成时序紧张甚至违例的关键路径,如下图所示:
为了对这条路径进行时序优化,我们可以在其中插入寄存器,使得乘法和加法不会发生在同一个周期,例如下面的代码所示:
module fir(
input wire clk,
input wire [7:0] A,
input wire [7:0] B,
input wire [7:0] C,
input wire [7:0] X,
input wire validsample,
output reg [7:0] Y
);
reg [7:0] X1,X2;
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
end
always @(posedge clk) begin
Y <= prod1 + prod2 + prod3;
end
endmodule
生成的电路如下所示:
可见,原本时序紧张的路径已经被寄存器分割开来,这对时序优化是有好处的。
二、采用并行结构
通过对关键路径进行并行化处理也可以对时序进行优化,例如假设有一个8位的乘法器可以用字段A和B表示:
X={A, B};
其中A是高有效部分,B是低有效部分。假设有求X的3次幂,乘法操作可变为:
X*X = {A,B}*{A,B}={(A*A),(2*A*B),(B*B)};
这样就把原本求3次幂的问题简化位一个串行的4位乘法器,于是有以下实现代码:
module power(
input wire clk,
input wire [7:0] X,
output reg [7:0] X_POWER
);
reg [7:0] X1,X2,XPower1;
wire [7:0] XPower2;
reg [3:0] XPower2_ppAA,XPower2_ppAB,XPower2_ppBB;
reg [3:0] XPower3_ppAA,XPower3_ppAB,XPower3_ppBB;
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];
assign XPower2 = (XPower2_ppAA << 8) + ((2*XPower2_ppAB)<<4) + XPower2_ppBB;
assign XPower = (XPower3_ppAA << 8) + ((2*XPower3_ppAB)<<4) + XPower3_ppBB;
always @(posedge clk) begin
//pipeline stage 1
X1 <= X;
XPower1 <= X;
//pipeline stage 2
X2 <= X1;
XPower2_ppAA <= XPower1_A * X1_A;
XPower2_ppAB <= XPower1_A * X1_B;
XPower2_ppBB <= XPower1_B * X1_B;
//pipeline stage 3
XPower3_ppAA <= XPower2_A * X2_A;
XPower3_ppAB <= XPower2_A * X2_B;
XPower3_ppBB <= XPower2_B * X2_B;
end
endmodule
简单说明一下上面的代码,通过将原本的输入X分成高低两部分,以及插入了3级寄存器,将原本求3个8bit数据的乘法,转变为了每个周期求3个4bit数据的乘法再相加。通过将原本的乘法器拆分成更小的操作,将原本很大的路径延时变为了拆分后的这些子结构的最长延时。
三、优先级电路优化
当我们使用if有时会形成带优先级的电路,例如下面的代码:
module prior_sel(
input wire a,
input wire b,
input wire c,
input wire [2:0] sel,
output reg z
);
always @(*) begin
if (sel[0]) begin
z = a;
end else if (sel[1]) begin
z = b;
end else if (sel[2]) begin
z = c;
end else begin
z = 1'b0;
end
end
endmodule
该代码就会形成带优先级的电路结构,例如:
此时第一级if的优先级最高,在生成的电路中表现为最靠后。最后一级的if的优先级最低,在电路图中经过的组合逻辑最多。利用多if语句的这个特性,可以对时序进行优化。即,我们可以通过改变if的优先级来使得关键路径最小化,通过重新安排组合路径,以使得关键路径可以移动到更加接近目的寄存器的位置。
举个例子,假设b到z的路径延时最长,那么可以将b放在第一级if里,有以下代码和电路:
module prior_sel(
input wire a,
input wire b,
input wire c,
input wire [2:0] sel,
output reg z
);
always @(*) begin
if (sel[1]) begin
z = b;
end else if (sel[0]) begin
z = a;
end else if (sel[2]) begin
z = c;
end else begin
z = 1'b0;
end
end
endmodule
可见b更加接近输出z了,因此我们可以重新安排与关键路径组合的路径来改善时序,通过改变if语句的优先级,以使得关键路径可以移动到更加接近目的寄存器的位置。
当然,我们也可以使用case语句,生成不带优先级的电路,如果sel[2:0]中的各位是互斥的,即只有1bit可以为1,那么可以使用以下的代码,例如:
module prior_sel(
input wire a,
input wire b,
input wire c,
input wire [2:0] sel,
output reg [2:0] z
);
always @(*) begin
case(1'b1)
sel[0] : z=a;
sel[1] : z=b;
sel[2] : z=c;
default: z=1'b0;
endcase
end
endmodule
生成的电路图如下:
可见,我们可以通过去除不必要的优先级电路,展平逻辑结构,减少路径延时。
四、寄存器平衡
寄存器平衡是通过平等的重新分步寄存器之间的逻辑,减少两个寄存器之间最坏路径的延时。由于一条路径的时序是否违例主要是由其最坏路径的决定,因此可以对关键路径进行改变而优化时序。
例如有以下的代码以及电路:
module sum(
input wire clk,
input wire [7:0] A,
input wire [7:0] B,
input wire [7:0] C,
output reg [7:0] Sum
);
reg [7:0] rA, rB, rC;
always @(posedge clk) begin
rA <= A;
rB <= B;
rC <= C;
Sum<= rA + rB + rC;
end
endmodule
可见,上面的流水线共有两级,一级是对输入打一拍,并没有组合逻辑;第二级是打拍后的结果相加。如果第二级的时序较差,形成了关键路径,那么我们可以将关键路径中的一些逻辑移到前面一级,从而平衡两级寄存器之间的逻辑负载,例如:
module sum(
input wire clk,
input wire [7:0] A,
input wire [7:0] B,
input wire [7:0] C,
output reg [7:0] Sum
);
reg [7:0] rABSum, rC;
always @(posedge clk) begin
rABSum <= A + B;
rC <= C;
Sum<= rABSum + rC;
end
endmodule
通过将输入与第一级寄存器之间移回一个加法操作,来平衡各级流水线之间的逻辑,从而缩短了关键路径。因此可以通过从关键路径移动组合逻辑到相邻路径,通过寄存器平衡来改善时序。
五、复制寄存器
当一个寄存器有过多的下级时,甚至超过了其本身的驱动能力,导致延迟实际过大,不能满足时序要求,我们称之为高扇出。
面对这一问题,我们可以通过寄存器复制来解决,即复制与原寄存器A具有相同功能的寄存器B,以减少原寄存器A的扇出,来达到优化时序的目的。
我们可以手动复制寄存器,当然,为了避免复制的寄存器被综合工具优化,例如在vivado中可以加上(* DONT_TOUCH= “TRUE” *)。
也可以通过设置最大扇出,让工具自动帮你复制寄存器,例如vivado中可以设置(MAX_FANOUT=10),这里是设置寄存器的最大扇出为10,当某个寄存器的扇出超过10时,工具会自动复制寄存器。
如果你喜欢这篇文章的话,请关注我的公众号-熊熊的ic车间,里面还有ic设计和ic验证的学习资料和书籍等着你呢~欢迎您的关注!