Working with Multiple Instances of vr_ad Registers

The devices that we verify often have multiple instances of a certain register type. These registers are instantiated in a regular structure inside the design. In our tests, we want to be able to access each of them.

In vr_ad, accessing a register is typically done using the write/read_reg family of macros. These macros encapsulate the very flexible access mechanisms that the package provides into one convenient call. For unique registers, they just work, without having to pass in any additional options, but things get a bit more involved if the register type we are trying to access is instantiated multiple times. There are multiple scenarios where this can be the case. Let's have a look at them!

The first scenario that comes to mind is when there are multiple instances of the same register inside a register file. Let's take the example from the last post, with the graphics processing device:

<'
extend vr_ad_reg_file_kind : [ GRAPHICS ];
extend GRAPHICS vr_ad_reg_file {
keep size == 512;
post_generate() is also {
reset();
};
};


reg_def TRIANGLE {
reg_fld SIDE0 : uint(bits : 8);
reg_fld SIDE1 : uint(bits : 8);
reg_fld SIDE2 : uint(bits : 8);
};


extend GRAPHICS vr_ad_reg_file {
triangles[3] : list of TRIANGLE vr_ad_reg;

add_registers() is also {
for each (triangle) in triangles {
add_with_offset(index * 0x4, triangle);
};
};
};
'>

In this case, we have three TRIANGLE registers, each at a different offset. Let's try to use the write_reg macro to access a triangle:

<'
extend MAIN vr_ad_sequence {
!triangle : TRIANGLE vr_ad_reg;

body() @driver.clock is only {
write_reg triangle;
};
};
'>

Trying to use write_reg like this will result in an error message, stating that there are multiple TRIANGLE registers inside the address map. It's impossible for the macro to know exactly which instance we want. We can solve this ambiguity in two ways.

The first way is by using the less known operation generate block argument to the macro (which is optional). We get the exact instance of the model register we want and pass that in as the static_item:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
var static_triangle := driver.addr_map.get_regs_by_kind(TRIANGLE)[0];
write_reg { .static_item == static_triangle } triangle;
};
};
'>

This lets the macro know exactly which instance we want to access.

The other way we can do this is by using the static triangle as the macro argument:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
var static_triangle :=
driver.addr_map.get_regs_by_kind(TRIANGLE)[1].as_a(TRIANGLE vr_ad_reg);
write_reg static_triangle { .SIDE0 == 1 };
};
};
'>

In this case, we don't have to pass a static_item anymore, but we have to cast the static triangle to be able to access the TRIANGLE subtype's fields. In both cases, though, we have to write quite a bit of code just to access the register we want.

Instead of always getting the static_item ourselves and using that, why not encapsulate the whole operation inside a macro of our own? Since macros are anyway used to access vr_ad registers, it shouldn't cause any confusion for anybody. What we need is an extra macro argument, so our macro should look something like this: write_triangle_reg <inst'idx> <reg'exp> <reg_gen'block> (let's ignore the <op_gen'block> argument for simplicity).

Such a macro would just fetch the appropriate static_item based on the <inst'idx> argument and forward the other arguments to write_reg. This is how such a macro would look like:

