CDC Lint and the Mistakes It Catches
The last three articles built the CDC toolkit. This one is about the uncomfortable fact that none of it can be verified by simulation.
Run every CDC example from this section through Icarus Verilog and every one passes, including broken versions with the synchronizer stages optimised away, or with a data bus crossing raw with no synchronization at all. That is not a flaw in the testbenches. It is a structural limit of what RTL simulation is.
Why simulation cannot see this class of bug
An RTL simulator models signals as discrete values, 0, 1, X or Z, updated at discrete time steps. There is no notion of a voltage sitting between logic levels for a few picoseconds before resolving, because that is an analog, continuous-time phenomenon and RTL simulation is a digital, discrete-event abstraction. Metastability cannot occur in a Verilog simulator, not because your simulator is a bad one, but because no RTL simulator models the physics that produces it.
So every failure this section has warned about, a multi-bit value sampled inconsistently mid-transition, a synchronizer retimed away by synthesis, a bus crossed with no synchronizer, either works perfectly in simulation every time (the simulator always delivers a value that changed atomically) or fails deterministically for some unrelated logical reason. The dangerous CDC bugs are exactly the ones that are timing-dependent in the physical world and therefore timing-independent, and invisible, in an idealised digital simulation.
The tool that catches them: CDC lint
A CDC lint tool does not simulate the design. It performs static structural analysis on the RTL: it traces every signal path that crosses between two clock domains (as declared, usually, in a clock definition file that says which signals belong to which clock) and checks each path against a list of safe and unsafe patterns, independent of any stimulus.
The checklist every CDC lint tool is built around
Unsynchronized single-bit crossing. A signal generated in one domain, consumed directly by logic in another, with no synchronizer at all. The most basic check, and still the most commonly flagged issue in a first lint run on a design that was not built with CDC discipline from the start.
Combinational logic between synchronizer stages. The two-flop pattern requires the first flop’s output to feed the second flop’s input directly: no gates, no muxes, nothing in between. Even a single inverter between the stages can introduce a glitch that the second flop samples and locks in, defeating the structure. Lint checks this by tracing the netlist path between the two registers.
Multi-bit bus crossing without Gray code or a handshake. The failure from the start of Section 7.4. Lint flags any multi-bit signal crossing a domain that is not provably Gray-coded (adjacent values differ by one bit, checked structurally) or gated behind a recognised handshake.
Missing synchronizer attributes. The ASYNC_REG-style attribute from the two-flop article. Lint checks that every synchronizer pair is tagged, because an untagged pair is exactly what a downstream synthesis optimisation could break without warning.
Reconvergence. A subtle one worth knowing about. If the same source signal is synchronized independently by two different synchronizer instances, and both synchronized copies are later combined in destination logic (compared with each other, or fed into the same mux), the two copies can resolve on different destination edges relative to each other. That is the same kind of momentary inconsistency Gray code exists to prevent, except it happens after synchronization, somewhere you might not think to look. The fix is almost always to synchronize once and fan the single copy out.
A design passing all of its functional tests and a design passing CDC lint are two independent claims. A design can have perfect functional coverage and still ship a CDC bug that fails once in a thousand power-on cycles in the field, because that failure could never be exercised by a simulator. Equally, CDC lint is no substitute for functional verification: it can tell you a crossing is structurally unsafe, but nothing about whether your protocol logic is functionally correct. Real signoff needs both, run independently, because each checks something the other cannot see.
Practice Exercise
A colleague argues: “Our simulation has run a simulated year of continuous operation with random stimulus and found zero CDC failures, so we do not need CDC lint.” Explain, using what you know about how RTL simulation models signals, why the argument does not hold.
Solution
No amount of simulated time changes the underlying limitation. An RTL simulator resolves every transition atomically and instantly, with no intermediate metastable state and no notion of a flop being retimed by synthesis. A simulated year of random stimulus still only produces the clean, digital version of every transition. It can never produce the physical timing relationship a real CDC bug depends on: a source transition landing inside a destination flop’s setup/hold window, or a synthesis pass moving a synchronizer register. The argument mistakes “we have tested extensively” for “we have tested the failure mode CDC lint catches”, and those are different claims, because that failure mode is by construction outside what RTL simulation can exercise.
RTL simulation is an idealised digital abstraction that cannot model metastability, so every CDC bug in this section is invisible to it no matter how much stimulus you apply. CDC lint fills the gap with static analysis of every crossing against a checklist: unsynchronized single bits, logic between synchronizer stages, ungated multi-bit crossings, missing attributes, and reconvergence. That closes Section 7.5. In the next article, Section 7.6 turns to the last topic of the module, making all of this run within a power budget, starting with the one clock-gating technique that is actually safe.