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)
typedefandenum
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:
op | Operation |
|---|---|
3'd0 | a + b |
3'd1 | a - b |
3'd2 | a & b |
3'd3 | a | b |
3'd4 | a ^ b |
| anything else | 0 |
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,qbecomes0 - otherwise,
qstoresd
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:
countsynchronously resets to zero.countincrements on each rising edge whenenable = 1.countholds its value whenenable = 0.- Use
always_ffwith 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:
- Detect
1011. - Accept one input bit per clock cycle.
- Implement a Moore FSM with a dedicated detection state.
- Assert
detectwhen the final matching bit is sampled. - Keep
detecthigh for only one cycle per detection. - Support overlapping detections.
- Use
typedef enum logic. - Use
always_fffor the state register. - Use
always_combfor 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
1011sequence 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.