<'
define <write_triangle_reg'action>
"write_triangle_reg <idx'exp> <reg'exp>[ <any>]" as computed
{
var los : list of string;
los.add( "{");
los.add( "var static_triangle :=");
los.add(appendf("driver.addr_map.get_regs_by_kind(TRIANGLE)[%s];", <idx'exp>));
los.add(appendf("write_reg { .static_item == static_triangle } %s %s;", <reg'exp>, <any>));
los.add( "};");

result = str_join(los, "\n");
};
'>

To access the last triangle, we would simply do:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
write_triangle_reg 2 triangle { .SIDE1 == 1 };
};
};
'>

Now this is all fine and dandy, but what if we also want to read the registers? We would need a second macro, whose expansion would be very similar to the first one's. We could generalize the macro body code inside a function that can return the result for both access operations. The vr_ad package defines such functions inside the global singleton, so if it's good enough for Cadence, then it's good enough for us:

<'
extend global {
vgm__access_triangle_reg_body(operation : string,
idx : string, reg : string, block : string = "") : string is
{
var los : list of string;
los.add( "{");
los.add( "var static_triangles :=");
los.add( "driver.addr_map.get_regs_by_kind(TRIANGLE);");
los.add(appendf("assert %s in [0..static_triangles.size() - 1];", idx));
los.add(appendf("%s { .static_item == static_triangles[%s] } %s %s;", operation, idx, reg, block));
los.add( "};");

result = str_join(los, "\n");
};
};
'>

This function can take any access operation (write_reg, read_reg or write_reg_fields - a lesser know brother of the two), together with the macro arguments and return the macro body. Declaring the macros just means calling this function with the appropriate arguments:

<'
define <write_triangle_reg'action>
"write_triangle_reg <idx'exp> <reg'exp>[ <any>]" as computed
{
result = vgm__access_triangle_reg_body("write_reg", <idx'exp>, <reg'exp>, <any>);
};

define <write_triangle_reg_fields'action>
"write_triangle_reg_fields <idx'exp> <reg'exp>[ <any>]" as computed
{
result = vgm__access_triangle_reg_body("write_reg_fields", <idx'exp>, <reg'exp>, <any>);
};

define <read_triangle_reg'action>
"read_triangle_reg <idx'exp> <reg'exp>" as computed
{
result = vgm__access_triangle_reg_body("read_reg", <idx'exp>, <reg'exp>);
};
'>

Now we can also easily read any TRIANGLE register. Here's a read of the first triangle:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
read_triangle_reg 0 triangle;
};
};
'>

A pretty useful thing we can also do with the macros is use them inside loops:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
for i from 0 to 2 {
write_triangle_reg_fields i triangle { .SIDE2 = 1 };
};
};
};
'>

What we have up to now is great and all. It works perfectly for triangles, but let's throw circles into the mix as well:

<'
reg_def CIRCLE {
reg_fld RADIUS : uint(bits : 8);
};


extend GRAPHICS vr_ad_reg_file {
circles[5] : list of CIRCLE vr_ad_reg;

add_registers() is also {
for each (circle) in circles {
add_with_offset(0x20 + index * 0x4, circle);
};
};
};
'>

The *_triangle_reg macros won't work on these registers so we're back to where we started. It would be very silly to create a new macro that can handle only circles, because that would become really unmaintainable if we were to add more and more shapes. What we need is a macro that can work with any register, regardless of type.

We already have the information about the register's kind inside the register itself. We just need to use that when calling get_regs_by_kind(...) to get the static register. We'll call this new macro write_graphics_reg and define a new function that implements all three versions of it:

<'
extend global {
vgm__access_graphics_reg_body(operation : string,
idx : string, reg : string, block : string = "") : string is
{
var los : list of string;
los.add( "{");
los.add( "var kind : vr_ad_reg_kind;");
los.add(appendf("if %s == NULL { %s = new };", reg, reg));
los.add(appendf("kind = %s.kind;", reg));
los.add( "var static_regs :=");
los.add( "driver.addr_map.get_regs_by_kind(kind);");
los.add(appendf("assert %s in [0..static_regs.size() - 1];", idx));
los.add(appendf("%s { .static_item == static_regs[%s] } %s %s;", operation, idx, reg, block));
los.add( "};");

result = str_join(los, "\n");
};
};
'>

The three macros will be:

<'
define <write_graphics_reg'action>
"write_graphics_reg <idx'exp> <reg'exp>[ <any>]" as computed
{
result = vgm__access_graphics_reg_body("write_reg", <idx'exp>, <reg'exp>, <any>);
};

