vr_ad Twin Registers

In their quest to come up with ever more efficient architectures, concept engineers sometimes do crazy things. Twin registers (also called multiview registers) are one of them: when accessing an address location, it sometimes behaves like it's one register, while at other times it's like there's another register residing there.

While this may be fairly straightforward to implement in hardware (throw in a couple of multiplexers here and there), it's going to be a bit more involved for the verification engineer trying to check that it works as it should. Luckily vr_ad already provides an example of how to do this. "Then why are you writing this post?" you may be asking yourself. Well, the thing with that example is that it's not fully explained how the pieces fit together and there are also some mistakes in it.

There are two cases that come to mind where I've encountered twin registers. The first case is when the layout of the register is different depending on whether we're doing a read or a write. The second case is when the layout of the register depends on some external condition, such as the value of a certain signal, the state of the device or a specific configuration setting in some other register.

Let's start by looking at the first case. The code we'll look at is based on the vr_ad example. We have to define two different registers, one for each layout:

<'
reg_def STATUS {
reg_fld VALID : uint(bits : 1) : R : 0;
reg_fld DONE : uint(bits : 1) : R : 0;
};

reg_def CONTROL {
reg_fld SETVALID : uint(bits : 1) : W : 0;
reg_fld CLRVALID : uint(bits : 1) : W : 0;
reg_fld CLRDONE : uint(bits : 1) : W : 0;
reg_fld START : uint(bits : 1) : W : 0;
};
'>

Notice that we don't instantiate them in any register file, as that would lead to an error message because they would overlap. When reading the register the value we get will represent the STATUS layout, whereas any values we write will be interpreted according to the CONTROL layout.

Since we can't instantiate these two registers inside the register file, we'll need to define a dummy register to take their place:

<'
reg_def STATUS_CONTROL_PROXY {
reg_fld DATA : uint(bits : 32);

set_static_info() is also {
set_compare_mask(0);
};
};
'>

This register will only be used to forward the access operations to the appropriate twin, so we'll set its compare mask to 0. But to be able to forward these operations, we need to instantiate the twins somewhere. We'll do this inside a dummy register file:

<'
extend vr_ad_reg_file_kind : [ STATUS_CONTROL_TWINS ];
extend STATUS_CONTROL_TWINS vr_ad_reg_file {
status : STATUS vr_ad_reg;
control : CONTROL vr_ad_reg;

keep size == 8;

add_registers() is also {
add_with_offset(0x0, status);
add_with_offset(0x4, control);
};
};
'>

The offsets where we add the twins don't matter; they just need to be different. Whenever the address location where the twins reside in the hardware gets accessed, the proxy register will be accessed inside the vr_ad model. We can connect this to the dummy register file using indirect_access(...) and based on the access information we can access either STATUS or CONTROL:

<'
extend STATUS_CONTROL_TWINS vr_ad_reg_file {
indirect_access(direction : vr_ad_rw_t, ad_item : vr_ad_base) is {
var data := ad_item.as_a(vr_ad_reg).get_access_data();
if direction == WRITE {
control.update(0, data, {});
}
else {
compute status.compare_and_update(0, data);
};
};
};
'>

When we're writing to the shared address we update the CONTROL register. When we're reading, we need to check that the data we got from the device matches the one from our model's STATUS register. In the main register file we just need to instantiate the proxy register and the dummy register file and connect them:

<'
extend EXAMPLE vr_ad_reg_file {
status_control_proxy : STATUS_CONTROL_PROXY vr_ad_reg;
status_control_twins : STATUS_CONTROL_TWINS vr_ad_reg_file;

add_registers() is also {
add_with_offset(0x0, status_control_proxy);
status_control_proxy.attach(status_control_twins);
};
};
'>

Notice that we didn't add the dummy register file to any specific offset inside the main register file. Doing that would imply that the twins are accessible at other offsets than the one where we've added the proxy register. The vr_ad example shows the dummy register file being instantiated outside of the main one (for whatever reason), but I chose to do it here since it makes everything better encapsulated.

