Skip to content
VLSI Mentor

CXL · Module 7

Configuration Over CXL.io

What a configuration register actually is in hardware: field access types, reset values, address aliasing, byte enables, one-cycle command pulses, and the hardware-versus-software race that loses events. Six RTL models simulated, thirteen mutations, thirteen killed.

Chapter 7.1 listed configuration as one of CXL.io's five jobs and moved on. This chapter stops there, because "the host reads and writes configuration space" hides every decision that makes a device work or brick.

A configuration register is not storage. It is a contract — and a single field with the wrong access type can make a device undiscoverable in a way that looks like a link problem.

1. The Engineering Problem — A Register Is Not a Variable

Software writes a value. It reads it back. It matches. Everything looks correct.

That test passes on a register file where reserved bits are writable, where a status field can be overwritten by software, where a command bit fires an action every cycle it stays set, where two addresses reach the same storage, and where a partial write destroys three fields the access never named. Read-back-matches is compatible with all five defects.

The reason is that a register has properties a variable does not: an owner, a reset value, an access type, a side effect, and a visibility. Software's write is one participant in a contract, and hardware is the other.

2. The One-Sentence Model

A configuration register is the hardware/software contract table — every field declares who owns it, what it resets to, what a write means, and what a write does — and configuration bugs are almost always a field whose declared contract and implemented behaviour disagree.

Call it the contract: not what the register holds, but what each side is allowed to do to it.

3. What This Chapter Owns

QuestionOwned by
What CXL.io carries7.1
Register contracts and accessthis chapter
Errors and event capture7.3
Walking the hierarchy7.4
Why a config write needs a completion7.5
PCIe configuration mechanismPCIe: configuration access
PCIe configuration header layoutPCIe: configuration header

Deliberately not repeated: the PCIe configuration mechanism and header layout, which the PCIe track covers in depth. This chapter is about what happens inside the device when one of those accesses lands.

4. What a Field Declares

Five properties, and every configuration bug in this chapter is one of them being wrong.

PropertyThe question it answers
Ownerwho is allowed to change it
Reset valuewhat it reads before anyone writes
Access typewhat a software write means
Side effectwhat a write does, beyond storing
Visibilitywhether reading it changes anything

The access type is where most of the damage lives, because there are only a few and they are easy to conflate.

TypeSoftware writeOwner
RWstores the valuesoftware
ROignoredhardware
W1Cwriting 1 clears; 0 does nothinghardware sets, software clears

W1C is the one that gets implemented as RW, and it is the one whose failure loses evidence rather than corrupting data. §9 shows why.

The five properties are not independent checks scattered through a design — they sit on one path, and each stage of that path owns one of them.

A configuration access from software enters a decoder, which either selects a register or rejects the access as unsupported or misaligned. A selected access reaches the register bank, where per-field access type decides what a write means. From there one branch stores the value, another triggers a one-cycle side effect, and a third path returns the response that every accepted access must produce exactly once.software accessaddress, byteenables, datadecoderwhole address,alignment separatelyrejectedunsupported ormisalignedregister bankaccess type per fieldstoredRW fields onlyside effectone write, one actionresponseexactly one, neverzerono matchhitnon-posted12

The response node is the one that looks optional and is not. A configuration access is non-posted in both directions, so every accepted access owes exactly one response — which is why RTL 5 counts them and why 7.5 makes the posted/non-posted split its subject.

5. Teaching-model boundary

6. RTL 1 — The Contract, in Logic

