Skip to content
VLSI Mentor

I²C · Module 5

START/STOP Timing and Malformed Framing

Three framing margins, each with two anchor events, all of them minimums: the hold after a START, the setup before a repeated START, and the setup before a STOP. Build a sequencer that generates all three and refuses an illegal configuration, then catalogue the malformed framing the margins exist to prevent.

Four chapters have now defined the framing events, built hardware to detect and classify them, and said — every single time an interval came up — that the interval has a specified minimum owned by a later chapter. This is where that stops.

Three margins govern framing. Each one has two anchor events, each one is a minimum, and each one exists to make a framing edge unambiguous rather than merely present. Chapter 5.2 showed that collapsing a framing interval to zero produces no framing event at all; this chapter is about the space between zero and legal, which is where the failures are much harder to see.

1. Why Framing Needs Margins At All

Start with the question rather than the parameters, because the parameters are unmemorable and the question is not.

A framing event is an SDA edge occurring while SCL is high. Chapter 5.2 built a detector for exactly that and it worked. So why is any interval specified? If the edge is in the right place, the condition is met — what is a margin for?

Three answers, and they are the three parameters.

A receiver has to notice, and noticing takes time. Chapter 5.1 quoted UM10204's note that a device without dedicated hardware must sample SDA at least twice per clock period to sense the transition. Any sampled observer — and every real observer is sampled, including the ones built in this module — needs the condition to persist long enough to fall inside its sampling. An edge that is instantly followed by the next bus transition can be legal in shape and invisible in practice.

An edge must be attributable to one phase. The whole detection mechanism rests on SCL being unambiguously high across the SDA transition. Chapter 5.2 §5 derived a guard requiring SCL high on both samples, precisely because an SDA edge coincident with an SCL edge cannot be attributed. A margin is what keeps the two edges far enough apart that the attribution is not a judgement call.

Two events that must be ordered need separation, not just sequence. "SDA falls, then SCL falls" is an ordering. On a real bus with real edge rates the two edges have finite width and arrive at different devices at slightly different times, so an ordering with no separation is an ordering that some observers will see the other way round.

All three reduce to the same engineering statement: a framing event is a claim made to every device on the bus, and the margin is how long the claim is held so that everyone can hear it.

2. tHD;STA — The Hold After a START

What is measured

The interval from the START (or repeated START) edge to the first falling edge of SCL.

Anchor events

event
startSDA's high-to-low transition, while SCL is high — the framing edge itself
endSCL's first falling edge after it

Constraint direction

Minimum. UM10204's table gives a minimum and no maximum. Holding longer is legal and costs only throughput.

Why it exists

Because the START must be observable before the bus starts doing anything else. Once SCL falls, the bus has entered the first bit period of a transfer — and a device that had not yet registered the START is now sampling data bits it thinks are something else. The specification's own wording for this row makes the causality explicit, describing the condition as: after this period, the first clock pulse is generated. The clock waits for the framing to land.

Who owns it

The controller, unambiguously and alone. Both anchor events are edges the controller produces, and no other device influences either. This is the cleanest ownership of any parameter in this module, and it is worth noting because most timing parameters are not like this — Chapter 4.3 made the point that ownership is a question to ask of every measurement arrow, and here the answer is uncomplicated.

What consumes the budget

Only the controller's own sequencing — the number of internal clock cycles it counts between asserting SDA low and asserting SCL low. There is no board contribution on the start anchor, because the controller pulls SDA low actively and a driven falling edge is fast. The end anchor is also a driven fall. This is the rare framing margin where the pull-up network is not part of the story.

What a violation looks like

Targets intermittently fail to respond to the address that follows. Not a corrupted address — no response at all, because the target never registered a START and therefore was not shifting in an address. On a capture the framing looks present and the address looks clean; only measuring the interval reveals it. §12 catalogues the shape.

RTL implication

A counted state between the SDA assertion and the SCL assertion. This is FS_S_EDGE in §9's sequencer, and Chapter 5.2's debugging case is what its absence costs.

DV implication

Measure from the observed SDA fall to the observed SCL fall — on the bus, not from the controller's internal state. A check written against the internal counter confirms the counter, which was never in doubt.

FPGA / ASIC implication

At a fast internal clock this is a wide counter: a 4.0 µs minimum at 100 MHz is 400 cycles. §8 works the conversion and §14 covers what the width means.

tHD;STA — from the START edge to the first SCL fall

10 cycles
Ten intervals. Both lines begin high. SDA falls while SCL is still high, which is the START edge. SCL remains high for several further intervals and only then falls, beginning the first clock pulse. The interval between the SDA fall and the SCL fall is marked as the hold time for the START condition.tHD;STA — minimumtHD;STA — minimumSTART edge — SDA fallsSTART edge — SDA fallsfirst SCL falling edgefirst SCL falling edgesclsda1000000000t0t1t2t3t4t5t6t7t8t9
Figure 1 — tHD;STA. The measurement runs from the START edge forward to SCL's first falling edge, and both anchors are edges the controller produces itself. One parameter per figure, with the measurement in its own band below the signals, so no two measurement intervals can ever overlap. Conceptual figure: interval widths are chosen for legibility and are not to specification scale.

3. tSU;STA — The Setup Before a Repeated START

What is measured

The interval from SCL's rising edge to the SDA falling edge that forms a repeated START.

Anchor events

event
startSCL's rising edge — the beginning of the high phase the framing edge will sit in
endSDA's high-to-low transition — the repeated START edge

Constraint direction

Minimum.

Why it exists — and the subtlety that matters

Note the row's wording in UM10204's table: set-up time for a repeated START condition. The qualifier is doing real work, and Chapter 5.4 §3 set up why.

A first START emerges from a free bus. Both conductors have been high for the whole bus-free interval, so SCL was not recently rising — there is no setup relationship to constrain, and what gates the START instead is the bus-free requirement.

A repeated START emerges from a busy bus. Chapter 5.4 §4 showed the forced sequence: SDA is released during SCL's low phase, then SCL is released, and then SDA falls. That third step happens shortly after SCL's rising edge, and "shortly" needs a floor — otherwise the SDA fall crowds the SCL rise and the two edges become mutually unattributable, which is exactly the ambiguity Chapter 5.2's two-sample guard refuses to resolve.

So this parameter exists for the repeated START because only the repeated START has an SCL rising edge immediately before its framing edge. Reading the row as though it governed every START is a mistake, and it leads to a controller that waits a setup interval before a first START — harmless — or, much worse, to one that satisfies this margin and believes it has therefore satisfied the bus-free requirement, which is Chapter 5.1's failure.

Who owns it

The controller produces both edges, so it owns the interval — but with a qualification the previous parameter did not have. The start anchor is SCL being released, and a released line does not go high instantly: the pull-up network and the bus capacitance determine when it actually crosses the receiver's threshold. The controller decides when to stop pulling; the board decides when the line arrives. So the interval the controller counts and the interval a receiver observes are not the same interval, and the difference is board-dependent.

This is the drive-intent versus observed-bus distinction from Chapter 4.1 §7, appearing as a budget item. The numbers are Module 11's; the structural point is that this margin is shared between the controller and the board while tHD;STA was not.

What consumes the budget

The controller's counted interval, minus whatever the rise time consumes. A controller that counts from its own release rather than from the line actually being high has already spent part of the margin before the interval it thinks it is measuring has begun.

What a violation looks like

Targets miss the repeated START specifically, while first STARTs work perfectly. The signature is a device that responds correctly to simple transfers and fails on combined write-then-read access — which looks like a device or driver problem and is a controller timing problem.

RTL implication

A counted state entered on SCL's release. FS_RS_SCL in §9. And a robust controller waits for SCL to be observed high rather than assuming its release took effect, for the reason in Chapter 4.1 §7.

DV implication

Measure from the observed SCL rise to the observed SDA fall. Because the start anchor is a released edge, this is the first of the three margins where a cycle-based check on the controller's internals can pass while the pin-level interval is short.

tSU;STA — from the SCL rise to the repeated-START edge

10 cycles
Ten intervals. SCL is low and SDA is low. SDA is released high while SCL is still low, which is ordinary preparation. SCL is then released and goes high. After several intervals SDA falls while SCL is high, forming the repeated START. The interval from the SCL rise to the SDA fall is marked as the set-up time for a repeated START.tSU;STA — minimumtSU;STA — minimumSDA released while SCL is lowSDA released while SCL islowSCL risesSCL risesSr edge — SDA fallsSr edge — SDA fallssclsda0111110000t0t1t2t3t4t5t6t7t8t9
Figure 2 — tSU;STA. SDA is first released during SCL's low phase, which is ordinary preparation and not framing. SCL is then released, and the measurement runs from SCL's rising edge forward to the SDA fall that forms the repeated START. The start anchor is a released edge, so the board's rise time is inside this budget in a way it was not inside tHD;STA. Conceptual figure, not to specification scale.

4. tSU;STO — The Setup Before a STOP

What is measured

The interval from SCL's rising edge to the SDA rising edge that forms the STOP.

Anchor events

event
startSCL's rising edge — the controller releasing the clock for the last time
endSDA's low-to-high transition — the STOP edge

Constraint direction

Minimum.

Why it exists

For the same attribution reason as tSU;STA, in the opposite direction. The STOP's SDA rise must be clearly inside SCL's high phase rather than crowding its beginning. And there is a second reason specific to this event: Chapter 5.3 §8 established that a STOP is what makes every target release SDA and return to idle, so the STOP is the last thing that happens in a transfer and there is nothing afterwards to disambiguate it. A crowded START is followed by a whole transfer that might reveal the problem; a crowded STOP is followed by silence.

Who owns it

The controller, with the same board qualification as tSU;STA — the start anchor is SCL being released. And now the end anchor is also a release: SDA rises because the controller stops pulling it down, not because anything drives it high. So both of this parameter's anchors are pull-up-determined edges, which makes it the most board-sensitive of the three.

What consumes the budget

The controller's counted interval, with rise time eating into it at the start anchor. The end anchor's rise time does not consume this margin — it delays the STOP, which pushes into the bus-free interval that follows.

What a violation looks like

The subtlest of the three, and the reason is §12's central point: a too-short STOP setup can result in the STOP being missed entirely, and a missed STOP does not look like a framing error. It looks like a bus that is still busy — a target still waiting for more bytes, a controller that thinks the transfer ended, and the next transfer beginning into devices that never reset.

RTL implication

A counted state entered on SCL's release, ending with SDA's release. FS_STO_SCL in §9.

DV implication

Both anchors are releases, so this is the margin where a pin-level real-time check is least substitutable by a cycle-based one.

tSU;STO — from the SCL rise to the STOP edge

10 cycles
Ten intervals. SCL and SDA both begin low at the end of a transfer. SCL is released and rises while SDA is still held low. After several intervals SDA is released and rises while SCL is high, forming the STOP condition. The interval from the SCL rise to the SDA rise is marked as the set-up time for the STOP condition.tSU;STO — minimumtSU;STO — minimumSCL rises — SDA still held lowSCL rises — SDA still heldlowP edge — SDA releasedP edge — SDA releasedsclsda0000001111t0t1t2t3t4t5t6t7t8t9
Figure 3 — tSU;STO. SCL is released while SDA is still held low, and the measurement runs from SCL's rising edge forward to the SDA release that forms the STOP. Both anchors of this parameter are pull-up-determined edges rather than driven ones, which makes it the most board-sensitive of the three framing margins. Conceptual figure, not to specification scale.

5. The Specified Values

Only now the numbers, because a number attached to an interval whose anchors you cannot name is not knowledge.

All three framing margins are minimums; the maximum column is empty for every one of them.

symbolparameter, as the specification words itStandard-modeFast-modeFast-mode Plusunit
tHD;STAhold time (repeated) START condition4.00.60.26µs
tSU;STAset-up time for a repeated START condition4.70.60.26µs
tSU;STOset-up time for STOP condition4.00.60.26µs

