Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
The line that ties a sequence to one device
   // 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
   endtask

That 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from i2c_sequences.sv — the access every register device uses
         // 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

Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from i2c_sequences.sv — inherited rather than remembered
   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
   endclass

Without 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

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_sequences.sv — situations, with no expectations in them
   // -----------------------------------------------------------------------------
   // 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

   endclass

Four 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 cycles
A twelve-cycle schematic waveform showing a register read. A START marker opens the transfer, followed by an address write phase and a pointer byte, then a repeated START marker, then an address read phase and a data byte, then a STOP marker. The bus-held signal stays high from the START through to the STOP, showing one continuous occupancy.write: set the pointerwrite: set the pointerread: same occupancyread: same occupancySTARTSTARTrepeated STARTrepeated STARTSTOPSTOPSCLSDAbus heldt0t1t2t3t4t5t6t7t8t9
Schematic rather than bit-accurate: each phase is nine bit slots on a real bus and this shows the framing only. What it shows faithfully is that 'bus held' never drops between the phases — which is the property hold_bus exists to produce and the one a STOP in between would destroy.
Figure 1 — a register read as it actually happens on the bus. Two phases, one bus occupancy: a write that sets the pointer, then a repeated START that changes direction without releasing the line. The STOP appears only at the end. A library whose read sequence issues a single transfer is testing something the device never does.

The read test that only worked on a single-master bus

Pitfall — a register read issued as two independent transfers
Buggy Code
// 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
Buggy Code
// 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