Rules

Introduction

The semantics behind SpinalHDL are important to learn, so that you understand what is really happening behind the scenes, and how to control it.

These semantics are defined by multiple rules:

  • Signals and registers are operating concurrently with each other (parallel behavioral, as in VHDL and Verilog)

  • An assignment to a combinational signal is like expressing a rule which is always true

  • An assignment to a register is like expressing a rule which is applied on each cycle of its clock domain

  • For each signal, the last valid assignment wins

  • Each signal and register can be manipulated as an object during hardware elaboration in a OOP manner

Concurrency

The order in which you assign each combinational or registered signal has no behavioral impact.

For example, both of the following pieces of code are equivalent:

val a, b, c = UInt(8 bits) // Define 3 combinational signals
c := a + b  // c will be set to 7
b := 2      // b will be set to 2
a := b + 3  // a will be set to 5

This is equivalent to:

val a, b, c = UInt(8 bits) // Define 3 combinational signals
b := 2      // b will be set to 2
a := b + 3  // a will be set to 5
c := a + b  // c will be set to 7

More generally, when you use the := assignment operator, it’s like specifying a new rule for the left side signal/register.

Last valid assignment wins

If a combinational signal or register is assigned multiple times, the last valid one wins.

As an example:

val x, y = Bool             // Define two combinational signals
val result = UInt(8 bits)   // Define a combinational signal

result := 1
when(x) {
  result := 2
  when(y) {
    result := 3
  }
}

This will produce the following truth table:

x

y

=>

result

False

False

1

False

True

1

True

False

2

True

True

3

Signal and register interactions with Scala (OOP reference + Functions)

In SpinalHDL, each hardware element is modeled by a class instance. This means you can manipulate instances by using their references, such as passing them as arguments to a function.

As an example, the following code implements a register which is incremented when inc is True and cleared when clear is True (clear has priority over inc) :

val inc, clear = Bool            // Define two combinational signals/wires
val counter = Reg(UInt(8 bits))  // Define an 8 bit register

when(inc) {
  counter := counter + 1
}
when(clear) {
  counter := 0    // If inc and clear are True, then this  assignment wins (Last valid assignment rule)
}

You can implement exactly the same functionality by mixing the previous example with a function that assigns to counter:

val inc, clear = Bool
val counter = Reg(UInt(8 bits))

def setCounter(value : UInt): Unit = {
  counter := value
}

when(inc) {
  setCounter(counter + 1)  // Set counter with counter + 1
}
when(clear) {
  counter := 0
}

You can also integrate the conditional check inside the function:

val inc, clear = Bool
val counter = Reg(UInt(8 bits))

def setCounterWhen(cond : Bool,value : UInt): Unit = {
  when(cond) {
    counter := value
  }
}

setCounterWhen(cond = inc,   value = counter + 1)
setCounterWhen(cond = clear, value = 0)

And also specify what should be assigned to the function:

val inc, clear = Bool
val counter = Reg(UInt(8 bits))

def setSomethingWhen(something : UInt, cond : Bool, value : UInt): Unit = {
  when(cond) {
    something := value
  }
}

setSomethingWhen(something = counter, cond = inc,   value = counter + 1)
setSomethingWhen(something = counter, cond = clear, value = 0)

All of the previous examples are strictly equivalent both in their generated RTL and also in the SpinalHDL compiler’s perspective. This is because SpinalHDL only cares about the Scala runtime and the objects instantiated there, it doesn’t care about the Scala syntax itself.

In other words, from a generated RTL generation / SpinalHDL perspective, when you use functions in Scala which generate hardware, it is like the function was inlined. This is also true case for Scala loops, as they will appear in unrolled form in the generated RTL.