SystemVerilog Struct and Union – for Designers too

SystemVerilog struct (ure) and union are very similar to their C programming counterparts, so you may already have a good idea of how they work. But have you tried using them in your RTL design? When used effectively, they can simplify your code and save a lot of typing. Recently, I tried incorporating SystemVerilog struct and union in new ways that I had not done before with surprisingly (or not surprisingly?) good effect. In this post I would like to share with you some tips on how you can also use them in your RTL design.

What is a SystemVerilog Struct (ure)?

A SystemVerilog struct is a way to group several data types. The entire group can be referenced as a whole, or the individual data type can be referenced by name. It is handy in RTL coding when you have a collection of signals you need to pass around the design together, but want to retain the readability and accessibility of each separate signal.

When used in RTL code, a packed SystemVerilog struct is the most useful. A packed struct is treated as a single vector, and each data type in the structure is represented as a bit field. The entire structure is then packed together in memory without gaps. Only packed data types and integer data types are allowed in a packed struct. Because it is defined as a vector, the entire structure can also be used as a whole with arithmetic and logical operators.

An unpacked SystemVerilog struct, on the other hand, does not define a packing of the data types. It is tool-dependent how the structure is packed in memory. Unpacked struct probably will not synthesize by your synthesis tool, so I would avoid it in RTL code. It is, however, the default mode of a structure if the packed keyword is not used when defining the structure.

SystemVerilog struct is often defined with the typedef keyword to give the structure type a name so it can be more easily reused across multiple files. Here is an example:

typedef enum logic[15:0]
{
  ADD = 16'h0000,
  SUB = 16'h0001
} my_opcode_t;

typedef enum logic[15:0]
{
  REG = 16'h0000,
  MEM = 16'h0001
} my_dest_t;

typedef struct packed
{
  my_opcode_t  opcode; // 16-bit opcode, enumerated type
  my_dest_t    dest; // 16-bit destination, enumerated type
  logic [15:0] opA;
  logic [15:0] opB;
} my_opcode_struct_t;

my_opcode_struct_t cmd1;

initial begin
  // Access fields by name
  cmd1.opcode <= ADD;
  cmd1.dest <= REG;
  cmd1.opA <= 16'h0001;
  cmd1.opB <= 16'h0002;

  // Access fields by bit position
  cmd1[63:48] <= 16'h0000
  cmd1[47:32] <= 16'h0000;
  cmd1[31:16] <= 16'h0003;
  cmd1[15: 0] <= 16'h0004;

  // Assign fields at once
  cmd1 <= '{SUB, REG, 16'h0005, 16'h0006};
end

What is a SystemVerilog Union?

A SystemVerilog union allows a single piece of storage to be represented different ways using different named member types. Because there is only a single storage, only one of the data types can be used at a time. Unions can also be packed and unpacked similarly to structures. Only packed data types and integer data types can be used in packed union. All members of a packed (and untagged, which I’ll get to later) union must be the same size. Like packed structures, packed union can be used as a whole with arithmetic and logical operators, and bit fields can be extracted like from a packed array.

A tagged union is a type-checked union. That means you can no longer write to the union using one member type, and read it back using another. Tagged union enforces type checking by inserting additional bits into the union to store how the union was initially accessed. Due to the added bits, and inability to freely refer to the same storage using different union members, I think this makes it less useful in RTL coding.

Take a look at the following example, where I expand the earlier SystemVerilog struct into a union to provide a different way to access that same piece of data.

typedef union packed
{
  my_opcode_struct_t opcode_s; // "fields view" to the struct
  logic[1:0][31:0] dword; // "dword view" to the struct
} my_opcode_union_t;

my_opcode_union_t cmd2;

initial begin
  // Access opcode_s struct fields within the union
  cmd2.opcode_s.opcode = ADD;
  cmd2.opcode_s.dest = REG;
  cmd2.opcode_s.opA = 16'h0001;
  cmd2.opcode_s.opB = 16'h0002;

  // Access dwords struct fields within the union
  cmd2.dword[1] = 32'h0001_0001; // opcode=SUB, dest=MEM
  cmd2.dword[0] = 32'h0007_0008; // opA=7, opB=8
end

Ways to Use SystemVerilog Struct in a Design

There are many ways to incorporate SystemVerilog struct into your RTL code. Here are some common usages.

Encapsulate Fields of a Complex Type

One of the simplest uses of a structure is to encapsulate signals that are commonly used together into a single unit that can be passed around the design more easily, like the opcode structure example above. It both simplifies the RTL code and makes it more readable. Simulators like Synopsys VCS will display the fields of a structure separately on a waveform, making the structure easily readable.