We've figured out how to handle our register modeling, but we're not done yet. We need to handle the stimulus part. Since the twins' register file wasn't mapped anywhere inside the address space, vr_ad won't know what address to access if we want to write to the CONTROL register, for example. For this reason we have the indirect sequence mechanism. An indirect sequence is a sequence that gets started automatically when we try to access an unmapped element we've previously associated with it. In our case we'll associate an indirect sequence with the dummy register file. Whenever we try to access an element of this register file (i.e. the CONTROL or the STATUS register), the indirect sequence which gets called will convert this access to the unmapped item to an access to the proxy register (which is mapped inside the main register file):

<'
extend vr_ad_sequence_kind : [ACCESS_TWIN_REG];
extend ACCESS_TWIN_REG INDIRECT vr_ad_sequence {
!proxy_reg : STATUS_CONTROL_PROXY vr_ad_reg;

body() @driver.clock is only {
if direction == WRITE {
write_reg proxy_reg val reg.read_reg_rawval();
} else {
read_reg proxy_reg;
if not reg.in_model {
reg.write_reg_rawval(proxy_reg.read_reg_rawval());
};
};
};
};
'>

In the code snippet above, reg is a predefined field which will hold the sequence register (i.e. the register we call write_reg on). This register has been generated based on the constraints passed to the macro and contains the value we want to write. We pass it as the val argument to a write_reg call to the proxy register. In the vr_ad example, the value to be written is extracted using the read_reg_val() method, but this approach won't work in our case because the CONTROL register contains write-only fields. To extract the write value we have to use the read_reg_rawval() method to bypass the access mask. When doing a read, aside from calling the read_reg macro on the proxy reg we also want to update the sequence register so that we can query its value. The same comment related to the access mask applies here as well, hence the use of read_reg_rawval(). Also, in case the register we pass in to the initial macro is an instance from inside the model, we don't want to mess with the update mechanism, because this would cause side effects from the pre/post hooks to be applied and potentially ruin the modeled value.

Somewhere in the testbench we need to connect the indirect sequence to the dummy register file and tell the address map that we have some items that can't be accessed directly:

<'
extend sys {
connect_pointers() is also {
addr_map.add_unmapped_item(reg_file.status_control_twins);
reg_file.status_control_twins.set_indirect_seq_name(ACCESS_TWIN_REG);
};
};
'>

I don't get why the call to add_unmapped_item(...) has to be done on the address map and couldn't just be done inside the main register file. If I were to instantiate the EXAMPLE register file inside another register file, I'd have to call add_unmapped_item(...) again on the new map. If this call were encapsulated at the place I instantiate the dummy register file, doing vertical reuse would be easier. The problem is especially painful if I have a lot of twin registers from different register models I want to reuse.

For the test writer, accessing a twin register is no different than accessing a normal one:

<'
extend MAIN vr_ad_sequence {
!control : CONTROL vr_ad_reg;

body() @driver.clock is only {
write_reg_fields control { .SETVALID = 1 };
};
};
'>

The flow of execution is the following:

  1. control gets filled with the proper value, based on the argument block; in our case this value would be 0x8
  2. the indirect sequence gets called with control being passed in as its reg field
  3. a write_reg to the proxy register happens with the value stored in control
  4. the monitor notifies the register model of an access to address 0x0
  5. the model updates the proxy register (which is located at address 0x0)
  6. the proxy register notifies the dummy register file (via indirect_access(...)) that it has been accessed
  7. the dummy register file updates the value of the CONTROL register

As you can see, the indirect sequence and the indirect_access(...) method are complementary to each other.

Another case of twin registers I'd like us to look at is when the layout of a specific register depends on a configuration field in another register. Let's take as an example a register specifying what type of math operation we want to perform and another register that stores the arguments of that operation:

<'
type math_op_t : [NOP, INC, DEC, ADD, SUB];

reg_def MATH_OP EXAMPLE 0x4 {
reg_fld OP : math_op_t : RW : NOP;
};

reg_def UNARY_ARG {
reg_fld ARG : byte : RW : 0;
};

reg_def BINARY_ARGS {
reg_fld ARG0 : byte : RW : 0;
reg_fld ARG1 : byte : RW : 0;
};
'>

When we want to perform a increment or a decrement, we can only pass in one argument, so the UNARY_ARG layout applies. When we want to perform an addition or a subtraction, we need to pass in two arguments, so the BINARY_ARGS layout applies. When there is no operation set, the register isn't accessible.

