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 PASSEDThe log proves the slave idles with
ack_olow (nocyc/stb), accepts eight classic write cycles, returns the exact written data on read-back for every address, and dropsack_oback to zero after each cycle completes. Any corrupted register or stuck ack would print a FAIL line instead ofALL TESTS PASSED.A variant that honors the 4-bit byte-select
sel_ion 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, asel_i=0no-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 endmodulePASS 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 PASSEDThe log shows a full-word write/read-back, then a masked write with
sel_i=4'b0101that updates only byte lanes 0 and 2 (0x11223344becomes0x11BB33DD), asel_i=0write that changes nothing, and that reads of the top address always return the fixed status value0x1ACE0001even 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_iare both high, and the transfer completes on the rising clock edge whereack_ois sampled high. The slave must never assertack_ounless it is being selected bycyc_i & stb_i. In the code above,wire valid = cyc_i & stb_i;gates everything, andack_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_iandstb_itogether for one transfer, gets oneack_o, then drops both. In a block cycle the master keepscyc_ihigh across several back-to-back transfers, togglingstb_i/adr_ifor 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_iper transfer and emits one ack per request, a master can either do isolated accesses or holdcyc_ifor 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_iindexes directly intomem[adr_i]. In Example 2, the comparisonadr_i == STATUS_ADRcarves 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 ofdat_i/dat_o. A write withsel_i=4'b0011updates only the lower halfword;4'b1000writes only the top byte. This is how a CPU performsSB/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 that0x11223344masked withsel_i=4'b0101becomes0x11BB33DD. A common bug is ignoringsel_ientirely and always writing the full word — that silently corrupts adjacent bytes on sub-word stores.Common mistakes
- Asserting ack without select.
ack_omust depend oncyc_i & stb_i. An ack that is only a function ofstb_i(ignoringcyc_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_ohigh while the master still holds its request causes a second phantom transfer — the& ~ack_rterm 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_rpowers 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_iuntil it seesack_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_imarks only the request phase and may deassert beforeack_oarrives, the slave can usestall_oto 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 tyingstall_olow and emitting one ack per strobe.Synthesis notes
Both designs are fully synthesizable Verilog-2005. The register array
meminfers flip-flops (a small register file); for a larger address space, pointmemat 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),integerloop variables, and the Verilog-2001 part-select[b*8 +: 8], all supported iniverilog -g2005.How does the master know data is ready on a read?
On a read,
dat_ois valid in the same cycle the slave assertsack_o. The master samplesdat_oon the rising edge where it seesack_ohigh — which is exactly whatwb_readdoes in the testbench.Can the slave insert wait states?
Yes — simply delay asserting
ack_o. The master holdscyc_i/stb_iuntil 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_imeans “I own the bus for this cycle”;stb_imeans “I am presenting a valid request to you right now.” In a single transfer they rise and fall together, but in a block cyclecyc_istays high whilestb_ipulses 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 LabRun it in the LabOpen this design in the free in-browser simulator — edit, compile and see the waveform/log instantly.
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 Hi! Ask me about courses, fees, batches or discounts. ×ChipXpert AssistantAsk about courses, fees, batches and centres