Exercise: Find and Fix a Timing Failure
This exercise teaches a core FPGA skill: when timing fails, do not panic. Read the path, understand the logic depth, and change the architecture.
Learning Objectives
You will:
- recognize a long combinational path;
- understand why pipelining improves timing;
- preserve functional behavior while adding latency;
- verify the new latency in simulation.
Prerequisites
- Sequential logic and testbenches.
- Basic static timing concepts: clock period, data path delay, slack.
- Any FPGA tool or open-source flow that reports timing.
Task
Start with an intentionally heavy one-cycle datapath:
module slow_mix (
input wire clk,
input wire [15:0] a,
input wire [15:0] b,
input wire [15:0] c,
input wire [15:0] d,
output reg [31:0] y
);
always @(posedge clk) begin
y <= ((a * b) + (c * d)) ^ ((a + c) * (b + d));
end
endmodule
At a high clock frequency this may fail timing because multiplication, addition, XOR, and routing all sit between two clock edges.
Buildable Pipelined Version
Split the work across stages:
module pipelined_mix (
input wire clk,
input wire [15:0] a,
input wire [15:0] b,
input wire [15:0] c,
input wire [15:0] d,
output reg [31:0] y
);
reg [31:0] p0;
reg [31:0] p1;
reg [16:0] s0;
reg [16:0] s1;
reg [33:0] p2;
always @(posedge clk) begin
p0 <= a * b;
p1 <= c * d;
s0 <= a + c;
s1 <= b + d;
p2 <= s0 * s1;
y <= (p0 + p1) ^ p2[31:0];
end
endmodule
This version has extra latency, but each stage has less work per cycle.
Expected Behavior
The output of pipelined_mix should match the original expression after pipeline latency. It will not match in the same cycle.
title "Illustrative pipeline latency"
time start=0 end=8 unit=cycles divisions=8
CLK: square label="clk" low=0 high=1 duty=50 cycles=4 unit=logic color=#2563eb
IN: pulse label="input sample" low=0 high=1 at=1 width=1 unit=logic color=#dc2626
STAGE1: pulse label="stage 1 result" low=0 high=1 at=2 width=1 unit=logic color=#7c3aed
OUT: pulse label="output valid later" low=0 high=1 at=3 width=1 unit=logic color=#16a34a
marker LAT at=3 label="latency"
Verification Steps
- Simulate the original and pipelined versions with the same input stream.
- Add a queue or shift register in the testbench for expected results.
- Compare
pipelined_mix.yagainst the delayed expected value. - Implement both versions with the same clock constraint.
- Compare timing reports.
- Confirm the pipelined version has better slack.
Common Failure Symptoms
| Symptom | Likely cause |
|---|---|
| Pipelined output is numerically wrong | stage alignment bug |
| Output is right but one cycle early/late | expected-value delay mismatch |
| Timing still fails | multiplier too large, DSP not inferred, or clock too aggressive |
| Resource use increases | normal cost of pipeline registers |
Debugging Guidance
- Add valid signals through the pipeline.
- Name each stage clearly.
- Check bit widths after multiplication and addition.
- Read the timing path: identify whether the multiplier, adder, or routing dominates.
- Do not add random registers without preserving data alignment.
Extension Challenge
Add valid_in and valid_out signals so the pipeline can process bursts and idle cycles. Then update the testbench to compare only when valid_out is high.
Explained Solution
Pipelining works because timing is checked between adjacent registers. The original version puts several arithmetic operations in one cycle. The pipelined version places registers between operations, reducing the maximum combinational delay per stage. The tradeoff is latency: the result appears later, but throughput can still be one result per clock after the pipeline fills.
Summary
Timing failures are design feedback. Read the critical path, reduce logic depth, use dedicated FPGA resources, and verify any added latency. Pipelining is one of the most important FPGA techniques because it improves frequency without reducing throughput.
Next: Exercise: Design an Asynchronous FIFO.
Further Reading
- Vendor timing closure methodology guides
- DSP block inference guides
- Project F tutorials on pipelining