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 asserts PSEL with PENABLE held low.
  • ACCESS phase: the master asserts PENABLE. The slave samples the write, or returns PRDATA, and asserts PREADY to 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):

OffsetIndexNameAccess
0x000CTRLRead/Write
0x041STATUSRead/Write
0x082DATA0Read/Write
0x0C3DATA1Read/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, not PSEL alone, or you will capture the register twice or with stale data.
  • Ignoring PREADY as a master. If your testbench drops PENABLE after a fixed number of cycles instead of waiting for PREADY, it will silently miss transfers once the slave inserts a wait state.
  • Latches on PRDATA. A combinational read block that does not assign PRDATA in every branch infers a latch. Always include the else (drive 0) as shown.
  • Byte vs. word addressing. With word-aligned 32-bit registers, decode paddr[3:2], not paddr[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.

Have questions about this topic?
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
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