Skip to content
VLSI Mentor

UART · Module 12

Synthesis and Timing Constraints

Constraining an input that has no real setup requirement, and the two structures a tool will silently build from ambiguous RTL — both demonstrated in simulation.

Synthesis believes exactly what it is told, including the things you did not realise you were saying. Two categories of problem follow, and they are opposites:

Constraints that are missing or wrong. The tool invents a requirement, and either fails timing on a path that has none or meets an imaginary one while ignoring a real constraint.

RTL that is ambiguous. The tool builds something reasonable and unintended — a latch, or a clock made of combinational logic. Both are demonstrated below, in simulation, where their behaviour is visible.

1. The Asynchronous Input Has No Setup Requirement

rx_i arrives from a device with an unrelated oscillator. There is no launching clock, so there is no meaningful setup or hold relationship to constrain — and a tool given no guidance will assume one anyway, usually relative to the clock's period, then try to close it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# The clock, and the input that is not related to it.
create_clock -name clk -period 10.000 [get_ports clk]

# rx_i is asynchronous. Cutting the path says so.
set_false_path -from [get_ports rx_i]
set_false_path -from [get_ports cts_n_i]

set_false_path says "do not time this", and that is correct here because the receiving flop is a synchroniser whose whole purpose is to tolerate an arbitrary arrival. Metastability is handled structurally (Chapter 12.1), not by timing analysis.

2. Reset Needs Constraints Too

Chapter 12.4 made reset release a synchronous event with recovery and removal requirements. Those are real checks on a real path, and the reset tree is typically the highest-fanout net in the design.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# The asynchronous ASSERTION has no timing requirement.
set_false_path -from [get_ports arst_n]

# The synchronised release does. Recovery and removal are checked on the
# path from the reset synchroniser's output to every flop it resets, and
# they are NOT cut — cutting them is what lets flops leave reset on
# different edges, which is the failure the synchroniser exists to prevent.

Cut the asynchronous source; keep the synchronised distribution. A blanket set_false_path on everything named reset removes the one check that matters and leaves the one that does not.

3. The Inferred Latch

An always_comb block that does not assign its output on every path describes something that must hold — and a tool will build storage to hold it.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MISSING else -> y_latch must HOLD its previous value -> a latch.
always_comb begin
    if (sel) y_latch = a;
end

// Complete: every path assigns.
always_comb begin
    if (sel) y_comb = a;
    else     y_comb = b;
end

Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 1. incomplete always_comb
     sel=1 : y_latch=aa  y_comb=aa   (a=aa)
     sel=0 : y_latch=aa  y_comb=55   (b=55)
  pass complete version switches to b
  pass INCOMPLETE version HOLDS the old value — this is a latch
     a changed to 3c, sel still 0 -> y_latch=aa (stale, storing state)
  pass the latch is storing state the design never asked for

The last line is the one to read. With sel low, a was changed to 0x3C and the incomplete version still reported 0xAA — a value from two changes ago. It is storing state, and nothing in the source says so.

4. The Accidental Gated Clock

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wire gclk = clk & en_comb;         // WRONG — a clock made of logic
always @(posedge gclk) cnt <= cnt + 1;

This looks like a power optimisation and is a functional hazard. A combinationally-derived enable can glitch, because its inputs arrive at different times, and a glitch on a clock is a clock edge.

Measured, with the enable built from two signals whose paths differ by 2 ns — which is ordinary, not contrived — and the transition arranged to fall inside a clock-high interval:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 2. a clock built from combinational logic
     during ONE clk-high interval, with a 2 ns glitch on the enable:
       gated-clock counter  = 1
       clock-enable counter = 0
  pass clock-enable version: ZERO extra counts — the glitch is invisible to it
  pass gated-clock version: the glitch became a real rising edge

One spurious clock edge, from a glitch the RTL says nothing about. The counter advanced at a moment no clock edge existed.

The correct form keeps one clock and makes the enable data:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// RIGHT — the enable is SAMPLED at a clock edge, so a glitch between edges
// is invisible. This is Chapter 8.1's rule, and the reason for it.
always_ff @(posedge clk) if (en_comb) cnt <= cnt + 1;

The enable version counted zero, because a glitch between clock edges is simply not observed.

5. Keeping It One Honest Timing Path

The properties that make a design analysable, all of which the UART as built satisfies:

PropertyHow the UART satisfies it
one clock, defined in the constraintsclk, everything else is an enable
no generated clocksChapter 8.1 — os_tick and baud_tick are enables
no latchesevery always_comb assigns on all paths
asynchronous inputs cutrx_i, cts_n_i
CDC datapaths bounded, not cutthe gray pointer buses
reset release timedrecovery and removal kept
no combinational loopsone was found and fixed — Chapter 12.3 §5

