Skip to content
VLSI Mentor

I²C · Module 21

The Sequence Item — Fields, Constraints and Direction

Why an intent and an observation must be two classes that are deliberately siblings rather than parent and child, which single field forces the split, and why every constraint an error sequence might relax needs a name. Ends on the gap a type system cannot close.

Chapter 20.3 reached a conclusion with packed structs: an environment needs two transaction types, not one. An intent exists before the bus and contains nothing measured; an observation exists only after the bus and contains nothing chosen.

Carrying that into a class hierarchy introduces a hazard the struct version did not have, and it is the subject of this chapter.

1. The Field That Settles the Argument

Ask where the acknowledge bit goes and the answer decides how many classes you have.

A sequence cannot choose whether the target acknowledges. That is the target's decision, taken from a contract the sequence knows nothing about. So a single merged transaction class has exactly three options, and all of them are bad:

what the merged class does with ackconsequence
leaves it at its default and compares itevery legal NACK is reported as a mismatch
excludes it from the comparisonevery NACK becomes invisible, including wrong ones
has the driver write it back into the same objectthe object is now both stimulus and measurement, and the comparison is circular

The third is the one teams actually choose, because it looks like efficiency. It is how this line comes to be written:

Azvya Education Pvt. Ltd.VLSI Mentor
The comparison that holds with the DUT removed
   txn = seq.next();          // we decide: write 0xA5 to register 3
   driver.send(txn);          // the driver puts that on the bus, and writes ack back
   scoreboard.expect(txn);    // ... the SAME handle
   observed = monitor.get();
   scoreboard.compare(observed);

Comment out the DUT instantiation and every test still passes. The loop runs from the sequence through the driver to the monitor and back to the sequence's own object, and the design is not on it.

Two classes make that a type error. The intent has no ack field to write back into, so there is nothing for the driver to corrupt and nothing for the scoreboard to be handed.

This is the UVM-specific trap, and it is easy to walk into while trying to do the right thing.

Having decided on two classes, the natural next thought is that an observation is a kind of transaction, so i2c_txn extends i2c_seq_item. It compiles. It is worse than one class.

3. Which Fields Are rand, and Which Must Not Be

Every protocol field of the intent is rand. The reason is a silent failure mode: a non-rand field is simply never randomised, so a constrained-random campaign exercises exactly one value of it for the lifetime of the project, and coverage on that field shows a single bin that looks deliberate.

The observation's fields are not rand, and that asymmetry is not an oversight. Randomising a measurement is meaningless — there is nothing to choose.

The 7-bit address space has reserved regions — 0x00 is the general call, 0x01–0x07 and 0x78–0x7F are reserved. Ordinary random traffic must avoid them, or a fraction of every random run is illegal and every resulting failure is the environment's fault.

The temptation is to bake the range into the field's declaration or into each sequence. Both are worse than a named constraint:

Azvya Education Pvt. Ltd.VLSI Mentor
Abbreviated from i2c_seq_item.sv — named, so it can be disabled deliberately
      constraint c_addr_legal {
         addr inside {[7'h08:7'h77]};
      }

Naming it means Chapter 21.10's error sequences can switch it off by name — it.c_addr_legal.constraint_mode(0) — which is visible at the point of use and impossible to do by accident. A hard-coded range forces an error sequence to bypass randomisation entirely, and a sequence that assigns fields directly is a sequence that stops obeying every other constraint at the same time.

The same argument applies to the soft constraint on the truncation field. Transfers complete by default; an error has to be asked for.

5. The Two Classes