Two neighbours are worth listing alongside them, because §6 needs them and because they are the parameters most often confused with these:

symbolparameterStandard-modeFast-modeFast-mode Plusunit
tBUFbus free time between a STOP and START condition4.71.30.5µs
tLOWLOW period of the SCL clock4.71.30.5µs
tHIGHHIGH period of the SCL clock4.00.60.26µs

6. A Pattern Worth Noticing

Look down those six rows and the framing margins are not independent numbers at all.

In every mode: tHD;STA = tSU;STO = tHIGH. In Fast-mode and Fast-mode Plus, tSU;STA joins them. And in every mode, tBUF = tLOW.

Standard-mode is the only one where tSU;STA breaks away from tHIGH and instead equals tLOW and tBUF, all three at 4.7 µs.

7. What This Chapter Does Not Own

Three adjacent things, so the boundaries are explicit.

tBUF is not a framing margin. It is listed above because it is constantly confused with tSU;STA — both gate a START, and there the resemblance ends. tBUF is measured between two different framing events on a bus that belongs to nobody in between; tSU;STA is measured between two edges of one controller's own deliberate sequence. Chapter 5.3 built the state machine for the bus-free interval; Module 11 owns its budget.

Rise and fall times are not developed here. §3 and §4 noted that two of the three margins have release edges as anchors and are therefore board-sensitive, which is as far as this chapter goes. The edge-rate limits, the bus capacitance ceiling, and how they trade against pull-up sizing are Chapter 11.7, building on Chapter 2.4.

Data timing is not here either. tSU;DAT, tHD;DAT, tVD;DAT and tVD;ACK govern ordinary bits and are Chapter 11.3 and Chapter 11.4. The framing margins in this chapter constrain three specific edges; the data parameters constrain every other edge on the bus.

8. Converting the Three Margins to Cycle Counts

Chapter 5.3 §5 established the rule — round up, because a minimum rounded down is a violation — and the integer-safe idiom. Apply it to all three margins at one clock frequency, because doing all three at once exposes something a single conversion hides.

Azvya Education Pvt. Ltd.VLSI Mentor
MATHEMATICAL DERIVATION — Fast-mode framing margins at a 50 MHz internal clock
   f_clk = 50 MHz   ->   T_clk = 20 ns
   Fast-mode minimums, from the table in section 5:

     tHD;STA  0.6 us = 600 ns    600 / 20 = 30.0  exactly  ->  30 cycles
     tSU;STA  0.6 us = 600 ns    600 / 20 = 30.0  exactly  ->  30 cycles
     tSU;STO  0.6 us = 600 ns    600 / 20 = 30.0  exactly  ->  30 cycles

   All three equal, all three exact. A designer who stopped here would conclude
   that one constant serves all three margins -- and would be right, at 50 MHz,
   in Fast-mode. Both qualifiers matter.

   Now the SAME margins at 48 MHz, a very common crystal-derived frequency:

     T_clk = 1 / 48 MHz = 20.8333... ns

     ceil(600 / 20.8333)  = ceil(28.8) = 29 cycles       (floor would give 28)
     29 x 20.8333 ns = 604.2 ns  >=  600 ns              legal, 4.2 ns of margin
     28 x 20.8333 ns = 583.3 ns  <   600 ns              ILLEGAL by 16.7 ns

   And now STANDARD-mode at 48 MHz, where the exception in section 6 bites:

     tHD;STA  4.0 us = 4000 ns   ceil(4000 x 48 / 1000) = ceil(192.0) = 192
     tSU;STO  4.0 us = 4000 ns                                        = 192
     tSU;STA  4.7 us = 4700 ns   ceil(4700 x 48 / 1000) = ceil(225.6) = 226
                                                                       ^^^
     NOT the same number. A controller that programmed 192 into all three
     registers would be 34 cycles -- 708 ns -- short on repeated-START setup,
     and would fail ONLY on combined transactions, ONLY in Standard-mode.

   Using the integer-safe form from Chapter 5.3 throughout:

     cycles = (T_NS * FCLK_MHZ + 999) / 1000      [integer division]

     tSU;STA, Sm, 48 MHz:  (4700 * 48 + 999) / 1000
                        =  (225600 + 999) / 1000  =  226599 / 1000  =  226

Three things that example is chosen to show.

A conveniently exact frequency hides the rounding question entirely. At 50 MHz nothing forces a decision, so a design developed at 50 MHz and ported to 48 MHz meets the rounding rule for the first time in the field.

Equal specification values do not imply one register. They are equal in Fast-mode and unequal in Standard-mode, so hardware that shares one count across the three margins is mode-dependent in a way its interface does not reveal.

The failure is mode- and feature-selective. A controller short on tSU;STA only works differently on transfers that use a repeated START, and only in Standard-mode. That is a narrow enough slice to survive a great deal of testing.

9. The Sequencer in Three Languages

Now the generator. Its job is to emit all three framing events with the correct edge ordering and counted margins between them, to take those counts from conceptual registers rather than parameters, and to refuse to operate when the configuration is illegal.

Two design commitments, stated before the code.

Margins are runtime inputs, not parameters. Real controllers expose timing configuration to firmware, which is how one peripheral supports several speed modes. That is also precisely why the legality check has to exist — a parameter is fixed by a designer who read the table, while a register is written by a driver that may not have.

