PCIe · Module 8
Status Register — Condition, Not Control
The status register reports what a Function has observed and what it provides. Why error flags are sticky, how write-one-to-clear works in RTL, and what must happen when hardware sets a bit in the same cycle software clears it.
Chapter 8.4 covered a register software writes to grant permission. Two bytes further along sits its counterpart — a register that mostly reports back.
The difference is not cosmetic. It changes who owns each bit, how writes behave, and what the RTL underneath has to do.
What does the Status Register expose, and how should RTL represent fields whose behaviour is fundamentally different from ordinary software-controlled configuration bits?
1. Command Versus Status
Command Register (04h) | Status Register (06h) | |
|---|---|---|
| Who writes the meaning | software | hardware, or the implementation |
| What a write does | sets policy | acknowledges or clears a condition — or nothing |
| Direction of information | software → hardware | hardware → software |
| Typical field behaviour | read/write | a mixture of read-only and write-one-to-clear |
| What clearing achieves | withdraws permission | acknowledges an observation |
The consequence that catches people out: in the Command Register, writing a bit sets it. In the Status Register, writing a 1 to certain bits clears them. Same operation, opposite effect, two bytes apart.
2. The Register
What the verified subset gives us is exactly the two behaviours worth building:
- A read-only field reporting implementation state — bit 4, which tells software whether a capability list exists.
- Sticky condition fields that hardware sets and software clears by writing one — the error-reporting group.
Those are different problems in RTL, and §§6–7 build them separately.
3. Why Status Bits Must Be Sticky
A condition occurs for an instant. Software reads configuration space at some unrelated later time. Between those two events, the information has to survive.
A hardware event is a moment. A status bit is a record of that moment.
If a status bit merely reflected a live condition, software would observe it only by reading during the instant it was true — which for a transient event is essentially never. The bit must latch: set when the condition occurs, stay set until software acknowledges it.
That is why status fields cannot be wired directly from event logic to the read path, and it is the first misconception §13 addresses.
4. Write-One-to-Clear
For the error-reporting group, acknowledgement works like this:
- Hardware sets the bit when the condition occurs.
- Software reads it and learns the condition happened.
- Software writes
1to that bit position to clear it. - Software writing
0leaves it alone.
5. The Same-Cycle Problem
Here is the case that makes this an RTL chapter rather than a register description.
A hardware event and a software clear can arrive in the same cycle. A new condition occurs at exactly the moment software writes 1 to acknowledge the previous one.
Both are legitimate. Both want to change the same bit in opposite directions. Something must win, and whichever wins must be decided by the designer, not by whichever assignment happens to come last in the code.
The policy this chapter uses: the hardware set wins.
next = (current & ~software_clear) | hardware_setThe clear is applied first, then the set — so a condition arriving in the clear cycle survives.
The reasoning. The event genuinely happened. If the clear won, the bit would read clear afterwards and software would believe nothing occurred — a real condition silently discarded. The opposite failure, a bit that stays set slightly longer than software expected, is visible and harmless: software reads it again and clears it again.
6. RTL — Sticky Status With Write-One-to-Clear
// SYNTHESIZABLE. The verified Status Register subset: read-only capability
// indication plus sticky write-one-to-clear condition bits.
// Bit positions and access classes: NORMATIVE (see §2).
// HW-set-wins precedence and structure: IMPLEMENTATION POLICY (see §5).
module pcie_status_register (
input logic clk,
input logic rst_n,
// Hardware condition pulses, one per W1C bit position. Asserted for a
// cycle when the corresponding condition is observed.
input logic [15:0] hw_event,
// Live implementation state feeding read-only bits. Bit 4 here reports
// whether this Function implements a capability list.
input logic cap_list_present,
// Configuration write, after identity and offset decode selected 06h.
input logic cfg_wr_en,
input logic [15:0] cfg_wdata,
input logic [1:0] cfg_be, // byte 0 = bits 7:0, byte 1 = bits 15:8
output logic [15:0] cfg_rdata
);
// Verified bit positions (§2).
localparam int BIT_CAP_LIST = 4; // read-only
localparam int BIT_MSTR_DPE = 8; // W1C
localparam int BIT_SIG_TGT_ABORT = 11; // W1C
localparam int BIT_REC_TGT_ABORT = 12; // W1C
localparam int BIT_REC_MST_ABORT = 13; // W1C
localparam int BIT_SIG_SYS_ERR = 14; // W1C
localparam int BIT_DET_PAR_ERR = 15; // W1C
// bits 15,14,13,12,11,8 -> 0xF900
localparam logic [15:0] W1C_MASK = 16'hF900;
localparam logic [15:0] RO_MASK = 16'h0010; // bit 4
logic [15:0] sticky_q;
// Byte-enable expansion. A configuration write need not cover the whole
// register, and a byte that is not enabled must contribute no clear at all
// — otherwise a write aimed at one half acknowledges conditions in the
// other half that software never saw.
function automatic logic [15:0] be_expand(input logic [1:0] be);
be_expand = {{8{be[1]}}, {8{be[0]}}};
endfunction
// Software clear request: bits written as 1, in byte-enabled bytes, that
// are actually W1C. Writing 0 requests nothing (§4).
wire [15:0] sw_clear = cfg_wr_en ? (cfg_wdata & be_expand(cfg_be) & W1C_MASK)
: 16'h0000;
// Hardware set request, restricted to W1C positions.
wire [15:0] hw_set = hw_event & W1C_MASK;
// READ COMPOSITION. Sticky bits come from storage; the read-only bit comes
// from live implementation state and has no storage at all; everything
// outside the verified subset reads zero in this model.
always_comb begin
cfg_rdata = 16'h0000;
cfg_rdata |= (sticky_q & W1C_MASK);
cfg_rdata[BIT_CAP_LIST] = cap_list_present;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sticky_q <= 16'h0000;
end else begin
// THE PRECEDENCE RULE, in one expression: clear first, then set.
// A condition arriving in the same cycle as its acknowledgement
// SURVIVES, because the set is applied to the already-cleared value.
// Reversing these two operations loses the event silently (§5).
sticky_q <= (sticky_q & ~sw_clear) | hw_set;
end
end
endmoduleClassification: synthesizable.
Register semantics, stated per field class:
| Dimension | W1C condition bits (8, 11–15) | Read-only bit (4) |
|---|---|---|
| Reset | cleared in this model | none — no storage |
| Read | returns the latched condition | returns live implementation state |
| Write | 1 in an enabled byte clears; 0 does nothing | ignored entirely |
| Byte enables | honoured — an unenabled byte clears nothing | irrelevant |
| Hardware may update | yes, and wins a same-cycle conflict | the source may change at any time |
| Software may update | only by clearing | never |
| Side effects | none beyond the clear | none |
What it teaches — three things:
- One expression carries the whole precedence decision.
(sticky_q & ~sw_clear) | hw_setis the policy. Swapping the two operations silently loses events, and the difference is invisible in a port list or a block diagram. - Byte enables gate the clear, not just the write. A write covering only byte 0 must not acknowledge conditions in bits 8–15. Expanding the byte enables into a bit mask before applying
W1C_MASKmakes that automatic. - Read-only and sticky fields compose in the read path, not in storage. Bit 4 has no flop. Mixing a live source and latched state in one read mux is the normal shape of a status register, and building it as a uniform register bank gets it wrong.
Deliberately simplified: only the verified subset is modelled; bits outside it read zero; hw_event is an abstract pulse with no origin; and there is no per-event qualification such as whether reporting is enabled elsewhere.
Production implication: a real implementation must implement every architecturally defined bit with its correct access class and reset value, source each condition from the logic that actually detects it, respect whatever enabling or masking the specification defines for each condition, and instantiate per Function.
7. RTL — A Read-Only Field From Live State
Bit 4 deserves its own treatment, because it is read-only in a different way from Chapter 8.2's identity fields.
Identity is a constant. It is fixed at synthesis or loaded once and frozen, and the read path returns it unchanged forever.
A read-only status field is a live view. Its value is produced by hardware that may change it, and software sees whatever is true when it reads.
// SYNTHESIZABLE. A read-only status bit whose value is a live view of
// implementation state rather than a stored constant.
// Contrast with Chapter 8.2's identity, which is fixed.
module ro_status_view (
// Implementation state. In a real Function this comes from whatever logic
// owns the fact — here, whether a capability list is implemented.
input logic cap_list_implemented,
// A configuration write reaches this module and does nothing. The write
// path is not connected to the value at all, which is what makes the
// read-only property structural rather than enforced.
output logic cap_list_bit
);
assign cap_list_bit = cap_list_implemented;
endmoduleClassification: synthesizable.
What it teaches: that "read-only" describes where the value comes from, not a register with writes suppressed. There is no storage, no reset, and no write port — so no edit can accidentally make it writable, and no test needs to prove that writes are ignored, because there is nothing for a write to reach.
Why it is worth showing separately from the sticky bits. A designer who builds the whole Status Register as one uniform register bank must then add per-bit write suppression, per-bit source selection, and per-bit reset behaviour — recreating in logic what the composition in §6 gets for free. Status registers are heterogeneous by nature, and the read path is where the heterogeneity belongs.
Deliberately simplified: one bit, one source, no qualification.
Production implication: real read-only status fields are driven from the blocks that own the underlying facts, which may be in other clock domains — requiring synchronisation before the value reaches a configuration read, and a defined behaviour for a value observed mid-transition.
8. Assertions
// SVA over pcie_status_register. Implementation invariants for THIS design —
// not PCIe protocol requirements. Every property refers to explicit RTL state.
// SAFETY — P1: a hardware event is never lost to a same-cycle clear. The
// precedence policy of §5 made checkable, and the property whose failure is
// a silently discarded condition.
generate
for (genvar b = 0; b < 16; b++) begin : g_set_wins
if (W1C_MASK[b]) begin : g_bit
property p_hw_set_survives_clear;
@(posedge clk) disable iff (!rst_n)
hw_event[b] |=> sticky_q[b];
endproperty
a_set_wins : assert property (p_hw_set_survives_clear);
end
end
endgenerate
// CORRECTNESS — P2: writing 1 to an enabled W1C bit clears it, provided no
// hardware event arrives in the same cycle.
generate
for (genvar c = 0; c < 16; c++) begin : g_clear
if (W1C_MASK[c]) begin : g_bit
property p_write_one_clears;
@(posedge clk) disable iff (!rst_n)
(cfg_wr_en && cfg_wdata[c] && be_expand(cfg_be)[c] && !hw_event[c])
|=> !sticky_q[c];
endproperty
a_w1c_clears : assert property (p_write_one_clears);
end
end
endgenerate
// SAFETY — P3: writing 0 never clears. Catches a W1C bit implemented as an
// ordinary read/write register, which would let software clear conditions by
// writing back a stale value.
generate
for (genvar d = 0; d < 16; d++) begin : g_zero
if (W1C_MASK[d]) begin : g_bit
property p_write_zero_preserves;
@(posedge clk) disable iff (!rst_n)
(sticky_q[d] && cfg_wr_en && !cfg_wdata[d]) |=> sticky_q[d];
endproperty
a_write_zero_safe : assert property (p_write_zero_preserves);
end
end
endgenerate
// SAFETY — P4: a byte that is not enabled clears nothing. Catches byte
// enables ignored on the clear path, which acknowledges conditions in the
// other half of the register that software never observed.
property p_unenabled_byte_preserved;
@(posedge clk) disable iff (!rst_n)
(cfg_wr_en && !cfg_be[1]) |=> ((sticky_q & 16'hFF00)
== (($past(sticky_q) | $past(hw_set)) & 16'hFF00));
endproperty
a_be_respected : assert property (p_unenabled_byte_preserved);
// SAFETY — P5: sticky bits do not clear spontaneously. A condition record
// must survive until acknowledged; a bit that clears on its own loses the
// event with no indication.
generate
for (genvar e = 0; e < 16; e++) begin : g_sticky
if (W1C_MASK[e]) begin : g_bit
property p_sticky_until_cleared;
@(posedge clk) disable iff (!rst_n)
(sticky_q[e] && !(cfg_wr_en && cfg_wdata[e] && be_expand(cfg_be)[e]))
|=> sticky_q[e];
endproperty
a_sticky : assert property (p_sticky_until_cleared);
end
end
endgenerate
// SAFETY — P6: the read-only bit is never affected by a write. It has no
// storage, so this holds structurally — asserted so a later edit that adds
// storage fails here.
property p_ro_bit_follows_source;
@(posedge clk) disable iff (!rst_n)
cfg_rdata[BIT_CAP_LIST] == cap_list_present;
endproperty
a_ro_live : assert property (p_ro_bit_follows_source);
// CORRECTNESS — P7: the readback composition is exactly sticky W1C bits plus
// the live read-only bit, and nothing else. Catches an unmodelled bit leaking
// a non-zero value into the response.
property p_readback_composition;
@(posedge clk) disable iff (!rst_n)
cfg_rdata == ((sticky_q & W1C_MASK)
| (cap_list_present ? RO_MASK : 16'h0000));
endproperty
a_readback : assert property (p_readback_composition);
// SAFETY — P8: reset establishes a defined exposed value with no unknown
// state. An X in a status read becomes an arbitrary condition report in
// silicon, which software may act on.
property p_reset_defined;
@(posedge clk)
!rst_n |=> (sticky_q == 16'h0000 && !$isunknown(cfg_rdata));
endproperty
a_reset_defined : assert property (p_reset_defined);P1 is the chapter's property, and it is generated per bit deliberately. An aggregate form would report that something was lost without saying which condition — and status bits report different conditions with different consequences, so knowing which one vanished is most of the diagnosis.
Why the failure P1 catches is so hard to find by testing. It requires a hardware event and a software clear in the same cycle. The event source and the software write are asynchronous with respect to one another, so the window is one cycle wide and hit by chance. A design with reversed precedence works in almost every run, loses an occasional error report, and produces a system that under-reports faults — which nobody notices until someone is chasing a fault that hardware saw and never told anyone about.
P3 catches the most common W1C implementation error: building the field as an ordinary read/write register. That version passes a naive test — write 1, read back, bit is set... except it is set because the write set it, not because a condition occurred. P3 fires the moment software writes 0 to a set bit and the bit clears.
P4 is byte enables applied to the clear path. Software reading the whole register and writing back only byte 1 must not acknowledge conditions in bits 7:0. The property compares against $past(sticky_q) | $past(hw_set) rather than $past(sticky_q) alone, so a hardware event in the same cycle does not make it fire spuriously — which is itself the kind of detail an aggregate property gets wrong.
9. Verification
Monitors observe: hardware event pulses per bit, the configuration write with data and byte enables, the live read-only source, the sticky state, and the readback.
The scoreboard independently models each field's semantics by class — sticky-W1C bits with the precedence rule, and the read-only bit as a pass-through of its source. It must not use one generic read/write model for the whole register, because the register is not uniform. That is the point of §7.
Scenarios:
- Event sets the bit. For each W1C position individually. Verify only that bit changes.
- Repeated events with no clear. Verify the bit stays set and nothing else changes — a second event on an already-set bit is normal and must be harmless.
- Software clear. Write
1to a set bit with the byte enabled. Verify it clears (P2). - Software writes zero. Write
0to a set bit. Verify it does not clear (P3). This is the scenario that catches a W1C implemented as ordinary storage. - Byte-enable miss. Set bits in both halves, then write
1s across the register with only byte 0 enabled. Verify bits 8–15 are untouched (P4). - Byte-enable hit. The same with only byte 1 enabled. Verify bits 8–15 clear and bits 7:0 do not.
- Event and clear in the same cycle. The P1 scenario, and it must be constructed deliberately — drive
hw_event[b]in the exact cyclecfg_wr_enasserts withcfg_wdata[b]set. Verify the bit remains set. Repeat for every W1C position. - Clear one bit while another is set. Verify only the addressed bit clears.
- Back-to-back events. An event in consecutive cycles, and an event in the cycle after a clear. Verify no event is lost.
- Read-only field write attempt. Write
1and0to bit 4. Verify the readback still follows its source (P6). - Read-only source changes. Toggle
cap_list_present. Verify the readback follows it with no write involved. - Reset while bits are set. Verify all sticky bits clear and the readback is defined (P8).
- Software polling. Read repeatedly with no writes. Verify reads have no side effect — a status read must not clear anything.
Coverage should include: every W1C bit set, cleared, and same-cycle set-and-cleared; every byte-enable pattern crossed with set bits in each half; write data of all-ones and all-zeros; the read-only source in both states; reset with each distinct sticky value; and reads with no intervening write.
10. Debugging
Symptom: a driver clears a status bit and it is set again immediately
The first question is whether this is a fault at all. Several correct behaviours produce it.
Candidates, cheapest first:
- The condition is genuinely recurring. The clear worked; a new event set it again. This is the most common explanation and it is not a bug — the status bit is doing its job, and the real question is why the condition keeps occurring.
- The clear was written as
0. Software wrote back a cleared value expecting that to clear, which under write-one-to-clear does nothing (§4). The bit was never acknowledged. - The byte enables missed the field. A write covering the wrong half acknowledges nothing in the half that mattered (P4).
- The W1C mask is wrong in the design. A bit not included in
W1C_MASKcannot be cleared by any write. - The wrong Function or offset was addressed. The clear landed somewhere else entirely — Chapter 7.6 and Chapter 7.7.
The observation that separates 1 from the rest. Clear the bit and read back immediately, before any traffic that could regenerate the condition. If it reads clear, the clear mechanism works and the condition is recurring — go and find what is causing it. If it reads set, the clear did not take effect and candidates 2–5 apply.
Why candidate 1 deserves to be first. A recurring condition is a real fault being correctly reported. Investigating the clear mechanism in that case means debugging the messenger while the actual problem continues.
Symptom: RTL observes an event but software never sees the status bit
The event happened and the record did not survive to be read. The candidates are ordered by where the information is lost:
- The event was never latched. Status logic wired as a live view of a transient condition rather than as sticky state (§3). The bit is true for one cycle and software reads at some unrelated time.
- Precedence lost it. The event arrived in the same cycle as a clear and the clear won (§5, P1). This is the intermittent case — it happens rarely and is not reproducible from the software side.
- The event was masked before reaching the sticky logic. A qualification the design applies that the debugger has not accounted for.
- The readback does not include the bit. A composition error in the read mux (P7): the bit is set in storage and does not appear in the response.
- A clear landed between the event and the read. Another agent — a different driver, a diagnostic tool, an earlier polling loop — acknowledged it first. Status is global to the Function, not private to one reader.
- The wrong Function was read.
The measurement that splits these fastest. Observe sticky_q directly and compare against the readback. If the bit is set in storage and absent from the response, the fault is the read path (4). If it is not set in storage, the fault is upstream — latching, masking, or precedence (1, 2, 3). If both are correct at the moment of the event, someone cleared it before the read (5).
Why candidate 5 is worth keeping on the list. Status registers are shared state with no ownership protocol. Two readers polling the same Function will steal each other's events, and that is not a hardware defect — it is a property of the mechanism that surprises people building diagnostic tooling alongside a driver.
11. Common Misconceptions
- "The Status Register is just Command Register readback." They are different registers at different offsets with opposite information flow. Command carries software's policy to hardware; Status carries hardware's observations to software.
- "Every status bit is read-only." Some are — bit 4 reports implementation state and cannot be written. Others are write-one-to-clear and exist precisely to be modified by software.
- "Every status bit is write-one-to-clear." Equally wrong. The register is a mixture, which is why §6 composes the read path from two different kinds of source.
- "Writing zero clears a W1C bit." Writing
0does nothing. Writing1clears. The convention exists so software can write back exactly what it read, acknowledging only what it saw (§4). - "Software owns the Status Register." Hardware sets the condition bits; software can only acknowledge them. Ownership of the value is hardware's, which is the reverse of the Command Register.
- "Hardware events can be wired straight to the read path." A condition is a moment; a status bit is a record. Without latching, software would have to be reading during the exact cycle the event occurred.
- "Clearing a status bit fixes the underlying problem." It acknowledges the report. If the condition recurs, the bit sets again — which is the mechanism working, not failing.
- "A status bit that keeps returning proves the W1C logic is broken." It much more often proves the condition keeps happening. The discriminator is whether an immediate read-back after clearing shows it clear.
- "All legacy PCI status fields mean the same thing in PCIe." Several bits carry meanings inherited from PCI — bus-speed and transfer-mode capabilities, device-select timing — and many are hardwired for PCIe. Reading a legacy PCI status table as though it described PCIe behaviour is a real source of wrong conclusions, and it is why §2 publishes a verified subset rather than the full sixteen bits.
12. Understanding Check
13. What's Next
Module 8 has now covered the mechanism and four fields: identity, permission, and condition.
Chapter 8.6 — Configuration Header assembles them. Every field so far has been shown in isolation with only its own local context; 8.6 puts them into the standardised structure they actually occupy, and introduces the thing that makes that structure more than a table — the header exists in more than one layout, because Functions with different roles need to expose different information. Which layout a Function uses is itself something software must discover, and something RTL must decode.