Azvya Education Pvt. Ltd.VLSI Mentor
i2c_seq_item.sv — an intent and an observation, as siblings
   // -----------------------------------------------------------------------------
   // i2c_seq_item.sv
   // TWO transaction classes, because there are two kinds of object.
   //
   // NOT EXECUTED -- see i2c_if.sv.
   //
   // Chapter 20.3 made the argument with packed structs: an INTENT exists before the bus
   // and contains nothing measured; an OBSERVATION exists only after the bus and contains
   // nothing chosen. The reason to carry that distinction into a class hierarchy rather
   // than quietly merge it is that a single class makes this line natural to write:
   //
   //     driver.send(txn);  scoreboard.expect(txn);     // the SAME handle
   //
   // and that comparison holds with the DUT removed. Two classes make it a type error.
   //
   // A UVM-specific hazard that the struct version did not have: the two classes must NOT
   // be related by inheritance. `i2c_txn extends i2c_seq_item` would compile, would let an
   // observation be passed anywhere an intent is expected, and -- because the factory and
   // `$cast` both accept a derived handle for a base type -- would put the circular
   // comparison back exactly where the type split was meant to remove it.
   // -----------------------------------------------------------------------------

   // ============================================================================
   // WHAT WAS ASKED FOR. Constructed by a sequence; consumed by a driver.
   // ============================================================================
   class i2c_seq_item extends uvm_sequence_item;

      // Every field is a CHOICE. Randomised because the environment picks them, and every
      // protocol field is `rand` deliberately: a non-rand field in a sequence item is
      // silently never randomised, so a constrained-random run quietly exercises one value
      // for the lifetime of the project.
      rand bit [6:0] addr;
      rand bit       read;
      rand int unsigned n_bytes;
      rand bit [7:0] data [];
      rand bit       restart_first;   // open with a repeated START rather than a START
      rand bit       hold_bus;        // decline to send the trailing STOP

      // Abandon the transfer after this many BITS of the current byte, then frame a STOP.
      // Zero means complete it normally.
      //
      // This field is here rather than in a derived error item, and the reason is worth
      // stating: abandoning a transfer is something a REAL CONTROLLER DOES -- a driver
      // whose software timed out, a master that lost arbitration. It is legal controller
      // behaviour producing an illegal-looking bus event, so it belongs on the ordinary
      // item. Violations a controller CANNOT produce -- SDA moving while SCL is high
      // mid-byte, a line held down forever -- are not driver modes at all: they are a third
      // participant on the bus, which is i2c_error_injector.
      rand int unsigned abort_after_bits;

      // There is NO acknowledge field, and its absence is the design. Whether the target
      // acknowledges is the TARGET's decision, taken from a contract this object knows
      // nothing about. A sequence cannot choose it, so an intent cannot carry it -- and a
      // single merged class would have to either misreport every NACK or ignore
      // acknowledgement entirely. Chapter 20.3's T8 is the concrete case.

      `uvm_object_utils_begin(i2c_seq_item)
         `uvm_field_int(addr,          UVM_ALL_ON)
         `uvm_field_int(read,          UVM_ALL_ON)
         `uvm_field_int(n_bytes,       UVM_ALL_ON)
         `uvm_field_array_int(data,    UVM_ALL_ON)
         `uvm_field_int(restart_first, UVM_ALL_ON)
         `uvm_field_int(hold_bus,      UVM_ALL_ON)
         `uvm_field_int(abort_after_bits, UVM_ALL_ON)
      `uvm_object_utils_end

      function new(string name = "i2c_seq_item");
         super.new(name);
      endfunction

      // Keep the payload and its length in step. Without this the array can be one size
      // and `n_bytes` another, and the driver then either sends garbage or walks off the
      // end -- a defect that shows up as an intermittent, length-dependent failure.
      constraint c_len {
         n_bytes inside {[1:8]};
         data.size() == n_bytes;
      }

      // Complete transfers by default. An error has to be asked for explicitly, or random
      // traffic quietly becomes mostly-illegal traffic and every failure is the
      // environment's fault.
      constraint c_no_abort_by_default { soft abort_after_bits == 0; }

      // A 7-bit address space has reserved regions (0x00 general call, 0x01-0x07 and
      // 0x78-0x7F). Excluded by default so that ordinary random traffic is legal; the
      // error sequences in Chapter 21.10 override this deliberately, which is why it is a
      // named constraint rather than a hard-coded range.
      constraint c_addr_legal {
         addr inside {[7'h08:7'h77]};
      }

   endclass

   // ============================================================================
   // WHAT THE WIRE CARRIED. Produced by a monitor; consumed by a scoreboard.
   // Deliberately NOT derived from i2c_seq_item -- see the header.
   // ============================================================================
   class i2c_txn extends uvm_sequence_item;

      // Measured, every one of them.
      bit [6:0] addr;
      bit       read;
      bit       addr_acked;          // <-- no intent counterpart: the target decided this
      bit [7:0] data [];
      bit       acks [];             // <-- nor this, one per data byte
      bit       began_with_restart;  // the framing that actually opened the transfer
      bit       ended_by_restart;    // <-- no intent counterpart for THIS transfer: the
      bit       ended_by_stop;       //     NEXT transfer's opening decides it
      bit       truncated;           // framing arrived mid-byte; those bits are not data

      `uvm_object_utils_begin(i2c_txn)
         `uvm_field_int(addr,               UVM_ALL_ON)
         `uvm_field_int(read,               UVM_ALL_ON)
         `uvm_field_int(addr_acked,         UVM_ALL_ON)
         `uvm_field_array_int(data,         UVM_ALL_ON)
         `uvm_field_array_int(acks,         UVM_ALL_ON)
         `uvm_field_int(began_with_restart, UVM_ALL_ON)
         `uvm_field_int(ended_by_restart,   UVM_ALL_ON)
         `uvm_field_int(ended_by_stop,      UVM_ALL_ON)
         `uvm_field_int(truncated,          UVM_ALL_ON)
      `uvm_object_utils_end

      function new(string name = "i2c_txn");
         super.new(name);
      endfunction

      // The difference between an intent and an observation, field by field -- and it
      // returns a MASK rather than a bit, because collapsing a comparison to pass-or-fail
      // discards the only information that makes a failure diagnosable.
      //
      // `defined` is low for a READ, and refusing is the correct answer: a read's payload
      // came from the TARGET, so no field of the intent predicts it. Comparing them would
      // not be a weak check, it would be a comparison of two unrelated quantities that
      // happens to be expressible. A read's payload has to be checked against the device
      // contract, which is the predictor's job and not this function's.
      function void diff(i2c_seq_item w,
                         output bit defined,
                         output bit d_addr, d_dir, d_nbytes, d_data, d_framing);
         defined = 1'b0;
         d_addr = 1'b0; d_dir = 1'b0; d_nbytes = 1'b0; d_data = 1'b0; d_framing = 1'b0;
         if (w == null) return;
         if (w.read) return;                       // UNDEFINED for a read -- see above
         defined   = 1'b1;
         d_addr    = (w.addr != addr);
         d_dir     = (w.read != read);
         d_nbytes  = (w.n_bytes != data.size());
         d_framing = (w.restart_first != began_with_restart);
         if (!d_nbytes)
            foreach (data[i]) if (w.data[i] != data[i]) d_data = 1'b1;
      endfunction

      // Address and direction compare in EVERY direction, so a read whose payload cannot
      // be compared still has two fields that can be. Losing them because the payload
      // check is impossible would give up more than the situation requires.
      function bit framing_only_differs(i2c_seq_item w);
         if (w == null) return 1'b0;
         return (w.addr != addr) || (w.read != read);
      endfunction

   endclass