cfg_reg_bank.sv — one register, four field types
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // Teaching layout of one 32-bit control register:
  //   [7:0]   RW    software-owned control
  //   [15:8]  RO    hardware-owned status
  //   [16]    W1C   sticky event, set by hardware, cleared by writing 1
  //   [31:17] RSVD  must read zero and must not be writable
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      rw_q <= 8'h00; ro_q <= 8'hA5; w1c_q <= 1'b0; rsvd_q <= 15'd0;
      clr_req_q <= 1'b0;
      ro_changed_err <= 1'b0; rsvd_set_err <= 1'b0; w1c_not_cleared_err <= 1'b0;
    end else begin
      // --- RW: byte-enabled, software-owned
      if (sw_write && sw_be[0]) rw_q <= sw_wdata[7:0];
 
      // --- RO: hardware-owned. A software write must not change it.
      if (RO_WRITABLE && sw_write && sw_be[1]) ro_q <= sw_wdata[15:8];
 
      // --- W1C: hardware SETS, software clears by writing 1.
      // Hardware set must win a same-cycle race, or an event is lost.
      if (hw_event)                                   w1c_q <= 1'b1;
      else if (sw_write && sw_be[2] && sw_wdata[16])  w1c_q <= W1C_AS_RW ? 1'b1 : 1'b0;
      else if (W1C_AS_RW && sw_write && sw_be[2])     w1c_q <= sw_wdata[16];
 
      // --- RSVD: must stay zero.
      if (RSVD_WRITABLE && sw_write && sw_be[3]) rsvd_q <= sw_wdata[31:17];
 
      // ---- diagnostics, stated about the OUTPUT
      if (ro_q != ro_reset)                       ro_changed_err      <= 1'b1;
      if (rsvd_q != 15'd0)                        rsvd_set_err        <= 1'b1;
      // The property is about the cycle AFTER the write: on the write cycle
      // itself w1c_q has not had an edge to change on, so checking it there
      // flags the correct behaviour. Register the request and check the result.
      clr_req_q <= sw_write && sw_be[2] && sw_wdata[16] && !hw_event;
      if (clr_req_q && w1c_q) w1c_not_cleared_err <= 1'b1;
    end
  end

State. Four independently-owned fields plus three sticky diagnostics. Each field's if has a different guard, and that asymmetry is the contract.

Synthesis. Thirty-two flops, four write-enable terms and three comparators. Trivial — which is exactly why register bugs ship: nothing about the logic is hard, so nothing about it gets reviewed.

DV. Every field type needs its own negative test: write a value to RO and confirm it does not move; write zero to W1C and confirm it does not clear; write ones to RSVD and confirm it stays zero. Read-back-matches tests none of those.

Icarus Verilog 13.0 — EXP1 and EXP2
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  after reset : rdata=0000a500
  write 0000003C : rdata=0000a53c
  write 0000FF00 : RO field=a5 (must stay A5) | RO-writable=ff
  write FFFE0000 : RSVD=0 (must stay 0) | RSVD-writable=7fff
  hardware event : W1C bit=1
  write 0 to W1C : bit=1 (must stay 1)
  write 1 to W1C : bit=0 (must be 0) | W1C-as-RW bit=1

Every one of those values is asserted, not printed. The RO-writable=ff and RSVD-writable=7fff columns are the deliberately-broken variants, which exist so the diagnostics have a reachable failing case.

7. RTL 2 — Decoding, and the Alias

cfg_decoder.sv — decode the word address, check alignment separately
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  localparam logic [11:0] A_CTRL = 12'h010, A_STATUS = 12'h014, A_CAP = 12'h018;
 
  // The bug shape: decoding only the low 8 bits makes 0x110 alias 0x010.
  // Decode on the WORD address so alignment is a separate question -- a
  // decoder that folds the two together makes its own alignment check
  // unreachable, because a misaligned address can never equal an aligned one.
  assign eff = IGNORE_UPPER ? {4'h0, addr[7:2], 2'b00} : {addr[11:2], 2'b00};
 
  assign misaligned = acc_valid && (addr[1:0] != 2'b00);
 
  assign sel_ctrl   = acc_valid && (eff == A_CTRL)   && (ALLOW_UNALIGNED || !misaligned);
  assign sel_status = acc_valid && (eff == A_STATUS) && (ALLOW_UNALIGNED || !misaligned);
  assign sel_cap    = acc_valid && (eff == A_CAP)    && (ALLOW_UNALIGNED || !misaligned);
 
  assign hit         = sel_ctrl || sel_status || sel_cap;
  assign unsupported = acc_valid && !hit && !misaligned;
Icarus Verilog 13.0 — EXP3
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  addr 010 : ctrl=1 status=0 cap=0 hit=1
  addr 110 : correct hit=0 unsupported=1 | alias-decoder hit=1
  addr 012 : misaligned=1 hit=0 | allow-unaligned hit=1

Address 0x110 is the whole lesson. A decoder comparing only the low byte sees 0x10 and selects the control register. Software using the documented address never notices; anything that walks the space — enumeration, a memory test, a dump tool — writes to a register it did not intend to touch.

An alias is worse than an unmapped address because an unmapped address is reported. unsupported exists so that a walk over the space produces a refusal rather than a silent hit somewhere unexpected.

8. RTL 3 and 4 — Partial Writes and One-Cycle Commands

byte_enable_merge.sv — the bytes you did not name must survive
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  always_comb begin
    merged = data_q;
    if (IGNORE_BE) merged = wdata;
    else begin
      if (be[0]) merged[7:0]   = wdata[7:0];
      if (be[1]) merged[15:8]  = wdata[15:8];
      if (be[2]) merged[23:16] = wdata[23:16];
      if (be[3]) merged[31:24] = wdata[31:24];
    end
  end
Icarus Verilog 13.0 — EXP4
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  full write : data=aabbccdd
  write byte0 only : data=aabbcc11 (upper bytes must survive)
  ignore-BE variant : data=00000011

A register holding four independent fields is normal, and software writing one of them is normal. A design that writes the whole word on every access destroys the other three — and the corruption is invisible whenever software happens to write the full word, which is most of the time in a directed test.

Then the command register:

side_effect_reg.sv — a command is an action, not a state
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // The command bit is stored so software can read back what it asked for.
  // The ACTION is a one-cycle pulse derived from the write, not from the bit.
  assign fire   = sw_write && sw_cmd_bit;
  assign action = LEVEL_NOT_PULSE ? cmd_q : fire;
Icarus Verilog 13.0 — EXP5
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  one command write : action pulses=1 writes=1
  five cycles later : action pulses=1 | level-variant pulses=5

One write, one action. The level variant fires once per cycle for as long as the bit stays set — five actions from one command, and in a real device that is five DMA kicks, five resets, or five of whatever the command starts.

The invariant is that actions never outnumber commands, which is checkable with two counters and no knowledge of what the command does.

9. Waveform — Four Accesses Against the Real RTL

Read, write, W1C clear, and an illegal access

12 cycles
A configuration read at cycle one hits the control register. A write at cycle three lands and the read data shows the new value at cycle four. A hardware event at cycle five sets the sticky bit visible at cycle six. A write-one-to-clear at cycle six clears it by cycle seven while the read-write field survives because only byte two was enabled. A command write at cycle eight produces exactly one action pulse. A misaligned access at cycle ten is refused with no register selected.readreadwritewriteevent set, then W1C clearevent set, then W1C clearcommandcommandillegal accessillegal accesssticky bit visible, then cleared by writing onesticky bit visible, thencleared by writing oneexactly one action pulseexactly one action pulsemisaligned: refused, nothing selectedmisaligned: refused,nothing selectedclkacc_validsw_writeaddr010010010010010010010010010010012010hw_eventrdata0000a5000000a5000000a5000000a5000000a53c0000a53c0001a53c0000a53c0000a53c0000a53c0000a53c0000a53chiterractiont0t1t2t3t4t5t6t7t8t9t10t11
Icarus Verilog 13.0. Cycle values taken from the simulation transcript of the real RTL.

Three things are worth reading off it.

The write at cycle 3 appears in rdata at cycle 4, not cycle 3 — a register updates on the edge, so a read in the same cycle as a write returns the old value. That is correct and it is a routine source of confusion in bring-up.

The W1C clear at cycle 6 leaves rdata at 0000a53c, not 0000a500. The clear enabled only byte 2, so the RW field's 3c survives. A clear that enabled all four bytes would have zeroed it — correct behaviour, and a good demonstration of why byte enables exist.

action is high for exactly one cycle at cycle 8, and err is high for exactly one cycle at cycle 10. Both are one-cycle events by construction, and both are counted so the invariant is checkable.

10. RTL 5 and 6 — Responses, and the Race

A configuration write is non-posted — it requires a completion. So the device owes exactly one response per accepted access.

cfg_access_track.sv — exactly one response, never two, never none
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  assign accepted  = acc_valid && acc_ready && (pending_q < DEPTH[3:0]);
  assign rsp_c     = NEVER_RESPOND ? 1'b0 : (svc_done && (pending_q != 4'd0));
 
      // Responses may never outnumber accepted accesses.
      if (n_rsp_q > n_acc_q)                 double_response_err <= 1'b1;
      if (svc_done && (pending_q == 4'd0))   response_without_access_err <= 1'b1;
Icarus Verilog 13.0 — EXP6
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  3 accesses accepted : pending=3
  3 responses : pending=0 accesses=3 responses=3
  accesses and responses matched an oracle            : ok
  9 accesses into depth 4 : pending=4
  response with nothing pending : detected=1 pending=0

Two responses to one access is data corruption in software's view; zero is a hang. The pending_q != 0 guard is what makes the second response a detected error rather than a decrement of a zero counter — mutation M10 removes it and the count wraps to fifteen.

Then the race that loses evidence:

hw_sw_race.sv — hardware set must win
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  always_comb begin
    if (CLEAR_WINS) begin
      if      (sw_clear) next_sticky = 1'b0;
      else if (hw_set)   next_sticky = 1'b1;
      else               next_sticky = sticky_q;
    end else begin
      if      (hw_set)   next_sticky = 1'b1;
      else if (sw_clear) next_sticky = 1'b0;
      else               next_sticky = sticky_q;
    end
  end
Icarus Verilog 13.0 — EXP7
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  simultaneous set+clear : set-wins sticky=1 | clear-wins sticky=0
  events=1 observable=1 | clear-wins events=1 observable=0

An event that happened cannot be un-happened by a clear that was issued before it arrived. Software's clear refers to the events it has already read; a hardware event arriving in the same cycle is new information, and losing it means nothing anywhere records that it occurred.

The n_set_q versus n_observed_q pair is what makes the loss measurable rather than arguable: events that happened, and events software could have seen. On the correct design they are equal. Chapter 7.3 builds the whole observability story on this distinction.

11. Assertions

Concurrent SVA execution: NOT SUPPORTED BY Icarus Verilog — the only simulator available here. The properties below are bind-ready and were not executed; each maps to a procedural stand-in and a mutation.

cfg_over_io_sva.sv — bind-ready properties
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SAFETY -------------------------------------------------------------------
// V1 — a read-only field never changes, whatever software writes.
a_ro_stable: assert property (@(posedge clk) disable iff (!rst_n)
  $stable(ro_q));
 
// V2 — reserved bits read zero, always.
a_rsvd_zero: assert property (@(posedge clk) disable iff (!rst_n)
  rsvd_q == '0);
 
// V3 — writing one to a W1C field clears it by the next cycle, unless
//      hardware set it in the same cycle.
a_w1c_clears: assert property (@(posedge clk) disable iff (!rst_n)
  (sw_write && sw_be[2] && sw_wdata[16] && !hw_event) |=> !w1c_q);
 
// V4 — writing zero to a W1C field never clears it.
a_w1c_zero_noop: assert property (@(posedge clk) disable iff (!rst_n)
  (sw_write && sw_be[2] && !sw_wdata[16] && w1c_q) |=> w1c_q);
 
// V5 — a byte whose enable is low is unchanged by the write.
a_be_respected: assert property (@(posedge clk) disable iff (!rst_n)
  (wr && !be[0]) |=> $stable(data_q[7:0]));
 
// V6 — at most one register is selected.
a_decode_onehot: assert property (@(posedge clk) disable iff (!rst_n)
  $onehot0({sel_ctrl, sel_status, sel_cap}));
 
// V7 — a hit implies the word address is a mapped offset.
a_no_alias: assert property (@(posedge clk) disable iff (!rst_n)
  hit |-> ({addr[11:2],2'b00} inside {A_CTRL, A_STATUS, A_CAP}));
 
// V8 — an access reaches a register, or is reported unsupported or misaligned.
a_access_total: assert property (@(posedge clk) disable iff (!rst_n)
  acc_valid |-> (hit || unsupported || misaligned));
 
// V9 — a command write produces exactly one action pulse.
a_one_pulse: assert property (@(posedge clk) disable iff (!rst_n)
  action |=> !action);
 
// V10 — responses never outnumber accepted accesses.
a_no_double_response: assert property (@(posedge clk) disable iff (!rst_n)
  n_rsp_q <= n_acc_q);
 
// V11 — a hardware set wins a same-cycle software clear.
a_set_wins: assert property (@(posedge clk) disable iff (!rst_n)
  hw_set |=> sticky_q);
 
// LIVENESS -----------------------------------------------------------------
// V12 — an accepted configuration access eventually produces a response.
//       ENVIRONMENT ASSUMPTION: the service side completes. A configuration
//       write is non-posted, so violating this is a software hang rather than
//       a silent loss — which is why the NEVER_RESPOND variant exists.
a_eventual_response: assert property (@(posedge clk) disable iff (!rst_n)
  accepted |-> s_eventually rsp_valid);

V1 is deliberately written as $stable rather than as an implication over writes. A read-only field must not change for any reason — not merely "not because software wrote it" — and the stronger form catches a hardware path that was never supposed to exist.

V4 is the pair to V3 and the one that is usually missing. V3 alone is satisfied by a field that clears on every write, which is exactly the W1C-as-RW bug.

12. Mutation Testing

Thirteen mutations. Clean code restored after each.

IDMutationResult
M1a read-only field becomes writableKILLED — RO check
M2W1C implemented as read/writeKILLED — write-zero check
M3reserved bits accept writesKILLED — RSVD check
M4wrong reset valueKILLED — reset check
M5decoder ignores upper address bitsKILLED — alias check
M6decode ranges overlapKILLED — alias check
M7unmapped address silently ignoredKILLED — unsupported
M8byte enables ignoredKILLED — partial write
M9command pulse becomes a levelKILLED — action count
M10response with nothing pendingKILLED — pending guard
M11accesses accepted past capacityKILLED — added stimulus
M12clear beats a same-cycle setKILLED — race check
M13lost events counted as observableKILLED — set vs observed
Mutation run — final
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
13/13 killed, 0 escaped

One escaped on the first run. M11 accepts accesses past capacity, and the original test only issued three into a depth-four tracker — so capacity was never reached. Nine accesses killed it.

Two defects were found in my own RTL before mutation testing began, and both are recorded in the callouts above: a W1C checker that fired on correct behaviour, and a decoder that folded alignment into its address comparison and thereby made its own alignment checker unreachable. Neither would have been caught by mutation — the first because it failed on the good design, the second because an unreachable checker produces an equivalent mutant.

13. Verification Plan

ItemApproach and goal
Reset valuesread every field before any write — exact match asserted
RWwrite and read back — value takes
ROwrite a different value — field does not move
W1C sethardware event — bit sets
W1C write-zerowrite 0 — bit does not clear
W1C write-onewrite 1 — bit clears by the next cycle
Reservedwrite all-ones — stays zero
Aliasaccess a shadow address — no hit, reported unsupported
Alignmentmisaligned access — refused, and the check is reachable
Partial writeone byte enabled — other three survive
Commandhold the bit set — exactly one action
Responsesaccept, respond, and respond with nothing pending
Capacityissue past depth — tracker stops accepting
Raceset and clear in the same cycle — set wins, nothing lost
Diagnostic livenesseach broken variant — every diagnostic observed firing

Rows 5 and 9 are the ones that are usually missing. Write-zero-does-not-clear is what separates W1C from RW, and a plan with only the write-one case passes the buggy design.

14. Debug Lab

1

A sticky error bit clears itself whenever software reads and rewrites the register

W1C-AS-RW
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Sticky event bit.
if (sw_write && sw_be[2]) w1c_q <= sw_wdata[16];
Symptom

Error bits are set in silicon and software almost never sees them. When it does, the error is gone by the time anything reads the detail registers. The pattern correlates with any driver doing read-modify-write on the register.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  write 0 to W1C : bit=1 (correct: stays set)
  W1C-as-RW variant : bit=0 after the same write
Root Cause

The field was implemented as read/write. A driver reading the register, modifying an unrelated field and writing the word back writes zero to the event bit — and under RW semantics, zero clears it.

Under W1C, writing zero means "leave it alone" precisely so that read-modify-write on neighbouring fields is safe. The bug is not that the bit clears; it is that it clears on a write that never asked to.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (hw_event)                                  w1c_q <= 1'b1;
else if (sw_write && sw_be[2] && sw_wdata[16]) w1c_q <= 1'b0;   // only a ONE clears
Lesson

Test that writing zero does nothing. The write-one-clears case is obvious and passes on the buggy design too. Only the write-zero case distinguishes W1C from RW, and it is the case a directed test almost never contains because it looks like a no-op.

2

Two different addresses reach the same register

DECODER-ALIAS
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Compare the offset within the register block.
assign eff = {4'h0, addr[7:0]};
Symptom

A device works under normal driver operation and corrupts its own configuration whenever a diagnostic tool dumps the configuration space. The corruption is reproducible and appears unrelated to what the tool was reading.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  addr 110 : correct hit=0 unsupported=1 | alias-decoder hit=1
Root Cause

The decoder compared only the low eight bits, so 0x110 and 0x010 select the same register. Software using documented addresses never generates 0x110; anything that walks the space does.

An alias is worse than an unmapped address because the unmapped case is reported. The alias produces a successful access to the wrong place.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign eff = {addr[11:2], 2'b00};       // full word address
if (hit && ({addr[11:2],2'b00} inside {A_CTRL, A_STATUS, A_CAP}) == 0)
  alias_err <= 1'b1;
Lesson

Sweep the whole address space, not just the documented offsets. Every address must produce exactly one of: a hit at its own offset, an unsupported report, or a misalignment report. That totality property is one assertion and it catches every alias at once.

3

One command write starts the operation five times

COMMAND-PULSE-IS-A-LEVEL
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The command bit drives the action.
assign action = cmd_q;
Symptom

A single software command produces multiple operations. The count varies with how long software leaves the bit set before clearing it, so the symptom looks nondeterministic and load-dependent.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  one command write : action pulses=1 (correct)
  five cycles later : level-variant pulses=5
Root Cause

The action followed the level of the stored bit rather than the event of the write. A command register stores the bit so software can read back what it asked for, but the action must be derived from the write itself.

The variability is what makes it confusing: the number of spurious operations equals the number of cycles the bit stayed set, which depends on software timing rather than on anything in the device.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign fire   = sw_write && sw_cmd_bit;   // the EVENT
assign action = fire;                     // one cycle, by construction
if (n_action_q > n_write_q) multi_pulse_err <= 1'b1;
Lesson

Actions must never outnumber the commands that requested them, and that is checkable with two counters and no knowledge of what the command does. The assertion action |=> !action is the cycle-level form. Any register whose write has a side effect needs one of the two.

4

Writing one field corrupts the other three

BYTE-ENABLES-IGNORED
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Store the written word.
if (wr) data_q <= wdata;
Symptom

Configuration works when the driver writes whole registers and breaks when it updates a single field. Fields the access never mentioned read back as zero. It looks like the device is forgetting configuration.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  write byte0 only : correct=aabbcc11 | ignore-BE=00000011
Root Cause

Byte enables were ignored, so a partial write stored the whole word — and the bytes software did not intend to write carried whatever the access happened to place there, usually zero.

The bug is invisible whenever software writes all four bytes, which is most of a directed test and almost none of real driver behaviour.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (be[0]) merged[7:0]   = wdata[7:0];
if (be[1]) merged[15:8]  = wdata[15:8];
if (be[2]) merged[23:16] = wdata[23:16];
if (be[3]) merged[31:24] = wdata[31:24];
Lesson

Assert that a byte with its enable low is stable across the write. It is one property per byte lane and it catches the entire class. The test must issue a genuinely partial write — a full-word write cannot distinguish the two designs.

5

A hardware event arriving during a software clear is lost

CLEAR-WINS-THE-RACE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if      (sw_clear) sticky_q <= 1'b0;
else if (hw_set)   sticky_q <= 1'b1;
Symptom

Under load, error counts in software are lower than error counts in hardware telemetry. The discrepancy grows with how often software polls, which is the opposite of what anyone expects.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  simultaneous set+clear : set-wins sticky=1 | clear-wins sticky=0
  events=1 observable=1  | clear-wins events=1 observable=0
Root Cause

Branch priority. Software's clear refers to the events it has already read; a hardware event arriving in the same cycle is new information the clear could not have known about. Giving the clear priority discards it with no record.

More polling means more clear cycles means more windows in which an event can be lost — which is why the discrepancy scales with polling frequency.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if      (hw_set)   sticky_q <= 1'b1;    // the event wins
else if (sw_clear) sticky_q <= 1'b0;
if (hw_set && !next_sticky) event_lost_err <= 1'b1;
Lesson

Count events that happened and events that were observable, and require them equal. A single sticky bit cannot tell you it lost something; the counter pair can. This is the foundation 7.3 builds the whole observability argument on.

6

A configuration access is answered twice

DOUBLE-RESPONSE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Service completed; send the response.
assign rsp_valid = svc_done;
Symptom

Software sees configuration reads returning data it did not request, and occasional completion errors. The pattern appears under concurrent access from two agents and vanishes when either is stopped.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  response with nothing pending : detected=1 pending=0   (correct)
  mutated design : pending decrements from 0 and wraps to 15
Root Cause

The response was emitted from the service-done signal alone, with no check that anything was actually pending. A spurious or repeated svc_done produces a response with no request behind it — and decrements a zero counter, which wraps.

Because a configuration write is non-posted, every accepted access owes exactly one response. Two is corruption in software's view; the wrapped counter then reports capacity the design does not have.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign rsp_c = svc_done && (pending_q != 4'd0);
if (n_rsp_q > n_acc_q)               double_response_err <= 1'b1;
if (svc_done && (pending_q == 4'd0)) response_without_access_err <= 1'b1;
Lesson

Responses must never outnumber accepted accesses, and a response with nothing pending must be detected rather than acted on. The counter comparison is the cheapest form and needs no knowledge of the transaction contents. Chapter 7.5 generalises this to tag-matched completions.

15. Design Review

  • Who owns each field, and what resets it?
  • Can software race hardware on any bit? Which side wins, and why that side?
  • Is any field with a side effect driven from the stored bit rather than the write?
  • Can two addresses reach the same register?
  • What happens to a malformed or misaligned access — hit, refusal, or nothing?
  • Does a partial write preserve the bytes it did not enable?
  • Can a configuration access produce zero responses, or two?
  • Which registers survive reset, and is that intentional for every one of them?
  • Is any register crossing a clock domain, and what synchronises it?

16. How This Appears in Real Engineering

Register bugs ship because the logic is trivial. Nothing in a register file is architecturally hard, so nothing gets reviewed with the attention given to a datapath — and the failure modes are subtle, load-dependent and easy to blame on software.

W1C-as-RW is the most common single defect in this class. It survives because the obvious test — write one, observe clear — passes on the broken design. Only the write-zero case separates them.

Aliasing is found by tools, not by drivers. Drivers use documented offsets. Diagnostic dumps, memory tests and enumeration walk the space, which is why the corruption first appears when someone tries to debug something else.

The hardware/software race scales the wrong way. More polling should mean better observability and instead means more lost events, so the symptom actively misleads the person investigating it.

17. Common Misconceptions

ClaimWhy it is wrong
"Write and read back proves the register works"It passes on writable reserved bits, RO fields, W1C-as-RW, aliases and byte-enable bugs.
"W1C just means the bit clears on write"Writing zero must do nothing. That is what makes read-modify-write on neighbours safe.
"The command bit drives the operation"The write drives it. A level produces one action per cycle.
"Reserved bits do not matter"They are how future revisions extend the register. Writable reserved bits break forward compatibility.
"A configuration write is fire-and-forget"It is non-posted and requires a completion — unlike a memory write.
"Software clearing a bit is always safe"If a hardware event arrives in the same cycle, clear-priority discards it silently.
"An unmapped address is harmless"Only if it is reported. An alias produces a successful write to the wrong register.

18. Interview Reasoning

19. Exercises

  1. Implement. Write a register with an RW field, an RO field and a W1C field, then write the three negative tests that distinguish it from a design where all three are RW. State which of your tests the buggy design passes.

  2. Calculate. A 12-bit configuration space is decoded on the low 8 bits. How many distinct addresses alias onto each mapped register? Now decode on the low 10 bits and recompute. State the general relationship.

  3. RTL task. Add a fourth access type: a field hardware may update at any time and software may also write, with a defined winner. State the new invariant and which existing assertion must be weakened.

  4. DV task. Write the coverage cross for register access, and explain why crossing address with read/write alone leaves the byte-enable and W1C bugs uncovered.

  5. Debug task. Hardware telemetry reports more errors than software sees, and the gap widens as polling frequency increases. Give your investigation order and name the single counter pair that settles it.

  6. Design review. A colleague proposes clear-on-read for the status register "to save an access". Give the strongest version of the argument, then name what it costs and which specific tool it breaks.

20. Summary

A configuration register is a contract, and configuration bugs are a contract and an implementation disagreeing.

  • Every field declares an owner, reset value, access type, side effect and visibility. Getting one wrong is the whole defect class.
  • W1C is not RW. Writing zero must do nothing, or read-modify-write on neighbouring fields destroys evidence.
  • A command is an event, not a level — one write, exactly one action, checkable with two counters.
  • A decoder must compare the whole address, and alignment is a separate question — folding them makes the alignment check unreachable.
  • Partial writes must preserve the bytes they did not enable, and full-word tests cannot see the difference.
  • A configuration write is non-posted: exactly one response per accepted access, never zero, never two.
  • Hardware set wins a same-cycle software clear, and the loss is only measurable with an events-versus-observable counter pair.
  • Verification lesson: read-back-matches proves almost nothing. The coverage is in the negative tests.

Chapter 7.3 takes the sticky bit from §10 and asks the harder question: how do you report errors and events to software without losing evidence, and without drowning it?

Standards & specifications

Governing standard
CXL Specification (CXL Consortium)(opens CXL Consortium in a new tab)

Defines CXL.io, CXL.cache and CXL.mem, and the coherence and memory-pooling behaviour built on them. System design and deployment topology are not mandated.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the CXL curriculum.