Verify This! · el curso para leer

Día 8RAL, el golden model y el segundo capstone

Las mismas slides del deck, con las notas del instructor adentro del texto y el código completo. Si estás haciendo el curso solo, empezá por acá y tené el deck al lado para los repasos.

Ayer quedó…

Dónde dejamos el testbench

Este día es opcional y va después del cierre, y las dos cosas son a propósito. El curso termina en el día 7: el que hizo hasta ahí escribió un testbench de UVM completo desde una hoja en blanco y no le falta nada para trabajar. Lo de acá es lo que se agrega cuando ya pasó eso. La recap tiene una función concreta el día 8: las tres piezas de hoy sólo se entienden como reemplazo de algo que el alumno acaba de escribir a mano. RAL reemplaza la predicción registro por registro, el golden model en C reemplaza la predicción entera, y el segundo capstone muestra dónde deja de servir el patrón del primero. Sin el capstone hecho, las tres son abstracciones sin problema.

ver esta slide en el deck ›

Agenda

Día 8 · ≈ 4 h · opcional · lo que sigue después del capstone

Al final del día podés:

Para el que dicta: en una empresa esto es media jornada más, y suele darse cuando el grupo tiene registros de verdad en el proyecto. En un cuatrimestre es la unidad optativa, o el trabajo final de los que quieren nota alta. Los tres objetivos son deliberadamente concretos y no "entender RAL": el día 8 no tiene fila en la autoevaluación de 175b, así que la rúbrica que el alumno se lleva son estos tres verbos y el repaso de ocho preguntas. El tercero es el que más rinde de los tres y el que menos se espera — una FIFO tiene orden y ocupación, y eso no entra en una tabla de cuatro filas.

ver esta slide en el deck ›

RAL

Unidad 9 · opcional · el registro deja de ser una dirección

Esta unidad es corta y es la última, y las dos cosas son deliberadas. RAL es la pieza de UVM que más se enseña sin contexto —hay cursos que la dan en el día 2— y así no se entiende: parece una API más. Con el capstone recién hecho, en cambio, el alumno tiene la comparación adentro de la cabeza. Él escribió el scoreboard que predice CTRL; ahora le muestro la biblioteca que lo hace sola, y también dónde no lo hace. El número de 200 registros no es retórico. Un bloque de PHY o de controlador de memoria tiene esa cantidad, y la spec de registros llega como una planilla. El paso siguiente, que este curso no da, es generar el modelo desde esa planilla con IP-XACT o SystemRDL: nadie escribe 200 registros a mano. Vale decirlo, porque si no el alumno se lleva la idea de que RAL es tipear mucho. Y el argumento de la mudanza de 0x04 a 0x24 es el que convence a un escéptico: no es elegancia, es que el mapa quede escrito una sola vez.

ver esta slide en el deck ›

RAL

Tres piezas, y una sola sabe qué bus hay abajo

Pieza Qué es Cuántas veces se escribe
uvm_reg_block el mapa: registros, campos, direcciones una por DUT
uvm_reg_adapter traduce acceso ↔ transacción del bus una por protocolo
uvm_reg_predictor lo que el monitor vio, dicho en registros ninguna: viene hecha

La tabla es la slide. Que la tercera columna diga una por DUT, una por protocolo y ninguna es todo el argumento económico de RAL, y es el que sostiene la decisión en una reunión de proyecto. Conviene marcar con el dedo la última línea: acá no se reemplaza nada de lo que el alumno escribió ayer. El code/u9/ral/ compila la interface, la transaction, el driver, el monitor y el agent de d7-final/solucion/ tal cual están, por +incdir. RAL es una capa, no un testbench distinto — y es la razón por la que esta unidad entra en tres archivos. Si alguien pregunta por el backdoor: existe, se declara con add_hdl_path y lee la señal del RTL sin pasar por el bus. No entra en el curso porque depende del soporte de jerarquía del simulador, que es justo lo que cambia entre herramientas. El frontdoor —el que usa el bus de verdad— es el que hay que saber.

ver esta slide en el deck ›

RAL

