Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Lab 2: SystemVerilog RTL Fundamentals

- Made by Hans :)

Overview

Due date: 11:59 PM 09/28/26 via design notebook.

In Lab 1, you set up your development environment, practiced the Git/GitHub workflow, and verified a small SystemVerilog design.

In this lab, you will learn the core SystemVerilog constructs used to write synthesizable RTL:

  • combinational logic with always_comb
  • sequential logic with always_ff
  • blocking (=) and nonblocking (<=) assignments
  • parameters
  • finite state machines (FSMs)
  • typedef and enum

By the end of this lab, you should be able to read a small hardware specification and write synthesizable SystemVerilog RTL for it.

Most testbenches are provided so you can focus on RTL design. Later onboarding labs will cover testbenches and verification in more detail.

If anything is unclear, ask a lead or experienced member.


1. Set Up Lab 2

Update your onboarding repository:

git switch main
git pull

Create a branch and Lab 2 directories:

git switch -c lab_2
mkdir -p lab_2/rtl lab_2/tb

Your Lab 2 directory will contain:

lab_2/
├── rtl/
│   ├── alu.sv
│   ├── counter.sv
│   └── sequence_detector.sv
└── tb/
    ├── alu_tb.sv
    ├── counter_tb.sv
    └── sequence_detector_tb.sv

2. RTL and Synthesis

Register-Transfer Level (RTL) describes digital hardware using registers, combinational logic, and the movement of data between them.

Synthesis is the process of translating synthesizable RTL into digital hardware such as gates, registers, multiplexers, and adders.

Not all SystemVerilog is synthesizable. For example:

#5;
$display(...);
$fatal(...);

are useful for simulation/testbenches but do not represent hardware.

In this lab:

rtl/ → synthesizable hardware
tb/  → simulation/testbench code

3. Exercise 1: Combinational ALU

A combinational circuit has no stored state; its output depends only on its current inputs.

You will implement an Arithmetic Logic Unit (ALU), a combinational circuit that performs arithmetic and logical operations. ALUs are a major component of processors.

Your ALU should support:

opOperation
3'd0a + b
3'd1a - b
3'd2a & b
3'd3a | b
3'd4a ^ b
anything else0

3'd means a 3-bit decimal literal. For example, 3'd4 represents decimal 4 using 3 bits. Similarly, 2'b11 is a 2-bit binary literal.

always_comb

In Lab 1, you used a continuous assignment:

assign y = a & b;

For more complex combinational logic, use always_comb:

always_comb begin
    if (sel)
        y = b;
    else
        y = a;
end

This describes a 2-to-1 multiplexer.

The block executes once at the start of simulation, and re-evaluates whenever a signal used inside it changes. Older Verilog commonly uses always @(*); always_comb is the clearer SystemVerilog form.

Blocking Assignments

Combinational procedural logic normally uses blocking assignments:

=

Example:

result = a + b;

A blocking assignment takes effect immediately inside the block, so later statements see the new value.

case

A case statement selects behavior based on a value:

always_comb begin
    case (op)
        2'd0: y = a + b;
        2'd1: y = a - b;
        default: y = '0;
    endcase
end

default handles values not explicitly listed.

'0 is a SystemVerilog literal that fills the entire signal with zeros regardless of its width.

Combinational outputs should receive a value on every possible path. Otherwise, synthesis may infer unintended storage. Therefore, using default is good practice.

ALU

Create:

lab_2/rtl/alu.sv

with:

module alu (
    input  logic [7:0] a,
    input  logic [7:0] b,
    input  logic [2:0] op,
    output logic [7:0] result
);

    // Your logic here

endmodule

Implement the opcode table using:

always_comb
case with default
blocking assignments

ALU Testbench

Create:

lab_2/tb/alu_tb.sv

with:

