josuah.net | panoramix-labs.fr

cv | links | quotes | mail

SystemVerilog structs as ersatz to interafces

It happen that interfaces are not well supported by yosys.

The syntax is recognised, but the wiring does not happen, and interfaces passed to modules let many signals disconnected..

The real solution is to look at yosys source and get a hang of what is happening, to patch it.

The alternative to bundle signals together are structs.

They have the inconvenient to not permit input and output signals, nor to allow parameters.

So in practices, multiple structs are to be populated, while a single interface would have been sufficient.

Below is a comparison.

Interfaces

Interfaces are more verbose for declaration, but less for each instance (more efforts permitting to deduplicate more)

Declaration:

interface wishbone (
  input logic clk,
  input logic rst
);
  logic stb;
  logic we;
  logic ack;
  logic[3:0] adr;
  logic[7:0] dat_c;
  logic[7:0] dat_p;

  modport peri (
    input stb,
    input we, 
    output ack,
    input adr,
    output dat_p,
    input dat_c
  );

  modport ctrl (
    output stb,
    output we, 
    input ack,
    output adr,
    input dat_p,
    output dat_c
  );
endinterface

Instantiation:

  ...
  wishbone wb ();

  peri p3 (
    .wb(wb.peri)
    .gpio_led
  );
  ...

  module peri (
    wishbone_ctrl_t wb,
    output logic gpio_led
  );

Structs

Structs are less verbose to declare, but more verbose to instanciate: lower cost, lower gain

Declaration:

typedef struct packed {
  logic ack;
  logic[7:0] dat;
} wishbone_peri_t;

typedef struct packed {
  logic stb;
  logic we;
  logic[3:0] adr;
  logic[7:0] dat;
} wishbone_ctrl_t;

Instantiation:

  ...
  wishbone_peri_t wb_p;  // this is not two interfaces, one for peripheral,
  wishbone_ctrl_t wb_c;  // and one for controller, but a same interface with
        // signals coming from the peripheral or controller
  peri p3 (
    .clk, .rst, .wb_p .wb_c
    .gpio_led
  );
  ...

  module peri (
    input logic clk,
    input logic rst,
    input wishbone_ctrl_t wb_c,
    output wishbone_peri_t wb_p,
    output logic gpio_led
  );
  ...
endmodule

Naming signals

How to keep it together with the directions (aka, RX/TX problem in UART, or COPI/CIPO in SPI):

Signals coming from the peripheral:

  wishbone_peri_t wb_p;

Signals coming from the controller:

  wishbone_ctrl_t wb_c;

From here, the naming becomes rather intuitive and natural:

  // the wishbone peripheral's acknoledgement is assigned from
  // the controller's strobe signal
  assign wb_p.ack = wb_c.stb; 

  // if the wishbone controller's strobe signal goes high
  if (wb_c.stb) begin
    // set the duty cycle to the wishbone controller's data
    duty_cycle <= wb_c.dat;
  end

For choosing for where to find ack between wb_p (peripheral) and wb_c (controller), the question to ask is: "who acknoledges"?

The peripheral acknoledges, so the ack signal belongs to the wb_p (peripheral) struct.

It is also not needed to add a suffix to the data pins, such as MISO/MOSI or CIPO/COPI for SPI, or DAT_I/DAT_O for Wishbone, as the struct already contains the direction of the signal:

It becomes: spi_p.dat, wb_p.dat, spi_c.dat, wb_c.dat.

Using the source of the signal also means it is never needed to write .dat_i(dat_o): the data always go in from somewhere toward somewhere. By naming the destination, it tells more about where is that signal coming from and what content it has, and the signals names are the same through the whole design.

For UART, though, TX and RX make sense, as the connexion is purely symmetrical, and there is no controller or peripheral device, or master and slave device.

In that case, it is possible to name them according to the specific use-case. For instance data_rs485_chip for the data coming from an external RS-485 transceiver, and data_sensor for data coming from the sensor peripheral