Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   ┌─────────────────────────── 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:

categorywhat it is herewhat the tool should do
primary clockclktime everything in it
asynchronous inputscl_pin, sda_pindo not time the crossing — there is no reference edge
synchronizer-internal pathFF1 → FF2 inside 19.4time 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

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_slave.xdc — timing intent, with every constraint's purpose stated
   # -----------------------------------------------------------------------------
   # 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.

probefirst attemptafter repair
blanket false path with no -from/-tocaughtcaught
false path to all_registerscaughtcaught
a port that does not exist in the RTLcaughtcaught
a bus line used as a clock in the RTLcaughtcaught
reference to an undeclared clockmissedcaught
SCL declared as a clockmissedcaught

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
Buggy Code
# 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.
Symptom

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.

Root Cause

-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