define <write_graphics_reg_fields'action>
"write_graphics_reg_fields <idx'exp> <reg'exp>[ <any>]" as computed
{
result = vgm__access_graphics_reg_body("write_reg_fields", <idx'exp>, <reg'exp>, <any>);
};

define <read_graphics_reg'action>
"read_graphics_reg <idx'exp> <reg'exp>" as computed
{
result = vgm__access_graphics_reg_body("read_reg", <idx'exp>, <reg'exp>);
};
'>

These new macros can work with triangles, circles and any new register types we might add:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
write_graphics_reg 2 triangle;
read_graphics_reg 4 circle;
};
};
'>

Let's look at a different scenario now. Let's assume that our DUT is composed of multiple slices, each of which is able to do some graphics processing independently of the others. An individual slice can only process one triangle and one circle, but there are many such slices. In this case, the register definitions would look like this:

<'
extend vr_ad_reg_file_kind : [ SLICE ];
extend SLICE vr_ad_reg_file {
keep size == 8;
post_generate() is also {
reset();
};
};


reg_def TRIANGLE SLICE 0x0 {
reg_fld SIDE0 : uint(bits : 8);
reg_fld SIDE1 : uint(bits : 8);
reg_fld SIDE2 : uint(bits : 8);
};

reg_def CIRCLE SLICE 0x4 {
reg_fld RADIUS : uint(bits : 8);
};


extend vr_ad_reg_file_kind : [ GRAPHICS ];
extend GRAPHICS vr_ad_reg_file {
slices[3] : list of SLICE vr_ad_reg_file;

keep size == 64;
post_generate() is also {
reset();
};

add_registers() is also {
for each (slice) in slices {
add_with_offset(index * 0x10, slice);
};
};
};
'>

As before, we can access a certain triangle or circle by getting the appropriate register instance from the address map and using that with write_reg, like we did in the previous scenario. What we can also do is get a handle to the appropriate register file and use that as the static_item:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
var static_reg_file := driver.addr_map.get_reg_files_by_kind(SLICE)[0];
write_reg { .static_item == static_reg_file } triangle;
write_reg { .static_item == static_reg_file } circle;
};
};
'>

The same comment as above still applies; we'd have to do this operation every time we want to access a register, which can become tedious. Why not write a new macro, then?

Writing a macro that expands to the new code is pretty straightforward. Here's how the global function for that would look like:

<'
extend global {
vgm__access_slice_reg_body(operation : string,
idx : string, reg : string, block : string = "") : string is
{
var los : list of string;
los.add( "{");
los.add( "var static_slices :=");
los.add( "driver.addr_map.get_reg_files_by_kind(SLICE);");
los.add(appendf("assert %s in [0..static_slices.size() - 1];", idx));
los.add(appendf("%s { .static_item == static_slices[%s] } %s %s;", operation, idx, reg, block));
los.add( "};");

result = str_join(los, "\n");
};
};
'>

We'll then wrap calls to this method inside the actual macro declarations, which I won't show here. Using these macros, we can easily access any slice register:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
write_slice_reg 1 triangle { .SIDE1 == 1 };
read_slice_reg 1 triangle;

write_slice_reg_fields 1 circle { .RADIUS = 1 };
read_slice_reg 1 circle;
};
};
'>

We can also, for example, loop over all triangles in the design:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
for i from 0 to 2 {
write_slice_reg i triangle val 0x010203;
};
};
};
'>

Things start to get fun when the design contains a mixture of the two cases we've looked at above. Our registers are organized in slices, but within one slice some registers may be instantiated multiple times:

<'
// TRIANGLE and CIRCLE reg_defs
// ...

reg_def SQUARE SLICE 0x20 {
reg_fld SIDE : uint(bits : 8);
};


extend SLICE vr_ad_reg_file {
triangles[3] : list of TRIANGLE vr_ad_reg;
circles[4] : list of CIRCLE vr_ad_reg;

add_registers() is also {
for each (triangle) in triangles {
add_with_offset(index * 0x4, triangle);
};

for each (circle) in circles {
add_with_offset(0x10 + index * 0x4, circle);
};
};
};


