Wishbone Slave in Verilog: Classic Bus Interface with Code

The Wishbone bus is the de-facto interconnect for open-source silicon — OpenRISC, LiteX, the RISC-V cores on OpenCores, and countless Caravel/OpenLane tape-outs all speak it. A slave is any peripheral that sits on the bus and responds to the master: a register block, a RAM, a UART, a timer. This page gives you clean, synthesizable Wishbone slave Verilog code for the B4 classic (non-pipelined) handshake, with a testbench that plays the role of the master.

Both examples below are complete, single-file designs that compile and run under Icarus Verilog in strict Verilog-2005 mode. The first wraps an 8-entry register file and self-checks every write/read round-trip and the ack timing; the second adds a byte-select (sel_i) write mask and a read-only status register.

A minimal Wishbone B4 classic slave over an 8-entry register file. The testbench acts as the master: it asserts cyc_i/stb_i, waits for ack_o, writes all eight registers, then reads them back and checks each value plus the ack handshake timing.

// Wishbone B4 classic (non-pipelined) slave wrapping an 8-entry register file.
module wb_regfile_slave #(
    parameter AW = 3,          // address width (word address) -> 8 registers
    parameter DW = 32
)(
    input  wire          clk_i,
    input  wire          rst_i,   // synchronous, active high
    input  wire          cyc_i,
    input  wire          stb_i,
    input  wire          we_i,
    input  wire [AW-1:0] adr_i,
    input  wire [DW-1:0] dat_i,
    output reg  [DW-1:0] dat_o,
    output wire          ack_o
);
    reg [DW-1:0] mem [0:(1<<AW)-1];
    reg          ack_r;

    // Classic handshake: a valid access is cyc_i & stb_i. We ack the next
    // cycle and deassert after one cycle (single-cycle ack pulse).
    wire valid = cyc_i & stb_i;

    always @(posedge clk_i) begin
        if (rst_i) begin
            ack_r <= 1'b0;
        end else begin
            // ack only when a request is present and not already acked
            ack_r <= valid & ~ack_r;
        end
    end

    always @(posedge clk_i) begin
        if (valid & we_i & ~ack_r)
            mem[adr_i] <= dat_i;
    end

    // Registered read data, presented alongside ack
    always @(posedge clk_i) begin
        if (valid & ~ack_r)
            dat_o <= mem[adr_i];
    end

    assign ack_o = ack_r;
endmodule

