Verify This! · the course to read

Day 8RAL, the golden model and the second capstone

The same slides as the deck, with the instructor notes inside the text and the complete code. If you are taking the course on your own, start here and keep the deck alongside for the reviews.

Yesterday we left…

Where the testbench stands

This day is optional and it goes after the wrap-up, and both things are on purpose. The course ends on day 7: whoever got that far wrote a complete UVM testbench from a blank sheet and lacks nothing to go to work. What is here is what gets added once that has happened. The recap has a concrete job on day 8: the three pieces of today are only understood as a replacement for something the student has just written by hand. RAL replaces the register-by-register prediction, the golden model in C replaces the whole prediction, and the second capstone shows where the pattern of the first stops working. Without the capstone done, all three are abstractions without a problem.

see this slide in the deck ›

Agenda

Day 8 · ≈ 4 h · optional · what comes after the capstone

By the end of the day you can:

For whoever teaches it: in a company this is another half day, and it usually gets taught when the group has real registers in the project. In a term it is the optional unit, or the final assignment for those who want a high mark. The three objectives are deliberately concrete and not "understand RAL": day 8 has no row in the 175b self-assessment, so the rubric the student takes away is these three verbs and the eight-question review. The third is the one that pays off the most of the three and the least expected — a FIFO has order and occupancy, and that does not fit in a four-row table.

see this slide in the deck ›

RAL

Unit 9 · optional · the register stops being an address

This unit is short and it is the last one, and both things are deliberate. RAL is the piece of UVM most often taught without context —there are courses that give it on day 2— and that way it is not understood: it looks like one more API. With the capstone just done, on the other hand, the student has the comparison inside their head. They wrote the scoreboard that predicts CTRL; now I show them the library that does it on its own, and also where it does not. The number of 200 registers is not rhetorical. A PHY block or a memory controller has that many, and the register spec arrives as a spreadsheet. The next step, which this course does not take, is generating the model from that spreadsheet with IP-XACT or SystemRDL: nobody writes 200 registers by hand. It is worth saying, because otherwise the student walks away with the idea that RAL is a lot of typing. And the argument about moving 0x04 to 0x24 is the one that convinces a sceptic: it is not elegance, it is that the map gets written only once.

see this slide in the deck ›

RAL

Three pieces, and only one knows what bus is underneath

Piece What it is How many times it gets written
uvm_reg_block the map: registers, fields, addresses one per DUT
uvm_reg_adapter translates access ↔ bus transaction one per protocol
uvm_reg_predictor what the monitor saw, said in registers none: it comes done

The table is the slide. That the third column says one per DUT, one per protocol and none is the whole economic argument for RAL, and it is the one that holds the decision up in a project meeting. It is worth pointing at the last line with a finger: nothing the student wrote yesterday gets replaced here. code/u9/ral/ compiles the interface, the transaction, the driver, the monitor and the agent of d7-final/solucion/ exactly as they are, through +incdir. RAL is a layer, not a different testbench — and it is the reason why this unit fits into three files. If somebody asks about the backdoor: it exists, it gets declared with add_hdl_path and it reads the signal of the RTL without going through the bus. It does not go into the course because it depends on the hierarchy support of the simulator, which is precisely what changes between tools. The frontdoor —the one that uses the real bus— is the one you have to know.

see this slide in the deck ›

RAL

The model: the table of the spec, in code