As before, we'll need to define a proxy register:

<'
reg_def ARGS_PROXY {
reg_fld DATA : uint(bits : 32);

set_static_info() is also {
set_compare_mask(0);
};
};
'>

We also need to define a dummy register file to store the two layouts, together with its indirect_access(...) method:

<'
extend vr_ad_reg_file_kind : [ ARGS_TWINS ];
extend ARGS_TWINS vr_ad_reg_file {
unary_arg : UNARY_ARG vr_ad_reg;
binary_args : BINARY_ARGS vr_ad_reg;

keep size == 8;

add_registers() is also {
add_with_offset(0x0, unary_arg);
add_with_offset(0x4, binary_args);
};


!p_math_op : MATH_OP vr_ad_reg;

// this is called whenever the proxy reg is accessed
indirect_access(direction : vr_ad_rw_t, ad_item : vr_ad_base) is {
var data := ad_item.as_a(vr_ad_reg).get_access_data();

if direction == WRITE {
unary_arg.update(0, data, {});
binary_args.update(0, data, {});
}
else {
case p_math_op.OP {
[INC, DEC] : { compute unary_arg.compare_and_update(0, data) };
[ADD, SUB] : { compute binary_args.compare_and_update(0, data) };
[NOP] : { check that %{data} == 0 };
};
};
};
};
'>

In order to know what operation is currently configured and what layout to apply, we need a reference to the MATH_OP register. Because the two twins also share their storage elements, we need to update both when doing a write. When doing a read, we select the appropriate register based on the math operation.

In the main register file we instantiate the one containing the twins and do the necessary connections:

<'
extend EXAMPLE vr_ad_reg_file {
args_proxy : ARGS_PROXY vr_ad_reg;
args_twins : ARGS_TWINS vr_ad_reg_file;

add_registers() is also {
add_with_offset(0x0, args_proxy);
args_proxy.attach(args_twins);
args_twins.p_math_op = math_op;
};
};
'>

The indirect sequence will look very similar to the one we defined in the first case:

<'
extend vr_ad_sequence_kind : [ACCESS_TWIN_REG];
extend ACCESS_TWIN_REG INDIRECT vr_ad_sequence {
!proxy_reg : ARGS_PROXY vr_ad_reg;

body() @driver.clock is only {
// a sanity check to make sure I'm not trying to access the wrong twin
var math_op := driver.addr_map.get_reg_by_kind(MATH_OP).as_a(MATH_OP vr_ad_reg);
assert(
(reg.kind == UNARY_ARG => math_op.OP in [INC, DEC]) and
(reg.kind == BINARY_ARGS => math_op.OP in [ADD, SUB])
)
else error(appendf("Cannot access %s when math_op is %s",
reg.kind.as_a(string), math_op.OP.as_a(string)));

if direction == WRITE {
write_reg proxy_reg val reg.read_reg_rawval();
} else {
read_reg proxy_reg;
if not reg.in_model {
reg.write_reg_rawval(proxy_reg.read_reg_rawval());
};
};
};
};
'>

What I've also added here are some sanity checks. It doesn't make much sense to access the BINARY_ARGS twin when doing an increment, for example, so the assert statement can warn the test writer that he may be doing something unintended.

The final piece of the puzzle is connecting the indirect sequence to the dummy register file:

<'
extend sys {
connect_pointers() is also {
addr_map.add_unmapped_item(reg_file.args_twins);
reg_file.args_twins.set_indirect_seq_name(ACCESS_TWIN_REG);
};
};
'>

Now we can access the twins like any other register:

<'
extend MAIN vr_ad_sequence {
!math_op : MATH_OP vr_ad_reg;
!unary_arg : UNARY_ARG vr_ad_reg;

body() @driver.clock is only {
write_reg_fields math_op { .OP = INC };
write_reg unary_arg { .ARG == 5 };
};
};
'>

As you can see, setting up the register model when we have twin register requires a bit more work. Luckily, vr_ad already provides a mechanism for handling this. Once we have all the plumbing in place, accessing twin registers is done in the same way as accessing any vanilla register. If you want to try it out yourselves, you can find the full code on SourceForge.

Do you have any other scenario where you've had to verify twin register? I'd love to hear about it in the comments section below.

Comments