Combinational circuits#

Learning goals#

  • Apply basic features of the language to describe combinational logic

Introductory problem#

Implement a circuit with the following requirements:

  1. The first output two_are_high must be high if exactly two of the inputs i{0..3} are high.

  2. The second output three_are_low must be low if exactly three of the inputs i{4..7} are low.

First test the circuit using a testbench. Then transfer the circuit to the board using:

  1. Slide switches for inputs

  2. An LED for each output

Tasks#

Read sections 3 and 4 of SV Guide.

Quiz#

# Which can be found inside a `module`? <!-- gate-level comb modeling --> - [x] Signal declarations - [x] Other module instantiations - [x] Continuous assignments - [x] Procedural blocks - [ ] A primitive gate definition # Which stands for *don't care*? - [ ] `-1` - [x] `X` - [ ] `Z` - [ ] `DC` - [ ] `D` # Which are correct regarding the following? ```sv wire a; logic b; ``` - [x] Both of them can be used to model data objects. - [ ] A `wire` is like a *variable* in typical programming languages. > `wire` cannot store a value. It must always remain driven by another signal. - [ ] `logic b` has the same language semantics as `reg b`. So `b` describes a *register*. > `b` does not necessarily describe a register. `logic` can also model a combinational signal like `x & b`. As the literal `reg` suggests a register, modern SV standard discourages the use of the `reg` keyword. You should confine to using `logic` when declaring signals. - [ ] `b` cannot be assigned in a continuous assignment but `a` can. > `a` is a net and `b` is a variable. Both nets and variables can be assigned in a continuous assignment. - [ ] `b` can have multiple drivers at the same time. > Only *net* objects like `wire` can be driven by multiple signals at the same time. - [x] When writing SV for circuits that will be implemented on an FPGA it is typically sufficient to use `logic` to declare signals inside a module. # What does `and and_i (a, b, c)` represent? - [x] AND gate with `b` and `c` as inputs and `a` as output - [ ] AND gate with `a` and `b` as inputs and `c` as output - [ ] Concatenation of `b` and `c` assigned to `a` - [ ] Concatenation of `a` and `b` assigned to `c` # Generally we should not use gate level modeling in abstract modeling. Which is/are the preferred way/s of writing *a is true only if (1) either b or c is true, and (2) both b and c are true*? - [x] `a = b | c` - [ ] `or or_i (a, b, c)` - [x] `if (b | c) a = 1; else a = 0;` # Put the components of a `module` in order: 1. `module _name_` 1. parameters 1. port declarations, e.g, input, output 1. internal signal declarations 1. module instantiations 1. continuous assignments or procedural blocks 1. `endmodule` # What is the output of `{2{a, b}}` - [ ] `{{a, b}, {a, b}}` - [ ] `{a, b}, {a, b}` - [ ] `2*a, 2*b` - [x] `{a, b, a, b}`

Mini-lecture#

Exercise 1

What is the typical difference of SystemVerilog to traditional programming languages?

Exercise 2

What is the difference between Verilog and SystemVerilog?

Different abstraction levels#

  • gate-level

  • register-transfer-level (RTL)

  • structural

  • behavioral

Exercise 3

What does the synthesis step involve in an FPGA development tool?

Exercise 4

What does the implementation step involve in an FPGA development tool?

Exercise 5

Generally we should not use gate level modeling in abstract modeling. What is the purpose of this low-level of abstraction then?

Data types and data objects#

Definitions:

data type

A set of values and a set of operations that can be performed on these values.

Note that a data type is not only defined by possible values but also the operations that we can apply on them. Data types are used (1) to declare data objects or (2) other data types.

data object

A named entity that is associated with a data value and type, e.g., a parameter, variable or net.

So a data object has always have a data type.

Logic values:
  1. x unknown logic

  2. z high impedance

  3. 0 logic zero or false condition

  4. 1 logic one or true condition

SV allows only these four states of values. The name of the basic 4-state data type is logic.

logic can be used to construct other data types. There are data types which are not 4-state, e.g., 2-state values, e.g., int, bit.

Integer data types#

Integer data types

2-state:

  • shortint, int, longint: 16, 32, 64 bit signed integer, respectively

  • byte: 8 bit signed integer or ASCII character

  • bit: user-defined vector size, unsigned

4-state:

  • logic, reg: user-defined vector size, unsigned

  • integer, 32-bit signed integer

  • time, 64-bit signed integer

We can still make a logic array signed by augmenting with the keyword signed: logic signed[15:0].

