Three driver bugs repeat on every chip: a byte-wise write to a register that only accepts full words, a byte-swapped value read from a peripheral, and an interrupt that arrives before the write it announces has landed. All three are contract violations, not hardware faults, and all three are prevented by rules short enough to print on one page. This lesson states NanoSoC's page.
NanoSoC is little-endian and word-oriented, and the worked example is worth memorising: storing the word 0x1122_3344 at 0x2000_0000 leaves the byte at 0x2000_0000 reading 0x44, the byte at 0x2000_0001 reading 0x33, and the halfword at 0x2000_0002 reading 0x1122. Peripheral registers are 32-bit and word aligned, and the APB carries no byte strobes and no protection signals, which has a hard consequence: the AXI-to-APB bridge cannot express a partial write, so it drops the master's write strobes and the APB slave sees a full 32-bit write whose other three bytes are whatever the master happened to drive on those lanes. A byte store to DMA KEY therefore does not update one byte; it overwrites all four with lane garbage. The bridge policy must be stated, not discovered: partial-strobe writes to a slot are a software error to be caught in review and test, and the preferred hardware behaviour is to answer them with SLVERR rather than silently widen them; either way it must be written in the contract. In practice: every register write is a full-word read-modify-write in software, never read-modify-write on a register that mixes write-1-to-clear status with configuration, and never let the compiler merge or split register accesses, which is why driver code uses volatile accesses of exactly the register width.
Rule | NanoSoC form | Why |
|---|---|---|
Endianness | Little-endian everywhere on the fabric | One convention at the bridge; no swizzle anywhere |
Register access width | Exactly 32-bit loads and stores to slots | APB has no byte strobes; partial writes do not exist |
Field updates | Read-modify-write, never on mixed status registers | A blind writeback can clear status or retrigger actions |
Ordering to devices | Device-type in the CPU memory attributes: non-cacheable, non-bufferable, in-order | Two accesses to the device complete in program order |
Completion check | Read back after clearing an interrupt source | Proves the clear reached the device; does not by itself drain the interrupt synchronisers (see text) |
Ordering deserves the worked numbers, because the rule software most often gets wrong is what a readback proves. Device-type regions are non-bufferable and in-order, so when a write to a device register completes, the slave has accepted it: there are no posted writes hiding in the fabric on this path. A readback of the same block (125 ns for a DMA or SBR register) therefore proves the clear reached the device. It does not prove the CPU has stopped seeing the interrupt, because the level still has to travel from the DMA on clk_sys through a two-flop synchroniser into clk_per, the interrupt controller's output register, and a second two-flop synchroniser into clk_cpu: 135.4 ns at best and 182.3 ns at worst, counted from the moment the clear lands in the DMA. The script below puts both timelines on one axis. The decomposition of the write is illustrative: it assumes a write costs the same 125 ns as a read, with the clear landing at the end of the APB access.
T_CPU = 1000 / 192 # ns
T_SYS = 1000 / 96 # ns
# CPU write to a DMA register, assumed to cost the same as the 125 ns read.
land = 1 * T_CPU + (3 + 1 + 1 + 3) * T_SYS # issue, crossing, xbar, bridge accept, APB access
write_done = 24 * T_CPU # response is back at the CPU
readback_done = write_done + 24 * T_CPU # then the 125 ns readback
best = land + 26 * T_CPU # level seen at the CPU, best case (135.4 ns after landing)
worst = land + 35 * T_CPU # level seen at the CPU, worst case (182.3 ns after landing)
assert round(land, 1) == 88.5 and round(write_done, 1) == 125.0
assert round(readback_done, 1) == 250.0
assert round(best, 1) == 224.0 and round(worst, 1) == 270.8
stale_after_readback = max(0.0, worst - readback_done)
assert round(stale_after_readback, 1) == 20.8 # one spurious entry is still possible
assert best < readback_done < worst # the readback does not settle the question
print("PASS: readback returns at 250.0 ns; the old level can persist to 270.8 ns")So the readback returns at about 250 ns, while the CPU can still see the old level until about 224 ns at best and 271 ns at worst: for up to roughly 21 ns after the handler believes the source is cleared, one more, spurious, entry is possible. The correct closing sequence is: clear the source, read the block back to prove the clear landed, then re-read the interrupt controller's PENDING register, which sits on clk_per, until the bit reads zero. That read takes 281.25 ns, longer than the remaining synchroniser path, so the CPU's own level has dropped by the time it returns. The alternative is to tolerate and dismiss one spurious entry that finds nothing pending. Where a CPU provides explicit barrier instructions, they belong at the data boundaries instead: after filling a DMA buffer and before ringing the doorbell.
Driver code uses generated-width volatile accesses to registers; no byte pointers into slots anywhere.
Every interrupt clear is followed by a readback of the same block and then a PENDING re-read, or the handler is written to tolerate one spurious entry.
Buffer-then-doorbell sequences for DMA are separated by the ordering the fabric actually guarantees, stated in the driver guide.
Test the rules, do not assume them: store 0x1122_3344 in SRAM0 and read the bytes and halfwords back against the worked example, attempt a byte write to DMA KEY at the bus monitor and confirm the slave sees a full-word write, and clear an interrupt source under load to observe the one tolerated spurious entry arriving within the synchroniser window. If a driver works in simulation and fails on the bench, width and ordering are the first two suspects, ahead of the hardware.
First, slot accesses are full 32-bit words: the APB has no byte strobes and the bridge drops the write strobes, so the byte store becomes a full-word write that overwrites the other three bytes with whatever the master drove on those lanes (or is refused with SLVERR if the bridge policy says so). Second, even as a word read-modify-write it must not touch a mixed register blindly; DIR is plain read-write so it is safe here, but the habit fails on status registers.