AMBA AHB · Module 16
HRESP Generation
Generating correct slave responses — HRESP OKAY (low) for normal transfers, ERROR (high) for accesses the slave cannot or should not perform (reserved addresses, illegal access types, protection violations, unsupported sizes). The ERROR response is a mandatory two-cycle sequence (HRESP=ERROR both cycles, HREADYOUT low then high) so the pipelined master can cancel the next address-phase transfer it had already issued; an errored access must not change slave state. AHB-Lite uses only OKAY and ERROR.
The slave's outputs so far: HREADYOUT (the pace — 16.4) and HRDATA (the read data — 16.5). This chapter builds the last one: HRESP, the slave's response — did the transfer succeed? For a normal transfer the slave responds OKAY (HRESP low), completing in one cycle. But for an access the slave cannot or should not perform — a reserved/unmapped address, an illegal access type (write to read-only), a protection violation, an unsupported size — the slave must signal ERROR (HRESP high). The catch: ERROR is a mandatory two-cycle sequence, not a single cycle. In the first error cycle the slave drives HRESP = ERROR with HREADYOUT low (not yet completing); in the second, it keeps HRESP = ERROR and drives HREADYOUT high (completing, error reported). Why two cycles? Because the master is pipelined — by the time the slave signals the error, the master has already issued the next transfer's address phase; the two-cycle sequence gives the master a cycle to see the error coming and cancel that already-issued transfer before it commits (the two-cycle ERROR response). (AHB-Lite uses only OKAY and ERROR — the RETRY/SPLIT of full AHB aren't used — chapter 7.) And critically, an errored access must not change the slave's state. This chapter builds HRESP generation, the two-cycle ERROR FSM, and the do-not-corrupt-state rule.
1. What Is It?
HRESP generation is the slave logic that drives the HRESP response — OKAY for normal transfers, the two-cycle ERROR for faulted ones. Its parts:
- OKAY (
HRESPlow) — the normal response: the transfer succeeded; complete in one cycle (HREADYOUThigh). - ERROR (
HRESPhigh) — for accesses the slave can't/shouldn't do (reserved address, illegal type, protection violation, unsupported size). - The two-cycle ERROR — cycle 1:
HRESP = ERROR,HREADYOUTlow; cycle 2:HRESP = ERROR,HREADYOUThigh (completes). - No state change on error — an errored write must not update state; an errored read returns no meaningful data. The slave stays clean.
So HRESP generation is the slave's status report on each transfer. Most transfers are fine — the slave responds OKAY (HRESP low) and completes normally. But some accesses are invalid — a read/write to an address with nothing there (reserved), a write to a read-only register, an access forbidden by protection/privilege, a size the slave can't handle. For these, the slave must not silently complete (which would hide the fault, or corrupt state) — it must signal ERROR (HRESP high). And the ERROR response is specifically a two-cycle handshake — HREADYOUT low then high, HRESP held ERROR throughout — mandated by the protocol so the pipelined master can react (cancel its already-issued next transfer). Plus the invariant: an errored access leaves the slave's state unchanged (no corruption). HRESP generation is therefore the slave's correct, two-cycle error reporting — how a subordinate says "no" without corrupting itself.
2. Why Does It Exist?
HRESP exists because some bus accesses are invalid or impossible, and the slave must report that rather than silently failing — and the ERROR is two cycles because the pipelined bus requires the master a chance to unwind the transfer it has already started after the faulted one.
The errors must be reported is the root: not every access can succeed. The master might address a reserved location (nothing there), write a read-only register, attempt a forbidden (privilege/security) access, or request an unsupported size. If the slave silently completed such an access (responding OKAY), the fault would be hidden — the software would think a bad write succeeded, a read of nothing returned valid data, a forbidden access was allowed. That's dangerous — silent corruption, security holes, undetected bugs. So the slave must report the fault — HRESP = ERROR — so the master (and software) knows and can handle it (a bus-fault exception). So HRESP exists to surface invalid accesses rather than hiding them. So it's fault reporting. So errors must be visible.
The pipeline requires two cycles drives the ERROR timing: the bus is pipelined — the address phase of transfer N+1 overlaps the data phase of transfer N (chapter 3). So when the slave, in transfer N's data phase, detects an error, the master has already driven transfer N+1's address phase (it's committed to issuing N+1). If the error were a single cycle (complete immediately with ERROR), the master would have no clean way to cancel N+1 — it's already in flight. So the ERROR response takes two cycles: the first cycle (HREADYOUT low) signals the error while holding — giving the master a cycle to see HRESP = ERROR and decide to cancel N+1; the second cycle (HREADYOUT high) completes the errored transfer, and the master does not commit N+1 (it re-issues or abandons it). So the two cycles exist to let the pipelined master unwind the already-issued next transfer cleanly. So ERROR is two cycles because of pipelining. So the timing is structural.
The no-state-change invariant is why error handling is careful: an errored access must leave the slave's state unchanged — an errored write must not update the register/memory; an errored read must not trigger side effects (no FIFO pop, no clear). Because the access was invalid, performing it would corrupt state. So the slave must detect the error before committing the access and suppress the state change. So HRESP generation is coupled to suppressing the access. So HRESP exists because: invalid accesses must be reported, not hidden (the why — fault visibility, security, correctness); the pipelined bus needs two cycles so the master can cancel its already-issued next transfer (the timing — pipeline unwinding); and the errored access must not change slave state (the invariant — no corruption). So HRESP is the slave's correct failure mode — surfacing invalid accesses with a protocol-mandated two-cycle handshake that keeps the pipeline coherent, while leaving the slave uncorrupted. So this chapter builds the slave's "no" correctly. So errors are reported cleanly.
3. Mental Model
Model the two-cycle ERROR as a bank teller rejecting a transaction you handed over while you were already sliding the next form under the window. The teller can't just stamp "DENIED" and vanish — you've got another request in motion. So they first hold up a "PROBLEM — wait" card (cycle 1: error flagged, not done), giving you a beat to pull back the next form you'd already pushed; then they finalize the rejection and free the window (cycle 2: error completes), and you don't submit that next form. And crucially, a rejected transaction changes nothing in your account — no money moves.
A bank window where you and the teller work in a pipeline — while the teller processes your current request, you're already sliding the next form under the window (the next transfer's address phase, issued before the current one finishes). Now suppose your current request is invalid — you tried to withdraw from a closed account (a reserved address), or write to a statement-only record (a read-only register), or access an account you're not authorized for (a protection violation). The teller must reject it. But they can't just stamp "DENIED" and instantly slam the window — because you've already got the next form in motion, and slamming would leave that next request in a messy, half-submitted state. So the rejection takes two beats. Beat one: the teller holds up a "PROBLEM — please wait" card (HRESP = ERROR, HREADYOUT low) — flagging the error but not finalizing. This gives you a beat to notice the problem and pull back the next form you were sliding in (the master sees the error and prepares to cancel its already-issued next transfer). Beat two: the teller finalizes the rejection and frees the window (HRESP = ERROR held, HREADYOUT high — the errored transfer completes), and you don't submit that next form (the master doesn't commit N+1). And crucially: the rejected transaction changed nothing — no money moved, your balance is exactly as before (the errored access doesn't change the slave's state). If the teller had single-beat rejected, your half-slid next form would've jammed the window (the pipeline would be incoherent). The two beats exist precisely to let you cleanly pull back what you'd already started.
This captures the two-cycle ERROR: you sliding the next form while the current is processed = the pipelined master issuing N+1's address phase during N's data phase; the invalid request = a reserved/illegal/forbidden access; beat one, the "PROBLEM — wait" card = cycle 1 (HRESP = ERROR, HREADYOUT low — error flagged, not complete); you noticing and pulling back the next form = the master seeing the error and preparing to cancel N+1; beat two, finalizing and freeing the window = cycle 2 (HRESP = ERROR, HREADYOUT high — completes); you not submitting the next form = the master not committing N+1; no money moved = the errored access not changing slave state. Two beats so the pipeline unwinds cleanly, and the account stays untouched.
Watch the two-cycle ERROR and the master cancelling its next transfer:
The mandatory two-cycle ERROR response
4 cyclesThe model's lesson: two beats so the pipeline unwinds cleanly, and the account stays untouched. In the waveform, HRESP is ERROR for both error cycles, HREADYOUT goes low then high, the master cancels its already-issued next transfer, and the slave's state is unchanged.
4. Real Hardware Perspective
In hardware, HRESP is a single output driven OKAY by default and ERROR by a small FSM (the two-cycle sequence) when an error condition is detected; the error detection is combinational logic over the captured access; and the state-update logic is gated to suppress changes on error.
The error detection: the slave detects an error by combinational checks over the captured access context (addr_q, write_q, size, protection). The checks: is addr_q in a reserved/unmapped sub-region? Is this a write to a read-only register (write_q && is_RO(addr_q))? Does the protection/privilege fail (!allowed(HPROT_q, addr_q))? Is the size unsupported? Any true → an error condition for this access. So in hardware, error detection is combinational logic over the captured access. So it's a set of checks. So the slave knows when to error.
The two-cycle ERROR FSM: when an error is detected, a small FSM drives the two-cycle sequence. From the OKAY state, on an error condition the FSM goes to ERROR-1: HRESP = ERROR, HREADYOUT = 0 (signal, don't complete). Next cycle, ERROR-2: HRESP = ERROR (held), HREADYOUT = 1 (complete). Then back to OKAY. The FSM ensures HRESP is ERROR for both cycles and HREADYOUT is low then high — the exact protocol sequence. (Note: HRESP must be ERROR in both cycles — a common bug is asserting it only one cycle.) So in hardware, the two-cycle ERROR is a 2-state FSM sequence. So it's a small state machine. So that's the response timing.
The state-update suppression: critically, the state-update logic must be gated by the error condition — if (sel_q && write_q && !error_condition) regs[addr_q] <= HWDATA (and likewise suppress read side effects on error). So an errored write does not update state; an errored read does not pop a FIFO or clear a flag. The slave detects the error and suppresses the access atomically. So in hardware, the write-enable / side-effect logic is gated by !error — keeping state clean on errors. So in hardware, HRESP generation is: combinational error detection over the captured access, a 2-state FSM driving the two-cycle ERROR sequence (HRESP=ERROR both cycles, HREADYOUT low-then-high), and gated state-update logic suppressing changes on error. The FSM sequence and the state suppression are the two pieces to get right. So in hardware, detect, sequence, and suppress. So it's three coupled pieces.
5. System Architecture Perspective
At the system level, HRESP is the bus's error-signaling mechanism — how a hardware fault (bad address, illegal access) becomes a software-visible exception — and its two-cycle timing and state-protection are what make error handling coherent across the pipelined bus and safe for the system.
The hardware-to-software fault path: HRESP = ERROR is the start of a fault path that ends in software. The slave signals ERROR; the master (the CPU's bus interface) receives it and raises a bus-fault exception (a hardware trap); the software exception handler then responds (logs the fault, kills the offending process, recovers). So HRESP is the hardware origin of a bus fault — the mechanism by which a bad access becomes a handled exception. So at the system level, HRESP is the bus's fault-reporting channel into software. So it's the error path's source. So faults flow from HRESP.
The pipeline coherence: the two-cycle timing is what keeps the pipelined bus coherent on an error. Without it, an error on transfer N would leave the already-issued transfer N+1 in an ambiguous state (did it happen? was it cancelled?). The two cycles give the master a defined window to cancel N+1 cleanly — so the bus always has a coherent view of which transfers happened. So at the system level, the two-cycle ERROR is a pipeline-coherence mechanism — ensuring errors don't corrupt the transfer stream. So it's coherence under faults. So the pipeline stays consistent.
The system safety: the no-state-change invariant is a safety property — an invalid access must not corrupt the system. A forbidden write (security violation) must not alter protected state; a bad address must not trigger a side effect. So the slave's error-then-suppress behavior is a system-safety guarantee — invalid accesses are inert. This is especially critical for security (a denied access leaks/corrupts nothing) and robustness (a buggy access doesn't cascade). So at the system level, HRESP is the bus's error-signaling mechanism (the hardware origin of a software bus-fault exception — the fault path into software), its two-cycle timing maintains pipeline coherence under errors (a clean window to cancel the already-issued next transfer), and its no-state-change invariant is a system-safety guarantee (invalid accesses are inert — critical for security and robustness). So HRESP is where a hardware fault becomes a coherent, safe, software-visible event — making correct error generation essential to a trustworthy system. So report faults, keep the pipeline coherent, and never corrupt state.
6. Engineering Tradeoffs
HRESP generation embodies the report-not-hide, two-cycle, protect-state design.
- ERROR vs silent OKAY on a bad access. Signaling
ERRORsurfaces the fault (correct, safe, debuggable); silently respondingOKAYhides it (dangerous — corruption, security holes, undetected bugs). AlwaysERRORon an invalid access. - Two-cycle ERROR vs (illegal) one-cycle. The two-cycle sequence is mandatory (keeps the pipeline coherent — lets the master cancel N+1); a one-cycle "error" violates the protocol and leaves N+1 ambiguous. Always two cycles.
- Detect-and-suppress vs perform-then-undo. Detecting the error before committing and suppressing the access keeps state clean (correct); performing then trying to undo risks corruption/side effects. Detect and suppress atomically.
- Robust (full error checks) vs minimal (no checks). Full error checks (reserved, RO, protection, size) catch faults (robust, secure) at the cost of logic; minimal checks (assume valid) are smaller but unsafe. Implement the checks that matter for the slave's role.
The throughline: HRESP is the slave's response — OKAY (HRESP low) for a normal transfer, ERROR (HRESP high) for an access the slave cannot/should not perform (reserved address, illegal type, protection violation, unsupported size). The ERROR response is a mandatory two-cycle sequence: cycle 1 (HRESP=ERROR, HREADYOUT low — signal), cycle 2 (HRESP=ERROR held, HREADYOUT high — complete) — the two cycles let the pipelined master cancel its already-issued next transfer. An errored access must not change slave state (no corruption — a safety invariant). AHB-Lite uses only OKAY/ERROR. It's the bus's error-signaling mechanism — the hardware origin of a software bus-fault, keeping the pipeline coherent and the system safe.
7. Industry Example
Build the error responses for a peripheral with a reserved region and read-only registers.
A peripheral has registers (some read-only), a data region, and reserved address holes — and must error correctly on misuse.
- Reserved-address access → ERROR. A read or write to a reserved hole in the peripheral's region (no register there) triggers
ERROR. The slave's detection:addr_qdecodes to a reserved index → error condition. The two-cycle sequence runs; the master takes a bus fault. No state changes (there's nothing there anyway). - Write to a read-only register → ERROR. A write to the STATUS register (read-only) triggers
ERROR:write_q && is_RO(addr_q)→ error condition. Critically, the STATUS register is not written (state-update gated by!error). The two-cycle sequence runs; the master faults. (A read of STATUS is fine —OKAY.) - Protection violation → ERROR. If the peripheral is privileged-only and an unprivileged access arrives (
HPROT_qindicates user mode), the slave errors:!privileged(HPROT_q)→ error condition. The access is suppressed (no read/write performed) — a security guarantee. Two-cycle ERROR; master faults. - The two-cycle sequence. For each error, the slave drives
HRESP = ERRORwithHREADYOUTlow (cycle 1), thenHRESP = ERRORheld withHREADYOUThigh (cycle 2). The master, having issued the next transfer's address phase, sees the error and cancels that next transfer. - Normal accesses → OKAY. All valid accesses (read any register, write a RW/W1C register, read/write the data region) respond
OKAY(single cycle) — the common case. - The master's handling. On
ERROR, the master raises a bus-fault exception; software's handler logs the fault (which address, read/write) and recovers (chapter 7.7).
The example shows HRESP in action: OKAY for valid accesses (the common case), the two-cycle ERROR for reserved-address, read-only-write, and protection violations, with the errored access suppressed (no state change — especially the security-critical protection case), and the master faulting on the error. This is correct slave error reporting — surfacing faults safely and coherently. This is the slave's "no".
7b. The Error-Capable Subordinate, Written Out
Section 7 lists three error conditions and the invariant that binds them: an
errored access must not change the subordinate's state. Writing the RTL makes
one consequence visible that prose does not, and it is the subject of the Debug
Lab below — the second cycle of the two-cycle ERROR has HREADYOUT high, so
a write commit gated on completion alone fires exactly there.
A note on width, since it differs by AHB version: AHB-Lite HRESP is one bit
(1'b0 OKAY, 1'b1 ERROR). Full AHB carries a two-bit HRESP with RETRY and
SPLIT, which this page and this RTL deliberately exclude.
module ahb_err_slave #(
parameter int AW = 32,
parameter int DW = 32,
parameter int WAITS = 2 // wait cycles before the response is formed
) (
input logic HCLK,
input logic HRESETn,
input logic HSEL,
input logic [1:0] HTRANS,
input logic [AW-1:0] HADDR,
input logic HWRITE,
input logic [2:0] HSIZE,
input logic [3:0] HPROT,
input logic HREADY, // GLOBAL ready. Sequential use only.
input logic [DW-1:0] HWDATA,
output logic HREADYOUT,
output logic [DW-1:0] HRDATA,
output logic HRESP, // AHB-Lite: 1 bit
output logic [DW-1:0] status_o
);
localparam logic RSP_OKAY = 1'b0, RSP_ERROR = 1'b1;
localparam logic [2:0] SIZE_WORD = 3'b010;
// Map: 0x00 CTRL (RW), 0x04 STATUS (RO), 0x08 DATA (RW), rest reserved.
localparam logic [3:0] OFF_CTRL = 4'h0, OFF_STATUS = 4'h4, OFF_DATA = 4'h8;
// ── Address phase captured on the BUS's pace.
wire ap_valid = HSEL && HTRANS[1];
logic dp_sel, dp_write;
logic [AW-1:0] dp_addr;
logic [2:0] dp_size;
logic [3:0] dp_prot;
always_ff @(posedge HCLK or negedge HRESETn)
if (!HRESETn) begin
dp_sel <= 1'b0; dp_write <= 1'b0; dp_addr <= '0;
dp_size <= '0; dp_prot <= '0;
end else if (HREADY) begin
dp_sel <= ap_valid; dp_write <= ap_valid && HWRITE;
dp_addr <= HADDR; dp_size <= HSIZE;
dp_prot <= HPROT;
end
// ── Error detection, from the CAPTURED context only.
// Deriving any of these from live HADDR/HWRITE would evaluate the NEXT
// transfer's address phase against this transfer's data phase - the
// "error context lost across wait states" failure.
wire sel_ctrl = (dp_addr[3:0] == OFF_CTRL);
wire sel_status = (dp_addr[3:0] == OFF_STATUS);
wire sel_data = (dp_addr[3:0] == OFF_DATA);
wire mapped = sel_ctrl | sel_status | sel_data;
wire err_reserved = dp_sel && !mapped; // no register here
wire err_ro_write = dp_sel && dp_write && sel_status; // write to read-only
wire err_prot = dp_sel && !dp_prot[1]; // HPROT[1]=privileged
wire err_size = dp_sel && (dp_size != SIZE_WORD); // word-only subordinate
wire error_req = err_reserved | err_ro_write | err_prot | err_size;
// ── Wait counter. The response - OKAY or ERROR - is only formed once the
// subordinate has finished deciding, so waits precede the ERROR pair.
localparam int CW = (WAITS == 0) ? 1 : $clog2(WAITS + 1);
logic [CW-1:0] cnt;
wire waits_met = (cnt >= WAITS[CW-1:0]);
always_ff @(posedge HCLK or negedge HRESETn)
if (!HRESETn) cnt <= '0;
else if (!dp_sel) cnt <= '0;
else if (!waits_met) cnt <= cnt + 1'b1;
else if (HREADYOUT) cnt <= '0; // completing: re-arm
// ── The response FSM. ERR1 and ERR2 exist because AHB mandates two cycles.
typedef enum logic [1:0] { S_OK, S_ERR1, S_ERR2 } state_e;
state_e state;
wire resp_ready = dp_sel && waits_met; // done waiting; response is formed
always_ff @(posedge HCLK or negedge HRESETn)
if (!HRESETn) state <= S_OK;
else unique case (state)
S_OK : state <= (resp_ready && error_req) ? S_ERR1 : S_OK;
S_ERR1: state <= S_ERR2;
S_ERR2: state <= S_OK;
endcase
// HREADYOUT: low while waiting, low for the FIRST error cycle, high on the
// second - and high for a clean completion.
assign HREADYOUT = (state == S_ERR1) ? 1'b0
: (state == S_ERR2) ? 1'b1
: (dp_sel ? (waits_met && !error_req) : 1'b1);
assign HRESP = (state == S_ERR1 || state == S_ERR2) ? RSP_ERROR : RSP_OKAY;
// ── State. THE INVARIANT: an errored access changes nothing.
logic [DW-1:0] ctrl_q, data_q, status_q;
// Note both terms. HREADYOUT alone is NOT sufficient: it is high on the
// second ERROR cycle too, which is precisely how an errored write commits
// in the buggy version below.
wire commit = dp_sel && dp_write && (state == S_OK)
&& waits_met && !error_req;
always_ff @(posedge HCLK or negedge HRESETn)
if (!HRESETn) begin
ctrl_q <= '0; data_q <= '0; status_q <= 32'hC0DE_0000;
end else begin
if (commit && sel_ctrl) ctrl_q <= HWDATA;
if (commit && sel_data) data_q <= HWDATA;
// status_q is hardware-owned; software writes to it error and are dropped.
end
assign status_o = status_q;
// Read data is only required to be valid when HREADYOUT is high. Returning
// zero on an errored read keeps a failure reproducible rather than X.
assign HRDATA = (dp_sel && !dp_write && !error_req)
? (sel_ctrl ? ctrl_q : sel_status ? status_q : data_q)
: '0;
endmoduleProving the sequence
module ahb_err_slave_tb;
localparam int AW = 32, DW = 32, W = 2;
logic HCLK = 0, HRESETn = 0, HSEL = 0, HWRITE = 0;
logic [1:0] HTRANS = 2'b00;
logic [AW-1:0] HADDR = '0;
logic [2:0] HSIZE = 3'b010;
logic [3:0] HPROT = 4'b0010; // privileged
logic [DW-1:0] HWDATA = '0;
logic HREADYOUT; logic [DW-1:0] HRDATA; logic HRESP;
logic [DW-1:0] status_o;
wire HREADY = HREADYOUT; // single-subordinate interconnect
ahb_err_slave #(.AW(AW), .DW(DW), .WAITS(W)) dut (.*);
always #5 HCLK = ~HCLK;
int fails = 0;
// Drive one transfer; report how many cycles HRESP was ERROR and whether
// HREADYOUT was low on the first of them.
task automatic xfer (input bit wr, input logic [AW-1:0] a,
input logic [DW-1:0] wd, input logic [3:0] prot,
input logic [2:0] size,
output int err_cycles, output bit first_low);
err_cycles = 0; first_low = 1'b0;
@(negedge HCLK);
HSEL = 1; HTRANS = 2'b10; HWRITE = wr; HADDR = a; HPROT = prot; HSIZE = size;
@(posedge HCLK);
while (!HREADY) @(posedge HCLK); // address phase accepted
@(negedge HCLK); HSEL = 0; HTRANS = 2'b00; HWDATA = wd;
forever begin
@(posedge HCLK);
if (HRESP) begin
if (err_cycles == 0) first_low = !HREADYOUT;
err_cycles++;
end
if (HREADYOUT) break;
end
endtask
task automatic chk (input string what, input int got, exp);
if (got !== exp) begin
$error("%s: got %0d expected %0d", what, got, exp); fails++;
end else $display("PASS %-40s = %0d", what, got);
endtask
int ec; bit fl;
initial begin
repeat (2) @(negedge HCLK); HRESETn = 1;
// 1. A normal write: no ERROR at all.
xfer(1'b1, 32'h0000_0000, 32'hA5A5_1234, 4'b0010, 3'b010, ec, fl);
chk("normal write reports no ERROR cycles", ec, 0);
// 2. Write to the read-only STATUS: the two-cycle ERROR, and the register
// must be UNCHANGED. This is the invariant the Debug Lab is about.
begin
logic [DW-1:0] before = status_o;
xfer(1'b1, 32'h0000_0004, 32'hDEAD_BEEF, 4'b0010, 3'b010, ec, fl);
chk("RO write asserts ERROR for exactly 2 cycles", ec, 2);
chk("first ERROR cycle has HREADYOUT low", fl, 1'b1);
chk("STATUS unchanged after errored write",
(status_o === before), 1'b1);
end
// 3. Reserved address and 4. unprivileged access also error.
xfer(1'b0, 32'h0000_0030, '0, 4'b0010, 3'b010, ec, fl);
chk("reserved address asserts ERROR for 2 cycles", ec, 2);
xfer(1'b0, 32'h0000_0000, '0, 4'b0000, 3'b010, ec, fl);
chk("unprivileged access asserts ERROR for 2 cycles", ec, 2);
if (fails == 0) $display("All HRESP checks passed.");
else $fatal(1, "%0d check(s) failed", fails);
$finish;
end
endmoduleCheck 2 is the one that matters, and it has three parts for a reason: the ERROR
must last exactly two cycles, the first must have HREADYOUT low, and the
register must be unchanged. A test asserting only that an error was reported
passes a subordinate that errors correctly and corrupts itself anyway.
module ahb_err_slave_sva (
input logic HCLK, HRESETn, HREADYOUT, HRESP,
input logic dp_sel, dp_write, error_req,
input logic [31:0] status_q
);
// The two-cycle ERROR, forwards: an ERROR with HREADYOUT low must be
// followed by an ERROR with HREADYOUT high.
a_error_two_cycle: assert property (@(posedge HCLK) disable iff (!HRESETn)
(HRESP && !HREADYOUT) |=> (HRESP && HREADYOUT));
// ...and backwards: a completing ERROR cannot appear out of nowhere. This
// is the one that catches a one-cycle ERROR, which the forward property
// alone does not.
a_error_not_one_cycle: assert property (@(posedge HCLK) disable iff (!HRESETn)
(HRESP && HREADYOUT) |-> $past(HRESP && !HREADYOUT));
// The invariant: nothing changes on an errored access.
a_no_state_change_on_error: assert property (@(posedge HCLK) disable iff (!HRESETn)
(dp_sel && error_req) |=> $stable(status_q))
else $error("subordinate state changed during an errored access");
// Coverage - a two-cycle-ERROR assertion over a run with no errors is
// vacuously true, and a suite that never errors has not tested the path.
c_error_seen: cover property (@(posedge HCLK) HRESP && !HREADYOUT);
c_ro_write: cover property (@(posedge HCLK) dp_sel && dp_write && error_req);
endmoduleThe subordinate reported the error correctly and performed the write anyway
ERRORED-WRITE-COMMITS-ON-ERR2A peripheral correctly rejected writes to its read-only STATUS register: the
master took a bus fault every time, the two-cycle sequence was clean on the
waveform, and the protocol checker was silent.
STATUS changed anyway. Firmware that probed the register map by writing a
pattern to every address and checking which ones faulted left the peripheral in a
state its own hardware had never produced — and because the fault was reported,
nobody suspected the write had landed. It was found weeks later when a driver
read STATUS and got a value that could not physically occur.
// Commit on completion. Reasonable-looking, and wrong.
wire commit = dp_sel && dp_write && HREADYOUT;
always_ff @(posedge HCLK)
if (commit && sel_status) status_q <= HWDATA; // RO register, writtenThe waveform of a single rejected write showed it in three cycles:
cycle 1 2 3
state S_OK S_ERR1 S_ERR2
error_req 1 1 1
HRESP 0 1 1
HREADYOUT 0 0 1 <-- high on the second ERROR cycle
commit 0 0 1 <-- so the write commits HERE
status_q C0DE0000 C0DE0000 DEADBEEFHREADYOUT is high on cycle 3 because that is what the second cycle of a
two-cycle ERROR is — the completing cycle. A commit gated on HREADYOUT alone
therefore fires on exactly the cycle the protocol uses to finish reporting the
error.
The protocol checker stayed silent because nothing about the protocol was wrong. The sequence was a textbook two-cycle ERROR. The violation was of the subordinate's own invariant, which no bus-level checker knows about.
HREADYOUT means "this transfer completes now", and a transfer completing with
HRESP = ERROR has still completed. Using completion as the commit condition
therefore commits errored transfers along with successful ones.
This is a trap specific to the two-cycle ERROR, and it is worth seeing why the
usual fix for side-effect timing does not help here. Gating a side effect on
completion — dp_sel && HREADYOUT — is exactly right for making it fire once per
transfer rather than once per wait cycle. That reasoning is correct and it is
insufficient: it controls when the effect fires, not whether it should fire at
all. The ERROR path needs the second, independent term.
The reason it survives review is that the buggy expression reads as complete. It names the transfer, the direction, and the completion; the only thing missing is the condition nobody thinks to write down, because "obviously we do not perform an access we are rejecting" feels like it follows from rejecting it.
Note also that a zero-wait subordinate would still have this bug — the two
ERROR cycles exist regardless of WAITS — so it is not a wait-state artefact and
cannot be found by testing without wait states.
// Both terms. The transfer must be completing AND not be the one we rejected.
wire commit = dp_sel && dp_write && (state == S_OK)
&& waits_met && !error_req;Testing state == S_OK rather than HREADYOUT is the clearer form here, because
it says what is meant: commit only on a clean completion, never on either cycle
of an error sequence.
The check that fails against the old RTL is the three-part one from the
testbench: the ERROR lasts exactly two cycles, the first has HREADYOUT low, and
the register is unchanged. Only the third part catches this, which is why a
test asserting merely that an error was reported passes a subordinate that
corrupts itself.
The durable guard is the invariant expressed directly, so it holds for every register and every error condition without enumerating them:
a_no_state_change_on_error: assert property (@(posedge HCLK) disable iff (!HRESETn)
(dp_sel && error_req) |=> $stable(status_q));Bind one of these per architecturally-visible register, or one over a
concatenation of them, and pair it with c_ro_write — an invariant assertion over
a run that never errored is vacuously satisfied.
The generalisation is worth carrying: an error response is a completion. Anywhere a design keys a side effect off "the transfer finished" — a write commit, a FIFO pop, a command strobe, an interrupt acknowledgement — it needs a second term saying the transfer also succeeded. See the two-cycle ERROR response for the sequence itself and error response for when a subordinate is required to produce one.
8. Common Mistakes
9. Interview Insight
HRESP generation is a classic RTL interview topic — the OKAY/ERROR meaning, the two-cycle ERROR sequence (and why), and the no-state-change invariant are the signals.
The answer that lands gives the response and the two-cycle reasoning: "HRESP is the slave's response output. For a normal transfer it drives OKAY, which is HRESP low, completing in one cycle. For an access it can't or shouldn't perform — a reserved or unmapped address, a write to a read-only register, a protection violation, an unsupported size — it drives ERROR, HRESP high. The key detail is that ERROR is a mandatory two-cycle response, not one cycle. In the first cycle the slave drives HRESP to ERROR with HREADYOUT low — signalling the error but not completing. In the second cycle it holds HRESP at ERROR and drives HREADYOUT high — completing the transfer. Two things must be right: HRESP is held ERROR for both cycles, because the master samples the response when HREADYOUT is high, which is the second cycle — if you dropped HRESP to OKAY in the second cycle, the master would miss the error. And HREADYOUT is low then high. The reason it's two cycles is pipelining: by the time the slave detects the error in the current transfer's data phase, the master has already issued the next transfer's address phase. The two cycles give the master a window to see the error coming and cancel that already-issued next transfer cleanly, keeping the pipeline coherent. The other critical thing is that an errored access must not change the slave's state — an errored write must not update the register, an errored read must not trigger a side effect like a FIFO pop. You gate the state update by the not-error condition, so an invalid access is inert. That's a safety property, especially for protection violations. And note AHB-Lite only has OKAY and ERROR — RETRY and SPLIT are full AHB; a busy slave just inserts wait states." The OKAY/ERROR meaning, the two-cycle sequence with the pipeline reasoning, the hold-HRESP-both-cycles detail, and the no-state-change invariant are the senior signals.
10. Practice Challenge
Build and reason from HRESP generation.
- OKAY and ERROR. State what
OKAYandERRORmean, and list the conditions under which a slave respondsERROR. - The two-cycle sequence. Describe the two-cycle
ERROR(HRESP and HREADYOUT in each cycle) and explain why it's two cycles. - Read the waveform. From Figure 2, explain the two error cycles, when the master cancels its next transfer, and that the slave's state is unchanged.
- Hold HRESP both cycles. Explain why
HRESPmust beERRORin both cycles and the bug if it's asserted only once. - Protect state. Explain why an errored access must not change state, and how the RTL enforces it (gating).
11. Key Takeaways
HRESPis the slave's response —OKAY(low) for a normal transfer,ERROR(high) for an access the slave can't/shouldn't perform (reserved address, illegal type, protection violation, unsupported size).- The
ERRORresponse is a mandatory two-cycle sequence — cycle 1:HRESP=ERROR,HREADYOUTlow; cycle 2:HRESP=ERROR(held),HREADYOUThigh (completes). - Two cycles exist for the pipeline — the master has already issued the next transfer's address phase; the two cycles let it cancel that transfer cleanly (pipeline coherence).
- Hold
HRESP=ERRORin both cycles — the master samples the response whenHREADYOUTis high (the second cycle); dropping it early makes the master miss the error. - An errored access must not change slave state — gate the write-enable/side-effects by
!errorso invalid accesses are inert (a safety/security guarantee). - AHB-Lite uses only
OKAY/ERROR—RETRY/SPLITare full-AHB; a busy slave inserts wait states, not aRETRY.HRESPis the hardware origin of a software bus-fault.
11b. Where This Is Specified
- Arm AMBA 3 AHB-Lite Protocol Specification (ARM IHI 0033A). The single-bit
HRESPwith only OKAY and ERROR; the two-cycle ERROR requirement —HRESPasserted withHREADYOUTlow, then asserted again withHREADYOUThigh — and the reason for it: the manager has already issued the next address phase and needs a cycle in which to cancel it. - Arm AMBA 5 AHB Protocol Specification (ARM IHI 0033). The address-phase/data-phase pipeline that makes the two-cycle sequence necessary, the
HPROTprotection encoding used here for the privilege check, and theHSIZEalignment rules. - Full AHB (legacy). A two-bit
HRESPcarrying RETRY and SPLIT in addition to OKAY and ERROR. Deliberately outside the scope of this page, and not mixed into the RTL above. - IEEE 1800-2023 §16 — Assertions. The concurrent properties used for the two-cycle sequence in both directions,
$past,$stable, andcover property.
12. What Comes Next
You now can generate correct responses, including the two-cycle ERROR. The next chapters drill into the slave's capture and FSM internals:
- Address / Control Capture (next) — capture address-phase control for use in the data phase (the discipline underlying every slave output).
- Write FSM, Read FSM, and Bridge FSM (RTL) — the slave's sequential control.
To revisit the ERROR response from the protocol side, see The Two-Cycle ERROR Response and How the Master Reacts; for the pace signal the sequence uses, see HREADYOUT Generation.