Read first: Handshake & Backpressure: distinguish an offered operation from an accepted one
The Advanced Peripheral Bus (APB) carries one selected peripheral access at a time. This lesson uses APB4, Arm IHI 0024C, sections 3.1–3.4 and 4.1. APB_BRIDGE requests transfers and DMA_CFG responds on the course's 32-bit peripheral path. PCLK is clk_per at 50 MHz; all rows below show signals sampled at its rising edges, outside reset.
SETUP occupies one cycle with PSEL=1 and PENABLE=0. ACCESS follows with both high; the access completes only at an edge where PSEL && PENABLE && PREADY is true. PREADY=0 extends ACCESS. APB has separate read and write data buses but one transfer handshake, so they do not complete independent concurrent accesses. A minimum transfer uses one SETUP cycle and one ACCESS cycle.
Keep the selected target, address, direction and protection fields unchanged from SETUP through ACCESS completion. For a write, retain the active write-data bytes and write strobes as well. APB4 PSTRB has one bit per byte lane; the examples use 1111 for a full 32-bit write and 0000 for reads. PPROT remains 000 in these teaching traces. PREADY may already be high during SETUP; that alone completes nothing.
APB4 phase and completion rules
Phase | PSEL | PENABLE | PREADY | At the sampled edge |
|---|---|---|---|---|
IDLE | 0 | 0 | Not used | No selected access |
SETUP | 1 | 0 | Not used | No completion; move to ACCESS |
ACCESS wait | 1 | 1 | 0 | No completion; retain this access |
ACCESS finish | 1 | 1 | 1 | Complete; next cycle is SETUP or IDLE |
What to notice
SETUP must be followed by ACCESS even when PREADY is already high.
PREADY extends the access phase, rather than replacing APB's setup/access sequencing with an AXI handshake.
No. Those are SETUP signals. The write completes only on an ACCESS edge with PSEL, PENABLE and PREADY all high.
Before E1, DMA_CFG.LEN_BYTES at 0x4000_0008 is 0. The bridge requests a full-word write of 0x0000_0010, meaning 16 bytes. In this isolated register exercise the target updates LEN_BYTES only at a successful completed write; no other agent writes it and no reset occurs. PADDR stays 0x4000_0008, PWRITE=1, PWDATA=0x0000_0010, PSTRB=1111 and PPROT=000 from SETUP through E4.
The target inserts two ACCESS waits at E2 and E3. Seeing the intended data on PWDATA does not mean the register has changed. At E4 the completion condition is true with PSLVERR=0, so the target commits the write once. Each row's register value is the result after that edge.
Waited LEN_BYTES write
PREADY is shown high during SETUP to demonstrate that completion still requires PENABLE. PSLVERR is 0 on the final edge.
Edge | PSEL / PENABLE / PREADY | Phase | Completed write | LEN_BYTES after edge |
|---|---|---|---|---|
E1 | 1 / 0 / 1 | SETUP | No | 0x0000_0000 |
E2 | 1 / 1 / 0 | ACCESS wait | No | 0x0000_0000 |
E3 | 1 / 1 / 0 | ACCESS wait | No | 0x0000_0000 |
E4 | 1 / 1 / 1 | ACCESS finish | Yes, once | 0x0000_0010 |
What to notice: PSEL starts the access and PENABLE distinguishes ACCESS from SETUP. Follow PREADY through the waits, then compare the later write_commits and len_bytes values. injected_change is the separate faulty observation; it is not the real target's PWDATA.
Only PSEL && PENABLE && PREADY completes the write. SETUP and PREADY-low ACCESS cycles do not commit it; write_commits advances once.
What to notice
The requested value stays stable through the two waits and the completing edge.
Two waiting edges and one completing ACCESS edge are one write, not three writes.
No. The original write is still waiting, so its write data must remain 0x10 through E4. A new value needs a separate SETUP/ACCESS transfer.
The source below models the local 32-bit APB register path at 50 MHz. wait_cycles and reject_access configure the exercise target; they are not APB signals. The bench writes LEN_BYTES through two waits, reads it back, completes a deliberately rejected STATUS read, then checks another write/read pair. A second observation changes PWDATA during the wait so the hold checker must flag it. The error example drives PSLVERR low until completion, unlike the earlier legal trace that illustrated an ignored high value before completion. The eight-cycle watchdog is a bench budget, not an APB response deadline.
// APB4 register exercise. wait_cycles and reject_access are test configuration,
// not APB wires. This models LEN_BYTES and a deliberately rejected STATUS read.
`timescale 1ns/1ps
module apb_register (
input wire clk,
input wire rst,
input wire psel,
input wire penable,
input wire pwrite,
input wire [31:0] paddr,
input wire [31:0] pwdata,
input wire [3:0] pstrb,
input wire [1:0] wait_cycles,
input wire reject_access,
output wire pready,
output wire pslverr,
output wire [31:0] prdata,
output reg [31:0] len_bytes,
output integer write_commits
);
localparam [31:0] LEN_ADDR = 32'h4000_0008;
reg [1:0] wait_left;
assign pready = !penable || wait_left == 0;
assign pslverr = psel && penable && pready && reject_access;
assign prdata = paddr == LEN_ADDR ? len_bytes : 32'h0000_0000;
always @(posedge clk) begin
if (rst) begin
wait_left <= 0;
len_bytes <= 0;
write_commits <= 0;
end else begin
if (psel && !penable) wait_left <= wait_cycles;
else if (psel && penable && !pready) wait_left <= wait_left - 1'b1;
if (psel && penable && pready && pwrite && !pslverr && paddr == LEN_ADDR) begin
for (integer lane = 0; lane < 4; lane = lane + 1)
if (pstrb[lane]) len_bytes[8*lane +: 8] <= pwdata[8*lane +: 8];
write_commits <= write_commits + 1;
end
end
end
endmodule
// Check the held request from SETUP through the completing ACCESS edge.
module apb_hold_checker (
input wire clk,
input wire rst,
input wire psel,
input wire penable,
input wire pready,
input wire pwrite,
input wire [31:0] paddr,
input wire [31:0] pwdata,
input wire [3:0] pstrb,
input wire [2:0] pprot,
output reg changed_request
);
reg holding;
reg saved_write;
reg [31:0] saved_addr, saved_data;
reg [3:0] saved_strb;
reg [2:0] saved_prot;
always @(posedge clk) begin
if (rst) begin
holding <= 0;
changed_request <= 0;
end else begin
if (holding) begin
if (!psel || !penable || pwrite !== saved_write ||
paddr !== saved_addr || pprot !== saved_prot ||
(saved_write && (pwdata !== saved_data || pstrb !== saved_strb)))
changed_request <= 1;
end
holding <= psel && !(penable && pready);
saved_write <= pwrite;
saved_addr <= paddr;
saved_data <= pwdata;
saved_strb <= pstrb;
saved_prot <= pprot;
end
end
endmodule
module tb;
reg clk = 0;
always #10 clk = ~clk; // clk_per = 50 MHz
reg rst = 1;
reg psel = 0, penable = 0, pwrite = 0;
reg [31:0] paddr = 0, pwdata = 0;
reg [3:0] pstrb = 0;
reg [2:0] pprot = 0;
reg [1:0] wait_cycles = 0;
reg reject_access = 0;
reg corrupt_wait = 0;
wire pready, pslverr;
wire [31:0] prdata, len_bytes;
wire changed_request, injected_change;
wire [31:0] faulty_observation = corrupt_wait && penable && !pready ? pwdata ^ 32'h1 : pwdata;
integer write_commits;
integer completed = 0;
reg [31:0] expected_len = 0;
apb_register target (
.clk(clk), .rst(rst), .psel(psel), .penable(penable), .pwrite(pwrite),
.paddr(paddr), .pwdata(pwdata), .pstrb(pstrb), .wait_cycles(wait_cycles),
.reject_access(reject_access), .pready(pready), .pslverr(pslverr),
.prdata(prdata), .len_bytes(len_bytes), .write_commits(write_commits)
);
apb_hold_checker good_check (
.clk(clk), .rst(rst), .psel(psel), .penable(penable), .pready(pready),
.pwrite(pwrite), .paddr(paddr), .pwdata(pwdata), .pstrb(pstrb),
.pprot(pprot), .changed_request(changed_request)
);
apb_hold_checker fault_check (
.clk(clk), .rst(rst), .psel(psel), .penable(penable), .pready(pready),
.pwrite(pwrite), .paddr(paddr), .pwdata(faulty_observation), .pstrb(pstrb),
.pprot(pprot), .changed_request(injected_change)
);
always @(posedge clk) begin
if (rst) expected_len <= 0;
else begin
if (len_bytes !== expected_len) begin
$display("FAIL: LEN_BYTES changed before completion or has an incorrect value");
$finish;
end
if (psel && penable && pready && pwrite && !pslverr && paddr == 32'h4000_0008)
for (integer lane = 0; lane < 4; lane = lane + 1)
if (pstrb[lane]) expected_len[8*lane +: 8] <= pwdata[8*lane +: 8];
end
end
task access_register(
input wr,
input [31:0] addr,
input [31:0] data,
input [1:0] waits,
input rejected,
input [31:0] expected_read
);
begin
@(negedge clk);
psel = 1; penable = 0; pwrite = wr;
paddr = addr; pwdata = data; pstrb = wr ? 4'b1111 : 4'b0000;
wait_cycles = waits; reject_access = rejected;
@(negedge clk); penable = 1;
begin : completion
// Eight cycles is this bench's watchdog budget, not an APB deadline.
for (integer cycle = 0; cycle < 8; cycle = cycle + 1) begin
@(posedge clk);
if (pready) begin
if (pslverr !== rejected || (!wr && !rejected && prdata !== expected_read)) begin
$display("FAIL: wrong APB completion response or read data");
$finish;
end
completed = completed + 1;
#1;
disable completion;
end
end
$display("FAIL: APB exercise exceeded its stated watchdog budget");
$finish;
end
end
endtask
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, tb);
repeat (2) @(posedge clk);
@(negedge clk); rst = 0;
corrupt_wait = 1;
access_register(1, 32'h4000_0008, 32'h10, 2, 0, 0);
corrupt_wait = 0;
access_register(0, 32'h4000_0008, 0, 0, 0, 32'h10);
access_register(0, 32'h4000_0010, 0, 1, 1, 0);
access_register(1, 32'h4000_0008, 32'h20, 0, 0, 0);
access_register(0, 32'h4000_0008, 0, 0, 0, 32'h20);
@(negedge clk); psel = 0; penable = 0;
if (changed_request || !injected_change || completed != 5 ||
write_commits != 2 || len_bytes !== 32'h20) begin
$display("FAIL: good hold, injected hold fault, transaction count or register state mismatch");
$finish;
end
$display("PASS: waited write, back-to-back read and error completed; changed waiting request detected");
$finish;
end
endmoduleContinue the same register exercise after E4. A read of LEN_BYTES can follow immediately: keep this target's PSEL high, lower PENABLE for the read's SETUP cycle, set PWRITE=0 and PSTRB=0000, then enter ACCESS. PADDR and PPROT remain unchanged. The target supplies PRDATA=0x0000_0010 by the successful completing read edge E6.
An idle cycle between transfers is optional; a SETUP cycle for the next transfer is required. PRDATA before a successful read completion is not evidence that the read has completed. The timing rows do not show the upstream AXI request or bridge clock crossing: E5/E6 are clk_per edges, not clk_sys edges.
Back-to-back read after the E4 write
LEN_BYTES remains 0x10 throughout. The read has no wait states and PSLVERR=0 at completion.
Edge | PSEL / PENABLE / PREADY | PWRITE | PRDATA significance | Completion |
|---|---|---|---|---|
E5 | 1 / 0 / 1 | 0 | Not sampled as a result | None: read SETUP |
E6 | 1 / 1 / 1 | 0 | 0x0000_0010 | Read returns 0x10 |
What to notice: Do not use continuous PSEL as the transfer counter. Locate the low PENABLE SETUP sample and the subsequent ACCESS completion, then read PRDATA at that completion.
No. The next access still has a PENABLE-low SETUP phase. Continuous target selection does not merge two transfers.
What to notice
PSEL can remain high between accesses to the same target; PENABLE must return low for the new SETUP phase.
The returned value matches the most recent completed write in this isolated example.
No. E5 is the new read's SETUP cycle with PENABLE=0. The earliest no-wait completion is E6 after entering ACCESS.
PSLVERR reports an error only at a completing ACCESS edge. For this separate exercise, select DMA_CFG.STATUS at 0x4000_0010 with PWRITE=0, PSTRB=0000 and PPROT=000, and assume the target rejects that read. The rejection is an exercise condition, not a new STATUS register rule. Use fresh edges F1–F3 on clk_per, outside reset.
The access completes with an error at F3; it is no longer waiting. Its PRDATA is not a valid successful-read result. A write error is not a general rollback guarantee either: peripheral state effects depend on the target contract. PSLVERR support is optional in APB4; a target without it connects a low error indication to the requester. The trace includes PSLVERR explicitly.
Sample the error at completion
The target happens to drive PSLVERR high before completion. Only its value at the completing ACCESS edge determines this transfer's error response.
Edge | PSEL / PENABLE / PREADY | PSLVERR | Phase | Result |
|---|---|---|---|---|
F1 | 1 / 0 / 1 | 1 | SETUP | No error sampled yet |
F2 | 1 / 1 / 0 | 1 | ACCESS wait | No completion |
F3 | 1 / 1 / 1 | 1 | ACCESS finish | Completed with error |
What to notice: Find the sample where PENABLE, PSEL and PREADY are all high, then inspect PSLVERR there. A completed error is different from an unfinished wait; this target leaves LEN_BYTES unchanged for the rejected read.
No. This transfer has completed with an error. Software or an upstream bridge handles that outcome under its own recovery contract.
What to notice
A high PSLVERR during SETUP or a wait is not the sampled completion response.
An error completion and an access stuck with PREADY=0 are different observations.
No. F3 already completed the access with an error. The next cycle moves to SETUP for another transfer or to IDLE; the requester handles the completed error response.
This comparison uses AMBA 3 AHB-Lite v1.0, Arm IHI 0033A, sections 3.1, 3.2, 3.6 and chapters 4–5. A transfer has an address/control phase followed by a data phase. While A is in its data phase, the bus can present B's address. The data-phase target and request context therefore come from an earlier accepted address, not from the current HADDR alone.
For the following isolated 32-bit AHB-Lite read example, HCLK=100 MHz and reset is released before E1. The address space uses the course's SRAM0 range, but this comparison does not add an AHB connection to the base AXI system. Initialise words at 0x2000_0000 and 0x2000_0004 to 0x0000_0011 and 0x0000_0022 respectively; no writes occur. A and B are independent word reads with HWRITE=0, HSIZE=2 and HBURST=SINGLE. Each starts with HTRANS=NONSEQ; these are not beats of one burst.
At this selected target, a valid address phase advances at an edge with HSEL=1, HTRANS[1]=1 and HREADY=1. On the same edge, any previously active data phase completes with its HRESP status. Before E1 there is no active data phase, so the first ready edge accepts A but returns no read. IDLE on the address phase does not cancel a data phase already in progress.
Two independent reads without waits
HRESP is OKAY and HSEL=1 throughout. A addresses 0x2000_0000; B addresses 0x2000_0004. Each row shows pre-edge bus signals and events at that edge.
Edge | Address phase | HREADY | Data phase at this edge | Result |
|---|---|---|---|---|
E1 | A / NONSEQ | 1 | None | Accept A address |
E2 | B / NONSEQ | 1 | A: HRDATA=0x11 | Complete A; accept B address |
E3 | IDLE | 1 | B: HRDATA=0x22 | Complete B; accept no new read |
What to notice: Compare accepted with completed, not HADDR alone with HRDATA. The pipeline retains the accepted address for the following data phase; both comparison observers agree in this no-wait replay.
The visible address can be the next address phase while data completes for the saved previous request. Match HRDATA to that saved context.
What to notice
At E2 the visible address is B, but the returned data is for A.
E3 completes B even though the address-phase HTRANS is IDLE.
No. HRDATA belongs to the saved data-phase context for A. Compare 0x11 with A's expected data, then retain B's accepted address for the next data phase.
Restart from the same initial words with no active transfer. This time A's target inserts two ordinary OKAY waits. B is visible in the address phase at E2 but HREADY=0, so B is not yet accepted. Keep B's NONSEQ request and address/control stable through E4 in this trace. A completes and B advances together at E4; B returns its own data at E5.
HREADYOUT is a target's completion output. Interconnect selection for the active data phase determines the HREADY seen by the bus; a newly decoded address must not steal the previous transfer's response. Track accepted address context and the selected data-phase target across waits. The ordinary NONSEQ example here does not cover the specification's IDLE/BUSY or ERROR exceptions to address/control changes.
A waits while B remains offered
Independent replay; HSEL=1 and HRESP=OKAY. A and B use the same addresses and values as above. HRDATA need only be treated as a successful result at completion.
Edge | Address phase | HREADY | Data phase | Events |
|---|---|---|---|---|
E1 | A / NONSEQ | 1 | None | Accept A address |
E2 | B / NONSEQ | 0 | A waiting | No completion; B not accepted |
E3 | B / NONSEQ | 0 | A waiting | No completion; B not accepted |
E4 | B / NONSEQ | 1 | A returns 0x11 | Complete A; accept B address |
E5 | IDLE | 1 | B returns 0x22 | Complete B |
What to notice: Follow the HREADY-low samples. The correct mismatch signal stays clear; faulty_mismatch identifies the observer that replaced the saved context too soon. Only HREADY-high edges permit the shown progression.
The faulty observer. Its mismatch flag rises when it compares the completing data against the premature replacement; the correct observer retains the old context through the wait.
What to notice
B appears on three sampled edges, but its address is accepted once, at E4.
An observer that overwrites the data-phase context from HADDR at E2/E3 will associate A's data with B.
One, at E4. HREADY is low at E2/E3, so those edges extend A's data phase and B's address phase rather than accepting additional requests.
The isolated source models only the two word reads above, with OKAY responses. A no-wait replay passes both observers. In the waited replay the faulty observer overwrites its saved address from B while A is still waiting; it must detect the resulting mismatch when A completes. The correct observer preserves A until that edge, then accepts B once. next_wait_cycles configures the target; it is not an AHB wire. HREADYOUT is connected directly to HREADY because this model has one target, with no fabric response multiplexor.
// Isolated read-only AHB-Lite teaching subset: SINGLE/NONSEQ word reads,
// one selected target, fixed initial words, OKAY responses and no reset in traffic.
`timescale 1ns/1ps
module ahb_read_target (
input wire clk,
input wire rst,
input wire hsel,
input wire [1:0] htrans,
input wire [31:0] haddr,
input wire [1:0] next_wait_cycles,
output wire hreadyout,
output wire [31:0] hrdata
);
reg active;
reg [31:0] data_addr;
reg [1:0] wait_left;
assign hreadyout = !active || wait_left == 0;
assign hrdata = data_addr == 32'h2000_0000 ? 32'h11 :
data_addr == 32'h2000_0004 ? 32'h22 : 32'h0;
always @(posedge clk) begin
if (rst) begin
active <= 0;
data_addr <= 0;
wait_left <= 0;
end else if (hreadyout) begin
active <= hsel && htrans[1];
if (hsel && htrans[1]) begin
data_addr <= haddr;
wait_left <= next_wait_cycles;
end
end else wait_left <= wait_left - 1'b1;
end
endmodule
module ahb_read_observer #(parameter IGNORE_READY = 0) (
input wire clk,
input wire rst,
input wire hsel,
input wire [1:0] htrans,
input wire [31:0] haddr,
input wire hready,
input wire [31:0] hrdata,
output reg mismatch,
output integer accepted,
output integer completed
);
reg pending;
reg [31:0] expected_addr;
wire [31:0] expected_word = expected_addr == 32'h2000_0000 ? 32'h11 :
expected_addr == 32'h2000_0004 ? 32'h22 : 32'h0;
always @(posedge clk) begin
if (rst) begin
pending <= 0;
expected_addr <= 0;
accepted <= 0;
completed <= 0;
mismatch <= 0;
end else begin
if (hready) begin
// Compare the old data context before accepting a new address.
if (pending) begin
completed <= completed + 1;
if (hrdata !== expected_word) mismatch <= 1;
end
pending <= hsel && htrans[1];
if (hsel && htrans[1]) begin
accepted <= accepted + 1;
expected_addr <= haddr;
end
end
// Fault: overwrite context from an offered, unaccepted next address.
if (IGNORE_READY && hsel && htrans[1]) expected_addr <= haddr;
end
end
endmodule
module tb;
reg clk = 0;
always #5 clk = ~clk; // isolated HCLK = 100 MHz
reg rst = 1;
reg hsel = 0;
reg [1:0] htrans = 0;
reg [31:0] haddr = 32'h2000_0000;
reg [1:0] next_wait_cycles = 0; // target configuration, not an AHB signal
wire hreadyout;
// One target only: this explicit connection stands in for data-phase mux selection.
wire hready = hreadyout;
wire [31:0] hrdata;
wire mismatch, faulty_mismatch;
integer accepted, completed, faulty_accepted, faulty_completed;
ahb_read_target target (
.clk(clk), .rst(rst), .hsel(hsel), .htrans(htrans), .haddr(haddr),
.next_wait_cycles(next_wait_cycles), .hreadyout(hreadyout), .hrdata(hrdata)
);
ahb_read_observer #(.IGNORE_READY(0)) good (
.clk(clk), .rst(rst), .hsel(hsel), .htrans(htrans), .haddr(haddr),
.hready(hready), .hrdata(hrdata), .mismatch(mismatch),
.accepted(accepted), .completed(completed)
);
ahb_read_observer #(.IGNORE_READY(1)) bad (
.clk(clk), .rst(rst), .hsel(hsel), .htrans(htrans), .haddr(haddr),
.hready(hready), .hrdata(hrdata), .mismatch(faulty_mismatch),
.accepted(faulty_accepted), .completed(faulty_completed)
);
task drive_address(input [31:0] addr, input active_address, input [1:0] waits);
begin
@(negedge clk);
haddr = addr;
hsel = active_address;
htrans = active_address ? 2'b10 : 2'b00;
next_wait_cycles = waits;
end
endtask
initial begin
$dumpfile("wave.vcd");
$dumpvars(0, tb);
repeat (2) @(posedge clk);
@(negedge clk); rst = 0;
// No-wait replay: complete A while accepting B, then finish B during IDLE.
drive_address(32'h2000_0000, 1, 0);
drive_address(32'h2000_0004, 1, 0);
drive_address(32'h2000_0004, 0, 0);
@(negedge clk);
if (mismatch || faulty_mismatch || accepted != 2 || completed != 2) begin
$display("FAIL: no-wait A/B association or event counts were wrong");
$finish;
end
// Fresh replay with the same immutable words and no in-flight reset.
@(negedge clk); rst = 1; hsel = 0; htrans = 0;
@(negedge clk); rst = 0;
drive_address(32'h2000_0000, 1, 2);
drive_address(32'h2000_0004, 1, 0);
drive_address(32'h2000_0004, 1, 0);
drive_address(32'h2000_0004, 1, 0);
drive_address(32'h2000_0004, 0, 0);
repeat (2) @(negedge clk);
if (mismatch || !faulty_mismatch || accepted != 2 || completed != 2 ||
faulty_accepted != 2 || faulty_completed != 2 || target.active) begin
$display("FAIL: good association, waiting-context fault or drain counts were wrong");
$finish;
end
$display("PASS: A/B reads preserved across two waits; premature context replacement detected");
$finish;
end
endmoduleAHB-Lite has OKAY and ERROR responses; SPLIT and RETRY belong to the older full AHB protocol. With HRESP=OKAY, HREADY distinguishes an ongoing data wait from a successful completion. The named AHB-Lite revision uses a two-cycle ERROR response: HRESP=1/HREADY=0 first, then HRESP=1/HREADY=1 to end the failed transfer. Extra ordinary waits, if needed, precede that pair with HRESP=0.
The ERROR sequence gives the requester time to cancel the following pipelined address phase. Check the selected revision's exception rules rather than applying an unconditional 'all signals stay fixed whenever HREADY=0' rule. An undecoded active NONSEQ/SEQ address uses a default target returning ERROR; an IDLE/BUSY decode miss is not the same request.
AHB-Lite response meanings for an active data phase
HRESP | HREADY | Meaning |
|---|---|---|
0 / OKAY | 0 | Ordinary data wait; no completion |
0 / OKAY | 1 | Successful data completion |
1 / ERROR | 0 | First ERROR cycle |
1 / ERROR | 1 | Second ERROR cycle; failed transfer ends |
What to notice
HRESP=0 alone does not establish a completed successful transfer.
A completed ERROR is distinct from an ordinary wait and has a prescribed response sequence.
No. For IHI 0033A the ERROR response has two cycles: first HRESP=1/HREADY=0, then HRESP=1/HREADY=1. Do not copy APB's single completing-error edge into AHB-Lite.
APB completes only at PSEL && PENABLE && PREADY; setup and waits are not repeated register writes.
AHB-Lite returns data for saved address-phase context, which can differ from the address currently visible.
Sample response status with the completing event and obey each protocol's distinct error sequence.