Enums, typedef and casting

Name your types with typedef, model an opcode field as an enum, and convert raw bus bits into an enum without accepting illegal codes.

Lesson
1.4
Time
25 min
Runs on
Your browser
Check
5 questions

A bus monitor converts a 2-bit opcode with op = op_t'(raw). After a firmware change, the DUT starts sending code 3, which the spec reserves for future use. The cast accepts it, op.name() returns an empty string, and the coverage model counts only IDLE, READ and WRITE. The test reports full opcode coverage while the DUT emits illegal traffic.

Reserved code 3 passes through a static cast
012345validraw_op012313op.name()IDLEREADWRITE""READ""code 3 accepted, no message

The monitor never reports the reserved code; it simply has no name.

typedef names a type

typedef gives a type a name without creating any storage. After typedef logic [11:0] addr_t; you can write addr_t a; instead of repeating the range everywhere.

The payoff is a single point of change. If the address grows to 16 bits, you edit the typedef, and every port, function and struct that uses addr_t follows automatically.

A typedef of a vector is still that vector, so an addr_t value and a logic [11:0] value mix freely in expressions and assignments.

An enum is a set of named values

An enumerated type, or enum, lists the legal values of a field and gives each one a name, called a label. typedef enum logic [1:0] { IDLE, READ, WRITE } op_t; declares a type with three labels.

Labels without a value count up from 0, so IDLE is 0, READ is 1 and WRITE is 2. In general, a label without a value is one more than the label before it.

The base type, here logic [1:0], sets the width and the number of states. If you leave the base type out, it defaults to int.

LabelCodeBits
IDLE000
READ101
WRITE210
(no label)311

A 2-bit base type has four codes, but op_t names only three.

An enum variable starts at its base type's default value rather than at its first label, so an op_t starts as 2'bxx. An enum with base type int starts at 0, even when no label has the value 0.

Choose the base type to match the field. A 2-bit opcode wants logic [1:0], because the width matches the bus and the variable can hold an x from the DUT. With an int base, a cast of 2'bx1 stores 1 and reads as a legal READ.

Labels live in the scope where the enum is declared, just like variable names, so two enums in one package cannot both have a label called IDLE. Give the labels a prefix, as in OP_IDLE and ST_IDLE, when enums share a scope.

Converting raw bits into an enum

SystemVerilog checks enum assignments at compile time. Writing op = raw; with a logic [1:0] raw is an error, because raw might hold a value that has no label.

A static cast such as op = op_t'(raw); forces the conversion: the bits are copied, but nothing checks them. If raw is 3, op holds 3 and op.name() returns an empty string.

The dynamic cast $cast(op, raw) checks the value first. Called as a function, it assigns and returns 1 when raw matches a label, and it returns 0 and leaves op unchanged when it does not. Called as a task, a failed cast is a run-time error.

For data from the DUT, decide what should happen to codes without a label, such as counting them, reporting them or both. A silent fallback to IDLE hides exactly the traffic from the opening story.

Static cast of bus data

op = op_t'(raw);       // 3 is stored as-is
cov.sample();          // no bin for 3

Code 3 enters the testbench with no name and no message.

Checked conversion

if (!$cast(op, raw))
  $error("reserved opcode %b", raw);
else
  cov.sample();

Code 3, or any x bit, produces an error that names the raw bits.

Other casts you will meet

The same cast syntax also changes width and sign. 4'(8'hAB) keeps the low 4 bits and gives 4'hB, signed'(4'b1111) reads the same bits as -1, and int'(3.7) rounds to 4.

Each of these casts converts without complaint, whatever the value. Use them to show what you intend, and still check the value range yourself when the data comes from the DUT.

Enum methods help diagnostics

Every enum has built-in methods. name() returns the label as a string, first() and last() return the end labels, and num() returns how many labels there are.

The method next() steps to the following label and wraps from the last label back to the first. With op holding WRITE, for example, op.next() returns IDLE.

Print op.name() in messages rather than the number, because labels are easier to read in a log. An empty name means the variable holds a value with no label, which deserves an error of its own.

Try it

The first line shows the enum starting at xx, the default of its logic base type. The static cast stores 3, and name() returns an empty string.

The checked conversion accepts 2, rejects 3 and leaves op unchanged, just as $cast would. The x1 code matches no case item, so it is rejected as well.

RUNIcarus Verilog · in your browser
Edit the code, then run it
Expected: before assignment: op=xx static cast of 3: value=3 name='' checked cast of 2: op=WRITE checked cast of 3: rejected, op stays WRITE checked cast of x1: rejected after WRITE comes IDLE; 3 labels

Check yourself

Predict the outputPredict the exact output. No value is ever assigned to lvl.
module tb;
  typedef enum { LOW = 1, MID, HIGH } level_t;
  level_t lvl;
  initial $display("value=%0d name=[%s]", lvl, lvl.name());
endmodule
Conceptop_t has labels IDLE = 0, READ = 1 and WRITE = 2 only. What does op = op_t'(2'd3); do?
Conceptop holds READ. Then ok = $cast(op, raw); runs with raw equal to 3, which has no label. What is op afterwards?
ConceptWhat does typedef logic [11:0] addr_t; create?
Spot the bugEach option is the body of function automatic bit legal_op(logic [1:0] code). It must return 1 for codes 0, 1 and 2, and 0 for code 3 or any x or z bit. Which option accepts an illegal code?

Remember

  • typedef names a type without creating storage; change the typedef and every user follows.
  • Enum labels count up from 0 unless given values; the variable starts at its base type's default, not at the first label.
  • A static cast op_t'(raw) copies any value, even one with no label.
  • $cast called as a function returns 0 and leaves the destination unchanged when the value has no label.
  • Decide what happens to codes without a label, and report them instead of mapping them to a default.

Practice