PCIe · Module 31
"BARs Contain Memory" — A Claim on Addresses, Not a Box of Bytes
Two reads of one offset returned different values 60,058 times out of 200,000. A BAR claims an address window; what answers inside it is whatever the device decodes.
The belief: a BAR is a region of memory on the device. Program its base, and you have mapped some of the card's RAM into your address space.
The test that settles it is one line of code: read the same offset twice. If the second read returns something different, there was never memory there.
1. Why a Competent Engineer Believes It
The name says Base Address Register, and the first thing you do with it is map memory. Four reinforcing reasons:
The programming model is mmap and a pointer. A driver maps a BAR and dereferences it. From C, the interface is indistinguishable from memory — same syntax, same addressing, same pointer arithmetic.
Every teaching example is a RAM. The simplest device you can build behind a BAR is a block of registers or a buffer, and every introductory example does exactly that. The simplification is true for the example.
BAR sizing looks like allocating memory. The write-all-ones handshake reports a size (9.4), and a size is what you ask for when you allocate memory. Nothing in the handshake mentions what is behind it.
And most BAR accesses genuinely are memory-like. Control registers you can read repeatedly, buffers you can prefetch. The myth is right most of the time, which is exactly what makes it dangerous — it fails at the offsets that matter.
2. The Locally True Kernel
Do not overcorrect. The simplification is genuinely useful, and it is correct within a stated scope:
For an offset backed by plain storage with no side effects, a BAR window behaves exactly like memory — reads are idempotent, order between independent offsets does not matter, and re-reading is free.
That covers a large fraction of real BAR space: scratch registers, configuration values, descriptor rings held in device RAM, frame buffers. A driver author who treats those as memory is right.
The scope boundary is a single question, and §3 is it.
3. The Hidden Assumption
The myth assumes the address window implies storage behind it. It does not.
A BAR is a claim. It says: "I will respond to accesses in this range." (9.1). What the device does with an in-window offset is entirely its own decode logic (23.2) — and that logic may:
| offset behaviour | is it memory? |
|---|---|
| plain storage | yes |
| read-to-clear status | no — the read is a write |
| write-1-to-clear | no — write semantics inverted |
| FIFO port | no — the read pops |
| doorbell | no — the write is a command (26.2 §5) |
| unimplemented hole | no — may return a defined pattern or a UR |
| aliased range | no — several offsets, one register |
The replacement model, in one sentence:
A BAR names an address range the device will answer for; the offset within it selects a behaviour the device defines, and only some of those behaviours are storage.
That sentence survives contact with every row of the table, and it is what makes the next question askable: for this offset, is a read an observation or an operation?
4. The Root-Cause Tree
| stage | what happens |
|---|---|
| misconception | "the BAR contains memory" |
| hidden assumption | an address window implies storage with idempotent reads |
| architecture decision | mark the BAR region prefetchable / cacheable; allow speculation and combining |
| RTL / driver decision | re-read on retry; read a status register in a debug path; prefetch ahead |
| first divergence | the second read of a read-to-clear register returns 0 |
| visible symptom | events are missed intermittently, under load, on some machines |
| likely wrong diagnosis | "the device isn't setting the status bit" — investigation moves into the device |
| correct diagnosis | the bit was set and a duplicated read consumed it |
| corrected model | the offset's behaviour is part of its contract; reads with side effects are not memory |
The wrong diagnosis is the expensive part. The engineer looks at the device's event generation, which is working perfectly. §13 is why the symptom points away from the cause.
5. The Minimal Counterexample
One offset. Two reads. No concurrency, no load, no timing.
t0 hardware sets status bit 3
t1 software reads offset 0x10 -> returns 0x08 (bit 3 set)
t2 software reads offset 0x10 -> returns 0x00 (cleared by t1)If offset 0x10 were memory, t2 would return 0x08. It returns 0x00 because the read at t1 was the clear.
That is the entire disproof, and it is whiteboardable in ten seconds. Every consequence in this chapter follows from it.
6. What the Device Actually Decodes
Read the figure as an ownership statement. The window check answers "is this ours"; the offset decode answers "what does this do". They are different questions and different logic, and the myth collapses them into one.
7. The RTL the Myth Produces
The wrong RTL. This is what a competent engineer writes when they believe the BAR contains memory — and it is almost right.
// WRONG — the whole window is treated as an array. A read is a lookup and a
// write is a store, with no notion that an offset might mean something.
module bar_target_wrong #(
parameter int unsigned WORDS = 1024
)(
input logic clk,
input logic rst_n,
input logic req_valid,
input logic req_is_write,
input logic [31:0] req_offset,
input logic [31:0] req_wdata,
input logic [3:0] req_be,
output logic [31:0] rsp_rdata,
output logic rsp_valid
);
logic [31:0] mem [WORDS];
logic [$clog2(WORDS)-1:0] idx;
assign idx = req_offset[$clog2(WORDS)+1:2];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) rsp_valid <= 1'b0;
else begin
rsp_valid <= req_valid;
if (req_valid && req_is_write) mem[idx] <= req_wdata; // byte enables ignored
if (req_valid && !req_is_write) rsp_rdata <= mem[idx];
end
end
endmoduleWhy this passes basic tests. Write a value, read it back, get the value. Every directed test a designer writes for "does my BAR work" passes, because those tests are written by someone holding the same model. The status register is never tested as a status register; it is tested as a location.
Three things it silently gets wrong, and each is a real contract:
Byte enables are ignored. A partial write stores the full word, corrupting the bytes the host did not intend to touch. For a status register with mixed fields this destroys unrelated state.
Every offset is storage. There is no way to express "reading this clears it" or "writing 1 clears a bit".
And unimplemented offsets alias into the array. An access above the real register count wraps into idx and touches a live register — 25.5 §3's boundary-5 failure.
8. The Failure Timeline
A read-to-clear interrupt status register, behind the wrong RTL, with a driver that re-reads on a retry path.
cycle 0 hardware sets STATUS bit 3 (a completion event occurred)
cycle 40 driver reads offset 0x10 -> 0x08. Correct.
The device's INTENT is that this read clears the bit.
The wrong RTL does not clear it — it is an array.
cycle 41 driver's retry wrapper re-reads 0x10 -> 0x08 again
cycle 42 driver handles the SAME event twice
(symptom A: duplicate event processing)
--- now the same wrong model on the DRIVER side, with correct RTL ---
cycle 100 hardware sets STATUS bit 3
cycle 140 driver reads 0x10 -> 0x08. Correct RTL clears the bit.
cycle 141 retry wrapper re-reads 0x10 -> 0x00
cycle 142 driver uses the SECOND response. Event lost.
cycle 200 hardware sets bit 3 again; the cycle repeats
(symptom B: intermittent missed events, load-dependent)The first divergence is cycle 41 in both halves — the moment a second read is issued for an offset where the read is an operation.
§10 measured symptom B at 1,258 lost events at a 2% duplication rate and 6,155 at 10%. Both reads returned successfully; both carried valid data; nothing was logged.
9. Measured — Two Reads of One Offset
| behind the BAR offset | same value | different | events consumed |
|---|---|---|---|
| RAM — genuinely memory | 200,000 | 0 | 0 |
| read-to-clear status register | 139,942 | 60,058 | 60,058 |
| FIFO read port | 139,942 | 60,058 | 60,058 |
The read-to-clear and FIFO rows are identical, and that is worth noticing: from the outside, "reading changed it" is the only observable, and it does not tell you which. The register map tells you which — which is why §16's review question asks for it explicitly.
10. Measured — What a Duplicated Read Costs
| behind the offset | duplication rate | posted events | events lost |
|---|---|---|---|
| RAM | 0% / 2% / 10% | 59,790 | 0 / 0 / 0 |
| read-to-clear | 0% | 59,790 | 0 |
| read-to-clear | 2% | 59,790 | 1,258 |
| read-to-clear | 10% | 59,790 | 6,155 |
Three readings.
RAM is immune at every rate. Both responses carry the same value, so consuming either is correct. This is exactly the property the myth generalises from.
The read-to-clear loss scales linearly with duplication. It is not a threshold effect and there is no safe rate.
And the loss is invisible. Two successful reads, two valid responses, no error at any layer — 25.5 §4's Direction B reached by an entirely different route.
11. The Corrected RTL
// A BAR target that expresses BEHAVIOUR per offset rather than storage.
// The register map is IMPLEMENTATION POLICY; what is structural is that each
// offset declares what a read and a write mean.
package bar_tgt_pkg;
typedef enum logic [2:0] {
T_RAM = 3'd0, // plain storage; reads idempotent
T_RO = 3'd1, // read-only; writes ignored (and reported)
T_RTC = 3'd2, // read-to-clear; the READ is an operation
T_W1C = 3'd3, // write-1-to-clear
T_FIFO = 3'd4, // the read pops
T_DOORBELL = 3'd5, // the WRITE is a command
T_NONE = 3'd6 // unimplemented — defined response, never an alias
} tgt_kind_e;
endpackagemodule bar_target_correct #(
parameter int unsigned NREG = 64,
parameter int unsigned WORDS = 1024
)(
input logic clk,
input logic rst_n,
input logic req_valid,
input logic req_is_write,
input logic [31:0] req_offset,
input logic [31:0] req_wdata,
input logic [3:0] req_be,
output logic req_ready,
output logic [31:0] rsp_rdata,
output logic rsp_valid,
input logic rsp_ready,
output logic rsp_unsupported, // an unimplemented offset — REPORTED
output logic wr_to_ro, // a write to a read-only offset
// event source
input logic [31:0] hw_status_set
);
import bar_tgt_pkg::*;
logic [31:0] ram [WORDS];
logic [31:0] status; // the read-to-clear register
logic [31:0] w1c;
tgt_kind_e kind;
logic [$clog2(WORDS)-1:0] ram_idx;
logic accepted, rd_accepted;
// Offset decode is a MAP, not an index. An offset with no target becomes
// T_NONE and is answered explicitly — it never aliases into the array,
// which is the wrong RTL's third silent failure (§7).
always_comb begin
ram_idx = req_offset[$clog2(WORDS)+1:2];
unique case (req_offset[11:2])
10'h004: kind = T_RTC;
10'h005: kind = T_W1C;
10'h006: kind = T_FIFO;
10'h008: kind = T_DOORBELL;
10'h009: kind = T_RO;
default: kind = (req_offset[11:2] < NREG) ? T_RAM : T_NONE;
endcase
req_ready = !rsp_valid || rsp_ready;
accepted = req_valid && req_ready;
rd_accepted = accepted && !req_is_write;
rsp_unsupported = accepted && (kind == T_NONE);
wr_to_ro = accepted && req_is_write && (kind == T_RO);
end
// Byte-enable-aware write helper. The wrong RTL stores the whole word,
// which destroys neighbouring fields in a packed register (§7).
function automatic logic [31:0] apply_be(input logic [31:0] old,
input logic [31:0] neu,
input logic [3:0] be);
apply_be = old;
for (int b = 0; b < 4; b++)
if (be[b]) apply_be[b*8 +: 8] = neu[b*8 +: 8];
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
status <= '0; w1c <= '0; rsp_valid <= 1'b0; rsp_rdata <= '0;
end else begin
if (rsp_valid && rsp_ready) rsp_valid <= 1'b0;
// Hardware sets status bits independently of software access. The
// set and the read-clear can land in the SAME cycle — §12 audit A.
status <= (status | hw_status_set);
if (accepted) begin
rsp_valid <= !req_is_write;
unique case (kind)
T_RAM: begin
if (req_is_write) ram[ram_idx] <= apply_be(ram[ram_idx], req_wdata, req_be);
else rsp_rdata <= ram[ram_idx];
end
T_RTC: begin
if (!req_is_write) begin
rsp_rdata <= status;
// THE READ IS THE OPERATION. The set from this same cycle must
// survive the clear, or an event that arrived while the read
// was in flight is destroyed (§12 audit A).
status <= hw_status_set;
end
end
T_W1C: begin
if (req_is_write) w1c <= w1c & ~apply_be('0, req_wdata, req_be);
else rsp_rdata <= w1c;
end
T_RO: if (!req_is_write) rsp_rdata <= 32'hA5A5_0000;
T_DOORBELL: ; // the write is consumed as a command
T_NONE: if (!req_is_write) rsp_rdata <= 32'hFFFF_FFFF;
default: ;
endcase
end
end
end
endmoduleThe six lenses.
ARCHITECTURE. The module exists because an address window and a behaviour are different things (§3). Collapsing them is the myth.
STATE. ram (storage), status (event accumulator with a destructive read), w1c (software-cleared), and the response register. Only ram is memory.
EVENT. hw_status_set sets bits asynchronously to software; an accepted read of T_RTC clears them. Two independent writers, which is §12 audit A.
CONTRACT. The driver relies on a read of the status offset reporting and consuming the events, exactly once. Any duplication of that read breaks the contract, which is why the register map must say so.
FAILURE. If the set and the clear are not combined in the same cycle, an event arriving during the read is destroyed — silently, at a rate proportional to event frequency.
DV / DEBUG. §14's p3_rtc_read_consumes and p4_no_event_lost_on_read catch it; §15's scoreboard must model the read as an operation rather than a lookup.
12. Same-Cycle Audit
13. Debugging
One more debugging note, because it is the cheapest test in the chapter. 25.9 can settle this instantly: the trace shows two reads where the driver author believes there is one. This is one of the few myth-driven bugs where the analyzer is the right first instrument, because the fault is genuinely on the link.
14. Assertions
// P1 — an unimplemented offset never aliases into storage. This is the
// wrong RTL's silent third failure: an out-of-range index wrapping into a
// live register (§7). Catches it structurally rather than by testing every
// offset.
property p1_no_alias_on_unimplemented;
@(posedge clk) disable iff (!rst_n)
(accepted && (kind == bar_tgt_pkg::T_NONE)) |-> rsp_unsupported;
endproperty
a_p1: assert property (p1_no_alias_on_unimplemented);
// P2 — a write to a read-only offset is reported, not silently absorbed.
// Absorbing it makes a driver bug invisible for the life of the product.
property p2_ro_write_reported;
@(posedge clk) disable iff (!rst_n)
(accepted && req_is_write && (kind == bar_tgt_pkg::T_RO)) |-> wr_to_ro;
endproperty
a_p2: assert property (p2_ro_write_reported);
// P3 — a read of the read-to-clear register consumes exactly what it
// reported, and nothing else. The invariant is "the read is the operation".
// |=> because the clear lands on the NEXT edge, after the NBA.
property p3_rtc_read_consumes;
@(posedge clk) disable iff (!rst_n)
(rd_accepted && (kind == bar_tgt_pkg::T_RTC))
|=> (status == $past(hw_status_set));
endproperty
a_p3: assert property (p3_rtc_read_consumes);
// P4 — an event set in the same cycle as the read is NOT destroyed. This is
// §12 audit A, and it is the property that separates the correct RTL from
// the plausible-but-lossy `status <= '0`.
// Assumption: hw_status_set is a per-cycle pulse, not a level.
property p4_no_event_lost_on_read;
@(posedge clk) disable iff (!rst_n)
(rd_accepted && (kind == bar_tgt_pkg::T_RTC) && (hw_status_set != '0))
|=> ((status & $past(hw_status_set)) == $past(hw_status_set));
endproperty
a_p4: assert property (p4_no_event_lost_on_read);
// P5 — no second request is accepted while a response is outstanding.
// Without it the device duplicates its own destructive read (§12 audit B).
property p5_one_outstanding;
@(posedge clk) disable iff (!rst_n)
(rsp_valid && !rsp_ready) |-> !accepted;
endproperty
a_p5: assert property (p5_one_outstanding);
// P6 — byte enables are honoured. Vacuity risk is real here: if the
// testbench only issues full-word writes, this never evaluates. The cover
// below is what proves it did.
property p6_partial_write_preserves;
@(posedge clk) disable iff (!rst_n)
(accepted && req_is_write && (kind == bar_tgt_pkg::T_RAM) && (req_be != 4'hF))
|=> (ram[$past(ram_idx)] != $past(req_wdata) || $past(req_be) == 4'hF);
endproperty
a_p6: assert property (p6_partial_write_preserves);
c1_partial_write: cover property (@(posedge clk) disable iff (!rst_n)
accepted && req_is_write && (req_be != 4'hF));
c2_set_during_read: cover property (@(posedge clk) disable iff (!rst_n)
rd_accepted && (kind == bar_tgt_pkg::T_RTC) && (hw_status_set != '0));
c3_unimplemented: cover property (@(posedge clk) disable iff (!rst_n) rsp_unsupported);P4 is the one to insist on, and c2_set_during_read is what makes it non-vacuous. A testbench that never sets a status bit during a read satisfies P4 without evaluating it, and that is precisely the stimulus a designer holding the myth would write.
15. If DV Believes the Same Myth
The testbench becomes wrong in the same direction, and this is the part that lets the bug reach silicon.
| DV artefact | what the myth makes it do | consequence |
|---|---|---|
| reference model | a logic [31:0] mem[] array, updated on writes | predicts the second read returns the same value — matches the wrong RTL exactly |
| scoreboard | compares read data against the model's array | agrees with the DUT, because both hold the myth |
| stimulus | write-then-read-back sequences | never issues two reads of a status offset |
| coverage | offsets touched, read/write mix | 100% coverage with the side-effect behaviour never exercised |
| assertions | "a read returns what was last written" | encodes the myth as a checked property |
The last row is the failure mode that matters. An assertion derived from the wrong model does not merely miss the bug — it actively certifies it, and it will fire on the correct RTL, which is how a good implementation gets "fixed" into a broken one.
What a correct testbench does differently. The reference model holds, per offset, a behaviour rather than a value — and its read() method is allowed to mutate state. That single change is the DV expression of §3's replacement model.
16. Review and Interview
The review gate this myth corrupts. An architecture review that accepts "BAR 0 is a 4 KB register region" has not established anything about read semantics.
The review question: "For every offset in this BAR, does a read have a side effect — and is the region marked prefetchable?"
Those two must be answered together. A prefetchable attribute on a region containing a read-to-clear register is a latent bug that the platform is licensed to trigger at any time.
The interview exchange.
Weak answer: "A BAR maps device memory into the address space."
Why it sounds plausible: it is how the driver uses it, and it is true for most offsets.
Interviewer follow-up: "I read the same offset twice and get different values. Is the device broken?"
Where the weak model breaks: it has no way to answer yes-or-no, because under the myth a second read must return the same value.
Strong answer: "A BAR claims an address range; the offset selects a behaviour the device defines. If two reads differ, the likely explanation is that the read has a side effect — a read-to-clear status register or a FIFO port — and that is normal. What I'd want to know is whether the register map documents it, and whether the region is marked prefetchable, because a prefetchable region containing a destructive read is a bug waiting for the platform to speculate."
Senior follow-up: "Your driver reads that register once. Where else could a second read come from?" — a retry wrapper, a speculative prefetch, a debug or crash-dump path that walks the BAR, or a second CPU polling concurrently. §10 measured the cost at 2% and 10% duplication rates.
17. Misconceptions Inside the Misconception
"If it's not memory, the device must return an error for a repeated read." Why it sounds plausible: a changed value feels like a fault, and faults are reported. What really happens: both reads are entirely legal accesses that complete successfully (§9). There is nothing to report — the second read correctly returned the register's current value, which is zero. What it causes: the wrong diagnosis of §13 — an engineer looking for an error that does not and should not exist.
"Marking the BAR non-prefetchable is a performance decision." Why it sounds plausible: the attribute's name is about prefetching, which is a performance mechanism. What really happens: for a region containing side-effecting offsets it is a correctness decision. It tells the platform it may not speculate, combine or re-issue. What it causes: the attribute set for throughput on a region where it licenses destructive access.
"Byte enables only matter for narrow buses." Why it sounds plausible: they are a bus-width mechanism, and modern paths are wide. What really happens: they express which bytes the host intended to write (§7). Ignoring them turns a one-byte write into a four-byte write over neighbouring fields. What it causes: a driver updating one field of a packed control register and silently resetting three others.
"A FIFO behind a BAR is unusual." Why it sounds plausible: the mental model of a BAR is a register block. What really happens: it is ordinary — §9 measured it producing exactly the same two-read signature as a read-to-clear register. What it causes: a driver that reads the FIFO port twice to "confirm" a value, discarding an entry each time.
18. Understanding Check
Q1. You read one BAR offset twice and get 0x08 then 0x00, with no other activity. Is the device broken? Give the reasoning, not the verdict.
Almost certainly not (§5, §9). Under the myth this is impossible, which is what makes it the disproof. The likely explanation is that the read has a side effect — a read-to-clear status register or a FIFO port, both of which produce this exact signature. §9 measured both at 60,058 differing pairs in 200,000 trials while RAM produced zero. The question that settles it is not a measurement but a document: what does the register map say offset 0x10 does on a read? And the follow-up is whether the region is marked prefetchable, because if it is, the platform may already be duplicating those reads.
Q2. A driver misses events intermittently under load. The device's status bit is provably being set. Where do you look, and why does the myth send you elsewhere?
Look for a second read of the status offset (§8, §13). The myth says a read is an observation, so an engineer holding it concludes the bit was never set and investigates event generation — which is working. The first divergence is the duplicated read, and its sources are a retry wrapper, a speculative prefetch licensed by a prefetchable attribute, a crash-dump path walking the BAR, or a second poller. §10 measured 1,258 lost events at a 2% duplication rate. The cheapest evidence is a device-side counter of reads to that offset compared against events posted; if reads exceed events beyond the idle-poll rate, something is duplicating them.
Q3. Why is a reference model built as an array not merely incomplete but actively harmful?
Because it agrees with the wrong RTL (§15). A model holding mem[offset] predicts that a second read returns the same value; the wrong RTL does the same; the scoreboard compares them and passes. The testbench and the design share the misconception, so nothing disagrees. Worse, an assertion derived from that model — "a read returns what was last written" — will fail on the correct RTL, which is the mechanism by which a good implementation gets "fixed" into a broken one. The correct model stores a behaviour per offset and permits its read() to mutate state.
Q4. In §11's corrected RTL, why is the read branch status <= hw_status_set rather than status <= '0'?
Because an event can arrive in the same cycle as the read (§12 audit A, P4). The response carries the value sampled before this cycle's set, so a bit arriving now is correctly not reported — but it must survive to be reported by the next read. status <= '0' destroys it: it was set, it was not reported, and it is gone. The rate of loss is proportional to event frequency, which makes it a load-dependent intermittent bug. c2_set_during_read is what proves the testbench actually exercised the case, because a stimulus that never sets a bit during a read satisfies P4 vacuously.
Q5. State the replacement model in one sentence and show it handles a doorbell.
"A BAR names an address range the device will answer for; the offset within it selects a behaviour the device defines, and only some of those behaviours are storage" (§3). For a doorbell the behaviour is that the write is a command — writing a value does not store it, and reading the offset back does not return it. Under the myth a doorbell is incoherent ("I wrote 5 and read back 0, the register is broken"); under the replacement model it is unremarkable, and the only question is what the map says the write means. The sentence survives because it never promised storage in the first place.
19. What Comes Next
| Chapter | The myth it corrects |
|---|---|
| 31.1 | "PCIe is just a faster PCI" |
| 31.2 | "PCIe is memory-mapped only" |
| 31.3 (this) | "BARs contain memory" — a BAR claims addresses; behaviour is per offset |
| 31.4 | "DMA bypasses PCIe protocol" |
| 31.5 | "MSI is just a software interrupt" |
| 31.6 | "LTSSM only matters during boot" |
This chapter was about what an address means. 31.4 is about what a transfer is — the belief that DMA is a separate fast path that skips the protocol, when it is the same Memory Reads and Writes this chapter's BAR answers, differing only in who initiates them.