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 | |
|---|---|
| start | SDA's high-to-low transition, while SCL is high — the framing edge itself |
| end | SCL'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 cycles3. 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 | |
|---|---|
| start | SCL's rising edge — the beginning of the high phase the framing edge will sit in |
| end | SDA'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 cycles4. 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 | |
|---|---|
| start | SCL's rising edge — the controller releasing the clock for the last time |
| end | SDA'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 cycles5. 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.
| symbol | parameter, as the specification words it | Standard-mode | Fast-mode | Fast-mode Plus | unit |
|---|---|---|---|---|---|
tHD;STA | hold time (repeated) START condition | 4.0 | 0.6 | 0.26 | µs |
tSU;STA | set-up time for a repeated START condition | 4.7 | 0.6 | 0.26 | µs |
tSU;STO | set-up time for STOP condition | 4.0 | 0.6 | 0.26 | µs |
Two neighbours are worth listing alongside them, because §6 needs them and because they are the parameters most often confused with these:
| symbol | parameter | Standard-mode | Fast-mode | Fast-mode Plus | unit |
|---|---|---|---|---|---|
tBUF | bus free time between a STOP and START condition | 4.7 | 1.3 | 0.5 | µs |
tLOW | LOW period of the SCL clock | 4.7 | 1.3 | 0.5 | µs |
tHIGH | HIGH period of the SCL clock | 4.0 | 0.6 | 0.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.
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 = 226Three 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.
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 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 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 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 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; 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.
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.
// 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.
| check | what it establishes |
|---|---|
| reset releases both lines | a controller in reset must not jam the bus |
| exactly three framing edges from S, Sr, P | no accidental framing; §12's failures produce extra or missing edges |
| edge directions: fall, fall, rise | S and Sr are falls, P is a rise |
measured tHD;STA after S == 4 | the hold is counted, not skipped |
measured tSU;STA before Sr == 6 | and it is the right register |
measured tHD;STA after Sr == 4 | the hold applies to Sr too, not only to S |
measured tSU;STO before P == 3 | |
| bus released and idle after the STOP | the sequence terminates cleanly |
exactly three done pulses | one per event, no more |
| margin equal to the minimum accepted | the boundary is legal — §9a decision 1 |
| margin one below the minimum refused, and no framing emitted | refusal is real, not advisory |
| a zero margin refused | and 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:
| language | simulator | result | measured margins | completes at |
|---|---|---|---|---|
| SystemVerilog | Icarus Verilog, -g2012 | PASS | tHD;STA=4, tSU;STA=6, tSU;STO=3 | 380 ns |
| Verilog-2005 | Icarus Verilog, -g2005 | PASS | tHD;STA=4, tSU;STA=6, tSU;STO=3 | 380 ns |
| VHDL | nvc 1.23.0 | PASS | tHD;STA=4, tSU;STA=6, tSU;STO=3 | 380 ns |
i2c_framing_sequencer — a START with a counted hold
10 cycles10. Cross-Language Parity
| SystemVerilog | Verilog-2005 | VHDL | |
|---|---|---|---|
| states | typedef enum logic [3:0] | localparam constants | type state_t is (...) |
| margin inputs | logic [CNT_W-1:0] | wire [CNT_W-1:0] | unsigned(CNT_W-1 downto 0) |
| counter | logic [CNT_W-1:0] | reg [CNT_W-1:0] | unsigned(CNT_W-1 downto 0) |
| zero test | cnt != '0 | cnt != {CNT_W{1'b0}} | cnt /= ZERO constant |
| legality check | always_comb | continuous assign | conditional signal assignment |
| minimum comparison | CNT_W'(T_..._MIN) cast | T_..._MIN[CNT_W-1:0] slice | to_unsigned(T_..._MIN, CNT_W) |
busy | always_comb | assign | conditional 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.
| mutation | what it breaks | result |
|---|---|---|
tHD;STA counter loaded without the -1 | one cycle too long — legal, slow | FAIL — measured 5, required 4 |
tHD;STA counter loaded one short | one cycle too short — illegal | FAIL — measured 3, required 4 |
tSU;STA reads the tSU;STO register | swapped fields | FAIL — measured 3, required 6 |
minimum check uses <= instead of < | rejects a margin equal to the minimum | FAIL — boundary config rejected |
framing emitted despite cfg_error | knowingly illegal bus | FAIL — framing emitted under illegal config |
| reset drives both lines low | a controller in reset jams the bus | FAIL — reset did not release |
the tHD;STA hold is skipped entirely | Chapter 5.2's collapsed START | FAIL — 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 shape | what it looks like on a capture | likely cause | who can detect it |
|---|---|---|---|---|
| 1 | No START at all — SDA and SCL fall together | a plausible transfer; many decoders resynchronise on SCL and display address and data | the two edges issued in one clocked decision | a detector written from the specification sentence reports zero STARTs |
| 2 | Short tHD;STA — START present, first clock too early | framing present and correctly shaped; address clean; no ACK | hold state omitted or its count too small | pin-level measurement of the START-to-SCL-fall interval |
| 3 | Premature START — inside the bus-free interval | a clean transfer; a short STOP-to-START gap if measured | "both lines high" tested as a level instead of a state | measure STOP-to-next-START; Chapter 5.3's tracker |
| 4 | STOP where an Sr was intended | transfer ends, then a new transfer begins; register pointer lost | SDA released while SCL was high during the Sr sequence | classifier reports P where the driver expected Sr |
| 5 | No Sr at all — SDA never released first | the clock continues; no framing edge; bytes run together | the Sr sequence started from SDA already low | framing-edge count is one short |
| 6 | Short tSU;STA | combined transfers fail; simple transfers fine | one count shared across three margins in Standard-mode (§8) | pin-level measurement of SCL-rise-to-Sr-fall |
| 7 | Short tSU;STO — or a missed STOP | the bus appears still busy; the next transfer misbehaves | STOP setup state omitted or too short | framing-edge count; targets never returning to idle |
| 8 | Stray STOP — no preceding START | one framing rise with no matching fall | reset or abort mid-transfer releasing SDA while SCL is high | classifier reports P with bus_busy already clear |
| 9 | Accidental framing from a data violation | the transfer silently reframes mid-byte | a transmitter changed SDA during SCL high — Chapter 4.2's violation | 4.2's stability checker and a framing-edge count |
| 10 | Framing edge lost to a slow rise | intermittent, temperature- and board-dependent | pull-up too weak for the bus capacitance | analog 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.
// 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))
endfunctionFour 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.
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] };
}
endgroupone_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
// 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.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.
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?"
// 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
Related tutorials
- Related topic
Repeated START — Holding the Bus Between Phases
A repeated START is not a new waveform. It is the START edge again, and what makes it a different event is that the bus was already busy. That single fact is why a classifier needs state and why a monitor that joins late cannot classify what it sees.
- Related topic
The Address Byte — Seven Address Bits and the R/W Bit
The first byte after a START is not an address followed by a direction bit. It is one eight-bit field that the bus, the slave and the datasheet all treat as a unit — and treating it as two things is the single most common source of I²C address confusion.
- Related topic
I²C Byte and Bit Transmission — MSB First, Nine Clocks
Every byte on this bus costs nine clock pulses, not eight. Eight carry data most-significant-bit first, and the ninth belongs to somebody else — which makes the transmitter's most important job in that slot to stop driving.
- Related topic
The Data-Valid Rule — SDA Stable While SCL Is High
One sentence governs every bit on an I²C bus, and it is derived rather than decreed: the receiver needs a settled value at the instant it looks. What falls out is that an SDA edge while SCL is HIGH cannot be data — which is why the bus reserves it for framing.
