Interfaces and modports from the design side

Declare a bus once as an interface, give each side a modport, connect each module in one line, and see what the tools check and what it costs in gates.

Lesson
R7.1
Time
25 min
Runs on
Verilator
Check
5 questions

A register block was wired to its requester by hand, one line per bus signal. At the completer, the address and the write data went to each other's pins. Both were 8 bits, so it compiled, Lint said nothing and Synthesize reported the same 100 cells. Only the bench noticed: every write came back with an error.

Wired by hand: two pins swapped

apb_regs u_regs (
  .pclk(pclk), .presetn(presetn),
  .psel(psel), .penable(penable), .pwrite(pwrite),
  .paddr(pwdata), .pwdata(paddr),      // swapped
  .pready(pready), .prdata(prdata), .pslverr(pslverr),
  .leds(leds));

Verilator compiles it and Lint is silent. Yosys 0.70 builds 100 cells, the count of the correct wiring. The bench gets err=1 on both writes, and every read returns 0x00, even ID.

One interface, one line

apb_if   bus    (.pclk(pclk), .presetn(presetn));
apb_req  u_req  (.bus(bus), .start(start), /* ... */);
apb_regs u_regs (.bus(bus), .leds(leds));

Inside the module the completer reads bus.paddr and bus.pwdata by name. No pin list is left to get in the wrong order.

One bundle, two views

An interface declares the signals of a connection once, as one named thing. A modport names one side of it: which signals that side drives (output) and which it reads (input). A module takes a modport as a port, apb_if.cmp bus, and then uses bus.paddr like any signal.

The lab's bus: eight signals plus clock and reset, one helper function and two views.
// apb_if: the wires between one requester and one completer, declared once.
// Each modport is one side's view: what it drives (output) and what it reads (input).
interface apb_if (input logic pclk, input logic presetn);
  logic       psel, penable, pwrite;
  logic [7:0] paddr, pwdata;
  logic       pready, pslverr;
  logic [7:0] prdata;

  // The access phase of a transfer, written once for every completer on this bus.
  function automatic logic access();
    return psel && penable;
  endfunction

  modport req (input  pclk, presetn, pready, prdata, pslverr,
               output psel, penable, pwrite, paddr, pwdata);
  modport cmp (input  pclk, presetn, psel, penable, pwrite, paddr, pwdata,
               output pready, prdata, pslverr,
               import access);
endinterface
SignalWidthreq (apb_req)cmp (apb_regs)
pclk, presetn1inputinput
psel, penable, pwrite1outputinput
paddr, pwdata8outputinput
pready, pslverr1inputoutput
prdata8inputoutput
access()functionnot visibleimported

Every signal is driven by exactly one side. The completer may call access() because its modport imports it.

One interface instance, two modportsArrows show which module drives which signals. The interface instance bus lives in the top module, apb_sys, which connects both sides.
One interface instance, two modportsArrows show which module drives which signals. The interface instance `bus` lives in the top module, `apb_sys`, which connects both sides. bench Bench start, wr, addr, wdata component apb_req takes apb_if.req interface bus (apb_if) one instance inapb_sys component apb_regs takes apb_if.cmp component leds[7:0] a port of apb_sys one commandpsel, penable, pwrite,paddr, pwdatapready, prdata,pslverrregister value
  1. The requester drives its side

    Through req, apb_req may drive the five request signals and only read the rest.

    Follow the calls, values and waits
    1. call write 0x5a to 0x00: Bench → apb_req
    2. data write 0x5a to 0x00: apb_req → bus (apb_if)
  2. The completer answers

    Through cmp, apb_regs drives the three response signals and the LEDs. Writing bus.pwrite from here is a compile error.

    Follow the calls, values and waits
    1. return pready=1 pslverr=0: apb_regs → bus (apb_if)
Read the diagram as text
  • Bench: start, wr, addr, wdata
  • apb_req: takes apb_if.req
  • bus (apb_if): one instance in apb_sys
  • apb_regs: takes apb_if.cmp
  • leds[7:0]: a port of apb_sys
  • Bench → apb_req: one command
  • apb_req → bus (apb_if): psel, penable, pwrite, paddr, pwdata
  • apb_regs → bus (apb_if): pready, prdata, pslverr
  • apb_regs → leds[7:0]: register value