El modelo: la tabla de la spec, en código

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) — nueve argumentos posicionales, y es la firma más fea de UVM. Vale escribirla en el pizarrón una vez; después el alumno la copia del ejemplo, que es lo que hace todo el mundo. CLR es el campo que justifica la slide. La spec del capstone dice "escribir un 1 pone ACC y OVF en cero, y el bit siempre se lee 0", y ésa es la trampa 2 de la letra chica — la que hace fallar al scoreboard escrito a mano. El acceso que dice exactamente eso es WC: la escritura borra el campo, el espejo baja a cero, y la lectura devuelve ese cero. W1C sirve igual acá. Y acá está la trampa muda de RAL, que vale más que la slide. WOC —el nombre que uno escribe primero, porque suena a "write-only, clears"— también da cero errores, pero los da por el motivo equivocado: uvm_reg_bit_bash_seq.svh:129-133 saltea todo campo cuyo acceso empiece con WO (el comentario de Accellera dice "Ignore Write-only fields because you are not supposed to read them"), y do_check lo saca de la máscara de comparación (uvm_reg.svh:2782-2788), así que el mirror(UVM_CHECK) tampoco lo mira. Con WOC el bit 1 de CTRL no se testea nunca y nadie imprime nada; el único rastro está en los tiempos, porque el bashing de CTRL dura cuatro transferencias menos. Verde no es chequeado, y un acceso WO* es la forma más barata que hay de apagar un chequeo sin enterarse. La lista completa de accesos está en el LRM 1800.2, y son veinticinco. No hay que saberlos de memoria: hay que saber que existe la lista, y buscar ahí antes de inventar un set_compare o un callback. La mitad de los modelos de registros malos que uno se cruza son un campo RW con parches encima.

ver esta slide en el deck ›

RAL

El adapter: lo único que sabe que hay un 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

Veintidós líneas, y es el archivo entero. Vale decir el número en voz alta, porque la fama de RAL es de ser pesado y la parte específica del proyecto es ésta. La simetría reg2bus / bus2reg es la que hay que fijar: una función por sentido, y ninguna de las dos tiene estado. El uvm_reg_bus_op es la moneda común —kind, addr, data, status— y es a propósito lo más chato posible: cualquier bus de registros entra ahí. El const ref del reg2bus es de la firma de la clase base y hay que copiarlo tal cual. Anda en Verilator 5.052, que no era obvio. Y una advertencia de campo: bus2reg lo llama también el predictor, con el item que vio el monitor. Un adapter que lea algo del driver —o que dependa de haber armado él la transaction— funciona con predicción automática y se rompe el día que se pasa a explícita. Por eso las dos funciones son puras.

ver esta slide en el deck ›

RAL

Engancharlo: tres líneas y un 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

La trampa de fases que se lleva la primera tarde de todo el mundo es la última línea de la slide. apb_reg_block::type_id::create("model") devuelve un objeto con cero registros, porque build() acá es un método normal que se llama a mano —no el build_phase de un component, que UVM recorre solo—. El síntoma es un uvm_fatal de uvm_reg_map que habla de un registro nulo y no menciona la palabra build. La segunda cosa que hay que remarcar es de dónde sale el sequencer: es agent_h.sequencer_h, el mismo del capstone, el mismo que corren las sequences normales. RAL no trae un sequencer propio. Eso quiere decir que una sequence de registros y una de tráfico compiten por el mismo driver, y ahí aparecen las prioridades — que es tema de otro curso, pero conviene que sepan que existe.

ver esta slide en el deck ›

RAL

Predicción: quién le cuenta al modelo qué pasó

Quién predice Qué pasa si el DUT hace otra cosa
automática la propia llamada write() el modelo no se entera: cree lo que quiso mandar
explícita el uvm_reg_predictor, desde el monitor el modelo cree lo que pasó en el cable