If you need to use the same structure in multiple modules, a tip is to put the definition of the structure (defined using typedef) into a SystemVerilog package, then import the package into each RTL module that requires the definition. This way you will only need to define the structure once.

SystemVerilog Struct as a Module Port

A module port can have a SystemVerilog struct type, which makes it easy to pass the same bundle of signals into and out of multiple modules, and keep the same encapsulation throughout a design. For example a wide command bus between two modules with multiple fields can be grouped into a structure to simplify the RTL code, and to avoid having to manually decode the bits of the command bus when viewing it on a waveform (a major frustration!).

Using SystemVerilog Struct with Parameterized Data Type

A structure can be used effectively with modules that support parameterized data type. For example if a FIFO module supports parameterized data type, the entire structure can be passed into the FIFO with no further modification to the FIFO code.

module simple_fifo
(
  parameter type DTYPE = logic[7:0],
  parameter      DEPTH = 4
)
(
  input  logic                      clk,
  input  logic                      rst_n,
  input  logic                      push,
  input  logic                      pop,
  input  DTYPE                      data_in,
  output logic[$clog2(DEPTH+1)-1:0] count,
  output DTYPE                      data_out
);
  // rest of FIFO design
endmodule

module testbench;
  parameter MY_DEPTH = 4;

  logic clk, rst_n, push, pop, full, empty;
  logic [$clog2(MY_DEPTH+1)-1:0] count;
  my_opcode_struct_t data_in, data_out;

  simple_fifo
  #(
    .DTYPE (my_opcode_struct_t),
    .DEPTH (MY_DEPTH)
  )
  my_simple_fifo (.*);
endmodule

Ways to Use SystemVerilog Union in a Design

Until very recently, I had not found a useful way to use a SystemVerilog union in RTL code. But I finally did in my last project! The best way to think about a SystemVerilog union is that it can give you alternative views of a common data structure. The packed union opcode example above has a “fields view” and a “dword view”, which can be referred to in different parts of a design depending on which is more convenient. For example, if the opcode needs to be buffered in a 64-bit buffer comprised of two 32-bit wide memories, then you can assign one dword from the “dword view” as the input to each memory, like this:

my_opcode_union_t my_opcode_in, my_opcode_out;

// Toy code to assign some values into the union
always_comb begin
  my_opcode_in.opcode_s.opcode = ADD;
  my_opcode_in.opcode_s.dest   = REG;
  my_opcode_in.opcode_s.opA    = 16'h0001;
  my_opcode_in.opcode_s.opB    = 16'h0002;
end

// Use the "dword view" of the union in a generate loop
generate
  genvar gi;
  for (gi=0; gi<2; gi=gi+1) begin : gen_mem
    // instantiate a 32-bit memory
    mem_32 u_mem
    (
      .D (my_opcode_in.dword[gi]),
      .Q (my_opcode_out.dword[gi]),
      .*
    );
  end // gen_mem
endgenerate

In my last project, I used a union this way to store a wide SystemVerilog struct into multiple 39-bit memories in parallel (32-bit data plus 7-bit SECDED encoding). The memories were divided this way such that each 32-bit dword can be individually protected by SECDED encoding so it is individually accessible by a CPU. I used a “dword view” of the union in a generate loop to feed the data into the SECDED encoders and memories. It eliminated alot of copying and pasting, and made the code much more concise!

Conclusion

SystemVerilog struct and union are handy constructs that can encapsulate data types and simplify your RTL code. They are most effective when the structure or union types can be used throughout a design, including as module ports, and with modules that support parameterized data types.

Do you have another novel way of using SystemVerilog struct and union? Leave a comment below!

References

[lab_subscriber_download_form download_id=6].

4 thoughts on “SystemVerilog Struct and Union – for Designers too”

  1. Love your posts!

    I think you got something wrong in the first code example:
    Structs should have each element separated by semicolons, not commas.

    i.e.
    typedef struct packed
    {
    my_opcode_t opcode; // 16-bit opcode, enumerated type
    my_dest_t dest; // 16-bit destination, enumerated type
    logic [15:0] opA;
    logic [15:0] opB;
    } my_opcode_struct_t;

    Reply
    • Thanks Jonas for noticing the typo! You’re completely correct, I’ve fixed the code snippet above. The same code is correct in the download for this post (I did compile and test that one).

      Reply
  2. Hi Jason,

    I noticed another typo in your second code example:
    my_opcode_struct_t opcode_s; “fields view” to the struct
    Should be:
    my_opcode_struct_t opcode_s; // “fields view” to the struct

    Keep the helpful articles coming.

    Reply

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.