Three details in that file are worth pointing at.

diff() returns a mask, not a bit. Collapsing a comparison to pass-or-fail discards the only information that makes a failure diagnosable. Which field differed tells you which logic to look at before opening a waveform.

diff() refuses to compare a read's payload. The defined output is low for a read, and refusing is the correct answer rather than a limitation: a read's bytes came from the target, so no field of the intent predicts them. Comparing them would not be a weak check — it would be a comparison of two unrelated quantities that happens to be expressible.

framing_only_differs() exists so that a read is not left unchecked. Address and direction compare in every direction. Giving them up because the payload comparison is impossible would concede more than the situation requires.

The scoreboard that had never failed, and the field that explains why

Pitfall — one class, and a driver that writes its result back
Buggy Code
// A single transaction class used for stimulus and for checking.
//
//    class i2c_txn extends uvm_sequence_item;
//      rand bit [6:0] addr;  rand bit read;  rand bit [7:0] data[];
//      bit            ack;                    // <-- written by the DRIVER
//    endclass
//
//    task run_phase(uvm_phase phase);
//      forever begin
//        seq_item_port.get_next_item(req);
//        drive(req);
//        req.ack = observed_ack;              // the item is now a MEASUREMENT too
//        seq_item_port.item_done();
//      end
//    endtask
//
// The scoreboard is handed the same handle and compares it against the monitor's
// reconstruction. It has never reported a mismatch in eleven months.
//
// It cannot. Both sides of the comparison descend from one object: the driver
// wrote what it saw into the item, and the monitor saw what the driver sent.
// Remove the DUT and every test still passes.
Pitfall — an error sequence that bypassed randomisation
Buggy Code
// The legal address range is hard-coded into the item's declaration:
//
//    rand bit [6:0] addr;
//    constraint c { addr >= 7'h08 && addr <= 7'h77; }   // unnamed
//
// An error sequence needs a reserved address. The constraint has no name, so it
// cannot be switched off -- and the sequence does this instead:
//
//    it = i2c_seq_item::type_id::create("it");
//    start_item(it);
//    it.addr    = 7'h00;        // assigned directly: randomize() never called
//    it.read    = 1'b0;
//    it.n_bytes = 1;
//    finish_item(it);
//
// Now EVERY other constraint is bypassed too. data.size() is left at 0 while
// n_bytes is 1, so the driver indexes past the end of an empty array. The
// failure is a null-ish array access in the DRIVER, on a test about addressing,
// and it moves around as unrelated fields change.

6. What 21.2 Settled

The acknowledge field decides the class count. It cannot be chosen by a sequence and cannot be dropped from a measurement, so one class is forced into misreporting, blindness, or circularity.

The two classes are siblings, deliberately. Relating them by inheritance disables the type system that was the whole mechanism, while leaving a design that looks correct.

rand on every protocol field of the intent, on none of the observation. A non-rand stimulus field is silently never randomised; a rand measurement field is meaningless.

Every constraint that an error sequence might relax gets a name. Otherwise the only way past it is to abandon randomisation, which abandons the constraints that were keeping the item consistent and loses the randomize() failure check as well.

Two types are necessary and not sufficient. A completely refused transfer still compares equal, which is why the contract-driven predictor is a separate component and not a refinement of this one.

Next, the component that turns one of these objects into edges — and the tool that checks its algorithm against the version that was actually simulated. Chapter 21.3 — The Master Driver.

Continue learning