Ésta es la slide de criterio de la unidad y conviene no apurarla. Con set_auto_predict(1), model.CTRL.write(status, 'h1) actualiza el espejo en el momento en que se llama, antes de que el bus haya hecho nada. Si el driver manda mal la transferencia, o el DUT la ignora, el modelo sigue convencido de que EN=1. Es exactamente el error de diseño que el curso ataca desde el día 1: el que chequea no puede creerle al que estimula. Con predicción explícita el camino es el largo y el correcto: el driver maneja, el monitor ve el cable, el analysis port lo publica, el predictor llama a bus2reg y recién ahí el espejo cambia. Es el mismo dibujo del scoreboard, con una pieza de la librería en lugar de una clase escrita a mano. El bonus, que se ve en el ejemplo: como el predictor escucha el bus y no las llamadas, el modelo también se entera de las transferencias que hizo otro — otra sequence, otro maestro, un módulo de siempre. La predicción automática no puede hacer eso ni en principio.

ver esta slide en el deck ›

RAL

El camino completo, de la llamada al espejo

El camino de datos de RAL: del write al bus, y del monitor al espejo

Ésta es la figura que conviene tener en pantalla mientras se lee el connect_phase: cada una de las tres líneas de esa función es una flecha del dibujo. set_sequencer empalma la bajada, predictor.map/predictor.adapter le dan al predictor las dos cosas que necesita, y agent_h.ap.connect(...) es la flecha que sube del monitor. La simetría es el punto: el adapter aparece dos veces, una por sentido, y es la única pieza del dibujo que sabe que abajo hay un APB. Cambiarlo por uno de AHB mueve esas dos cajas y ninguna otra. Y la mitad derecha no es de RAL: el monitor, el analysis port y el agent son los del capstone, sin un cambio. Vale decirlo con el dedo en el dibujo, porque es lo que explica que la unidad entre en tres archivos. El atajo punteado es la trampa de la unidad. Con set_auto_predict(1) esa flecha alcanza para que el espejo cambie, así que la mitad de abajo del dibujo se vuelve decorativa: el modelo cree lo que el test quiso mandar, no lo que pasó.

ver esta slide en el deck ›

RAL

El estímulo, sin un solo 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/

Vale correrlo en vivo y mirar el log con grep MONITOR: por cada línea de estímulo hay una transferencia de APB completa, con su dirección y su handshake, hecha por el driver que escribieron ellos. Es la demostración de que RAL no reemplaza al testbench, lo maneja. La línea que más rinde discutir es model.CTRL.read(status, data) contra model.CTRL.mirror(status, UVM_CHECK). Las dos leen del DUT. La diferencia es que mirror compara contra lo que el modelo creía antes de la lectura, y si no coincide reporta un uvm_error sin que nadie escriba un chequeo. Es un scoreboard de registros con una palabra. Y la tercera forma, que no está en la slide y conviene nombrar: set() + update(). set() cambia el valor deseado en el modelo, sin tocar el bus, y update() escribe sólo los registros cuyo valor deseado difiere del espejo. Es como se configura un bloque entero al arrancar un test, y es donde RAL deja de ser cómodo y pasa a ahorrar tiempo de simulación de verdad.

ver esta slide en el deck ›

RAL

Los tests que no escribiste

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

Este par de sequences es el argumento de RAL que gana discusiones, porque no apela al gusto: son dos tests reales, gratis, que en un bloque de 200 registros cubren el 80 % de los bugs de decodificación de direcciones y de campos cableados al revés. Hay más en la librería y vale nombrarlas: uvm_reg_access_seq (escribe por frontdoor y lee por backdoor, y viceversa), uvm_reg_shared_access_seq para registros en más de un mapa, y uvm_mem_walk_seq para memorias. Las que necesitan backdoor quedan fuera del curso por lo que se dijo antes. El detalle de implementación que conviene contar, porque explica por qué el modelo tiene que estar bien: bit_bash no saltea los campos de sólo lectura — les escribe el bit y exige que se lean como estaban—, y hace lo mismo con los bits reservados, que trata como RO (uvm_reg_bit_bash_seq.svh:137-139, 149-152). Lo que saltea de verdad es la familia WO*, NOACCESS y los campos con set_compare(UVM_NO_CHECK); individually_accessible ni lo consulta. Y usa el string de acceso para saber qué esperar de vuelta. O sea que la calidad del test que te regalan es exactamente la calidad del modelo que escribiste.

ver esta slide en el deck ›

RAL

Y por eso el modelo es la 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 corre las dos versiones y las compara

Ésta es la slide que justifica la unidad, y el run.sh del ejemplo la produce sola: corre builtin_test con el modelo bien y con +MAL, que declara CLR como RW, y muestra los dos números uno al lado del otro. El remate para decir en voz alta: en el capstone, esa misma trampa costó escribir un scoreboard, modelar el autoclear a mano y descubrir el bug con un mensaje propio. Acá cuesta elegir bien una palabra, y el mensaje de error lo escribe Accellera, con el número del bit adentro. La contra honesta, que hay que decir para que no suene a folleto: si te equivocás al revés —declarás RW un campo que el DUT implementó como RW cuando la spec decía otra cosa— el modelo y el DUT se ponen de acuerdo en el error y ninguna sequence dice nada. El modelo de registros verifica el DUT contra la spec escrita en el modelo, no contra la spec de verdad. Por eso la revisión del modelo contra el documento sigue siendo trabajo humano, y por eso la etapa 1 del ejercicio imprime el mapa para compararlo a mano.

ver esta slide en el deck ›

RAL

Lo que RAL no hace, y hay que decirlo

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

Si de esta unidad se llevan una sola idea, que sea ésta: una dirección no es un registro. RAL modela almacenamiento direccionable, y lo hace muy bien. No modela comportamiento. El acumulador del capstone —que suma sólo con EN=1, que desborda, que se limpia con CLR— es comportamiento, y eso vive en el scoreboard, como vivía ayer. El modo de falla de no decirlo es el peor de todos y es cotidiano: el equipo pone RAL, prende mirror(UVM_CHECK) sobre todo el mapa, y la regresión se llena de errores en los registros de estado. Como son falsos positivos, alguien apaga el chequeo entero — y a partir de ahí el modelo no verifica nada y nadie se entera. La diferencia entre apagar tres campos con nombre y apagar el chequeo global es toda la diferencia. Los dos UVM_WARNING que imprime el ejemplo —GET_MIRRORED_VAL/VOL— son la librería diciendo lo mismo por su cuenta: el espejo de un campo volatile no es evidencia. Están a propósito en la salida.

ver esta slide en el deck ›

RAL

Resumen de la unidad

El cierre de la unidad y del curso técnico. La última línea no es marketing: al escribir esta unidad se probó primero si uvm_reg_adapter y el frontdoor funcionaban en Verilator, antes de escribir una sola slide, y el resultado está medido en docs/verilator.md. Anda todo lo que la unidad usa: el modelo, el adapter, el uvm_reg_predictor, mirror(UVM_CHECK) y las dos sequences de la librería. Para el que sigue solo, el orden de lo que falta: uvm_reg_access_seq con backdoor, la generación del modelo desde IP-XACT o SystemRDL —que es como se hace de verdad—, y uvm_mem para las memorias. Los tres son extensiones de lo que vieron acá, no temas nuevos. Y el puente al ejercicio: lo primero que le van a pedir en un proyecto con registros es exactamente esto, con la planilla de la spec del lado izquierdo de la pantalla.

ver esta slide en el deck ›

Ejercicio · Día 8 · 1 de 2

Modelar el mapa de registros

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

Es el ejercicio más corto de los quince y el más parecido a una tarea real de las primeras semanas de un proyecto. Cuarenta líneas, y la mitad son copiar y pegar el patrón de CTRL. La etapa que enseña es la 1, y por una razón de método: el modelo se imprime —+UVM_TESTNAME=mapa_test— y se compara línea por línea contra la tabla de registros del README del ejercicio, antes de simular nada. Es lo que hay que hacer con un modelo de registros de verdad, donde el error típico no es de UVM sino un offset copiado mal de una planilla de 200 filas. La etapa 2 es donde cae CLR: el que lo declaró RW pasa la etapa 1 sin problema y se entera acá, con el número del bit, gracias a un test que no escribió. Vale dejar que caigan ahí — es el momento en que el argumento de RAL deja de ser una opinión. Y la etapa 3 es la que separa: pide apagar un chequeo, que es lo contrario de lo que el curso viene pidiendo hace siete días. La justificación tiene que salir de ellos: STATUS.EN cambió por una escritura a otra dirección, el modelo no puede saberlo, y un falso positivo que se deja pasar termina en alguien apagando el chequeo entero.

ver esta slide en el deck ›

El modelo de referencia en C

El scoreboard que reescribe la spec

Ésta es la técnica que separa un testbench de curso de uno de producción, y el argumento no es de comodidad: es de autoridad. En un proyecto con aritmética seria, la spec no es un PDF — es un programa en C que alguien corrió contra mil vectores antes de que existiera el RTL. Un scoreboard que reimplementa ese programa en SystemVerilog no verifica el DUT contra la spec: verifica el DUT contra la lectura que el verificador hizo de la spec, que es otra cosa. La pregunta que conviene tirar al grupo antes de mostrar el código: "si el modelo de C y el scoreboard difieren, ¿quién tiene razón?". La respuesta incómoda es que no se sabe, y averiguarlo cuesta días. Con DPI la pregunta no existe porque hay un solo modelo. Y el motivo por el que esta sección es tan barata en este curso: DPI es parte del LRM y los simuladores comerciales la traen de base —lo que la deja afuera son las ediciones gratuitas de estudiante—; lo que cuesta en un flujo comercial es el toolchain: -sv_lib, compilar el .c aparte, y acordarse de la librería en cada corrida. Acá el simulador compila a C++ por diseño, así que el puente es un archivo más en la línea de comandos y no hay nada que configurar.

ver esta slide en el deck ›

El modelo de referencia en C

import "DPI-C": una declaración de cada lado

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

Vale leer las dos declaraciones al lado, porque la simetría es todo el mecanismo: la de arriba es la promesa y la de abajo es quien la cumple, y el compilador no las cruza — el que las junta es el linker, al final. De ahí sale el modo de falla número uno de DPI, que conviene nombrar antes de que aparezca: un undefined reference a una función que está escrita, ahí, a la vista. Casi siempre es una de dos cosas — el nombre no coincide, o el archivo se compiló como C++ y el símbolo salió con el nombre decorado. Eso último es literalmente lo que pasa acá: Verilator le pasa los fuentes del usuario al compilador de C++, así que el .c necesita el extern "C" que está en el archivo. Sin esa guarda, el ejemplo no enlaza y el error no dice por qué. La tabla de tipos del LRM es más larga —hay svBitVecVal para vectores anchos, svOpenArrayHandle para arreglos— y no hace falta acá. La regla práctica: si el dato entra en un int, la firma se escribe sola; si no, se busca el anexo H del 1800 y se copia.

ver esta slide en el deck ›

El modelo de referencia en C

El scoreboard: la predicción se pide, no se escribe

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

El recuento vale hacerlo en voz alta porque es el argumento de diseño de toda la unidad: cambió una función de seis líneas y no cambió nada más. Eso pasa porque el scoreboard ya estaba bien repartido —predecir y comparar son dos trabajos, y estaban en dos métodos—, y es la recompensa tardía de haber separado predict_result de write hace tres días. La línea del int'(...) es la que hay que señalar: los tipos de UVM no cruzan la frontera. Del otro lado no hay command_transaction, hay tres enteros. Cuando el dato es un objeto —una imagen, un paquete, una matriz— lo que se pasa es un puntero o un arreglo abierto, y el que decide el formato es el modelo de C, no el testbench. Y el vtalu_golden_bug no es un adorno: un scoreboard que nunca vio un error no está probado. run.sh corre el mismo test contra un modelo mutado —la multiplicación truncada a ocho bits— y exige que el scoreboard grite. Si no grita, el testbench está comparando contra sí mismo y nadie se iba a enterar. Es el mismo test de mutación que el corrector del capstone hace con +BUG=1, del otro lado del cable.

ver esta slide en el deck ›

El modelo de referencia en C

Lo que se gana, lo que se paga

Se gana Se paga
Un solo modelo, el que firmó la spec los opcodes se sincronizan a mano: un enum de cada lado
El modelo se testea con las herramientas de C un undefined reference no dice cuál de las dos declaraciones está mal
Aritmética que en SystemVerilog es dolorosa —punto flotante, matrices, cripto— el modelo de C corre en tiempo cero: no puede esperar un flanco
Rendimiento: es una llamada a función, no un IPC un segfault en el modelo se lleva puesta la simulación entera

La tabla es la que hay que dejar escrita, y la fila que más discusión da es la primera de la derecha: el enum duplicado. Es real y no tiene solución elegante —el LRM no comparte tipos entre los dos lados—, y es la primera cosa que hay que mirar cuando todas las comparaciones fallan a la vez. Si falla una de cada seis, es el DUT; si fallan todas, es el mapeo. La tercera fila de la derecha es la que sorprende y conviene anticipar: una function de DPI corre en tiempo cero, así que el modelo de C no puede sincronizar con el reloj. Para eso está import "DPI-C" task — con una aclaración que casi todos los tutoriales se saltean: el C no puede bloquear por sí mismo. Una task de DPI consume tiempo sólo si se declara context y desde el C llama de vuelta a una export "DPI-C" task de SystemVerilog, que es la que espera el flanco. Y ahí ya no es un golden model, es un modelo de bus, que es otro tema. La regla del anteúltimo bullet es la que hay que llevarse, porque el riesgo de esta sección es que alguien salga queriendo hacer todo por DPI. El VTALU no justifica DPI: cinco operaciones de ocho bits se escriben en un case y se leen mejor. El ejemplo existe para que la técnica esté vista cuando aparezca el DUT que sí la justifica, y ese DUT llega antes de lo que uno cree.

ver esta slide en el deck ›

Repaso · Día 8

1 de 8 · El string de acceso

CLR se limpia solo cuando le escribís un 1. Lo declarás "WOC", y bit_bash y mirror(UVM_CHECK) dan cero errores. ¿Qué pasó?

Verde no es chequeado — uvm_reg_bit_bash_seq.svh:129-133 saltea todo campo cuyo acceso empiece con WO ("you are not supposed to read them"), y do_check lo saca de la máscara de comparación (uvm_reg.svh:2782-2788). El único rastro está en los tiempos: el bashing de CTRL dura cuatro transferencias menos. El acceso correcto es WC (o W1C), y con él el bit sí se batea. Un acceso WO* es la forma más barata que hay de apagar un chequeo sin enterarse.

ver esta slide en el deck ›

Repaso · Día 8

2 de 8 · Predicción automática o explícita

¿Qué cambia entre set_auto_predict(1) y enganchar un uvm_reg_predictor al monitor?

El que chequea no puede creerle al que estimula — con set_auto_predict(1), model.CTRL.write() actualiza el espejo en el momento de la llamada, antes de que el bus haya hecho nada: si el driver manda mal la transferencia, el modelo sigue convencido. Con el predictor, el espejo cambia recién cuando el monitor vio el cable. Es el mismo dibujo del scoreboard del capstone, con una pieza de la librería en lugar de una clase a mano. Y es gratis: el monitor y su analysis port ya estaban.

ver esta slide en el deck ›

Repaso · Día 8

3 de 8 · volatile

STATUS cambia solo, sin que nadie le escriba. ¿Qué querés decir al declararlo volatile en configure()?

El espejo deja de ser evidencia — un modelo predice "lo que escribí es lo que voy a leer", y para un campo volatile esa frase es falsa. Los dos UVM_WARNING GET_MIRRORED_VAL/VOL del ejemplo son la librería diciendo exactamente eso, y están en la salida a propósito. Lo que sí hay que chequear se chequea donde se sabe: en el scoreboard, y en el modelo va set_compare(UVM_NO_CHECK) sobre ese campo para que RAL no invente un error.

ver esta slide en el deck ›

Repaso · Día 8

4 de 8 · mirror() contra read()

model.CTRL.read(status, data) y model.CTRL.mirror(status, UVM_CHECK). ¿En qué se diferencian?

mirror es un scoreboard de registros en una palabra — las dos leen del DUT; la diferencia es que mirror compara contra lo que el modelo creía antes de la lectura, y si no coincide reporta un uvm_error sin que nadie escriba un chequeo. Y una lectura es una predicción: las dos actualizan el espejo después.

ver esta slide en el deck ›

Repaso · Día 8

5 de 8 · Quién sabe del bus

¿Qué sabe el uvm_reg_block sobre el APB?

El modelo no sabe que abajo hay un APB — cambiás el adapter y el mismo modelo maneja un AHB. El adapter son veinte líneas, una vez por protocolo, y es el archivo que te viene con un VIP comprado. Ojo con una: bus2reg lo llama también el predictor, con el item que vio el monitor, así que un adapter que dependa de algo que puso el driver funciona en un sentido y falla en el otro.

ver esta slide en el deck ›

Repaso · Día 8

6 de 8 · DPI y el tiempo

Una import "DPI-C" function del golden model, ¿puede esperar un flanco de reloj?

Tiempo cero, como cualquier function — para consumir tiempo hace falta import "DPI-C" task, y con una aclaración que casi todos los tutoriales se saltean: el C no puede bloquear por sí mismo. Una task de DPI consume tiempo sólo si se declara context y desde el C llama de vuelta a una export "DPI-C" task de SystemVerilog, que es la que espera el flanco. Y ahí ya no es un golden model, es un modelo de bus.

ver esta slide en el deck ›

Repaso · Día 8

7 de 8 · undefined reference

El .c está escrito y compila, y el link falla con undefined reference a la función del golden model. ¿Qué mirás primero?

El compilador no cruza las dos declaraciones: el que las junta es el linker — de ahí que el error hable de algo que está escrito, ahí, a la vista. Casi siempre es una de dos: el nombre no coincide letra por letra, o el archivo se compiló como C++ y el símbolo salió con el nombre decorado. Eso último es literal acá: Verilator le pasa los fuentes del usuario al compilador de C++, así que el .c necesita su extern "C". Y no hay flag de DPI: el .c va en la línea de verilator como un fuente más.

ver esta slide en el deck ›

Repaso · Día 8

8 de 8 · El scoreboard que nunca gritó

El scoreboard con golden model en C corre mil operaciones y no reporta ni una. ¿Alcanza?

Hay que romper el modelo a propósito — vtalu_golden_bug(1) muta la multiplicación y el run.sh exige que el scoreboard grite. Si no grita, el testbench está comparando contra sí mismo y nadie se iba a enterar: es el mismo test de mutación que el corrector del capstone hace con +BUG=1, del otro lado del cable. Y el enum duplicado es real pero es otro síntoma: si fallan todas las comparaciones a la vez, es el mapeo; si falla una de cada seis, es el DUT.

ver esta slide en el deck ›

Ejercicio · Día 8 · 2 de 2

El modelo de referencia en C, y las dos mutaciones que lo prueban

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

La etapa 2 es la que hace que el ejercicio valga, y conviene explicarla antes: con el DUT sano, un modelo que devuelva cualquier cosa fija podría pasar de casualidad si el estímulo fuera pobre. Con el DUT mintiendo en el bit 0, no hay forma de pasar sin haber calculado — es el mismo test de mutación del capstone, apuntado al modelo en vez de al scoreboard. La resta es la que se cae, y no por la aritmética: a - b es una línea. Lo que se olvida es la segunda mitad de la respuesta, el borrow, y se olvida porque cruza la frontera de otra forma —por puntero, que es como viaja un output int de SystemVerilog—. El scoreboard lo compara, así que sin él fallan todas las restas y ninguna otra operación: ese patrón de fallas es el que hay que aprender a leer. Y la trampa que no se ve, que está resuelta en el esqueleto a propósito para que no pierdan la hora ahí: sin extern "C", Verilator le pasa el archivo al compilador de C++, el símbolo sale mangled, y el link falla con un undefined reference a una función que está escrita dos líneas más arriba. Es el modo de falla número uno de DPI con Verilator.

ver esta slide en el deck ›

Capstone 2 · Día 8

El segundo final: una FIFO con backpressure

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

Éste es el ejercicio que separa al que entendió del que copió el patrón, y el mecanismo por el que separa está en el tercer bullet: el bug está en una salida de control, no en el camino de datos. Un scoreboard que compara lo que sale por rd_data cierra seis de las siete filas del plan y pasa en verde con el DUT roto. Para verlo hay que predecir las banderas, y para predecir las banderas hay que modelar la ocupación — o sea, escribir un modelo de referencia con estado, que es lo que el primer capstone no podía pedir. Vale decir por qué va segundo y no primero: el APB es el DUT que se cruza el primer año, y el patrón tabla-dirección-valor es el correcto ahí. Éste enseña cuándo ese patrón deja de servir, y esa lección no se entiende sin haber usado el patrón antes. La trampa que más cae es la del orden de actualización del modelo: primero mirar si la lectura saca —eso libera un lugar— y recién después si la escritura entra. Al revés, la escritura simultánea con la FIFO llena aparece como un dato perdido que el DUT nunca perdió, y el alumno pasa una hora buscando el bug en el RTL. Y la segunda: el check_phase que reclama datos que nunca salieron. Casi siempre el test terminó un ciclo antes de tiempo, y es el drain_time del día 3 apareciendo donde nadie lo esperaba.

ver esta slide en el deck ›