Running CTS on a Gated Design
This closes Module 5. The UART gets one modification, a gated variant of its receive-side baud generator, and goes through CTS for real, ending the module with the exact structure the previous two articles described in the abstract.
The gated variant
The unmodified UART runs u_rx_baud continuously. This variant, uart_top_gated, adds an enable, rx_baud_en, generated by the receiver’s frame-state logic and asserted only while the receiver is mid-frame, from start bit detected to stop bit complete. While the line sits idle the enable is low and the baud counter’s clock stops.
Because Yosys does not infer clock gating cells, the ICG is instantiated directly from the SKY130 library:
// uart_top_gated.sv: receive-side baud generator, now clock-gated
logic rx_baud_en; // asserted by u_rx only while mid-frame
logic rx_clk_gated; // ICG output; CTS treats this as a local clock root
sky130_fd_sc_hd__dlclkp_1 u_icg_rx_baud (
.CLK (clk),
.GATE (rx_baud_en),
.GCLK (rx_clk_gated)
);
baud_tick_gen_16x #(.CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE))
u_rx_baud (.clk(rx_clk_gated), .rst_n(rst_n), .tick_16x(tick_16x));
rx_baud_en comes from u_rx’s own frame-state register, which is exactly the enable path the previous article flagged as needing its own setup check into the ICG latch.
Running CTS
% run_cts
[INFO]: Building RC values for the design based on the extraction rules...
[INFO]: Detected 1 integrated clock gating cell on net clk: sky130_fd_sc_hd__dlclkp_1 (u_icg_rx_baud)
[INFO]: Treating u_icg_rx_baud/GCLK as a local clock root for downstream sinks.
[INFO]: Root buffer selected: sky130_fd_sc_hd__clkbuf_16
[INFO]: Clustering 58 clock sinks across 2 domains (ungated: 51, gated: 7)...
[INFO]: Inserting buffered clock tree...
[INFO]: Clock tree synthesis complete.
The line that matters is the second. TritonCTS detects the ICG and treats its output, u_icg_rx_baud/GCLK, as a local clock root for the seven flops downstream of it, the receive baud counter’s bits. That is the first-class treatment the previous article argued for, not an afterthought bolted onto the ungated tree.
The resulting tree
Two clock domains, structurally: 51 sinks fed by the ordinary ungated tree exactly as built in Section 5.3, and 7 sinks, the gated counter’s bits, fed through the ICG as their local root. The ICG sits as a genuine node in the tree, not a leaf and not an annotation. Every skew and latency number for the 7 gated sinks is computed starting from GCLK and carries the ICG’s own insertion delay, exactly as the previous article’s equation required.
Checking the new paths
The two checks the earlier articles predicted, run for real against this variant. First the enable path:
Startpoint: u_rx/frame_state_reg (enable-generating register)
Endpoint: u_icg_rx_baud (integrated clock gating cell)
Path Group: clk
Path Type: max
data required time 19.612
data arrival time 0.847
--------------------------------------------------------------
slack (MET) 18.765
It passes with wide margin, unsurprising at 50 MHz with a short enable-generation path, but this is the check that would not exist at all in the ungated design, now confirmed clean rather than assumed clean. Then the cross-domain path:
Startpoint: u_rx/shift_reg[3] (ungated domain)
Endpoint: u_rx_baud/count_reg[2] (gated domain)
Path Group: clk
Path Type: min
data required time 0.520
data arrival time 0.541
--------------------------------------------------------------
slack (MET) 0.021
One register in the ungated tree, one downstream of the ICG. It passes with 21 ps of hold margin, because TritonCTS’s ICG-aware latency accounting, the log line treating GCLK as a real local root, gave the check accurate numbers from the start rather than an underestimate a designer would only discover was wrong once it was expensive to fix.
Not every violation on a gated-domain boundary is this clean. These numbers land comfortably because the design and frequency were chosen so the concept lands clearly. A larger design with tighter margins and more gated domains needs this same class of check run far more times, across every boundary and every corner (Module 7), before a tree touching several gated domains can be signed off.
Practice Exercise
Suppose a second gated domain is added: the transmit baud generator, u_tx_baud, gated by a tx_baud_en from u_tx’s frame-state logic, structured identically to the receive-side gating above. List every new check this module’s articles would require that a fully ungated UART would never need.
Solution
For the new u_tx_baud domain, by direct analogy:
- A setup check on the
tx_baud_enenable path into its own ICG latch, the enable-path check for a second ICG. - Skew and hold checks on any path crossing between the
u_tx_baudgated domain and the ungated rest of the design, the cross-domain check for a second boundary. - If any path connects the two gated domains directly,
u_tx_baudtou_rx_baudor the reverse, a third check spanning both ICGs’ accumulated insertion delay. Unlikely in this UART, since transmit and receive are largely independent, but worth ruling out explicitly rather than assuming.
None of the three exist in a design with no gating. Each is a direct consequence of an ICG entering the clock network as a first-class element.
The UART now has a complete, timing-clean, gating-aware clock tree. Module 6 picks it up exactly here and takes it through global and detailed routing, turning every remaining net, clock and data alike, into real, DRC-legal metal.