Structures and Unions

Jan. 6, 2024 || Item:1.6.Structures and Unions

Structure and union declarations follow the C syntax, but without the optional structure tags before the ‘{‘.

struct { bit [7:0] opcode;

bit [23:0] addr;

} IR;         // anonymous structur defines variable IR

IR.opcode = 1;  // set field in IR.

Some additional examples of declaring structure and unions are:

typedef struct {

bit [7:0] opcode;

bit [23:0] addr;

} instruction; // named structure type

instruction IR; // define variable

typedef union { int i; shortreal f; } num;   // named union type num n;

n.f = 0.0; // set n in floating point format

typedef struct {

bit isfloat;

union { int i; shortreal f; } n;     // anonymous type

} tagged_st; // named structure

tagged_st a[9:0];   // array of structures

A structure can be assigned as a whole, and passed to or from a function or task as a whole.

A packed structure consists of bit fields, which are packed together in memory without gaps. This means that they are easily converted to and from bit vectors. An unpacked structure has an implementation-dependent packing, normally matching the C compiler.

Like a packed array, a packed structure can be used as a whole with arithmetic and logical operators. The first member specified is the most significant and subsequent members follow in decreasing significance. The structures are declared using the packed keyword, which can be followed by the signed or unsigned keywords, according to the desired arithmetic behavior. The default is unsigned:

struct packed signed { int a;

shortint b;

byte c;

bit [7:0] d;

} pack1; // signed, 2-state

struct packed unsigned { time a;

integer b;

logic [31:0] c;

} pack2; // unsigned, 4-state

If any data type within a packed structure is 4-state, the whole structure is treated as 4-state. Any 2-state members are converted as if cast. One or more bits of a packed structure can be selected as if it were a packed array, assuming an [n-1:0] numbering:

pack1 [15:8] // c

Non-integer data types, such as real and shortreal, are not allowed in packed structures or unions. Nor are unpacked arrays.

A packed structure can be used with a typedef.

typedef struct packed { // default unsigned bit [3:0] GFC;

bit [7:0] VPI;

bit [11:0] VCI;

bit CLP;

bit [3:0] PT ;

bit [7:0] HEC;

bit [47:0][7:0] Payload;

bit [2:0] filler;

} s_atmcell;

A packed union shall contain members that must be packed structures, or packed arrays or integer data types all of the same size (in contrast to an unpacked union, where the members can be different sizes). This ensures that you can read back a union member that was written as another member. A packed union can also be used as a whole with arithmetic and logical operators, and its behavior is determined by the signed or unsigned key- word, the latter being the default. If a packed union contains a 2-state member and a 4-state member, the entire union is 4 state. There is an implicit conversion from 4-state to 2-state when reading and from 2-state to 4-state when writing the 2-state member.

For example, a union can be accessible with different access widths:

typedef union packed {          // default unsigned s_atmcell acell;

bit [423:0] bit_slice;

bit [52:0][7:0] byte_slice;

} u_atmcell;

u_atmcell u1;

byte b;

bit [3:0] nib;

b = u1.bit_slice[415:408];      // same as b = u1.byte_slice[51];

nib = u1.bit_slice [423:420];   // same as nib = u1.acell.GFC;

Note that writing one member and reading another is independent of the byte ordering of the machine, unlike a normal union of normal structures, which are C-compatible and have members in ascending address order.

The signing of unpacked structures is not allowed. The following declaration would be considered illegal:

typedef struct signed {

int f1 ;

logic f2 ;

} sIllegalSignedUnpackedStructType; // illegal declaration

The qualifier tagged in a union declares it as a tagged union, which is a type-checked union. An ordinary (untagged) union can be updated using a value of one member type and read as a value of another member type, which is a potential type loophole. A tagged union stores both the member value and a tag, i.e., additional bits representing the current member name. The tag and value can only be updated together consistently, using a statically type-checked tagged union expression. The member value can only be read with a type that is consistent with the current tag value (i.e., member name). Thus, it is impossible to store a value of one type and (mis)interpret the bits as another type.

In addition to type safety, the use of member names as tags also makes code simpler and smaller than code that has to track unions with explicit tags. Tagged unions can also be used with pattern matching, which improves readability even further.

In tagged unions, members can be declared with type void, when all the information is in the tag itself, as in the following example of an integer together with a valid bit:

typedef union tagged {

void Invalid;

int Valid;

} VInt;

A value of VInt type is either Invalid and contains nothing, or is Valid and contains an int.

Example:

typedef union tagged {

struct {

bit [4:0] reg1, reg2, regd;

} Add;

union tagged {

bit [9:0] JmpU;

struct {

bit [1:0] cc;

bit [9:0] addr;

} JmpC;

} Jmp;

} Instr;

A value of Instr type is either an Add instruction, in which case it contains three 5-bit register fields, or it is a Jmp instruction. In the latter case, it is either an unconditional jump, in which case it contains a 10-bit destina- tion address, or it is a conditional jump, in which case it contains a 2-bit condition-code register field and a 10- bit destination address. 

When the packed qualifier is used on a tagged union, all the members must have packed types, but they do not have to be of the same size. The (standard) representation for a packed tagged union is the following.

- The size is always equal to the number of bits needed to represent the tag plus the maximum of the sizes of the members.

- The size of the tag is the minimum number of bits needed to code for all the member names (e.g., 5 to 8 members would need 3 tag bits).

- The tag bits are always left-justified (i.e., towards the most-significant bits).

- For each member, the member bits are always right-justified (i.e., towards the least significant bits).

- The bits between the tag bits and the member bits are undefined. In the extreme case of a void member, only the tag is significant and all the remaining bits are undefined.

The representation scheme is applied recursively to any nested tagged unions.

Example:

If the VInt type definition had the packed qualifier, Invalid and Valid values will have the following layouts, respectively:

tag is 0 for Invalid, 1 for Valid

Example:

If the Instr type had the packed qualifier, its values will have the following layouts:

Outer tag is 0 for Add, 1 for Jmp

Inner tag is 0 for JmpU, 1 for JmpC



Comments:

Leave a comment: