Skip to content

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 responsedid 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 (HRESP low) — the normal response: the transfer succeeded; complete in one cycle (HREADYOUT high).
  • ERROR (HRESP high) — 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, HREADYOUT low; cycle 2: HRESP = ERROR, HREADYOUT high (completes).
  • No state change on error — an errored write must not update state; an errored read returns no meaningful data. The slave stays clean.
An FSM with an OKAY state and a two-state ERROR sequence: ERROR-1 (HREADYOUT low) then ERROR-2 (HREADYOUT high), returning to OKAY.
Figure 1 — the HRESP FSM: an OKAY state and the two-cycle ERROR sequence. Normally the slave is in OKAY (HRESP=0, HREADYOUT=1), completing each transfer in one cycle. When an error condition is detected, it goes to ERROR-1 (HRESP=ERROR, HREADYOUT=0 — signalling the error but not yet completing), then to ERROR-2 (HRESP=ERROR, HREADYOUT=1 — completing the transfer with the error reported), then back to OKAY. The two cycles let the pipelined master see the error coming (ERROR-1) and cancel the already-issued next transfer before it commits (ERROR-2).

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 nothingno 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 cycles
Cycle 0: address phase to a reserved address. Cycle 1: HRESP=ERROR, HREADYOUT=0 (error flagged, not complete), master has already pipelined the next transfer. Cycle 2: HRESP=ERROR held, HREADYOUT=1 (completes), master cancels the next transfer. HRESP is ERROR for both error cycles; HREADYOUT is low then high; slave state unchanged.ERROR cycle 1: HRESP=ERROR, HREADYOUT=0 (flagged, not complete)ERROR cycle 1: HRESP=ERROR,HREADYOUT=0 (flagged, notcomplete)ERROR cycle 2: HRESP=ERROR, HREADYOUT=1 (completes) → master cancels nextERROR cycle 2: HRESP=ERROR,HREADYOUT=1 (completes) →master cancels nextBack to OKAY — slave state unchanged (no corruption)Back to OKAY — slave stateunchanged (no corruption)HCLKHADDRRSVDnextnextnextHRESPOKAYERRORERROROKAYHREADYOUTslave stateSSSSt0t1t2t3
Figure 2 — the two-cycle ERROR response. Cycle 0: the master issues a transfer to a reserved address (address phase) — the slave will error. Cycle 1: ERROR cycle 1 — the slave drives HRESP=ERROR and HREADYOUT=0 (error flagged, not complete); meanwhile the master has already issued the NEXT transfer's address phase (it was pipelined). Cycle 2: ERROR cycle 2 — the slave holds HRESP=ERROR and drives HREADYOUT=1 (the errored transfer completes); the master, having seen the error, cancels/re-issues the next transfer rather than committing it. The slave's state is unchanged throughout (no corrupt write). HRESP stays ERROR for both cycles; HREADYOUT is low then high.

The 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.

Four ERROR conditions: reserved/unmapped, illegal access type, protection violation, and size/alignment, all returning the two-cycle ERROR.
Figure 3 — when a slave drives ERROR instead of OKAY. Reserved/unmapped: an access to a hole in the slave's region (no register/memory there). Illegal access type: a write to a read-only register, or a read of a write-only location (violates the register attribute). Protection violation: an access not permitted in the current privilege/security mode (HPROT/secure check fails). Size/alignment: an unsupported HSIZE or a misaligned address the slave can't honor. All return the two-cycle ERROR response (not silent completion), and the master takes a bus fault. AHB-Lite has only OKAY and ERROR — RETRY and SPLIT belong to full AHB.

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 ERROR surfaces the fault (correct, safe, debuggable); silently responding OKAY hides it (dangerous — corruption, security holes, undetected bugs). Always ERROR on 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 responseOKAY (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_q decodes 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_q indicates 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 = ERROR with HREADYOUT low (cycle 1), then HRESP = ERROR held with HREADYOUT high (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.

ahb_err_slave.sv — OKAY, two-cycle ERROR, and the no-state-change invariant
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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;
endmodule

Proving the sequence

ahb_err_slave_tb.sv — self-checking, counts the ERROR cycles
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

Check 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.

ahb_err_slave_sva.sv — the response sequence, both directions
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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);
endmodule
1

The subordinate reported the error correctly and performed the write anyway

ERRORED-WRITE-COMMITS-ON-ERR2
Symptom

A 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.

Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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, written
Diagnostic Evidence

The waveform of a single rejected write showed it in three cycles:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 DEADBEEF

HREADYOUT 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.

Root Cause

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.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

A summary card on HRESP generation: OKAY/ERROR, the two-cycle ERROR sequence, AHB-Lite scope, and no-state-change.
Figure 4 — a strong answer in one card: OKAY (HRESP=0) for normal transfers, ERROR (HRESP=1) for reserved/illegal/protection/size faults; ERROR is two cycles (HRESP=ERROR both, HREADYOUT low then high) so the pipelined master can cancel its already-issued next transfer; AHB-Lite is OKAY+ERROR only; the slave must not change state on an errored access. The senior point: ERROR is a two-cycle response, HREADYOUT low then high, so the pipelined master can cancel the following transfer.

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.

  1. OKAY and ERROR. State what OKAY and ERROR mean, and list the conditions under which a slave responds ERROR.
  2. The two-cycle sequence. Describe the two-cycle ERROR (HRESP and HREADYOUT in each cycle) and explain why it's two cycles.
  3. 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.
  4. Hold HRESP both cycles. Explain why HRESP must be ERROR in both cycles and the bug if it's asserted only once.
  5. Protect state. Explain why an errored access must not change state, and how the RTL enforces it (gating).

11. Key Takeaways

  • HRESP is the slave's responseOKAY (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 ERROR response is a mandatory two-cycle sequence — cycle 1: HRESP=ERROR, HREADYOUT low; cycle 2: HRESP=ERROR (held), HREADYOUT high (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=ERROR in both cycles — the master samples the response when HREADYOUT is 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 !error so invalid accesses are inert (a safety/security guarantee).
  • AHB-Lite uses only OKAY/ERRORRETRY/SPLIT are full-AHB; a busy slave inserts wait states, not a RETRY. HRESP is 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 HRESP with only OKAY and ERROR; the two-cycle ERROR requirement — HRESP asserted with HREADYOUT low, then asserted again with HREADYOUT high — 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 HPROT protection encoding used here for the privilege check, and the HSIZE alignment rules.
  • Full AHB (legacy). A two-bit HRESP carrying 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, and cover 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.