The combinational loop is worth remembering as the practical warning. The asynchronous FIFO's first version computed full combinationally, closing a cycle through the pointer arithmetic. Synthesis reports those, but the simulator found it first by hanging — no error, no message, just a run that never advanced. A tool that produces no output is not always telling you nothing.

6. Attributes: Telling the Tool the Chain Is a Synchroniser

Constraints describe timing. They do not tell a synthesiser that uart_sync_edge's two flops are a synthesiser chain rather than two ordinary flops that happen to be in series — and that distinction changes what the tool is allowed to do to them.

Three optimisations are perfectly legal on ordinary flops and destroy a synchroniser:

Retiming. A retiming pass may move combinational logic across a flop boundary to balance path delays. Do that to a synchroniser and there is now logic between the stages, consuming exactly the settling time the second flop existed to provide.

Merging. Two synchronisers fed by the same source and clocked by the same clock look like duplicate logic. A tool may merge them — which is correct for logic and wrong here, because Chapter 12.2 §3 wants them not merged only when they are genuinely separate; when they are meant to be one, merging is fine. The dangerous direction is the other one: a tool replicating a synchroniser for fanout, producing two chains that can resolve the same ambiguous edge differently.

Placement. The first and second flop should be physically adjacent, so that the settling time available is as close to a full clock period as routing allows. Nothing in the RTL says so.

The attribute that says all of this varies by vendor, and it belongs on the chain register, not on the module:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Xilinx (Vivado): ASYNC_REG keeps the chain together, blocks retiming
// across it, and tells the placer to keep the flops in one slice.
(* ASYNC_REG = "TRUE" *) logic [STAGES-1:0] chain_q;

// Intel (Quartus): the same intent, plus an explicit MTBF optimisation hint.
(* altera_attribute = "-name SYNCHRONIZER_IDENTIFICATION FORCED_IF_ASYNCHRONOUS" *)
logic [STAGES-1:0] chain_q;

// Synopsys DC / Fusion Compiler: keep the chain, forbid retiming through it.
// Usually applied from the constraint file rather than in the RTL:
//   set_dont_touch    [get_cells u_sync/chain_q_reg*]
//   set_dont_retime   [get_cells u_sync/chain_q_reg*] true

7. A Complete Constraint File for the Three Published Blocks

Scattered examples are easy to agree with and hard to use. This is the whole constraint set for a UART with the three CDC blocks of this module instantiated — a 100 MHz system clock, a 12 MHz peripheral clock, and an asynchronous line.

Every constraint is annotated with the RTL decision it corresponds to, because a constraint whose justification nobody remembers is a constraint that will be deleted by the next engineer who sees it failing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#=========================================================================
#  uart_cdc.sdc — constraints for uart_sync_edge, uart_reset_sync,
#                 uart_async_fifo
#=========================================================================

#---- 1. The clocks, and the fact that they are unrelated ----------------
create_clock -name clk_sys  -period 10.000 [get_ports clk]
create_clock -name clk_peri -period 83.333 [get_ports pclk]

# Declaring the groups asynchronous is what stops the tool inventing a
# setup relationship between two clocks that have none. Without this a
# tool will try to close a path between them and report failures that
# mean nothing.
set_clock_groups -asynchronous \
    -group [get_clocks clk_sys] \
    -group [get_clocks clk_peri]

#---- 2. The serial line: one bit, cut ----------------------------------
# uart_sync_edge absorbs the arrival uncertainty structurally. There is no
# launching clock, so there is no setup requirement to meet.
set_false_path -from [get_ports rx_i]
set_false_path -from [get_ports cts_n_i]

# The TX side is an OUTPUT to an unrelated receiver. Its timing is a
# board-level question, not an STA one.
set_false_path -to [get_ports tx_o]
set_false_path -to [get_ports rts_n_o]

#---- 3. Reset: cut the assertion, KEEP the release ----------------------
set_false_path -from [get_ports arst_n]

# Recovery and removal on the synchronised release are real checks on a
# real path and are deliberately NOT cut. See Chapter 12.4 section 2.
# A blanket false path on "*reset*" removes exactly this check.

#---- 4. The FIFO pointer crossing: BOUND, do not cut --------------------
# Gray coding guarantees one bit changes per increment. That guarantee is
# only worth anything if the bits ARRIVE together, so the datapath is
# bounded rather than cut. -datapath_only excludes clock skew, which is
# meaningless between unrelated clocks.
set_max_delay -datapath_only 8.000 \
    -from [get_cells u_fifo/wgray_q_reg[*]] \
    -to   [get_cells u_fifo/rq1_wgray_reg[*]]
