josuah.net | panoramix-labs.fr

cv | links | quotes | ascii | tgtimes | gopher | mail

Interface in Open-Source SystemVerilog Synthesis

Verilog lacks a way to build complex data types, akin to C's struct.

SystemVerilog introduces a struct feature, but it cannot declare inputs and outputs.

SystemVerilog introduces an interface feature for use in module ports, along with modport.

It simplify connections between modules, that do not need to declare the signals one by one anymore.

For instance for bundling together the many signals of

Declaring an interface

The syntax follows the same as the module keyword:

interface bus_if;
  logic req;
  logic ack;
  logic[7:0] data;
endinterface

There is an implicit typedef in interface declaration: bus_if then becomes a type that you can use instead of logic, wire, reg, etc.

Using the interface

The type can be used anywhere You can also access the interface individual signals with the dot notation as below.

module lighter (
  input logic clk,
  bus_if bus,  // the interface is passed like any signal
  output logic[2:0] rgb_leds
);
  assign bus.ack = bus.req;

  always_ff @(posedge clk) begin
    if (bus.req) begin
      rgb_leds <= bus.data[2:0];
    end
  end
endmodule

Instanciating a verilog module goes by calling its name and filling all signals.

Interfaces are passed in the list of signals as if it was a regular signal.

For that, the interface must be instantiated separately:

module top (
  input logic clk
);
  bus_if bus ();
  // at this point, "bus" is an interface with empty signals,
  // each signal has to be integrated in the parent module as
  // needed

  mBusController controller (
    .clk(clk),
    .bus(bus)  // writes to "req" and "data", reads from "ack"
  );

  lighter peripheral1 (
    .clk(clk),
    .bus(bus)  // reads from "req" and "data", writes to "ack"
  );
endmodule

Note that input or output directions are not described, which is the role of modport.

Declaring modports

Modport specify the input or output direction of interfaces signals.

Taking the analogy of a physical connector, modport permits to differentiate a male and female plug, give a polarity to the signals.

interface bus_if;
  // the list of signals are preserved even with modport
  logic req;
  logic ack;
  logic[7:0] data;

  modport mpController (
    output req,
    output data,
    input ack
  );

  modport mpPeripheral (
    input req,
    input data,
    output ack
  );
endinterface

Then we have the choice to pass the entire interface to modules, or to pass only a modport that restricts the direction for extra safety:

module lighter (
  input logic clk,
  bus_if.mpPeripheral bus,  // the modport is passed instead
  output logic[2:0] rgb_leds
);
  assign bus.ack = bus.req;  // no syntax change here
  [...]
endmodule

module top (
  input logic clk
);
  bus_if bus ();  // instantiate the interface with all its modports

  always @(posedge clk) begin
    if (bus.req) begin // accessing the interface signals directly
      bus.mpController.req <= 1;  // or through the modport
    end
  end

  lighter peripheral1 (
    .clk(clk),
    .bus(bus.mpPeripheral)  // passing the modport for safety
  );
endmodule

Integrating external signals

Interfaces can declare signals that always have the same direction. This is a convenience for clock and resets signals:

interface bus_if (
  input clk,  // declared as input here, they will be input
  input rst   // signals everywhere in the modport
);
  logic req;
  logic ack;
  logic[7:0] data;

  modport mpController (
    input clk,  // they also need to be declared here for them
    input rst,  // to be reachable through the modport
    output req,
    output data,
    input ack
  );

  modport mpPeripheral (
    input clk,  // to be declared in every modport they need to be
    input rst,  // used
    input req,
    input data,
    output ack
  );
endinterface

Integrating parameters

Like for modules, interfaces can have parameters, such as the size of some signals:

interface bus_if #(
  parameter pDataSize = 8  // this parameter is optional
) (
  input clk  // external signals list are next, like for modules
);
  logic[pDataSize-:0] data;  // note the use
  ...
endinterface

module top ( ... );
  bus_if #( pDataSize = 8 ) bus;  // same syntax as modules
  ...
endmodule

Complete example

bus_if.sv:

interface bus_if (
  input logic clk
);
  logic req;

  modport mpController (
    input clk,
    output req
  );

  modport mpPeripheral (
    input clk,
    input req
  );
endinterface

lighter.sv:

module lighter (
  bus_if.mpPeripheral bus,
  output logic led
);
  always_ff @(posedge bus.clk) begin
    if (bus.req) begin
      led <= 1;
    end
  end
endmodule

top.sv:

module top (
  input logic clk,
  output logic led // led
);

  // instantiate the bus interface
  bus_if bus ( .clk(clk) );

  // default value for simulation only
  initial begin
    bus.req = 0;
  end

  // issue a request over the bus
  always_ff @(posedge clk) begin
    // set a toggle train for demo
    bus.req <= !bus.req;
  end

  // instantiate the module
  lighter peripheral0 (
    .bus(bus.mpPeripheral),
    .led(led)
  );
endmodule

mSynthesis.sv:

