Closing a Real Timing Violation on the UART
In the last two articles we looked at what an ECO is and at the two techniques it uses. In this article we apply them to the real violation from Section 7.3, the parity path failing by 2.00 ns at the slow corner, using the multi-corner session from that section.
The starting point
% report_checks -corner ss -path_delay max -to rx_ctrl/parity_err_reg/D
Startpoint: rx_ctrl/shift_reg_reg[7]
Endpoint: rx_ctrl/parity_err_reg
Corner: ss
...
2.86 5.54 ^ _2204_/X (sky130_fd_sc_hd__xor2_2)
0.02 5.56 ^ rx_ctrl/parity_err_reg/D (sky130_fd_sc_hd__dfxtp_2)
5.56 data arrival time
3.56 data required time
-----------------------------------------------------------------
-2.00 slack (VIOLATED)
This is the same result as Section 7.3, and the thing to fix.
Running the repair
repair_timing looks at every endpoint that fails setup, across all the corners loaded in the session, and tries buffering and sizing on the paths into it until they pass or it runs out of options:
% repair_timing -setup -setup_margin 0.1
-setup_margin 0.1 asks it to aim for at least 0.1 ns of slack, not just zero. We will see in Section 7.5 why that margin matters. The log is a summary rather than a list of every change, something like:
[INFO RSZ-0094] Found 1 endpoints with setup violations.
[INFO RSZ-0040] Inserted 1 buffers.
[INFO RSZ-0041] Resized 1 instances.
[INFO RSZ-0033] No setup violations found.
The report after the repair shows what it actually did.
Making the change real
The resizer has changed the netlist and placed the new buffer, but the routing on disk still describes the old net. Before any new number can be trusted, the change has to go through the back end again:
- Legalise the placement with
detailed_placement, so the new buffer and the wider XOR sit on legal sites without overlapping anything. - Re-route the affected nets. OpenLane does this by re-running its routing steps on the modified design. In a bare OpenROAD session,
global_routehas an incremental mode for exactly this, followed bydetailed_route. - Write the updated DEF, then re-extract at each RC corner, as in Section 7.3, and reload the new SPEFs.
% write_def runs/uart_full/results/routing/uart_top.eco.def
# re-extract with the min, nom and max rules as before, writing *.eco.spef, then:
% read_spef -corner ss runs/uart_full/results/extraction/uart_top.max.eco.spef
It is easy to take the resizer’s own estimate as proof that the violation is gone. repair_timing makes its decisions with its own delay estimates for the new wiring, which are good enough to guide the repair but are not a substitute for re-routing, re-extracting and re-running STA. That is the same discipline this whole module is built on.
The corrected report
% report_checks -corner ss -path_delay max -to rx_ctrl/parity_err_reg/D
Startpoint: rx_ctrl/shift_reg_reg[7]
(rising edge-triggered flip-flop clocked by clk)
Endpoint: rx_ctrl/parity_err_reg
(rising edge-triggered flip-flop clocked by clk)
Path Group: clk
Path Type: max
Corner: ss
Delay Time Description
-----------------------------------------------------------------
0.00 0.00 clock clk (rise edge)
0.00 0.00 clock network delay (propagated)
0.00 0.00 ^ rx_ctrl/shift_reg_reg[7]/CLK (sky130_fd_sc_hd__dfxtp_2)
0.99 0.99 ^ rx_ctrl/shift_reg_reg[7]/Q (sky130_fd_sc_hd__dfxtp_2)
0.53 1.52 v _2201_/X (sky130_fd_sc_hd__xor2_2)
0.58 2.10 ^ _2202_/X (sky130_fd_sc_hd__xor2_1)
0.58 2.68 v _2203_/X (sky130_fd_sc_hd__xor2_1)
0.36 3.04 ^ _2204_/X (sky130_fd_sc_hd__xor2_4)
0.01 3.05 ^ _eco_buf_1_/A (sky130_fd_sc_hd__buf_4)
0.35 3.40 ^ _eco_buf_1_/X (sky130_fd_sc_hd__buf_4)
0.02 3.42 ^ rx_ctrl/parity_err_reg/D (sky130_fd_sc_hd__dfxtp_2)
3.42 data arrival time
4.00 4.00 clock clk (rise edge)
0.00 4.00 clock network delay (propagated)
-0.15 3.85 clock uncertainty
0.00 3.85 clock reconvergence pessimism
3.85 ^ rx_ctrl/parity_err_reg/CLK (sky130_fd_sc_hd__dfxtp_2)
-0.29 3.56 library setup time
3.56 data required time
-----------------------------------------------------------------
3.56 data required time
-3.42 data arrival time
-----------------------------------------------------------------
0.14 slack (MET)
The repair did exactly what the previous article predicted for a net like this. _2204_ was upsized from xor2_2 to xor2_4, and a buffer, _eco_buf_1_, was inserted right after it. The name will be different in your run, since the resizer picks its own names. The original net is now two: a short parity_net from the XOR to the buffer, and a new net from the buffer to the flop that carries the long route.
Arrival dropped from 5.56 ns to 3.42 ns, enough to turn −2.00 ns into +0.14 ns. As Figure 1 shows, all of the 2.14 ns came from one stage. The XOR used to spend 2.86 ns charging the whole net. Now it spends 0.36 ns charging a short stub and a buffer input, and the buffer spends 0.35 ns driving the long route, which a buffer does far better than an XOR. The first four stages are unchanged. _2203_ now drives the slightly larger input of xor2_4, but here that costs less than 0.01 ns.
Checking the other corners
An ECO that fixes one corner and breaks another is not a fix, it is a new bug. Re-checking tt and ff with their re-extracted SPEFs shows both improved, since the same stage got faster at every corner:
| Corner | Before ECO | After ECO |
|---|---|---|
ff |
+1.45 ns | +2.08 ns |
tt |
+0.19 ns | +1.36 ns |
ss |
−2.00 ns | +0.14 ns |
The slow-corner margin, +0.14 ns, is still thin compared with the +1.36 ns at typical. That is normal. The repair was aimed at closing the slow corner with a small, deliberate margin (the -setup_margin 0.1 above), not at maximising slack everywhere, since extra margin costs area and power. A thin but positive margin, confirmed on a re-extracted design, is a closed violation. Whether it is enough once signoff margins are added is the question Section 7.5 answers.
Practice Exercise
After the ECO, a hold check at the fast corner (report_checks -corner ff -path_delay min) shows that the hold slack at parity_err_reg dropped from +0.95 ns to +0.52 ns. Why did it drop, is it a problem, and what would you do if the same effect pushed a different endpoint’s hold slack negative?
Solution
It dropped because the ECO made the path faster. The hold check compares the earliest possible data change with the capturing flop’s hold requirement, and the shortest path into parity_err_reg now goes through the upsized XOR and a buffer instead of a weak XOR fighting a big load, so data arrives earlier. At +0.52 ns this endpoint is still comfortably safe: hold fails only when data arrives before the hold window closes, and a five-stage path is far from that.
The same effect matters on short paths, such as flop-to-flop connections in a shift register, which may have only a few tenths of a nanosecond of hold margin to begin with. If an ECO pushed one of those negative, you would not undo the setup fix, which was real. You would run the same repair and re-extract cycle for hold, with repair_timing -hold, which adds delay (usually small buffers or delay cells) on the short paths only. This back-and-forth is a normal part of timing closure: every change is followed by a fresh check at every corner and mode.
The module’s violation is closed and re-checked. In Section 7.5 we look at which corners SKY130 designs are actually signed off at, and run the complete signoff timing pass.