The bench reaches the bus only through the requester's command ports.

What the tools check for you

A modport is checked when the design compiles. In the lab, add assign bus.pwrite = 1'b0; inside apb_regs. pwrite is an input of cmp, so every tool stops.

MistakeRun (Verilator 5.052)Synthesize (Yosys 0.70)
completer drives bus.pwrite, an input of its modport%Error: main.sv:68:14: Attempt to drive input-only modport: 'pwrite'main.sv:68: ERROR: cannot assign to input port 'pwrite'
discrete ports, paddr and pwdata swapped at the completerruns; both writes end with err=1100 cells, the same as the correct wiring
discrete ports, prdata and pwdata swappedstops with %Error-BLKANDNBLK on pwdata, which now has two driversbuilds 42 cells, with multiple conflicting drivers warnings

Lint gives the same error as Run for the first row and says nothing about the second. For the third it warns UNDRIVEN on prdata.

The first row is the modport doing its job. The third puts two outputs on one logic signal, and the tools catch that too. The second is the hook's bug: two inputs of the same width trade places, and only behaviour shows it. With an interface the completer names bus.paddr itself, so that swap cannot be written.

What an interface costs in gates

Synthesize the lab: 100 cells, 48 flops, 0 latches, 9 levels of logic on this flow. Rewrite the same two modules with discrete ports, ten named bus connections each. Yosys 0.70 reports 100 cells again, with the same count of every cell type. The flow flattens the hierarchy, and an interface becomes plain wires.

Synthesize and Lint read the design file alone, main.sv. So the interface sits in that file with the modules that use it. The bench in tb.sv goes only to Run.

Rule for design interfacesWhy
Name the modport in the module's port: apb_if.cmp busthe module states its own directions, and a reader sees them in its header
Put the interface instance in the module that connects both sideshere that is apb_sys; each child takes a port
Put shared helper logic in functionsaccess() is written once; the lab still builds 100 cells
Give widths as parameters when they varythe lab with parameters AW and DW (both 8) builds 100 cells too
Keep clocking blocks and tasks for bench interfacesa clocking block in apb_if stops Yosys 0.70 with Internal frontend error

Each row was measured on the lab design with one change at a time.

The lab: a requester and a completer on one bus

The brief of apb_sys
PortDirectionWidthMeaning
pclkinput1clock
presetninput1reset, asynchronous, active-low
startinput1one-cycle pulse: begin a transfer
wrinput11: write, 0: read
addrinput[7:0]LEDS 0x00 and SCRATCH 0x04 read and write; ID 0x08 reads 0x4c
wdatainput[7:0]write data
doneoutput1pulses at the completion edge
rdataoutput[7:0]read data, held until the next transfer
erroutput1any other address, or a write to ID, completes with pslverr 1
ledsoutput[7:0]the LEDS register
Clock
rising edge of pclk
Reset
presetn asynchronous, active-low
Latency
SETUP for one cycle, ACCESS until pready; done pulses at the completion edge
Parameters
Bus: apb_if with modports req and cmp; one instance, in apb_sys
  1. Run and Synthesize as shipped

    Six lines, one per transfer; the write to ID ends with err=1. Synthesize reports 100 cells, 48 flops, 9 levels.

  2. Break a direction

    Add assign bus.pwrite = 1'b0; to apb_regs. Run, Lint and Synthesize all stop on that line.

  3. Remove the modport

    Change the completer's port to apb_if bus and run. The bench still passes, but the module no longer says which signals it drives.

Asked in interviews
  1. What is a modport?

    A named view of an interface that gives one side its port directions. A module that takes bus_if.cmp may drive only that view's outputs; driving one of its inputs is a compile error.

  2. Do interfaces synthesize?

    Yes. The synthesizer flattens an interface port into one ordinary port per modport signal. Measured here, the bus with an interface and the same bus with discrete ports both build 100 cells.

  3. Why use an interface in RTL and not only in the testbench?

    Fewer connection lines, so fewer swaps; directions checked at compile time; helper functions written once. Keep clocking blocks and tasks out of design interfaces.

Try it

Each transfer takes three edges: the start pulse, SETUP and ACCESS. The write to ID completes with an error, and LEDS keeps 0x5a.

