Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutRoadmapCareersInternshipsFAQContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • The NanoSoC Tour
  • Reading Path
  • SoC Glossary10
  • SoC Metrics Reference
  • SoC Cheatsheet
  • What SoC Integration Is12
  • Architecture and Budgets21
  • Memory Maps and Address Decode26
  • Interconnect and Bus Fabrics26
    • Topologies and Bridges
      • Shared Bus, Crossbar and Hierarchy
      • Inside the AXI to APB Bridge
      • Bridge Costs and Where Bridges Belong
      • Register Slices and Timing Closure
    • AXI Integration Details
    • Ordering and Deadlock at the Top
    • QoS and Arbitration
    • NoC Basics
    • Coherent Interconnect View
    • Firewalls and Protection
    • Bandwidth and Latency Verification
  • Clock and Reset Architecture24
  • Power Architecture21
  • Interrupts, DMA and Peripherals15
  • Memories, Boot and Security21
  • IP Integration and Handoff21
  • Top-Level Verification and Bring-up21
  • Package, Pads and I/O18
  • Chip Signoff and Tapeout18
  • SoC Interview Prep16
  • Capstone: Integrate NanoSoC14
Loading lesson

←Topologies and Bridges
  1. Topics
  2. SoC Design
  3. Interconnect and Bus Fabrics
  4. Topologies and Bridges
  5. Inside the AXI to APB Bridge
On this page

    Part 4 of 14, lesson 2 of 26Part 4 of 14/Interconnect and Bus Fabrics/Lesson 2 of 26All levels

    Inside the AXI to APB Bridge

    How the axi2apb bridge turns one AXI beat into a 3-clock APB access, and what the apb_cdc adds.

    Why it matters

    A driver writes the DMA CTRL register and the write reaches the register five system clocks after the bridge accepts it, not one. The NanoSoC axi2apb bridge returns the write response (B) only after the APB access has completed (it is non-posted), so the instruction after the store does not run for about 125 ns for a clk_sys register and 281.25 ns for a clk_per register, the same path lengths as the reads. A reader who believes the fabric is a wire will be surprised that a store costs that much; a reader who knows the bridge is non-posted can rely on it: when the store retires, the register has the value. This lesson traces one write and one read through the bridges between the CPU and a peripheral register, so every later latency number has a visible cause.

    The core idea

    The axi2apb bridge accepts one AXI beat, runs a normal APB access, then answers the AXI side. For a write it returns B only after the APB access completes, so software that waits for the store waits for the register. A normal access with the DMA-style slave takes three peripheral clocks: setup with psel high, one access cycle with penable high and pready low, and a second access cycle with pready high. The bridge adds one clock to accept the AXI beat and one to present the response, so a register on the 96 MHz APB segment costs five system clocks per 4-byte access: 4 bytes times 96 MHz divided by 5 gives 76.8 MB/s sustained, against 128.0 MB/s for the bus alone. The apb_cdc then carries the five slow peripherals to 24 MHz with a request and acknowledge handshake: two peripheral clocks to synchronise the request, two for the access, two system clocks to synchronise the acknowledge back, and two system clocks for the bridge to accept and respond: 208.33 ns in total, which is 4 bytes per 208.33 ns, 19.2 MB/s. Without the bridge's own accept and respond clocks the same path would take 187.5 ns.

    Two rules complete the contract. APB3 has no PSTRB and the DMA accepts only full 32-bit writes, so the bridge answers an AXI write whose WSTRB is not 0xF with SLVERR and does not touch the register (the alternative, promoting a byte write to a 32-bit write, would corrupt the neighbouring bytes of a register that cannot be read-modified safely). An AXI burst longer than one beat is split into one APB access per beat with the address stepped by four, and the bridge returns one B after the last beat or one R beat per access; INCR bursts up to 16 beats are possible on S3, and the cost is 5 clk_sys per beat on the fast segment. These two rules are NanoSoC choices stated here, because the protocol leaves them to the bridge designer.

    The waveform below is simulation output for the wave source of this topic: one write and one read on the APB segment, each with one wait state. One slot is one pclk cycle.

    How to read it: psel rises with pwrite high for the write setup while penable stays low; penable rises next cycle with pready still low for the wait cycle; pready rises and the write completes, then all three fall for one idle cycle. The same shape repeats with pwrite low for the read. pwrite stays high in the idle slot between the write and the read, which is not a glitch: nothing samples it while psel is low, and it changes with the next setup. pslverr stays low throughout because both accesses hit a mapped offset; an unmapped offset would raise it in the completing cycle instead of data. The waveform is the APB side only: the bridge's accept and respond clocks (the five clocks of the text) are not drawn on it.

    What an AHB hop would add

    NanoSoC has no AHB layer: the AXI4 fabric reaches APB directly. A chain AXI to AHB to APB, common where an IP block only speaks AHB or where an older peripheral subsystem is reused, adds a second protocol conversion. Each conversion adds its own accept and respond clocks (illustrative: two more clk_sys, which would take the 125 ns fast-segment read to 145.8 ns), its own burst handling (AHB bursts and HREADY wait states instead of AXI beats), and one more place where an error response can be dropped. The review rule is the same as for any bridge: state the clocks per hop, whether writes are posted, and how the error response travels. The protocol itself is in the AHB lesson of the protocol course.

    A bridge skeleton that honours pready

    The skeleton below is the APB half of the bridge: it has no AXI side (no ID capture for BID and RID, no WLAST handling), so the AXI half only has to hand it one request at a time. It holds the whole APB contract in one module: accept one request, run setup then access, hold penable through wait states, answer exactly once, and give up after a timeout. A bridge that assumes zero-wait access or drops the error bit is a classic APB integration failure, and this shape is its fix. The testbench below the module drives a one-wait-state slave, an unmapped offset and a dead slave, and fails if the wait state is not held, if pslverr does not reach err, or if the watchdog does not end the dead access (the testbench uses a 16-cycle timeout so the run is short).

    nano_axi2apb_skel.sv
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    // NanoSoC teaching values: the APB half of the bridge, one request at a time.
    // Holds penable until pready, propagates pslverr as err, answers once, and
    // gives up after TIMEOUT clk_sys cycles (the watchdog) with err set.
    module nano_axi2apb_skel #(
      parameter int TIMEOUT = 256
    ) (
      input  logic        aclk,
      input  logic        aresetn,
      input  logic        req,
      input  logic        wr,
      input  logic [11:0] addr,
      input  logic [31:0] wdata,
      output logic        ack,
      output logic [31:0] rdata,
      output logic        err,
      output logic        psel,
      output logic        penable,
      output logic        pwrite,
      output logic [11:0] paddr,
      output logic [31:0] pwdata,
      input  logic [31:0] prdata,
      input  logic        pready,
      input  logic        pslverr
    );
      typedef enum logic [1:0] {S_IDLE, S_SETUP, S_ACCESS, S_RESP} state_t;
      state_t st;
      logic [8:0] wd; // watchdog counter: counts access cycles, reset on every request
    
      always_ff @(posedge aclk or negedge aresetn) begin
        if (!aresetn) begin
          st <= S_IDLE;
          rdata <= 32'h0;
          err <= 1'b0;
          psel <= 1'b0;
          penable <= 1'b0;
          pwrite <= 1'b0;
          paddr <= 12'h0;
          pwdata <= 32'h0;
          wd <= 9'd0;
        end else case (st)
          S_IDLE: if (req) begin
            psel <= 1'b1;
            pwrite <= wr;
            paddr <= addr;
            pwdata <= wdata;
            wd <= 9'd0;
            st <= S_SETUP;
          end
          S_SETUP: begin
            penable <= 1'b1;
            st <= S_ACCESS;
          end
          S_ACCESS: begin
            wd <= wd + 9'd1;
            if (pready) begin
              penable <= 1'b0;
              psel <= 1'b0;
              rdata <= prdata;
              err <= pslverr;
              st <= S_RESP;
            end else if (wd == TIMEOUT - 1) begin
              // watchdog: the slave never answered (for example its clock is gated)
              penable <= 1'b0;
              psel <= 1'b0;
              rdata <= 32'h0;
              err <= 1'b1;
              st <= S_RESP;
            end
          end
          S_RESP: st <= S_IDLE;
          default: st <= S_IDLE;
        endcase
      end
    
      // ack is a single-cycle pulse in the response state.
      always_ff @(posedge aclk or negedge aresetn) begin
        if (!aresetn) ack <= 1'b0;
        else ack <= (st == S_RESP);
      end
    endmodule
    
    // ---- self-checking testbench: one wait state, an error offset, a dead slave ----
    module tb;
      logic aclk = 0, aresetn = 0;
      always #5 aclk = ~aclk;
      logic req = 0, wr = 0;
      logic [11:0] addr = 0;
      logic [31:0] wdata = 0;
      wire ack, err, psel, penable, pwrite;
      wire [31:0] rdata, pwdata;
      wire [11:0] paddr;
      logic dead = 0, waited = 0;
      wire pready = psel && penable && waited && !dead;
      wire pslverr = pready && (paddr == 12'h014);       // unmapped offset
      wire [31:0] prdata = {20'hCAFE0, paddr};
    
      nano_axi2apb_skel #(.TIMEOUT(16)) dut (.aclk(aclk), .aresetn(aresetn), .req(req), .wr(wr),
        .addr(addr), .wdata(wdata), .ack(ack), .rdata(rdata), .err(err), .psel(psel),
        .penable(penable), .pwrite(pwrite), .paddr(paddr), .pwdata(pwdata),
        .prdata(prdata), .pready(pready), .pslverr(pslverr));
    
      always_ff @(posedge aclk) waited <= psel && penable && !waited;
    
      int errors = 0;
      task automatic access(input bit w, input [11:0] a, input [31:0] d, output int cycles);
        @(negedge aclk); req = 1; wr = w; addr = a; wdata = d;
        @(negedge aclk); req = 0;
        cycles = 1;
        while (!ack && cycles < 100) begin @(negedge aclk); cycles++; end
      endtask
    
      int c;
      initial begin
        repeat (3) @(negedge aclk); aresetn = 1;
        access(1, 12'h004, 32'h1234, c);                  // write, one wait state
        if (err || pwdata !== 32'h1234 || c < 5) begin errors++; $display("FAIL write err=%0d c=%0d", err, c); end
        access(0, 12'h008, 0, c);                         // read
        if (err || rdata !== 32'hCAFE0008) begin errors++; $display("FAIL read %h", rdata); end
        access(0, 12'h014, 0, c);                         // unmapped: pslverr must reach err
        if (!err) begin errors++; $display("FAIL error not propagated"); end
        dead = 1;
        access(0, 12'h008, 0, c);                         // slave never answers: watchdog
        if (!err || rdata !== 0 || c > 16 + 6) begin errors++; $display("FAIL watchdog err=%0d c=%0d", err, c); end
        if (errors == 0) $display("PASS: wait state held, pslverr propagated, watchdog ends a dead access");
        $finish;
      end
      initial #20000 begin $display("FAIL timeout"); $finish; end
    endmodule

    What the review looks for

    • The bridge specification states the clocks-per-access for every path, including the accept and respond cycles, not just the bus cycles.

    • Wait-state handling is explicit: penable held until pready, with a watchdog (256 clk_sys cycles on NanoSoC) that is longer than the slowest documented slave access, 20 clk_sys at the bridge.

    • The error path is drawn end to end: PSLVERR from the APB slave becomes SLVERR on AXI, never silence. The bridge states what it does with partial strobes and bursts, and whether B is posted or not.

    Verify and debug

    Randomise wait states in the APB slaves during integration simulation: a bridge that passes with zero-wait models and fails with one wait state ignores PREADY. Force PSLVERR on an unmapped offset and confirm SLVERR arrives on AXI with the right beat; a dropped error here corrupts silently. Then confirm the five-clock and 208.33 ns figures with a timestamped back-to-back access pair.

    APB bridge ignores PREADY or PSLVERR: the bridge assumes zero-wait access or drops the error, so a slow peripheral returns stale data and errors vanish. Symptom is wrong data with no error flag; caught by wait-state randomisation and error-injection tests; fixed by honouring pready and propagating pslverr, as the skeleton does.

    If asked how long an APB access takes, never answer with the bus cycles alone. Count accept, setup, access plus waits, and respond, then convert to nanoseconds in the right domain. The NanoSoC answer is five 96 MHz clocks for the fast segment. If asked whether the bridge is posted, say what the response means: B after PREADY means the register has the value.

    Check yourself A read of a clk_per register returns the old value right after a write to it from the CPU, although the NanoSoC bridge is non-posted. What would explain it?

    A bridge that returns B early (a posted write) lets the read overtake the write, or the read came from a path that did not wait for B. Return B only after PREADY, as the NanoSoC bridge does, or read the register back.

    Related topics

    • Shared Bus, Crossbar and Hierarchy

    • Next: Bridge Costs and Where Bridges Belong

    • Protocol First Principles: Bridges and Protocol Conversion

    • Protocol First Principles: AHB

    • Protocols: AMBA Integration Debug

    Previous

    60 / 289

    Next: Bridge Costs and Where Bridges Belong

    Up next

    Up nextBridge Costs and Where Bridges Belong

    Sign in to save progress

    Practice this lessonPrevious lessonReport an error

    See also

    • Register Slices and Timing Closure
    pclkpselpenablepwritepreadypslverr
    Simulation output: an APB write then read, each taking three clocks through setup, access with wait, and access with ready