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.
// 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
| Signal | Width | req (apb_req) | cmp (apb_regs) |
|---|---|---|---|
pclk, presetn | 1 | input | input |
psel, penable, pwrite | 1 | output | input |
paddr, pwdata | 8 | output | input |
pready, pslverr | 1 | input | output |
prdata | 8 | input | output |
access() | function | not visible | imported |
Every signal is driven by exactly one side. The completer may call access() because its modport imports it.
bus lives in the top module, apb_sys, which connects both sides.- The requester drives its side
Through
req,apb_reqmay drive the five request signals and only read the rest.Follow the calls, values and waits
- call write 0x5a to 0x00: Bench → apb_req
- data write 0x5a to 0x00: apb_req → bus (apb_if)
- The completer answers
Through
cmp,apb_regsdrives the three response signals and the LEDs. Writingbus.pwritefrom here is a compile error.Follow the calls, values and waits
- 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.
| Mistake | Run (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 completer | runs; both writes end with err=1 | 100 cells, the same as the correct wiring |
discrete ports, prdata and pwdata swapped | stops with %Error-BLKANDNBLK on pwdata, which now has two drivers | builds 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 interfaces | Why |
|---|---|
Name the modport in the module's port: apb_if.cmp bus | the module states its own directions, and a reader sees them in its header |
| Put the interface instance in the module that connects both sides | here that is apb_sys; each child takes a port |
| Put shared helper logic in functions | access() is written once; the lab still builds 100 cells |
| Give widths as parameters when they vary | the lab with parameters AW and DW (both 8) builds 100 cells too |
| Keep clocking blocks and tasks for bench interfaces | a 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
apb_sys| Port | Direction | Width | Meaning |
|---|---|---|---|
| pclk | input | 1 | clock |
| presetn | input | 1 | reset, asynchronous, active-low |
| start | input | 1 | one-cycle pulse: begin a transfer |
| wr | input | 1 | 1: write, 0: read |
| addr | input | [7:0] | LEDS 0x00 and SCRATCH 0x04 read and write; ID 0x08 reads 0x4c |
| wdata | input | [7:0] | write data |
| done | output | 1 | pulses at the completion edge |
| rdata | output | [7:0] | read data, held until the next transfer |
| err | output | 1 | any other address, or a write to ID, completes with pslverr 1 |
| leds | output | [7:0] | the LEDS register |
- Clock
- rising edge of
pclk - Reset
presetnasynchronous, active-low- Latency
- SETUP for one cycle, ACCESS until
pready;donepulses at the completion edge - Parameters
- Bus:
apb_ifwith modportsreqandcmp; one instance, inapb_sys
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.Break a direction
Add
assign bus.pwrite = 1'b0;toapb_regs. Run, Lint and Synthesize all stop on that line.Remove the modport
Change the completer's port to
apb_if busand run. The bench still passes, but the module no longer says which signals it drives.
Asked in interviews
What is a modport?
A named view of an interface that gives one side its port directions. A module that takes
bus_if.cmpmay drive only that view's outputs; driving one of its inputs is a compile error.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.
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.
Synthesize the design to measure its cells and inspect the mapped gates.
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=0Original 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
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.