A 64-bit bus moves 8 bytes per beat. Software wants to write 3 bytes at address 0x05, perhaps one field of a packed structure. The memory word at 0x00 holds 8 bytes, and 5 of them belong to other fields that must not change. The bus has to carry "write these 3 bytes, leave the other 5 alone" in a single beat.
How does a wide bus write fewer bytes than its width, or bytes that do not start at the beginning of a word?
The obvious way is read-modify-write: read the whole 8-byte word, replace 3 bytes, write all 8 back. It works, but it costs a full read round trip for every partial write, and it is not safe: if anything else writes the other 5 bytes between the read and the write-back, those changes are overwritten. A partial write needs to say, in the write itself, which bytes to change.
Treat a W-byte data bus as W byte lanes. Lane n carries the byte whose address, within the aligned W-byte word, is n. Each write beat carries one strobe bit per lane (also called a byte enable): 1 means "write this lane's byte", 0 means "leave that byte in memory unchanged". The target writes only the strobed bytes, so no read is needed and the other bytes are never touched.
The generator turns a write of any length at any address into beats and strobes on an 8-byte bus, and does the same for a narrow burst, whose beats are smaller than the bus.
"""Byte strobes for writes on a 64-bit (8-byte) data bus.
Each beat carries one aligned 8-byte word; strobe bit n says byte lane n holds a
byte to write (lane n = address offset n within the word). A write of `length`
bytes starting at `addr` becomes one beat per word it touches.
"""
LANES = 8
def write_beats(addr, length):
out, a, end = [], addr, addr + length
while a < end:
word = a - a % LANES
last = min(end, word + LANES)
strobe = sum(1 << (b - word) for b in range(a, last))
out.append((word, strobe))
a = last
return out
def narrow_beats(addr, size, n):
"""An INCR burst of n beats, each `size` bytes, on the 8-byte bus."""
out = []
for i in range(n):
a = addr + i * size
word = a - a % LANES
out.append((word, sum(1 << (b - word) for b in range(a, a + size))))
return out
def show(title, rows):
print(title + ": " + ", ".join(f"word 0x{w:02X} strobe {s:08b}" for w, s in rows))
return rows
r1 = show("3 bytes at 0x05", write_beats(0x05, 3))
r2 = show("6 bytes at 0x05", write_beats(0x05, 6))
r3 = show("2-byte INCR4 at 0x02", narrow_beats(0x02, 2, 4))
assert r1 == [(0x00, 0b11100000)]
assert sum(bin(s).count("1") for _, s in r2) == 6 and len(r2) == 2
assert [s for _, s in r3] == [0b00001100, 0b00110000, 0b11000000, 0b00000011]
print("PASS: strobes mark exactly the bytes written; an unaligned write spills into a second beat")It prints:
3 bytes at 0x05: word 0x00 strobe 11100000
6 bytes at 0x05: word 0x00 strobe 11100000, word 0x08 strobe 00000111
2-byte INCR4 at 0x02: word 0x00 strobe 00001100, word 0x00 strobe 00110000, word 0x00 strobe 11000000, word 0x08 strobe 00000011
PASS: strobes mark exactly the bytes written; an unaligned write spills into a second beat3 bytes at 0x05 fit in one beat: the word at 0x00, lanes 5 to 7.
6 bytes at 0x05 are unaligned and cross a word boundary, so they spill into a second beat: lanes 5 to 7 of word 0x00, then lanes 0 to 2 of word 0x08.
A narrow burst of 2-byte beats on the 8-byte bus uses a different pair of lanes each beat (lanes 2–3, 4–5, 6–7), then moves to the next word. The data travels in the lanes its address selects, not always in the low lanes.
Approach | Cost | When |
|---|---|---|
Strobes on every write beat | One wire per byte lane | Memory-like targets that hold data for many owners |
Read-modify-write in the target | A read per partial write; must be atomic inside the target | Memories with error-correcting codes over the whole word, which must recompute the code anyway |
Whole words only, no strobes | Software may not do partial writes | Simple links and register blocks where every access is a full word |
Error-correcting memories are the interesting case: a code computed over 8 bytes cannot be updated from 3 of them, so the memory controller must read, merge and rewrite internally even though the bus carried strobes.
Protocol | Partial writes |
|---|---|
AXI | WSTRB: one bit per byte lane, per beat; strobes may differ from beat to beat; a narrow transfer uses only the lanes its size and address select. |
APB | PSTRB from APB4; earlier APB versions wrote whole words. |
DCLink | Whole 32-bit flits only, no strobes. A design that needed byte writes would add four strobe bits per data flit. |
A bridge (illustrative) converted narrow writes by always placing the data in the low byte lanes, ignoring the address. A 2-byte write to 0x02 arrived at the memory in lanes 0–1 with strobes for lanes 0–1, and overwrote the bytes at 0x00 instead. Software saw corrupted neighbouring fields, intermittently, depending on alignment. The rule is: a byte's lane is fixed by its address, and the strobe marks that lane.
Two beats: word 0x00, lanes 6–7 (11000000); word 0x08, lanes 0–1 (00000011).
Another writer can change the other bytes between the read and the write-back, and the write-back overwrites its change. Strobes avoid the read entirely.
A wide bus is a row of byte lanes; a byte's lane is set by its address within the aligned word.
Strobes mark which lanes to write, so a partial write needs no read and touches no other bytes.
Unaligned writes can spill into a second beat; narrow bursts move across the lanes.