The interface, both modules and the top are all in the design file, so Synthesize sees the whole bus: 100 cells, 48 flops, 0 latches, 9 levels of logic.

RTLVerilator · simulator service · Lint: Verilator service · Synthesize: Yosys service
Edit the design, then Run, Lint or Synthesize
Expected: e4 write 0x00 <- 0x5a err=0 leds=0x5a e7 write 0x04 <- 0x3c err=0 leds=0x5a e10 read 0x04 -> 0x3c err=0 e13 read 0x08 -> 0x4c err=0 e16 write 0x08 <- 0xff err=1 leds=0x5a e19 read 0x00 -> 0x5a err=0

Original design · 100 cells · 48 flip-flops · 0 latches · 9 logic levels. Synthesis uses Yosys; counts depend on the toolchain.

Read included file: tb.sv
`timescale 1ns/1ps
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                   // rising edges at 5, 15, 25, ...
  logic rst_n = 1;                        // falls at 1 ns: a real edge, so an asynchronous reset acts before any clock
  int edge_n = -1;
  always @(posedge clk) edge_n = edge_n + 1;
  logic       start = 0, wr = 0;
  logic [7:0] addr = 0, wdata = 0;
  logic       done, err;
  logic [7:0] rdata, leds;
  // One transfer: a start pulse, then wait for done and print one line.
  task automatic xfer(input bit w, input logic [7:0] a, input logic [7:0] d = 8'h00);
    start = 1; wr = w; addr = a; wdata = d;
    @(posedge clk); #1 start = 0;
    while (!done) begin @(posedge clk); #1; end
    if (w) $display("e%0d write 0x%h <- 0x%h err=%0d leds=0x%h", edge_n, a, d, err, leds);
    else   $display("e%0d read  0x%h -> 0x%h err=%0d", edge_n, a, rdata, err);
  endtask
  apb_sys dut (.pclk(clk), .presetn(rst_n), .*);
  // Advance to the next rising edge, wait for the design to settle, then print one line.
  task automatic tick();
    @(posedge clk); #1;
    
  endtask
  initial begin
    #1 rst_n = 0; tick(); tick(); rst_n = 1;   // asserted at 1 ns, edges 0 and 1 in reset, released 1 ns after edge 1
    xfer(1, 8'h00, 8'h5a);       // LEDS
    xfer(1, 8'h04, 8'h3c);       // SCRATCH
    xfer(0, 8'h04);
    xfer(0, 8'h08);              // ID
    xfer(1, 8'h08, 8'hff);       // ID is read-only: error
    xfer(0, 8'h00);
    $finish(0);                             // quiet: no "$finish called" line
  end
endmodule

Check yourself

Predict the outputThe program writes 0xff to the ID register at 0x08, then reads it back. What does it print?
// apb_if: the wires between one requester and one completer, declared once.
// Each modport is one side's view: what it drives (output) and what it reads (input).
interface apb_if (input logic pclk, input logic presetn);
  logic       psel, penable, pwrite;
  logic [7:0] paddr, pwdata;
  logic       pready, pslverr;
  logic [7:0] prdata;

  // The access phase of a transfer, written once for every completer on this bus.
  function automatic logic access();
    return psel && penable;
  endfunction

  modport req (input  pclk, presetn, pready, prdata, pslverr,
               output psel, penable, pwrite, paddr, pwdata);
  modport cmp (input  pclk, presetn, psel, penable, pwrite, paddr, pwdata,
               output pready, prdata, pslverr,
               import access);
endinterface

// apb_req: turns one command (start, wr, addr, wdata) into one transfer on the bus.
module apb_req (
  apb_if.req         bus,
  input  logic       start,      // one-cycle pulse: begin a transfer
  input  logic       wr,         // 1: write, 0: read
  input  logic [7:0] addr, wdata,
  output logic       done,       // one-cycle pulse: the transfer completed
  output logic [7:0] rdata,      // read data, held until the next transfer
  output logic       err         // the completer's error, held with rdata
);
  typedef enum logic [1:0] {IDLE, SETUP, ACCESS} state_e;
  state_e state_q;

  always_ff @(posedge bus.pclk or negedge bus.presetn)
    if (!bus.presetn) begin
      state_q <= IDLE; bus.psel <= 1'b0; bus.penable <= 1'b0; bus.pwrite <= 1'b0;
      bus.paddr <= '0; bus.pwdata <= '0; done <= 1'b0; rdata <= '0; err <= 1'b0;
    end else begin
      done <= 1'b0;
      case (state_q)
        IDLE:    if (start) begin
                   state_q <= SETUP; bus.psel <= 1'b1;
                   bus.pwrite <= wr; bus.paddr <= addr; bus.pwdata <= wdata;
                 end
        SETUP:   begin state_q <= ACCESS; bus.penable <= 1'b1; end
        ACCESS:  if (bus.pready) begin
                   state_q <= IDLE; bus.psel <= 1'b0; bus.penable <= 1'b0;
                   done <= 1'b1; rdata <= bus.prdata; err <= bus.pslverr;
                 end
        default: state_q <= IDLE;
      endcase
    end
endmodule

// apb_regs: the completer. LEDS at 0x00 and SCRATCH at 0x04 read and write; ID at 0x08
// reads 0x4c. Any other address, or a write to ID, completes with pslverr = 1.
module apb_regs (
  apb_if.cmp         bus,
  output logic [7:0] leds
);
  logic [7:0] scratch_q;
  wire is_leds = (bus.paddr == 8'h00);
  wire is_scr  = (bus.paddr == 8'h04);
  wire is_id   = (bus.paddr == 8'h08);
  wire bad     = !(is_leds || is_scr || is_id) || (bus.pwrite && is_id);

  assign bus.pready  = 1'b1;                      // no wait states
  assign bus.pslverr = bus.access() && bad;
  assign bus.prdata  = is_leds ? leds : is_scr ? scratch_q : is_id ? 8'h4c : 8'h00;

  always_ff @(posedge bus.pclk or negedge bus.presetn)
    if (!bus.presetn) begin leds <= '0; scratch_q <= '0; end
    else if (bus.access() && !bad && bus.pwrite) begin
      if (is_leds) leds      <= bus.pwdata;
      if (is_scr)  scratch_q <= bus.pwdata;
    end
endmodule

// apb_sys: the top. One interface instance; each module takes its own side.
module apb_sys (
  input  logic       pclk, presetn,
  input  logic       start, wr,
  input  logic [7:0] addr, wdata,
  output logic       done,
  output logic [7:0] rdata,
  output logic       err,
  output logic [7:0] leds
);
  apb_if   bus    (.pclk(pclk), .presetn(presetn));
  apb_req  u_req  (.bus(bus), .start(start), .wr(wr), .addr(addr), .wdata(wdata),
                   .done(done), .rdata(rdata), .err(err));
  apb_regs u_regs (.bus(bus), .leds(leds));
endmodule

module tb;
  logic clk = 0, rst_n = 0;
  always #5 clk = ~clk;
  logic       start = 0, wr = 0, done, err, write_err;
  logic [7:0] addr = 0, wdata = 0, rdata, leds;
  apb_sys dut (.pclk(clk), .presetn(rst_n), .*);
  task automatic xfer(input bit w, input logic [7:0] a, input logic [7:0] d);
    start = 1; wr = w; addr = a; wdata = d;
    @(posedge clk); #1 start = 0;
    while (!done) begin @(posedge clk); #1; end
  endtask
  initial begin
    #12 rst_n = 1;
    xfer(1, 8'h08, 8'hff);  write_err = err;   // write to ID
    xfer(0, 8'h08, 8'h00);                     // read ID back
    $display("write_err=%0d id=%h", write_err, rdata);
    $finish(0);
  end
endmodule
Spot the bugThe completer has discrete ports. Each option is three of its connections in the bench's instance. The bench writes LEDS and SCRATCH, then reads them back. Which option fails?
ConceptWhat does a modport add to an interface?
ConceptThe lab builds 100 cells with the interface. You rewrite the same two modules with discrete ports and ten named bus connections each. What does Synthesize report?
ConceptWhich of these belongs in an interface that you synthesize?

Remember

  • An interface declares a connection's signals once; a modport gives one side its directions.
  • Name the modport in the module's port, put the instance in the module that connects both sides, and connect by name.
  • Driving an input of your modport stops Run, Lint and Synthesize; swapping two same-width discrete ports stops nothing.
  • An interface costs no gates: the lab builds 100 cells with it and 100 with discrete ports. Keep clocking blocks and tasks out of it.