module tb;
    localparam AW = 3;
    localparam DW = 32;
    reg clk, rst, cyc, stb, we;
    reg  [AW-1:0] adr;
    reg  [DW-1:0] dat_w;
    wire [DW-1:0] dat_r;
    wire ack;
    integer i;
    integer errors;
    reg [DW-1:0] golden [0:7];
    reg [DW-1:0] rd;

    wb_regfile_slave #(.AW(AW), .DW(DW)) dut (
        .clk_i(clk), .rst_i(rst), .cyc_i(cyc), .stb_i(stb), .we_i(we),
        .adr_i(adr), .dat_i(dat_w), .dat_o(dat_r), .ack_o(ack));

    always #5 clk = ~clk;

    // One classic WRITE cycle: assert cyc/stb/we, hold until ack, then drop.
    task wb_write(input [AW-1:0] a, input [DW-1:0] d);
        begin
            @(negedge clk);
            cyc = 1; stb = 1; we = 1; adr = a; dat_w = d;
            // wait for ack (sampled on rising edges)
            while (ack !== 1'b1) @(posedge clk);
            @(negedge clk);
            cyc = 0; stb = 0; we = 0;
        end
    endtask

    // One classic READ cycle: assert cyc/stb, we=0, capture dat_o at ack.
    task wb_read(input [AW-1:0] a, output [DW-1:0] d);
        begin
            @(negedge clk);
            cyc = 1; stb = 1; we = 0; adr = a;
            while (ack !== 1'b1) @(posedge clk);
            d = dat_r;
            @(negedge clk);
            cyc = 0; stb = 0;
        end
    endtask

    initial begin
        clk = 0; rst = 1; cyc = 0; stb = 0; we = 0; adr = 0; dat_w = 0;
        errors = 0;
        golden[0]=32'hDEAD_BEEF; golden[1]=32'h0000_0001; golden[2]=32'hA5A5_5A5A;
        golden[3]=32'hFFFF_FFFF; golden[4]=32'h1234_5678; golden[5]=32'h0000_0000;
        golden[6]=32'hCAFE_F00D; golden[7]=32'h8000_0000;
        repeat (2) @(negedge clk);
        rst = 0;

        // Check ack is low while idle (no cyc/stb)
        @(posedge clk);
        if (ack !== 1'b0) begin
            errors = errors + 1; $display("FAIL idle ack asserted");
        end else $display("PASS ack low while idle (cyc=stb=0)");

        // Write all 8 registers
        for (i = 0; i < 8; i = i + 1)
            wb_write(i[AW-1:0], golden[i]);
        $display("PASS wrote 8 registers via classic write cycles");

        // Read back and self-check
        for (i = 0; i < 8; i = i + 1) begin
            wb_read(i[AW-1:0], rd);
            if (rd !== golden[i]) begin
                errors = errors + 1;
                $display("FAIL read adr=%0d got %h exp %h", i, rd, golden[i]);
            end else
                $display("PASS read adr=%0d = %h", i, rd);
        end

        // After a cycle completes, ack must return to 0
        @(posedge clk);
        if (ack !== 1'b0) begin
            errors = errors + 1; $display("FAIL ack stuck high after cycle");
        end else $display("PASS ack returns low after cycle ends");

        if (errors == 0) $display("ALL TESTS PASSED");
        else $display("TESTS FAILED: %0d error(s)", errors);
        $finish;
    end
endmodule
PASS ack low while idle (cyc=stb=0)
PASS wrote 8 registers via classic write cycles
PASS read adr=0 = deadbeef
PASS read adr=1 = 00000001
PASS read adr=2 = a5a55a5a
PASS read adr=3 = ffffffff
PASS read adr=4 = 12345678
PASS read adr=5 = 00000000
PASS read adr=6 = cafef00d
PASS read adr=7 = 80000000
PASS ack returns low after cycle ends
ALL TESTS PASSED

The log proves the slave idles with ack_o low (no cyc/stb), accepts eight classic write cycles, returns the exact written data on read-back for every address, and drops ack_o back to zero after each cycle completes. Any corrupted register or stuck ack would print a FAIL line instead of ALL TESTS PASSED.

A variant that honors the 4-bit byte-select sel_i on writes and reserves the top address as a read-only status register. The testbench verifies full-word writes, a partial (masked) write that leaves untouched byte lanes intact, a sel_i=0 no-op, and that the status word cannot be overwritten.

// Wishbone B4 classic slave with byte-select (sel_i) write mask and a
// read-only status register at the top address.
module wb_sel_slave #(
    parameter AW = 4,          // 16 words
    parameter DW = 32
)(
    input  wire          clk_i,
    input  wire          rst_i,
    input  wire          cyc_i,
    input  wire          stb_i,
    input  wire          we_i,
    input  wire [AW-1:0] adr_i,
    input  wire [3:0]    sel_i,   // one bit per byte lane
    input  wire [DW-1:0] dat_i,
    output reg  [DW-1:0] dat_o,
    output wire          ack_o
);
    localparam STATUS_ADR = {AW{1'b1}};     // top address = read-only status
    localparam [DW-1:0] STATUS_VAL = 32'h1ACE_0001;

    reg [DW-1:0] mem [0:(1<<AW)-1];
    reg          ack_r;
    integer      b;

    wire valid = cyc_i & stb_i;
    wire is_status = (adr_i == STATUS_ADR);

    always @(posedge clk_i) begin
        if (rst_i) ack_r <= 1'b0;
        else       ack_r <= valid & ~ack_r;
    end

    // Byte-masked write; writes to the status address are ignored (read-only)
    always @(posedge clk_i) begin
        if (valid & we_i & ~ack_r & ~is_status) begin
            for (b = 0; b < 4; b = b + 1)
                if (sel_i[b])
                    mem[adr_i][b*8 +: 8] <= dat_i[b*8 +: 8];
        end
    end

    always @(posedge clk_i) begin
        if (valid & ~ack_r)
            dat_o <= is_status ? STATUS_VAL : mem[adr_i];
    end

    assign ack_o = ack_r;
endmodule

module tb;
    localparam AW = 4;
    localparam DW = 32;
    reg clk, rst, cyc, stb, we;
    reg  [AW-1:0] adr;
    reg  [3:0]    sel;
    reg  [DW-1:0] dat_w;
    wire [DW-1:0] dat_r;
    wire ack;
    integer errors;
    reg [DW-1:0] rd;

    wb_sel_slave #(.AW(AW), .DW(DW)) dut (
        .clk_i(clk), .rst_i(rst), .cyc_i(cyc), .stb_i(stb), .we_i(we),
        .adr_i(adr), .sel_i(sel), .dat_i(dat_w), .dat_o(dat_r), .ack_o(ack));

    always #5 clk = ~clk;

    task wb_write(input [AW-1:0] a, input [3:0] s, input [DW-1:0] d);
        begin
            @(negedge clk);
            cyc=1; stb=1; we=1; adr=a; sel=s; dat_w=d;
            while (ack !== 1'b1) @(posedge clk);
            @(negedge clk); cyc=0; stb=0; we=0;
        end
    endtask

    task wb_read(input [AW-1:0] a, output [DW-1:0] d);
        begin
            @(negedge clk);
            cyc=1; stb=1; we=0; adr=a; sel=4'hF;
            while (ack !== 1'b1) @(posedge clk);
            d = dat_r;
            @(negedge clk); cyc=0; stb=0;
        end
    endtask

    initial begin
        clk=0; rst=1; cyc=0; stb=0; we=0; adr=0; sel=0; dat_w=0; errors=0;
        repeat (2) @(negedge clk);
        rst = 0;

        // Full-word write then read back
        wb_write(4'h2, 4'hF, 32'h1122_3344);
        wb_read(4'h2, rd);
        if (rd !== 32'h1122_3344) begin errors=errors+1;
            $display("FAIL full write got %h", rd); end
        else $display("PASS full-word write/read = %h", rd);

        // Partial write: only lanes 0 and 2 enabled -> other bytes unchanged
        wb_write(4'h2, 4'b0101, 32'hAABB_CCDD);
        // 0xAABBCCDD: byte0=DD,byte1=CC,byte2=BB,byte3=AA; sel lanes 0,2 -> byte0=DD,byte2=BB
        wb_read(4'h2, rd);
        if (rd !== 32'h11BB_33DD) begin errors=errors+1;
            $display("FAIL sel mask got %h exp 11BB33DD", rd); end
        else $display("PASS byte-select mask kept lanes = %h", rd);

        // sel=0 write must change nothing
        wb_write(4'h2, 4'b0000, 32'hFFFF_FFFF);
        wb_read(4'h2, rd);
        if (rd !== 32'h11BB_33DD) begin errors=errors+1;
            $display("FAIL sel=0 modified data %h", rd); end
        else $display("PASS sel=0 write is a no-op = %h", rd);

        // Read-only status register reads a fixed value
        wb_read(4'hF, rd);
        if (rd !== 32'h1ACE_0001) begin errors=errors+1;
            $display("FAIL status got %h", rd); end
        else $display("PASS status register = %h", rd);

        // Attempt to write status: must stay read-only
        wb_write(4'hF, 4'hF, 32'h0000_0000);
        wb_read(4'hF, rd);
        if (rd !== 32'h1ACE_0001) begin errors=errors+1;
            $display("FAIL status became writable %h", rd); end
        else $display("PASS status stayed read-only = %h", rd);

        if (errors == 0) $display("ALL TESTS PASSED");
        else $display("TESTS FAILED: %0d error(s)", errors);
        $finish;
    end
endmodule
PASS full-word write/read = 11223344
PASS byte-select mask kept lanes = 11bb33dd
PASS sel=0 write is a no-op = 11bb33dd
PASS status register = 1ace0001
PASS status stayed read-only = 1ace0001
ALL TESTS PASSED

The log shows a full-word write/read-back, then a masked write with sel_i=4'b0101 that updates only byte lanes 0 and 2 (0x11223344 becomes 0x11BB33DD), a sel_i=0 write that changes nothing, and that reads of the top address always return the fixed status value 0x1ACE0001 even after an attempted write — confirming the byte lanes and the read-only decode behave correctly.

The Wishbone classic handshake: cyc, stb, ack

A Wishbone bus cycle is governed by three control signals driven by the master and one acknowledgement from the slave:

  • cyc_i — cycle: high for the whole bus cycle, marking that the master owns the bus. It frames one or more transfers.
  • stb_i — strobe: high when the master is actually presenting a valid request (address, data, write-enable) to this slave.
  • ack_o — acknowledge: driven by the slave to say “transfer complete, data latched / data valid.”

The core rule: a transfer is valid only while cyc_i & stb_i are both high, and the transfer completes on the rising clock edge where ack_o is sampled high. The slave must never assert ack_o unless it is being selected by cyc_i & stb_i. In the code above, wire valid = cyc_i & stb_i; gates everything, and ack_r <= valid & ~ack_r; produces a clean one-cycle ack pulse so a held request yields exactly one transfer.

Single cycles vs. block cycles

In a single cycle the master asserts cyc_i and stb_i together for one transfer, gets one ack_o, then drops both. In a block cycle the master keeps cyc_i high across several back-to-back transfers, toggling stb_i/adr_i for each one while the bus stays owned — this avoids re-arbitration between accesses to the same slave.

The slave code is identical for both: because it keys off cyc_i & stb_i per transfer and emits one ack per request, a master can either do isolated accesses or hold cyc_i for a burst. The testbench here performs a sequence of single cycles, but the same DUT serves a block cycle without change.

Address decode and register mapping

The slave receives a word address on adr_i. A real peripheral decodes this into named registers. In Example 1, adr_i indexes directly into mem[adr_i]. In Example 2, the comparison adr_i == STATUS_ADR carves out one special address whose behavior differs (read-only).

Word vs. byte addressing

Wishbone addresses are commonly byte addresses on the master side, but a 32-bit slave only cares about word boundaries — so the low 2 address bits are typically dropped (adr_i[AW+1:2]) before indexing the register array. These examples use a clean word address for clarity; in an SoC you decode the byte address down to the word index in the slave wrapper.

Byte lanes and sel_i

On a 32-bit data bus, sel_i[3:0] is the byte-select: each bit enables one 8-bit lane of dat_i/dat_o. A write with sel_i=4'b0011 updates only the lower halfword; 4'b1000 writes only the top byte. This is how a CPU performs SB/SH (store-byte / store-halfword) without a read-modify-write.

Example 2 implements this with a per-lane loop: if (sel_i[b]) mem[adr_i][b*8 +: 8] <= dat_i[b*8 +: 8];. Lanes whose select bit is 0 retain their old contents, which the testbench confirms by checking that 0x11223344 masked with sel_i=4'b0101 becomes 0x11BB33DD. A common bug is ignoring sel_i entirely and always writing the full word — that silently corrupts adjacent bytes on sub-word stores.

Common mistakes

  • Asserting ack without select. ack_o must depend on cyc_i & stb_i. An ack that is only a function of stb_i (ignoring cyc_i) will corrupt block cycles and multi-slave buses.
  • Multi-cycle ack. For classic single transfers the ack should be one clock wide. Holding ack_o high while the master still holds its request causes a second phantom transfer — the & ~ack_r term prevents this.
  • Combinational ack with registered data. If you ack combinationally but register the read data, the master latches stale dat_o. Keep ack and valid read data aligned to the same edge (as done here) or register both.
  • Writing a read-only region. Decode and suppress writes to status/ID registers, as Example 2 does with ~is_status.
  • Forgetting reset on the ack flop. If ack_r powers up as X or 1, the first access can be dropped or doubled.

Why Wishbone is popular in open-source SoCs

Wishbone is an open, royalty-free specification originally stewarded by OpenCores and now by the OpenPOWER/community ecosystem. Unlike AMBA AXI/AHB, there are no licensing or trademark concerns for publishing IP, which is why it became the lingua franca of open silicon: OpenRISC (mor1kx), many RISC-V cores, the LiteX SoC builder, the Google/Efabless Caravel harness, and large swaths of the OpenCores catalog all use it. Its classic handshake is simple enough to implement in a few lines (as above), which lowers the barrier for hobbyist and academic designs while still scaling to real tape-outs.

Classic vs. pipelined Wishbone

The examples here implement the classic (B3-style, non-pipelined) cycle: the master holds stb_i until it sees ack_o, so address and data are stable for the whole transfer and there is at most one outstanding request.

Wishbone B4 also defines a pipelined mode. There, stb_i marks only the request phase and may deassert before ack_o arrives, the slave can use stall_o to throttle new requests, and multiple transfers can be in flight — acks come back in order, one per accepted request. Pipelined mode gives much higher throughput for RAMs and bursty masters but needs request/response decoupling (typically a small FIFO or a one-deep skid buffer). A classic slave like these can be bridged to a pipelined bus by tying stall_o low and emitting one ack per strobe.

Synthesis notes

Both designs are fully synthesizable Verilog-2005. The register array mem infers flip-flops (a small register file); for a larger address space, point mem at a block-RAM primitive or let the tool infer BRAM by keeping reads and writes synchronous and avoiding asynchronous read ports. The byte-lane loop in Example 2 unrolls into four write-enable-gated lanes — exactly the byte-write-enable structure that maps onto FPGA BRAM byte-enables. Keep the reset synchronous and on the control flops at minimum; the data array usually does not need reset, which helps BRAM inference.

FAQ

Is this valid Verilog-2005 (no SystemVerilog)?

Yes. It uses only reg/wire, always @(posedge clk), integer loop variables, and the Verilog-2001 part-select [b*8 +: 8], all supported in iverilog -g2005.

How does the master know data is ready on a read?

On a read, dat_o is valid in the same cycle the slave asserts ack_o. The master samples dat_o on the rising edge where it sees ack_o high — which is exactly what wb_read does in the testbench.

Can the slave insert wait states?

Yes — simply delay asserting ack_o. The master holds cyc_i/stb_i until ack in classic mode, so a slave that needs extra cycles (e.g. a slow memory) just acks later. Replace the one-shot ack logic with a small counter to model fixed-latency peripherals.

What is the difference between stb and cyc again?

cyc_i means “I own the bus for this cycle”; stb_i means “I am presenting a valid request to you right now.” In a single transfer they rise and fall together, but in a block cycle cyc_i stays high while stb_i pulses per transfer.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.
▶ Try it in the ChipXpert Lab

Open this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.

Run it in the Lab

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD