# Question about pipeline techniques in FPGA

I'm trying to improve timing in my FPGA design. I need my logic to work with a 150MHz clock, the synthesizer is saying it can work only with ~138MHz maximum.

I know that one of the popular ways to improve timing is by pipelining some of the logic in the design.

I have a finite state machine (FSM) that reads data from an ADC, all the data reading, and the creating of SCLK for the interface occurs inside one process (I am using VHDL.)

In this FSM there are two counters that increment in every state. One of the counters is used to generate the SCLK (dividing the internal FPGA clock to create the SCLK for the ADC.) The other counter counta the bits I read/write, together these counters dictate when I switch states in the FSM.

One of the counters is counting as well as the condition to read bits from the ADC all in the same clock cycle.

The critical path is the path between the output of the counter flip-flop to the data shift register that occurs inside an "if (count = ...)" block.

I am using a Lattice MachXO3LF FPGA with a custom board.

As a result, I thought about pipelining the counter, which means to leave the counter as is in the process but just to assign inside the process the counter to another counter_reg, and use inside the if conditions the counter_reg instead of the counter:

(note that the pieces of code are incomplete and just to show the idea in example)

Without pipeline

process(clk)
if (rising_edge(clk)) then
case (state) is
when STATE0 =>
if (count = NUM) then
state <= STATE1;
count <= 0;
else
count <= count+1;
end if;
...


With pipeline:

process(clk)
if (rising_edge(clk)) then
count_reg <= count;
case (state) is
when STATE0 =>
if (count_reg = NUM-1) then
state <= STATE1;
count <= 0;
else
count <= count+1;
end if;
...


Is this a valid way to make a pipeline in FPGA design if I need to pipeline and improve the timing between the counter and the logic created inside the if condition used by the counter?
I found that it does improve my timing.

If it improved my timing, what else do I want from you guys?

It is improved but not enough (now the maximum clock is now 148MHz,) so I tried something:

Another attempt to pipeline:

process(clk) begin
count_reg <= count;
if (rising_edge(clk)) then
case (state) is
when STATE0 =>
if (count_reg = NUM) then
state <= STATE1;
count <= 0;
else
count <= count+1;
end if;
...


As you can see, I tried to make the assignment to my pipeline register outside of the clock rising edge, and my counter_reg now changing every change in clk (rising/falling edge.) As you can see now there no need to do NUM-1 anymore because there is no delay in the counter (because the counter_reg changing in the falling edge, and until next rising edge of clk, the counter reg is already ready to go.)

What I found out is that the synthesizer loved this move, now there is no timing error for this path anymore, and my maximum clock cycle is 153MHz, so from a timing report perspective, I solved my problem.

BUT, I am not sure if it's a good design practice I am using the rising and falling edges of the clock to do my logic and far as I know it's not recommended, but it does solve my timing errors and works perfectly.

What do you think about this? Is it ok to make a pipeline like this? What am I missing?

• Have you checked what is the actual critical path of your design? Also, you should include what's the board and CAD you're designing for.
– edmz
Jun 29, 2021 at 12:36
• Usually, the synthesizer doesn't actually care about the sensitivity list. Rather it looks for a template that includes a sensitivity list and if rising edge statement. So you need to double-check what exactly is happening there. The first method, of course, is a good way to pipeline assuming that functionality is intact. Jun 29, 2021 at 12:46
• You can also have improve timing, in general, by pipelining the if conditions as well. Instead of writing a long if statement, you can write it in a clocked process and use it. As usual, careful here, check the functionality is not changing (draw the timing diagram) Jun 29, 2021 at 12:51
• The second statement on count_reg. I really doubt if this will work on an actual board after synthesis. It doesn't make any sense. Jun 29, 2021 at 12:54
• + the piece of code you posted doesn't look like critical path. You should find the actual timing path before optimizing. + What are your timing constraints, targeted technology etc? Jun 29, 2021 at 12:58

The first thing to do is look at the STA (Static Timing Analysis) report. That will show the longest paths, their start and end points, and the logic between them. There are probably a lot of similar ones, pointing to one specific design problem.

Re-pipeline that path, adjusting anything else necessary (e.g. delaying signals elsewhere) to keep the logic correct, re-simulate to show it's still correct, and re-synthesise etc to get a new STA result. There may be another failing path in this one; rinse and repeat.

If you're stuck, or can't see why the longest path is so long, edit that path's analysis into the question so more eyes can help. It's not always obvious...

From 148 to 150MHz you may get away with applying more synthesis or P&R effort : e.g. ask some tools for 160MHz and they'll still fail but give 155 MHz...

Critique on code...

process(clk)
if (rising_edge(clk)) then
case (state) is
when STATE0 =>
if (count = NUM) then
state <= STATE1;
count <= 0;
else
count <= count+1;
end if;
...


This ought to synthesise just fine and get close to optimal.

However some synthesis tools are a bit sub-par, and may make the assumption that incrementing Count follows a conditional test on itself, and unnecessarily sequentialises the two operations.

In fact the two operations (increment, and test) can happily proceed in parallel, with no need to add a pipeline register (which feels like a crutch here.

I would signal that parallelism explicitly to synthesis...

process(clk)
if (rising_edge(clk)) then
new_count := count+1;     -- variable, updates immediately
case (state) is
when STATE0 =>
if (count = NUM) then
state <= STATE1;
count <= 0;
else
count <= new_count; -- variable used after assignment
end if;
...


This related Q&A separates out the delay counter altogether, allowing multiple states to use it for different length delays; also taking advantage of the fact that a zero comparison is simpler and often faster than an equality counter.

Your first pipelined version includes a cycle delay, giving further problems requiring complex little fixes, and your second implicitly updates count_reg on the NEGATIVE clock edge, requiring the counter to work in HALF a cycle. The implicitness dares synthesis to get it wrong!!! but you can add an ELSE or elsif falling_edge(clk) to see if that reproduces the faster result. If not, carefully inspect the generated RTL to see if the expected -ve-edge clocked reg is there...

• Thanks for the answer. I already used one of your suggestions to applying more synthesis effort by telling it I need the clock speed is higher than what I really need and look like it helps a little bit. Jun 29, 2021 at 14:51
• about using a variable to count, just to be sure, new_count is variable and count will stay signal right? I want to try it now Jun 29, 2021 at 14:53
• The counter isn't the problem here, but if it were, the best thing you could do to it is to have it count from a non-zero starting point so that the comparison can be replaced by checking the carry flag on the addition, as the wide comparison needs chained combinatorics while neither the addition nor loading a constant do. Jul 13, 2021 at 18:30

The speed at which your design can be clocked safely (without race conditions / violating timing) is governed by the worst case combinational logic path in your design (i.e. between flip-flops). The way pipelining can help with that is by making the path length of your combinational logics shorter by introducing flip-flop boundaries in those paths. The trade off, of course, is that it requires multiple clock cycles to push a signal all the way through a pipelined path. So, generally speaking while pipelining has the benefit of possibly increasing your maximum clock rate, it's real benefit is increasing throughput. Basically I'd be looking at other implementation details to find the choke points before considering introducing pipelining.

Here's an example that's in the spirit of what you were trying to do, but it moves the comparison to the previous clock cycle.

process(clk)
if (rising_edge(clk)) then
if count = NUM-1 then
count_equals_num <= '1';
else
count_equals_num <= '0';
end if;
case (state) is
when STATE0 =>
if (count_equals_num = '1') then
state <= STATE1;
count <= 0;
else
count <= count+1;
end if;
...