I²C · Module 21
Sequencer and Sequences — Write, Read and Combined Transactions
Why a sequence describes a situation and never an expectation, why reading a register is two phases in one bus occupancy, and why the objection belongs in the base class. Includes a read test that only ever worked on a single-master bus.
A sequence library is the part of an environment that gets copied between projects most often and reviewed least. It is also where one rule, broken, makes the whole library unusable elsewhere.
1. A Sequence Describes a Situation, Never an Expectation
The rule:
A sequence chooses what happens. It never says what should come back.
A sequence containing an expected value has to be edited when the device changes, and a library that needs per-device editing is not a library. Every expectation in this environment lives in the predictor, transcribed from the datasheet, in one place.
This is easy to state and easy to violate accidentally, because the natural way to write a read test is:
// DO NOT DO THIS
task body();
do_read(7'h50, 8'h02, got);
if (got != 8'h5A) `uvm_error("SEQ", "wrong data") // an expectation, in stimulus
endtaskThat sequence now encodes a fact about register 2 of one part. Point the environment at a device whose register 2 holds something else and the sequence fails on correct behaviour — so the sequence gets edited, and the edit is a fork.
The same test, written correctly, contains no 0x5A anywhere: the sequence drives the read, and the scoreboard compares the observed byte against what the contract says the selected register holds.
2. Reading a Register Is Two Phases, Not One Transfer
The most important sequence in the library, and the one a naive library omits.
Reading register 2 of an I²C device is not one transfer. It is a write that sets the pointer, followed by a read — and the two must be joined by a repeated START, so the bus is never released in between.
// Phase 1: set the pointer, and KEEP THE BUS.
if (!wr.randomize() with {
addr == local::addr;
read == 1'b0;
n_bytes == 1;
restart_first == 1'b0;
hold_bus == 1'b1; // <-- no STOP
})
...
// Phase 2: change direction without releasing the bus.
if (!rd.randomize() with {
read == 1'b1;
restart_first == 1'b1; // <-- a GENUINE repeated START
})3. The Objection Belongs in the Base Class
virtual class i2c_base_seq extends uvm_sequence #(i2c_seq_item);
task pre_body();
if (starting_phase != null)
starting_phase.raise_objection(this, {get_type_name(), " running"});
endtask
task post_body();
if (starting_phase != null)
starting_phase.drop_objection(this, {get_type_name(), " done"});
endtask
endclassWithout an objection the run phase can end while stimulus is still outstanding. The report says PASS, the last transfer never completed, and the scoreboard never saw it.
It is in the base class because it is the single most commonly omitted pair of lines in a sequence library, and inheriting it is more reliable than remembering it. Note that the base class is virtual and therefore deliberately not registered with the factory — you cannot create an abstract class, and a registration macro on one generates a create that will not compile. Chapter 21.11's linter originally reported this file for the missing macro, which was the checker being wrong rather than the code.
4. Check the Return Value of randomize()
Every randomize() call in this library is inside an if with a fatal on failure.
An over-constrained item does not throw. randomize() returns 0 and leaves every field at its previous value — so a sequence that ignores the result drives whatever was in the object before, silently, and the resulting traffic is legal-looking and wrong. On the first item of a sequence those values are the defaults, which makes the failure look like a sequence that only ever drives address zero.
5. The Sequence Library
// -----------------------------------------------------------------------------
// i2c_sequences.sv
// What a real device access looks like, expressed as stimulus.
//
// NOT EXECUTED -- see i2c_if.sv.
//
// A SEQUENCE DESCRIBES A SITUATION AND NEVER AN EXPECTED VALUE. That rule is what keeps
// the sequence library reusable: a sequence containing expected values has to be edited
// when the device changes, and a sequence library that needs editing per device is not a
// library. Every expectation in this environment lives in the predictor, from the
// datasheet.
//
// THE SHAPE THAT MATTERS MOST is `i2c_write_then_read_seq`. Reading a register from an
// I2C device is not one transfer -- it is a write that sets the pointer followed by a
// read, and the two are joined by a REPEATED START so the bus is never released in
// between. Release it and another master may interleave, leaving the pointer somewhere
// else by the time the read arrives. That is the access pattern every register device
// actually uses, and it is the one a naive sequence library omits.
// -----------------------------------------------------------------------------
// ---- base: everything shared, including the thing most often forgotten -------
virtual class i2c_base_seq extends uvm_sequence #(i2c_seq_item);
function new(string name = "i2c_base_seq");
super.new(name);
endfunction
// Raise an objection for the duration, so the run phase cannot end while stimulus is
// still outstanding. Without it a test can finish mid-transfer: the report says PASS,
// the last transfer never completed, and the scoreboard never saw it. This is in the
// BASE class because it is the single most commonly omitted line in a sequence
// library, and inheriting it is more reliable than remembering it.
task pre_body();
if (starting_phase != null)
starting_phase.raise_objection(this, {get_type_name(), " running"});
endtask
task post_body();
if (starting_phase != null)
starting_phase.drop_objection(this, {get_type_name(), " done"});
endtask
endclass
// ---- a single write ---------------------------------------------------------
class i2c_write_seq extends i2c_base_seq;
`uvm_object_utils(i2c_write_seq)
rand bit [6:0] addr = 7'h50;
rand bit [7:0] pointer = 8'h00;
rand bit [7:0] payload [];
constraint c_payload { payload.size() inside {[1:4]}; }
function new(string name = "i2c_write_seq");
super.new(name);
endfunction
task body();
i2c_seq_item it;
it = i2c_seq_item::type_id::create("it");
start_item(it);
// The pointer byte is the first DATA byte of a write, not a separate field. That
// is a property of this device family rather than of I2C, and encoding it here --
// in the sequence, where device conventions belong -- keeps the driver ignorant of
// register maps.
if (!it.randomize() with {
addr == local::addr;
read == 1'b0;
restart_first == 1'b0;
hold_bus == 1'b0;
n_bytes == local::payload.size() + 1;
})
`uvm_fatal("RAND", "i2c_write_seq could not randomize its item")
it.data[0] = pointer;
foreach (payload[i]) it.data[i + 1] = payload[i];
finish_item(it);
endtask
endclass
// ---- a single read ---------------------------------------------------------
class i2c_read_seq extends i2c_base_seq;
`uvm_object_utils(i2c_read_seq)
rand bit [6:0] addr = 7'h50;
rand int unsigned n_bytes = 1;
rand bit restart_first = 1'b0;
constraint c_len { n_bytes inside {[1:8]}; }
function new(string name = "i2c_read_seq");
super.new(name);
endfunction
task body();
i2c_seq_item it;
it = i2c_seq_item::type_id::create("it");
start_item(it);
if (!it.randomize() with {
addr == local::addr;
read == 1'b1;
n_bytes == local::n_bytes;
restart_first == local::restart_first;
hold_bus == 1'b0;
})
`uvm_fatal("RAND", "i2c_read_seq could not randomize its item")
finish_item(it);
endtask
endclass
// ---- the access every register device actually uses -------------------------
class i2c_write_then_read_seq extends i2c_base_seq;
`uvm_object_utils(i2c_write_then_read_seq)
rand bit [6:0] addr = 7'h50;
rand bit [7:0] pointer = 8'h00;
rand int unsigned n_bytes = 1;
constraint c_len { n_bytes inside {[1:8]}; }
function new(string name = "i2c_write_then_read_seq");
super.new(name);
endfunction
task body();
i2c_seq_item wr, rd;
// Phase 1: set the pointer, and KEEP THE BUS. hold_bus is what makes phase 2's
// repeated START a real one -- without it the driver sends a STOP, the bus is
// released, and a "repeated START" afterwards is indistinguishable from a plain
// START to every device on the bus.
wr = i2c_seq_item::type_id::create("wr");
start_item(wr);
if (!wr.randomize() with {
addr == local::addr;
read == 1'b0;
n_bytes == 1;
restart_first == 1'b0;
hold_bus == 1'b1;
})
`uvm_fatal("RAND", "the write phase could not randomize")
wr.data[0] = pointer;
finish_item(wr);
// Phase 2: change direction without releasing the bus.
rd = i2c_seq_item::type_id::create("rd");
start_item(rd);
if (!rd.randomize() with {
addr == local::addr;
read == 1'b1;
n_bytes == local::n_bytes;
restart_first == 1'b1;
hold_bus == 1'b0;
})
`uvm_fatal("RAND", "the read phase could not randomize")
finish_item(rd);
endtask
endclass
// ---- random traffic, bounded ------------------------------------------------
class i2c_random_seq extends i2c_base_seq;
`uvm_object_utils(i2c_random_seq)
rand int unsigned n_txns = 20;
constraint c_n { n_txns inside {[1:200]}; }
function new(string name = "i2c_random_seq");
super.new(name);
endfunction
task body();
i2c_seq_item it;
repeat (n_txns) begin
it = i2c_seq_item::type_id::create("it");
start_item(it);
// Unconstrained except by the item's own constraints, which already exclude
// the reserved address regions. A random sequence that generates illegal
// traffic by accident produces failures that are the environment's fault, and
// the time goes into the DUT.
if (!it.randomize())
`uvm_fatal("RAND", "i2c_random_seq could not randomize an item")
finish_item(it);
end
endtask
endclassFour sequences, and the division between them is deliberate: a single write, a single read, the combined write-then-read that real software performs, and a bounded random stream. The random sequence is unconstrained except by the item's own constraints, which already exclude the reserved address regions — because a random sequence that generates illegal traffic by accident produces failures that are the environment's fault, and the time goes into the DUT.
One occupancy, two phases, and the bus never released
10 cyclesThe read test that only worked on a single-master bus
Pitfall — a register read issued as two independent transfers
// A read sequence that sets the pointer and then reads, as two separate
// transfers. It passes every test on a single-master bus.
//
// task body();
// i2c_seq_item wr, rd;
//
// wr = i2c_seq_item::type_id::create("wr");
// start_item(wr);
// void'(wr.randomize() with { addr == 7'h50; read == 1'b0;
// n_bytes == 1; hold_bus == 1'b0; }); // <-- STOP sent
// wr.data[0] = pointer;
// finish_item(wr);
//
// rd = i2c_seq_item::type_id::create("rd");
// start_item(rd);
// void'(rd.randomize() with { addr == 7'h50; read == 1'b1;
// restart_first == 1'b1; }); // <-- not real
// finish_item(rd);
// endtask
//
// The STOP releases the bus. So restart_first produces a repeated START on an IDLE
// bus, which every device reads as an ordinary START -- the flag has no effect.
//
// On a single-master bus nothing notices. Add a second master, or a bus monitor
// that reports framing, and the read intermittently returns the wrong register:
// the other master interleaved and moved the pointer.Pitfall — a sequence that carries the expected value
// A read test written the way a directed test naturally is:
//
// class i2c_read_reg2_seq extends i2c_base_seq;
// task body();
// i2c_seq_item rd;
// rd = i2c_seq_item::type_id::create("rd");
// start_item(rd);
// void'(rd.randomize() with { addr == 7'h50; read == 1'b1; n_bytes == 1; });
// finish_item(rd);
//
// // the expectation, in the stimulus
// if (rd.data[0] != 8'h5A)
// uvm_error("SEQ", "register 2 did not read back 0x5A");
// endtask
// endclass
//
// Three separate problems, and the third is the expensive one:
//
// 1. rd.data[0] is an INTENT field. The driver does not write read data back into
// it, so this compares 0x5A against whatever randomize() put there.
// 2. Even if it did, that is the circular comparison of chapter 21.2.
// 3. The sequence now encodes a fact about ONE part. Point the environment at a
// device whose register 2 holds something else and it fails on correct
// behaviour -- so the sequence gets edited, and the edit is a fork.6. What 21.9 Settled
A sequence chooses what happens and never what should come back. An expectation in a sequence ties the library to one device, and grepping for data literals finds the violations quickly.
A register read is two phases in one bus occupancy. The repeated START must be genuine, which requires the driver to be able to decline the STOP — a capability that was missing until a test could not be written.
The objection is inherited, not remembered. Its absence lets a run phase end mid-transfer and report PASS.
randomize()'s return value is always checked, because failure is silent and leaves stale field values that look like deliberate stimulus.
A flag set in stimulus and never observed on the bus is a distinct failure class, and only the monitor can report it.
Next, the stimulus that breaks the rules on purpose — and the line between a violation a controller can produce and one that is another device misbehaving. Chapter 21.10 — Negative and Error Sequences.
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
Write-Then-Read — The I²C Register-Pointer Pattern
Almost every real I²C access is this one shape: write the register pointer, repeated START, read the data, without ever letting the bus go. This chapter derives it from the specification's own example and builds the transaction sequencer.
- Related topic
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.
- 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.