code/u9/ral/tb_classes/apb_reg_block.svh · #ctrl_reg
// 0x00 -- CTRL. EN is an ordinary bit. CLR is not: writing a 1 clears ACC and
// OVF, and the bit always reads back 0.
class ctrl_reg extends uvm_reg;
   `uvm_object_utils(ctrl_reg)

   rand uvm_reg_field EN;
   rand uvm_reg_field CLR;

   function new(string name = "ctrl_reg");
      super.new(name, 32, UVM_NO_COVERAGE);
   endfunction : new

   // configure(parent, size, lsb_pos, access, volatile, reset, has_reset,
   //           is_rand, individually_accessible)
   virtual function void build();
      EN = uvm_reg_field::type_id::create("EN");
      EN.configure(this, 1, 0, "RW", 0, 'h0, 1, 1, 0);

      // "WC" is the name UVM has for exactly this: the write clears the field,
      // the mirror drops to zero, and the read returns that zero. Modelling it as
      // "RW" is the classic mistake, and it is what +MAL does. "WOC" also passes,
      // for the wrong reason: any WO* access takes the field out of bit_bash and
      // out of do_check, so nobody looks at it. See the RAL unit slides.
      CLR = uvm_reg_field::type_id::create("CLR");
      CLR.configure(this, 1, 1, $test$plusargs("MAL") ? "RW" : "WC", 0, 'h0, 1, 1, 0);
   endfunction : build

endclass : ctrl_reg
el archivo entero, 149 líneas
// The register map of spec.md, written as a UVM register model.
//
// The table of the spec maps one to one onto this file, and that is the check
// worth doing out loud: four registers, six fields, four addresses. Nothing here
// knows that the bus is APB -- swap the adapter and the same model drives AHB.
//
// The access string of every field IS the specification. Get it wrong and the
// built-in sequences of the last file catch it; see the +MAL run of run.sh.

// 0x00 -- CTRL. EN is an ordinary bit. CLR is not: writing a 1 clears ACC and
// OVF, and the bit always reads back 0.
class ctrl_reg extends uvm_reg;
   `uvm_object_utils(ctrl_reg)

   rand uvm_reg_field EN;
   rand uvm_reg_field CLR;

   function new(string name = "ctrl_reg");
      super.new(name, 32, UVM_NO_COVERAGE);
   endfunction : new

   // configure(parent, size, lsb_pos, access, volatile, reset, has_reset,
   //           is_rand, individually_accessible)
   virtual function void build();
      EN = uvm_reg_field::type_id::create("EN");
      EN.configure(this, 1, 0, "RW", 0, 'h0, 1, 1, 0);

      // "WC" is the name UVM has for exactly this: the write clears the field,
      // the mirror drops to zero, and the read returns that zero. Modelling it as
      // "RW" is the classic mistake, and it is what +MAL does. "WOC" also passes,
      // for the wrong reason: any WO* access takes the field out of bit_bash and
      // out of do_check, so nobody looks at it. See the RAL unit slides.
      CLR = uvm_reg_field::type_id::create("CLR");
      CLR.configure(this, 1, 1, $test$plusargs("MAL") ? "RW" : "WC", 0, 'h0, 1, 1, 0);
   endfunction : build

endclass : ctrl_reg

// 0x04 -- SCRATCH. A plain RW register. That every write to it also adds to ACC
// is a side effect the model does not know about, and does not need to.
class scratch_reg extends uvm_reg;
   `uvm_object_utils(scratch_reg)

   rand uvm_reg_field VALUE;

   function new(string name = "scratch_reg");
      super.new(name, 32, UVM_NO_COVERAGE);
   endfunction : new

   virtual function void build();
      VALUE = uvm_reg_field::type_id::create("VALUE");
      VALUE.configure(this, 32, 0, "RW", 0, 'h0, 1, 1, 1);
   endfunction : build

endclass : scratch_reg

// 0x08 -- ACC. Read-only, and volatile: the DUT changes it behind the model's
// back, on every write to SCRATCH. See the comment on set_compare() below.
class acc_reg extends uvm_reg;
   `uvm_object_utils(acc_reg)

   uvm_reg_field VALUE;

   function new(string name = "acc_reg");
      super.new(name, 32, UVM_NO_COVERAGE);
   endfunction : new

   virtual function void build();
      VALUE = uvm_reg_field::type_id::create("VALUE");
      VALUE.configure(this, 32, 0, "RO", 1, 'h0, 1, 0, 1);
   endfunction : build

endclass : acc_reg

// 0x0C -- STATUS. Two read-only bits, and both are volatile for the same reason
// as ACC: EN is a copy of CTRL.EN, and OVF turns on when the accumulator
// overflows -- neither of them happens on a write to STATUS.
class status_reg extends uvm_reg;
   `uvm_object_utils(status_reg)

   uvm_reg_field EN;
   uvm_reg_field OVF;

   function new(string name = "status_reg");
      super.new(name, 32, UVM_NO_COVERAGE);
   endfunction : new

   virtual function void build();
      EN = uvm_reg_field::type_id::create("EN");
      EN.configure(this, 1, 0, "RO", 1, 'h0, 1, 0, 0);
      OVF = uvm_reg_field::type_id::create("OVF");
      OVF.configure(this, 1, 1, "RO", 1, 'h0, 1, 0, 0);
   endfunction : build

endclass : status_reg

class apb_reg_block extends uvm_reg_block;
   `uvm_object_utils(apb_reg_block)

   rand ctrl_reg    CTRL;
   rand scratch_reg SCRATCH;
   acc_reg          ACC;
   status_reg       STATUS;

   function new(string name = "apb_reg_block");
      super.new(name, UVM_NO_COVERAGE);
   endfunction : new

   // Not build_phase: a uvm_reg_block is a uvm_object, not a component. Nobody
   // calls this for you -- the test does, right after the create().
   virtual function void build();
      // create_map(name, base_addr, n_bytes, endian, byte_addressing)
      default_map = create_map("default_map", 'h0, 4, UVM_LITTLE_ENDIAN, 1);

      CTRL = ctrl_reg::type_id::create("CTRL");
      CTRL.configure(this, null, "");
      CTRL.build();
      default_map.add_reg(CTRL, CTRL_ADDR, "RW");

      SCRATCH = scratch_reg::type_id::create("SCRATCH");
      SCRATCH.configure(this, null, "");
      SCRATCH.build();
      default_map.add_reg(SCRATCH, SCRATCH_ADDR, "RW");

      ACC = acc_reg::type_id::create("ACC");
      ACC.configure(this, null, "");
      ACC.build();
      default_map.add_reg(ACC, ACC_ADDR, "RO");

      STATUS = status_reg::type_id::create("STATUS");
      STATUS.configure(this, null, "");
      STATUS.build();
      default_map.add_reg(STATUS, STATUS_ADDR, "RO");

      // The honest line of the whole unit. ACC and STATUS have an address, but
      // they are not registers: their value is produced by writes to OTHER
      // addresses. A register model predicts "what I wrote is what I will read",
      // and for these two that sentence is false, so mirror(UVM_CHECK) is turned
      // off on them. Reads still update the mirror -- a read is a prediction --
      // and the accumulator itself stays where it was in the capstone: in the
      // scoreboard, which is the thing that does know how to add.
      ACC.VALUE.set_compare(UVM_NO_CHECK);
      STATUS.EN.set_compare(UVM_NO_CHECK);
      STATUS.OVF.set_compare(UVM_NO_CHECK);

      lock_model();
   endfunction : build

endclass : apb_reg_block

configure(parent, size, lsb_pos, access, volatile, reset, has_reset, is_rand, individually_accessible) — nine positional arguments, and it is the ugliest signature in UVM. It is worth writing it on the blackboard once; afterwards the student copies it from the example, which is what everybody does. CLR is the field that justifies the slide. The spec of the capstone says "writing a 1 puts ACC and OVF at zero, and the bit always reads 0", and that is trap 2 of the small print — the one that makes the hand-written scoreboard fail. The access that says exactly that is WC: the write clears the field, the mirror goes down to zero, and the read returns that zero. W1C works just as well here. And here is RAL's silent trap, which is worth more than the slide. WOC —the name you write first, because it sounds like "write-only, clears"— also gives zero errors, but it gives them for the wrong reason: uvm_reg_bit_bash_seq.svh:129-133 skips every field whose access starts with WO (Accellera's own comment says "Ignore Write-only fields because you are not supposed to read them"), and do_check takes it out of the comparison mask (uvm_reg.svh:2782-2788), so the mirror(UVM_CHECK) does not look at it either. With WOC bit 1 of CTRL is never tested and nobody prints anything; the only trace is in the timing, because the bashing of CTRL takes four transfers less. Green is not checked, and a WO* access is the cheapest way there is to switch off a check without noticing. The complete list of accesses is in the LRM 1800.2, and there are twenty-five. You do not have to know them by heart: you have to know that the list exists, and look there before inventing a set_compare or a callback. Half of the bad register models you come across are an RW field with patches on top.

see this slide in the deck ›

RAL

The adapter: the only thing that knows there is an APB

code/u9/ral/tb_classes/apb_reg_adapter.svh · #reg2bus-and-bus2reg
   // Downstream: the model asks for a register access, this builds the bus item
   // the sequencer will hand to the driver.
   virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
      apb_transaction t = apb_transaction::type_id::create("t");
      t.write = (rw.kind == UVM_WRITE);
      t.addr  = rw.addr[7:0];
      t.wdata = rw.data[31:0];
      return t;
   endfunction : reg2bus

   // Upstream: a bus item comes back -- from the driver's response, or from the
   // monitor when prediction is explicit -- and this fills the generic operation
   // the model understands.
   //
   // PSLVERR is what makes status meaningful: an access to an unmapped address
   // comes back UVM_NOT_OK, and the write()/read() call reports it.
   virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
      apb_transaction t;
      if (!$cast(t, bus_item)) `uvm_fatal("REG ADAPTER", "Not an apb_transaction")
      rw.kind   = t.write ? UVM_WRITE : UVM_READ;
      rw.addr   = t.addr;
      rw.data   = t.write ? t.wdata : t.rdata;
      rw.status = t.slverr ? UVM_NOT_OK : UVM_IS_OK;
   endfunction : bus2reg
el archivo entero, 39 líneas
// The only class in the unit that knows the bus exists. Twenty lines, and they
// are the whole reason the register model is portable: a generic uvm_reg_bus_op
// in, an apb_transaction out.
//
// Written once per protocol, not once per project. This is the file you would
// get with a commercial APB VIP.
class apb_reg_adapter extends uvm_reg_adapter;
   `uvm_object_utils(apb_reg_adapter)

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

   // Downstream: the model asks for a register access, this builds the bus item
   // the sequencer will hand to the driver.
   virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
      apb_transaction t = apb_transaction::type_id::create("t");
      t.write = (rw.kind == UVM_WRITE);
      t.addr  = rw.addr[7:0];
      t.wdata = rw.data[31:0];
      return t;
   endfunction : reg2bus

   // Upstream: a bus item comes back -- from the driver's response, or from the
   // monitor when prediction is explicit -- and this fills the generic operation
   // the model understands.
   //
   // PSLVERR is what makes status meaningful: an access to an unmapped address
   // comes back UVM_NOT_OK, and the write()/read() call reports it.
   virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
      apb_transaction t;
      if (!$cast(t, bus_item)) `uvm_fatal("REG ADAPTER", "Not an apb_transaction")
      rw.kind   = t.write ? UVM_WRITE : UVM_READ;
      rw.addr   = t.addr;
      rw.data   = t.write ? t.wdata : t.rdata;
      rw.status = t.slverr ? UVM_NOT_OK : UVM_IS_OK;
   endfunction : bus2reg

endclass : apb_reg_adapter

Twenty-two lines, and that is the whole file. It is worth saying the number out loud, because RAL has a reputation for being heavy and the project-specific part is this one. The reg2bus / bus2reg symmetry is the one to fix: one function per direction, and neither of the two has state. The uvm_reg_bus_op is the common currency —kind, addr, data, status— and it is deliberately as flat as possible: any register bus fits in there. The const ref of the reg2bus comes from the signature of the base class and has to be copied exactly. It works on Verilator 5.052, which was not obvious. And a warning from the field: bus2reg is called by the predictor too, with the item the monitor saw. An adapter that reads something from the driver —or that depends on having built the transaction itself— works with automatic prediction and breaks the day you switch to explicit. That is why both functions are pure.

see this slide in the deck ›

RAL

Hooking it up: three lines and a connect_phase

code/u9/ral/tb_classes/ral_base_test.svh · #connect_phase
   function void connect_phase(uvm_phase phase);
      // The three lines that turn a model into a testbench component: which
      // sequencer carries the accesses, which adapter translates them, and where
      // the truth about the DUT comes from.
      model.default_map.set_sequencer(agent_h.sequencer_h, adapter);
      model.default_map.set_auto_predict(0);

      predictor.map     = model.default_map;
      predictor.adapter = adapter;
      agent_h.ap.connect(predictor.bus_in);
   endfunction : connect_phase
el archivo entero, 85 líneas
// The structure, and nothing else. It is the capstone's active agent with three
// objects on top: the register model, the adapter, and the predictor.
virtual class ral_base_test extends uvm_test;
   `uvm_component_abstract_utils(ral_base_test)

   apb_agent        agent_h;
   apb_agent_config agent_cfg_h;

   apb_reg_block    model;
   apb_reg_adapter  adapter;

   // Parameterised by the BUS item, not by the register: the predictor's job is
   // to turn what the monitor saw into a prediction on the model.
   uvm_reg_predictor #(apb_transaction) predictor;

   function new(string name, uvm_component parent);
      super.new(name, parent);
   endfunction : new

   function void build_phase(uvm_phase phase);
      virtual apb_if bfm;

      if (!uvm_config_db#(virtual apb_if)::get(this, "", "bfm", bfm))
         `uvm_fatal("RAL BASE TEST", "Failed to get bfm")

      agent_cfg_h = new(.bfm(bfm), .is_active(UVM_ACTIVE));
      uvm_config_db#(apb_agent_config)::set(this, "agent_h*", "config", agent_cfg_h);
      agent_h = apb_agent::type_id::create("agent_h", this);

      // A uvm_reg_block is a uvm_object: create() does not call build(), the way
      // the component build_phase would. Forgetting this line leaves a model
      // with zero registers and an error that says nothing.
      model = apb_reg_block::type_id::create("model");
      model.build();

      adapter   = apb_reg_adapter::type_id::create("adapter");
      predictor = uvm_reg_predictor #(apb_transaction)::type_id::create("predictor", this);
   endfunction : build_phase

   function void connect_phase(uvm_phase phase);
      // The three lines that turn a model into a testbench component: which
      // sequencer carries the accesses, which adapter translates them, and where
      // the truth about the DUT comes from.
      model.default_map.set_sequencer(agent_h.sequencer_h, adapter);
      model.default_map.set_auto_predict(0);

      predictor.map     = model.default_map;
      predictor.adapter = adapter;
      agent_h.ap.connect(predictor.bus_in);
   endfunction : connect_phase

   // The reset the driver does at time 0 happens on the wire; the model has to
   // be told, or its mirror starts out as "unknown" instead of as the reset
   // values of the spec.
   task reset_model();
      model.reset();
   endtask : reset_model

   // The model printed back as the table it came from. One line per field, in
   // the order of the map, with the four things the spec says about it: where it
   // lives, how wide it is, and how it is accessed.
   //
   // It is worth doing once out loud, because a register model is the only
   // document in a project that is both the spec and the code -- and this is
   // the diff between the two.
   function void print_map();
      uvm_reg   regs[$];
      uvm_reg_field fields[$];
      model.get_registers(regs);
      foreach (regs[i]) begin
         fields.delete();   // get_fields() APPENDS: without this, register i
         regs[i].get_fields(fields);   // prints the fields of 0..i as well
         foreach (fields[j])
            `uvm_info("MAPA", $sformatf("%-8s @0x%02h  %-7s [%0d+:%0d]  %s",
                      regs[i].get_name(), regs[i].get_offset(model.default_map),
                      fields[j].get_name(), fields[j].get_lsb_pos(),
                      fields[j].get_n_bits(), fields[j].get_access()), UVM_NONE)
      end
   endfunction : print_map

   function void start_of_simulation_phase(uvm_phase phase);
      if ($test$plusargs("MAPA")) print_map();
   endfunction : start_of_simulation_phase

endclass : ral_base_test

The phase trap that takes everybody's first afternoon is the last line of the slide. apb_reg_block::type_id::create("model") gives back an object with zero registers, because build() here is an ordinary method that gets called by hand —not the build_phase of a component, which UVM walks on its own—. The symptom is a uvm_fatal from uvm_reg_map that talks about a null register and does not mention the word build. The second thing to point out is where the sequencer comes from: it is agent_h.sequencer_h, the same one from the capstone, the same one the normal sequences run on. RAL does not bring a sequencer of its own. That means a register sequence and a traffic one compete for the same driver, and there the priorities show up — which is a topic for another course, but it is worth them knowing it exists.

see this slide in the deck ›

RAL

Prediction: who tells the model what happened

Who predicts What happens if the DUT does something else
automatic the write() call itself the model does not find out: it believes what it meant to send
explicit the uvm_reg_predictor, from the monitor the model believes what happened on the wire

This is the judgement slide of the unit and it is worth not rushing it. With set_auto_predict(1), model.CTRL.write(status, 'h1) updates the mirror at the moment it gets called, before the bus has done anything. If the driver sends the transfer wrong, or the DUT ignores it, the model goes on being convinced that EN=1. It is exactly the design mistake the course has been attacking since day 1: the one who checks cannot believe the one who drives. With explicit prediction the path is the long and correct one: the driver drives, the monitor sees the wire, the analysis port publishes it, the predictor calls bus2reg and only then does the mirror change. It is the same drawing as the scoreboard, with a piece of the library instead of a hand-written class. The bonus, which can be seen in the example: since the predictor listens to the bus and not to the calls, the model also finds out about the transfers somebody else made — another sequence, another master, an ordinary module. Automatic prediction cannot do that even in principle.

see this slide in the deck ›

RAL

The whole path, from the call to the mirror

The RAL data path: from the write to the bus, and from the monitor to the mirror

This is the figure worth having on screen while reading the connect_phase: every one of the three lines of that function is an arrow of the drawing. set_sequencer splices the way down, predictor.map/predictor.adapter give the predictor the two things it needs, and agent_h.ap.connect(...) is the arrow that goes up from the monitor. The symmetry is the point: the adapter shows up twice, once per direction, and it is the only piece of the drawing that knows there is an APB underneath. Swapping it for an AHB one moves those two boxes and no other. And the right half is not RAL: the monitor, the analysis port and the agent are the ones from the capstone, without a change. Worth saying it with a finger on the drawing, because it is what explains that the unit fits in three files. The dashed shortcut is the trap of the unit. With set_auto_predict(1) that arrow is enough for the mirror to change, so the bottom half of the drawing turns decorative: the model believes what the test meant to send, not what happened.

see this slide in the deck ›

RAL

The stimulus, without a single PADDR

code/u9/ral/tb_classes/ral_test.svh · #the-stimulus
      // --- the frontdoor -----------------------------------------------------
      // write() puts the value on the wire through the sequencer. There is no
      // second argument for the address: the address is in the map.
      model.CTRL.write(status, 'h1);              // EN = 1
      model.CTRL.read(status, data);
      `uvm_info("RAL", $sformatf("CTRL read back 0x%08h (status %s)", data, status.name()),
                UVM_MEDIUM)

      // --- prediction --------------------------------------------------------
      // Nothing above updated the mirror by hand. The monitor saw the two
      // transfers, the predictor turned them into predictions, and the model
      // knows -- with set_auto_predict(0), which is the honest setting: what the
      // model believes comes from the BUS, not from what the test meant to send.
      if (model.CTRL.EN.get_mirrored_value() != 1)
         `uvm_error("RAL", "the predictor did not update CTRL.EN")
      `uvm_info("RAL", $sformatf("mirrored CTRL.EN = %0d, and nobody assigned it",
                model.CTRL.EN.get_mirrored_value()), UVM_MEDIUM)
el archivo entero, 82 líneas
// What the register layer buys, in ten lines of stimulus: not one PADDR, not one
// PWDATA, not one wait state. The same driver as the capstone is doing all of
// that underneath.
class ral_test extends ral_base_test;
   `uvm_component_utils(ral_test)

   function new(string name, uvm_component parent);
      super.new(name, parent);
   endfunction : new

   task run_phase(uvm_phase phase);
      uvm_status_e   status;
      uvm_reg_data_t data;

      phase.raise_objection(this);
      reset_model();

      // --- the frontdoor -----------------------------------------------------
      // write() puts the value on the wire through the sequencer. There is no
      // second argument for the address: the address is in the map.
      model.CTRL.write(status, 'h1);              // EN = 1
      model.CTRL.read(status, data);
      `uvm_info("RAL", $sformatf("CTRL read back 0x%08h (status %s)", data, status.name()),
                UVM_MEDIUM)

      // --- prediction --------------------------------------------------------
      // Nothing above updated the mirror by hand. The monitor saw the two
      // transfers, the predictor turned them into predictions, and the model
      // knows -- with set_auto_predict(0), which is the honest setting: what the
      // model believes comes from the BUS, not from what the test meant to send.
      if (model.CTRL.EN.get_mirrored_value() != 1)
         `uvm_error("RAL", "the predictor did not update CTRL.EN")
      `uvm_info("RAL", $sformatf("mirrored CTRL.EN = %0d, and nobody assigned it",
                model.CTRL.EN.get_mirrored_value()), UVM_MEDIUM)

      // --- mirror(UVM_CHECK): the read that checks itself --------------------
      model.SCRATCH.write(status, 'h1000_0000);
      model.SCRATCH.mirror(status, UVM_CHECK);

      // --- and the part RAL does not do --------------------------------------
      // ACC changed because of the write to SCRATCH, at another address. The
      // model never predicted that and never will: mirror(UVM_CHECK) is off on
      // ACC (see the reg block). The read still updates the mirror, so after
      // this line the model agrees with the DUT again.
      model.ACC.read(status, data);
      `uvm_info("RAL", $sformatf("ACC = 0x%08h -- predicted by the SCOREBOARD, not by the model",
                data), UVM_MEDIUM)
      if (data != 'h1000_0000)
         `uvm_error("RAL", $sformatf("ACC read 0x%08h, expected 0x10000000", data))

      // CLR is "WC": the write clears the accumulator, the mirror drops to zero
      // and the bit reads 0, so the mirror(UVM_CHECK) right after really compares
      // it -- which is the point of getting the access policy right.
      model.CTRL.write(status, 'h3);             // EN = 1, CLR = 1
      model.CTRL.mirror(status, UVM_CHECK);
      model.ACC.read(status, data);
      if (data != 0) `uvm_error("RAL", $sformatf("after CLR, ACC = 0x%08h", data))

      // STATUS.EN is a copy of CTRL.EN, and it got there through a transfer to a
      // DIFFERENT address. Without the set_compare(UVM_NO_CHECK) of the reg
      // block this line fails -- and the failure would be the model's fault, not
      // the DUT's. Knowing which of the two is lying is the whole skill.
      //
      // The two UVM_WARNING it prints (UVM/FLD/GET_MIRRORED_VAL/VOL) are the
      // library saying the same thing on its own: a volatile field's mirror is
      // not evidence. They are the expected output of this line, not noise.
      `uvm_info("RAL", "the two VOL warnings below are the point, not a problem",
                UVM_MEDIUM)
      model.STATUS.mirror(status, UVM_CHECK);

      // --- the status that only a bus can give -------------------------------
      // Every call returns a status, and it is not decoration: PSLVERR comes back
      // through bus2reg as UVM_NOT_OK. A pure software model could not tell you
      // that the slave refused the access -- it is the one thing in this file
      // that could only come from the wire.
      if (status != UVM_IS_OK)
         `uvm_error("RAL", $sformatf("the mapped accesses came back %s", status.name()))

      phase.drop_objection(this);
   endtask : run_phase

endclass : ral_test

bash run.sh · code/u9/ral/

It is worth running it live and looking at the log with grep MONITOR: for every line of stimulus there is a complete APB transfer, with its address and its handshake, done by the driver they wrote. It is the demonstration that RAL does not replace the testbench, it drives it. The line most worth discussing is model.CTRL.read(status, data) against model.CTRL.mirror(status, UVM_CHECK). Both read from the DUT. The difference is that mirror compares against what the model believed before the read, and if it does not match it reports a uvm_error without anybody writing a check. It is a register scoreboard in one word. And the third form, which is not on the slide and is worth naming: set() + update(). set() changes the desired value in the model, without touching the bus, and update() writes only the registers whose desired value differs from the mirror. It is how a whole block gets configured at the start of a test, and it is where RAL stops being convenient and starts genuinely saving simulation time.

see this slide in the deck ›

RAL

The tests you did not write

code/u9/ral/tb_classes/builtin_test.svh · #the-two-seqs
      rst_seq = uvm_reg_hw_reset_seq::type_id::create("rst_seq");
      rst_seq.model = model;
      rst_seq.start(null);

      bash_seq = uvm_reg_bit_bash_seq::type_id::create("bash_seq");
      bash_seq.model = model;
      bash_seq.start(null);
el archivo entero, 40 líneas
// The argument for RAL that survives a code review: tests you did not write.
//
// Both sequences below ship with uvm-core. They read the register model -- the
// addresses, the field positions, the access strings -- and generate the
// stimulus and the checks from it. Nothing here is specific to this DUT.
//
//   uvm_reg_hw_reset_seq   reads every register after reset and compares it
//                          against the reset value declared in configure()
//   uvm_reg_bit_bash_seq   writes and reads back every writable bit of every
//                          register, one at a time, in both directions
//
// Run it with +MAL and the register model declares CTRL.CLR as "RW" instead of
// "WC" -- one wrong word in one string. bit_bash catches it without anyone
// writing a test for CTRL.
class builtin_test extends ral_base_test;
   `uvm_component_utils(builtin_test)

   function new(string name, uvm_component parent);
      super.new(name, parent);
   endfunction : new

   task run_phase(uvm_phase phase);
      uvm_reg_hw_reset_seq rst_seq;
      uvm_reg_bit_bash_seq bash_seq;

      phase.raise_objection(this);
      reset_model();

      rst_seq = uvm_reg_hw_reset_seq::type_id::create("rst_seq");
      rst_seq.model = model;
      rst_seq.start(null);

      bash_seq = uvm_reg_bit_bash_seq::type_id::create("bash_seq");
      bash_seq.model = model;
      bash_seq.start(null);

      phase.drop_objection(this);
   endtask : run_phase

endclass : builtin_test

This pair of sequences is the RAL argument that wins discussions, because it does not appeal to taste: they are two real tests, free, that on a block of 200 registers cover 80 % of the bugs of address decoding and of fields wired the wrong way round. There are more in the library and it is worth naming them: uvm_reg_access_seq (writes through the frontdoor and reads through the backdoor, and the other way round), uvm_reg_shared_access_seq for registers in more than one map, and uvm_mem_walk_seq for memories. The ones that need a backdoor stay out of the course for what was said before. The implementation detail worth telling, because it explains why the model has to be right: bit_bash does not skip the read-only fields — it writes the bit and demands that they read back as they were —, and it does the same with the reserved bits, which it treats as RO (uvm_reg_bit_bash_seq.svh:137-139, 149-152). What it really skips is the WO* family, NOACCESS and the fields with set_compare(UVM_NO_CHECK); individually_accessible it does not even look at. And it uses the access string to know what to expect back. Which means the quality of the test they give you is exactly the quality of the model you wrote.

see this slide in the deck ›

RAL

And that is why the model is the spec

    CTRL.CLR as "WC"   0 UVM_ERROR, and bit_bash did write bit 1
    CTRL.CLR as "RW"   2 UVM_ERROR, from uvm_reg_bit_bash_seq
    > Writing a 1 in bit #1 of register "model.CTRL" ...
      yielded 'h0000000000000000 instead of 'h0000000000000002

bash run.sh runs both versions and compares them

This is the slide that justifies the unit, and the run.sh of the example produces it on its own: it runs builtin_test with the model right and with +MAL, which declares CLR as RW, and shows the two numbers side by side. The punchline to say out loud: in the capstone, that same trap cost writing a scoreboard, modelling the autoclear by hand and discovering the bug with a message of your own. Here it costs picking one word well, and the error message is written by Accellera, with the number of the bit inside. The honest downside, which has to be said so it does not sound like a brochure: if you get it wrong the other way round —you declare RW a field the DUT implemented as RW when the spec said something else— the model and the DUT agree on the mistake and no sequence says anything. The register model verifies the DUT against the spec written in the model, not against the real spec. That is why the review of the model against the document is still human work, and that is why stage 1 of the exercise prints the map so it can be compared by hand.

see this slide in the deck ›

RAL

What RAL does not do, and it has to be said

code/u9/ral/tb_classes/apb_reg_block.svh · #no-check
      // The honest line of the whole unit. ACC and STATUS have an address, but
      // they are not registers: their value is produced by writes to OTHER
      // addresses. A register model predicts "what I wrote is what I will read",
      // and for these two that sentence is false, so mirror(UVM_CHECK) is turned
      // off on them. Reads still update the mirror -- a read is a prediction --
      // and the accumulator itself stays where it was in the capstone: in the
      // scoreboard, which is the thing that does know how to add.
      ACC.VALUE.set_compare(UVM_NO_CHECK);
      STATUS.EN.set_compare(UVM_NO_CHECK);
      STATUS.OVF.set_compare(UVM_NO_CHECK);
el archivo entero, 149 líneas
// The register map of spec.md, written as a UVM register model.
//
// The table of the spec maps one to one onto this file, and that is the check
// worth doing out loud: four registers, six fields, four addresses. Nothing here
// knows that the bus is APB -- swap the adapter and the same model drives AHB.
//
// The access string of every field IS the specification. Get it wrong and the
// built-in sequences of the last file catch it; see the +MAL run of run.sh.

// 0x00 -- CTRL. EN is an ordinary bit. CLR is not: writing a 1 clears ACC and
// OVF, and the bit always reads back 0.
class ctrl_reg extends uvm_reg;
   `uvm_object_utils(ctrl_reg)

   rand uvm_reg_field EN;
   rand uvm_reg_field CLR;

   function new(string name = "ctrl_reg");
      super.new(name, 32, UVM_NO_COVERAGE);
   endfunction : new

   // configure(parent, size, lsb_pos, access, volatile, reset, has_reset,
   //           is_rand, individually_accessible)
   virtual function void build();
      EN = uvm_reg_field::type_id::create("EN");
      EN.configure(this, 1, 0, "RW", 0, 'h0, 1, 1, 0);

      // "WC" is the name UVM has for exactly this: the write clears the field,
      // the mirror drops to zero, and the read returns that zero. Modelling it as
      // "RW" is the classic mistake, and it is what +MAL does. "WOC" also passes,
      // for the wrong reason: any WO* access takes the field out of bit_bash and
      // out of do_check, so nobody looks at it. See the RAL unit slides.
      CLR = uvm_reg_field::type_id::create("CLR");
      CLR.configure(this, 1, 1, $test$plusargs("MAL") ? "RW" : "WC", 0, 'h0, 1, 1, 0);
   endfunction : build

endclass : ctrl_reg

// 0x04 -- SCRATCH. A plain RW register. That every write to it also adds to ACC
// is a side effect the model does not know about, and does not need to.
class scratch_reg extends uvm_reg;
   `uvm_object_utils(scratch_reg)

   rand uvm_reg_field VALUE;

   function new(string name = "scratch_reg");
      super.new(name, 32, UVM_NO_COVERAGE);
   endfunction : new

   virtual function void build();
      VALUE = uvm_reg_field::type_id::create("VALUE");
      VALUE.configure(this, 32, 0, "RW", 0, 'h0, 1, 1, 1);
   endfunction : build

endclass : scratch_reg

// 0x08 -- ACC. Read-only, and volatile: the DUT changes it behind the model's
// back, on every write to SCRATCH. See the comment on set_compare() below.
class acc_reg extends uvm_reg;
   `uvm_object_utils(acc_reg)

   uvm_reg_field VALUE;

   function new(string name = "acc_reg");
      super.new(name, 32, UVM_NO_COVERAGE);
   endfunction : new

   virtual function void build();
      VALUE = uvm_reg_field::type_id::create("VALUE");
      VALUE.configure(this, 32, 0, "RO", 1, 'h0, 1, 0, 1);
   endfunction : build

endclass : acc_reg

// 0x0C -- STATUS. Two read-only bits, and both are volatile for the same reason
// as ACC: EN is a copy of CTRL.EN, and OVF turns on when the accumulator
// overflows -- neither of them happens on a write to STATUS.
class status_reg extends uvm_reg;
   `uvm_object_utils(status_reg)

   uvm_reg_field EN;
   uvm_reg_field OVF;

   function new(string name = "status_reg");
      super.new(name, 32, UVM_NO_COVERAGE);
   endfunction : new

   virtual function void build();
      EN = uvm_reg_field::type_id::create("EN");
      EN.configure(this, 1, 0, "RO", 1, 'h0, 1, 0, 0);
      OVF = uvm_reg_field::type_id::create("OVF");
      OVF.configure(this, 1, 1, "RO", 1, 'h0, 1, 0, 0);
   endfunction : build

endclass : status_reg

class apb_reg_block extends uvm_reg_block;
   `uvm_object_utils(apb_reg_block)

   rand ctrl_reg    CTRL;
   rand scratch_reg SCRATCH;
   acc_reg          ACC;
   status_reg       STATUS;

   function new(string name = "apb_reg_block");
      super.new(name, UVM_NO_COVERAGE);
   endfunction : new

   // Not build_phase: a uvm_reg_block is a uvm_object, not a component. Nobody
   // calls this for you -- the test does, right after the create().
   virtual function void build();
      // create_map(name, base_addr, n_bytes, endian, byte_addressing)
      default_map = create_map("default_map", 'h0, 4, UVM_LITTLE_ENDIAN, 1);

      CTRL = ctrl_reg::type_id::create("CTRL");
      CTRL.configure(this, null, "");
      CTRL.build();
      default_map.add_reg(CTRL, CTRL_ADDR, "RW");

      SCRATCH = scratch_reg::type_id::create("SCRATCH");
      SCRATCH.configure(this, null, "");
      SCRATCH.build();
      default_map.add_reg(SCRATCH, SCRATCH_ADDR, "RW");

      ACC = acc_reg::type_id::create("ACC");
      ACC.configure(this, null, "");
      ACC.build();
      default_map.add_reg(ACC, ACC_ADDR, "RO");

      STATUS = status_reg::type_id::create("STATUS");
      STATUS.configure(this, null, "");
      STATUS.build();
      default_map.add_reg(STATUS, STATUS_ADDR, "RO");

      // The honest line of the whole unit. ACC and STATUS have an address, but
      // they are not registers: their value is produced by writes to OTHER
      // addresses. A register model predicts "what I wrote is what I will read",
      // and for these two that sentence is false, so mirror(UVM_CHECK) is turned
      // off on them. Reads still update the mirror -- a read is a prediction --
      // and the accumulator itself stays where it was in the capstone: in the
      // scoreboard, which is the thing that does know how to add.
      ACC.VALUE.set_compare(UVM_NO_CHECK);
      STATUS.EN.set_compare(UVM_NO_CHECK);
      STATUS.OVF.set_compare(UVM_NO_CHECK);

      lock_model();
   endfunction : build

endclass : apb_reg_block

If they take a single idea away from this unit, let it be this one: an address is not a register. RAL models addressable storage, and it does it very well. It does not model behaviour. The accumulator of the capstone —which adds only with EN=1, which overflows, which gets cleared with CLR— is behaviour, and that lives in the scoreboard, as it lived yesterday. The failure mode of not saying it is the worst of all and it is an everyday one: the team puts RAL in, switches on mirror(UVM_CHECK) over the whole map, and the regression fills up with errors on the status registers. Since they are false positives, somebody switches the check off entirely — and from then on the model verifies nothing and nobody finds out. The difference between switching off three named fields and switching off the global check is the whole difference. The two UVM_WARNING the example prints —GET_MIRRORED_VAL/VOL— are the library saying the same thing on its own account: the mirror of a volatile field is not evidence. They are in the output on purpose.

see this slide in the deck ›

RAL

Summary of the unit

The close of the unit and of the technical course. The last line is not marketing: when writing this unit it was first tested whether uvm_reg_adapter and the frontdoor worked on Verilator, before writing a single slide, and the result is measured in docs/en/verilator.md. Everything the unit uses works: the model, the adapter, the uvm_reg_predictor, mirror(UVM_CHECK) and the two sequences of the library. For whoever goes on alone, the order of what is missing: uvm_reg_access_seq with a backdoor, generating the model from IP-XACT or SystemRDL —which is how it is really done—, and uvm_mem for memories. All three are extensions of what they saw here, not new topics. And the bridge to the exercise: the first thing they are going to be asked for in a project with registers is exactly this, with the spreadsheet of the spec on the left-hand side of the screen.

see this slide in the deck ›

Exercise · Day 8 · 1 of 2

Modelling the register map

cd code/ejercicios/d8-ral && cat README.md

It is the shortest exercise of the fifteen and the one most like a real task from the first weeks of a project. Forty lines, and half of them are copying and pasting the pattern of CTRL. The stage that teaches is stage 1, and for a reason of method: the model gets printed —+UVM_TESTNAME=mapa_test— and gets compared line by line against the register table of the exercise README, before simulating anything. It is what has to be done with a real register model, where the typical mistake is not about UVM but an offset copied wrong from a 200-row spreadsheet. Stage 2 is where CLR falls: whoever declared it RW passes stage 1 without a problem and finds out here, with the number of the bit, thanks to a test they did not write. It is worth letting them fall there — it is the moment when the RAL argument stops being an opinion. And stage 3 is the one that separates: it asks you to switch off a check, which is the opposite of what the course has been asking for the last seven days. The justification has to come from them: STATUS.EN changed because of a write to another address, the model cannot know it, and a false positive that gets let through ends up with somebody switching the whole check off.

see this slide in the deck ›

The reference model in C

The scoreboard that rewrites the spec

This is the technique that separates a course testbench from a production one, and the argument is not about convenience: it is about authority. In a project with serious arithmetic, the spec is not a PDF — it is a program in C that somebody ran against a thousand vectors before the RTL existed. A scoreboard that reimplements that program in SystemVerilog does not verify the DUT against the spec: it verifies the DUT against the reading the verification engineer made of the spec, which is another thing. The question worth throwing at the group before showing the code: "if the C model and the scoreboard differ, who is right?". The uncomfortable answer is that you do not know, and finding out costs days. With DPI the question does not exist because there is only one model. And the reason why this section is so cheap in this course: DPI is part of the LRM and the commercial simulators ship it as a base feature —what leaves it out are the free student editions—; what costs you in a commercial flow is the toolchain: -sv_lib, compiling the .c separately, and remembering the library on every run. Here the simulator compiles to C++ by design, so the bridge is one more file on the command line and there is nothing to configure.

see this slide in the deck ›

The reference model in C

import "DPI-C": one declaration on each side

code/u8/dpi/vtalu_pkg.sv · #the-imports
   // The reference model lives in vtalu_golden.c and is called from here. This
   // is all that is needed on the SystemVerilog side: one declaration per
   // function, with the LRM types that cross the boundary.
   //
   //   input int  -> int          output int -> int *
   //   function   -> returns      task       -> may consume time (not here)
   //
   // The name after "DPI-C" is the symbol the linker looks for, so it has to
   // match the one in the .c letter by letter.
   import "DPI-C" function int  vtalu_golden(input int op, input int a,
                                             input int b, output int ovf);
   import "DPI-C" function void vtalu_golden_bug(input int on);
el archivo entero, 63 líneas
package vtalu_pkg;
   import uvm_pkg::*;
   `include "uvm_macros.svh"

   // The reference model lives in vtalu_golden.c and is called from here. This
   // is all that is needed on the SystemVerilog side: one declaration per
   // function, with the LRM types that cross the boundary.
   //
   //   input int  -> int          output int -> int *
   //   function   -> returns      task       -> may consume time (not here)
   //
   // The name after "DPI-C" is the symbol the linker looks for, so it has to
   // match the one in the .c letter by letter.
   import "DPI-C" function int  vtalu_golden(input int op, input int a,
                                             input int b, output int ovf);
   import "DPI-C" function void vtalu_golden_bug(input int on);

   typedef enum bit [2:0] {
      no_op  = 3'b000,
      add_op = 3'b001,
      sub_op = 3'b010,
      and_op = 3'b011,
      xor_op = 3'b100,
      mul_op = 3'b101,
      rst_op = 3'b111
   } operation_t;

   // Configs first: the driver and the monitors use them.
   `include "env_config.svh"
   `include "vtalu_agent_config.svh"

   `include "command_transaction.svh"
   `include "add_transaction.svh"
   `include "result_transaction.svh"

   // The sequencer is not extended: it is parameterized and given a name.
   // It goes AFTER command_transaction and BEFORE the driver and the agent.
   typedef uvm_sequencer #(command_transaction) sequencer;

   // Sequences are uvm_object: they go before the tests that start them.
   `include "reset_sequence.svh"
   `include "random_sequence.svh"
   `include "maxmult_sequence.svh"
   `include "fibonacci_sequence.svh"
   `include "full_sequence.svh"

   `include "coverage.svh"
   `include "scoreboard.svh"
   `include "driver.svh"
   `include "command_monitor.svh"
   `include "result_monitor.svh"

   `include "vtalu_agent.svh"
   `include "env.svh"

   `include "base_test.svh"
   `include "full_test.svh"
   `include "fibonacci_test.svh"
   `include "add_test.svh"
   `include "default_seq_test.svh"
   `include "no_objection_test.svh"

endpackage : vtalu_pkg
code/u8/dpi/vtalu_golden.c · #the-golden
/* op, a and b arrive as SystemVerilog `int` (32-bit signed). The result goes
 * back as the return value and the borrow through a pointer, which is how an
 * `output int` argument crosses the boundary. */
int vtalu_golden(int op, int a, int b, int *ovf) {
   int r;

   a &= 0xff;
   b &= 0xff;
   *ovf = 0;

   switch (op) {
      case ADD_OP: r = a + b; break;
      /* 8-bit operands into a 16-bit result: a - b wraps, and the borrow is
       * the second half of the answer the scoreboard has to check. */
      case SUB_OP: r = a - b; *ovf = (a < b); break;
      case AND_OP: r = a & b; break;
      case XOR_OP: r = a ^ b; break;
      case MUL_OP: r = mutar ? (a * b) & 0xff : a * b; break;
      default:     r = 0; break;   /* no_op and rst_op do not answer */
   }

   return r & 0xffff;
}
el archivo entero, 62 líneas
/* The VTALU reference model, in C. This is the whole point of the example:
 * the scoreboard does NOT predict in SystemVerilog -- it asks this file.
 *
 * Why it matters: for a DUT with real arithmetic -- a DSP, a codec, a crypto
 * engine -- the reference model already exists in C, written by the algorithm
 * team, and it is the thing the specification was signed off against.
 * Rewriting it in SystemVerilog means maintaining two models and debugging the
 * difference between them, which is not verification.
 *
 * Verilator compiles to C++, so this file is linked straight into the
 * simulation: no IPC, no socket, no FLI. One `import "DPI-C"` on the
 * SystemVerilog side (vtalu_pkg.sv) and one function call on this side.
 *
 * The `extern "C"` guard is not decoration: Verilator hands user sources to
 * the C++ compiler, and without it the symbol gets mangled and the link fails
 * with an undefined reference that names a function that is obviously there.
 */
#include <svdpi.h>

#ifdef __cplusplus
extern "C" {
#endif

/* Same opcodes as operation_t in vtalu_pkg.sv. Keeping the two in sync by hand
 * is the tax of DPI, and it is the first thing to check when every comparison
 * fails at once. */
enum { NO_OP = 0, ADD_OP = 1, SUB_OP = 2, AND_OP = 3, XOR_OP = 4, MUL_OP = 5, RST_OP = 7 };

/* A deliberate mutation, switched on with +GOLDEN_BUG. A scoreboard that never
 * saw an error is not tested: this is what proves the DPI path is actually
 * wired, and it is the same idea as the +BUG=1 of the day 7 capstone. */
static int mutar = 0;

void vtalu_golden_bug(int on) { mutar = on; }

/* op, a and b arrive as SystemVerilog `int` (32-bit signed). The result goes
 * back as the return value and the borrow through a pointer, which is how an
 * `output int` argument crosses the boundary. */
int vtalu_golden(int op, int a, int b, int *ovf) {
   int r;

   a &= 0xff;
   b &= 0xff;
   *ovf = 0;

   switch (op) {
      case ADD_OP: r = a + b; break;
      /* 8-bit operands into a 16-bit result: a - b wraps, and the borrow is
       * the second half of the answer the scoreboard has to check. */
      case SUB_OP: r = a - b; *ovf = (a < b); break;
      case AND_OP: r = a & b; break;
      case XOR_OP: r = a ^ b; break;
      case MUL_OP: r = mutar ? (a * b) & 0xff : a * b; break;
      default:     r = 0; break;   /* no_op and rst_op do not answer */
   }

   return r & 0xffff;
}

#ifdef __cplusplus
}
#endif

It is worth reading the two declarations side by side, because the symmetry is the whole mechanism: the one above is the promise and the one below is who keeps it, and the compiler does not cross-check them — the one who joins them is the linker, at the end. Out of that comes the number one failure mode of DPI, which is worth naming before it shows up: an undefined reference to a function that is written, right there, in plain sight. Almost always it is one of two things — the name does not match, or the file got compiled as C++ and the symbol came out with a mangled name. That last one is literally what happens here: Verilator hands the user's sources to the C++ compiler, so the .c needs the extern "C" that is in the file. Without that guard, the example does not link and the error does not say why. The type table of the LRM is longer —there is svBitVecVal for wide vectors, svOpenArrayHandle for arrays— and it is not needed here. The rule of thumb: if the data fits in an int, the signature writes itself; if not, you look up annex H of the 1800 and copy it.

see this slide in the deck ›

The reference model in C

The scoreboard: the prediction gets asked for, not written

code/u8/dpi/tb_classes/scoreboard.svh · #predict_result
   // The ONLY difference with the sequences scoreboard, and it is the whole
   // section: there is no case with the arithmetic of the spec here. The
   // prediction is asked of the C reference model, the one that signed it.
   //
   // What does not change: the monitor, the analysis port, the FIFO, the
   // do_compare and the uvm_error. The golden model replaces SIX LINES -- the
   // ones that predict -- and no other piece of the testbench finds out.
   function result_transaction predict_result(command_transaction cmd);
      result_transaction predicted;
      int                ovf;

      predicted = result_transaction::type_id::create("predicted");

      predicted.result = vtalu_golden(int'(cmd.op), int'(cmd.A), int'(cmd.B), ovf);
      predicted.ovf    = (ovf != 0);

      return predicted;

   endfunction : predict_result
el archivo entero, 58 líneas
class scoreboard extends uvm_subscriber #(result_transaction);
   `uvm_component_utils(scoreboard);

   uvm_tlm_analysis_fifo #(command_transaction) cmd_f;

   function new(string name, uvm_component parent);
      super.new(name, parent);
   endfunction : new

   function void build_phase(uvm_phase phase);
      cmd_f = new("cmd_f", this);
   endfunction : build_phase

   // The ONLY difference with the sequences scoreboard, and it is the whole
   // section: there is no case with the arithmetic of the spec here. The
   // prediction is asked of the C reference model, the one that signed it.
   //
   // What does not change: the monitor, the analysis port, the FIFO, the
   // do_compare and the uvm_error. The golden model replaces SIX LINES -- the
   // ones that predict -- and no other piece of the testbench finds out.
   function result_transaction predict_result(command_transaction cmd);
      result_transaction predicted;
      int                ovf;

      predicted = result_transaction::type_id::create("predicted");

      predicted.result = vtalu_golden(int'(cmd.op), int'(cmd.A), int'(cmd.B), ovf);
      predicted.ovf    = (ovf != 0);

      return predicted;

   endfunction : predict_result

   function void write(result_transaction t);
      string data_str;
      command_transaction cmd;
      result_transaction predicted;

      do
      if (!cmd_f.try_get(cmd))
         `uvm_fatal("SELF CHECKER", "Missing command in self checker")
      while ((cmd.op == no_op) || (cmd.op == rst_op));

      predicted = predict_result(cmd);

      data_str = {
         cmd.convert2string(),
         " ==>  Actual ",
         t.convert2string(),
         "/Predicted ",
         predicted.convert2string()
      };

      if (!predicted.compare(t)) `uvm_error("SELF CHECKER", {"FAIL: ", data_str})
      else `uvm_info("SELF CHECKER", {"PASS: ", data_str}, UVM_HIGH)

   endfunction : write
endclass : scoreboard

The count is worth doing out loud because it is the design argument of the whole unit: one six-line function changed and nothing else did. That happens because the scoreboard was already well divided up —predicting and comparing are two jobs, and they were in two methods—, and it is the late reward for having separated predict_result from write three days ago. The int'(...) line is the one to point at: the UVM types do not cross the border. On the other side there is no command_transaction, there are three integers. When the data is an object —an image, a packet, a matrix— what gets passed is a pointer or an open array, and the one who decides the format is the C model, not the testbench. And the vtalu_golden_bug is not an ornament: a scoreboard that has never seen an error is not tested. run.sh runs the same test against a mutated model —the multiplication truncated to eight bits— and demands that the scoreboard scream. If it does not scream, the testbench is comparing against itself and nobody was going to find out. It is the same mutation test the checker of the capstone does with +BUG=1, from the other side of the wire.

see this slide in the deck ›

The reference model in C

What you gain, what you pay

You gain You pay
One single model, the one that signed off the spec the opcodes get synchronized by hand: one enum on each side
The model gets tested with the tools of C an undefined reference does not say which of the two declarations is wrong
Arithmetic that in SystemVerilog is painful —floating point, matrices, crypto— the C model runs in zero time: it cannot wait for an edge
Performance: it is a function call, not an IPC a segfault in the model takes the whole simulation down with it

The table is what has to be left written down, and the row that gives the most discussion is the first one on the right: the duplicated enum. It is real and it has no elegant solution —the LRM does not share types between the two sides—, and it is the first thing to look at when all the comparisons fail at once. If one in every six fails, it is the DUT; if all of them fail, it is the mapping. The third row on the right is the one that surprises and is worth getting ahead of: a DPI function runs in zero time, so the C model cannot synchronize with the clock. That is what import "DPI-C" task is for — with one clarification almost every tutorial skips: C cannot block by itself. A DPI task consumes time only if it is declared context and the C calls back into an export "DPI-C" task of SystemVerilog, which is the one that waits for the edge. And there it is no longer a golden model, it is a bus model, which is another topic. The rule of the second-to-last bullet is the one to take away, because the risk of this section is that somebody comes out wanting to do everything through DPI. The VTALU does not justify DPI: five eight-bit operations get written in a case and read better. The example exists so the technique has been seen when the DUT that does justify it comes along, and that DUT arrives sooner than you think.

see this slide in the deck ›

Review · Day 8

1 of 8 · The access string

CLR clears itself when you write a 1 to it. You declare it "WOC", and bit_bash and mirror(UVM_CHECK) give zero errors. What happened?

Green is not checked — uvm_reg_bit_bash_seq.svh:129-133 skips every field whose access starts with WO ("you are not supposed to read them"), and do_check takes it out of the comparison mask (uvm_reg.svh:2782-2788). The only trace is in the timing: bashing CTRL takes four transfers less. The right access is WC (or W1C), and with it the bit does get bashed. A WO* access is the cheapest way there is of switching a check off without noticing.

see this slide in the deck ›

Review · Day 8

2 of 8 · Automatic or explicit prediction

What changes between set_auto_predict(1) and hooking a uvm_reg_predictor to the monitor?

The one who checks cannot believe the one who drives — with set_auto_predict(1), model.CTRL.write() updates the mirror at the moment of the call, before the bus has done anything: if the driver sends the transfer wrong, the model stays convinced. With the predictor, the mirror only changes once the monitor has seen the wire. It is the same drawing as the capstone scoreboard, with a library piece instead of a hand-written class. And it is free: the monitor and its analysis port were already there.

see this slide in the deck ›

Review · Day 8

3 of 8 · volatile

STATUS changes on its own, without anybody writing it. What are you saying when you declare it volatile in configure()?

The mirror stops being evidence — a model predicts "what I wrote is what I am going to read", and for a volatile field that sentence is false. The two UVM_WARNING GET_MIRRORED_VAL/VOL of the example are the library saying exactly that, and they are in the output on purpose. What does have to be checked gets checked where it is known: in the scoreboard, and in the model goes set_compare(UVM_NO_CHECK) on that field so that RAL does not invent an error.

see this slide in the deck ›

Review · Day 8

4 of 8 · mirror() against read()

model.CTRL.read(status, data) and model.CTRL.mirror(status, UVM_CHECK). How do they differ?

mirror is a register scoreboard in one word — both read from the DUT; the difference is that mirror compares against what the model believed before the read, and if it does not match it reports a uvm_error without anybody writing a check. And a read is a prediction: both update the mirror afterwards.

see this slide in the deck ›

Review · Day 8

5 of 8 · Who knows about the bus

What does the uvm_reg_block know about the APB?

The model does not know there is an APB underneath — swap the adapter and the same model drives an AHB. The adapter is twenty lines, once per protocol, and it is the file that comes with a VIP you bought. Watch out for one thing: bus2reg is called by the predictor too, with the item the monitor saw, so an adapter that depends on something the driver put there works in one direction and fails in the other.

see this slide in the deck ›

Review · Day 8

6 of 8 · DPI and time

Can an import "DPI-C" function of the golden model wait for a clock edge?

Zero time, like any function — to consume time you need import "DPI-C" task, and with a clarification almost every tutorial skips: C cannot block by itself. A DPI task consumes time only if it is declared context and from the C it calls back into an export "DPI-C" task of SystemVerilog, which is the one that waits for the edge. And at that point it is no longer a golden model, it is a bus model.

see this slide in the deck ›

Review · Day 8

7 of 8 · undefined reference

The .c is written and it compiles, and the link fails with undefined reference to the golden model function. What do you look at first?

The compiler does not cross the two declarations: the one that joins them is the linker — hence an error about something that is written, right there, in plain sight. It is almost always one of two things: the name does not match letter for letter, or the file got compiled as C++ and the symbol came out mangled. That last one is literally the case here: Verilator hands the user's sources to the C++ compiler, so the .c needs its extern "C". And there is no DPI flag: the .c goes on the verilator command line like any other source.

see this slide in the deck ›

Review · Day 8

8 of 8 · The scoreboard that never shouted

The scoreboard with the golden model in C runs a thousand operations and reports not one. Is that enough?

The model has to be broken on purpose — vtalu_golden_bug(1) mutates the multiplication and run.sh demands that the scoreboard shout. If it does not shout, the testbench is comparing against itself and nobody was going to find out: it is the same mutation test the capstone grader does with +BUG=1, from the other side of the wire. And the duplicated enum is real but it is a different symptom: if every comparison fails at once, it is the mapping; if one in six fails, it is the DUT.

see this slide in the deck ›

Exercise · Day 8 · 2 of 2

The reference model in C, and the two mutations that prove it

cd code/ejercicios/d8-dpi && bash run.sh

Stage 2 is what makes the exercise worth it, and it is worth explaining first: against the healthy DUT, a model returning some fixed thing could pass by accident if the stimulus were poor. With the DUT lying on bit 0, there is no way of passing without having computed — it is the same mutation test as the capstone, pointed at the model instead of at the scoreboard. The subtraction is the one that trips people, and not because of the arithmetic: a - b is one line. What gets forgotten is the second half of the answer, the borrow, and it gets forgotten because it crosses the boundary in another way —through a pointer, which is how a SystemVerilog output int travels—. The scoreboard compares it, so without it every subtraction fails and no other operation does: that pattern of failures is the one to learn to read. And the trap you cannot see, which is solved in the skeleton on purpose so they do not lose the hour there: without extern "C", Verilator hands the file to the C++ compiler, the symbol comes out mangled, and the link fails with an undefined reference to a function written two lines above. It is DPI's number one failure mode with Verilator.

see this slide in the deck ›

Capstone 2 · Day 8

The second final: a FIFO with backpressure

cd code/ejercicios/d8-fifo && cat spec.en.md

This is the exercise that separates whoever understood from whoever copied the pattern, and the mechanism by which it separates is in the third bullet: the bug is in a control output, not in the data path. A scoreboard that compares what comes out of rd_data closes six of the seven rows of the plan and passes green with the DUT broken. To see it the flags have to be predicted, and to predict the flags the occupancy has to be modelled — that is, writing a reference model with state, which is what the first capstone could not ask for. It is worth saying why it goes second and not first: the APB is the DUT you run into in the first year, and the table-address-value pattern is the right one there. This one teaches when that pattern stops being any use, and that lesson is not understood without having used the pattern before. The trap that catches the most people is the order of updating the model: first look at whether the read takes something out —that frees a place— and only then whether the write goes in. The other way round, the simultaneous write with the FIFO full shows up as a lost piece of data that the DUT never lost, and the student spends an hour looking for the bug in the RTL. And the second one: the check_phase that claims data that never came out. Almost always the test ended one cycle too early, and it is the drain_time of day 3 showing up where nobody expected it.

see this slide in the deck ›