APB Slave Verilog Code with Read/Write Testbench
The Advanced Peripheral Bus (APB) is the simplest member of the AMBA family, which is exactly why almost every register block in a System-on-Chip sits behind an APB slave. There is no burst, no pipelining, and no out-of-order traffic to reason about: a master selects the slave, waits one cycle, and the transfer completes. If you can write clean apb slave verilog code, you can wire up control-and-status registers for timers, GPIO, UART configuration, and dozens of other peripherals.
This tutorial builds a complete, synthesizable APB3 slave in Verilog-2005, exposes a four-entry register map, and drives it with a self-checking testbench that performs an APB write followed by an APB read. Everything here compiles and runs with Icarus Verilog, so you can copy it, simulate it, and watch the read-back match the value you wrote.
APB transfer basics: SETUP and ACCESS
Every APB transfer is two phases, each one clock long:
- SETUP phase: the master drives
PADDR,PWRITE, and (for writes)PWDATA, and assertsPSELwithPENABLEheld low. - ACCESS phase: the master asserts
PENABLE. The slave samples the write, or returnsPRDATA, and assertsPREADYto signal completion.
The three control signals that define the handshake are PSEL (this slave is addressed), PENABLE (we are in the ACCESS phase), and PWRITE (1 for a write, 0 for a read). A write commits only when PSEL & PENABLE & PWRITE are all high together.
The register map
We decode four 32-bit registers from bits [3:2] of the address (word-aligned, so each register is four bytes apart):
| Offset | Index | Name | Access |
|---|---|---|---|
| 0x00 | 0 | CTRL | Read/Write |
| 0x04 | 1 | STATUS | Read/Write |
| 0x08 | 2 | DATA0 | Read/Write |
| 0x0C | 3 | DATA1 | Read/Write |
APB slave Verilog code with a write-then-read testbench
This first design uses a zero-wait-state slave: PREADY is tied high, so every transfer finishes in the ACCESS cycle. The testbench resets the DUT, writes 0xDEADBEEF into DATA0, reads it back, and compares.
// apb_slave.v -- APB3 slave with a 4-register map
// Zero-wait-state: PREADY is always high.
module apb_slave #(
parameter ADDR_WIDTH = 8,
parameter DATA_WIDTH = 32
)(
input wire pclk,
input wire presetn, // active-low reset
input wire [ADDR_WIDTH-1:0] paddr,
input wire psel,
input wire penable,
input wire pwrite,
input wire [DATA_WIDTH-1:0] pwdata,
output reg [DATA_WIDTH-1:0] prdata,
output wire pready
);
// Four 32-bit registers decoded from paddr[3:2]:
// 0x00 CTRL, 0x04 STATUS, 0x08 DATA0, 0x0C DATA1
reg [DATA_WIDTH-1:0] regfile [0:3];
wire [1:0] index = paddr[3:2];
assign pready = 1'b1; // no wait states
// Write happens only in the ACCESS phase (PSEL & PENABLE & PWRITE)
integer i;
always @(posedge pclk or negedge presetn) begin
if (!presetn) begin
for (i = 0; i < 4; i = i + 1)
regfile[i] <= {DATA_WIDTH{1'b0}};
end else if (psel && penable && pwrite) begin
regfile[index] <= pwdata;
end
end
// Read is combinational: drive PRDATA whenever selected for a read
always @(*) begin
if (psel && !pwrite)
prdata = regfile[index];
else
prdata = {DATA_WIDTH{1'b0}};
end
endmodule
// ---------------------------------------------------------------
// Testbench: drive one APB write, then one APB read, to the slave
// ---------------------------------------------------------------
module apb_slave_tb;
reg pclk, presetn;
reg [7:0] paddr;
reg psel, penable, pwrite;
reg [31:0] pwdata;
wire [31:0] prdata;
wire pready;
apb_slave dut (
.pclk(pclk), .presetn(presetn), .paddr(paddr),
.psel(psel), .penable(penable), .pwrite(pwrite),
.pwdata(pwdata), .prdata(prdata), .pready(pready)
);
// 10 ns clock
initial pclk = 1'b0;
always #5 pclk = ~pclk;
// One APB write transfer (SETUP then ACCESS)
task apb_write(input [7:0] addr, input [31:0] data);
begin
@(posedge pclk); // SETUP phase
paddr <= addr; pwdata <= data;
pwrite <= 1'b1; psel <= 1'b1; penable <= 1'b0;
@(posedge pclk); // ACCESS phase
penable <= 1'b1;
@(posedge pclk); // slave samples here
psel <= 1'b0; penable <= 1'b0;
$display("[%0t] WRITE addr=0x%02h data=0x%08h", $time, addr, data);
end
endtask
// One APB read transfer; returns captured data in rd
task apb_read(input [7:0] addr, output [31:0] rd);
begin
@(posedge pclk); // SETUP phase
paddr <= addr; pwrite <= 1'b0;
psel <= 1'b1; penable <= 1'b0;
@(posedge pclk); // ACCESS phase
penable <= 1'b1;
#1 rd = prdata; // sample data in ACCESS phase
@(posedge pclk);
psel <= 1'b0; penable <= 1'b0;
$display("[%0t] READ addr=0x%02h data=0x%08h", $time, addr, rd);
end
endtask
reg [31:0] got;
initial begin
// init + reset
psel = 0; penable = 0; pwrite = 0; paddr = 0; pwdata = 0;
presetn = 0;
repeat (2) @(posedge pclk);
presetn = 1;
apb_write(8'h08, 32'hDEAD_BEEF); // write DATA0
apb_read (8'h08, got); // read it back
if (got === 32'hDEAD_BEEF)
$display("PASS: read-back matches written value");
else
$display("FAIL: expected 0xDEADBEEF got 0x%08h", got);
repeat (2) @(posedge pclk);
$finish;
end
endmodule[45] WRITE addr=0x08 data=0xdeadbeef [75] READ addr=0x08 data=0xdeadbeef PASS: read-back matches written value /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmplnppn_cs/apb_slave_tb.v:110: $finish called at 95 (1s)
How the design works
The register file is a simple array, regfile[0:3], and index = paddr[3:2] picks one of four words. The write path lives in a clocked always block with an active-low asynchronous reset: on reset all registers clear, and otherwise a write commits only when psel && penable && pwrite are asserted together — that is the ACCESS phase of a write. The read path is purely combinational: whenever the slave is selected for a read (psel && !pwrite) it drives the addressed register onto PRDATA.
In the testbench, the apb_write and apb_read tasks model the two-phase handshake cycle by cycle. The read task samples PRDATA with a small #1 delay inside the ACCESS phase, then the === comparison (which also catches X/Z values) confirms the read-back equals 0xDEADBEEF.
Adding a wait state with PREADY
Real peripherals are not always ready in one cycle — a register behind a slow clock domain or a FIFO may need to stall the bus. The slave does this by holding PREADY low until it can complete. The master must then keep PSEL, PENABLE, and the address stable until PREADY goes high. This version inserts exactly one wait state and writes two registers before reading both back.
// apb_slave_wait.v -- APB slave that inserts ONE wait state via PREADY
module apb_slave_wait #(
parameter DATA_WIDTH = 32
)(
input wire pclk,
input wire presetn,
input wire [7:0] paddr,
input wire psel,
input wire penable,
input wire pwrite,
input wire [DATA_WIDTH-1:0] pwdata,
output reg [DATA_WIDTH-1:0] prdata,
output wire pready
);
reg [DATA_WIDTH-1:0] regfile [0:3];
wire [1:0] index = paddr[3:2];
// Hold PREADY low for the first ACCESS cycle, high on the next.
reg wait_done;
always @(posedge pclk or negedge presetn) begin
if (!presetn) wait_done <= 1'b0;
else if (psel && penable) wait_done <= 1'b1; // ready next cycle
else wait_done <= 1'b0;
end
assign pready = psel && penable && wait_done;
integer i;
always @(posedge pclk or negedge presetn) begin
if (!presetn)
for (i = 0; i < 4; i = i + 1) regfile[i] <= 32'h0;
else if (psel && penable && pready && pwrite)
regfile[index] <= pwdata; // commit on READY
end
always @(*) prdata = (psel && !pwrite) ? regfile[index] : 32'h0;
endmodule
// Testbench: write two registers, read both back, honouring PREADY
module apb_slave_wait_tb;
reg pclk, presetn;
reg [7:0] paddr;
reg psel, penable, pwrite;
reg [31:0] pwdata;
wire [31:0] prdata;
wire pready;
apb_slave_wait dut (
.pclk(pclk), .presetn(presetn), .paddr(paddr),
.psel(psel), .penable(penable), .pwrite(pwrite),
.pwdata(pwdata), .prdata(prdata), .pready(pready)
);
initial pclk = 0;
always #5 pclk = ~pclk;
task apb_write(input [7:0] addr, input [31:0] data);
begin
@(posedge pclk); paddr <= addr; pwdata <= data;
pwrite <= 1; psel <= 1; penable <= 0;
@(posedge pclk); penable <= 1;
while (!pready) @(posedge pclk); // stretch for wait states
@(posedge pclk); psel <= 0; penable <= 0;
$display("[%0t] WRITE addr=0x%02h data=0x%08h", $time, addr, data);
end
endtask
task apb_read(input [7:0] addr, output [31:0] rd);
begin
@(posedge pclk); paddr <= addr; pwrite <= 0; psel <= 1; penable <= 0;
@(posedge pclk); penable <= 1;
while (!pready) @(posedge pclk);
rd = prdata;
@(posedge pclk); psel <= 0; penable <= 0;
$display("[%0t] READ addr=0x%02h data=0x%08h", $time, addr, rd);
end
endtask
reg [31:0] a, b;
initial begin
psel=0; penable=0; pwrite=0; paddr=0; pwdata=0; presetn=0;
repeat (2) @(posedge pclk); presetn = 1;
apb_write(8'h00, 32'h1234_5678); // CTRL
apb_write(8'h0C, 32'hCAFE_0001); // DATA1
apb_read (8'h00, a);
apb_read (8'h0C, b);
if (a === 32'h1234_5678 && b === 32'hCAFE_0001)
$display("PASS: both registers read back correctly");
else
$display("FAIL: a=0x%08h b=0x%08h", a, b);
repeat (2) @(posedge pclk);
$finish;
end
endmodule[65] WRITE addr=0x00 data=0x12345678 [115] WRITE addr=0x0c data=0xcafe0001 [165] READ addr=0x00 data=0x12345678 [215] READ addr=0x0c data=0xcafe0001 PASS: both registers read back correctly /var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpstild_lx/apb_slave_wait_tb.v:94: $finish called at 235 (1s)
The key addition is wait_done, a flop that is set once the ACCESS phase begins so that PREADY asserts on the following cycle. The write now commits on psel && penable && pready && pwrite, guaranteeing data is captured only when the transfer actually completes. Notice the testbench tasks now spin on while (!pready) @(posedge pclk); — this is the correct master behaviour and the pattern you will reuse for any slave that can stall.
Common mistakes to avoid
- Writing in the SETUP phase. Data is only valid in ACCESS. Gate every write with
PENABLE, notPSELalone, or you will capture the register twice or with stale data. - Ignoring PREADY as a master. If your testbench drops
PENABLEafter a fixed number of cycles instead of waiting forPREADY, it will silently miss transfers once the slave inserts a wait state. - Latches on PRDATA. A combinational read block that does not assign
PRDATAin every branch infers a latch. Always include theelse(drive 0) as shown. - Byte vs. word addressing. With word-aligned 32-bit registers, decode
paddr[3:2], notpaddr[1:0]. Mixing these up maps four registers onto one. - Forgetting the reset. Leaving registers uninitialised gives X values that propagate through reads; use an asynchronous (or synchronous) reset consistently.
- Driving PRDATA when not selected. A slave that keeps driving the read bus can collide with other slaves on a shared
PRDATA; return 0 (or tri-state at the fabric) when!PSEL.
Quick check
During which APB phase does the slave actually capture write data, and which signals must be high?
The ACCESS phase. A write commits when PSEL, PENABLE, and PWRITE are all asserted together (and, for a slave with wait states, when PREADY is also high).
Why is PRDATA generated in a combinational block while the register writes use a clocked block?
Reads must present data within the ACCESS cycle, so PRDATA is combinational off the selected register. Writes change state and must be synchronous to the clock with a reset, so they live in an edge-triggered always block.
What must an APB master keep stable while PREADY is low?
It must hold PADDR, PWRITE, PWDATA (for writes), PSEL, and PENABLE unchanged until the slave raises PREADY, then complete the transfer on that cycle.
With a 32-bit word-aligned register map, which address bits select one of four registers, and why?
Bits [3:2]. Each register is 4 bytes wide, so bits [1:0] are the byte offset within a word; the next two bits, [3:2], index the four separate registers.
Share your question in comments or talk to our mentor team for batch guidance.
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