I²C · Module 19
Clock Domains and Timing Constraints for an I²C Block
SCL toggles, which makes it look like a clock, and in this architecture it provably is not one. Determines that from the RTL rather than by assertion, then writes constraints that state what is genuinely unconstrained, what must still be timed, and why a blanket false path is the most expensive line you can add.
Five chapters have built a front end: a pad, a resistor, a synchronizer and a filter. None of them has said anything to the tool.
That conversation has one question at its centre, and getting it wrong shapes everything downstream: is SCL a clock?
1. Answer It From the Design, Not From the Name
SCL is called a clock. It toggles. It has a duty cycle and a frequency. Every instinct says to write create_clock on it.
For the architecture Modules 17 and 18 built, the answer is determined mechanically:
edge-sensitive signals across all 12 RTL files: ['clk']Every clocked process in i2c_slave_sync, i2c_slave_framing, i2c_slave_addr, i2c_slave_ack, i2c_slave_rx, i2c_slave_tx, i2c_slave_mack, i2c_slave_regs, i2c_slave_txn, i2c_slave, plus this module's i2c_line_sync and i2c_glitch_filter, is sensitive to clk and to the asynchronous de-assertion of rst_n. scl appears in no sensitivity list anywhere in the design.
So in this architecture SCL is sampled data. Not by preference — by construction, provably, and the check that proves it is run in Section 5 rather than asserted here.
2. What Is Actually in This Design
┌─────────────────────────── clk domain (the only one) ───────────────────────┐
│ │
│ 19.4 synchroniser ──▶ 19.5 filter ──▶ 18.3 framing ──▶ … ──▶ 18.9 regs │
│ ▲ │
└────────┼────────────────────────────────────────────────────────────────────┘
│
scl_pin, sda_pin ← asynchronous inputs. No reference edge exists.Three categories, and every constraint decision follows from which category a path is in:
| category | what it is here | what the tool should do |
|---|---|---|
| primary clock | clk | time everything in it |
| asynchronous input | scl_pin, sda_pin | do not time the crossing — there is no reference edge |
| synchronizer-internal path | FF1 → FF2 inside 19.4 | time it, strictly — see Section 4 |
There are no generated clocks in this design. Nothing divides clk; the bus rate is not derived from it in the target. (A controller does divide clk to make SCL — Module 17's timing generator — and that output is a clk-domain register, not a generated clock, for exactly the same reason.)
3. Why the Bus Inputs Cannot Be Given a Number
The reflex for an input is set_input_delay. It does not apply here, and the reason is worth being precise about.
set_input_delay says: this data arrives a known time after a known edge of a known clock. Each of those three is absent:
- there is no clock the arrival is referenced to — the controller's clock is not in your design;
- there is no known time — the controller's SCL period is whatever it is, within a wide legal range;
- and the arrival instant is not even a digital event. Chapter 19.3 established that the rising edge is an RC curve whose threshold crossing is set by a resistor and a capacitance.
A number written here would be a fiction, and worse, a fiction the tool would believe and report timing against.
4. The One Path That Must Not Be Excluded
This is the most important paragraph in the chapter.
The path from the synchronizer's first flop to its second is inside the clk domain, is fully timed, and must meet setup. It is not a clock-domain crossing. The crossing already happened, at the first flop's input.
Which is why the constraint file below false-paths from the ports and nothing else. A set_false_path drawn from the pin all the way to the second flop, or between a real domain and an imaginary SCL domain, will swallow this path — and the design will close timing, pass every simulation, and be less reliable than the report says.
Some flows want the chain identified so a CDC report can classify it, and that is a reporting aid, not a timing exclusion. The distinction is the whole of good CDC constraint practice: tell the tool what the structure is; never tell it to stop looking.
5. The Constraints
# -----------------------------------------------------------------------------
# i2c_slave.xdc
# Timing intent for the Module 18 target on an FPGA.
#
# DIALECT: Xilinx/AMD XDC syntax (Tcl-based, `create_clock` / `set_false_path`).
# Intel Quartus SDC and Lattice equivalents express the same intent with different
# command names; the INTENT is what transfers, not the syntax. Every constraint
# below is labelled with what it asserts, because a constraint whose purpose is not
# written down is a constraint the next engineer will delete.
#
# WHAT THIS FILE IS NOT: evidence of timing closure. No timing report exists for it
# and no vendor tool has read it. It has been checked STATICALLY -- every port it
# names exists, every clock it references is declared, and it contains no blanket
# false path -- and that is the whole of the claim.
# -----------------------------------------------------------------------------
# ---- 1. the only real clock ------------------------------------------------
# Derived from the design, not assumed: every clocked process in all ten Module 18
# RTL files is sensitive to `clk` alone. SCL appears in no sensitivity list, so it
# is sampled DATA and must not be declared as a clock here. See section 2.
create_clock -name clk -period 20.000 [get_ports clk]
# ---- 2. the two asynchronous bus inputs ------------------------------------
# SCL and SDA arrive from another device, and their threshold crossing is set by an
# RC curve (Chapter 19.3), so no setup/hold relationship to `clk` exists or can be
# met. `set_input_delay` with a number would be a fiction: there is no reference
# edge to measure the number against.
#
# WHAT IS ASSERTED: these paths are genuinely unconstrained-by-nature, and the
# containment is structural -- the synchroniser of Chapter 19.4. The tool is told
# not to time the crossing, and is NOT told to stop timing anything else.
set_false_path -from [get_ports scl_pin]
set_false_path -from [get_ports sda_pin]
# ---- 3. what is deliberately NOT false-pathed ------------------------------
# The path from the first synchroniser flop to the second is a REAL synchronous
# path inside the clk domain, and the tool must time it. If it does not meet setup,
# the second flop samples the first before it has settled and the synchroniser
# stops working -- which is the one path in this design where a timing violation
# silently removes a safety mechanism rather than producing a wrong value.
#
# Some flows additionally mark the chain so a CDC report can recognise it. That is
# a report-quality aid, not a timing exclusion, and it must never be written as a
# false path between the two flops.
# ---- 4. the open-drain outputs ---------------------------------------------
# These are driven from clk-domain registers and their consumer is a bus whose
# timing budget is measured in hundreds of nanoseconds (Chapter 11.9), so the pin
# timing is not tight. The constraint exists so that the tool reports SOMETHING
# rather than leaving the output paths unanalysed.
set_output_delay -clock clk 2.000 [get_ports scl_drive_low]
set_output_delay -clock clk 2.000 [get_ports sda_drive_low]
# ---- 5. reset ---------------------------------------------------------------
# `rst_n` is asynchronous de-assertion into every block. Its release is an event the
# whole design sees at once, which is why Chapters 19.4 and 19.5 both care about
# what their registers reset TO.
set_false_path -from [get_ports rst_n]create_clock -period 20.000 is 50 MHz, matching the figures used in 19.4 and 19.5. The set_output_delay values on the drive-low outputs are deliberately loose: the consumer is a bus whose budget is hundreds of nanoseconds (Chapter 11.9), so the purpose of those two lines is to make the tool report on the output paths rather than leave them unanalysed.
6. Dialects
The file above is XDC — AMD/Xilinx, Tcl-based. The same intent in Intel Quartus SDC or Lattice's flow uses different command names and sometimes different argument shapes.
7. What the Static Check Found — About Itself
The six checks in Section 5 were each tested by breaking the thing they check. Four fired immediately. Two did not, and both were holes in the checker rather than in the constraints.
| probe | first attempt | after repair |
|---|---|---|
blanket false path with no -from/-to | caught | caught |
false path to all_registers | caught | caught |
| a port that does not exist in the RTL | caught | caught |
| a bus line used as a clock in the RTL | caught | caught |
| reference to an undeclared clock | missed | caught |
| SCL declared as a clock | missed | caught |
The undeclared-clock check only recognised the get_clocks {name} form and not the -clock name form, which is the form set_output_delay actually uses. The SCL-as-a-clock check did not exist at all — the checker recorded which clocks were declared without ever asking whether declaring them was legitimate, which meant the check most central to this chapter's argument was absent from the tool that claimed to verify it.
8. ASIC Contrast, Briefly
The intent is identical; the machinery around it is heavier. An ASIC flow expects a structured CDC methodology — a dedicated CDC tool, not just STA — that classifies every crossing, requires each synchronizer to be recognised as one, and reports crossings that have none. It will also want the synchronizer's internal path handled explicitly so a later optimisation pass cannot retime or merge the two flops, which would remove the settling time while leaving the schematic looking correct.
Two differences are worth carrying back to FPGA work:
The tool is expected to enumerate crossings, not to be told about them. An FPGA flow lets you get away with a set_false_path and no further thought; an ASIC flow asks you to justify each one.
Retiming is a real hazard. "Do not let the optimiser touch these two flops" is an explicit instruction in an ASIC flow, and it is worth checking whether your FPGA synthesis has an equivalent — because a synchronizer whose two flops were merged is not a synchronizer.
9. Focused Verification Insight
Constraints are not verified by simulation, and this is the gap. RTL simulation runs with zero delay and will pass whatever the constraints say or do not say. The evidence types are different in kind: simulation verifies behaviour, a timing report verifies implementation, and a CDC report verifies structure. A design with all three is covered; a design with only the first has verified the part that was never in doubt.
What Module 20's environment can legitimately do is check the consequences of constraint intent that are visible in RTL: that no register is clocked by a bus line, that no logic reads the first flop of a synchronizer chain, that both lines use the same filter threshold. Those are structural facts checkable by a script — as Section 5 does — and they are worth automating because they are exactly the facts a later edit breaks.
Coverage does not apply to constraints, and claiming it does is a category error. What applies is a checklist with an owner: which paths are timed, which are excluded, and a one-line justification for each exclusion, reviewed when the clock frequency or the device changes.
10. Misconceptions
11. Debugging
The design closed timing and the field failure rate went up
Pitfall — a false path wide enough to swallow the synchroniser's internal path
# An I2C target that had been closing timing comfortably is moved to a faster part
# and the clock raised. A handful of paths now fail setup, among them the two flops
# of the SDA synchroniser. The engineer reasons:
#
# "These are the synchroniser flops. Synchroniser paths are clock-domain
# crossings, and CDC paths are false paths. This is a constraint bug, not a
# timing bug."
#
# Every clause of that sounds right and the conclusion is wrong. The fix applied:
#
# set_false_path -from [get_ports sda_pin] -to [all_registers]
# set_false_path -from [get_ports scl_pin] -to [all_registers]
#
# Timing now closes with margin. The report is clean. Nothing in simulation changes,
# because RTL simulation has no timing at all.
#
# What the constraint actually says is: "do not time ANY path that begins at a bus
# pin." The synchroniser's FF1 -> FF2 path begins, transitively, at a bus pin -- so
# it is excluded. So is every path from the filter, the framing detector, the address
# comparator and the register file, because all of them are downstream of a bus pin.
#
# The two lines removed most of the design from timing analysis.Timing closes, with better margin than the previous build.
Simulation is unchanged -- all thirty tri-HDL combinations of Module 18 still pass, because none of them models timing.
In the field, on the new hardware: intermittent corrupted bytes, roughly one transfer in a few thousand, uncorrelated with temperature, bus load, which device is addressed, or anything else anyone could find. Retries always succeed. The rate is low enough that early production looked fine and rose with volume.
The ILA is the confusing part: internal state always looks self-consistent. The framing block reports a START, the address matches, bytes are received -- and occasionally one received byte differs from what the external analyser shows on the wire. There is no error, no NACK, no protocol violation. One bit in one byte is simply wrong, and the target's own view of it is internally coherent.
That signature -- correct protocol, occasional wrong data, internally consistent logic, no error condition -- is what a metastability failure looks like from inside. It is not a protocol bug and it will not be found by protocol analysis.
-to [all_registers] excludes every register in the design, including the second flop of each synchroniser chain. So the FF1 -> FF2 path -- an ordinary intra-clk path that must meet setup -- was no longer analysed, and on the faster clock it did not meet setup.
FF2 therefore sampled FF1 before FF1 had settled. That is exactly the condition the synchroniser exists to prevent, reintroduced inside the synchroniser. The MTBF did not go to zero; it dropped by orders of magnitude, which is why the failure is rare, random and untraceable rather than reproducible.
Three things made it hard:
- the constraint change and the symptom are separated by a layer nobody looks across: a constraints file edit produced a silicon reliability change, with no step in between that any simulation covers; - timing MET, so the timing report actively argued the design was fine. The report was honest about everything it was asked to check; - the remaining failure is statistical, so every attempt to reproduce it on the bench succeeded (in the sense of not failing) and looked like evidence.
The fix is to narrow the exclusion to what is genuinely unconstrained -- the arrival at the first flop -- and nothing beyond it:
set_false_path -from [get_ports sda_pin] set_false_path -from [get_ports scl_pin]
and then to deal with the ACTUAL timing failure on the FF1 -> FF2 path, which is a real violation with real fixes: less logic between the flops (there should be none), placement constraints keeping the pair together, or a slower clock in that region.
The general rule: A FALSE PATH MUST NAME BOTH ENDS AND BE AS NARROW AS THE FACT IT ASSERTS. "-to [all_registers]" is never that, because no true statement about timing applies to every register in a design. Section 5's checker rejects both the no-endpoint form and the all_registers form for this reason, and the rejection was tested by writing them deliberately.
12. Reason It Through
13. Questions
14. What This Chapter Settled
Whether SCL is a clock is a question about your RTL, and for this architecture the answer is determined mechanically: no bus line appears in any sensitivity list across twelve files, so SCL is sampled data and there is exactly one clock domain. The bus inputs are unconstrained by nature and are false-pathed from the ports as a statement of that fact. The synchronizer's internal path stays fully timed, because a violation there removes a safety mechanism without producing a visible symptom.
The file was checked statically — ports exist, clocks are declared, no blanket exclusions — with zero failures, and the checker itself was tested by breaking each thing it checks. Two of its six checks did not work on the first attempt, which is the more useful finding than the zero.
No timing report exists and none is claimed. That is the honest boundary of what this environment can establish, and it is also the point at which the module's subject changes: everything from here is about the difference between what the tools said and what the board does. Chapter 19.7 catalogues that difference.
Continue learning
Related tutorials
- Related topic
Asynchronous SDA/SCL Inputs — Metastability and Synchronization
SDA and SCL have no relationship to the FPGA's clock, so every sample of them lands somewhere in a flip-flop's aperture. Explains metastability as settling time rather than a propagating X, derives what each synchronizer stage buys and costs, and is explicit about the one claim RTL simulation can never support.
- Related topic
Where I²C Lives — Boards, SoCs and Real Devices
Place the derived bus in a real system: the host controller inside an SoC or FPGA, the regulators, sensors, memories and clock devices attached to it, and what each one is actually doing. The traffic turns out to have a specific shape — control plane, not data plane — and that shape is why the bus remains useful.
- Related topic
SCL Generation and the Bit Period
A legal I²C clock is not a frequency. LOW and HIGH are separately constrained phases, the controller pulls SCL low and releases it rather than driving it high, and a naive integer divider satisfies none of that. Build a parameterised phase generator in three languages and measure it.
- Related topic
FPGA as Master and as Target — Integration Patterns
Assembles everything: Module 18's verified target behind Module 19's pad, synchronizer and filter, proven end to end on a wired-AND bus in three languages. Shows the controller-side shape on Module 17's real interface, where a soft CPU attaches, and closes with two mutations that cannot be killed in simulation — the module's thesis stated as evidence.
