Verilog/SystemVerilog: Why use structs?

“Why do we need structs when we can represent all our ports and internal signals with wires, registers, and logic type?”

This is one of the questions that I have had to address many times while discussing design methodologies at work, or by interns and students who were trying to design something.

Let’s try to understand what a struct represents conceptually.

Take the example of household electrical wiring. You have the main breaker panel for the house in one location, often the garage. The panel is the hub from where you have wires supplying power to practically all parts of your house: the bedrooms, bathrooms, kitchen, frontyard, backyard, and so on.

Each of these zones is part of an independent circuit that has its own set of wires that run from the main panel to the zone, e.g., the kitchen, and a breaker that control them.

Let’s say that your kitchen has separate wires to power the lights, the 15 Amp outlets, at 120V, and a separate set of wires for your cooking range that operates at 240V, with a current capability of 25 Amps. All these wires would be running from your main panel to your kitchen.

Extending this example to other parts of your house, you would have separate set of wires running between your panel and other parts of the house.

Imagine all these wires were connected point-to-point from the panel to each end point like the light firtures, or outlets. You would see big bundles of wires running all across your attic.

But the way the wiring is done is to use separate electrical conduits that typically run between your panel and each of your zones. Within each conduit, you would find wires running from your panel to one particular zone. For example, the conduit from the panel to your kitchen would only have the wires that power the various end points in your kitchen.

The conduits effectively bundle groups of wires that run between two points. So you don’t see the wires going to your kitchen criss-crossing the wires going to your bedroom. This makes all the wiring modular; it greatly simplifies the wiring and keeps it organized. If you had to add a new set of wires to go to your kitchen, you would just need to deal with the task of adding them to the particular conduit, without affecting any others.

Let’s use this analogy to look at what structs do in languages like C, Verilog or SystemVerilog.

It is common to have a Verilog module that contains several functional units within. Most of the internal signals (wires, registers, or logic) that are used by these units would be separate. Using discrete signals, we often find several dozens of declarations, making the code hard to read, and possibly hard to maintain, and potentially error prone.

Let’s look at a simple example to illustrate this.

// Internal signals
logic [7:0] rx_count;
logic rx_error;
logic rx_done;
logic [4:0] rx_state;
logic [7:0] tx_count;
logic tx_error;
logic tx_done;
logic [4:0] tx_state;

// rx state machine
...

// tx state machine
...

Even though this example has only a handful of signals, you can see that they belong to two functionally different units of logic. You can also see that the two sets of signals are very similar. Now, if you wanted to add a new signal e.g., abort to the two state machines, you would have to declare two copies of this new signal: rx_abort and tx_abort. Or let’s say you wanted to change the error signals to counters. Now you have to change the two error signal declarations to a vector.

Now let’s see what happens if we bundle the two sets of signals into structs.

struct packed {
    logic [7:0] rx_count;
    logic rx_error;
    logic rx_done;
    logic [4:0] rx_state;
} rx_vars;

struct packed {
    logic [7:0] tx_count;
    logic tx_error;
    logic tx_done;
    logic [4:0] tx_state;
} tx_vars;

// rx state machine
// uses rx_vars
...

// tx state machine
// uses tx_vars
...

This creates two bundles (or conduits from the electrical wiring analogy) for rx and tx. This makes the code more modular and organized. It can also help to enforce a coding rule that the rx and tx state machines operate only on the rx_vars and tx_vars respectively, to avoid mistakes.

This is better than the unbundled version without structs. But we still need to keep the two sets identical when we modify the design, or even when we fix bugs.

How can we make this even better? Let’s look at another example.

typedef struct packed {
    logic [7:0] count;
    logic error;
    logic done;
    logic [4:0] state;
} vars_t;

vars_t rx_vars;
vars_t tx_vars;

/ rx state machine
// uses rx_vars
...

// tx state machine
// uses tx_vars
...

What happened here? We defined a struct as a ‘type’ and included all the signals that we need within. Since this is a type, you can declare multiple instances of this, in this case, rx_vars and tx_vars. Now we have two copies of structs that are based on a common definition. Any changes that are common to both need to be done only in the definition.

In this example, we have two functional units: rx and tx. But in a larger design, you might have 8 instances of rx and tx each. So you would need an instance of the struct for each of these.

The next example shows a way to achieve this.

typedef struct packed {
    logic [7:0] count;
    logic error;
    logic done;
    logic [4:0] state;
} vars_t;

vars_t [7:0] rx_vars;
vars_t [7:0] tx_vars;

/ rx state machines 0..7
// uses rx_vars [7:0]
...

// tx state machine 0..7
// uses tx_vars [7:0]
...

Hopefully, you can see how structs can make your design very concise, well organized, also easy to maintain.

Author: editor

Leave a Reply

Your email address will not be published. Required fields are marked *