For hardware signals we should use 4-state types, because this aids the designer to debug initialization problems. 2-state variables are useful for:

  • simulation code

  • For declaring loop variables which are only for describing repetitive logic

  • Constants

It would be more convenient to use 4-state logic only, however 2-state values require less memory in a typical simulator, so this may be the reason why they exist in the language.

The SV simulator Verilator uses mostly two-states for speed.


When you search for SV code on the web, you will very often find people using reg for signals. You should prefer using logic which was introduced 2005 to the language:

reg vs logic

reg and logic denote the same type, however logic is a more descriptive term because reg does not always describe a register and thus does not accurately describe the writer’s intent.

Arrays#

Indexing and slicing of arrays#

Probably you are used to the slicing concept from other languages, e.g., arr[1:4]. SystemVerilog allows slicing relative to an anchor.

An expression can select part of a packed array, or any integer type, which is assumed to be numbered down to 0.

The term part-select refers to a selection of one or more contiguous bits of a single-dimension packed array.

logic [63:0] data;
logic [7:0] byte2;
byte2 = data[23:16]; // an 8-bit part-select from data

The term slice refers to a selection of one or more contiguous elements of an array.

The size of the part-select or slice shall be constant, but the position can be variable.

int i = bitvec[j +: k]; // k is a constant
int a[x:y], b[y:z], e;
a = {b[c -: d], e}; // d is a constant

[j +: k] is a constant width (of k) slice beginning at j. This construct can be used whenever we want to select a fixed width according to a dynamic signal, e.g., an address:

Listing 1 code/slicing-dynamic-constant-width/m.sv#
module m;
  byte bit_array = 'b11_00_10_10;
  int address;
  logic [1:0] current_bit_pair;

  assign current_bit_pair = bit_array[2*address+:2];
  initial begin
    for (address = 0; address < 4; ++address) #1 $displayb(current_bit_pair);
    $finish;
  end
endmodule
🟩 Simulation start
10
10
00
11
- m.sv:9: Verilog $finish

Modules#

The logic data type#

Exercise 6

logic data type is 4-state. What does this mean?

Exercise 7

  1. What do the values X and Z mean?

Exercise 8

Why should we not use int to model signals?

Exercise 9

What is the difference between wire and var (variable)?

All port signals are wires as default. So we need to augment an output port with logic if we want to use this port in a procedural block, e.g.:

module m(
    input a, b, enable,
    output logic c  // without logic it is an error
);
always_comb begin
    c = a & b;
    if (!enable)
        c = 0;
end
endmodule

Try it yourself.

Exercise 10

Is logic a net or variable?

Exercise 11

Why is it sufficient to use the data type logic in FPGA design?

Module instantiation#

Exercise 12

In the example we see that and, or and not are instantiated even they were not defined before. How is this possible?

Exercise 13

How can we connect the ports of a module during instantiation? Show some of them and explain how they work.

Literals#

Integer literals

  • most basic form: 10. This is a 32 bit signed decimal.

  • providing the size: 2'10

  • providing the base: 2'b10, (h, o, d also exist)

  • signedness: 2'sb10

More details are in SV2017-5.7.1.

Filling an array with the same value: x = '1

All the bits of x are assigned to 1.

Parameters#

  • param is a compile-time module constant. It is typically used to design modules that support variable input and output sizes. param can be provided when a module is instantiated.

  • localparam is a compile-time constant. It is used for constants used in the design.

RT-level combinational modeling#

Continuous assignments#

assign a = b;

Conditional operator#

Ternary operator:

output = condition ? output_if_true : output_if_false;

Exercise 14

