Using the Always Block to Model Sequential Logic in SystemVerilog
In this post, we discuss one of the most important constructs in SystemVerilog design – the always block.
As we discussed in the post on continuous assignment in SystemVerilog, there are two main classes of digital circuit which we can model in SystemVerilog – combinational and sequential.
In contrast to combinational logic, sequential circuits use a clock and require storage elements such as flip flops.
As a result, the output signals are synchronised to the circuit clock and changes do not occur immediately.
We use the always block to write code which executes sequentially in SystemVerilog. This is crucial when describing sequential logic circuits.
If you already have an understanding of the always block in verilog you can skip much of this post as most of the behavior is inherited from verilog.
However, you may still wish to read the sections on the always_ff, always_comb and always_latch blocks. All of these features were introduced as a part of the SystemVerilog extension.
The Always Block in Verilog
We use procedural blocks to create statements which execute sequentially in SystemVerilog. Procedural blocks are particularly important for the modelling of sequential digital circuits.
In contrast, SystemVerilog continuous assignment statements execute concurrently (i.e. in parallel) in our designs. This matches the nature of the underlying circuits, which consist of a number of separate logic gates.
The always block is one of the most commonly used procedural blocks in SystemVerilog. Whenever one of the signals in the sensitivity list changes state, all of the statements in the always block execute in sequence.
The SystemVerilog code below shows the general syntax for the always block. We talk about the sensitivity list in more depth in the next section.
always @(<sensitivity_list>) begin // Code to be executed goes here end
However, we need to be careful when using this construct as there are some features which are unique to SystemVerilog.
In particular, beginners often find it difficult to understand how signals in an always block are updated.
When we use always blocks, we can update the value of our signals either in parallel or sequentially. This depends on whether we use blocking or non-blocking assignment, which we discuss in more depth later in this post.
In order to be an effective SystemVerilog designer, it is important that we have a good understanding of the always block.
Let’s look at some of the key features of the always block in more detail.
Sensitivity Lists
Any code which we write within an always block runs continuously. This means that the statements within the code block execute in sequence until we reach the last line.
Once the last line in the sequence has executed, the program then loops back to the start of the always block.
However, this behaviour is not representative of a real circuit which will remain in a steady state until one of the input signals changes state.
We use the sensitivity list in the alway block to emulate this behaviour.
To do this, the code within the always block will only execute after one of the signals in the sensitivity list changes state.
Flip Flop Example
As an example, let’s consider how we would model a basic D type flip flop using the always block.
As with all clocked flip flops, the output of a D type flip flop only changes state when there is a positive clock edge.
Therefore, we have to include the clock signal in the sensitivity list so that the always block only executes when it is clocked.
The SystemVerilog code below shows how we would model a D type flip flop using the always block.
always @(posedge clock) begin q <= d; end
In this code example, we use the posedge macro to determine when there is a transition from 0 to 1.
The single line of code within the always block will execute when this macro evaluates as true. This line of code assigns the value of D to the output signal (Q).
When we use the posedge macro in SystemVerilog, all other changes of state are simply ignored. This is exactly what we would expect from a D type flip flop.
SystemVerilog also has a negedge macro which has the opposite functionality. When we use this macro, the always block will execute whenever the clock changes from 1 to 0.
We can also omit this macro altogether. In this case, the code executes whenever a signal in the sensitivity list changes state.
We should only ever use the posedge macro for clock signals in our SystemVerilog design. This is because synthesis tools will attempt to utilize clock resources within the FPGA to implement it.
Multiple Signals in a Sensitivity List
There are instances when we will want to include more than one signal in the sensitivity list.
A common example of this is when we write code to model the behaviour of flip flops with asynchronous resets.
When this is the case, we need the flip flop model to perform an action whenever the reset or clock signals change state.
To do this we simply list both of the signals inside the sensitivity list and separate them with a comma.
The SystemVerilog code below shows how we model such a flip flop.
always @(posedge clock, posedge reset) begin if (reset) begin q <= 1'b0; end else begin q <= d; end end
As this example uses an active high reset, we again use the posedge macro in the sensitivity list.
An active high reset means that the reset is only active when it is equal to one.
We then use a construct known as an if statement to determine whether the always block triggered by the reset signal or the clock signal.
We will discuss the SystemVerilog if statement in a future blog post, although it’s functionality is fairly self explanatory.
Blocking vs Non Blocking Assignment in SystemVerilog
In the code examples we have seen so far in this series of posts, we have used two different types of assignment operators.
This is because SystemVerilog has two different types of assignment – blocking and non-blocking.
When we write code with non-blocking assignments we use the <= symbol whilst blocking code uses the = symbol.
When we use continuous assignment in SystemVerilog, we can only use blocking assignment.
However, we can use both types of assignment in a procedural block.
Blocking assignment typically results in the implementation of combinational logic circuits after we have synthesized our code. In contrast, non-blocking assignment normally results in sequential circuits after synthesis.
Blocking assignment is the simplest of the two techniques to understand. When we assign signals using blocking assignment in SystemVerilog, the value of the signal updates as soon as the line of code executes.
We commonly use this type of assignment to write combinational logic in SystemVerilog. However, in some circumstances we can use it to create sequential circuits.
In contrast, signals which use the non-blocking technique do not update immediately after assignment. Instead, SystemVerilog uses assignment scheduling to update the values.
This is a little tricky to understand, so let’s consider it in a bit more depth.
Assignment Scheduling
When we write SystemVerilog code using non-blocking assignment, our code still executes sequentially. However, the signals which we are assigning data to will not update in this way.
To demonstrate why this is the case, let’s consider the twisted ring counter circuit below.
always @(posedge clock) begin q_dff1 <= ~q_dff2; q_dff2 <= q_dff1; end
First, let’s look at the behaviour if the signals did update immediately.
If we assume that the output of both flip flops is 0b when a clock edge occurs, then the second line of code will set the output of DFF1 to 1b.
We can then see that the line of code immediately beneath this would set the output of DFF2 to 1. This is clearly not the intended behaviour of this circuit.
To overcome this issue, non blocking assignment in SystemVerilog uses scheduled assignment.
As a result, signal changes don’t occur immediately after assignment but are instead scheduled to occur at some point in the future.
Normally, the signals update their value at the end of a simulation cycle. This refers to the time it takes the simulator to execute all of the code for a given time step.
To better demonstrate the way scheduled assignment works, let’s again consider the simple dual flip flop circuit.
When a rising edge is detected, our simulator will firstly execute the statement which updates DFF1. Once this line has executed, the output of DFF1 is scheduled to be updated.
The simulator then runs the second line of code, this time using the original value of the DFF1 flip flop and schedules the update of DFF2.
As there are only two statements in this design, the simulation cycle is now complete. At this point, all of the scheduled changes are applied and the values are updated for both flip flops.
Synthesis Example
To further demonstrate the difference between blocking and non blocking assignments in SystemVerilog, we will again model a basic two flip flop twisted ring counter circuit. The code snippet below shows he implementation of this circuit.
always @(posedge clock) begin q_dff1 <= ~q_dff2; q_dff2 <= q_dff1; end
However, we can also look at the output of a synthesis tool such as vivado to see a diagram of the resulting circuit. The circuit diagram below shows this circuit.
We can see that there are two flip flops in this circuit whilst the not gate is implemented using LUT1.
Now let’s take at look at the circuit we would get if we used blocking assignment in the code.
The SystemVerilog code below shows how we could (incorrectly) attempt to model this circuit using blocking assignment.
always @(posedge clock) begin q_dff1 = ~q_dff2; q_dff2 = q_dff1; end
This results in the circuit shown below after synthesis.
We can see from this that using non blocking has resulted in the removal of the second flip flop from our circuit.
The reason for this should be fairly obvious, given what we have learnt about blocking assignment so far.
As the value of the q_dff2 is immediately assigned to the same value as q_dff1, the circuit model does not imply that there should be a flip flop in this signal path.
This example actually shows us one of the most important differences between blocking and non blocking assignment in SystemVerilog.
When we use non-blocking assignment, the synthesis tool will always place a flip flop in the circuit. This means that we can only use non blocking assignment to model sequential logic.
In contrast, we can use blocking assignment to create either sequential or combinational circuits.
However, we should only use blocking assignment to model combinational logic circuits in SystemVerilog. The main reason for this is that our code will be much easier to understand and maintain.
Improved Always Blocks in SystemVerilog
As we have already discussed, we can use the verilog always block to model either sequential or combinational logic circuits.
However, when we model more complex circuits it can be difficult to understand which type of logic we are implementing.
In addition to this, we may also find that different synthesis tools infer different types of logic circuit.
Therefore, a number of improved always blocks were introduced as a part of the SystemVerilog language.
We can use these improved blocks to tell our tools which type of logic circuit we are trying to model.
By doing this, we can make our tools enforce extra rules which will help us to write more reliable code.
Let’s look at each of the improved SystemVerilog always blocks in more detail.
Systemverilog always_comb Block
We can use the always_comb block when we want to model a combinational logic circuit in SystemVerilog.
When we use this construct, we effectively tell our compiler and synthesis tools to enforce stricter rules. The purpose of these rules is to help ensure that we have correctly modelled our logic circuit.
The code snippet below shows the general syntax we use to declare an always_comb block in SystemVerilog.
always_comb begin // Function code goes here end
As we can see from this construct, we don’t need to include a sensitivity list when we use the always_comb block.
The reason for this is that our compiler automatically generates the sensitivity list when we use this construct.
In this respect, we can think of the SystemVerilog always_comb block as being roughly equialvent to always @(*) block in verilog.
In this construct, we use the * character to tell our verilog tools to automatically decide which signals to include in the sensitivity list.
However, we can also take advantage of some additional improvements in the always_comb block that help us to write more reliable code.
As a result of these improvements, we should always use the always_comb block to model combinational logic circuits in SystemVerilog
Let’s have a look at some of the main improvements which are include as part of the SystemVerilog always_comb block.
Improved Handling of Functions
One problem with the always @(*) block in verilog is the way which functions are handled.
We talk about SystemVerilog functions in more details in a later post. However, we can think of functions as a simple piece of SystemVerilog code which has one or more inputs and returns an output.
When we use functions inside the always @(*) block in verilog, our compiler and synthesis tools won’t include the outputs of these functions in our automatically generated sensitivity list.
This can lead to instances where our tools don’t properly generate the sensitivity list. As a result of this, we may find that our model doesn’t work as intended.
In contrast, our compiler and synthesis tools will fully incorporate all inputs and outputs of any function which we use inside of an always_comb block.
As a result of this, our tools are more reliable at generating the sensitivity list when we use the SystemVerilog always_comb block.
Improved Coding Rules
The always_comb block features some additional coding rules which are designed to prevent common mistakes when modelling combinational logic.
The first of these new rules means that we can only use blocking assignment inside of the always_comb block.
The reason for this is that our synthesis tool will typically insert flip flops into our design when we use non-blocking code.
As we are modelling combinational logic, we should obviously avoid using any constructs which result in sequential logic being implemented.
While this may seem like a trivial improvement, this is useful as it can help to reduce simulation vs synthesis mismatches due to poorly written code.
In addition to this, when we use the SystemVerilog always_comb block our compiler will also check that the resultant circuit is purely combinational.
When we write code that can’t be implemented as combinational logic, our compiler will raise an error.
Again, this is a useful feature as it helps to prevent us from unintentionally creating latches in our design.
We generally want to avoid latches in FPGAs as they tend to cause timing problems due to their inherently asynchronous nature.
Other Minor Improvements
In addition to the major improvements we have have already discussed, the always_comb block features a couple of minor improvements.
One improvement which was introduced as a part of the SystemVerilog always_comb block is the way it behaves during simulation.
When we use the verilog always block to model combinational logic circuits, the outputs of the block update when one of the signals in the sensitivity list change state.
As a result of this, we may have a period of time at the beginning of our simulations where the variables in the always block are in an undefined state.
In contrast, when we use the always_comb block our variables are updated at the start of our simulation. As a result, we won’t have any undefined signals in our simulation unless we have made a mistake in our code.
Although this makes no difference to the performance of our design, our simulations will be more accurate as a result of this.
Finally, we can only assign data to a variable from one always_comb block. Again, this is in contrast to the verilog always block which allows for variables to be driven by multiple processes.
When we write code which is driven by more than one block, our synthesis tool will generate circuits which have multiple drivers.
We should generally avoid this scenario in digital systems as it often leads to logic circuits which have non-deterministic behavior.
As we can’t compile code with multiple drivers in an always_comb block, this reduces our chances of writing code which is non-deterministic.
Multiplexor Example
In order to better demonstrate how we would use the always_comb block in practise, let’s consider a basic example.
For this example, we will model a basic 4 to 1 multiplexor.
As we mentioned in the post on modelling combinational logic, we often use always blocks to model multiplexors in SystemVerilog.
To do this, we can use a construct known as the case statement. The case statement is a construct which we can only use inside of procedural blocks such as an always_comb block.
In comparison to the methods we discussed in the last post, this provides a simpler and more intuitive way of modelling large multiplexors.
We talk about the SystemVerilog case statement in more detail in a later post. However, the code snippet below shows how we use the always_comb block and the case statement to model a simple four to one multiplexor. We can also simulate this code on eda playground.
always_comb begin case (addr) 0 : begin // This branch executes when addr = 0 mux_out = a; end 1 : begin // This branch executres when addr = 1 mux_out = b; end 2 : begin // This branch executes when addr = 2 mux_out = c; end 3 : begin // This branch executes when addr = 3 mux_out = d; end endcase end
As we can see from this example, it is simple to use the SystemVerilog always_comb block in place of the old verilog always block. When we do this, it makes it easier for our synthesis tool to understand what type of circuit we are implementing.
The case statement is also fairly simple to understand, as it uses a variable to select one of a number of branches to execute.
We can include as many different branches as we require in the case statement.
In addition, we use can a default branch to catch any values which we haven’t explicitly listed.
In order to use this as a multiplexor, we use the variable as if it were the address pins.
We can then assign the output of the multiplexor to the required value based on which branch we are executing.
SystemVerilog always_latch Block
As we previously mentioned, we should generally try to avoid using latches when we design an FPGA.
However, we may encounter an instance when this is unavoidable and we need to implement a latch in our design.
When we need to create a latch in SystemVerilog, we can use the always_latch block to do this.
The code syntax below shows the general syntax we use when we write an always_latch block.
always_latch begin if (enable) begin // Code to be executed goes here end end
We use the <enable> field in this construct to identify which signal in our design will control the latch.
When we use the SystemVerilog always_latch block, we also need to include an if statement in order to properly model the behavior of a latch. We talk about the SystemVerilog if statement in more detail in a later post.
As with the always_comb block, we don’t need to write a sensitivity list when we use the always_latch block. Our compiler will automatically generate the sensitivity list for us.
The rules which apply to the always_comb block, as we discussed above, also apply to the always_latch block.
The only exception to this is that our compiler won’t prevent the creation of latches when we use the always_latch block.
The reason for this is that we use the always_latch block when we specifically want to implement a latch in our SystemVerilog code.
However, we can also unintentionally create latches in our FPGA designs. As we generally want to avoid latches, we can use the always_latch block to tell our compiler and synthesis tools that this isn’t a mistake.
We should then use the always_comb block to model any other type of combinational logic in SystemVerilog. By doing this, our compiler and synthesis tools will tells us when we have created a latch through poor coding.
If we stick to this rule, we can improve the reliability of our code by taking advantage of the in built rules for these blocks.
Systemverilog always_ff Block
We use the always_ff block when we want to implement sequential logic circuits in SystemVerilog.
Infact, we can think of the always_ff block as being the sequential logic equivalent of the always_comb block.
The SystemVerilog code below shows the general syntax for the always_ff block.
always_ff @(<sensitivity_list>) begin // Code to be executed goes here end
As we can see from this, we need to include a sensitivity list when we use the SystemVerilog always_ff block.
We use the same syntax to write the sensitivity list as we do when we write a normal verilog always block. We discussed the format of the sensitivity list for SystemVerilog always blocks earlier in this post.
In fact, we use virtually the same syntax for both the SystemVerilog always_ff block and the verilog always block.
The reason for this is that the behavior of the SystemVerilog always_ff block is very similar to the verilog always block.
However, there are also some minor differences between these two blocks which we need to be aware of.
Let’s take a look at the main difference between these two blocks.
Differences Between always and always_ff Blocks
The main difference between always and always_ff blocks is the way that we can use blocking and non-blocking assignment.
When we model a circuit using the always_ff block, we can only use non-blocking signal assignment.
In contrast, we can use either blocking or non-blocking assignment in an always block.
The reason for this is that the always_ff block is designed specifically to model sequential logic circuits.
As we discussed previously, our synthesis tool will typically implement combinational logic when we use blocking statements inside an always block.
Therefore, we should have no need to use blocking statements when we are modelling sequential logic circuits.
The other difference between the always_ff and always blocks is that we can only assign data to a variable from one always_ff block.
In contrast, we can assign data to a variable from multiple verilog always blocks.
As we mentioned in the section on the always_comb block, assigning data to a signal from multiple blocks means our synthesis tool may generate circuits which have multiple drivers.
We should generally avoid this scenario in digital systems as it often leads to logic circuits which have non-deterministic behavior.
As we can’t compile code with multiple drivers in an always_ff block, this reduces our chances of writing code which is non-deterministic.