module alu_tb;

    logic [7:0] a;
    logic [7:0] b;
    logic [2:0] op;
    logic [7:0] result;

    alu dut (
        .a(a),
        .b(b),
        .op(op),
        .result(result)
    );

    initial begin

        a = 8'd10;
        b = 8'd3;

        op = 3'd0;
        #1;
        if (result !== 8'd13)
            $fatal(1, "ADD failed");

        op = 3'd1;
        #1;
        if (result !== 8'd7)
            $fatal(1, "SUB failed");

        // Add tests for AND, OR, XOR, and an unsupported opcode.

        $display("All ALU tests passed!");
        $finish;

    end

endmodule

Complete the missing tests yourself.

Compile and run:

verilator --binary --timing \
    lab_2/rtl/alu.sv \
    lab_2/tb/alu_tb.sv \
    --top-module alu_tb

./obj_dir/Valu_tb

Do not continue until all tests pass.


4. Exercise 2: Sequential Logic and a Counter

Sequential logic stores state, so its behavior can depend on previous clock cycles.

Examples include registers, counters, program counters, and FSM state registers.

Clocked sequential logic is commonly written using:

always_ff @(posedge clk)

always_ff

For example:

always_ff @(posedge clk) begin
    if (reset)
        q <= 8'b0;
    else
        q <= d;
end

On each rising edge of clk:

  • if reset = 1, q becomes 0
  • otherwise, q stores d

This is a synchronous reset because reset is only checked on a clock edge.

An asynchronous reset could instead use:

always_ff @(posedge clk or posedge reset)

which also reacts immediately when reset changes from 0 to 1.

Blocking vs. Nonblocking Assignments

A blocking assignment (=) takes effect immediately.

Suppose:

a = 5
b = 10

With:

a = b;
b = a;

the first statement changes a to 10, so the second statement also assigns 10 to b.

Result:

a = 10
b = 10

Now use nonblocking assignments:

a <= b;
b <= a;

Both right-hand sides use the old values before the updates take effect:

a = 10
b = 5

The values swap.

This models physical flip-flops updating together on a clock edge.

Use:

always_comb → blocking assignments (=)
always_ff   → nonblocking assignments (<=)

Parameters

Parameters make modules reusable.

Instead of fixing a counter to 8 bits:

logic [7:0] count;

you can make its width configurable:

module counter #(
    parameter WIDTH = 8
) (
    ...
);

and use:

logic [WIDTH-1:0] count;

A parameter can be overridden when the module is instantiated:

counter #(
    .WIDTH(16)
) counter_inst (
    ...
);

This creates a 16-bit counter.

Counter

Create:

lab_2/rtl/counter.sv

with:

module counter #(
    parameter WIDTH = 8
) (
    input  logic             clk,
    input  logic             reset,
    input  logic             enable,
    output logic [WIDTH-1:0] count
);

    // Your logic here

endmodule

Requirements:

  1. count synchronously resets to zero.
  2. count increments on each rising edge when enable = 1.
  3. count holds its value when enable = 0.
  4. Use always_ff with nonblocking assignments.

Think about which condition should have the highest priority.

Counter Testbench

Create:

lab_2/tb/counter_tb.sv

with:

module counter_tb;

    logic clk;
    logic reset;
    logic enable;
    logic [3:0] count;

    counter #(
        .WIDTH(4)
    ) dut (
        .clk(clk),
        .reset(reset),
        .enable(enable),
        .count(count)
    );

    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    initial begin
        reset = 1;
        enable = 0;

        @(posedge clk);
        #1;

        if (count !== 4'd0)
            $fatal(1, "Reset failed");

        reset = 0;
        enable = 1;

        @(posedge clk);
        #1;

        if (count !== 4'd1)
            $fatal(1, "First increment failed");

        @(posedge clk);
        #1;

        if (count !== 4'd2)
            $fatal(1, "Second increment failed");

        enable = 0;

        @(posedge clk);
        #1;

        if (count !== 4'd2)
            $fatal(1, "Counter did not hold");

        enable = 1;

        @(posedge clk);
        #1;

        if (count !== 4'd3)
            $fatal(1, "Counter did not resume");

        $display("All counter tests passed!");
        $finish;
    end

endmodule

No need to write your own test cases this time!

Compile and run:

verilator --binary --timing \
    lab_2/rtl/counter.sv \
    lab_2/tb/counter_tb.sv \
    --top-module counter_tb

./obj_dir/Vcounter_tb

Do not continue until all tests pass.


5. Exercise 3: Finite State Machines

A finite state machine (FSM) is sequential logic whose behavior depends on its current state.

FSMs are commonly used for processor control, UART/SPI controllers, buses, caches, and many other stateful systems.

An FSM generally contains:

state register
next-state logic
output logic

typedef and enum

States could be represented directly as binary values, but SystemVerilog gives us a clearer method:

typedef enum logic [1:0] {
    IDLE,
    WAIT,
    DONE
} state_t;

typedef creates the new type state_t, while enum defines its named values.

You can then declare:

state_t state;
state_t next_state;

Named states make FSM RTL easier to read and debug.

Moore and Mealy FSMs

There are two common FSM styles:

Moore:
output depends only on current state

Mealy:
output depends on current state and input

A Mealy FSM can respond directly to a current input. A Moore FSM instead changes state and produces its output based on that state.

For this exercise, you will implement a Moore FSM.

Sequence Detector

Your FSM will detect:

1011

Input x provides one bit per clock cycle.

Use these states:

S0 → nothing useful matched
S1 → matched "1"
S2 → matched "10"
S3 → matched "101"
S4 → matched "1011"

S4 is the detection state:

S4 → detect = 1
all other states → detect = 0

For the input:

1 0 1 1

the FSM progresses:

S0 → S1 → S2 → S3 → S4

The final 1 is sampled on the edge that moves the FSM into S4, so detect is high immediately after that edge:

Input sampled:   1    0    1    1
                 ↑    ↑    ↑    ↑
Detect:          0    0    0    1

Overlapping Sequences

Your detector must support overlapping matches.

For example:

1011011

contains two occurrences of 1011.

The final 1 of the first match can also be the first 1 of another match. Therefore, after reaching S4, you should not automatically discard all previously useful information.

FSM Structure

A common Moore FSM separates the state register from the combinational next-state/output logic:

always_ff @(posedge clk) begin
    if (reset)
        state <= IDLE;
    else
        state <= next_state;
end

and:

always_comb begin
    next_state = state;
    output_signal = 1'b0;

    case (state)

        IDLE: begin
            // Determine next state
        end

        ...

        DONE: begin
            output_signal = 1'b1;
            // Determine next state
        end

        default: begin
            next_state = IDLE;
        end

    endcase
end

The initial assignments:

next_state = state;
output_signal = 1'b0;

provide defaults.

Remember that if a combinational signal is not assigned on every possible path, synthesis may infer a latch, an unintended storage element.

Your Task

Create:

lab_2/rtl/sequence_detector.sv

with:

module sequence_detector (
    input  logic clk,
    input  logic reset,
    input  logic x,
    output logic detect
);

    // Your FSM here

endmodule

Requirements:

  1. Detect 1011.
  2. Accept one input bit per clock cycle.
  3. Implement a Moore FSM with a dedicated detection state.
  4. Assert detect when the final matching bit is sampled.
  5. Keep detect high for only one cycle per detection.
  6. Support overlapping detections.
  7. Use typedef enum logic.
  8. Use always_ff for the state register.
  9. Use always_comb for next-state/output logic.

You need five states, so determine how many bits are required to represent them.

Sequence Detector Testbench

Create:

lab_2/tb/sequence_detector_tb.sv

with:

module sequence_detector_tb;

    logic clk;
    logic reset;
    logic x;
    logic detect;

    sequence_detector dut (
        .clk(clk),
        .reset(reset),
        .x(x),
        .detect(detect)
    );

    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    task send_bit(
        input logic bit_value,
        input logic expected_detect
    );
        begin
            x = bit_value;

            @(posedge clk);
            #1;

            if (detect !== expected_detect)
                $fatal(1, 
                    "Detection failed: x=%0b expected=%0b got=%0b",
                    bit_value,
                    expected_detect,
                    detect
                );
        end
    endtask

    initial begin
        reset = 1;
        x = 0;

        @(posedge clk);
        #1;

        reset = 0;

        // Detect 1011
        send_bit(1, 0);
        send_bit(0, 0);
        send_bit(1, 0);
        send_bit(1, 1);

        // No detection
        send_bit(0, 0);
        send_bit(0, 0);

        // Test overlapping detections in 1011011
        reset = 1;
        @(posedge clk);
        #1;
        reset = 0;

        send_bit(1, 0);
        send_bit(0, 0);
        send_bit(1, 0);
        send_bit(1, 1);
        send_bit(0, 0);
        send_bit(1, 0);
        send_bit(1, 1);

        $display("All sequence detector tests passed!");
        $finish;
    end

endmodule

This testbench uses a task to package repeated testbench behavior into a reusable block. Later labs will cover testbench design in more detail.

Compile and run:

verilator --binary --timing \
    lab_2/rtl/sequence_detector.sv \
    lab_2/tb/sequence_detector_tb.sv \
    --top-module sequence_detector_tb

./obj_dir/Vsequence_detector_tb

Do not continue until all tests pass.


6. Merge Lab 2

git status
git add lab_2
git commit -m "Complete Lab 2"
git push -u origin lab_2

Open a pull request:

lab_2 → main

Review the changes, then Squash and merge.

After merging:

git switch main
git pull
git branch -d lab_2

7. Design Notebook

Before starting your weekly design notebook work, update the documentation repository’s main.

If using the fork workflow from Lab 1:

git switch main
git pull upstream main
git push origin main

Then create your new design notebook branch.

In your entry, briefly discuss:

  • what you completed and learned
  • any problems or bugs you encountered
  • how you resolved them

Include a link to your onboarding repository.

Use the same PR title format:

docs(dn): First Last mm/dd/yy

Completion Checklist

  • ALU implemented and verified
  • Parameterized counter implemented and verified
  • Moore 1011 sequence detector implemented and verified
  • Lab 2 merged into your repo
  • Weekly design notebook entry submitted

After completing this lab, you should be comfortable writing basic combinational, sequential, and FSM-based RTL in SystemVerilog.