How can we describe a 2-to-1 multiplexer (two data inputs a, b, select signal sel, output signal o using a continuous assignment?

Bitwise operators#

  • &

  • ^

  • |

  • ~

Exercise 15

How can we shorten the following code?

assign
    x[0] = ~y[0],
    x[1] = ~y[1],
    x[2] = ~y[2];

Logical operators#

Exercise 16

Write code for the following as continuous assignment: x must be assigned 2 if the conditions a and b are true, and 3 otherwise.

Reduction operators#

Exercise 17

You have five two-way switches placed on different walls of your room that can control your room light. Every switch can toggle the light. Write code using a reduction operator that models this behavior. The switches are modeled by the signal logic [4:0] switches and the light as logic light.

Arithmetic operators#

Shift operators#

  • <<: logical

  • <<<: arithmetic

Difference between a logical and arithmetic shift

The arithmetic shift takes the sign of the number into account by extending the sign bit (the leftmost bit in a signed number). For example if 4 bit -2 = 1110 is logically shifted to the right, then we get 0111 = 7. In case of arithmetic right shift, which is the same as division by two: we get: 1111 = -1.

In arithmetic shift, the sign is extended — so called sign extension. For example we extend the sign bit of -1 = 1110, which is 1, and get 1111, when we shift to the right. Without a sign-extension we would have 0111.

Comparison operators#

Concatenate and replicate operators#

  • concatenate: {a, b, c}

  • replication: {3{x, z}} => {x, z, x, z, x, z}

Exercise 18

You have the signals a, b, c. Create the concatenation of signals aabcabcabcb and assign to f.

Loop statements#

  • for

  • forever

  • repeat

  • while

  • do ... while

  • foreach

Always block for combinational design#

  • always can be used both for combinational and sequential design

  • always_comb only for combinational

  • always_ff only for sequential

But what is the advantage of using an always_comb statement over a simple always or a continuous assignment?

  1. always statement typically needs a sensitivity list. always_comb has an implicit sensitivity list for all signals that are read in the block.

    always @* is an alternative and executes the block whenever a read signal inside the block changes. However always_comb has many advantages over always @*. For example always @* may not be executed in the beginning of a simulation which may lead to errors if some signals are initialized with constants. Constants are not signals, so they won’t trigger the always block. Another advantage is that the variables on the left-hand side of the assignments may not be written by other always blocks which may lead to errors. For other differences refer to LRM 2017-9.2.2.2.2.

  2. Advantage over continuous assignment: We can use procedural code like if/else statements which are more general than the ternary operator in continuous assignments.

Blocking assignment#

  • a = b: blocking

  • a <= b: non-blocking

Generally we use blocking assignments for combinational circuit design and non-blocking assignments for sequential circuits.

When we describe combinational logic procedurally, we typically want to break a long chain into smaller pieces with temporary variables, where every piece may be described using a single assignment. In this case we want a behavior like in C, every statement should be executed immediately.

When we describe sequential logic, we typically want to assign a signal value a to another signal value b if an event happens. Even we can use blocking assignments for basic code, using them can lead to problems like this: https://i.stack.imgur.com/rrgHa.png

If statements#

Case statements#

Note that a multiplexer does not always resemble an if-else or switch statement from general programming languages.

A compiler infers a multiplexer only in the following case:

  1. For every case an output y is assigned to an input signal.

  2. All conditions are mutually exclusive, in other words, two cases cannot be true at the same time.

Unique case#

The intent of unique case is a multiplexer, where an if-else statement actually describes a priority encoder

Case statement is like if/else and can lead to a priority encoder if the compiler misses the conditions for a multiplexer. unique case will try to a multiplexer (and will probably warn if not possible), e.g., even not all cases are complete. For example:

logic [1:0] sel;
always_comb
    unique case (sel)
        'b00: f = a;
        'b01: f = b;
    endcase

For other cases of sel, f can be assigned to any value.

Assertions#

Immediate assertions#

Deferred assertions#

Deferred assertions
assert #0 (expression) action_block 
assert final (expression) action_block 

As with all immediate assertions, a deferred assertion’s expression is evaluated at the time the deferred assertion statement is processed. However, in order to facilitate glitch avoidance, the reporting or action blocks are scheduled at a later point in the current time step.

In the following example we see an assignment from i to o. First browse briefly the code. The discussion follows after the code.

Listing 2 code/assertion_deferred/tb.sv#
module tb;
  logic i, o;
  assign o = i;

  initial begin
    i = 0;

    #1 $display("After #1 delay:");
    i = 1;
    $display("%d: o: %d", $time, o);
    $strobe("%dstrobe: o: %d", $time, o);
    assert (o == 1)
    else $display("Immediate assert failed");
    assert final (o == 1)
    else $display("Final deferred immediate assert failed");
    assert #0 (o == 1)
    else $display("Observed deferred immediate assert failed");

    #0 $display("After zero delay:");
    $display("%d: o: %d", $time, o);
    $strobe("%dstrobe: o: %d", $time, o);
    assert (o == 1)
    else $display("Immediate assert failed");
    assert final (o == 1)
    else $display("Final deferred immediate assert failed");
    assert #0 (o == 1)
    else $display("Observed deferred immediate assert failed");

    #1 $display("After #1 delay:");
    $display("%d: o: %d", $time, o);
    assert (o == 1);

    $finish;
  end
endmodule

Due to the sequential nature of the simulation, the new value of i requires time to be visible at o even we are in the same cycle. assert could be scheduled before o gets updated, so we may assert an intermediate value of o. The solutions are:

  1. Wait #0 or more before checking o.

  2. A deferred assertion as above.

For example the output can be:

After #1
                   1: o: 0
Immediate assert failed

After zero delay
                   1: o: 1
                   1strobe: o: 1
                   1strobe: o: 1

After #1
                   2: o: 1
** Note: $finish    : testbench.sv(32)

$strobe acts as a deferred $display call. We see that all #0, assert #0 and assert final solve the issue.

Warning

Currently, Verilator does not support deferred assertion. So the above output is from Questa on EDAplayground.

As a workaround the following macro can be used which I included in my util.sv:

Listing 3 code/util/util.sv#
`ifdef VERILATOR
// _verilator does not support assert final
// https://github.com/verilator/verilator/issues/5081
// Use #1 delay instead.
`define ASSERT_FINAL(arg) #1 assert (arg)
`else
`define ASSERT_FINAL(arg) assert final (arg)
`endif

This macro can be used as follows:

Listing 4 code/ram/tb.sv#
    din = test_word;
    din_addr = 1;
    wen = 1;
    // Dout should *not* be available in this clock cycle.
    `ASSERT_FINAL(test_word != dout);
    @(negedge clk);

There is a slight difference between the observed and final assertions. They will be scheduled in the reactive and postponed region of the simulation, respectively.

The value that assert #0 sees may still not be the latest value, because:

Observed deferred assertion may glitch

Note that if code in the Reactive region modifies signals and causes another pass to the Active region to occur, this still may create glitching behavior in observed deferred assertions, as the new passage in the Active region may re-execute some of the deferred assertions with different reported results. In general, observed deferred assertions prevent glitches due to order of procedural execution, but do not prevent glitches caused by execution loops between regions that the assignments from the Reactive region may cause.

However assert final cannot glitch because this statement is processed in the last and non-iterative step of a simulation cycle.

Final deferred assertion cannot glitch

… Due to their execution in the non-iterative Postponed region, final deferred assertions are not vulnerable to the potential glitch behavior previously described for observed deferred assertions.

Currently I don’t have an example where assert #0 would be more beneficial than assert final, so I would recommend using the final deferred assertion assert final in cases where an assertion must wait the checked signal to settle.

Solution for the introductory problem#

Listing 5 code/two-are-high-three-are-low/m.sv#
module m #(
    localparam int NInp = 8
) (
    input [NInp-1:0] i,
    output two_are_high,
    three_are_low
);
  logic [NInp/2-1:0] lower_slice, higher_slice;
  assign lower_slice = i[3:0], higher_slice = i[7:4];

  assign two_are_high =
    lower_slice == 4'b1100 |
    lower_slice == 4'b0110 |
    lower_slice == 4'b0011 |
    lower_slice == 4'b1001 |
    lower_slice == 4'b0101 |
    lower_slice == 4'b1010
    ? 1 : 0;

  assign three_are_low =
    higher_slice == 4'b1000 |
    higher_slice == 4'b0100 |
    higher_slice == 4'b0010 |
    higher_slice == 4'b0001
    ? 1 : 0;
endmodule
Listing 6 code/two-are-high-three-are-low/tb.sv#
module tb;
  localparam int NInp = 8;

  logic [NInp-1:0] i;
  logic [NInp/2-1:0] lower_slice, higher_slice;
  logic two_are_high, three_are_low;

  m m_i (.*);

  assign i = {higher_slice, lower_slice};

  initial begin
    $dumpfile("signals.fst");
    $dumpvars();

    repeat (2 ** $size(
        lower_slice
    )) begin
      #1++lower_slice;
    end
    repeat (2 ** $size(
        higher_slice
    )) begin
      #1++higher_slice;
    end
    $finish;
  end
endmodule

Homework#

Exercise 19

Design a calculator that supports addition and multiplication on unsigned integers. Use the following interface:

module calculator_simple #(
    int unsigned WIDTH_IN = 4
) (
    input multiply_instead_of_add,
    input [WIDTH_IN-1 : 0] i1,
    i2,
    output [2*WIDTH_IN-1 : 0] o
);

Implementation on the board:

  1. Use an input width of 4.

  2. Use the left-most 8 LEDs and slide switches for the two 4 bit inputs.

  3. Use the right-most 8 LEDs as the output.

  4. Use the right-most slide switch for multiply_instead_of_add.