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
- El capstone entregado: un esclavo APB, su spec, y un testbench entero escrito desde una hoja en blanco — monitor, driver, scoreboard, cobertura y properties
- El scoreboard de ese testbench predice a mano el valor de cada registro, y la cobertura del bus se escribió fila por fila
- Y eso se hace igual en el trabajo… hasta que el DUT tiene sesenta registros. Ahí las dos cosas se modelan, y eso es todo el día 8
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.
Agenda
Día 8 · ≈ 4 h · opcional · lo que sigue después del capstone
- RAL, la unidad 9 — el modelo de registros sobre el mismo APB
- El modelo de referencia en C, por DPI
- El segundo capstone: una FIFO con backpressure
Al final del día podés:
- Leer y escribir un registro por nombre, sin escribir un solo
PADDR, y decir qué hacen el adapter y el predictor - Enchufar un modelo de referencia escrito en C, y probarlo con una mutación
- Escribir un scoreboard para un DUT donde una tabla no alcanza
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.
RAL
Unidad 9 · opcional · el registro deja de ser una dirección
- Todo chip tiene registros de configuración, y un SoC chico tiene doscientos
- Sin RAL, el testbench los escribe así:
addr = 8'h04,wdata = ..., y el scoreboard guarda unlogic [31:0]por cada uno - El día que el arquitecto mueve
SCRATCHde0x04a0x24, hay que buscar8'h04en todo el testbench — y0x04también era el valor de otra cosa - RAL —Register Abstraction Layer— es la capa que le pone nombre a eso:
model.SCRATCH.write(status, 'h1000_0000) - Es opcional y va al final a propósito: se entiende recién cuando ya escribiste el testbench que reemplaza
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.
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 |
- El modelo no sabe que abajo hay un APB. Cambiás el adapter y el mismo modelo maneja AHB
- El adapter es el archivo que te viene con un VIP comprado. Son veinte líneas
- Y abajo de todo, sin un solo cambio: el driver, el monitor y el agent del capstone
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.
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_regel 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- Un campo se declara con
configure(), y el argumento que importa es el string de acceso:"RW","RO","WC"… - Ese string es la especificación. No es documentación: es lo que UVM usa para predecir, y para generar los tests de Los tests que no escribiste
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.
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 : bus2regel 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_adapterreg2busbaja: el modelo pide un acceso genérico, esto arma la transaction del busbus2regsube: la transaction vuelve —del driver, o del monitor— y esto la cuenta en términos de accesoPSLVERRse convierte enUVM_NOT_OK, y ahí elstatusde cadawrite()deja de ser decorativo
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.
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_phaseel 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_testset_sequenceres lo que hace quemodel.CTRL.write()termine en el driver: el modelo manda items por el sequencer del agent que ya existíaset_auto_predict(0)apaga la predicción automática, y eluvm_reg_predictorla reemplaza escuchando el analysis port del monitor- Y el
model.build()delbuild_phase: unuvm_reg_blockes unuvm_object, no un component. Nadie te lo llama
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.
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 |
- La automática es una línea menos y es la que sale en los tutoriales
- La explícita es la que hay que usar, y por la misma razón que el scoreboard del capstone mira el monitor y no el driver
- Y es gratis: el monitor y su analysis port ya estaban
É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.
RAL
El camino completo, de la llamada al espejo
- La bajada se escribe; la subida —monitor, analysis port y
bus2reg— ya estaba - El
uvm_reg_predictorno se escribe: se instancia y se le enchufa el mapa - La punteada es
set_auto_predict(1): el atajo que no hay que tomar
É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ó.
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- No hay dirección en ninguna llamada: la dirección está en el mapa
- Nadie asignó el espejo.
get_mirrored_value()devuelve 1 porque el monitor vio la transferencia y el predictor la contó - Abajo, el driver del capstone está haciendo SETUP, ACCESS y el wait state de la lectura, igual que ayer
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.
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- Las dos salen de
uvm-core. No saben nada de este DUT: leen el modelo y generan de ahí el estímulo y el chequeo uvm_reg_hw_reset_seqlee todo después del reset y lo compara con el valor de reset declaradouvm_reg_bit_bash_seqescribe y lee cada bit escribible de cada registro, uno por uno, en los dos sentidos
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.
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
- Una palabra cambiada en un string, y una sequence que nadie escribió lo caza con el número del bit
- Es la trampa 2 de la letra chica del capstone —
CLRnunca se lee en 1— y ahí hacía falta un scoreboard entero para verla - El modelo de registros es el único documento de un proyecto que es la spec y el código a la vez
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.
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_blockACCySTATUStienen dirección, pero no son registros: su valor lo produce una escritura a otra dirección- Un modelo predice "lo que escribí es lo que voy a leer", y para estos dos esa
frase es falsa.
set_compare(UVM_NO_CHECK)lo dice en el modelo - El acumulador se sigue chequeando donde ya estaba: en el scoreboard, que es el que sabe sumar
- La lectura igual actualiza el espejo — una lectura es una predicción
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.
RAL
Resumen de la unidad
- El modelo es el mapa de la spec en código, y el string de acceso de cada campo es la parte que verifica
- El adapter es lo único que sabe del bus: veinte líneas, una vez por protocolo
- La predicción va por el monitor, no por la llamada — misma regla que el scoreboard
- Te regala
hw_reset_seqybit_bash_seq, y te cobra saber qué no es un registro - Y todo esto corre en Verilator 5.052, que hasta hoy nadie había mostrado
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.
Ejercicio · Día 8 · 1 de 2
Modelar el mapa de registros
cd code/ejercicios/d8-ral && cat README.es.md
- Es literalmente lo primero que te piden en un proyecto con registros: te dan la tabla de la spec y devolvés el modelo
- El DUT y el testbench son los del capstone. El adapter, el predictor y los
tests salen de la unidad. Lo único que escribís es
apb_reg_block.svh - Cuatro registros, seis campos, cuarenta líneas
- El corrector va por etapas: el mapa —impreso y comparado con la tabla, sin simular nada—, los accesos —las dos sequences de la librería— y lo que el modelo no puede predecir
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.
El modelo de referencia en C
El scoreboard que reescribe la spec
- El scoreboard del curso predice en SystemVerilog: un
casecon la aritmética de la VTALU. Para cinco operaciones de ocho bits está perfecto - Para un DSP, un codec o un motor cripto, esa aritmética ya existe, y en C: la escribió el equipo de algoritmo, y es contra ese modelo que se firmó la spec
- Reescribirla en SystemVerilog es mantener dos modelos y pasarse el proyecto debuggeando la diferencia entre ellos. Eso no es verificar: es tener dos specs
- La salida es no reescribirla. Verilator compila a C++, así que el modelo se enlaza adentro de la simulación: sin socket, sin proceso aparte, sin IPC
É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.
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_pkgcode/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- El nombre después de
"DPI-C"es el símbolo que busca el linker: tiene que coincidir letra por letra con el del.c input intviaja comoint; unoutput intllega comoint *. Unafunctionno puede consumir tiempo; unataskde DPI puede, pero sólo declaradacontexty llamando de vuelta a unaexport "DPI-C" task- El
.cva en la línea deverilatorcomo un fuente más. No hay un flag de DPI
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.
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_resultel 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- Es la única diferencia con el scoreboard de las sequences. El monitor, el
analysis port, la FIFO, el
do_comparey eluvm_errorno cambian - El golden model reemplaza las seis líneas de predecir, y ninguna otra pieza del testbench se entera de que hay C del otro lado
- Y el tráfico va en los dos sentidos: el resultado vuelve de C, y
vtalu_golden_bug(1)va de SystemVerilog a cambiarle el estado al modelo
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.
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 regla para decidir: si la predicción ya existe en C, DPI. Si la escribís
vos de cero y son cinco líneas, un
caseen SystemVerilog y listo - Corre:
make u8/dpi— dos tests contra el modelo sano, y uno contra el mutado
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.
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ó?
- Nada:
WOCes el acceso que describe un campo que se limpia al escribirlo -
WOCno está en el LRM, así que UVM lo trata como unRWcualquiera - Los dos saltean los accesos
WO*: ese bit no se testeó - El predictor deja el espejo en
x, y una comparación contraxsiempre pasa
Verde no es chequeado —
uvm_reg_bit_bash_seq.svh:129-133saltea todo campo cuyo acceso empiece conWO("you are not supposed to read them"), ydo_checklo saca de la máscara de comparación (uvm_reg.svh:2782-2788). El único rastro está en los tiempos: el bashing deCTRLdura cuatro transferencias menos. El acceso correcto esWC(oW1C), y con él el bit sí se batea. Un accesoWO*es la forma más barata que hay de apagar un chequeo sin enterarse.
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?
- Con auto-predict el modelo cree lo que quiso mandar, no lo que pasó
- Nada: el predictor es la implementación interna del auto-predict
- El predictor es más rápido: no arma la transaction del bus
- El auto-predict sólo sirve para el frontdoor, y el predictor también para el backdoor
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.
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()?
- Que el espejo no es evidencia: el valor pudo cambiar sin pasar por el bus
- Que UVM lo va a releer del DUT antes de cada comparación, para no equivocarse
- Que el campo queda fuera del mapa y deja de tener dirección
- Que hay que leerlo por backdoor, porque el frontdoor no llega a tiempo
El espejo deja de ser evidencia — un modelo predice "lo que escribí es lo que voy a leer", y para un campo
volatileesa frase es falsa. Los dosUVM_WARNING GET_MIRRORED_VAL/VOLdel 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 vaset_compare(UVM_NO_CHECK)sobre ese campo para que RAL no invente un error.
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?
-
readva por el bus ymirrorse queda en el espejo, sin generar una transferencia -
mirrorescribe el espejo en el DUT, para dejar a los dos iguales -
readactualiza el espejo ymirrorno lo toca, para no tapar un error - Las dos leen del DUT;
mirrorademás compara contra lo que el modelo creía
mirrores un scoreboard de registros en una palabra — las dos leen del DUT; la diferencia es quemirrorcompara contra lo que el modelo creía antes de la lectura, y si no coincide reporta unuvm_errorsin que nadie escriba un chequeo. Y una lectura es una predicción: las dos actualizan el espejo después.
Repaso · Día 8
5 de 8 · Quién sabe del bus
¿Qué sabe el uvm_reg_block sobre el APB?
- La dirección base y el ancho de
PADDR, que le llegan por eluvm_reg_map - Nada: el único que sabe del bus es el adapter
- Todo: por eso hay un modelo de registros por protocolo
- Los tiempos de SETUP y ACCESS, para predecir el wait state de la lectura
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:
bus2reglo 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.
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?
- Sí, poniendo un
#1adentro del.c - No: una
functioncorre en tiempo cero - Sí, siempre que el
.cse compile con el soporte de timing de Verilator - Sí: el simulador suspende el hilo de C mientras dura la llamada
Tiempo cero, como cualquier
function— para consumir tiempo hace faltaimport "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 declaracontexty desde el C llama de vuelta a unaexport "DPI-C" taskde SystemVerilog, que es la que espera el flanco. Y ahí ya no es un golden model, es un modelo de bus.
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?
- Que falte el flag de DPI en la línea de
verilator - Que el
.cesté en un-faparte y no mezclado con los.sv - El nombre, y el
extern "C": el símbolo pudo salir decorado - Que la
functionde SystemVerilog esté declaradavirtualpara exportar el símbolo
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
.cnecesita suextern "C". Y no hay flag de DPI: el.cva en la línea deverilatorcomo un fuente más.
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?
- Sí: mil comparaciones sin una sola diferencia es la definición de verificado
- Sí, siempre que además la cobertura funcional haya cerrado al 100 %
- No, porque los dos lados comparten el
enumde opcodes y se anulan entre sí - No: un scoreboard que nunca vio un error no está probado
Hay que romper el modelo a propósito —
vtalu_golden_bug(1)muta la multiplicación y elrun.shexige 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 elenumduplicado 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.
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
- El único archivo del ejercicio es
vtalu_golden.c. Del testbench no se toca una línea - Faltan dos operaciones: la resta con su borrow —que sale por un puntero— y el producto con el gancho de mutación
- El corrector corre tres veces: DUT sano contra tu modelo, DUT mutado
(
VTALU_BUG=1), y tu modelo mutado (+GOLDEN_BUG)
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.
Capstone 2 · Día 8
El segundo final: una FIFO con backpressure
cd code/ejercicios/d8-fifo && cat spec.md
- Para el que ya entregó el APB. El protocolo es más simple —no hay direcciones, ni wait states, ni respuesta de error— y aun así es más difícil
- El del APB es un esclavo sin memoria útil: dirección y valor, y el scoreboard puede ser una tabla de cuatro filas. Una FIFO tiene orden y ocupación, y las dos hay que llevarlas
- El bug de
+BUG=1no es de datos:almost_fullse levanta un lugar tarde. Los datos siguen saliendo bien y en orden, así que un scoreboard que sólo comparard_datapasa en verde - La letra chica: las banderas describen el estado antes del flanco,
rd_datallega un ciclo tarde, escribir con la FIFO llena no es un error, y una lectura simultánea le hace lugar a la escritura
É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.