APB Series — APB-11: Designing an APB Peripheral — VLSI Trainers
APB Series · APB-11

Designing an APB Peripheral

A complete worked example — a real APB4 GPIO controller RTL from scratch. Register map design, the Completer interface logic, PREADY wait states, PSLVERR error reporting, PPROT access gating, byte-lane strobes, and a complete synthesisable SystemVerilog implementation that ties every concept from the series together.

📋 What We Are Building

We will design a complete APB4 GPIO controller — a 32-bit bidirectional GPIO peripheral accessible over APB. This is the most common APB peripheral type and uses every major APB concept: address decode, register reads and writes, byte-lane strobes, error responses for invalid addresses, and secure/non-secure access control.

APB4 GPIO Controller — Block Diagram APB4 Interface PCLK, PRESETn PADDR[11:0] PSELx, PENABLE PWRITE, PPROT PWDATA[31:0] PSTRB[3:0] PRDATA, PREADY, PSLVERR GPIO Controller Logic Address decode & error detect Write logic with PSTRB byte enables PPROT secure/non-secure gating PRDATA mux (reg select) PREADY & PSLVERR generation Register file (6 registers) GPIO Pads gpio_out[31:0] gpio_oe[31:0] gpio_in[31:0] irq_out IRQ Controller Interrupt pending
Figure 1 — GPIO controller block diagram. The APB4 interface block handles all protocol signalling. The controller logic decodes addresses, gates writes with PPROT and PSTRB, generates PRDATA, and drives PREADY and PSLVERR. GPIO pads and an optional interrupt output are the functional outputs.

The GPIO controller implements:

📋 Register Map Design

The GPIO controller occupies a 4 KB address window. The six registers are placed at the bottom of the address space. Accesses to addresses above 0x014 return PSLVERR=1.

Offset
Name
Access
Description
0x000
DATA
RW
GPIO output data register. Writing sets the logic level driven on gpio_out[31:0] when in output mode. Reading returns the current value of gpio_in[31:0] (actual pad values, not the written value).
0x004
DIR
RW
Direction register. 1=output, 0=input per bit. Controls gpio_oe[31:0].
0x008
IEN
RW
Interrupt enable register. 1=interrupt enabled for this GPIO pin.
0x00C
IPEND
RC
Interrupt pending register. Read to see which pins have a pending interrupt. Writing any value clears the pending bits that are written as 1 (write-1-to-clear).
0x010
ICTRL
RW
Interrupt control: [1:0] = edge/level select per group, [3:2] = rising/falling. Non-secure accessible.
0x014
IMASK
RW (Secure)
Secure interrupt mask. PPROT[1]=1 (non-secure) accesses return PSLVERR=1 and do not update the register. Only accessible from secure world.
0x018+
Unimplemented. All accesses return PSLVERR=1. Read data is 0.

📋 Port List

module apb4_gpio #(
  parameter ADDR_W = 12,   // 4 KB address window
  parameter DATA_W = 32    // 32-bit data bus
) (
  // Clock and reset
  input  logic                  PCLK,
  input  logic                  PRESETn,

  // APB4 Completer interface
  input  logic [ADDR_W-1:0]    PADDR,
  input  logic                  PSELx,
  input  logic                  PENABLE,
  input  logic                  PWRITE,
  input  logic [2:0]            PPROT,
  input  logic [DATA_W-1:0]    PWDATA,
  input  logic [DATA_W/8-1:0]  PSTRB,
  output logic [DATA_W-1:0]    PRDATA,
  output logic                  PREADY,
  output logic                  PSLVERR,

  // GPIO functional interface
  output logic [DATA_W-1:0]    gpio_out,  // driven pad values
  output logic [DATA_W-1:0]    gpio_oe,   // output enable
  input  logic [DATA_W-1:0]    gpio_in,   // sampled pad values
  output logic                  irq_out    // interrupt to IRQ controller
);

📋 Address Decode

Address decoding compares PADDR to the defined register offsets. Only PADDR bits within the peripheral’s address window are relevant — the bridge has already decoded the upper bits and asserted PSELx for this peripheral.

// ─── Address decode ───────────────────────────────────────
// Use only the lower 12 bits (4 KB window)
localparam REG_DATA  = 12'h000;
localparam REG_DIR   = 12'h004;
localparam REG_IEN   = 12'h008;
localparam REG_IPEND = 12'h00C;
localparam REG_ICTRL = 12'h010;
localparam REG_IMASK = 12'h014;

wire [ADDR_W-1:0] addr = PADDR[ADDR_W-1:0];

wire sel_data  = (addr == REG_DATA);
wire sel_dir   = (addr == REG_DIR);
wire sel_ien   = (addr == REG_IEN);
wire sel_ipend = (addr == REG_IPEND);
wire sel_ictrl = (addr == REG_ICTRL);
wire sel_imask = (addr == REG_IMASK);

// Any valid register hit
wire addr_valid = sel_data | sel_dir | sel_ien |
                  sel_ipend | sel_ictrl | sel_imask;

// PPROT[1]=1 means non-secure — reject IMASK access
wire ns_access    = PPROT[1];
wire sec_violation = sel_imask & ns_access;
Always decode only the address bits your peripheral owns. Do not compare the full 32-bit PADDR — the upper bits are not your peripheral’s concern. The bridge has already validated them. If your peripheral occupies 0x4000_0000 to 0x4000_0FFF, only decode bits[11:0]. Using more bits makes the peripheral fragile to address space remapping.

📋 Write Logic — PWDATA, PSTRB, PPROT

Registers are updated on the transfer completion cycle: when PSELx, PENABLE, PREADY, and PWRITE are all HIGH simultaneously. Each byte lane is independently gated by PSTRB. Writes to protected registers (IMASK from non-secure) are silently dropped — the PSLVERR is generated separately.

// ─── Register declarations ────────────────────────────────
logic [DATA_W-1:0] r_data, r_dir, r_ien;
logic [DATA_W-1:0] r_ipend, r_ictrl, r_imask;

// ─── Write enable — gated by transfer completion ──────────
wire wr_en = PSELx & PENABLE & PREADY & PWRITE;

// ─── Byte-lane write helper macro ─────────────────────────
// Updates reg_r byte by byte, only where PSTRB=1
task automatic byte_write;
  input [DATA_W-1:0] data_in;
  ref   [DATA_W-1:0] reg_r;
  integer b;
  begin
    for (b = 0; b < DATA_W/8; b++) begin
      if (PSTRB[b]) reg_r[(b*8)+:8] <= data_in[(b*8)+:8];
    end
  end
endtask

// ─── Register write logic ─────────────────────────────────
always_ff @(posedge PCLK or negedge PRESETn) begin
  if (!PRESETn) begin
    r_data  <= '0; r_dir   <= '0;
    r_ien   <= '0; r_ictrl <= '0;
    r_imask <= '1; // default: all masked (safe)
    r_ipend <= '0;
  end else begin
    // Interrupt pending — set on gpio_in edge, clear on write-1
    r_ipend <= (r_ipend | irq_set) &
               ~(wr_en & sel_ipend ? PWDATA : '0);

    if (wr_en & !sec_violation) begin
      if (sel_data)  byte_write(PWDATA, r_data);
      if (sel_dir)   byte_write(PWDATA, r_dir);
      if (sel_ien)   byte_write(PWDATA, r_ien);
      if (sel_ictrl) byte_write(PWDATA, r_ictrl);
      if (sel_imask)  // secure only — sec_violation gates this
        byte_write(PWDATA, r_imask);
    end
  end
end

📋 Read Logic — PRDATA Mux

PRDATA is combinationally muxed from the register outputs based on PADDR. For a zero-wait-state peripheral this is purely combinational — PRDATA is valid in the same cycle that PENABLE is asserted. The read data must be valid by the rising edge where PREADY=1.

// ─── PRDATA mux — combinational ───────────────────────────
always_comb begin
  PRDATA = '0; // default: unimplemented = 0
  if (PSELx & !PWRITE) begin
    case (1'b1)
      sel_data:  PRDATA = gpio_in;   // reads pad values
      sel_dir:   PRDATA = r_dir;
      sel_ien:   PRDATA = r_ien;
      sel_ipend: PRDATA = r_ipend;
      sel_ictrl: PRDATA = r_ictrl;
      sel_imask: PRDATA = ns_access ? '0 : r_imask;
      default:   PRDATA = '0;
    endcase
  end
end

// ─── GPIO functional outputs ──────────────────────────────
assign gpio_out = r_data;
assign gpio_oe  = r_dir;
assign irq_out  = |(r_ipend & r_ien & ~r_imask);
IMASK reads return 0 to non-secure initiators. A non-secure read of IMASK returns all zeros — this prevents non-secure software from discovering the mask configuration. PSLVERR is also asserted (see next section) to signal that the access was rejected, so the initiating software knows the read result is not meaningful.

📋 PREADY — Wait State Generation

This GPIO controller uses zero wait states — all register accesses complete in the minimum two cycles (one Setup + one Access). PREADY is tied HIGH permanently. The spec notes: “Peripherals that have a fixed two cycle access can tie PREADY HIGH.”

// ─── PREADY — zero wait states ────────────────────────────
// Completer always ready immediately in Access phase
assign PREADY = 1'b1;

// ─── If wait states were needed (example for slow memory) ──
// always_ff @(posedge PCLK or negedge PRESETn) begin
//   if (!PRESETn)       pready_r <= 1'b0;
//   else if (!PSELx)    pready_r <= 1'b0;
//   else if (PENABLE)   pready_r <= mem_ack; // assert when memory responds
// end
// assign PREADY = PSELx ? pready_r : 1'b1;

When to use wait states

📋 PSLVERR — Error Reporting

PSLVERR is asserted at the transfer completion cycle for two error conditions: access to an unimplemented address, or a non-secure access to the secure IMASK register. PSLVERR is only valid when PSELx, PENABLE, and PREADY are all HIGH.

// ─── PSLVERR generation ───────────────────────────────────
// Two error conditions:
//   1. Unimplemented address (addr_valid=0)
//   2. Non-secure access to secure register (sec_violation=1)

wire error_cond = !addr_valid | sec_violation;

// PSLVERR valid only at transfer completion
assign PSLVERR = PSELx & PENABLE & PREADY & error_cond;

// Recommended: drive LOW when not at completion cycle
// (above assign already does this — PSLVERR=0 when any of
//  PSELx, PENABLE, PREADY is deasserted)
Writes to invalid addresses still complete as APB transfers. The write goes through the full Setup + Access sequence. PSLVERR=1 at completion tells the bridge the write failed. However, because the address decode did not match any register, wr_en never enables any register update — the write is silently ignored at the register level while PSLVERR informs the initiator.

📋 Complete SystemVerilog

Putting all sections together into the complete synthesisable module:

module apb4_gpio #(
  parameter ADDR_W = 12,
  parameter DATA_W = 32
) (
  input  logic                  PCLK, PRESETn,
  input  logic [ADDR_W-1:0]    PADDR,
  input  logic                  PSELx, PENABLE, PWRITE,
  input  logic [2:0]            PPROT,
  input  logic [DATA_W-1:0]    PWDATA,
  input  logic [DATA_W/8-1:0]  PSTRB,
  output logic [DATA_W-1:0]    PRDATA,
  output logic                  PREADY, PSLVERR,
  output logic [DATA_W-1:0]    gpio_out, gpio_oe,
  input  logic [DATA_W-1:0]    gpio_in,
  output logic                  irq_out
);

  // ─ Address decode ─────────────────────────────────────────
  localparam [ADDR_W-1:0]
    A_DATA = 'h000, A_DIR   = 'h004,
    A_IEN  = 'h008, A_IPEND = 'h00C,
    A_ICTRL= 'h010, A_IMASK = 'h014;

  wire s_dat  = (PADDR == A_DATA),  s_dir  = (PADDR == A_DIR);
  wire s_ien  = (PADDR == A_IEN),   s_ip   = (PADDR == A_IPEND);
  wire s_ic   = (PADDR == A_ICTRL), s_im   = (PADDR == A_IMASK);
  wire a_ok  = s_dat|s_dir|s_ien|s_ip|s_ic|s_im;
  wire sec_v = s_im & PPROT[1]; // PPROT[1]=1: non-secure

  // ─ Registers ──────────────────────────────────────────────
  logic [DATA_W-1:0] r_dat,r_dir,r_ien,r_ip,r_ic,r_im;

  wire we = PSELx&PENABLE&PREADY&PWRITE&!sec_v;

  // Interrupt set logic (rising edge detector, simplified)
  logic [DATA_W-1:0] gpio_in_r;
  wire  [DATA_W-1:0] irq_set = (gpio_in & ~gpio_in_r) & ~r_dir;

  always_ff @(posedge PCLK or negedge PRESETn) begin
    if (!PRESETn) begin
      {r_dat,r_dir,r_ien,r_ip,r_ic} <= '0;
      r_im <= '1; gpio_in_r <= '0;
    end else begin
      gpio_in_r <= gpio_in;
      r_ip <= (r_ip|irq_set) & ~(we&s_ip ? PWDATA:'0);
      for (int b=0; b<DATA_W/8; b++) begin
        if (we&PSTRB[b]) begin
          if(s_dat) r_dat[(b*8)+:8]<=PWDATA[(b*8)+:8];
          if(s_dir) r_dir[(b*8)+:8]<=PWDATA[(b*8)+:8];
          if(s_ien) r_ien[(b*8)+:8]<=PWDATA[(b*8)+:8];
          if(s_ic)  r_ic[(b*8)+:8] <=PWDATA[(b*8)+:8];
          if(s_im)  r_im[(b*8)+:8] <=PWDATA[(b*8)+:8];
        end
      end
    end
  end

  // ─ PRDATA mux ─────────────────────────────────────────────
  always_comb begin
    PRDATA = '0;
    if (PSELx & !PWRITE)
      unique case (1'b1)
        s_dat: PRDATA = gpio_in;
        s_dir: PRDATA = r_dir;
        s_ien: PRDATA = r_ien;
        s_ip:  PRDATA = r_ip;
        s_ic:  PRDATA = r_ic;
        s_im:  PRDATA = sec_v ? '0 : r_im;
        default: PRDATA = '0;
      endcase
  end

  // ─ APB outputs ────────────────────────────────────────────
  assign PREADY  = 1'b1;
  assign PSLVERR = PSELx&PENABLE&PREADY&(!a_ok|sec_v);

  // ─ Functional outputs ─────────────────────────────────────
  assign gpio_out = r_dat;
  assign gpio_oe  = r_dir;
  assign irq_out  = |(r_ip & r_ien & ~r_im);

endmodule

📋 Peripheral Design Checklist

Use this checklist when implementing any APB Completer:

Protocol compliance

Address decode

Protection (APB4+)

Reset behaviour

Simulation and verification

📋 Quick Reference — Peripheral Design Rules

ItemRule
Write enable conditionPSELx & PENABLE & PREADY & PWRITE — all four HIGH simultaneously
Read data valid atRising PCLK where PSELx+PENABLE+PREADY=1 and PWRITE=0
PSLVERR valid atRising PCLK where PSELx+PENABLE+PREADY=1 only
PSLVERR drive ruleDrive LOW when PSELx=0 or PENABLE=0 or PREADY=0
PSTRB byte writeif (PSTRB[n]) reg[(8n+7):(8n)] <= PWDATA[(8n+7):(8n)]
PSTRB on readsIgnore entirely — bridge drives 0 on reads
Address decodeOnly bits within your window — typically PADDR[N:2] for word-aligned regs
Unimplemented addressPSLVERR=1, PRDATA=0, no register side effects
PPROT[1]=1 on secure regPSLVERR=1, PRDATA=0 (don’t reveal register value), no write
Zero wait statesTie PREADY=1 permanently — transfers always complete in 2 cycles
Variable wait statesAssert PREADY only when the slow operation (memory, CDC) completes
Reset defaultsConservative: interrupts masked, outputs disabled, no unsafe enables
Read-only register writeEither PSLVERR=1 (strict) or silently ignore (lenient) — document your choice
Write-1-to-clear registerreg <= reg & ~(wr_en ? PWDATA : 0)
DATA register readReturns gpio_in (actual pad values), not the written DATA register value
Scroll to Top