NanoSoC needs no width converter: every AXI link is 32 bits wide. The strobe question does not go away, though. It moves to the AXI-to-APB bridge, where APB3 carries no strobes and the DMA has no PSTRB port, so a byte store to a peripheral register reaches the block as a word write unless the bridge says otherwise. A bigger chip adds the classic version of the same failure: a width converter that mishandles byte strobes corrupts neighbouring bytes on partial writes, green in full-word tests and red in the field. Both cases need the same discipline, so this lesson covers the narrowing pattern done correctly and then the one protocol adapter NanoSoC really has.
A narrowing adapter maps each wide beat onto narrower beats with the strobes following the data: the high half of a 64-bit write with its upper four strobes becomes one 32-bit beat, the low half with its lower four strobes another, and the order of the two beats matches the address order so that per-ID ordering is preserved downstream. A widening adapter does the reverse and must synthesise strobes for narrow bursts explicitly: a single-byte write becomes a full-width beat with exactly one strobe set, never a read-modify-write the adapter invented, because an invented read changes ordering and fault behaviour. Endianness is fixed at the same boundary: NanoSoC is little endian throughout, and any future big-endian block gets its lane swizzle in its own wrapper, verified with byte and halfword accesses since full-word tests pass straight through a swizzle bug.
// Datapath slice of a narrowing adapter (64-bit producer to a 32-bit link).
// One wide beat becomes two narrow beats in address order; strobes
// follow their data half. The beat counter that drives hi_half, the
// burst-length rewrite and the response merge are not shown.
module narrow_adapter (
input logic [63:0] wdata_in,
input logic [7:0] wstrb_in,
input logic hi_half,
output logic [31:0] wdata_out,
output logic [3:0] wstrb_out
);
assign wdata_out = hi_half ? wdata_in[63:32] : wdata_in[31:0];
assign wstrb_out = hi_half ? wstrb_in[7:4] : wstrb_in[3:0];
endmoduleA real narrowing adapter also splits the burst (it rewrites AxLEN and AxSIZE), may skip a beat whose strobes are all zero, and keeps ordering; the sketch is only the datapath slice. The checker below proves the mapping that slice implements against an independent byte-lane model: for each of the 256 strobe patterns, byte lane i of the wide beat must appear as lane i mod 4 of narrow beat i div 4, with the same strobe bit, and nothing else. The test is not allowed to pass for the wrong reason, so it also runs a deliberately broken adapter (halves swapped) and requires that it is caught.
def narrow(wdata, wstrb, hi):
# Mirrors the narrow_adapter sketch: split data and strobes by half.
if hi:
return (wdata >> 32) & 0xFFFF_FFFF, (wstrb >> 4) & 0xF
return wdata & 0xFFFF_FFFF, wstrb & 0xF
def narrow_swapped(wdata, wstrb, hi):
# Broken variant: strobes of the other half (a classic wiring slip).
d, _ = narrow(wdata, wstrb, hi)
return d, (wstrb & 0xF) if hi else (wstrb >> 4) & 0xF
WDATA = 0xDEAD_BEEF_1234_5678
def check(fn):
for wstrb in range(256):
lanes = [(wstrb >> i) & 1 for i in range(8)] # reference model
for hi in (0, 1):
d, s = fn(WDATA, wstrb, hi)
for b in range(4):
lane = 4 * hi + b
assert ((s >> b) & 1) == lanes[lane], (hex(wstrb), hi, b)
want_byte = (WDATA >> (8 * lane)) & 0xFF
assert ((d >> (8 * b)) & 0xFF) == want_byte, (hex(wstrb), hi, b)
check(narrow)
caught = False
try:
check(narrow_swapped)
except AssertionError:
caught = True
assert caught, "the broken adapter must be rejected"
print("PASS: all 256 strobe patterns map lane by lane; swapped-strobe adapter is caught")NanoSoC has one protocol adapter, nano_axi2apb with the slot decoder behind it. It serialises an AXI burst into single APB accesses, maps PSLVERR to an AXI SLVERR response, and takes three clocks per access for a slave with the DMA's one wait state. AxPROT[1] goes to the slot decoder as a side signal, because APB3 has no PPROT to carry the secure attribute any further. Strobes are lost at this bridge: APB3 has no PSTRB, and the DMA has no PSTRB port, so every write is a full 32-bit write. A byte or halfword store from the CPU therefore reaches the peripheral as a word write, and the bytes the CPU did not mean to change are overwritten with whatever the write data lanes carry. The bridge has three legal answers: complete it as a word write, merge it with a read first, or refuse it. The review must see one of them written down; refusing (SLVERR for any partial strobe) is the least surprising, and software that needs byte access to a register is rewritten to use whole words.
# One 32-bit peripheral register behind nano_axi2apb. A byte store to
# lane 0 arrives with WSTRB = 0b0001 and write data lanes 1..3 not meaningful.
def bridge_write(reg, wdata, wstrb, policy):
"""Return (new_reg, response). APB3 has no strobes to forward."""
if policy == "refuse":
if wstrb != 0xF:
return reg, "SLVERR"
return wdata, "OKAY"
if policy == "word": # the silent behaviour: strobes dropped
return wdata, "OKAY"
raise ValueError(policy)
REG = 0xAABB_CC00 # neighbour bytes hold live data
BYTE_STORE = 0x0000_0011 # software meant: low byte = 0x11
new, resp = bridge_write(REG, BYTE_STORE, 0b0001, "word")
assert new == 0x0000_0011 and resp == "OKAY" # neighbours destroyed
new, resp = bridge_write(REG, BYTE_STORE, 0b0001, "refuse")
assert new == REG and resp == "SLVERR" # nothing changes, error seen
new, resp = bridge_write(REG, 0x1234_5678, 0xF, "refuse")
assert new == 0x1234_5678 and resp == "OKAY" # full words still work
print("PASS: word policy loses 3 neighbour bytes; refuse policy returns SLVERR and keeps them")The strobe-mapping proof above, or its widening equivalent, rerun for the exact widths shipped.
Byte, halfword and word accesses from software through every converter, with neighbours checked, not just the written bytes.
The written rule for partial strobes at the AXI-to-APB bridge, plus a software test that does a byte store to a register and then reads it and its neighbour back.
No converter invents reads: partial writes synthesise strobes, never a read-modify-write cycle.
Randomise transfer size and alignment against a scoreboard that checks every byte lane: a strobe failure needs narrow random traffic with neighbour checking, and directed full-word tests will never find it. On NanoSoC add the directed byte-store test through the APB bridge. If neighbours corrupt, dump the strobe bits beside the data beats; the offender is the adapter whose output strobes enable bytes the input strobes did not.
No. No width converter is needed, but APB3 has no strobes, so a byte store through nano_axi2apb becomes a word write unless the bridge refuses it. The review needs the partial-strobe rule written down and a byte-store test, and the width table must still show 32 bits on every link so a future wider port is flagged.