Counter convention is load N-1, act at zero, giving exactly N cycles. Chapter 5.3 §6 proved it on a timeline. The same convention is used for all three margins here.

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_framing_sequencer.sv — SYNTHESIZABLE RTL. Generates all three framing events with programmable margins, and refuses an illegal configuration.
   module i2c_framing_sequencer #(
       // Width of the programmed margin fields -- the conceptual register width.
       parameter int CNT_W        = 8,
       // Required minimums for the selected speed mode, in internal clock cycles.
       // These come from the mode, never from software, which is the whole point.
       parameter int T_HD_STA_MIN = 2,
       parameter int T_SU_STA_MIN = 2,
       parameter int T_SU_STO_MIN = 2
   )(
       input  logic clk,
       input  logic rst_n,
       // Programmed margins, in INTERNAL CLOCK CYCLES. Each is a time requirement
       // already converted by the ceiling rule. Module 11 owns the specified times.
       input  logic [CNT_W-1:0] cfg_hd_sta,   // START/Sr edge -> first SCL fall
       input  logic [CNT_W-1:0] cfg_su_sta,   // SCL rise      -> Sr edge
       input  logic [CNT_W-1:0] cfg_su_sto,   // SCL rise      -> STOP edge
       input  logic cmd_start,        // pulse -- emit S from a released bus
       input  logic cmd_rstart,       // pulse -- emit Sr without releasing the bus
       input  logic cmd_stop,         // pulse -- emit P and release the bus
       output logic scl_drive_low,    // DRIVE INTENT: pull LOW or release. Never 1.
       output logic sda_drive_low,    // DRIVE INTENT: pull LOW or release. Never 1.
       output logic busy,
       output logic done,             // one-cycle pulse as the event completes
       output logic cfg_error         // a programmed margin is below its minimum
   );
       typedef enum logic [3:0] {
           FS_IDLE,        // bus released, no transfer
           FS_S_EDGE,      // SDA driven low while SCL is high -- this IS the START
           FS_HELD,        // SCL low, SDA low: the byte engine's starting point
           FS_RS_SDA,      // release SDA during the SCL low phase
           FS_RS_SCL,      // release SCL, then wait tSU;STA
           FS_RS_EDGE,     // drive SDA low -- this IS the repeated START
           FS_STO_SDA,     // ensure SDA low during the SCL low phase
           FS_STO_SCL,     // release SCL, then wait tSU;STO
           FS_STO_EDGE     // release SDA -- this IS the STOP
       } state_e;

       state_e state;
       logic [CNT_W-1:0] cnt;

       // Configuration legality, checked in hardware next to the counters that
       // consume the values rather than in the driver that programmed them.
       // `<` (not `<=`) makes a margin EQUAL to its minimum legal, which is what
       // "minimum" means -- and is the boundary the testbench pins down.
       always_comb begin
           cfg_error = (cfg_hd_sta < CNT_W'(T_HD_STA_MIN))
                    || (cfg_su_sta < CNT_W'(T_SU_STA_MIN))
                    || (cfg_su_sto < CNT_W'(T_SU_STO_MIN));
       end

       always_ff @(posedge clk) begin
           if (!rst_n) begin
               state <= FS_IDLE;
               cnt   <= '0;
               // RELEASE both lines on reset: a controller in reset must not hold
               // either conductor low, or it jams the bus for every other device.
               scl_drive_low <= 1'b0;
               sda_drive_low <= 1'b0;
               done <= 1'b0;
           end else begin
               done <= 1'b0;

               case (state)
                   FS_IDLE:
                       // Refuse to emit framing under an illegal configuration.
                       // Emitting a knowingly-short margin is worse than refusing.
                       if (cmd_start && !cfg_error) begin
                           sda_drive_low <= 1'b1;               // the START edge
                           cnt   <= cfg_hd_sta - 1'b1;
                           state <= FS_S_EDGE;
                       end

                   FS_S_EDGE:
                       // Hold tHD;STA, THEN generate the first clock pulse -- the
                       // specification words the requirement in exactly that order.
                       if (cnt != '0) cnt <= cnt - 1'b1;
                       else begin
                           scl_drive_low <= 1'b1;
                           done  <= 1'b1;
                           state <= FS_HELD;
                       end

                   FS_HELD:
                       if (cmd_rstart && !cfg_error) begin
                           sda_drive_low <= 1'b0;               // release SDA, SCL still low
                           state <= FS_RS_SDA;
                       end else if (cmd_stop && !cfg_error) begin
                           sda_drive_low <= 1'b1;               // SDA low before SCL rises
                           state <= FS_STO_SDA;
                       end

                   FS_RS_SDA: begin
                       scl_drive_low <= 1'b0;                   // release SCL -- SCL rises
                       cnt   <= cfg_su_sta - 1'b1;
                       state <= FS_RS_SCL;
                   end

                   FS_RS_SCL:
                       if (cnt != '0) cnt <= cnt - 1'b1;
                       else begin
                           sda_drive_low <= 1'b1;               // the repeated-START edge
                           cnt   <= cfg_hd_sta - 1'b1;
                           state <= FS_RS_EDGE;
                       end

                   FS_RS_EDGE:
                       if (cnt != '0) cnt <= cnt - 1'b1;
                       else begin
                           scl_drive_low <= 1'b1;
                           done  <= 1'b1;
                           state <= FS_HELD;
                       end

                   FS_STO_SDA: begin
                       scl_drive_low <= 1'b0;                   // release SCL -- SCL rises
                       cnt   <= cfg_su_sto - 1'b1;
                       state <= FS_STO_SCL;
                   end

                   FS_STO_SCL:
                       if (cnt != '0) cnt <= cnt - 1'b1;
                       else begin
                           sda_drive_low <= 1'b0;               // the STOP edge
                           done  <= 1'b1;
                           state <= FS_STO_EDGE;
                       end

                   FS_STO_EDGE:
                       state <= FS_IDLE;                        // released, and free

                   default: state <= FS_IDLE;
               endcase
           end
       end

       always_comb busy = (state != FS_IDLE);
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_framing_sequencer_tb.sv — SELF-CHECKING TESTBENCH, SIMULATION ONLY. Measures all three margins on the observed bus; three distinct values; boundary and illegal configs.
   module i2c_framing_sequencer_tb;
       // Three DISTINCT margins. A design that satisfied one hand-tuned constant,
       // or that swapped two of the fields, cannot pass all three measurements.
       localparam int HD  = 4;   // tHD;STA
       localparam int SUA = 6;   // tSU;STA
       localparam int SUO = 3;   // tSU;STO
       localparam int MIN = 2;

       logic clk = 1'b0, rst_n;
       logic [7:0] cfg_hd, cfg_sua, cfg_suo;
       logic cmd_start, cmd_rstart, cmd_stop;
       logic scl_drive_low, sda_drive_low, busy, done, cfg_error;
       int   errors = 0;

       i2c_framing_sequencer #(.CNT_W(8), .T_HD_STA_MIN(MIN), .T_SU_STA_MIN(MIN), .T_SU_STO_MIN(MIN))
       dut (.clk(clk), .rst_n(rst_n),
            .cfg_hd_sta(cfg_hd), .cfg_su_sta(cfg_sua), .cfg_su_sto(cfg_suo),
            .cmd_start(cmd_start), .cmd_rstart(cmd_rstart), .cmd_stop(cmd_stop),
            .scl_drive_low(scl_drive_low), .sda_drive_low(sda_drive_low),
            .busy(busy), .done(done), .cfg_error(cfg_error));

       // The OBSERVED bus: one open-drain driver plus a pull-up. A released line
       // reads HIGH because nothing is pulling it down -- it is never driven high.
       // NO RC behaviour is modelled here: this proves release TIMING and edge
       // ORDER, and proves nothing about analog rise time (Chapter 2.4).
       wire scl_bus = ~scl_drive_low;
       wire sda_bus = ~sda_drive_low;

       always #5 clk = ~clk;
       initial begin #60000; $display("FAIL: watchdog expired"); $finish; end

       // ---- edge-timestamping monitor: record WHEN, then do arithmetic ----
       int cyc = 0;
       logic scl_q, sda_q;
       int f_cyc  [0:7];   int f_kind [0:7];   int n_f = 0;   // framing edges
       int scl_fall[0:7];  int n_scl_fall = 0;
       int scl_rise[0:7];  int n_scl_rise = 0;
       int n_done = 0;
       int bad_drive = 0;

       always @(posedge clk) if (rst_n) begin
           cyc <= cyc + 1;
           if (done) n_done <= n_done + 1;

           // An SDA edge while SCL is HIGH is a framing event, by definition.
           if (sda_q && !sda_bus && scl_bus && scl_q) begin
               if (n_f < 8) begin f_cyc[n_f] <= cyc; f_kind[n_f] <= 0; end  // fall = S or Sr
               n_f <= n_f + 1;
           end
           if (!sda_q && sda_bus && scl_bus && scl_q) begin
               if (n_f < 8) begin f_cyc[n_f] <= cyc; f_kind[n_f] <= 1; end  // rise = P
               n_f <= n_f + 1;
           end
           if (scl_q && !scl_bus) begin
               if (n_scl_fall < 8) scl_fall[n_scl_fall] <= cyc;
               n_scl_fall <= n_scl_fall + 1;
           end
           if (!scl_q && scl_bus) begin
               if (n_scl_rise < 8) scl_rise[n_scl_rise] <= cyc;
               n_scl_rise <= n_scl_rise + 1;
           end

           // Structural open-drain check: the drive-intent outputs are the only
           // things the DUT owns, and an X/Z on either would be a real bug.
           if (scl_drive_low !== 1'b0 && scl_drive_low !== 1'b1) bad_drive <= bad_drive + 1;
           if (sda_drive_low !== 1'b0 && sda_drive_low !== 1'b1) bad_drive <= bad_drive + 1;

           scl_q <= scl_bus;
           sda_q <= sda_bus;
       end

       task automatic pulse_start();  cmd_start  = 1'b1; @(negedge clk); cmd_start  = 1'b0; endtask
       task automatic pulse_rstart(); cmd_rstart = 1'b1; @(negedge clk); cmd_rstart = 1'b0; endtask
       task automatic pulse_stop();   cmd_stop   = 1'b1; @(negedge clk); cmd_stop   = 1'b0; endtask

       task automatic check(input int got, input int want, input string what);
           if (got != want) begin
               $display("FAIL: %s measured %0d cycles, required %0d", what, got, want);
               errors++;
           end
       endtask

       initial begin
           rst_n = 1'b0;
           cfg_hd = HD; cfg_sua = SUA; cfg_suo = SUO;
           cmd_start = 1'b0; cmd_rstart = 1'b0; cmd_stop = 1'b0;
           scl_q = 1'b1; sda_q = 1'b1;
           repeat (3) @(negedge clk);

           // 1 -- reset RELEASES both lines. A controller that holds either line low
           //      out of reset jams every other device on the bus.
           if (scl_drive_low !== 1'b0 || sda_drive_low !== 1'b0) begin
               $display("FAIL: reset did not release both lines"); errors++; end
           if (busy !== 1'b0) begin $display("FAIL: busy asserted out of reset"); errors++; end
           if (cfg_error !== 1'b0) begin $display("FAIL: legal configuration flagged as illegal"); errors++; end
           rst_n = 1'b1; @(negedge clk);

           // 2 -- S, Sr, P back to back without releasing the bus in between.
           pulse_start();
           wait (done); @(negedge clk);
           pulse_rstart();
           wait (done); @(negedge clk);
           pulse_stop();
           wait (done); repeat (3) @(negedge clk);

           // 3 -- exactly three framing edges: S (fall), Sr (fall), P (rise).
           if (n_f !== 3) begin
               $display("FAIL: %0d framing edges produced, expected exactly 3", n_f); errors++; end
           else begin
               if (f_kind[0] !== 0) begin $display("FAIL: S was not an SDA fall"); errors++; end
               if (f_kind[1] !== 0) begin $display("FAIL: Sr was not an SDA fall"); errors++; end
               if (f_kind[2] !== 1) begin $display("FAIL: P was not an SDA rise"); errors++; end
           end

           // 4 -- MEASURE every margin edge-to-edge on the observed bus.
           if (n_f >= 3 && n_scl_fall >= 2 && n_scl_rise >= 2) begin
               check(scl_fall[0] - f_cyc[0], HD,  "tHD;STA after S");
               check(f_cyc[1] - scl_rise[0], SUA, "tSU;STA before Sr");
               check(scl_fall[1] - f_cyc[1], HD,  "tHD;STA after Sr");
               check(f_cyc[2] - scl_rise[1], SUO, "tSU;STO before P");
           end else begin
               $display("FAIL: not enough bus edges to measure (f=%0d fall=%0d rise=%0d)",
                        n_f, n_scl_fall, n_scl_rise);
               errors++;
           end

           // 5 -- the bus is released after the STOP, and the sequencer is idle.
           if (scl_drive_low !== 1'b0 || sda_drive_low !== 1'b0) begin
               $display("FAIL: bus not released after STOP"); errors++; end
           if (busy !== 1'b0) begin $display("FAIL: still busy after STOP"); errors++; end

           // 6 -- one done pulse per framing event, no more.
           if (n_done !== 3) begin
               $display("FAIL: %0d done pulses, expected 3", n_done); errors++; end

           // 7 -- BOUNDARY: a margin EQUAL to its minimum is legal. "Minimum" means
           //      inclusive, and a `>` where `>=` belongs fails exactly here.
           cfg_hd = MIN; cfg_sua = MIN; cfg_suo = MIN; @(negedge clk);
           if (cfg_error !== 1'b0) begin
               $display("FAIL: margin equal to the minimum was rejected"); errors++; end

           // 8 -- ILLEGAL: one cycle below the minimum must be refused, and the
           //      sequencer must not emit framing at all.
           cfg_sua = MIN - 1; @(negedge clk);
           if (cfg_error !== 1'b1) begin
               $display("FAIL: margin below the minimum was accepted"); errors++; end
           pulse_start();
           repeat (6) @(negedge clk);
           if (busy !== 1'b0 || sda_drive_low !== 1'b0) begin
               $display("FAIL: sequencer emitted framing under an illegal configuration"); errors++; end
           if (n_f !== 3) begin
               $display("FAIL: illegal configuration still produced a framing edge"); errors++; end

           // 9 -- zero is illegal too, and must never underflow the counter.
           cfg_sua = SUA; cfg_hd = 0; @(negedge clk);
           if (cfg_error !== 1'b1) begin $display("FAIL: a zero margin was accepted"); errors++; end

           if (bad_drive != 0) begin
               $display("FAIL: drive-intent output was not a clean 0/1"); errors++; end

           if (errors == 0)
               $display("PASS: tHD;STA=%0d tSU;STA=%0d tSU;STO=%0d measured on the bus; config legality enforced",
                        scl_fall[0] - f_cyc[0], f_cyc[1] - scl_rise[0], f_cyc[2] - scl_rise[1]);
           else $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_framing_sequencer.v — SYNTHESIZABLE RTL. The same sequencer in Verilog-2005.
   module i2c_framing_sequencer #(
       parameter integer CNT_W        = 8,   // conceptual register width
       // Required minimums for the selected speed mode. These come from the mode,
       // never from software, which is the whole point of checking them.
       parameter integer T_HD_STA_MIN = 2,
       parameter integer T_SU_STA_MIN = 2,
       parameter integer T_SU_STO_MIN = 2
   )(
       input  wire clk,
       input  wire rst_n,
       // Programmed margins in INTERNAL CLOCK CYCLES, already converted from time
       // by the ceiling rule. Module 11 owns the specified times.
       input  wire [CNT_W-1:0] cfg_hd_sta,   // START/Sr edge -> first SCL fall
       input  wire [CNT_W-1:0] cfg_su_sta,   // SCL rise      -> Sr edge
       input  wire [CNT_W-1:0] cfg_su_sto,   // SCL rise      -> STOP edge
       input  wire cmd_start,
       input  wire cmd_rstart,
       input  wire cmd_stop,
       output reg  scl_drive_low,    // DRIVE INTENT: pull LOW or release. Never 1.
       output reg  sda_drive_low,    // DRIVE INTENT: pull LOW or release. Never 1.
       output wire busy,
       output reg  done,
       output wire cfg_error
   );
       localparam FS_IDLE     = 4'd0;
       localparam FS_S_EDGE   = 4'd1;
       localparam FS_HELD     = 4'd2;
       localparam FS_RS_SDA   = 4'd3;
       localparam FS_RS_SCL   = 4'd4;
       localparam FS_RS_EDGE  = 4'd5;
       localparam FS_STO_SDA  = 4'd6;
       localparam FS_STO_SCL  = 4'd7;
       localparam FS_STO_EDGE = 4'd8;

       reg [3:0]       state;
       reg [CNT_W-1:0] cnt;

       // `<` (not `<=`) makes a margin EQUAL to its minimum legal, which is what
       // "minimum" means.
       assign cfg_error = (cfg_hd_sta < T_HD_STA_MIN[CNT_W-1:0])
                       || (cfg_su_sta < T_SU_STA_MIN[CNT_W-1:0])
                       || (cfg_su_sto < T_SU_STO_MIN[CNT_W-1:0]);

       always @(posedge clk) begin
           if (!rst_n) begin
               state <= FS_IDLE;
               cnt   <= {CNT_W{1'b0}};
               // RELEASE both lines on reset, or the controller jams the bus.
               scl_drive_low <= 1'b0;
               sda_drive_low <= 1'b0;
               done <= 1'b0;
           end else begin
               done <= 1'b0;

               case (state)
                   FS_IDLE:
                       if (cmd_start && !cfg_error) begin
                           sda_drive_low <= 1'b1;               // the START edge
                           cnt   <= cfg_hd_sta - 1'b1;
                           state <= FS_S_EDGE;
                       end

                   FS_S_EDGE:
                       // Hold tHD;STA, THEN generate the first clock pulse.
                       if (cnt != {CNT_W{1'b0}}) cnt <= cnt - 1'b1;
                       else begin
                           scl_drive_low <= 1'b1;
                           done  <= 1'b1;
                           state <= FS_HELD;
                       end

                   FS_HELD:
                       if (cmd_rstart && !cfg_error) begin
                           sda_drive_low <= 1'b0;               // release SDA, SCL still low
                           state <= FS_RS_SDA;
                       end else if (cmd_stop && !cfg_error) begin
                           sda_drive_low <= 1'b1;               // SDA low before SCL rises
                           state <= FS_STO_SDA;
                       end

                   FS_RS_SDA: begin
                       scl_drive_low <= 1'b0;                   // release SCL -- SCL rises
                       cnt   <= cfg_su_sta - 1'b1;
                       state <= FS_RS_SCL;
                   end

                   FS_RS_SCL:
                       if (cnt != {CNT_W{1'b0}}) cnt <= cnt - 1'b1;
                       else begin
                           sda_drive_low <= 1'b1;               // the repeated-START edge
                           cnt   <= cfg_hd_sta - 1'b1;
                           state <= FS_RS_EDGE;
                       end

                   FS_RS_EDGE:
                       if (cnt != {CNT_W{1'b0}}) cnt <= cnt - 1'b1;
                       else begin
                           scl_drive_low <= 1'b1;
                           done  <= 1'b1;
                           state <= FS_HELD;
                       end

                   FS_STO_SDA: begin
                       scl_drive_low <= 1'b0;                   // release SCL -- SCL rises
                       cnt   <= cfg_su_sto - 1'b1;
                       state <= FS_STO_SCL;
                   end

                   FS_STO_SCL:
                       if (cnt != {CNT_W{1'b0}}) cnt <= cnt - 1'b1;
                       else begin
                           sda_drive_low <= 1'b0;               // the STOP edge
                           done  <= 1'b1;
                           state <= FS_STO_EDGE;
                       end

                   FS_STO_EDGE:
                       state <= FS_IDLE;                        // released, and free

                   default: state <= FS_IDLE;
               endcase
           end
       end

       assign busy = (state != FS_IDLE);
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_framing_sequencer_tb.v — SELF-CHECKING TESTBENCH, SIMULATION ONLY. The same measurements in Verilog idiom.
   module i2c_framing_sequencer_tb;
       // Three DISTINCT margins: no single constant, and no swapped pair, passes all.
       localparam integer HD  = 4;   // tHD;STA
       localparam integer SUA = 6;   // tSU;STA
       localparam integer SUO = 3;   // tSU;STO
       localparam integer MIN = 2;

       reg clk, rst_n;
       reg [7:0] cfg_hd, cfg_sua, cfg_suo;
       reg cmd_start, cmd_rstart, cmd_stop;
       wire scl_drive_low, sda_drive_low, busy, done, cfg_error;
       integer errors;

       i2c_framing_sequencer #(.CNT_W(8), .T_HD_STA_MIN(MIN), .T_SU_STA_MIN(MIN), .T_SU_STO_MIN(MIN))
       dut (.clk(clk), .rst_n(rst_n),
            .cfg_hd_sta(cfg_hd), .cfg_su_sta(cfg_sua), .cfg_su_sto(cfg_suo),
            .cmd_start(cmd_start), .cmd_rstart(cmd_rstart), .cmd_stop(cmd_stop),
            .scl_drive_low(scl_drive_low), .sda_drive_low(sda_drive_low),
            .busy(busy), .done(done), .cfg_error(cfg_error));

       // Observed bus: one open-drain driver plus a pull-up. A released line reads
       // HIGH because nothing pulls it down. NO RC behaviour is modelled.
       wire scl_bus = ~scl_drive_low;
       wire sda_bus = ~sda_drive_low;

       initial clk = 1'b0;
       always #5 clk = ~clk;
       initial begin #60000; $display("FAIL: watchdog expired"); $finish; end

       integer cyc;
       reg scl_q, sda_q;
       integer f_cyc  [0:7];  integer f_kind [0:7];  integer n_f;
       integer scl_fall[0:7]; integer n_scl_fall;
       integer scl_rise[0:7]; integer n_scl_rise;
       integer n_done;
       integer bad_drive;

       always @(posedge clk) if (rst_n) begin
           cyc <= cyc + 1;
           if (done) n_done <= n_done + 1;

           // An SDA edge while SCL is HIGH is a framing event, by definition.
           if (sda_q && !sda_bus && scl_bus && scl_q) begin
               if (n_f < 8) begin f_cyc[n_f] <= cyc; f_kind[n_f] <= 0; end
               n_f <= n_f + 1;
           end
           if (!sda_q && sda_bus && scl_bus && scl_q) begin
               if (n_f < 8) begin f_cyc[n_f] <= cyc; f_kind[n_f] <= 1; end
               n_f <= n_f + 1;
           end
           if (scl_q && !scl_bus) begin
               if (n_scl_fall < 8) scl_fall[n_scl_fall] <= cyc;
               n_scl_fall <= n_scl_fall + 1;
           end
           if (!scl_q && scl_bus) begin
               if (n_scl_rise < 8) scl_rise[n_scl_rise] <= cyc;
               n_scl_rise <= n_scl_rise + 1;
           end

           if (scl_drive_low !== 1'b0 && scl_drive_low !== 1'b1) bad_drive <= bad_drive + 1;
           if (sda_drive_low !== 1'b0 && sda_drive_low !== 1'b1) bad_drive <= bad_drive + 1;

           scl_q <= scl_bus;
           sda_q <= sda_bus;
       end

       task pulse_start;  begin cmd_start  = 1'b1; @(negedge clk); cmd_start  = 1'b0; end endtask
       task pulse_rstart; begin cmd_rstart = 1'b1; @(negedge clk); cmd_rstart = 1'b0; end endtask
       task pulse_stop;   begin cmd_stop   = 1'b1; @(negedge clk); cmd_stop   = 1'b0; end endtask

       task check; input integer got; input integer want; input integer id; begin
           if (got != want) begin
               $display("FAIL: margin %0d measured %0d cycles, required %0d", id, got, want);
               errors = errors + 1;
           end
       end endtask

       initial begin
           errors = 0; cyc = 0; n_f = 0; n_scl_fall = 0; n_scl_rise = 0;
           n_done = 0; bad_drive = 0;
           rst_n = 1'b0;
           cfg_hd = HD; cfg_sua = SUA; cfg_suo = SUO;
           cmd_start = 1'b0; cmd_rstart = 1'b0; cmd_stop = 1'b0;
           scl_q = 1'b1; sda_q = 1'b1;
           repeat (3) @(negedge clk);

           if (scl_drive_low !== 1'b0 || sda_drive_low !== 1'b0) begin
               $display("FAIL: reset did not release both lines"); errors = errors + 1; end
           if (busy !== 1'b0) begin $display("FAIL: busy asserted out of reset"); errors = errors + 1; end
           if (cfg_error !== 1'b0) begin $display("FAIL: legal configuration flagged illegal"); errors = errors + 1; end
           rst_n = 1'b1; @(negedge clk);

           pulse_start();
           wait (done); @(negedge clk);
           pulse_rstart();
           wait (done); @(negedge clk);
           pulse_stop();
           wait (done); repeat (3) @(negedge clk);

           if (n_f !== 3) begin
               $display("FAIL: %0d framing edges produced, expected exactly 3", n_f); errors = errors + 1; end
           else begin
               if (f_kind[0] !== 0) begin $display("FAIL: S was not an SDA fall"); errors = errors + 1; end
               if (f_kind[1] !== 0) begin $display("FAIL: Sr was not an SDA fall"); errors = errors + 1; end
               if (f_kind[2] !== 1) begin $display("FAIL: P was not an SDA rise"); errors = errors + 1; end
           end

           if (n_f >= 3 && n_scl_fall >= 2 && n_scl_rise >= 2) begin
               check(scl_fall[0] - f_cyc[0], HD,  1);   // tHD;STA after S
               check(f_cyc[1] - scl_rise[0], SUA, 2);   // tSU;STA before Sr
               check(scl_fall[1] - f_cyc[1], HD,  3);   // tHD;STA after Sr
               check(f_cyc[2] - scl_rise[1], SUO, 4);   // tSU;STO before P
           end else begin
               $display("FAIL: not enough bus edges to measure"); errors = errors + 1;
           end

           if (scl_drive_low !== 1'b0 || sda_drive_low !== 1'b0) begin
               $display("FAIL: bus not released after STOP"); errors = errors + 1; end
           if (busy !== 1'b0) begin $display("FAIL: still busy after STOP"); errors = errors + 1; end
           if (n_done !== 3) begin
               $display("FAIL: %0d done pulses, expected 3", n_done); errors = errors + 1; end

           // BOUNDARY: a margin EQUAL to its minimum is legal.
           cfg_hd = MIN; cfg_sua = MIN; cfg_suo = MIN; @(negedge clk);
           if (cfg_error !== 1'b0) begin
               $display("FAIL: margin equal to the minimum was rejected"); errors = errors + 1; end

           // ILLEGAL: one cycle short must be refused outright.
           cfg_sua = MIN - 1; @(negedge clk);
           if (cfg_error !== 1'b1) begin
               $display("FAIL: margin below the minimum was accepted"); errors = errors + 1; end
           pulse_start();
           repeat (6) @(negedge clk);
           if (busy !== 1'b0 || sda_drive_low !== 1'b0) begin
               $display("FAIL: sequencer emitted framing under an illegal configuration"); errors = errors + 1; end
           if (n_f !== 3) begin
               $display("FAIL: illegal configuration still produced a framing edge"); errors = errors + 1; end

           cfg_sua = SUA; cfg_hd = 0; @(negedge clk);
           if (cfg_error !== 1'b1) begin $display("FAIL: a zero margin was accepted"); errors = errors + 1; end

           if (bad_drive != 0) begin
               $display("FAIL: drive-intent output was not a clean 0/1"); errors = errors + 1; end

           if (errors == 0)
               $display("PASS: tHD;STA=%0d tSU;STA=%0d tSU;STO=%0d measured on the bus; config legality enforced",
                        scl_fall[0] - f_cyc[0], f_cyc[1] - scl_rise[0], f_cyc[2] - scl_rise[1]);
           else $display("FAIL: %0d error(s)", errors);
           $finish;
       end
   endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_framing_sequencer.vhd — SYNTHESIZABLE RTL. Enumerated states; unsigned margins from numeric_std.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_framing_sequencer is
       generic (
           CNT_W        : positive := 8;   -- conceptual register width
           -- Required minimums for the selected speed mode. These come from the
           -- mode, never from software, which is the point of checking them.
           T_HD_STA_MIN : positive := 2;
           T_SU_STA_MIN : positive := 2;
           T_SU_STO_MIN : positive := 2
       );
       port (
           clk   : in std_logic;
           rst_n : in std_logic;
           -- Programmed margins in INTERNAL CLOCK CYCLES, already converted from
           -- time by the ceiling rule. Module 11 owns the specified times.
           cfg_hd_sta : in unsigned(CNT_W - 1 downto 0);   -- START/Sr edge -> first SCL fall
           cfg_su_sta : in unsigned(CNT_W - 1 downto 0);   -- SCL rise      -> Sr edge
           cfg_su_sto : in unsigned(CNT_W - 1 downto 0);   -- SCL rise      -> STOP edge
           cmd_start  : in std_logic;
           cmd_rstart : in std_logic;
           cmd_stop   : in std_logic;
           scl_drive_low : out std_logic;   -- DRIVE INTENT: pull LOW or release. Never 1.
           sda_drive_low : out std_logic;   -- DRIVE INTENT: pull LOW or release. Never 1.
           busy          : out std_logic;
           done          : out std_logic;
           cfg_error     : out std_logic
       );
   end entity;

   architecture rtl of i2c_framing_sequencer is
       type state_t is (
           FS_IDLE,        -- bus released, no transfer
           FS_S_EDGE,      -- SDA driven low while SCL is high -- this IS the START
           FS_HELD,        -- SCL low, SDA low: the byte engine's starting point
           FS_RS_SDA,      -- release SDA during the SCL low phase
           FS_RS_SCL,      -- release SCL, then wait tSU;STA
           FS_RS_EDGE,     -- drive SDA low -- this IS the repeated START
           FS_STO_SDA,     -- ensure SDA low during the SCL low phase
           FS_STO_SCL,     -- release SCL, then wait tSU;STO
           FS_STO_EDGE     -- release SDA -- this IS the STOP
       );
       signal state : state_t := FS_IDLE;
       signal cnt   : unsigned(CNT_W - 1 downto 0) := (others => '0');
       signal cfg_bad : std_logic;
       constant ZERO : unsigned(CNT_W - 1 downto 0) := (others => '0');
   begin
       -- `<` (not `<=`) makes a margin EQUAL to its minimum legal, which is what
       -- "minimum" means.
       cfg_bad <= '1' when (cfg_hd_sta < to_unsigned(T_HD_STA_MIN, CNT_W))
                        or (cfg_su_sta < to_unsigned(T_SU_STA_MIN, CNT_W))
                        or (cfg_su_sto < to_unsigned(T_SU_STO_MIN, CNT_W))
                  else '0';
       cfg_error <= cfg_bad;

       process (clk)
       begin
           if rising_edge(clk) then
               if rst_n = '0' then
                   state <= FS_IDLE;
                   cnt   <= (others => '0');
                   -- RELEASE both lines on reset, or the controller jams the bus.
                   scl_drive_low <= '0';
                   sda_drive_low <= '0';
                   done <= '0';
               else
                   done <= '0';

                   case state is
                       when FS_IDLE =>
                           if cmd_start = '1' and cfg_bad = '0' then
                               sda_drive_low <= '1';                 -- the START edge
                               cnt   <= cfg_hd_sta - 1;
                               state <= FS_S_EDGE;
                           end if;

                       when FS_S_EDGE =>
                           -- Hold tHD;STA, THEN generate the first clock pulse.
                           if cnt /= ZERO then
                               cnt <= cnt - 1;
                           else
                               scl_drive_low <= '1';
                               done  <= '1';
                               state <= FS_HELD;
                           end if;

                       when FS_HELD =>
                           if cmd_rstart = '1' and cfg_bad = '0' then
                               sda_drive_low <= '0';                 -- release SDA, SCL low
                               state <= FS_RS_SDA;
                           elsif cmd_stop = '1' and cfg_bad = '0' then
                               sda_drive_low <= '1';                 -- SDA low before SCL rises
                               state <= FS_STO_SDA;
                           end if;

                       when FS_RS_SDA =>
                           scl_drive_low <= '0';                     -- release SCL -- SCL rises
                           cnt   <= cfg_su_sta - 1;
                           state <= FS_RS_SCL;

                       when FS_RS_SCL =>
                           if cnt /= ZERO then
                               cnt <= cnt - 1;
                           else
                               sda_drive_low <= '1';                 -- the repeated-START edge
                               cnt   <= cfg_hd_sta - 1;
                               state <= FS_RS_EDGE;
                           end if;

                       when FS_RS_EDGE =>
                           if cnt /= ZERO then
                               cnt <= cnt - 1;
                           else
                               scl_drive_low <= '1';
                               done  <= '1';
                               state <= FS_HELD;
                           end if;

                       when FS_STO_SDA =>
                           scl_drive_low <= '0';                     -- release SCL -- SCL rises
                           cnt   <= cfg_su_sto - 1;
                           state <= FS_STO_SCL;

                       when FS_STO_SCL =>
                           if cnt /= ZERO then
                               cnt <= cnt - 1;
                           else
                               sda_drive_low <= '0';                 -- the STOP edge
                               done  <= '1';
                               state <= FS_STO_EDGE;
                           end if;

                       when FS_STO_EDGE =>
                           state <= FS_IDLE;                         -- released, and free
                   end case;
               end if;
           end if;
       end process;

       busy <= '0' when state = FS_IDLE else '1';
   end architecture;
Azvya Education Pvt. Ltd.VLSI Mentor
i2c_framing_sequencer_tb.vhd — SELF-CHECKING TESTBENCH, SIMULATION ONLY. The same measurements with assert report severity.
   library ieee;
   use ieee.std_logic_1164.all;
   use ieee.numeric_std.all;

   entity i2c_framing_sequencer_tb is
   end entity;

   architecture sim of i2c_framing_sequencer_tb is
       -- Three DISTINCT margins: no single constant, and no swapped pair, passes all.
       constant HD  : positive := 4;   -- tHD;STA
       constant SUA : positive := 6;   -- tSU;STA
       constant SUO : positive := 3;   -- tSU;STO
       constant MIN : positive := 2;
       constant W   : positive := 8;

       signal clk   : std_logic := '0';
       signal rst_n : std_logic := '0';
       -- Initialised at declaration so the configuration is never a metavalue: an
       -- unassigned register at time zero makes the legality comparison meaningless.
       signal cfg_hd  : unsigned(W - 1 downto 0) := to_unsigned(HD,  W);
       signal cfg_sua : unsigned(W - 1 downto 0) := to_unsigned(SUA, W);
       signal cfg_suo : unsigned(W - 1 downto 0) := to_unsigned(SUO, W);
       signal cmd_start, cmd_rstart, cmd_stop : std_logic := '0';
       signal scl_drive_low, sda_drive_low, busy, done, cfg_error : std_logic;

       -- Observed bus: one open-drain driver plus a pull-up. A released line reads
       -- HIGH because nothing pulls it down. NO RC behaviour is modelled.
       signal scl_bus, sda_bus : std_logic;

       type nat_arr is array (0 to 7) of natural;
       signal f_cyc, f_kind, scl_fall, scl_rise : nat_arr := (others => 0);
       signal n_f, n_scl_fall, n_scl_rise : natural := 0;
       signal cyc, n_done, bad_drive : natural := 0;
       -- Set by the checker just before it suspends, so the watchdog can tell a
       -- finished run from a stalled one.
       signal test_done : std_logic := '0';
   begin
       dut : entity work.i2c_framing_sequencer
           generic map (CNT_W => W, T_HD_STA_MIN => MIN, T_SU_STA_MIN => MIN, T_SU_STO_MIN => MIN)
           port map (clk => clk, rst_n => rst_n,
                     cfg_hd_sta => cfg_hd, cfg_su_sta => cfg_sua, cfg_su_sto => cfg_suo,
                     cmd_start => cmd_start, cmd_rstart => cmd_rstart, cmd_stop => cmd_stop,
                     scl_drive_low => scl_drive_low, sda_drive_low => sda_drive_low,
                     busy => busy, done => done, cfg_error => cfg_error);

       scl_bus <= not scl_drive_low;
       sda_bus <= not sda_drive_low;

       clk <= not clk after 5 ns;

       -- Watchdog. A broken design must produce a REPORTED FAILURE, not a silent
       -- stop: without this, a `wait until` against a stalled DUT simply runs to the
       -- simulator's time limit and prints nothing that identifies the problem.
       watchdog : process
       begin
           wait for 60 us;
           if test_done = '0' then
               report "watchdog expired -- the design never reached the expected state"
                   severity failure;
           end if;
           wait;
       end process;

       -- Edge-timestamping monitor: record WHEN, then do arithmetic.
       stamp : process (clk)
           variable scl_q, sda_q : std_logic := '1';
       begin
           if rising_edge(clk) then
               if rst_n = '1' then
                   cyc <= cyc + 1;
                   if done = '1' then n_done <= n_done + 1; end if;

                   -- An SDA edge while SCL is HIGH is a framing event, by definition.
                   if sda_q = '1' and sda_bus = '0' and scl_bus = '1' and scl_q = '1' then
                       if n_f < 8 then
                           f_cyc(n_f) <= cyc; f_kind(n_f) <= 0;      -- fall = S or Sr
                       end if;
                       n_f <= n_f + 1;
                   end if;
                   if sda_q = '0' and sda_bus = '1' and scl_bus = '1' and scl_q = '1' then
                       if n_f < 8 then
                           f_cyc(n_f) <= cyc; f_kind(n_f) <= 1;      -- rise = P
                       end if;
                       n_f <= n_f + 1;
                   end if;
                   if scl_q = '1' and scl_bus = '0' then
                       if n_scl_fall < 8 then scl_fall(n_scl_fall) <= cyc; end if;
                       n_scl_fall <= n_scl_fall + 1;
                   end if;
                   if scl_q = '0' and scl_bus = '1' then
                       if n_scl_rise < 8 then scl_rise(n_scl_rise) <= cyc; end if;
                       n_scl_rise <= n_scl_rise + 1;
                   end if;

                   if scl_drive_low /= '0' and scl_drive_low /= '1' then bad_drive <= bad_drive + 1; end if;
                   if sda_drive_low /= '0' and sda_drive_low /= '1' then bad_drive <= bad_drive + 1; end if;

                   scl_q := scl_bus;
                   sda_q := sda_bus;
               end if;
           end if;
       end process;

       stim : process
           variable errs : natural := 0;

           procedure waitn (n : in positive) is
           begin
               for i in 1 to n loop wait until falling_edge(clk); end loop;
           end procedure;

           procedure check (got, want, id : in natural) is
           begin
               if got /= want then
                   report "margin " & integer'image(id) & " measured " & integer'image(got) &
                          " cycles, required " & integer'image(want) severity error;
                   errs := errs + 1;
               end if;
           end procedure;
       begin
           waitn(3);

           if scl_drive_low /= '0' or sda_drive_low /= '0' then
               report "reset did not release both lines" severity error; errs := errs + 1; end if;
           if busy /= '0' then
               report "busy asserted out of reset" severity error; errs := errs + 1; end if;
           if cfg_error /= '0' then
               report "legal configuration flagged illegal" severity error; errs := errs + 1; end if;
           rst_n <= '1'; waitn(1);

           cmd_start <= '1'; waitn(1); cmd_start <= '0';
           wait until done = '1'; waitn(1);
           cmd_rstart <= '1'; waitn(1); cmd_rstart <= '0';
           wait until done = '1'; waitn(1);
           cmd_stop <= '1'; waitn(1); cmd_stop <= '0';
           wait until done = '1'; waitn(3);

           if n_f /= 3 then
               report "framing edges = " & integer'image(n_f) & ", expected exactly 3" severity error;
               errs := errs + 1;
           else
               if f_kind(0) /= 0 then report "S was not an SDA fall" severity error; errs := errs + 1; end if;
               if f_kind(1) /= 0 then report "Sr was not an SDA fall" severity error; errs := errs + 1; end if;
               if f_kind(2) /= 1 then report "P was not an SDA rise" severity error; errs := errs + 1; end if;
           end if;

           if n_f >= 3 and n_scl_fall >= 2 and n_scl_rise >= 2 then
               check(scl_fall(0) - f_cyc(0), HD,  1);   -- tHD;STA after S
               check(f_cyc(1) - scl_rise(0), SUA, 2);   -- tSU;STA before Sr
               check(scl_fall(1) - f_cyc(1), HD,  3);   -- tHD;STA after Sr
               check(f_cyc(2) - scl_rise(1), SUO, 4);   -- tSU;STO before P
           else
               report "not enough bus edges to measure" severity error; errs := errs + 1;
           end if;

           if scl_drive_low /= '0' or sda_drive_low /= '0' then
               report "bus not released after STOP" severity error; errs := errs + 1; end if;
           if busy /= '0' then
               report "still busy after STOP" severity error; errs := errs + 1; end if;
           if n_done /= 3 then
               report "done pulses = " & integer'image(n_done) & ", expected 3" severity error;
               errs := errs + 1; end if;

           -- BOUNDARY: a margin EQUAL to its minimum is legal.
           cfg_hd <= to_unsigned(MIN, W); cfg_sua <= to_unsigned(MIN, W);
           cfg_suo <= to_unsigned(MIN, W); waitn(1);
           if cfg_error /= '0' then
               report "margin equal to the minimum was rejected" severity error; errs := errs + 1; end if;

           -- ILLEGAL: one cycle short must be refused outright.
           cfg_sua <= to_unsigned(MIN - 1, W); waitn(1);
           if cfg_error /= '1' then
               report "margin below the minimum was accepted" severity error; errs := errs + 1; end if;
           cmd_start <= '1'; waitn(1); cmd_start <= '0';
           waitn(6);
           if busy /= '0' or sda_drive_low /= '0' then
               report "sequencer emitted framing under an illegal configuration" severity error;
               errs := errs + 1; end if;
           if n_f /= 3 then
               report "illegal configuration still produced a framing edge" severity error;
               errs := errs + 1; end if;

           cfg_sua <= to_unsigned(SUA, W); cfg_hd <= to_unsigned(0, W); waitn(1);
           if cfg_error /= '1' then
               report "a zero margin was accepted" severity error; errs := errs + 1; end if;

           if bad_drive /= 0 then
               report "drive-intent output was not a clean 0/1" severity error; errs := errs + 1; end if;

           if errs = 0 then
               report "i2c_framing_sequencer self-check complete: tHD;STA=" &
                      integer'image(scl_fall(0) - f_cyc(0)) & " tSU;STA=" &
                      integer'image(f_cyc(1) - scl_rise(0)) & " tSU;STO=" &
                      integer'image(f_cyc(2) - scl_rise(1)) &
                      " measured on the bus; config legality enforced" severity note;
           else
               report "i2c_framing_sequencer self-check FAILED" severity error;
           end if;
           test_done <= '1';
           wait;
       end process;
   end architecture;

9a. Configuration Legality, and Why It Refuses

The check is three comparisons, and every detail of it is deliberate.

Azvya Education Pvt. Ltd.VLSI Mentor
ARCHITECTURAL PSEUDOCODE — the legality check, and the two decisions inside it
   cfg_error =  (cfg_hd_sta < T_HD_STA_MIN)
             || (cfg_su_sta < T_SU_STA_MIN)
             || (cfg_su_sto < T_SU_STO_MIN)

   DECISION 1 -- strict less-than, so a margin EQUAL to the minimum is LEGAL.
     That is what "minimum" means, and it is not a detail: the cycle counts in
     section 8 are the results of a CEILING, so the common case is a configured
     value sitting exactly ON the boundary. A check using <= would reject the
     correctly-converted value and accept only over-provisioned ones, which is
     a bug that presents as "our timing calculation is rejected by our hardware".
     Section 11 injects exactly this fault.

   DECISION 2 -- the minimums are PARAMETERS and the margins are REGISTERS.
     The minimum comes from the selected speed mode, which is a property of the
     system the controller is instantiated into. The margin comes from software.
     Software is allowed to choose how much margin to provision; it is not
     allowed to choose what the specification requires. Making one a parameter
     and the other a register puts that asymmetry in the type system.

   AND the consequence: the sequencer REFUSES. It does not clamp, and it does
   not warn and proceed.
     -- clamping silently produces different timing than software asked for,
        so software's model of the bus and the bus disagree, and nothing says so.
     -- proceeding produces a KNOWINGLY illegal bus, which is worse than a bus
        that does not start: a bus that does not start is diagnosed in minutes.
   Refusing turns a specification violation into an observable, attributable
   failure at the moment of configuration.

That last point is the general principle and it is worth stating plainly: hardware that can detect an illegal configuration should refuse it, not accommodate it. The alternative designs both convert a loud failure into a quiet one.

9b. What the Testbench Measures

The requirement is three durations, so the testbench measures three durations — on the observed bus, not on the sequencer's internal state.

Azvya Education Pvt. Ltd.VLSI Mentor
ARCHITECTURAL PSEUDOCODE — constructing the observed bus in the testbench
   // The DUT emits DRIVE INTENT. What a receiver sees is the resolved bus level:
   // one open-drain driver plus a pull-up, so a released line reads high because
   // nothing is pulling it down.
   wire scl_bus = ~scl_drive_low;
   wire sda_bus = ~sda_drive_low;

   // NO RC BEHAVIOUR IS MODELLED. This is a digital model and it is honest about
   // what it can prove:
   //   CAN prove -- edge ORDER, release timing in cycles, framing edge COUNT,
   //                which phase each edge falls in, configuration legality
   //   CANNOT prove -- analog rise time, threshold crossing, signal integrity,
   //                   that any interval met a specification in nanoseconds
   // Chapter 11.7 owns the electrical envelope; no digital simulation substitutes.

The three margins are configured to three different values — 4, 6 and 3 cycles — which is the single most important decision in the suite. §8 showed that the specified values are equal in some modes and unequal in others; a testbench using one value for all three could not distinguish a sequencer that read the right register from one that read the wrong one. With three distinct values, a swapped field is a measurement mismatch.

checkwhat it establishes
reset releases both linesa controller in reset must not jam the bus
exactly three framing edges from S, Sr, Pno accidental framing; §12's failures produce extra or missing edges
edge directions: fall, fall, riseS and Sr are falls, P is a rise
measured tHD;STA after S == 4the hold is counted, not skipped
measured tSU;STA before Sr == 6and it is the right register
measured tHD;STA after Sr == 4the hold applies to Sr too, not only to S
measured tSU;STO before P == 3
bus released and idle after the STOPthe sequence terminates cleanly
exactly three done pulsesone per event, no more
margin equal to the minimum acceptedthe boundary is legal — §9a decision 1
margin one below the minimum refused, and no framing emittedrefusal is real, not advisory
a zero margin refusedand the counter never underflows

The measurement subtlety, again. Every interval is measured between two edges detected the same way, so the one-cycle detection latencies cancel and the measured gap is exactly the configured count. Chapter 5.3 §7a made this point for a registered flag pair; here it applies to bus edges. Measuring between like-for-like observation points is what keeps the arithmetic clean.

Verified execution. All three languages, all three margins, identical results:

languagesimulatorresultmeasured marginscompletes at
SystemVerilogIcarus Verilog, -g2012PASStHD;STA=4, tSU;STA=6, tSU;STO=3380 ns
Verilog-2005Icarus Verilog, -g2005PASStHD;STA=4, tSU;STA=6, tSU;STO=3380 ns
VHDLnvc 1.23.0PASStHD;STA=4, tSU;STA=6, tSU;STO=3380 ns

i2c_framing_sequencer — a START with a counted hold

10 cycles
Ten internal clock cycles. A command start pulse is asserted in the second cycle. The SDA drive-low intent then asserts and the observed SDA bus line falls while the observed SCL line is still high, which is the START edge. Four cycles later the SCL drive-low intent asserts, the observed SCL line falls, and a done pulse is emitted.tHD;STA — 4 cycles measuredtHD;STA — 4 cycles measuredcmd_start acceptedcmd_start acceptedSTART edge on the busSTART edge on the busfirst SCL fall; donefirst SCL fall; doneclkcmd_startsda_drive_lowscl_drive_lowsda_bus1100000000scl_busdonet0t1t2t3t4t5t6t7t8t9
Figure 4 — the sequencer generating a START, from the simulation, with tHD;STA configured to 4 cycles. The command pulse is accepted, SDA is driven low on the next cycle while SCL is still released — that edge is the START — and SCL is only driven low four cycles later, when the hold count expires. The measurement band shows the interval actually produced on the observed bus. SIMULATION-DERIVED figure: the digital model of section 9b, with no RC behaviour.

10. Cross-Language Parity

SystemVerilogVerilog-2005VHDL
statestypedef enum logic [3:0]localparam constantstype state_t is (...)
margin inputslogic [CNT_W-1:0]wire [CNT_W-1:0]unsigned(CNT_W-1 downto 0)
counterlogic [CNT_W-1:0]reg [CNT_W-1:0]unsigned(CNT_W-1 downto 0)
zero testcnt != '0cnt != {CNT_W{1'b0}}cnt /= ZERO constant
legality checkalways_combcontinuous assignconditional signal assignment
minimum comparisonCNT_W'(T_..._MIN) castT_..._MIN[CNT_W-1:0] sliceto_unsigned(T_..._MIN, CNT_W)
busyalways_combassignconditional assignment

The comparison row is the only place where the three genuinely differ in substance rather than spelling, and it is worth a note. All three must compare a configured value against a minimum expressed as an integer, and all three have to get that comparison into a common width and a common signedness. SystemVerilog casts, Verilog slices, VHDL converts through numeric_std. The VHDL form is the most explicit and the Verilog form the most error-prone: a slice of an integer parameter that is wider than the target silently discards high bits, which for a minimum means the check can be satisfied by a value that should have failed. None of the three is wrong here; the VHDL version is the one where the tool would have complained if it were.

The VHDL testbench additionally initialises its configuration signals at declaration rather than in the stimulus process. Without that, the margins are uninitialised at time zero, the legality comparison operates on metavalues, and numeric_std emits a metavalue warning before any stimulus runs — a real diagnostic about a real modelling gap, not noise to be suppressed. Initialising at declaration is the fix, and the underlying point generalises: a configuration register with no reset value is a configuration register with no defined behaviour, which §15 returns to.

11. Mutation Testing

Seven faults injected into the verified RTL, with the testbench run against each.

mutationwhat it breaksresult
tHD;STA counter loaded without the -1one cycle too long — legal, slowFAIL — measured 5, required 4
tHD;STA counter loaded one shortone cycle too short — illegalFAIL — measured 3, required 4
tSU;STA reads the tSU;STO registerswapped fieldsFAIL — measured 3, required 6
minimum check uses <= instead of <rejects a margin equal to the minimumFAIL — boundary config rejected
framing emitted despite cfg_errorknowingly illegal busFAIL — framing emitted under illegal config
reset drives both lines lowa controller in reset jams the busFAIL — reset did not release
the tHD;STA hold is skipped entirelyChapter 5.2's collapsed STARTFAIL — measured 1, required 4

All seven caught. Module 5 mutation total: 22 faults injected across four designs, 22 caught.

Three observations that are about verification rather than about these particular faults.

The swapped-field mutation is the one that justifies three distinct values. Reading cfg_su_sto where cfg_su_sta belongs is not a typo a reviewer reliably catches — the identifiers differ by two characters and appear in structurally identical lines. With all three margins configured to the same number it is undetectable by any measurement, because every measurement still returns the expected value. The fault becomes visible only because the suite chose 4, 6 and 3. That is a testbench design decision doing work that no amount of additional stimulus could do.

The <= mutation is a fault whose symptom points away from itself. It makes the hardware reject a correctly-converted configuration. The engineer sees "hardware says my timing is illegal", checks the timing calculation, finds it correct, and concludes the calculation is being misread — when the comparison in the hardware is what is wrong. It is caught here only because the suite deliberately tests the boundary value rather than a comfortable one, which §9a decision 1 anticipated.

Two mutations differ only in direction and matter differently. Loading without the -1 produces a margin one cycle too long, which is legal and merely slow; loading one short produces an illegal bus. The testbench checks equality and catches both. A suite checking only measured >= required would arguably be the more faithful specification check and would pass the first — accepting, along with it, a design that had drifted arbitrarily long. On a block whose purpose is a configured interval, equality is the right check and the specification check belongs at the pin level.

12. Malformed Framing — The Catalogue

This is what the margins exist to prevent, and it is the part of the chapter that pays off on a bench.

The organising insight is that malformed framing rarely looks like a framing error. Framing is a bracket, and a broken bracket presents as a problem with the contents. Every row below is a shape whose reported symptom points somewhere other than at the framing.

#malformed shapewhat it looks like on a capturelikely causewho can detect it
1No START at all — SDA and SCL fall togethera plausible transfer; many decoders resynchronise on SCL and display address and datathe two edges issued in one clocked decisiona detector written from the specification sentence reports zero STARTs
2Short tHD;STA — START present, first clock too earlyframing present and correctly shaped; address clean; no ACKhold state omitted or its count too smallpin-level measurement of the START-to-SCL-fall interval
3Premature START — inside the bus-free intervala clean transfer; a short STOP-to-START gap if measured"both lines high" tested as a level instead of a statemeasure STOP-to-next-START; Chapter 5.3's tracker
4STOP where an Sr was intendedtransfer ends, then a new transfer begins; register pointer lostSDA released while SCL was high during the Sr sequenceclassifier reports P where the driver expected Sr
5No Sr at all — SDA never released firstthe clock continues; no framing edge; bytes run togetherthe Sr sequence started from SDA already lowframing-edge count is one short
6Short tSU;STAcombined transfers fail; simple transfers fineone count shared across three margins in Standard-mode (§8)pin-level measurement of SCL-rise-to-Sr-fall
7Short tSU;STO — or a missed STOPthe bus appears still busy; the next transfer misbehavesSTOP setup state omitted or too shortframing-edge count; targets never returning to idle
8Stray STOP — no preceding STARTone framing rise with no matching fallreset or abort mid-transfer releasing SDA while SCL is highclassifier reports P with bus_busy already clear
9Accidental framing from a data violationthe transfer silently reframes mid-bytea transmitter changed SDA during SCL high — Chapter 4.2's violation4.2's stability checker and a framing-edge count
10Framing edge lost to a slow riseintermittent, temperature- and board-dependentpull-up too weak for the bus capacitanceanalog measurement only — no digital simulation sees this

Four structural remarks about that table.

Rows 1, 5 and 7 are edge-count failures, and a count is the cheapest detector for all three. In each, the intended framing event simply does not exist on the bus. A testbench or monitor that asserts "exactly three framing edges, in the order fall, fall, rise" catches every one of them without measuring a single interval — which is why §9b's suite checks the count and the directions before it checks any duration.

Rows 2, 6 and 7 are interval failures and require pin-level real-time measurement. The framing is present and correctly shaped, so no count and no classifier finds them. Only measuring the interval in nanoseconds against the specification limit does, and that is §13.

Row 4 is the only one where the bus is entirely legal and the intent was wrong. Every edge is well formed; the controller simply emitted a different legal event than the one it meant to. No framing checker can catch this, because nothing is malformed — the detection has to come from higher up, where the expected transaction shape is known.

Row 10 is outside what any of this module's tooling can see. All the RTL here is digital and sampled; §9b said so explicitly. An edge that fails to cross a threshold in time is an analog failure and Chapter 11.7 owns it.

13. Verification — The Framing Timing Checker

§12 rows 2, 6 and 7 need something none of this module's designs provide: measurement in real time, against asynchronous pin edges, compared to limits in microseconds.

Chapter 5.3 §10 separated cycle-based from real-time checking. Framing margins are where the distinction becomes unavoidable, because two of the three margins have release edges as anchors — and a release's arrival time is determined by the board, not by the controller's counter. A cycle-based check on the sequencer confirms it counted correctly and is structurally blind to the interval a receiver actually saw.

Azvya Education Pvt. Ltd.VLSI Mentor
UVM CONCEPT — VERIFICATION ONLY. Timestamp-based framing margin checks.
   // Real-time observations of the pins. Not sampled on an internal clock,
   // because the specification's microseconds are a statement about the bus.
   timeunit      1ns;
   timeprecision 1ps;

   // Limits come from the configured speed mode, never hard-coded -- a checker
   // with one mode's numbers baked in is wrong on every other mode.
   class i2c_framing_timing_cfg extends uvm_object;
       `uvm_object_utils(i2c_framing_timing_cfg)
       time t_hd_sta_min;      // START/Sr edge  -> first SCL fall
       time t_su_sta_min;      // SCL rise       -> Sr edge
       time t_su_sto_min;      // SCL rise       -> STOP edge
       string mode_name;       // reported in every violation message
       function new(string name = "i2c_framing_timing_cfg"); super.new(name); endfunction
   endclass

   // One measurement = one object. Carrying the anchors alongside the result is
   // what makes a violation report diagnosable rather than merely alarming.
   class i2c_timing_observation extends uvm_object;
       `uvm_object_utils(i2c_timing_observation)
       string param_name;      // "tHD;STA"
       time   t_start_anchor;  // WHEN the interval opened
       time   t_end_anchor;    // WHEN it closed
       time   measured;        // the difference
       time   required_min;
       bit    passed;
       function new(string name = "i2c_timing_observation"); super.new(name); endfunction
   endclass

   // The checker times the anchors. Note it watches the PINS: scl_bus and
   // sda_bus are the resolved bus levels, which is what every other device sees.
   time t_scl_rise, t_scl_fall, t_framing_fall, t_framing_rise;

   always @(posedge scl_bus) t_scl_rise = $time;

   // A framing fall is S or Sr. tHD;STA runs from here to the next SCL fall.
   always @(negedge sda_bus) if (scl_bus) begin
       t_framing_fall = $time;
       // If this fall followed an SCL rise in the same high phase, it is an Sr
       // and tSU;STA applies to the interval that just closed.
       if (t_scl_rise > t_scl_fall)
           check_margin("tSU;STA", t_scl_rise, t_framing_fall, cfg.t_su_sta_min);
   end

   always @(negedge scl_bus) begin
       t_scl_fall = $time;
       if (t_framing_fall != 0)
           check_margin("tHD;STA", t_framing_fall, t_scl_fall, cfg.t_hd_sta_min);
   end

   always @(posedge sda_bus) if (scl_bus)
       check_margin("tSU;STO", t_scl_rise, $time, cfg.t_su_sto_min);

   function void check_margin(string p, time t0, time t1, time lim);
       i2c_timing_observation o = i2c_timing_observation::type_id::create("o");
       o.param_name = p;  o.t_start_anchor = t0;  o.t_end_anchor = t1;
       o.measured = t1 - t0;  o.required_min = lim;  o.passed = (o.measured >= lim);
       analysis_port.write(o);          // observations always published, pass or fail
       if (!o.passed)
           `uvm_error("I2C_FRAMING_TIMING", $sformatf(
               "%s violation in %s: measured %0t, required minimum %0t (anchors: %0t -> %0t)",
               p, cfg.mode_name, o.measured, lim, t0, t1))
   endfunction

Four things in that sketch are the lesson, and they matter more than the syntax.

The limits live in a configuration object. A checker with one mode's numbers compiled in is wrong on every other mode, and worse, it is confidently wrong. Making the limits configuration makes the mode a declared property of the test.

A measurement is an object carrying its anchors. "tHD;STA violation" tells an engineer nothing. "tHD;STA violation in Standard-mode: measured 3200ns, required minimum 4000ns (anchors: 148200ns -> 151400ns)" tells them the parameter, the mode, the shortfall, and exactly where in the capture to look. The anchors are the difference between a report that starts a debug session and one that starts an argument.

Observations are published whether they pass or fail. A checker that only speaks on failure cannot answer "how much margin do we actually have?" — which is the question that distinguishes a design that is comfortably legal from one that is legal by 4.2 ns and will not survive a temperature corner.

Every anchor is a pin edge. None of this reads the DUT's internal state, which is what makes it able to catch the class of fault a cycle-based check cannot: a correct counter counting a wrong number, or a released edge that arrived later than the controller assumed.

13a. Coverage Worth Collecting

Boundary coverage on a minimum is the whole point, because minimums fail at the boundary and nowhere else.

Azvya Education Pvt. Ltd.VLSI Mentor
UVM CONCEPT — VERIFICATION ONLY. Boundary coverage on a minimum.
   covergroup framing_margin_cg (time required_min) with function sample (time measured);
       option.per_instance = 1;
       // The bins that matter are AT and just EITHER SIDE of the limit. A suite
       // that only ever produced comfortable margins has verified that a legal
       // design is legal, which was not in doubt.
       cp : coverpoint measured {
           bins below      = { [0                  : required_min - 2] };
           bins one_short  = { required_min - 1 };            // must be REJECTED
           bins exactly    = { required_min     };            // must be ACCEPTED
           bins one_over   = { required_min + 1 };
           bins comfortable= { [required_min + 2 : required_min * 4] };
       }
   endgroup

one_short and exactly are the two bins with any diagnostic value. They are also the two that a randomly-generated stimulus will essentially never hit, which is the standard argument for directing stimulus at boundaries rather than hoping to reach them — and §11's <= mutation is caught by the exactly bin and by nothing else.

14. FPGA and ASIC Implications

Counter width is set by the slowest mode at the fastest clock, and getting it wrong wraps silently. Standard-mode tSU;STA is 4.7 µs; at 100 MHz that is 470 cycles, needing nine bits. A design sized for Fast-mode — 0.6 µs, 60 cycles, six bits — and later configured for Standard-mode cannot hold the count. It does not fail loudly: the counter wraps, and produces a margin short by a multiple of its range. The VHDL version's unsigned fields and the range-checked counter in Chapter 5.3's tracker are the kind of construct that converts this into a diagnosable error; in Verilog it is a review obligation.

Three registers, not one. §8 showed the values are equal in Fast-mode and unequal in Standard-mode. Hardware that shares one count across the three margins works until someone selects Standard-mode, and then is short on repeated-START setup only. Sharing the register is a false economy measured in flip-flops.

Configuration registers need reset values that are legal. §10 noted the VHDL testbench had to initialise its margins at declaration to avoid metavalue comparisons. The hardware version of that problem is a peripheral whose timing registers reset to zero: cfg_error is asserted out of reset, which is correct and safe — the sequencer refuses — but only if software is required to program the registers before use and the refusal is visible to it. A reset default of zero with a cfg_error nobody reads is a peripheral that silently does nothing.

The margins are counted in the controller's clock domain, and that ties them to its frequency. Nothing in the design notices a clock change; a cycle count does not know its own duration. Chapter 5.3 §11 made this point for the bus-free interval and it applies to all three margins here: compute the counts at elaboration from a frequency constant that lives with the clock declaration, rather than writing numbers into the instantiation.

On an ASIC, two of the three margins are shared with the board. §3 and §4 established that tSU;STA and tSU;STO have release edges as anchors, so the interval a receiver observes is shorter than the interval the controller counted by whatever the rise time consumed. The controller cannot measure this and cannot compensate for it; it can only be provisioned with margin. That provisioning is a system-level budget and it is Chapter 11.9.

Pad and synchroniser delay sit outside the counted interval on the observation side. A controller measuring its own framing through its own input path sees every edge late by the synchroniser depth. That does not affect the margins it generates, but it does affect any attempt to close the loop by checking its own output — one more reason framing timing verification belongs at the pins.

15. Debugging — The Combined Transfer That Only Failed on Slow Buses

A controller that passed timing review with one register too few

Pitfall — one timing register shared across three margins whose specified values are not always equal
Buggy Code
// A controller peripheral exposes its framing timing to firmware. The designer
// reads the specification table, notices that in Fast-mode all three framing
// margins are 0.6 us, and economises:
//
//   // "all three framing margins are the same value -- one register is enough"
//   reg [8:0] cfg_framing_margin;        // used for tHD;STA, tSU;STA AND tSU;STO
//
//   ... cnt <= cfg_framing_margin - 1;   // in the START hold state
//   ... cnt <= cfg_framing_margin - 1;   // in the repeated-START setup state
//   ... cnt <= cfg_framing_margin - 1;   // in the STOP setup state
//
// The driver computes the value correctly, with a correct ceiling:
//
//   margin_cycles = DIV_CEIL(600 /*ns*/ * FCLK_MHZ, 1000);   // Fast-mode
//
// Everything about this is right in Fast-mode. The conversion rounds up, the
// counter semantics are correct, the register is wide enough, and the three
// margins genuinely are 0.6 us -- so all three ARE equal and one register IS
// enough. It is reviewed against the Fast-mode column and it is correct.
//
// The Standard-mode support was added later, by changing the driver's constant
// from 600 to 4000 ns, which is also correct -- for two of the three margins.
Symptom

Fast-mode: flawless, on every board, for a year.

Standard-mode: sensor reads fail intermittently. Sensor WRITES are flawless, and so is every simple single-phase transfer. Only the write-then-read accesses fail, and only some of them, and the rate varies between boards built from the same design.

Every initial hypothesis is about the sensor. The driver is re-read, the register pointer logic is re-checked, the part is swapped, a different vendor's equivalent part shows the same behaviour -- which finally rules out the device and points back at the bus.

A capture of a failing access is maddening. START, address+W, ACK, pointer byte, ACK, then the repeated START, then address+R -- and no ACK. The repeated START is VISIBLE on the capture and correctly shaped: SDA clearly falls while SCL is clearly high. A decoder displays it as a repeated START. Nothing is missing and nothing is malformed.

And the failure rate tracks Standard-mode only, which nobody connects to a framing margin because framing "obviously" does not depend on speed mode -- the edges are the same edges.

Root Cause

In Standard-mode the three framing margins are NOT equal. From the specification table: tHD;STA is 4.0 us, tSU;STO is 4.0 us, and tSU;STA is 4.7 us.

One register cannot hold two values. Programmed with the 4.0 us conversion, the repeated-START setup interval was 4.0 us where 4.7 us was required -- 0.7 us short, which at a 48 MHz internal clock is 34 cycles of a 226-cycle requirement, about 15% short.

That is why the symptom is so narrow. The shortfall affects EXACTLY ONE of the three margins, and that margin is used by EXACTLY ONE framing event -- the repeated START -- which appears in EXACTLY ONE transfer shape: the combined write-then-read. Simple writes never touch it. Simple reads never touch it. Fast-mode never touches it, because there the three values really are equal and the shared register really is sufficient.

And it is why the capture looks clean. A 4.0 us setup is not a malformed waveform. It is a well-formed repeated START with a legal shape and an illegal interval, and section 12 row 6 is exactly this: an INTERVAL failure, invisible to any count, any classifier and any decoder. Only measuring the SCL-rise-to-SDA-fall interval against the Standard-mode limit reveals it -- and measuring it requires knowing that the limit for that one parameter is different from the other two.

The board-to-board variation has a compounding cause. tSU;STA's start anchor is SCL BEING RELEASED (section 3), so the interval a target actually observes is shorter than the 4.0 us the controller counted, by however long the rise took. A board with weaker pull-ups or more capacitance eats further into an already short margin, which is why the same firmware fails at different rates on electrically different boards -- and why it was mistaken for an analog problem.

The review missed it because the review question was "is the conversion correct?" and the conversion WAS correct. The unasked question was "is one register enough?"

Fix
// Three registers, because there are three independent requirements:
//
//   reg [8:0] cfg_hd_sta;    // tHD;STA
//   reg [8:0] cfg_su_sta;    // tSU;STA   <-- 4.7 us in Standard-mode, NOT 4.0
//   reg [8:0] cfg_su_sto;    // tSU;STO
//
// and per-mode minimums checked in hardware, so that a driver programming the
// wrong value is REFUSED rather than obeyed -- section 9a. The sequencer in
// section 9 is this design: three fields, three minimums, and cfg_error gating
// every command.
//
// The verification changes are the ones that generalise.
//
//   1. CONFIGURE THE THREE MARGINS TO THREE DIFFERENT VALUES. Section 9b uses
//      4, 6 and 3. With one shared value this entire bug class is invisible to
//      measurement, because every measurement returns the expected number.
//      Section 11's swapped-field mutation makes the same point: distinct values
//      are what turn a wiring error into a measurable mismatch.
//
//   2. TEST EVERY SPEED MODE AGAINST ITS OWN LIMITS, not against one set. The
//      bug is a Standard-mode-only bug in a design verified in Fast-mode. A
//      checker whose limits come from a configuration object (section 13) makes
//      the mode an explicit property of the test rather than an assumption
//      baked into the checker.
//
//   3. MEASURE AT THE PINS. The controller's internal counter reached its
//      programmed value correctly, every time. Only a real-time measurement of
//      the observed SCL rise to the observed SDA fall shows a 4.0 us interval
//      against a 4.7 us requirement -- and only a pin-level measurement also
//      captures the rise time that made it worse.
//
//   4. PUBLISH MARGIN, NOT JUST VIOLATIONS. Section 13's checker writes every
//      observation to an analysis port, pass or fail. Had that existed, the
//      Fast-mode sign-off report would have shown tSU;STA passing with a large
//      margin while the other two passed with a different one -- a visible
//      asymmetry, a year before it mattered.
//
// The habit that prevents the whole class: when several requirements happen to
// share a value, ask whether they share it in EVERY configuration, or only in
// the one on the screen. A table read column-by-column tells you. A table
// summarised as "the framing margins are all 0.6 us" does not.

16. Common Misconceptions

"Framing margins do not depend on speed mode, because the edges are the same edges." The edges are the same; the minimum durations are not. §15 is an entire failure built on this sentence.

"The three framing margins are the same value, so one register is enough." They are equal in Fast-mode and Fast-mode Plus and unequal in Standard-mode, where tSU;STA is 4.7 µs against 4.0 µs for the other two.

"tSU;STA is the setup before any START." It is worded for a repeated START, and that is the only case it governs. A first START comes out of a free bus and is gated by the bus-free interval instead — a different parameter measured between different anchors.

"tSU;STA and tBUF both gate a START, so they are interchangeable." One is measured within a single controller's own deliberate sequence on a bus it already owns; the other is measured between two framing events across a bus that belonged to nobody in between. §7 separates them.

"A framing edge that is visible on a capture is legal." Visibility establishes shape. Rows 2, 6 and 7 of §12 are all well-formed framing edges with illegal intervals, and no decoder, count, or classifier detects any of them.

"If the controller's counter reached the programmed value, the margin was met." For tSU;STA and tSU;STO the interval opens on a released edge, so the interval a receiver observes is shorter than the one the controller counted by whatever the rise consumed. The counter is not the bus.

"A margin equal to the minimum is risky, so hardware should reject it." A minimum is inclusive, and because cycle counts come from a ceiling, the exactly-on-the-boundary case is the common case. Hardware that rejects it rejects correctly converted configurations — §11's <= mutation.

"Hardware should clamp an illegal configuration to a legal value." Clamping makes software's model of the bus and the actual bus disagree with nothing reporting it. Refusing turns the violation into an attributable failure at configuration time.

"Malformed framing shows up as a framing error." It almost never does. Framing is a bracket, so a broken bracket is reported as a problem with the contents — a missing ACK, a lost register pointer, a queue that never drains.

17. Reason It Through

A controller supports Standard-mode and Fast-mode and has one framing_margin register. In which mode and on which transfer shape does it fail, and why does everything else pass?

Standard-mode, on transfers that use a repeated START. In Fast-mode all three framing minimums are 0.6 µs, so one register genuinely suffices. In Standard-mode tSU;STA is 4.7 µs while tHD;STA and tSU;STO are 4.0 µs, so a single register programmed for 4.0 µs is 0.7 µs short on repeated-START setup alone. Simple reads and writes never use a repeated START, so they never touch the short margin — which is why the failure looks like a device problem with one access pattern rather than a timing problem.

Why is tHD;STA entirely the controller's to own, while tSU;STO is shared with the board?

Because of what kind of edges the anchors are. tHD;STA runs from a driven SDA fall to a driven SCL fall — both actively pulled low by the controller, both fast, neither dependent on the pull-up network. tSU;STO runs from SCL being released to SDA being released, and a released line rises at a rate the pull-up resistance and the bus capacitance determine. The controller decides when to stop pulling; the board decides when the line arrives.

Your hardware rejects a configuration your driver computed with a correct ceiling. Where is the bug most likely to be?

In the hardware's comparison, using <= where < belongs. A ceiling conversion lands exactly on the boundary whenever the division is inexact, so the correctly-converted value is frequently the minimum itself — and a minimum is inclusive. The symptom points at the calculation, which is why this fault is worth a dedicated boundary test rather than a comfortable one.

Three framing edges are expected from a sequence of S, Sr, P. A test observes two. Which failures in §12 are consistent with that, and which are excluded?

Consistent: no Sr at all (row 5, because SDA was never released so no falling edge occurred), a missed STOP (row 7), and no START at all (row 1). Excluded: every interval failure — rows 2, 6 and 7's short-but-present variants — because those produce the correct number of correctly-shaped edges and differ only in duration. An edge count is a cheap and complete detector for one family and blind to the other, which is why §9b checks both count and intervals.

Why does the testbench configure the three margins to 4, 6 and 3 rather than to one value?

So that reading the wrong register is detectable. With all three equal, a sequencer that loaded cfg_su_sto where cfg_su_sta belonged would still measure correctly on every margin, because every expected value is the same number — the fault is invisible to measurement no matter how much stimulus is applied. Distinct values turn a wiring error into a measurement mismatch, and §11 confirms it: the swapped-field mutation is caught by exactly this choice.

A cycle-based check confirms your sequencer produced 226 cycles of repeated-START setup. What have you not established?

Two things. That 226 was the right number for the selected mode and clock frequency — a cycle count compared against a cycle count cannot notice a wrong conversion. And that a receiver observed 226 cycles' worth of setup, because the interval opens on a released SCL edge and the rise time is inside the budget. Both gaps need a real-time measurement at the pins against a limit in microseconds.

Why is the exactly bin of §13a's covergroup more valuable than the comfortable bin?

Because minimums fail at the boundary and nowhere else. A suite that produces only comfortable margins verifies that a legal design is legal, which was not in doubt; it cannot distinguish an inclusive comparison from an exclusive one, or catch an off-by-one in the conversion. The boundary bins are also the ones random stimulus will effectively never reach, which is the argument for directing stimulus at them.

18. Understanding Check

19. Summary

Three margins govern framing, all of them minimums: tHD;STA from the START or Sr edge to the first SCL fall, tSU;STA from SCL's rise to the Sr edge, and tSU;STO from SCL's rise to the STOP edge.

They exist so a framing claim is held long enough to be heard — by sampled observers, unambiguously attributable to one clock phase, and separated rather than merely ordered.

Ownership differs across the three. tHD;STA has two driven anchors and belongs entirely to the controller. tSU;STA and tSU;STO open on released edges, so the board's rise time is inside their budgets and the controller's counter is not the interval a receiver sees.

The values are not independent. tHD;STA = tSU;STO = tHIGH in every mode, and tSU;STA joins them in Fast and Fast-mode Plus — but not in Standard-mode, where it is 4.7 µs against 4.0 µs. That single exception is §15's entire failure.

Convert by rounding up, and convert each margin separately. A conveniently exact clock frequency hides the rounding question, and equal specification values in one mode do not license one shared register.

Programmable timing needs a hardware legality check that refuses. Strict less-than, so the boundary stays legal; minimums from the mode as parameters, margins from software as registers; and refusal rather than clamping, so a violation is attributable at configuration time.

Configure the three margins to three different values in verification. With one shared value, reading the wrong register is invisible to every possible measurement.

Malformed framing does not present as a framing error. §12's ten shapes report as missing acknowledgements, lost register pointers, and buses that never return to idle. Edge counts catch the missing-event family cheaply; only pin-level real-time measurement catches the short-interval family.

Module 5 totals: 4 tri-HDL designs, 12 testbench runs across three simulators, 22 mutations injected and 22 caught.

20. What Comes Next

Module 5 is complete. Every transfer on this bus now has a defined beginning, a defined end, a way to change phase without letting go, margins that make each of those events unambiguous, and a catalogue of what their absence looks like on a bench.

What the bus still has no account of is who a transfer is for. Four chapters have referred to "the address byte that follows the START" without once saying how a device recognises its own address, how the direction of the transfer is encoded, what happens when two devices answer to the same address, or how many addresses a board actually has available. Module 6 — Addressing supplies all of it, and it builds directly on this module: the reason an address byte can be recognised at all is that the framing told every device exactly where the byte began.

The numeric parameters this module kept pointing at — the clock envelope, the data window, the transmitter-valid delays, the electrical envelope, spike suppression, and the complete budget that combines them — are Module 11.

Browse the full path on the I²C tutorials index. For the events these margins constrain, see The START Condition, The STOP Condition and Repeated START.

Continue learning