Running Multi-Corner Analysis on the UART
In the previous article we looked at why one corner is not enough. In this article we run all three on the UART: extract the design at the matching extraction corners, load everything into one OpenSTA session, and check the parity path at each corner.
Extracting at every corner
Two separate things vary between corners. The larger one is the cells: the slow Liberty file describes weaker transistors, so every cell drives its load more slowly. The smaller one is the wires. Metal thickness, width and the dielectric between layers also vary in manufacturing, so the same routed geometry has slightly more or less resistance and capacitance from one wafer to another. That is what the three SKY130 extraction rules files from Section 7.2 describe: min, nom and max.
The usual pairing is to put the slow cells with the maximum RC and the fast cells with the minimum RC, so each corner is pessimistic in the same direction. Extraction runs once per RC corner. The simplest way is one short OpenROAD session per rules file, each ending with its own SPEF:
# run once with RC=min, once with RC=nom, once with RC=max
read_lef $PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/techlef/sky130_fd_sc_hd__nom.tlef
read_lef $PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/lef/sky130_fd_sc_hd.lef
read_def runs/uart_full/results/routing/uart_top.def
define_process_corner -ext_model_index 0 X
extract_parasitics -ext_model_file $PDK_ROOT/sky130A/libs.tech/openlane/rules.openrcx.sky130A.$::env(RC).calibre
write_spef runs/uart_full/results/extraction/uart_top.$::env(RC).spef
For parity_net, the max-RC extraction gives a few percent more capacitance than the nominal 118.6 fF, and the min-RC one a few percent less. The geometry did not change. The assumptions about how thick and wide the metal came out did.
Loading all three corners into one session
openroad
% define_corners tt ss ff
% read_liberty -corner tt $PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/lib/sky130_fd_sc_hd__tt_025C_1v80.lib
% read_liberty -corner ss $PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/lib/sky130_fd_sc_hd__ss_100C_1v60.lib
% read_liberty -corner ff $PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/lib/sky130_fd_sc_hd__ff_n40C_1v95.lib
% read_lef $PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/techlef/sky130_fd_sc_hd__nom.tlef
% read_lef $PDK_ROOT/sky130A/libs.ref/sky130_fd_sc_hd/lef/sky130_fd_sc_hd.lef
% read_def runs/uart_full/results/routing/uart_top.def
% read_sdc runs/uart_full/results/routing/uart_top.sdc
% set_propagated_clock [all_clocks]
% read_spef -corner tt runs/uart_full/results/extraction/uart_top.nom.spef
% read_spef -corner ss runs/uart_full/results/extraction/uart_top.max.spef
% read_spef -corner ff runs/uart_full/results/extraction/uart_top.min.spef
define_corners creates three named corners, and the -corner option on each read_liberty and read_spef attaches that file to one of them. OpenSTA keeps three separate sets of delays in the same session instead of overwriting one with the next, and any report can then be asked for at a given corner.
Checking the worst setup slack at each corner
% foreach c {ff tt ss} {
puts "corner $c"
report_checks -corner $c -path_delay max -format end
}
The endpoint summary for each corner gives the worst setup slack. Collected into a table:
| Corner | Liberty | Extraction | Worst setup slack | Status |
|---|---|---|---|---|
ff |
ff_n40C_1v95 |
min RC | +1.45 ns | Comfortable pass |
tt |
tt_025C_1v80 |
nominal RC | +0.19 ns | Passes, barely |
ss |
ss_100C_1v60 |
max RC | −2.00 ns | Violated |
There it is. The same netlist, the same routing and the same parity_net: a comfortable pass at the fast corner, a narrow pass at typical, and a clear failure at the slow corner.
Looking at the failing path
% 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)
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
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
-5.56 data arrival time
-----------------------------------------------------------------
-2.00 slack (VIOLATED)
Every cell got slower, as expected, and the flop’s setup time more than doubled. But the biggest change by far is the line for _2204_: 1.94 ns at typical, 2.86 ns at slow. The wire line only went from 0.01 to 0.02 ns. The load on _2204_ grew by a few percent with the max-RC extraction, but most of the increase comes from the XOR itself. At the slow corner its transistors deliver less current, and a weak driver charging a large capacitance is the case where that shows up most. The other XORs drive small local loads and grew by about 0.2 ns each. _2204_ drives 118.6 fF and grew by almost a nanosecond.
This is an important point about how corners work. The large load was always there, and at typical the XOR could just about cope with it. At the slow corner it cannot. Paths whose delay is dominated by a weak cell driving a big load are the ones that break at the slow corner, while paths made of small local nets stay positive everywhere.
It also means the fix is not in the parasitics. The SPEF describes the real wire correctly at each corner. What needs fixing is this path’s timing margin, and that is Section 7.4’s job.
Practice Exercise
A teammate suggests fixing this violation by re-extracting the slow corner with the nom rules file instead of max, arguing that the max rules are too pessimistic. Is that a legitimate fix? What would you check before agreeing to change which rules file is used?
Solution
It is not a legitimate fix, and it would not even work well: the max-RC load was only a few percent higher, and most of the extra delay came from the slow XOR. More importantly, the extraction corners are the PDK’s own description of how the metal can come out in manufacturing. Pairing the slow cell corner with nominal wires means signing off a combination that is less pessimistic than the one the foundry says can happen, for every net in the design, not just this one. A real violation needs a real timing fix on the path, the buffer insertion or gate sizing of Section 7.4, not a change to how it is measured. The only good reason to revisit the rules files is concrete evidence that they do not match the PDK release in use, for example a version mismatch, and that must be checked against the PDK documentation, not reasoned backwards from “the violation would disappear”.
We now have a confirmed violation at the slow corner. In Section 7.4 we fix it, starting with the idea of an engineering change order: a targeted fix instead of a rerun of the whole flow.