Part 1 of this series ended with a checklist and a promise: learn the five CDC patterns, apply them reflexively, and the bugs that make it to hardware will at least be interesting. This article is about where the interesting ones live.
Here is the uncomfortable truth that part 1 deliberately deferred. You can write a textbook two-flop synchronizer, verify it in simulation, pass code review with a reviewer who has the part 1 checklist taped to their monitor, and still ship a CDC bug. Not because the pattern was wrong, but because the pattern is a request, and the synthesis and place-and-route tools are under no obligation to honor a request they were never told about. The RTL says "two flip-flops, back to back, nothing between them, physically close." The tools see two registers and a wire, and they will do to those registers whatever their optimization passes consider profitable.
CDC bugs, in other words, often live in the gap between "the RTL pattern is correct" and "the tool actually implemented it as a CDC-safe structure." The attribute that pins the synchronizer down, the constraint file that tells the timing analyzer which paths are exceptions and which are load-bearing, the CDC report that confirms the netlist matches your intent, the reset sequencing that keeps a still-locking PLL from clocking garbage through your carefully synchronized release: all of that is part of the design. Not paperwork. Design. It just happens to be design that lives outside the RTL, which is why it gets skipped, and why the resulting bugs pass the bench and wait for the field.
This article walks that territory in the same spirit as part 1: mechanism first, then the fix. The examples use AMD/Xilinx Vivado vocabulary because it is the most common FPGA flow, with Intel/Altera Quartus equivalents called out along the way. The concepts are vendor-neutral even when the spelling isn't.
The netlist is the design
Start with the mindset shift, because everything else follows from it. The RTL you wrote is not what runs. What runs is a netlist of physical primitives, produced from your RTL by a long chain of transformations, each of which is allowed to restructure your logic as long as the result is functionally equivalent. And functional equivalence is exactly the wrong standard for a synchronizer, because a synchronizer's value is not its function. Two flip-flops in series compute nothing. Their entire purpose is a set of physical properties (dedicated flip-flop storage, no logic between stages, minimal routing delay) that functional equivalence does not preserve.
Three legal, routine, well-intentioned tool behaviors are enough to make the point.
Shift-register extraction. Here is a synchronizer as most people first write it:
// Perfectly correct CDC RTL, as written.reg meta, sync;always @(posedge clk_b) begin meta <= sig_a; sync <= meta;endTo a synthesis tool, registers in series with no intervening logic are the textbook signature of a shift register, and FPGA synthesis loves shift registers, because Xilinx-style fabrics can pack a whole chain of them into a single LUT configured as an SRL primitive. That optimization is a pure win for area on ordinary logic. On a synchronizer it is a disaster: an SRL's storage cells are not flip-flops, they are storage nodes inside a LUT that no vendor characterizes for metastability resolution at all, with none of the placement guarantees you were counting on. Your synchronizer chain silently becomes one LUT. The RTL still simulates perfectly, because simulation doesn't model any of this.
One precision that matters before you test that exact snippet and dismiss the story: Vivado's extraction threshold (-shreg_min_size) defaults to 3, so the two-stage chain above survives default settings, by the luck of a default rather than by anyone's intent. The chain that does get eaten is the three-stage one, which is exactly what the tip below and every high-reliability coding standard tell you to write, and only when it has no reset (SRLs have none), which describes plenty of hand-rolled synchronizers. A hazard you dodge by tool default is not a hazard you have handled.
Register replication. Synthesis and physical optimization replicate registers to fix high-fanout nets. Suppose your synchronized signal sync drives logic scattered across the die, and the tool decides to duplicate the register to shorten routes. If it duplicates the first stage, you now have two flip-flops independently sampling the same asynchronous input. Each one resolves metastability on its own; on a bad night they resolve to different values, and two downstream blocks that both believe they are reading "the" synchronized signal disagree with each other for a cycle. This is the same divergence bug you would get by instantiating two synchronizers for one signal in RTL, which part 1 didn't dwell on, so let's dwell for one paragraph now.
Retiming and logic merging. Retiming passes move registers across combinational logic to balance pipeline stages. Applied to a synchronizer, retiming can pull logic in between the two stages, which converts your synchronizer into an ordinary two-register pipeline with a metastability hazard in the middle. Optimization can also merge "equivalent" registers, absorb a stage into a nearby LUT's flip-flop with different placement, or sweep away what looks like redundant series storage.
None of these transformations is a tool bug. Every one of them is the tool doing its job on logic you failed to mark as special. Which brings us to the marking.
ASYNC_REG: what the attribute actually does
Vivado's answer is the ASYNC_REG property, which part 1 introduced along with its placement rationale. The full story is that it does three distinct things, and all three matter:
- It tells synthesis the truth: this register can receive an asynchronous signal. Synthesis responds by refusing to optimize the register away, refusing to extract it into an SRL, and refusing to retime across it. The register chain survives to the netlist exactly as written; the attribute behaves like a targeted
DONT_TOUCHthat follows the flops through every optimization pass. - It instructs the placer to keep the flip-flops of the chain physically together, in the same slice when possible. Every picosecond of routing delay between stage one and stage two is a picosecond subtracted from the metastability resolution time , and part 1's MTBF formula puts in an exponent: . Placement is not a nicety; it is where the actual MTBF comes from.
The attribute belongs in the RTL, not in a constraints file, for the same reason a function's type signature belongs next to the function. It travels with the module into every project that instantiates it:
(* ASYNC_REG = "TRUE" *) reg meta, sync;You can apply it from XDC with set_property ASYNC_REG TRUE [get_cells {meta_reg sync_reg}], and sometimes you must (third-party netlists you can't edit), but XDC-applied attributes rot: someone renames a register, the wildcard stops matching, and nothing complains.
On the Intel/Quartus side the machinery is different but the intent is identical, with one trap hiding in the defaults. Quartus's Synchronizer Identification setting defaults to AUTO, which sounds like the tool has you covered. It does not: AUTO only produces a list of likely synchronizers. To get the actual protections, you force identification per register with an altera_attribute (part 1 showed the syntax with FORCED; FORCED_IF_ASYNCHRONOUS is the flavor Intel recommends, since it backs off gracefully on registers that turn out to have synchronous sources), and Intel's guidance is to do this for every synchronizer in the design. Forced identification is what makes the Fitter exempt the chain from register duplication and retiming and optimize its placement for settling time (with Optimize Design for Metastability enabled, its default), and it is what makes the Timing Analyzer compute and report the MTBF of the chain in its metastability report, turning part 1's back-of-envelope math into a signoff number. A chain the report lists without an MTBF attached is the tool telling you it was never actually protected. Check the report, not your intentions.
Constraints at the crossing: exceptions are part of the design
Now the timing analyzer. STA's default worldview is that every clock is related to every other clock, and every register-to-register path must close timing under some computable phase relationship. For a genuinely asynchronous crossing this is nonsense: there is no phase relationship, which means the default analysis is meaningless, which means you must tell the analyzer something. Every option you have is a form of telling it what you meant, and each one un-checks something different. Choosing which checks to keep is design work.
The blunt instrument is declaring the clocks mutually asynchronous:
# Cuts EVERY setup and hold check between the two domains,# in both directions, forever.set_clock_groups -asynchronous -group [get_clocks clk_a] -group [get_clocks clk_b]This is honest (the clocks really are unrelated) and dangerously total. With the groups declared, the analyzer stops checking all paths between the domains: the ones you protected with synchronizers, and also the ones you forgot about. Worse, in Vivado's exception precedence, set_clock_groups outranks set_max_delay. If you group the clocks and then carefully add a max-delay bound on a crossing bus, the max-delay constraint is silently ignored. The routing on that bus is now completely unconstrained: the router can legally send one bit on a scenic tour of the die, and no report will ever object.
The surgical instruments keep more checks alive:
set_false_path scoped to a single synchronizer input cuts only the path that genuinely has no timing requirement:
# The D input of a synchronizer's first flop has no meaningful# setup/hold requirement. Cut that path, and only that path.set_false_path -to [get_cells u_sync/meta_reg]set_max_delay -datapath_only is the better default for anything wider than a bit. It removes the clock-phase term from the analysis (so the check passes regardless of how the unrelated clocks drift) but still bounds the total routing-plus-logic delay of the crossing:
# Handshake-held data bus: don't ignore the crossing, bound it.# One destination clock period is the usual budget. The braces around# the bus patterns are load-bearing: unbraced [*] is Tcl command# substitution, and the constraint dies before it ever matches a cell.set_max_delay -datapath_only \ -from [get_cells {u_cdc/data_src_held_reg[*]}] \ -to [get_cells {u_cdc/data_dst_reg[*]}] 4.0Why bound a path whose timing "doesn't matter"? Because staleness and skew still matter. A handshake gives the data bus multiple destination cycles of settling time, but only if the bus actually arrives within a cycle or two; an unconstrained route through a congested region can eat the entire margin the protocol thought it had. The bound is how the margin becomes enforceable.
The Gray-bus skew constraint is the one most designs are missing, and it deserves its own explanation, because it protects the exact property that makes Gray code work.
Part 1's argument for Gray-coded counters was: only one bit changes per increment, so the destination samples either the old value or the new one, never a Frankenstein mix. Quietly load-bearing in that argument is an assumption about routing: that a bit launched by increment arrives at the destination flops before the bit launched by increment . Each bit of the bus takes its own physical route with its own delay. If the slowest bit's route is more than one source clock period longer than the fastest bit's, the destination can observe increment 's change before increment 's change has landed. Two bits are now in flight at once, the one-bit-difference guarantee is void, and the destination can sample a value that was never on the counter. The RTL is a textbook Gray crossing; the layout isn't one.
Vivado's set_bus_skew exists precisely for this. It constrains the spread of arrival times across the bits of a crossing bus, and, critically, it is checked independently of clock groups and false paths, so it keeps working even in a design that used the blunt instrument everywhere:
# Keep the fastest and slowest bit of the Gray pointer within the# smaller of the two clock periods, preserving "one bit in flight."set_bus_skew \ -from [get_cells {u_fifo/wr_ptr_gray_reg[*]}] \ -to [get_cells {u_fifo/wr_ptr_gray_sync1_reg[*]}] 3.2Set the budget to the smaller of the two clock periods and you are safe from both directions of the argument.
On Quartus, the same intents are spelled set_net_delay -max (bound a crossing's routing delay) and set_max_skew (bound the spread across a bus), and in current Quartus Prime Pro the precedence story is friendlier than Vivado's: both are still analyzed on paths that set_clock_groups -asynchronous has cut, and Intel's documented CDC recipe is precisely the combination, clock groups to kill the meaningless phase analysis plus net-delay and max-skew bounds that survive the grouping. The exceptions that do disable the skew check are set_clock_groups -exclusive in current Pro, and set_false_path on Standard edition and on Pro releases before 21.3, where a false path on a crossing bus silently stops the skew analysis (Intel's own warning, not folklore). Different vendor, same moral: know which exception disables which check, in your edition and your version, before reaching for the big one.
# Quartus SDC: same intent, different verbs.set_net_delay -max 4.0 \ -from [get_registers *data_src_held*] \ -to [get_registers *data_dst*]set_max_skew \ -from [get_registers *wr_ptr_gray*] \ -to [get_registers *wr_ptr_gray_sync1*] 3.2A summary you can pin up:
Crossing | Structure (part 1) | Constraint (this article) |
|---|---|---|
Single-bit level | Two/three-flop synchronizer | |
Gray-coded counter | Gray encode, sync, decode | |
Handshake data bus | Req/ack, held data | |
Async FIFO pointers | Cummings FIFO | Same as Gray counter, on both pointer buses |
Reset release | Async-assert/sync-deassert | |
And the discipline that makes any of it durable: constraints are code. They live in version control next to the RTL they protect, they are scoped to the module rather than dumped in a top-level file (Vivado's scoped XDC mechanism exists for exactly this), and they go through the same review as the RTL. Your XDC is the only artifact in the whole project that records your beliefs about which clocks are related and which paths are exceptions. The RTL cannot express any of that. If the constraints are wrong, the design is wrong, in a way no simulation will ever show you.
One rot mechanism deserves its own paragraph, because it is the single most common way to end up with a constraint you thought you wrote and didn't: an exception whose -from or -to pattern matches nothing does not fail the build. It downgrades to a critical warning and silently ceases to exist, which is what happens the day someone renames a register your XDC referred to by name. The false path you wrote in March is gone by June, every report is green, and nothing anywhere says so louder than a line in a log nobody reads. Treat unmatched-pattern critical warnings as build failures; they are the timing-exception version of the attribute rot described earlier.
Reading the CDC report like you mean it
Everything so far is you telling the tools your intent. The CDC report is the tools telling you what they actually see in the netlist, and it is the closest thing this problem domain has to a truth serum.
Vivado's report_cdc performs structural analysis on the implemented design: it identifies paths that launch in one clock domain and capture in another, classifies the crossing topology it finds there, and grades it. It knows what a two-flop synchronizer looks like, what a Gray-coded bus feeding synchronizers looks like, what an async reset release looks like; when the structure at a crossing matches nothing in its catalog, that is a Critical finding.
One precondition before you trust it: the report only analyzes paths whose source and destination clocks are defined. A crossing from a clock nobody ever wrote a create_clock for is invisible to it, and to report_clock_interaction too. Run check_timing first and chase its no_clock and unconstrained-endpoint complaints, or your truth serum has a blind spot exactly where the design is least understood.
Here is a representative sample of what report_cdc flags (the full catalog is in the Vivado timing user guides):
Check | Severity | What it means |
|---|---|---|
CDC-1 | Critical | Single-bit crossing with no recognizable synchronizer at the destination |
CDC-2 | Warning | Synchronizer found, but the chain is missing |
CDC-4 | Critical | Multi-bit bus crossing with no recognized safe structure |
CDC-7 | Critical | Asynchronous reset crossing with no reset synchronizer |
CDC-10 | Critical | Combinational logic between the source register and the synchronizer |
CDC-11 | Critical | One source register synchronized in multiple places in the destination domain |
CDC-13 | Critical | Asynchronous signal sampled by something that isn't a flip-flop (SRL, RAM, DSP register) |
Look at that table again with part 1 in mind. CDC-13 is the shift-register extraction failure from earlier in this article. CDC-11 is the "one signal, one synchronizer" divergence rule. CDC-10 enforces two of part 1's rules at once (no combinational logic between the flops, and a registered source in front of them): a LUT in front of the first flop can glitch on input transitions, handing the synchronizer a signal that wiggles inside the sampling window far more often than the source register alone would. The report is not an auxiliary linter; it is the part 1 checklist, executed against the netlist instead of against your memory of the RTL.
Its sibling report_clock_interaction answers the prior question: for every pair of clocks with paths between them, is the pair constrained, and how? The matrix it prints has one entry you should treat as an alarm: "Timed (unsafe)", meaning paths exist between two clocks whose relationship makes the timing analysis meaningless, and no exception or group declaration says you know about it. A clean design has no unexpected entries in that matrix. An unexpected entry is either a crossing you didn't know you had, or a constraint you thought you wrote and didn't.
Quartus's equivalents live in the Timing Analyzer's clock-transfer reports, the Design Assistant's CDC rule set, and the metastability report mentioned earlier, which attaches MTBF numbers to each chain whose identification you forced (an AUTO-only chain appears as a nameless suspect, no MTBF, no protections). The serious-money tools (Questa CDC, SpyGlass CDC, part 1 covered the category) do deeper structural and protocol analysis and produce correspondingly deeper piles of findings.
Which brings up the failure mode of all such tools: the pile. A CDC report on a real SoC-scale design produces hundreds of findings, most of them duplicates of a handful of root causes, some of them genuinely benign. The response that kills people is the blanket waiver, applied at week one to make the report green, never revisited. Vivado formalizes waivers with create_waiver, which is a good mechanism with a sharp edge: a waiver is a permanent instruction to stop looking. The discipline that keeps waivers honest is the same as for constraints: every waiver carries a written justification and an owner, waiver files live in version control and get reviewed, and the diff of the waiver file is part of every design review. A CDC sign-off that says "zero Criticals, forty waivers, all reviewed" is trustworthy. One that says "zero Criticals" and hides the waiver file is not a sign-off; it is a bug report scheduled for delivery in the field.
The pragmatic default: use the vendor's CDC primitives
After two articles of mechanism, here is the advice that shortcuts most of it: for the common crossing shapes, instantiate the vendor's pre-verified CDC blocks instead of hand-rolling. On AMD/Xilinx these are the XPM_CDC macros: xpm_cdc_single, xpm_cdc_array_single, xpm_cdc_gray, xpm_cdc_pulse, xpm_cdc_handshake, xpm_cdc_async_rst, xpm_cdc_sync_rst. On Intel, the equivalent roles are filled by the altera_std_synchronizer library cells (which arrive pre-identified for the metastability analysis discussed earlier), the Platform Designer reset controller for per-domain async-assert/sync-deassert resets, and dcfifo for stream crossings.
// A Gray-coded counter crossing, with the attributes, constraints,// and structure already handled. WIDTH and stages are parameters.xpm_cdc_gray #( .DEST_SYNC_FF (3), .WIDTH (8)) u_count_cdc ( .src_clk (clk_a), .src_in_bin (count_a_bin), .dest_clk (clk_b), .dest_out_bin (count_b_bin));What you are buying is not the RTL, which part 1 showed you how to write. You are buying everything this article is about: the ASYNC_REG attributes already applied, the scoped timing constraints already attached and maintained by the vendor, and, not least, report_cdc recognizing the structure on sight instead of flagging it for manual review. The macros are also honest about their preconditions in a way hand-rolled code rarely is. xpm_cdc_gray, for instance, is only safe if the binary input changes by at most one per source clock cycle, which is the actual mathematical precondition for Gray-code CDC; it says so in the datasheet, and in my experience almost nobody who hand-rolls a Gray crossing writes that assumption down.
Hand-roll when you need something the catalog doesn't have. When you do, you own the attribute, the constraints, and the report triage. That's the deal.
Reset sequencing after PLL lock: the bug that waits for winter
Part 1 established the reset rule: async assertion, synchronous deassertion, one synchronizer per domain. All of that assumed something nobody stated: that the clock feeding the reset synchronizer is real. At power-up, it isn't.
Walk the actual boot sequence of an FPGA. Configuration completes and the fabric wakes up. Your MMCMs and PLLs begin acquiring lock, which takes real time (tens to hundreds of microseconds, depending on configuration and conditions). During acquisition, their output clocks are not merely "not yet at frequency"; they can be glitchy, wrong-frequency, or intermittently silent. Any logic clocked from those outputs during that window, including your beautiful reset synchronizer, is being clocked by garbage. A reset synchronizer that shifts its hardwired 1 through on garbage edges will happily "release" its domain while the clock is still slewing toward lock. Whatever state your logic latches during that window is the state it boots with.
Every PLL and MMCM exports the fix as a pin: locked. The rule is that a clock domain downstream of a PLL stays in reset until the PLL says the clock is real, and only then does the synchronized release begin:
// Hold the domain in reset until the board reset is released AND the// PLL that generates this domain's clock reports lock. Assertion is// asynchronous (works with no clock at all); deassertion is// synchronized to the now-stable clock.module reset_after_lock ( input wire clk, // MMCM/PLL output clock for this domain input wire ext_rst_n, // board/system reset, async, active-low input wire pll_locked, // LOCKED from the MMCM/PLL output wire rst_n // async-assert, sync-deassert domain reset); wire raw_rst_n = ext_rst_n & pll_locked; (* ASYNC_REG = "TRUE" *) reg rst_ff1, rst_ff2; always @(posedge clk or negedge raw_rst_n) begin if (!raw_rst_n) begin rst_ff1 <= 1'b0; rst_ff2 <= 1'b0; end else begin rst_ff1 <= 1'b1; rst_ff2 <= rst_ff1; end end assign rst_n = rst_ff2;endmoduleNotice what the structure gets you without extra logic. pll_locked is itself an asynchronous signal, but it doesn't need its own synchronizer here, because it only ever asserts reset asynchronously (which is safe by design) and its deassertion is laundered through the two-flop chain like any other release. The release itself is safe for the same reason the plain reset synchronizer is: at the instant raw_rst_n lets go, the hardwired 1 has not yet shifted through, so rst_ff2's D input is still a settled 0 and there is nothing metastable for it to sample. And because locked participates in the raw reset, a loss of lock at hour nine hundred (input reference glitches, upstream oscillator brownout) yanks the domain back into reset immediately instead of letting it free-run on a dying clock. Whether re-reset is your desired lock-loss policy is a system question, but it is the right default, and you should decide it on purpose rather than inherit whatever your reset tree happens to do.
Two footnotes for the netlist-minded, since this article's whole thesis is that the netlist is the design. First, the AND gate means a LUT now drives asynchronous clear pins, which Vivado's methodology checker flags (LUTAR-1); here that is expected, understood, and waivable, which is exactly what a disciplined waiver file is for. Second, if ext_rst_n and pll_locked ever transition near-simultaneously in opposite directions, that LUT can emit a runt low pulse. The glitch is in the safe direction (assert), but a runt can violate minimum reset pulse width, so think about whether your reset inputs can actually race before dismissing it.
Three generalizations complete the picture:
- Clock topologies are dependency graphs. If PLL B is clocked from an output of PLL A, then B's
lockedis only meaningful after A's, and the domains hanging off B must wait for the AND of both. Draw the graph once, and generate each domain's reset from the locked signals along its ancestry. A reset-sequencing block at the top level, boring and explicit, beats clever distributed cleverness every time. - Order of release across domains is part of the sequence. Part 1 mentioned releasing configuration registers before datapaths within one domain. The same applies across domains: the domain that produces into a CDC boundary and the domain that consumes from it generally shouldn't come out of reset in arbitrary order relative to each other, especially around async FIFOs, whose pointer logic on both sides must be out of reset and sane before either side trusts full/empty. Vendor FIFO datasheets specify exactly this cross-domain reset choreography; read the fine print once and encode it in the sequencer.
- The device itself is one more lock to wait for. Configuration completing is a dependency of the same shape as PLL lock, one level further down. Intel formalizes this on its newer families: the Reset Release IP exports an
nINIT_DONEsignal, and Stratix 10 and Agilex designs are required to hold logic in reset until the device has fully entered user mode. Same pattern, same reasoning, one more input to the AND gate at the top of your reset tree.
Why the bench passes and the field still finds it
It is worth being precise about why this whole class of bug survives testing, because the reason dictates the defense.
Your bench is one board, one build of the bitstream, one speed grade, room temperature, and hours of runtime. Every failure in this article is gated on getting outside that envelope:
- The SRL-packed synchronizer and the missing
ASYNC_REGare statistical failures. Their degraded MTBF might still be weeks. Weeks times one bench unit is "never"; weeks divided by ten thousand fielded units is a failure somewhere in the fleet every few minutes. - The unconstrained Gray bus and the ignored max-delay are build-dependent failures. This bitstream routes the bus tightly and works. Next quarter's build, with an unrelated change three modules away, shifts congestion, the router takes the scenic route on bit 5, and the FIFO starts corrupting under load in a release whose diff doesn't touch the FIFO. Nothing is spookier to a team than a bug that arrives in a build that "didn't change anything," and nothing is more mundane once you know the mechanism.
- The reset-versus-lock race is an environmental failure, keyed to temperature and supply ramp, which the bench holds constant and the field sweeps daily.
Simulation cannot see any of it: these failures do not exist in the RTL. Bench testing barely samples any of it: population, build diversity, and environment are exactly what a bench doesn't have. The defenses that work are static and structural, and they are cheap compared to one field failure: attributes in the RTL, constraints reviewed as code, CDC and clock-interaction reports as hard sign-off gates with a disciplined waiver file, vendor primitives for the common shapes, and one deliberate look at the implemented netlist (open the schematic viewer, find your synchronizer, confirm it is two flip-flops in one slice and not an SRL) the first time each CDC module goes through a new tool version.
The build-side checklist
Tape this next to part 1's:
- Every hand-rolled synchronizer chain carries
ASYNC_REG(or the vendor equivalent) in the RTL itself, not only in a constraints file. - No SRLs in synchronizer positions. Verified in the implemented netlist, not asserted from the RTL.
- No blanket cross-domain false paths. Each crossing gets the narrowest exception that expresses its actual requirement.
- Every multi-bit crossing bus has a delay bound (
set_max_delay -datapath_only/set_net_delay) sized to its protocol's assumption. - Every Gray-coded bus has a skew bound (
set_bus_skew/set_max_skew) no larger than the smaller clock period. - Clock groups are declared once, deliberately, with the understanding that they override max-delay constraints between those clocks.
report_cdc(or equivalent) is a sign-off gate: zero unwaived Criticals, every waiver justified in writing, waiver file under review.report_clock_interactionshows no unexpected pairs, nothing "Timed (unsafe)," andcheck_timingreports nono_clockendpoints (a crossing from an undeclared clock is invisible to every report on this list).- Every clock domain's reset waits for its PLL ancestry to lock, release order across domains is explicit, and the lock-loss policy is a decision, not an accident.
- Constraints live with the module and go through code review. If the XDC diff isn't in the pull request, the design isn't in the pull request.
Taking it away
Part 1's claim was that CDC is governed by a small set of learnable patterns. This article's claim is the necessary complement: the patterns are specifications, and specifications need enforcement.
- Synthesis and place-and-route preserve function, and a synchronizer's value is not functional. Unmarked, it is legal prey for SRL extraction, replication, retiming, and merging.
ASYNC_REGand its cousins are how the RTL tells the whole toolchain "this structure is load-bearing, hands off, keep it tight."- Timing exceptions are not bookkeeping; they are the design's statement of which paths matter and how much. Each vendor has an exception that silently disables another (in Vivado,
set_clock_groupsoutranksset_max_delay; in Quartus, exclusive clock groups, and on older editions false paths, kill the skew check), and the Gray-code guarantee is void without a skew bound to enforce it. - The CDC report is the part 1 checklist run against the netlist. Read it, gate on it, and treat the waiver file as code.
- A reset synchronizer clocked by an unlocked PLL is theater. Sequence resets against
locked, through the clocking dependency graph, with a deliberate lock-loss policy. - The bench cannot catch these. Population, build churn, and environment can, and the field supplies all three for free.
The tools are not your adversary. They are scrupulously, mechanically obedient, which is worse: they did exactly what you told them, and the constraints file is the transcript of what you told them. The skill this article asks you to build is making that transcript say everything you meant.
Further reading
- AMD/Xilinx, UG903: Vivado Design Suite User Guide, Using Constraints. The authoritative reference for
set_clock_groups,set_false_path,set_max_delay -datapath_only,set_bus_skew, and, crucially, the precedence rules among them that this article leans on. - AMD/Xilinx, UG906: Vivado Design Suite User Guide, Design Analysis and Closure Techniques. Documents
report_cdcand its full rule catalog,report_clock_interaction, and thecreate_waivermechanism. - AMD/Xilinx, UG949: UltraFast Design Methodology Guide for FPGAs and SoCs. The methodology-level treatment of
ASYNC_REG, synchronizer placement, and CDC sign-off; also part 1's vendor reference. - AMD/Xilinx, UG974: UltraScale Architecture Libraries Guide. The XPM_CDC macro reference: parameters, ports, preconditions, and the embedded constraints each macro carries.
- Intel, SYNCHRONIZER_IDENTIFICATION Assignment (Quartus Prime Settings File Reference). The Quartus counterpart to
ASYNC_REG, and the hook that feeds Quartus's per-chain MTBF reporting. - Chapman, WP272: Get Smart About Reset: Think Local, Not Global (Xilinx white paper). The classic argument for local, synchronized resets in FPGA fabric; the intellectual ancestor of this article's reset-sequencing section.
- Cummings, Mills & Golson (2003), Asynchronous and Synchronous Reset Design Techniques, Part Deux (SNUG Boston). The standard paper on reset style, reset synchronizers, and reset distribution, including the async-assert/sync-deassert structure used here.