Translating a SystemVerilog FSM to Cycle-Accurate SystemC
SystemVerilog Reference Implementation
To illustrate how SystemC can be utilized to model Register Transfer Level (RTL) hardware, we will examine a standard Finite State Machine (FSM) implementation. Below is a SystemVerilog module defining a three-state controller.
module seq_controller (
input logic clk,
input logic rst_n,
input logic [1:0] data_in,
output logic [1:0] status_out
);
typedef enum logic [1:0] {
S_IDLE = 2'b00,
S_BUSY = 2'b01,
S_DONE = 2'b10
} state_t;
state_t current_s, next_s;
// State Register Update
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
current_s <= S_IDLE;
else
current_s <= next_s;
end
// Next State Logic
always_comb begin
next_s = current_s;
case (current_s)
S_IDLE: if (data_in == 2'b01) next_s = S_BUSY;
S_BUSY: if (data_in == 2'b10) next_s = S_DONE;
S_DONE: if (data_in == 2'b11) next_s = S_IDLE;
default: next_s = S_IDLE;
endcase
end
// Output Logic
always_comb begin
status_out = 2'b00;
case (current_s)
S_IDLE: status_out = 2'b01;
S_BUSY: status_out = 2'b10;
S_DONE: status_out = 2'b11;
default: status_out = 2'b00;
endcase
end
endmodule
SystemC Implementation
The following code demonstrates the equivalent functionality in SystemC. This model uses processes registered with SC_METHOD to mimic the behavior of the SystemVerilog always blocks.
Header File (seq_ctrl.h)
#ifndef SEQ_CTRL_H_
#define SEQ_CTRL_H_
#include <systemc.h>
SC_MODULE(SeqController) {
sc_in<bool> clk;
sc_in<bool> rst_n;
sc_in<sc_uint<2>> data_in;
sc_out<sc_uint<2>> status_out;
sc_signal<sc_uint<2>> cs; // Current State
sc_signal<sc_uint<2>> ns; // Next State
void update_state();
void transition_logic();
void output_logic();
void monitor_trace();
SC_CTOR(SeqController) {
SC_METHOD(update_state);
sensitive << clk.pos() << rst_n.neg();
SC_METHOD(transition_logic);
sensitive << data_in << cs;
SC_METHOD(output_logic);
sensitive << cs;
SC_METHOD(monitor_trace);
sensitive << clk.pos();
}
enum StateType {
IDLE = 0,
BUSY = 1,
DONE = 2
};
};
#endif
Implementation File (seq_ctrl.cpp)
#include "seq_ctrl.h"
#include <iostream>
void SeqController::update_state() {
if (rst_n.read() == false) {
cs.write(IDLE);
} else {
cs.write(ns.read());
}
}
void SeqController::transition_logic() {
switch (cs.read()) {
case IDLE:
if (data_in.read() == 1) ns.write(BUSY);
else ns.write(cs.read());
break;
case BUSY:
if (data_in.read() == 2) ns.write(DONE);
else ns.write(cs.read());
break;
case DONE:
if (data_in.read() == 3) ns.write(IDLE);
else ns.write(cs.read());
break;
default:
ns.write(IDLE);
break;
}
}
void SeqController::output_logic() {
switch (cs.read()) {
case IDLE: status_out.write(1); break;
case BUSY: status_out.write(2); break;
case DONE: status_out.write(3); break;
default: status_out.write(0); break;
}
}
void SeqController::monitor_trace() {
std::cout << sc_time_stamp() << " | In: " << data_in.read()
<< " | State: " << cs.read()
<< " | Next: " << ns.read()
<< " | Out: " << status_out.read() << std::endl;
}
Testbench and Simulation
The testbanch below instantiates the module, applies the stimulus, and generates a VCD trace file for waveform analysis.
#include "seq_ctrl.h"
int sc_main(int argc, char* argv[]) {
sc_clock sys_clk("sys_clk", 2, SC_NS);
sc_signal<bool> rst;
sc_signal<sc_uint<2>> input_sig;
sc_signal<sc_uint<2>> output_sig;
// Instantiate Module
SeqController dut("dut");
dut.clk(sys_clk);
dut.rst_n(rst);
dut.data_in(input_sig);
dut.status_out(output_sig);
// Waveform Tracing
sc_trace_file* wf = sc_create_vcd_trace_file("waveform_dump");
sc_trace(wf, sys_clk, "clock");
sc_trace(wf, rst, "reset");
sc_trace(wf, input_sig, "input");
sc_trace(wf, output_sig, "output");
sc_trace(wf, dut.cs, "current_state");
sc_trace(wf, dut.ns, "next_state");
// Stimulus Process
rst.write(0);
input_sig.write(0);
sc_start(1, SC_NS);
rst.write(1);
sc_start(3, SC_NS);
input_sig.write(1); // Trigger IDLE -> BUSY
sc_start(2, SC_NS);
input_sig.write(2); // Trigger BUSY -> DONE
sc_start(2, SC_NS);
input_sig.write(3); // Trigger DONE -> IDLE
sc_start(5, SC_NS);
sc_close_vcd_trace_file(wf);
return 0;
}
Simulation Output
0 s | In: 0 | State: 0 | Next: 0 | Out: 1
2 ns | In: 0 | State: 0 | Next: 0 | Out: 1
4 ns | In: 1 | State: 0 | Next: 1 | Out: 1
6 ns | In: 2 | State: 1 | Next: 2 | Out: 2
8 ns | In: 3 | State: 2 | Next: 0 | Out: 3
10 ns | In: 3 | State: 0 | Next: 0 | Out: 1
Analysis and Design Considerations
By comparing the two implementations, we can establish a direct mapping between SystemVerilog and SystemC constructs:
- Ports:
input/outputin SystemVerilog correspond tosc_in/sc_outin SystemC. - Signals: Internal wires and regs map to
sc_signal. - Processes:
always_ffandalways_combblocks are implemented usingSC_METHODprocesses, which execute whenever their sensitivity list changes. - Instantiation: Module instantiation corresponds to the construction of a C++ object and binding its ports to signals.
While SystemC is fully capable of supporting cycle-accurate, clock-driven models as shown above, its primary advantage lies in Transaction-Level Modeling (TLM). In a standard top-down design flow, engineers often start with loosely timed or untimed models to rapidlly verify architectural functionality and communication protocols without the overhead of clock-cycle details. This allows the SystemC model to serve as an executable specification for software developers and as a reference for RTL implementation teams. The strict cycle-accurate modeling shown here is typically reserved for later stages when detailed timing verification is required.