module mSynthesis (
  output logic gpio_1
);
  // Lattice way to setup the clock
  SB_HFOSC hfosc ( .CLKHFPU(1'b1), .CLKHFEN(1'b1), .CLKHF(clk) );

  // instantiate the top module and bind the GPIO pins to it
  top top (
    .clk(clk),
    .led(gpio_1)
  );
endmodule

simulation.cpp

#include "verilated.h"
#include "verilated_vcd_c.h"
#include "Vtop.h"

int
main(int argc, char **argv)
{
	Verilated::commandArgs(argc, argv);
	Verilated::traceEverOn(true);

	Vtop *vsim = new Vtop;
	VerilatedVcdC *vcd = new VerilatedVcdC;

        vsim->trace(vcd, 99);
        vcd->open("simulation.vcd");

        vsim->eval();
        vcd->dump(0);

        for (unsigned long long ns = 0; ns < 1000;) {
		vsim->clk = 1;
		vsim->eval();
		vcd->dump(ns += 100);

		vsim->clk = 0;
		vsim->eval();
		vcd->dump(ns += 100);
        }

	vcd->flush();
}

When calling Verilator:

$ verilator --version
Verilator 4.224 2022-06-19 rev v4.224

$ verilator -Wall --trace --sv -cc --top-module top top.sv lighter.sv bus_if.sv

$ make -C obj_dir -f Vtop.mk
gmake: Entering directory '/home/josuah/example/obj_dir'
/usr/bin/perl /usr/local/share/verilator/bin/verilator_includer -DVL_INCLUDE_OPT=include Vtop.cpp Vtop.cpp Vtop.cpp Vtop.cpp Vtop.cpp Vtop.cpp Vtop.cpp Vtop.cpp Vtop.cpp Vtop.cpp Vtop.cpp > Vtop.cpp
c++  -I.  -MMD -I/usr/local/share/verilator/include -I/usr/local/share/verilator/include/vltstd -DVM_COVERAGE=0 -DVM_SC=0 -DVM_TRACE=1 -DVM_TRACE_FST=0 -DVM_TRACE_VCD=1 -faligned-new -fbracket-depth=4096 -fcf-protection=none -Qunused-arguments -Wno-bool-operation -Wno-tautological-bitwise-compare -Wno-parentheses-equality -Wno-sign-compare -Wno-uninitialized -Wno-unused-but-set-variable -Wno-unused-parameter -Wno-unused-variable -Wno-shadow      -std=gnu++14 -Os -c -o Vtop.o Vtop.cpp
echo "" > Vtop.verilator_deplist.tmp
Archive ar -rcs Vtop.a Vtop.o
rm Vtop.verilator_deplist.tmp
gmake: Leaving directory '/home/josuah/example/obj_dir'

$ VERILATOR_INC=/usr/local/share/verilator/include

$ c++ -I${VERILATOR_INC} -Iobj_dir   -o simulation.elf simulation.cpp ${VERILATOR_INC}/verilated.cpp ${VERILATOR_INC}/verilated_vcd_c.cpp obj_dir/Vtop.a

$ ./simulation.elf

$ gtkwave simulation.vcd

When calling yosys

$ yosys --version
Yosys 0.9+4081 (git sha1 UNKNOWN, c++ 13.0.0 -O2 -fPIC -Os)

$ yosys -p "read_verilog -sv mSynthesis.sv top.sv lighter.sv bus_if.sv; synth_ice40 -top mSynthesis -json synthesis.json"
[a lot of output from yosys]

Still (2022-07-06) not support from Icarus Verilog:

$ iverilog -g2012 bus_if.sv lighter.sv mSynthesis.sv top.sv
lighter.sv:2: syntax error
lighter.sv:2: Errors in port declarations.

Debugging interface syntax

Using SystemVerilog constructs might require to add a command-line flag like --sv.

Yosys does not like default_nettype none along with interfaces, you will have to disable it if you want to use interfaces, until the feature is implemented.

Discussion

SystemVerilog interfaces are fairly well supported by recent versions of Verilator (increasingly through 3.x and 4.x) and Yosys (I tested with 0.9+4081).

Slightly less with Icarus Verilog (iverilog).

Some features might not be available on all versions, and observing the mailing list and bugtrackers of both project would help checking if a bug is from misuse of interfaces, or lack of support by the upstream tool.

In every situation discussing about interfaces, the use-case was bundling these numerous master/slave bus signals present at the top of every module.

Without struct or interface, these signals have to be written one by one on every module declaration or instantiation, and this takes-up a lot of visual space distracting from the real content.

Interfaces was being discouraged by LowRISC coding style for a bug encountered in 2018 on one power-domain checking tool.

This may have gotten a lot better since. -- @tjaychen in June 2021

This at least works fairly well for the synthesis and simulation path today!

https://www.chipverify.com/systemverilog/systemverilog-struct

https://www.chipverify.com/systemverilog/systemverilog-interface

https://www.chipverify.com/systemverilog/systemverilog-modport

https://www.bilibili.com/video/BV1uS4y1z7JN (video)

https://github.com/lowRISC/style-guides/blob/master/VerilogCodingStyle.md