set_max_delay -datapath_only 8.000 \
    -from [get_cells u_fifo/rgray_q_reg[*]] \
    -to   [get_cells u_fifo/wq1_rgray_reg[*]]

# Skew between the bits of one gray bus must be small compared with the
# destination period, or two bits can be in flight at once.
set_max_skew 2.000 \
    -from [get_cells u_fifo/wgray_q_reg[*]] \
    -to   [get_cells u_fifo/rq1_wgray_reg[*]]

#---- 5. Protect the synchroniser chains from optimisation --------------
set_dont_retime [get_cells -hier *chain_q_reg*] true
set_dont_retime [get_cells -hier *q1_*gray_reg*] true
set_dont_retime [get_cells -hier *q2_*gray_reg*] true

8. Verification

The synthesis log is a verification artefact. Three warnings should be promoted to errors in any flow that is going to ship:

WarningWhy it is not benign
latch inferred§3 — almost always a missing case
combinational loopunanalysable, and simulation may just hang
clock from combinational logic§4 — edges nobody described

Two more deserve attention rather than promotion: multi-driven nets, and width mismatches in assignments. Neither is always a defect, and both are worth reading rather than filtering.

Constraints need review like code. A set_false_path that is too broad is invisible: the design meets timing, the report is clean, and the path that mattered was never analysed. The specific failure to look for is a wildcard cutting more than intended — cutting the distribution of a synchronised reset alongside its asynchronous source, for instance, which silently removes the guarantee Chapter 12.4 built.

Check the clock report, not just the timing summary. It lists every clock the tool found — including ones it inferred from your combinational logic. A UART should have exactly one, and a second entry named after an internal signal is §4 happening.

9. Debugging

10. What This Means on an FPGA

Mark the synchronisers. ASYNC_REG on Xilinx, and the equivalent elsewhere, keeps the stages adjacent and preserves the settling time Chapter 12.1's arithmetic assumed. Without it the placer may separate them and the margin is spent silently.

FPGAs have dedicated clock routing, and combinational clocks do not use it. A clock built from an AND gate travels on ordinary routing with ordinary skew, which adds a second problem on top of the glitch — and the tool will often warn about exactly this.

The UART is rarely the critical path. At 100 MHz with a 115,200 baud link, nothing in it is tight; the counters are short and the FSMs are small. If a UART is failing timing, suspect a constraint before suspecting the logic.

Constrain the pins, not just the internals. set_input_delay and set_output_delay on tx_o and rts_n_o matter for board-level timing even though the UART's internal paths are relaxed — and at UART speeds the requirements are so loose that the usual mistake is omitting them entirely rather than getting them wrong.

11. Understanding Check

12. Summary

Cut single-bit asynchronous inputs; bound multi-bit crossings. set_false_path is right for rx_i and wrong for a gray pointer bus, whose one-bit-at-a-time guarantee depends on bounded skew — set_max_delay -datapath_only is the constraint that preserves it.

Cut the asynchronous reset source; keep the synchronised distribution timed. Recovery and removal are the checks that make every flop leave reset together, and a blanket reset false path deletes exactly them.

An incomplete always_comb builds a latch, and it is storing state nobody asked for — demonstrated by changing an input and watching a stale value persist. Assign a default first; use always_comb; treat the warning as an error.

A clock built from combinational logic gets edges from glitches. A 2 ns path difference produced one spurious clock edge where the clock-enable form produced none. That is the implementation reason behind Chapter 8.1's rule: os_tick and baud_tick are enables, and the UART has one clock.

An over-broad constraint is more dangerous than a missing one, because it produces a clean report instead of a failure.

And the practical warning from building this module's RTL: a simulator that hangs with no output may be reporting a combinational loop. It was, and the loop was real.

13. What Comes Next

The design is correct, constrained, and analysable. Chapter 12.6 asks what actually gets built, and how the same RTL lands differently on two technologies.

FIFOs become block RAM or distributed logic depending on depth and width; reset is free on one technology and mandatory on the other; I/O behaviour, clock resources and synchroniser primitives all differ. It is the last thing between this IP and a real part, and it is the chapter that closes the module.

Browse the full path on the UART tutorials index. For the enable-versus-clock decision this chapter justifies at the implementation level, read back to Chapter 8.1.

Continue learning

Where this fits

Part of the UART curriculum.