SANBlaze I3C Controller Platform
V1531 Release Notes
Release Date: July 2026
Overview
V1531 is a stability and compatibility release focused on unblocking customer NVMe-MI operations over I3C. The headline fix is reliable multi-chunk MCTP delivery: back-to-back writes (VPD Write, NVMe-MI FW Download) now complete without the intermittent mid-stream address NAKs that V1530 hit. Delivery runs at full bus speed, and the occasional NAK is recovered by automatic per-packet retry rather than by a fixed pre-emptive delay on every burst. A full 4.25 MB NVMe firmware image downloads end-to-end without intervention in about a minute, and any packet that cannot be delivered even after retry is now reported as a hard failure rather than passing silently.
V1531 preserves the V1530 read contract and command surface. Existing host code targeting V1530 runs unchanged.
What’s New
fw_download — direct NVMe-MI FW image download over I3C
New CLI command that streams an NVMe-MI FW Image Download to the drive
directly over I3C, without the mi/SMBus host tool chain. It builds each
4 KB FW Download command, fragments it into MCTP packets with the required
SMBus PEC on each packet, and — with --read_to_ring — reads back and
validates the drive’s NVMe-MI status after every chunk, stopping
immediately (and reporting the offset and decoded NVMe status) if the drive
rejects one. Progress is shown as a single in-place progress bar.
fw_download is download-only: it stages the image in the drive’s download
buffer and does not issue a Firmware Commit / activate. No manual Maximum
Write/Read Length step is needed — the drive’s default permits MCTP and the
required per-packet SMBus PEC is applied automatically. A drive power cycle
clears the download buffer, avoiding the Overlapping Range rejection on the
first chunk. Full command reference and examples are in User Guide §7 and §12.
The existing mi-based fw_dl.sh path (with I2C fallback) remains available.
Full-speed burst delivery with automatic NAK retry
Multi-chunk MCTP writes transmit at full bus speed with no pre-emptive inter-burst delay. When the drive NAKs a packet’s address phase, the firmware backs off briefly and retries that same packet — replaying only the address phase, so no data is ever double-sent — until the address is accepted or a short per-packet ceiling is reached. This recovers the drive-recovery-time NAK that caused mid-stream chunks (e.g. chunks 3–4 of a 5-chunk MCTP message) to fail intermittently, without taxing every burst with a fixed wait.
The practical effect is throughput: a full 4.25 MB firmware image downloads in about a minute, versus roughly three minutes previously. In current testing the retry path is rarely exercised — the drive accepts full-speed writes cleanly across a whole image — so retry serves as reliability insurance rather than a routine cost. The earlier fixed 2 ms inter-burst pacing floor is retained in the code but disabled by default, and can be restored if a future drive proves to need a fixed recovery window rather than reactive retry.
fw_download reports an unrecoverable packet as a hard failure
If a packet cannot be delivered even after retry — a persistent address
NAK that exhausts the per-packet retry ceiling — the staged image has an
unfilled gap and is corrupt. fw_download now detects this, prints the
download as FAILED with the count of unrecovered packets, and returns
a non-zero exit code so automated flows abort instead of proceeding with a
bad image. A clean download reports its retry statistics on the summary
line — [0 NAK retries] in the normal case — so the count is visible on
every run, including non-verbose ones.
Stability test (soak)
Because fw_download returns a non-zero exit code on an unrecoverable
packet, it can be looped as an unattended stability soak that stops itself
the instant a download fails. Each pass re-initialises — which power-cycles
the drive, clearing its download buffer — then transfers the full image
with per-chunk validation, so every iteration is an independent clean
download:
# Set SLOT and the image path to your setup first.
while sb_i3c_init -d $SLOT -p i3c -c 8000 >/dev/null 2>&1 && sleep 3 && sb_i3c -d $SLOT fw_download 0x1D /path/to/fw-image.bin --read_to_ring; do :; done; echo "STOPPED: exit=$?"
The loop continues while each download exits 0 and stops on the first
non-zero exit — a capped/unrecovered packet (corrupt image) or a failed
re-initialisation — printing the failing exit code. The sleep 3 lets the
freshly power-cycled drive finish its internal initialisation before the
first FW-download transaction; without it the first chunk can stall on a
drive that is enumerated but not yet ready to service MCTP. A healthy bus
runs indefinitely at a steady per-image time with [0 NAK retries];
occasional recovered retries are normal, while a rising retry frequency
across passes is an early indicator of bus degradation.
burst_fire_writes SM-idle wait on the success path
Success path now waits for the initiator PIO state machine to return to its entry point (PC=0) before starting the next packet, mirroring the existing wait on the NAK path. Eliminates the garbled address bytes (0x28, 0x67, 0x55, etc.) previously observed on rapid back-to-back transactions.
SETDASA-specific autonomous-poll quiet window
After a SETDASA the drive prepares for Hot Join per I3C spec — the
resulting SDA-low priming window overlaps our autonomous IBI poll and
would trigger a false ENTDAA. Added a separate 100 ms muzzle
(g_last_setdasa_end_us) gated only after SETDASA operations. The
general 10 ms post-transaction quiet window is unchanged, so MPR and
multi-response MCTP traffic remain responsive.
Ring buffer duplicate-commit suppression
ring_commit_framed now rejects a commit whose content matches the
previous commit within a 50 ms window. Fixes the “got n<21> … got n<21>”
double capture observed when the autonomous poll fires twice on a single
IBI. Downstream effect: subsequent MCTP transactions no longer break
with “no response received” as collateral damage from a duplicated ring
entry.
setdasa CLI now surfaces drive-side NAK
Previously setdasa always reported success even when the drive NAK’d
the operation. V1531 tracks the drive-side status flag in the firmware
(g_setdasa_last_status) and the CLI returns a non-zero exit code when
the drive declines the assignment. Useful for automated bring-up scripts
that need to detect a static-address-not-matching-drive case.
Release script rebuild and update modes
tools/i3c_release.sh gains a -r flag for rebuilding the release
tarball from an in-place-edited release tree (bypassing the standard
“work in _TIP” workflow). Diffs old tarball vs new build file-by-file,
prompts for each change, backs up the old tarball with a timestamp
before regenerating. Filters CVS/ bookkeeping automatically.
A -u (update) flag runs the same flow but never rebuilds the firmware or
CLI binaries — the existing artifacts are packaged as-is. Because the
firmware embeds a build timestamp, a rebuild always produces a byte-different
image even when no source changed; -u is for docs-only releases where that
churn is unwanted. Both -r and the default mode now prompt before building,
defaulting to no.
Build artifacts are also published to the release-tree root, so clean no
longer disturbs their revision history.
sb_i3c_test.sh retry loop on TEST16
TEST16 (60-byte i2ctransfer target-receive loopback) has a
host-system-timing sensitivity that causes intermittent failures on some
configurations. Test now retries up to 3 times; the pass count is noted
in the summary line as (passed on attempt N/3) when a retry was
needed. Root-cause fix deferred to a future release.
What’s Fixed
CCC reads corrupted after MCTP traffic (SBSUPPORT-9999)
After any successful MCTP response read, every subsequent directed CCC read returned data shifted by one byte: the first payload byte was duplicated, each following byte moved right one position, and the last byte was lost.
getpid surfaced this as a PID mismatch — a PID of 02 16 D5 76 1D 6C read
back as 02 02 16 D5 76 1D — and the condition persisted across every
subsequent read until the controller was restarted. The same shift silently
corrupted getstatus, getmwl, and getmrl, which have no expected value to
check against: getmrl reported 260 for a Maximum Read Length of 69, and
getstatus reported 0x0000. Single-byte CCC reads (getbcr, getdcr) were
unaffected, because a duplicated first byte is indistinguishable from the
correct one.
The cause was on the controller, not the drive. The controller’s local target emulation shares the bus with its initiator, and must be held off the wire while the initiator drives a transaction. Around fifteen initiator paths already did this; the seven directed-CCC read paths did not. A completed MCTP transaction leaves the local target armed — and in that state it responded to the arbitration header and CCC opcode of the controller’s own read, contending on the data line and injecting one extra byte at the head of the transfer.
V1531 holds the local target off the bus across all directed-CCC reads and restores it afterwards. No drive-side change is involved, and no power cycle or re-enumeration is needed to recover a controller running affected firmware — a controller restart clears it.
IBI/MCTP response read could truncate the target and overrun its buffer
The IBI and MCTP response read loop terminated on the caller’s buffer size rather than on the target’s own end-of-data signal. If a target sent more than the buffer could hold, the controller stopped clocking mid-transfer and issued a STOP, leaving the target holding data it had not been allowed to send — after which every subsequent read on the bus returned shifted data. The same loop also wrote one byte more than the caller’s buffer could hold, because the leading interrupt byte was not counted against the limit.
Neither was reachable with the response sizes seen in practice, but both were latent: the drive under test reports a Maximum Read Length of 260 and, until the SETMRL fix above, could not be persuaded to lower it — while the MCTP command path called the poll with a 16-byte buffer.
V1531 always reads a target through to its own end-of-data signal and stores only what fits, discarding the remainder. Buffer capacity now determines how much of a response is kept, never whether the bus transaction is correct, so no Maximum Read Length can overrun a buffer or desync a target. Poll buffers are sized at 320 bytes.
SETMRL was silently ignored by the drive
SETMRL has never taken effect. The CLI sent a 2-byte frame — the opcode
followed by the 2-byte Maximum Read Length — and reported OK on every
call. The drive discarded it without complaint.
When a target advertises IBI-payload capability (BCR bit 2), its SETMRL
and GETMRL frames carry a third byte: a 1-byte IBI Payload Size after
the 2-byte length. A 2-byte SETMRL to such a target is malformed and is
dropped. The drive under test reports BCR 0x3E — bit 2 set — so every
SETMRL we ever issued, including the one in the standard bring-up
sequence, was thrown away. The drive simply kept its power-on Maximum Read
Length of 260, and GETMRL faithfully reported it. The value was never
wrong; the write never landed.
V1531 sizes the frame from the target’s BCR, which ENTDAA has already
learned, so no extra bus transaction is needed to decide. SETMRL now
takes, and a Maximum Read Length set to 69, 128, or 256 reads back exactly
as set.
SETMWL / SETMRL verify the value and fail when it does not take
Both commands previously printed OK whenever the CCC reached the wire,
which said nothing about whether the drive honoured it. That is what let
the SETMRL defect above survive undetected.
V1531 reads the value back after every set, prints what the drive actually
reports, and returns a non-zero exit code on any mismatch — the same
pattern as the setdasa NAK fix above. Automated bring-up now fails loudly
instead of proceeding on an unset length. Because GETMWL/GETMRL are
Direct-only CCCs with no broadcast form, a broadcast set is verified against
the first enumerated target.
SETMWL / SETMRL accept both broadcast and directed forms
setmwl <da7> [bytes] Direct CCC 0x89 to that target
setmwl 0x7E [bytes] Broadcast CCC 0x09 to all targets
setmrl <da7> [bytes] [ibi_pay] Direct CCC 0x8A
setmrl 0x7E [bytes] [ibi_pay] Broadcast CCC 0x0A
0x7E — the I3C broadcast address — selects the broadcast form; any other
address selects the spec-correct Direct form. Both are verified on the wire.
An earlier V1531 build used the broadcast form exclusively, on the belief that the Direct opcodes (0x89 / 0x8A) left the drive unresponsive to subsequent MCTP requests. That was a misdiagnosis on our side. The Direct form has now been re-tested against the same drive: it sets the value correctly, reads back correctly, and MCTP traffic runs normally afterwards. The original observation is explained by the two defects fixed in this release — a SETMRL that never landed, and a read path that returned shifted data after MCTP traffic (see below) — which together made a working Direct CCC look like a broken one. No drive-side defect is involved.
GETMWL / GETMRL return proper values
Two CLI/firmware defects, both corrected. First, ccc_getmwl_fill and
ccc_getmrl_fill sent broadcast CCC opcodes (0x0B, 0x0C) — invalid, because
GETMWL/GETMRL have no broadcast form. V1531 uses the correct Direct opcodes
(0x8B, 0x8C).
Second, the CLI decoded the returned length in the wrong byte order, reporting a byte-swapped value (e.g. 17664 for a length of 69). V1531 decodes MSB-first per I3C convention.
GETMRL additionally now reads the third byte when the target advertises
IBI-payload capability, and reports the IBI Payload Size alongside the
length. Reading only two bytes from a three-byte response left a byte
unclocked on the bus.
Note: setting MWL/MRL is not required for MCTP. The drive’s power-on
default already exceeds the packet size, and MCTP — including a full FW
image download — works without any SETMWL call. The real per-packet
requirement is the SMBus PEC, which fw_download applies automatically.
SETMWL/SETMRL remain available for setting a specific maximum when one is
needed.
Duplicated MCTP response capture on MPR sequences
Autonomous IBI poll could fire twice on a single physical bus event when the drive returned an MPR (More Processing Required) status. Both fires committed the same MCTP packet to the ring, and the second commit corrupted the ring state for the next unrelated transaction. See “Ring buffer duplicate-commit suppression” above.
Known Limitations
TEST16 intermittent
The 60-byte i2ctransfer target-receive loopback test may require a
second pass on some host system configurations. The test suite reports
the pass count in the status line as (passed on attempt N/3) when a
retry was used. Not a firmware defect — root cause is host-side USB /
Linux i2c-tiny-usb timing race with the PIO target RX FIFO drain rate.
A deterministic fix is planned for a future release.
FW download throughput and I3C clock
A full 4.25 MB image downloads in roughly 80 seconds with per-chunk
validation enabled — versus ~3 minutes in earlier V1531 builds, and well
ahead of the mi-based fw_dl.sh path. The remaining per-packet cost is
dominated by host/USB transfer overhead, not the I3C wire, so the I3C
clock rate has little effect on total download time: a full image takes
roughly 84 seconds at 4 MHz and roughly 80 seconds at 8 MHz — within a few
percent. Select the I3C clock for reliability, not speed.
The reliable maximum clock is setup-dependent — cabling, bus loading, and any inline instrumentation all affect it — and a moderate clock with margin is preferable to the highest clock that appears to work, since marginal signal integrity can produce a hard hang rather than a clean, recoverable error. To pick a clock on a given setup: bring the bus up at a candidate clock and confirm enumeration succeeds; if it does, that clock is safe for the download; if enumeration fails or is unreliable, step the clock down and retry. See User Guide §7 for the exact procedure.
Download time remains host- and drive-state dependent; the elapsed figure is indicative rather than guaranteed.
HDR-DDR mode not supported
I3C High-Data-Rate DDR mode is present in the PIO source but not wired into the C code paths. V1531 operates in SDR mode only. HDR-DDR is scheduled for a future release.
Compatibility
Firmware
Runs on all iRiser hardware supported by V1530 — no hardware requirement changes.
No new memory or resource requirements beyond V1530.
Host CLI (sb_i3c)
ABI/API unchanged from V1530 for all pre-existing commands except
setmwl/setmrl(see below).New commands:
ring_read,set_entdaa_start,fw_download(see User Guide §12).Modified
i3c_data_read/i3c_data_dumpaccept--with-header(opt-in; omitting it produces V1530-identical output).setmwl/setmrlbehaviour change. The first argument now selects the CCC form:0x7Esends the broadcast form, any other address sends the Direct form. The length argument is now required — there is no implicit default — and is range-checked to 8..65535; an out-of-range value is rejected with a non-zero exit rather than silently truncated. Both commands verify the value by read-back and return a non-zero exit code if the drive did not accept it; previously they always reported success. Scripts that relied on these commands exiting 0 unconditionally, or on the length defaulting to 69, will need updating.setmrlaccepts an optional third argument for the IBI Payload Size (0..255, default 1).
Test suite (sb_i3c_test)
Update
EXPECTED_VERSIONto1531and include1531inACCEPTED_VERSIONS.All V1530 tests run unchanged. Note the TEST16 retry behavior.
Existing customer workflows
V1530 host code that calls
i3c_data_readcontinues to see a header-stripped byte stream identical to V1530 output.V1530 SBExpress test scripts run without modification.
Upgrade Path
Standard firmware update:
sb_i3c_update -d $SLOT /etc/iRiser/sanblaze_i3c_fw_v1531.uf2
No configuration migration required. Persistent settings (clock
frequencies in /tmp/NVMe/iRiser/<slot>/) are format-compatible.
Support
For questions, defect reports, or feature requests, contact SANBlaze support with the output of:
sb_i3c -d $SLOT status
sb_i3c -d $SLOT i3c_err_get
cat /tmp/NVMe/iRiser/$SLOT/i3c_*
The V1531 firmware version string is V1531. Confirm with:
sb_i3c -d $SLOT status | grep "Firmware Version"