Timing Optimization in FPGA Design

✎ Author’s Note

Recently, I worked on a project that had poor performance, and optimizing the timing was quite challenging. During the optimization process, I encountered a unique optimization related to the adder, which I would like to document.

Issues with Reset Signal Fanout in Adders

For reset signals in FPGA design, they are primarily used on registers. However, in one project, I found that the reset was connected to a large number of LUTs, leading to excessive fanout, particularly in the design of adders.

Welcome FPGA engineers to join our official WeChat technical group.

Click the blue words to follow us on WeChat – the largest FPGA engineer community in China!

Timing Optimization in FPGA Design

The adder design is quite simple, as shown in the circuit below:

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

This is a very simple 8-bit adder. The cnt is set to 0 on reset, cleared when the clear signal is active, and increments by 1 when inc is 1.

This circuit, during synthesis, will connect rst and clear to a LUT2, which then drives the reset pin of the register.

This seems to indicate that the synthesis tool is not very intelligent. Ideally, we want the reset to be sent directly to the reset pin of the register, but the tool’s recognition of the code structure appears to be lacking.

How to Resolve This at the Code Level

Since the tool is not smart enough, we need to guide it. We can add constraints to the rst signal to resolve this issue. For the adder, the best practice here is to optimize the code to make it look more like the structure used in FPGAs.

The ideal model for a register is roughly:

always_ff @(posedge clk ) begin
    if(rst) begin
        Q <= 1'b0;
    end else begin
        Q <= D;
    end
end

Here, we similarly modify the adder circuit:

always_ff @(posedge clk ) begin
    if(rst) begin
        cnt <= 8'd0;
    end else begin
        cnt <= (cnt&{8{~clear}}) + 8'((~clear) & inc );
    end
end

In this case, when clear is 1, cnt will be reset to 0; otherwise, addition will be performed. This way, we can avoid the excessive fanout of the reset signal caused by passing it through a LUT2 before reaching the register’s reset pin.

Timing Optimization in FPGA Design

Extending the Concept

Based on the above idea, we can create a more general adder in SpinalHDL called FPGACounter:

/**
 * FPGA Adder
 * @param width Width of the adder
 * @param initValue Initial value of the adder
 * @param incEn Enable increment operation
 * @param decEn Enable decrement operation
 * @param withClearValue Whether there is a clear signal
 * @param withSetValue Whether there is a set signal
 * @param clearValue Value when cleared
 * @param setValue Value when set
 */
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))
    }
  } elseif (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)
    }
  } elseif (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
  } elseif (decEn) {
    io.count := count_mask - dec_mask_value
  } elseif (incEn) {
    io.count := count_mask + inc_mask_value
  }
}

This adder supports four operations: addition, subtraction, setting, and clearing. Depending on the required functionality, different parameters can be filled in (if both clear and set are enabled, it defaults to not activating both in the same clock cycle). For convenience, we define the FpgaCounter Object:

object FpgaCounter {

/**
   * @param bitCount : Width of the counter
   * @param inc : Increment condition
   * @param dec : Decrement condition
   * @param clear : Clear the counter
   * @param set : Set the counter
   * @param initValue : Initial value of count
   * @param clearValue : Value after clear
   * @param setValue : Value after set
   * @return Count counter
   */
  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 : Width of the counter
     * @param inc : Increment condition
     * @param dec : Decrement condition
     * @param clear : Clear the counter
     * @param set : Set the counter
     * @param initValue : Initial value of count
     * @param clearValue : Value after clear
     * @param setValue : Value after set
     * @return Count counter
     */
    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
    }

}

Here, we define BoolCount and UIntCount, corresponding to the scenarios of adding 1 or adding n. When using, it can be done as follows:

Timing Optimization in FPGA Design

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)
}

Final Thoughts

With FPGA, I increasingly feel that when designing circuits, one must consider the characteristics of FPGAs. This is very true.

Timing Optimization in FPGA Design

Scan to follow FPGA family

Welcome to join the largest FPGA technical community in China!

Timing Optimization in FPGA Design

Welcome FPGA, embedded, signal processing engineers to follow our public account.

Follow us for more technical insights!

Attackers! Join us!

Timing Optimization in FPGA Design

The largest FPGA technical community in the country!

Timing Optimization in FPGA Design

Welcome everyone to join the strongest FPGA WeChat technical group with tens of thousands of engineers and a group of passionate engineers who love technology and share knowledge in a rich technical atmosphere! Hurry up and invite your friends to join!

Timing Optimization in FPGA Design

Press and hold to join the FPGA national technical group!

Timing Optimization in FPGA Design

The best Xilinx and Altera chip suppliers in the country!

Self-operated platform, guaranteed quality of imported products!

Continuously supplying many military research institutes and listed companies!

The best FPGA chip prices and the best after-sales service!

With the most top-notch service reputation in the industry!

Global top suppliers with quality standards!

XILINX full series ordering or in-stock advantages!

XCVU9P-2FLG2104I 2000PCS

XCVU9P-2FLG2104I 5000PCS

XCVU13P-2FLG2104I 3000PCS

XCV7K325T-2FFG900I 15000PCS

XCV7K325T-2FFG676I 9500PCS

XCV7K325T-2FFG676I 8500PCS

XCV7K325T-2FFG676I 7500PCS

For more model inquiries, please consult us! Thank you! Scan the QR code to join!

Timing Optimization in FPGA Design

FPGA technical group thanks the following brands:

Xilinx, Intel (Altera), Microsemi (Actel), Lattice, Vantis, QuickLogic, Lucent, etc. for their support to the technical community!

Leave a Comment