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.
# 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.
# 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.
// 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;
endMeasured:
-- 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 forThe 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
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:
-- 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 edgeOne 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:
// 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:
| Property | How the UART satisfies it |
|---|---|
| one clock, defined in the constraints | clk, everything else is an enable |
| no generated clocks | Chapter 8.1 — os_tick and baud_tick are enables |
| no latches | every always_comb assigns on all paths |
| asynchronous inputs cut | rx_i, cts_n_i |
| CDC datapaths bounded, not cut | the gray pointer buses |
| reset release timed | recovery and removal kept |
| no combinational loops | one 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:
// 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*] true7. 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.
#=========================================================================
# 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*] true8. Verification
The synthesis log is a verification artefact. Three warnings should be promoted to errors in any flow that is going to ship:
| Warning | Why it is not benign |
|---|---|
| latch inferred | §3 — almost always a missing case |
| combinational loop | unanalysable, 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
Related tutorials
- Related topic
Reset Strategy and Reset During Active Traffic
Reset style and release, and what a reset asserted mid-frame actually leaves behind — measured, and worse than a framing error: well-formed frames carrying bytes nobody sent.
- Related topic
FPGA and ASIC Implementation Differences
Reset resources, memory choice for FIFOs, I/O and clocking — the places where identical UART RTL becomes two different pieces of hardware, with the numbers worked out.
- Related topic
Sequence Item and Sequence Library
Modelling a UART frame as a transaction, the constraints that keep a random frame legal and the soft ones that let a sequence ask for an illegal frame on purpose, and the distribution that puts stimulus where the failures are.
- Related topic
Input and Output Delay Constraints
A constraint cannot be simulated — set_input_delay describes a board. So the RTL is one register per pin and no logic, the bench checks only what the constraints assume, and the file is written three times for three tools. Adding that pad register also moves three numbers Module 14 published.
Where this fits
Part of the UART curriculum.
