Introduction

If you compare Verilog and VHDL to a general-purpose programming language, there is a striking difference. In addition to variable assignment, HDLs support a second kind of assignment. In VHDL, this is called signal assignment and I will use this terminology to refer to the general concept.

The Verilog incarnation of signal assignment has an even stranger name: nonblocking assignment. Remarkably, it is notoriously difficult to grasp the subtleties of Verilog assignments. Discussions about this subject pop up regularly on technical forums.

Typical explanations take signal assignments as a given and then try to explain how they work. In this post, I will start from a more fundamental question: why do we need signal assignments in the first place? The answer may provide more insight in the matter, irrespective of the HDL you use.

Concurrency?

So, why do we need signals assignments? Some designers may think the answer is obvious and reply “Because they provide the concurrent semantics needed to model hardware.”

But when you take a closer look, this answer is questionable. After all, HDLs already have a clear and general way to express concurrency: processes. To model concurrent assignments, we could simply put them in separate processes. If signal assignments are just some syntactic sugar that often causes confusion, we may be better off without them.

Determinism

To understand why we need signals assignments, imagine an HDL without them. Assume that all processes are triggered by the same clock edge. Concurrent assignments would be modeled by variable assignments in separate processes, as illustrated below (using a Verilog-like pseudo-code):

P1: always @(posedge clock)
   b = a
P2: always @(posedge clock)
   c = b

As the processes are concurrent, a simulator can run them in any order. For a given start point, this is what happens in the case where the simulator runs process P1 followed by process P2:

// a is 0, b is 1
// simulator runs P1, then P2
b = a
c = b
// all processes suspended
// b is 0, c is 0

And here is what happens in the other case where the simulator runs process P2 followed by process P1:

// a is 0, b is 1
// simulator runs P2, then P1
c = b
b = a
// all processes suspended
// b is 0, c is 1

Do you see the problem? The final value of c is different because it depends on the arbitrary order in which the simulator chooses to run the processes. We say that such a simulation is nondeterministic.

Needless to say, nondeterminism is unworkable in practice. You don’t want your shift register to behave randomly like a wire :-) Thus the conclusion is that we need to provide a way in which concurrent assignments become deterministic.

Signals

Enter signals. The basic idea behind them is to store the next value away until all processes have been run. To explain the details, I will use an explicit notation. Image that a signal is implemented as an object with a special attribute called next. Signal assignment is implemented as attribute assignment, like so:

P1: always @(posedge clock)
   b.next = a
P2: always @(posedge clock)
   c.next = b

Now let’s see what happens in the simulator in the first case where the simulator runs process P1 followed by process P2:

// a is 0, b is 1
// simulator runs P1, then P2
b.next = a
c.next = b
// all processes suspended
// b.next is 0, c.next is 1

And in the second case where the simulator runs process P2 followed by process P1:

// a is 0, b is 1
// simulator runs P2, then P1
c.next = b
b.next = a
// all processes suspended
// b.next is 0, c.next is 1

The value of the next attribute for c is now identical in the two cases. It no longer depends on the order in which the processes are run. This means that we have achieved determinism! Note how the next attribute acts as a buffer that prevents race conditions.

As a final step, the simulator engine updates the signal values with the values stored in the next attribute. It works likes this:

// all processes suspended
// b.next is 0, c.next is 1
// simulator runs b = b.next; c = c.next
// b is 0, c is 1
// simulator checks which processes become active
// simulator runs active processes in any order

And that is basically how a HDL simulator works!

Concurrency as a side effect

Consider what happens when we put the signal assignments in a single process as illustrated below:

always @(posedge clock)
   b.next = a
   c.next = b

or, alternatively, in the reverse order as illustrated below:

always @(posedge clock)
   c.next = b
   b.next = a

Clearly, in either case the result will still be the same as before. Therefore, the order of the assignments does not matter. This means that they will appear to happen concurrently, even though they are in the same process. This concurrency is a side effect of the way in which signals work to achieve determinism.

The last assignment wins

Within a process, signal assignments are still executed sequentially. However, you only notice this when you use consecutive signal assignments to the same signal. For example, consider the following snippet of code:

always @(posedge clock)
   flag.next = 0
   if condition:
       flag.next = 1

After the process has run, the value of flag.next is defined by the sequence of the assignments and the value of condition. In particular, the last assignment that is executed overwrites any previous assignment. This behavior is referred to as “the last assignment wins”. It is very useful to model assignments under complex conditions.

Signal guidelines

Time for conclusions. As we have seen, signals are essential to make communication between concurrent processes deterministic. This is the reason why they exist. The mother of all guidelines for signals is therefore:

To communicate between processes, use signals.

The details of how this translates into practical guidelines depend strongly on the particular HDL. This topic is beyond the scope of this essay. (Verilog complicates matters as it doesn’t have a separate signal object.)

In all other cases, signal assignments are optional. For example, they are not essential to describe local behavior within a process. Of course, their concurrent semantics can be handy. However, it can always be emulated by variable assignments, while the opposite is not true. In any case, there is no good reason to restrict local assignments to signal assignments only.

Epilogue: the MyHDL model

The model I have used cannot explain all aspects of signals assignments in Verilog and VHDL. However, it is perfect for zero-delay signal assignments which are the cornerstone of RTL modeling. I believe that the explicit next attribute makes things crystal clear.

In contrast, signals assignments in Verilog and VHDL use a special assignment syntax (<=). This is concise, but it hides the buffering behavior of a signal.

Did you like my model for signals? Then I have some good news! As you may have guessed, this is exactly how signals are implemented in MyHDL. Even if you have no immediate plans to use MyHDL, you may want to keep its model in mind.