最近在使用一款实际体验并不理想的FPGA芯片时,在时序优化上耗费了不少精力。优化过程中,一个关于加法器映射的优化点颇具特点,在此记录下来与大家分享。
加法器引入的复位扇出问题
在FPGA设计中,复位信号通常只连接到寄存器。但在一个工程中,发现复位信号被接到了大量的查找表(LUT)上,导致复位扇出过大。其中很大一部分问题就出在加法器的设计上。
一个典型的加法器设计很简单,电路代码如下:
always @(posedge clk) begin
if(rst) begin
cnt <= 8'd0;
end else if(clear) begin
cnt <= 8'd0;
end else begin
cnt <= cnt + 8'(inc);
end
end
这是一个简单的8位计数器。cnt 在复位时为0,当 clear 信号有效时清零,否则在 inc 为1时执行加1操作。
这个电路在实际综合时,综合工具可能会将 rst 和 clear 信号先接入一个LUT2,然后将结果送到寄存器的复位引脚上。
这看起来似乎是编译工具不够智能。我们的理想情况是让复位信号直接连接到寄存器的复位引脚,但工具对于代码结构的识别有时并不能完全符合预期。要理解如何优化,可能需要从更基础的电路原理与优化思路入手。
从代码层面如何解决
既然工具的处理方式不够理想,我们就需要通过代码来给予明确的指导。针对复位扇出问题,可以添加约束来解决,但对于加法器这种特定情况,最佳做法是从代码层面进行优化,使其更贴合FPGA底层硬件结构。
对于一个寄存器,其最理想的模型通常是:
always_ff @(posedge clk ) begin
if(rst) begin
Q <= 1'b0;
end else begin
Q <= D;
end
end
我们可以基于这个思路对之前的加法器电路进行改造:
always_ff @(posedge clk ) begin
if(rst) begin
cnt <= 8'd0;
end else begin
cnt <= (cnt & {8{~clear}}) + 8'((~clear) & inc ) ;
end
end
在这段改写后的代码中,当 clear 为1时,cnt 将被清除为0;否则将执行加法操作。通过这种方式,可以避免复位信号与 clear 信号先经过一个LUT2后再送往寄存器复位引脚,从而有效解决了复位信号扇出过大的问题。
扩展:一个通用的SpinalHDL加法器模块
基于上面的优化思路,我们可以在SpinalHDL中设计一个更通用的、为FPGA优化过的计数器模块 FpgaCounter:
/**
* FPGA加法器
* @param width 加法器位宽
* @param initValue 加法器初始值
* @param incEn 标识是否使能加操作
* @param decEn 标识是否使能减操作
* @param withClearValue 是否有清除信号
* @param withSetValue 是否有置位信号
* @param clearValue 清除时的清除值
* @param setValue 置位时的置位值
*/
class FpgaCounter(width: Int, initValue: BigInt = 0, incEn: Boolean, decEn: Boolean, withClearValue: Boolean, withSetValue: Boolean, clearValue: BigInt, setValue: BigInt) extends Component {
val io = new Bundle {
val clear = (withClearValue) generate in Bool()
val set = (withSetValue) generate in Bool()
val inc = (incEn) generate in UInt (width bits)
val dec = (decEn) generate in UInt (width bits)
val count = out UInt (width bits)
}
noIoPrefix()
io.count.setAsReg() init (initValue)
val clear_value = (withClearValue) generate (U(clearValue, width bits))
val set_value = (withSetValue) generate (U(setValue, width bits))
val inc_mask_value = (incEn) generate UInt(width bits)
val dec_mask_value = (decEn) generate UInt(width bits)
val count_mask = UInt(width bits)
val clear_mask = (withClearValue) generate Repeat(io.clear, width).asUInt
val set_mask = (withSetValue) generate Repeat(io.set, width).asUInt
if (withClearValue && withSetValue) {
count_mask := (io.count & (~(clear_mask | set_mask))) | (set_value & set_mask) | (clear_value & clear_mask)
if (incEn) {
inc_mask_value := io.inc & (~(clear_mask | set_mask))
}
if (decEn) {
dec_mask_value := io.dec & (~(clear_mask | set_mask))
}
} else if (withClearValue) {
count_mask := (io.count & (~clear_mask)) | (clear_value & clear_mask)
if (incEn) {
inc_mask_value := io.inc & (~clear_mask)
}
if (decEn) {
dec_mask_value := io.dec & (~clear_mask)
}
} else if (withSetValue) {
count_mask := (io.count & (~set_mask)) | (set_value & set_mask)
if (incEn) {
inc_mask_value := io.inc & (~set_mask)
}
if (decEn) {
dec_mask_value := io.dec & (~set_mask)
}
} else {
count_mask := io.count
inc_mask_value := io.inc
dec_mask_value := io.dec
}
if (decEn && incEn) {
io.count := count_mask + inc_mask_value - dec_mask_value
} else if (decEn) {
io.count := count_mask - dec_mask_value
} else if (incEn) {
io.count := count_mask + inc_mask_value
}
}
这个加法器模块支持加、减、置位、清除四种操作。根据需求传入不同的参数即可(注意,如果同时使能清除和置位,默认不应在同一个时钟周期内同时生效)。为了使用方便,可以定义对应的伴生对象 FpgaCounter:
object FpgaCounter {
/**
*
* @param bitCount :计数器位宽
* @param inc : 累加条件
* @param dec : 减1条件
* @param clear :计数器清零
* @param set :计数器置位
* @param initValue :count初始值
* @param clearValue :clear后值
* @param setValue :set后值
* @return Count计数器
*/
def BoolCount(bitCount: BitCount, inc: Bool=null,dec:Bool=null,clear:Bool=null,set:Bool=null,initValue:BigInt=0,clearValue:BigInt = 0, setValue:BigInt= -1): UInt = {
require(inc!=null || dec!=null)
val defaultSetValue= ((BigInt(1)<<bitCount.value)-1)&setValue
val count_inst= new FpgaCounter(width = bitCount.value, initValue = initValue, incEn = inc!=null, decEn = dec!=null, withClearValue = clear!=null, withSetValue = set!=null, clearValue = clearValue, setValue = defaultSetValue)
if(clear!=null){
count_inst.io.clear:=clear
}else{
count_inst.io.clear.clear()
}
if(set!=null){
count_inst.io.set:=set
}else{
count_inst.io.set.clear()
}
if(inc!=null){
count_inst.io.inc:=U(inc,bitCount)
}else{
count_inst.io.inc.clearAll()
}
if(dec!=null){
count_inst.io.dec:=U(dec,bitCount)
}else{
count_inst.io.dec.clearAll()
}
count_inst.io.count
}
/**
*
* @param bitCount :计数器位宽
* @param inc : 累加条件
* @param dec : 减1条件
* @param clear :计数器清零
* @param set :计数器置位
* @param initValue :count初始值
* @param clearValue :clear后值
* @param setValue :set后值
* @return Count计数器
*/
def UIntCount(bitCount: BitCount, inc: UInt=null,dec:UInt=null,clear:Bool=null,set:Bool=null,initValue:BigInt=0,clearValue:BigInt = 0, setValue:BigInt= -1): UInt = {
require(inc!=null || dec!=null)
val defaultSetValue= ((BigInt(1)<<bitCount.value)-1)&setValue
val count_inst= new FpgaCounter(width = bitCount.value, initValue = initValue, incEn = inc!=null, decEn = dec!=null, withClearValue = clear!=null, withSetValue = set!=null, clearValue = clearValue, setValue = defaultSetValue)
if(clear!=null){
count_inst.io.clear:=clear
}else{
count_inst.io.clear.clear()
}
if(set!=null){
count_inst.io.set:=set
}else{
count_inst.io.set.clear()
}
if(inc!=null){
count_inst.io.inc:=inc
}else{
count_inst.io.inc.clearAll()
}
if(dec!=null){
count_inst.io.dec:=dec
}else{
count_inst.io.dec.clearAll()
}
count_inst.io.count
}
}
这里定义了 BoolCount 和 UIntCount 两个方法,分别对应加1或加N的场景。使用时可以这样调用:
case class CounterTest() extends Component {
val io=new Bundle{
val inc= in UInt(8 bits)
val dec= in UInt(8 bits)
val set= in Bool()
val clear= in Bool()
val cnt= out UInt(8 bits)
}
noIoPrefix()
io.cnt:=FpgaCounter.UIntCount(bitCount = 8 bits, inc = io.inc, dec = io.dec, clear = io.clear, set = io.set, initValue = 0, clearValue = 0, setValue = 0xff)
}
写在最后
使用FPGA进行设计时,越来越体会到必须结合器件自身特性来设计电路这条经验的重要性。在云栈社区中,也有许多开发者分享过类似的硬件描述语言与底层硬件结合优化的心得,这对于写出高效、时序稳定的代码至关重要。