extend vr_ad_reg_file_kind : [ GRAPHICS ];
extend GRAPHICS vr_ad_reg_file {
slices[3] : list of SLICE vr_ad_reg_file;

keep size == 256;
post_generate() is also {
reset();
};

add_registers() is also {
for each (slice) in slices {
add_with_offset(index * 0x40, slice);
};
};
};
'>

To access a square it's enough to just pass in the register file as a static_item (this is the same situation as in scenario no. 2):

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
var static_slice := driver.addr_map.get_reg_files_by_kind(SLICE)[0];
write_reg { .static_item == static_slice } square;
};
};
'>

To access a triangle (or a circle), though, we need to get the appropriate register instance (similarly to how we did it in scenario no. 1):

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
var static_slice := driver.addr_map.get_reg_files_by_kind(SLICE)[0];
var static_triangle := static_slice.get_regs_by_kind(TRIANGLE)[0];
write_reg { .static_item == static_triangle } triangle;
};
};
'>

This means that our macro has to be able to handle both the register file and register indices. We can only process one square at a time, though, so in that case it doesn't make sense to pass in a register index (seeing as how there is only one instance per register file). The register index argument must therefore be optional. Starting top-down, this is how the definition of the write_graphics_reg macro would look like:

<'
define <write_graphics_reg'action>
"write_graphics_reg <rf_idx'exp>[ <reg_idx'exp>] <reg'exp>[ <any>]" as computed
{
result = vgm__access_graphics_reg_body("write_reg", <rf_idx'exp>, <reg_idx'exp>, <reg'exp>, <any>);
};
'>

The global function would then take both of these arguments into account to determine the static_item that gets passed:

<'
extend global {
vgm__access_graphics_reg_body(operation : string,
rf_idx : string, reg_idx : string, reg : string, block : string = "") : string is
{
var los : list of string;
los.add( "{");
los.add( "var static_slices := driver.addr_map.get_reg_files_by_kind(SLICE);");
los.add(appendf("assert %s in [0..static_slices.size() - 1];", rf_idx));

// multiply instantiated reg
if reg_idx != "" {
los.add( "var kind : vr_ad_reg_kind;");
los.add(appendf("if %s == NULL { %s = new };", reg, reg));
los.add(appendf("kind = %s.kind;", reg));
los.add( "var static_regs :=");
los.add(appendf("static_slices[%s].get_regs_by_kind(kind);", rf_idx));
los.add(appendf("assert %s in [0..static_regs.size() - 1];", reg_idx));
};

los.add(appendf("%s {", operation));
if reg_idx == "" {
los.add(appendf(".static_item == static_slices[%s];", rf_idx));
}
else {
los.add(appendf(".static_item == static_regs[%s];", reg_idx));
};
los.add(appendf("} %s %s;", reg, block));
los.add( "};");

result = str_join(los, "\n");
};
};
'>

Using the extended version of the macro we can now access both triangles and squares:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
write_graphics_reg 1 1 triangle;
read_graphics_reg 2 0 triangle;

write_graphics_reg 1 square;
read_graphics_reg 2 square;
};
};
'>

We can also loop over all squares (not shown) or double-loop over all triangles:

<'
extend MAIN vr_ad_sequence {
body() @driver.clock is only {
for i from 0 to 2 {
for j from 0 to 2 {
read_graphics_reg i j triangle;
};
};
};
};
'>

As we've seen, by encapsulating the write/read_reg macros inside our own we can easily select the register instance that we want to access. We save a lot of tedious typing and duplicated code to get the appropriate static_item every time. We pay a small price when using macros though, as it becomes more difficult to track down syntax errors, but with proper documentation the disadvantages can be reduced. For more examples and detailed code, check out the SourceForge repository.

If your next project involves a lot registers with multiple instances, why not try this approach out? See you next time!

Comments