SANBlaze I3C Controller Platform

Version 1531 User Guide

Release Date: May 2026


Table of Contents

  1. Introduction

  2. Installation

  3. Quick Start

  4. Linux i2c-tools Interface

  5. IBI Behavior

  6. Hot Join

  7. MCTP Operations

  8. Private Transfers

  9. Burst Write System

  10. Ring Buffer Framing

  11. Diagnostics

  12. Command Reference

  13. Test Suite

  14. Troubleshooting

  15. Support


1. Introduction

V1531 adds three capabilities to the iRiser firmware: a framed ring buffer where every write carries a 4-byte header for unambiguous demux of back-to-back entries; an explicit-offset read protocol on TARGET_CMD_DATA_READ that lets driver software request the framed header once per entry and stream the remaining payload as offset-keyed continuation reads; and autonomous Hot Join handling that detects a target asserting the I3C reserved address 0x02, runs ENTDAA from a configurable starting address (default 0x1D), and publishes a framed announcement entry to the ring.

The V1530 read contract is fully preserved. Existing host code using i3c_data_read continues to see a header-stripped byte stream identical to what V1530 produced. The new framed protocol is opt-in via a wValue flag on the read control transfer.

All V1530 features and fixes are preserved unchanged.


2. Installation

Package Contents

The release package i3c_v1531_release.gz contains firmware, tools, and documentation. Extract with the -P flag to install system binaries:

mkdir /tmp/sb_i3c
cp i3c_v1531_release.gz /tmp/sb_i3c
cd /tmp/sb_i3c
tar -xzvPf i3c_v1531_release.gz

Key package contents:

Path

Description

sb_i3c

Host CLI tool (updated for V1531)

sanblaze_firmware.uf2

Firmware image (UF2 format)

tools/sb_i3c_bind

Bind device to i2c-tiny-usb driver

tools/sb_i3c_unbind

Unbind device from driver

tools/sb_i3c_find

Find device by slot

tools/sb_i3c_find_all

List all I3C devices

tools/sb_i3c_update

Firmware update utility

tools/sb_i3c_test

Automated test suite

After extraction, all tools are installed system-wide and available without a path prefix.

Firmware Update

Set your slot first. Every sb_i3c and related command targets a specific iRiser slot via -d <N>. To keep examples in this guide directly cut-and-pasteable, they all reference a $SLOT shell variable. Set it once at the start of your session and the rest of the examples just work:

SLOT=2          # substitute your own slot number

To list available slots: sb_i3c_find_all To verify the slot you set: sb_i3c -d $SLOT status

Workflow examples wrap in a subshell ( ... ) for two reasons. First, the subshell aborts cleanly if SLOT isn’t set (the :? guard kills the subshell, not your interactive shell). Second, an early echo makes the active slot number visible in the output so you can spot a wrong-slot paste before any real commands run. The two resolution lines you’ll see throughout:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
I3C_TARGET=$(sb_i3c -d $SLOT i3c_target_status | grep "addr:" | gawk -F" " '{ print $2 }')
I2C_DEV=$(sb_i3c -d $SLOT status | grep "I2C Dev" | gawk -F" " '{ print $3 }' | gawk -F"-" '{ print $2 }')
...
)

The : guard line and echo are in every workflow example. The other two appear only in examples that actually need the local target address or the I2C bus number.

Command-output blocks in this guide may show specific slot numbers in their output (e.g. slot=2 from sb_i3c_find_all); those reflect one particular reference system and shouldn’t be changed when you cut and paste — they’re sample output, not commands.

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c_update -d $SLOT /etc/iRiser/sanblaze_i3c_fw_v1531.uf2
)

The device reboots automatically. Wait 3 seconds before issuing commands.

Verify the update:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT status
# Expected output includes:
#   Version: 1531
#   ENTDAA Start: 0x1D
)

CLI binary must be updated alongside firmware. The status response struct grew by one byte in V1531 to carry the ENTDAA starting DA. A V1530 CLI paired with V1531 firmware will display incorrect status values.

Hardware Requirement: Bus Termination

V1529 introduced the requirement for correct bus termination on the drive side of the I2C mux. A missing jumper on the drive-side mux leaves the bus unterminated, causing pattern-sensitive ACK failures that are difficult to distinguish from firmware bugs. Symptom: intermittent SMBus errors on specific byte values that pass consistently on the FT260 path.

Verify the drive-side mux jumper is installed before diagnosing SMBus reliability issues.


3. Quick Start

Discover and Configure Drive

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
ENTDAA_DA=$(sb_i3c -d $SLOT status | grep "ENTDAA Start" | gawk -F" " '{ print $3 }')

# Power cycle the drive for a clean starting state
sb_i2c2 -d $SLOT -f power -w 0
sleep 1
sb_i2c2 -d $SLOT -f power -w 1
sleep 3   # let the drive boot — NVMe drives may need 3+ seconds before responding to bus traffic

# Re-establish I3C mode (mode is not preserved across power cycles;
# subsequent I3C commands will fail and may fault the firmware otherwise)
sb_i3c -d $SLOT set_mode i3c

# Reset all dynamic addresses (forces drive back to its static address)
# and reset the bus so ENTDAA reliably finds the drive
sb_i3c -d $SLOT rstdaa
sb_i3c -d $SLOT targetreset
sleep 0.5
sb_i3c -d $SLOT entdaa $ENTDAA_DA

# Enable IBI events (required for MCTP)
sb_i3c -d $SLOT enec $ENTDAA_DA

# Set maximum write and read length (optional — the drive's default already
# permits MCTP; included here as part of the standard bring-up).
# Use 0x7E for the broadcast form, or the drive's address for the Direct form.
# Both verify the value by read-back and exit non-zero if it did not take.
sb_i3c -d $SLOT setmwl $ENTDAA_DA 69
sb_i3c -d $SLOT setmrl $ENTDAA_DA 69

# Verify drive is present
sb_i3c -d $SLOT getpid $ENTDAA_DA
# Output (6 bytes — drive-specific): PID: 0xMM 0xMM 0xMM 0xMM 0xLL 0xLL
#   bytes 0..3 = manufacturer ID (MIPI Provisional ID assigned per vendor)
#   bytes 4..5 = instance ID assigned by the device
)

The sb_i3c_init helper packages the power cycle, ENTDAA, ENEC, and SETMWL/SETMRL steps into a single command — use that for routine bring-up once you understand what the manual sequence above does.

Hot Join Quick Path

To demonstrate Hot Join, power-cycle the drive then provoke it with any CCC. The drive holds its HJN request until it sees bus activity, at which point the firmware’s autonomous handler picks up the HJN, runs ENTDAA, and commits the announcement to the ring buffer — no entdaa call needed from the host.

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"

# Drain any pending IBI before triggering the Hot Join
sb_i3c -d $SLOT i3c_poll

# Power cycle — drive will re-assert Hot Join when it comes up
sb_i2c2 -d $SLOT -f power -w 0
sleep 1
sb_i2c2 -d $SLOT -f power -w 1
sleep 3   # let the drive boot — NVMe drives may need 3+ seconds before responding to bus traffic

# Re-establish I3C mode (not preserved across power cycles)
sb_i3c -d $SLOT set_mode i3c

# Provoke the drive into asserting Hot Join. After power-up the drive
# waits for several I3C bus events before HJNing; one CCC is often not
# enough. rstdaa + targetreset is a reliable pair (both are broadcast
# CCCs and safe no-ops on an un-enumerated drive).
sb_i3c -d $SLOT rstdaa
sleep 0.5
sb_i3c -d $SLOT targetreset
sleep 2        # let the firmware's autonomous handler complete ENTDAA
               # and commit the HJN entry to the ring

# Read the Hot Join entry committed to the ring
sb_i3c -d $SLOT ring_read
# Expected when a Hot Join completes (8-byte payload is drive-specific):
#   [Entry 0] origin=IBI da=0x1D mdb=0xFE len=8
#      <PID0> <PID1> <PID2> <PID3> <PID4> <PID5> <BCR> <DCR>
)

The drive lands at the configured starting DA (default 0x1D). Configure with sb_i3c -d $SLOT set_entdaa_start 0xNN if a different DA is wanted.

Quick Target Mode Verification

After firmware update, verify the local target is working with framed reads:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT i3c_target_enable
I3C_TARGET=$(sb_i3c -d $SLOT i3c_target_status | grep "addr:" | gawk -F" " '{ print $2 }')
sb_i3c -d $SLOT i3c_target_buf_reset
sb_i3c -d $SLOT i3c_sdr_write $I3C_TARGET 0xAA 0xBB 0xCC 0xDD

# Header-blind read (V1530-compatible)
sb_i3c -d $SLOT i3c_data_read
# Expected: DATA(4): 0xAA 0xBB 0xCC 0xDD

# Header-aware read (V1531)
sb_i3c -d $SLOT i3c_sdr_write $I3C_TARGET 0xAA 0xBB 0xCC 0xDD
sb_i3c -d $SLOT ring_read
# Expected (da reflects the queried target address):
#   [Entry 0] origin=DATA da=<I3C_TARGET> mdb=0x00 len=4
#      AA BB CC DD
)

4. Linux i2c-tools Interface

The SANBlaze I3C device presents a standard i2c-tiny-usb interface to Linux, enabling use of standard i2c-tools (i2cdetect, i2cget, i2cset, i2ctransfer).

Finding Your Device

Use sb_i3c_find to discover the USB path, TTY, and I2C bus number for a slot:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c_find -d $SLOT
#   USB Path: 1-5.2.7.4
#   TTY:      /dev/ttyACM6
#   Version:  1531
#   I2C Dev:  /dev/i2c-15
)

Use sb_i3c_find_all to list all slots with I3C capability:

sb_i3c_find_all
# slot=2  usb=1-5.2.7.4  tty=/dev/ttyACM6  ver=1531  i2c=/dev/i2c-15

Driver Binding

If the i2c-tiny-usb driver is not automatically bound, the safe sequence is to unbind first (no-op if not currently bound), then bind — that way the block always finishes with the driver attached:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c_unbind -d $SLOT    # Unbind (idempotent — safe if not bound)
sb_i3c_bind -d $SLOT      # Bind
)

Common i2c-tools Commands

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
I2C_DEV=$(sb_i3c -d $SLOT status | grep "I2C Dev" | gawk -F" " '{ print $3 }' | gawk -F"-" '{ print $2 }')

# Scan for devices on the TinyUSB I2C bus
i2cdetect -y $I2C_DEV

# Read a single byte from address 0x53
i2cget -y $I2C_DEV 0x53

# Write to address 0x71
i2cset -y $I2C_DEV 0x71 0x00

# Write-restart-read (required for devices that need an offset before reading)
i2ctransfer -y $I2C_DEV w1@0x6a 0x00 r16

# Write multiple bytes
i2ctransfer -y $I2C_DEV w4@0x24 0x01 0x02 0x03 0x04
)

Note: Devices that require a write-restart-read sequence will hold SDA low if a plain read is attempted without first writing the offset. Clear with: sb_i3c -d $SLOT i3c_poll


5. IBI Behavior

IBI (In-Band Interrupt) is the I3C mechanism by which a target device signals the controller that it needs attention. Understanding IBI behavior is important for reliable operation.

When IBI Is Asserted

I3C targets commonly assert IBI after:

  • Initial power-on (typically a Hot Join — see section 6)

  • First CCC command after a power cycle

  • Completion of certain operations (device-specific)

When IBI is asserted, the target holds SDA low. This is normal device behavior. While IBI is pending, i2cdetect will show no devices — this does not indicate a firmware problem.

Autonomous IBI Handling (V1530+)

The firmware’s main loop runs the autonomous IBI poller whenever the bus is idle and an IBI-style SDA-low-during-idle event is detected. For a regular IBI from an enumerated device, the handler reads the IBI bytes and either delivers them to a pending MCTP transaction or commits them as an unsolicited framed ring entry. Every handled IBI also writes an ibi_async entry to the error log with the source DA and MDB for post-mortem analysis.

For a Hot Join request (DA = 0x02), the handler takes a different branch — see section 6 below.

Polling and Clearing IBI

For a manual IBI peek (e.g. while debugging):

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT i3c_poll
)

Example output when IBI is pending:

IBI: 0xFF 0xFF 0xFF 0xFF ...

Example output when bus is clear:

No IBI Bytes Found

In normal operation, the autonomous handler clears IBIs without intervention. i3c_poll is useful for diagnostics or when the autonomous path is disabled.


6. Hot Join

Hot Join is the I3C mechanism by which a target device announces itself and requests dynamic-address assignment after the bus is already initialized. A device that wasn’t present at init time, or that was previously enumerated and came back from a power cycle, asserts the reserved Hot Join address (0x02) to request service.

V1531 handles Hot Join autonomously on the iRiser. The host does not need to run entdaa after a drive comes online — the firmware does it.

What Happens on a Hot Join Event

  1. The drive asserts SDA low during bus idle and arbitrates with the reserved Hot Join address (0x02, with W bit on the wire).

  2. The firmware’s autonomous IBI poller detects the event, reads the arbitration byte, and identifies it as a Hot Join request.

  3. The internal target table is cleared (iRiser hardware is one-drive-per-slot, so any previously-tracked drive is by definition gone).

  4. ENTDAA runs against the configured starting DA (default 0x1D, settable).

  5. On success, the firmware learns the device’s 6-byte PID, BCR, and DCR.

  6. The device is recorded in slot 0 of the target table.

  7. A framed Hot Join announcement entry is committed to the ring buffer with MDB = 0xFE and an 8-byte payload of [PID0..5, BCR, DCR].

Verifying Hot Join in the CLI

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT i3c_err_clear

# Power-cycle the drive to trigger Hot Join
sb_i2c2 -d $SLOT -f power -w 0
sleep 1
sb_i2c2 -d $SLOT -f power -w 1
sleep 3   # let the drive boot — NVMe drives may need 3+ seconds before responding to bus traffic

# Re-establish I3C mode (not preserved across power cycles)
sb_i3c -d $SLOT set_mode i3c

# Provoke the drive: drive needs multiple bus events before HJNing.
# rstdaa + targetreset is a reliable trigger pair.
sb_i3c -d $SLOT rstdaa
sleep 0.5
sb_i3c -d $SLOT targetreset
sleep 2        # let the firmware's autonomous handler complete ENTDAA
               # and commit the HJN entry to the ring

sb_i3c -d $SLOT ring_read
# Expected (8-byte payload is drive-specific):
#   ring_read: 12 bytes total
#   [Entry 0] origin=IBI da=0x1D mdb=0xFE len=8
#      <PID0> <PID1> <PID2> <PID3> <PID4> <PID5> <BCR> <DCR>

sb_i3c -d $SLOT i3c_err_get
# Expected sequence (one block per Hot Join):
#   ctx="hjn_request"       addr=0x02
#   ctx="hjn_table_clear"   addr=0x1D
#   ctx="hjn_picked_da"     addr=0x1D
#   ctx="hjn_entdaa_ok"     addr=0x1D ccc=<BCR>
#   ctx="hjn_new_device"    addr=0x1D ccc=<slot index>
#   ctx="hjn_assigned"      addr=0x1D ccc=<BCR>
)

The [HJN] debug stream (visible on the CDC console at debug 1 or higher) shows the same sequence in human-readable form:

[HJN] request detected (da=0x02)
[HJN] picked DA=0x1D, running ENTDAA
[HJN] ENTDAA ok da=0x1D pid=<6 bytes> bcr=0x<BCR> dcr=0x<DCR>
[HJN] recorded slot=0 da=0x1D
[HJN] assigned da=0x1D, ring entry committed

Configuring the Starting DA

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT set_entdaa_start          # query current value
sb_i3c -d $SLOT set_entdaa_start 0x20     # set new value
sb_i3c -d $SLOT status
# Expected:
#   ENTDAA Start: 0x20
)

Valid range: 0x08 through 0x7D, excluding 0x3E (the I3C-reserved Hot Join request address itself) and 0x7E/0x7F (broadcast/reserved). The value persists until firmware reboot.

Failure Modes

If ENTDAA fails on the bus (drive not actually present, signal-integrity issue, etc.), the handler logs hjn_entdaa_failed and returns without committing a ring entry. The drive will not be reachable until either the power cycle is repeated or the drive re-asserts Hot Join.

If the target table is full (only possible in multi-drive deployments that disable the every-HJN clear), the handler logs hjn_table_full but still commits the ring entry so the driver sees the new assignment.


7. MCTP Operations

Overview

MCTP (Management Component Transport Protocol) over I3C enables communication with drive management interfaces. The firmware supports three MCTP read paths with increasing levels of autonomy:

Path

Command

Use Case

Manual

private_write + i3c_poll + mctp_read

Step-by-step debugging

Autonomous

mctp_command

CLI-based testing and development

Ring buffer

mctp_to_ring

Customer application integration

Maximum transfer unit (SETMWL / SETMRL): After ENTDAA and ENEC you may set the maximum write/read length. This is not required for MCTP on the drive under test — its power-on default already exceeds the packet size and MCTP works without any SETMWL call. Set it only if a specific maximum is needed (the value must be at least the packet size):

sb_i3c -d $SLOT setmwl $ENTDAA_DA 69
sb_i3c -d $SLOT setmrl $ENTDAA_DA 69

Multi-Packet MCTP over SMBus (V1529)

In I2C/SMBus mode, drives may return responses as multiple sequential SMBus frames, each addressed to the local target (0x24). V1529 firmware ACKs all packets and commits each complete frame to the ring buffer atomically. The mi tool reads packets one at a time, using the MCTP SOM/EOM flags to determine when the response is complete.

Commands returning large responses (VPD Read, Identify Controller) will produce multiple committed frames in sequence. With V1531 framing, each frame is a separate ring entry with its own 4-byte header — the driver can demux them cleanly. mi continues to use the V1530-compatible byte-stream path and behaves unchanged.

Manual Path (Debugging)

For detailed visibility into each phase:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
ENTDAA_DA=$(sb_i3c -d $SLOT status | grep "ENTDAA Start" | gawk -F" " '{ print $3 }')
# 1. Send MCTP command packets
sb_i3c -d $SLOT private_write --hold $ENTDAA_DA <pkt1_bytes>
sb_i3c -d $SLOT private_write $ENTDAA_DA <pkt2_bytes>    # no --hold = fires burst

# 2. Poll for IBI (drive signals response ready)
sb_i3c -d $SLOT i3c_poll

# 3. Read all response packets
sb_i3c -d $SLOT mctp_read $ENTDAA_DA
# Output: packet-by-packet hex dump with MCTP headers decoded
)

Autonomous Path (mctp_command)

Single command handles burst fire, IBI polling, and multi-packet read:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
ENTDAA_DA=$(sb_i3c -d $SLOT status | grep "ENTDAA Start" | gawk -F" " '{ print $3 }')
sb_i3c -d $SLOT private_write --hold $ENTDAA_DA <pkt1_bytes>
sb_i3c -d $SLOT private_write --hold $ENTDAA_DA <pkt2_bytes>
sb_i3c -d $SLOT mctp_command $ENTDAA_DA
)

Ring Buffer Path (mctp_to_ring)

Routes the complete MCTP response into the target ring buffer for customer application integration:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
ENTDAA_DA=$(sb_i3c -d $SLOT status | grep "ENTDAA Start" | gawk -F" " '{ print $3 }')
sb_i3c -d $SLOT private_write --hold $ENTDAA_DA <pkt1_bytes>
sb_i3c -d $SLOT private_write --hold $ENTDAA_DA <pkt2_bytes>
sb_i3c -d $SLOT mctp_to_ring $ENTDAA_DA

# Read header-blind (V1530-compatible byte stream)
sb_i3c -d $SLOT i3c_data_read

# Or read with framing (V1531 — multiple back-to-back responses demuxed)
sb_i3c -d $SLOT ring_read
)

The V1530 byte-stream contract is preserved on i3c_data_read without --with-header. New host code that wants to demux multiple back-to-back MCTP responses (for example MPR followed by Success in a deferred-write transaction) should use ring_read or i3c_data_read --with-header.

NVMe-MI Firmware Download

V1531 adds fw_download, a direct CLI command that streams an NVMe-MI FW Image Download to the drive over I3C. It builds each 4 KB FW Download command, fragments it into MCTP packets, and — with --read_to_ring — reads back and validates the drive’s NVMe-MI status after every chunk, stopping immediately if the drive rejects one. A fw_dl.sh helper that drives the same download through the mi tool chain (including an I2C fallback) is described at the end of this section.

Prerequisite: an enumerated drive

fw_download needs the drive enumerated and MCTP-capable — i.e. your standard bring-up (ENTDAA, ENEC) completed. There is no manual Maximum Write/Read Length step: the drive’s power-on default already permits MCTP, and the per-packet SMBus PEC that the drive requires is computed and appended by fw_download automatically. A drive power cycle before the download is still recommended so the download buffer starts empty (see “Starting offset” below).

: ${SLOT:?Set your slot number with SLOT=N first}
# Power cycle, then your normal bring-up (ENTDAA / ENEC) — that's all MCTP needs.

Downloading an image

# Validated download — checks each chunk's NVMe status and stops on any
# rejection (recommended for a real download):
sb_i3c -d $SLOT fw_download 0x1D /path/to/fw-image.bin --read_to_ring

# Send-only — pushes the image without reading responses (no validation;
# use only for throughput measurement):
sb_i3c -d $SLOT fw_download 0x1D /path/to/fw-image.bin

Progress is shown as a single bar that fills as the image transfers:

Downloading 4.2M firmware file fw-image.bin, please wait:
[..............................                                        ] 21384 of 71808 packets

and ends with a one-line summary:

[......................................................................] 71808 of 71808 packets
Done: 4.2M firmware (1088 chunks, 71808 packets) in 1:20 (74 ms/chunk).  All chunks accepted.  [0 NAK retries]

The summary line ends with the download’s NAK-retry statistics. [0 NAK retries] is the normal case. If the drive NAKs a packet’s address phase, the firmware retries that packet automatically and the line reports how many retries were needed and that they recovered. If a packet cannot be delivered even after retry, the download is marked FAILED, the count of unrecovered packets is shown, and fw_download exits non-zero — the staged image is incomplete and must not be committed. Re-run the download.

Download time depends on drive and host state and is not deterministic; treat the elapsed figure as indicative, not a guarantee. Note that the I3C clock rate has little effect on it (see “Choosing an I3C clock” below).

Per-chunk validation (--read_to_ring)

With --read_to_ring, the drive’s NVMe-MI FW Download response is read back after each 4 KB chunk and its status decoded. Any status other than Success stops the download at that chunk and prints the offset and decoded NVMe status — for example, a chunk whose offset overlaps a range already staged in the drive’s download buffer:

ERROR: chunk 1 (offset 0x0): NVMe status SCT=0x1 SC=0x14 (Overlapping Range) — download stopped

Because a download writes into the drive’s staging buffer, per-chunk validation is strongly recommended — it fails loud at the exact rejected chunk instead of continuing past it. The validation adds only a few seconds across a full image.

fw_download is download-only: it stages the image in the drive’s download buffer and does not issue an NVMe Firmware Commit / activate. Activation is a separate, deliberate step.

Starting offset and the download buffer

The drive rejects a chunk whose offset overlaps a range already staged in its download buffer, returning Overlapping Range. Start from a clean buffer — a drive power cycle clears it. To resume or write a sub-range, pass an explicit start (and optional end) offset:

# Start at 0x1000, skipping the first 4 KB chunk
sb_i3c -d $SLOT fw_download 0x1D /path/to/fw-image.bin 0x1000 --read_to_ring

Choosing an I3C clock

Download time is dominated by host/USB transfer overhead rather than the I3C wire, so a faster I3C clock buys very little — a full image takes about 84 seconds at 4 MHz and about 80 seconds at 8 MHz. Choose the clock for reliability, not speed, and prefer a moderate clock with margin: marginal signal integrity can cause a hard hang mid-download rather than a clean, recoverable error.

The highest reliable clock is specific to your setup (cabling, bus loading, and any inline analyzer or instrumentation all lower it). To find a good clock, bring the bus up at a candidate rate and confirm the drive enumerates — the PID: line and Ready banner both appear on success:

: ${SLOT:?Set your slot number with SLOT=N first}
# Try a candidate clock; success prints a PID line and the Ready banner.
sb_i3c_init -d $SLOT -p i3c -c 8000 | egrep "PID:|Ready"

If enumeration succeeds, that clock is safe to use for the download. If it fails or is unreliable, step the clock down (e.g. 6000, then 4000) and retry until enumeration is dependable, then run fw_download at that clock. Do not push to the highest clock that merely appears to work; back off for margin.

Options

Option

Effect

--read_to_ring

Read back and validate each chunk’s NVMe-MI status; stop on any non-Success. Recommended.

--test N

Send only the first N chunks — a quick end-to-end check without writing the whole image.

<start_hex> [end_hex]

Restrict the download to a 4 KB-aligned byte range.

-v

Verbose per-chunk detail instead of the progress bar.

Alternative: mi-based download (fw_dl.sh)

fw_dl.sh runs the same download through the mi/SMBus host tool chain and includes an I2C-mode fallback for drives that do not support MCTP over I3C. The FW image path inside the script is fixed to the customer’s test image; edit FWFILE= at the top to point at a different image.

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
../SANBlaze_I3C_Test/fw_dl.sh -d $SLOT -p i3c -c 4000    # 4 MHz I3C
# or
../SANBlaze_I3C_Test/fw_dl.sh -d $SLOT -p i3c -c 1000    # 1 MHz I3C
# or fall back to I2C mode for a drive that doesn't do I3C MCTP
../SANBlaze_I3C_Test/fw_dl.sh -d $SLOT -p i2c -c 100
)

Each invocation runs sb_i3c_init (power-cycle, ENTDAA, ENEC, SETMWL/SETMRL) then hands the image to the mi tool. It is the established path and offers the I2C fallback for drives that do not support MCTP over I3C. If a chunk fails partway through, retry the command; the drive tracks progress internally and skips already-written offsets.


8. Private Transfers

I3C Private Transfers use a 0x7E broadcast + Repeated START preamble before the addressed transaction. Required for MCTP communication with most NVMe drives.

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
ENTDAA_DA=$(sb_i3c -d $SLOT status | grep "ENTDAA Start" | gawk -F" " '{ print $3 }')
# Single private write
sb_i3c -d $SLOT private_write $ENTDAA_DA 0x01 0x02 0x03 ...

# Buffered (held) private write for burst
sb_i3c -d $SLOT private_write --hold $ENTDAA_DA 0x01 0x02 ...

# Private read
sb_i3c -d $SLOT private_read $ENTDAA_DA 64
)

Testing Private Transfers Without a Drive

The blaster’s local target (0x24) captures incoming private writes into the same ring buffer used by drive-originated traffic, so the private-transfer path can be exercised end-to-end without needing a specific drive in the system. Useful as a smoke test after firmware updates and when debugging the host-side burst protocol.

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
ENTDAA_DA=$(sb_i3c -d $SLOT status | grep "ENTDAA Start" | gawk -F" " '{ print $3 }')
sb_i3c -d $SLOT i3c_target_enable
I3C_TARGET=$(sb_i3c -d $SLOT i3c_target_status | grep "addr:" | gawk -F" " '{ print $2 }')
sb_i3c -d $SLOT i3c_target_buf_reset

# Buffered (--hold) write of 5 bytes to the local target
sb_i3c -d $SLOT private_write $I3C_TARGET --hold $ENTDAA_DA 1 2 3 4
# Expected: PRIVATE_WRITE OK: 5 bytes to <I3C_TARGET>

# Plain write of 4 bytes — fires the buffered bytes followed by these 4
sb_i3c -d $SLOT private_write $I3C_TARGET 5 6 7 8
# Expected: PRIVATE_WRITE OK: 4 bytes to <I3C_TARGET>

# Read back what landed in the local target's ring
sb_i3c -d $SLOT i3c_data_read
# Expected: DATA(9): 0x1D 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08
)

The 9 bytes confirm both the held write (5 bytes) and the non-held write (4 bytes) reached the ring in order. This is the same burst-write mechanism described in section 9; the local target just receives instead of a remote drive.


9. Burst Write System

The burst write system buffers multiple private_write --hold packets in firmware and transmits them back-to-back as a single burst when mctp_command or mctp_to_ring is called. This eliminates USB latency between packets and is required for multi-packet MCTP commands.

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
ENTDAA_DA=$(sb_i3c -d $SLOT status | grep "ENTDAA Start" | gawk -F" " '{ print $3 }')
# Buffer packet 1 (69 bytes) — not sent yet
sb_i3c -d $SLOT private_write --hold $ENTDAA_DA <pkt1_bytes>
# Output: PRIVATE_WRITE HELD: 69 bytes to 0x1D

# Buffer packet 2 (13 bytes) — not sent yet
sb_i3c -d $SLOT private_write --hold $ENTDAA_DA <pkt2_bytes>
# Output: PRIVATE_WRITE HELD: 13 bytes to 0x1D

# Fire all buffered packets in one burst
sb_i3c -d $SLOT mctp_command $ENTDAA_DA
# or
sb_i3c -d $SLOT mctp_to_ring $ENTDAA_DA
)

The burst buffer holds up to 512 bytes across multiple packets. Each packet is sent as a complete I3C Private Write transaction with START and STOP.

Full-Speed Delivery and NAK Retry (V1531)

Bursts transmit at full bus speed with no pre-emptive inter-burst delay. Rapid back-to-back multi-chunk MCTP writes (e.g. VPD Write, NVMe-MI FW Download) can intermittently hit an address NAK on a mid-stream chunk when the drive’s I3C receiver needs recovery time between transactions. Rather than slowing every burst to avoid that, the firmware reacts to it: when a packet’s address phase NAKs, it backs off briefly (100 µs) and retries the same packet, replaying only the address phase — no data is sent until the address is accepted, so a retry can never double-send data. Retries continue until the address is accepted or a short per-packet ceiling (5 ms) is reached.

In practice the retry path is rarely taken — the drive accepts full-speed writes cleanly across a whole image — so it costs nothing on the normal path while still recovering the occasional NAK. If a packet exhausts the retry ceiling, the operation treats it as an unrecoverable delivery failure (see the fw_download summary line in Section 7). The earlier fixed 2 ms inter-burst pacing floor is retained in the firmware but disabled by default; it can be restored if a specific drive is found to need a fixed recovery window instead of reactive retry.

Firing Without a Drive

mctp_command and mctp_to_ring require a drive to respond. To exercise the burst mechanism without a drive in the system, fire the held bytes by issuing a plain private_write (without --hold) — the held bytes are sent first, followed by the new bytes, in one burst. Direct the whole sequence at the local target (0x24) and read it back from the ring. See the Testing Private Transfers Without a Drive example in section 8 for the full command sequence.


10. Ring Buffer Framing

Frame Format

Every entry committed to the target ring buffer (8192 bytes, no change in size from V1530) is prefixed with a 4-byte header:

byte 0: [DA | flags]   bit 7  = origin (1 = IBI, 0 = DATA write)
                       bits 6:0 = device dynamic address
byte 1: MDB            For IBI entries:
                         - Mandatory Data Byte from the IBI
                         - 0xFE = Hot Join announcement (special)
                       For DATA writes: 0x00 (no MDB applies)
byte 2: size_hi        high byte of payload length (big-endian)
byte 3: size_lo        low byte of payload length
byte 4..N+3: payload   exactly 'size' bytes

Multiple entries pack back-to-back with no padding between them. A typical mctp_to_ring drain might look like:

[0x1D 0x00 0x00 0x09] [09 bytes of MPR response payload]
[0x1D 0x00 0x00 0x05] [05 bytes of Success response payload]

Per-Packet MCTP Commits

Multi-packet MCTP responses (e.g. a 4K Identify Controller) produce one ring entry per MCTP packet, not one entry for the whole response. A 4K Identify typically yields ~64 entries of ~69 bytes each (4-byte frame header + MCTP header + 64-byte payload + 1 PEC byte). Driver software reading via ring_read (or wValue=1 on the wire) sees the per-packet structure directly and can demux by tag/MDB without reassembly.

The header-blind i3c_data_read path is unaffected — partial reads slide past entry headers transparently and present a single concatenated byte stream identical to V1530.

Two Read Modes

The host chooses between two read modes via the wValue field on TARGET_CMD_DATA_READ:

V1530-compatible (header-blind): wValue = 0, wIndex = 0. Output is a continuous byte stream of payload only — headers are stripped transparently. Partial reads (host buffer smaller than the current entry) slide the header forward in the ring with an updated remaining-length field, so the next read picks up where this one left off. Existing scripts using i3c_data_read see this mode by default and need no changes.

Header-aware (framed): wValue = 1 to start a new packet read, then wValue = 0 with wIndex = K to continue reading the same packet at byte offset K. Used by ring_read and by driver software that wants to demux back-to-back entries. See the V1531 Ring Buffer Framing Driver Integration memo for detailed wire-level documentation.

CLI Surface

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
# V1530-compatible byte stream (no frame headers)
sb_i3c -d $SLOT i3c_data_read           # text output
sb_i3c -d $SLOT i3c_data_dump > out.bin # binary output (length-prefixed)

# Framed output with headers visible
sb_i3c -d $SLOT i3c_data_read --with-header
sb_i3c -d $SLOT i3c_data_dump --with-header > out.bin

# Decoded human-readable framed dump
sb_i3c -d $SLOT ring_read
# Example output:
#   ring_read: 12 bytes total
#   [Entry 0] origin=DATA da=0x24 mdb=0x00 len=8
#      01 02 03 04 05 06 07 08
)

MDB Sentinel Values

MDB

Meaning

0x00

Non-IBI DATA write — sent when a host or other initiator writes to our target address on the bus

0xFE

Hot Join announcement — payload is [PID0..5][BCR][DCR]

Other

Mandatory Data Byte from an IBI raised by an enumerated device

Stress Testing the Framing Path

A bash script stress_test_ring (provided with the release) writes random-sized chunks to the local target and drains via ring_read, verifying entry count, payload total, and byte-for-byte content match. See section 13 for full usage. Pass -v for chunk-by-chunk visibility:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
stress_test_ring -d $SLOT -v
)

11. Diagnostics

PIO Dump

Comprehensive bus and PIO state machine diagnostic:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT pio_dump
)

Output includes:

  • Bus state: SDA/SCL levels and GPIO function assignments

  • PIO0 SM1 (Initiator): program counter, enabled, FIFO state, stall flags

  • PIO1 SM0 (Target): state and GPIO configuration

  • PIO2 SM0 (Target 0x7E): state for Private transfer monitoring

  • IRQ flags for all PIO blocks

  • Diagnosis line (e.g., “All OK - bus idle” or specific fault identification)

  • Disassembled PIO instructions at current program counter

The test suite automatically runs pio_dump on any test failure.

Debug Levels

Set the firmware’s debug verbosity to print more or less info to the USB CDC console:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT set_debug_level 0     # Quiet (default)
sb_i3c -d $SLOT set_debug_level 1     # Key events (MCTP packet summaries, IBI, HJN, errors)
sb_i3c -d $SLOT set_debug_level 2     # Verbose (ring buffer commits, PIO state, SMBus trim, linger)
)

V1531 adds [HJN] debug-stream messages at level 1+ that trace every Hot Join event from detection through ring commit. The full IBI / ring trace including [PIO] START/was_init, [XFER_END], [CCC] complete, [LINGER] SCL low, [IRQ3], [RING] committed, and SMBus trim events is available at level 2.

Capturing the Debug Log

Debug output goes to the firmware’s USB CDC serial console. The exact device path is reported in status as TTY: — it varies by which USB ports were enumerated at boot, so always read it fresh rather than hardcoding /dev/ttyACM0:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
TTY=$(sb_i3c -d $SLOT status | grep "TTY:" | gawk -F" " '{ print $2 }')
echo $TTY        # confirm — e.g. /dev/ttyACM4
)

To capture a log while reproducing an issue, run two shells. In the first shell, start watching the console:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
TTY=$(sb_i3c -d $SLOT status | grep "TTY:" | gawk -F" " '{ print $2 }')
cat $TTY
)

In a second shell, raise the debug level, exercise the bug, then put debug back to quiet when done:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT set_debug_level 2
sb_i3c_test -d $SLOT -t 41        # or any other reproducer
sb_i3c -d $SLOT set_debug_level 0
)

The first shell will print the debug stream live as the test runs. Ctrl-C to stop, then redirect a fresh capture to a file if you want to save it for a support ticket:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
TTY=$(sb_i3c -d $SLOT status | grep "TTY:" | gawk -F" " '{ print $2 }')
cat $TTY > /tmp/sb_i3c_debug.log
)

cat is the simplest viewer. screen $TTY 115200 works too if you want scrollback and line editing; the console runs at 115200 baud with no flow control. The second-shell example above already sets debug back to 0 at the end so the firmware isn’t spending cycles formatting log lines no one is reading.

Error Log

V1531 expanded the error-log entries to include both IBI handling and Hot Join decision points:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT i3c_err_get
# Example:
#   [0] ERR=0  txn=0 addr=0x02 ccc=0x00 ts=110297 ctx="hjn_request"
#   [1] ERR=0  txn=0 addr=0x1D ccc=0x00 ts=110297 ctx="hjn_table_clear"
#   [2] ERR=0  txn=0 addr=0x1D ccc=0x00 ts=110297 ctx="hjn_picked_da"
#   [3] ERR=0  txn=0 addr=0x1D ccc=<BCR> ts=110301 ctx="hjn_entdaa_ok"
#   [4] ERR=0  txn=0 addr=0x1D ccc=0x00 ts=110301 ctx="hjn_new_device"
#   [5] ERR=0  txn=0 addr=0x1D ccc=<BCR> ts=110301 ctx="hjn_assigned"
#   [6] ERR=0  txn=1 addr=0x1D ccc=<MDB> ts=234120 ctx="ibi_async"

sb_i3c -d $SLOT i3c_err_clear
)

12. Command Reference

New in V1531

Command

Description

ring_read [N]

Read N bytes (default 8192) from ring with framing; decode each entry and pretty-print with origin/DA/MDB/length/payload

set_entdaa_start [<da>]

Query (no arg) or set the firmware’s ENTDAA starting DA. Default 0x1D

fw_download <da7> <path> [start_hex] [end_hex] [--read_to_ring] [--test N] [-v]

Direct NVMe-MI FW Image Download over I3C. Streams the image as MCTP FW Download commands (SMBus PEC applied automatically). --read_to_ring validates each chunk’s NVMe status and stops on any rejection. Download-only — does not Commit/activate.

Modified in V1531

Command

Change

i3c_data_read [--with-header] [N]

New --with-header flag emits framed bytes including 4-byte headers. Without it, V1530-compatible header-blind output (unchanged)

i3c_data_dump [--with-header] [N]

Same flag added to the binary dump variant

status

New line: ENTDAA Start: 0xNN

setmwl <0x7E|da7> <bytes>

First argument selects the CCC form: 0x7E sends the broadcast form (CCC 0x09), any other address sends the Direct form (CCC 0x89). The length argument is now required (no implicit default) and range-checked to 8..65535. The value is verified by read-back; a non-zero exit code is returned if the drive did not accept it. Previously reported success unconditionally.

setmrl <0x7E|da7> <bytes> [ibi]

Same form selection, range check and read-back verification as setmwl (CCC 0x0A broadcast / 0x8A Direct). Sends a 3-byte frame — length MSB, length LSB, IBI Payload Size — when the target advertises IBI-payload capability (BCR bit 2), otherwise 2 bytes. A 2-byte frame to such a target is malformed and is silently discarded, which is why setmrl never took effect before V1531. The optional third argument sets the IBI Payload Size (default 1). Verified by read-back, non-zero exit on mismatch.

getmwl / getmrl

Response length decoded MSB-first (I3C byte order). Previously reported a byte-swapped value (e.g. 17664 for a length of 69). getmrl additionally reports the IBI Payload Size when the target supplies it.

setdasa

CLI now surfaces the drive-side NAK status as a non-zero exit code (previously always reported success)

Release Script (Maintainers)

tools/i3c_release.sh gains a -r rebuild mode for the case where source changes were made directly in the release tree (not in _TIP):

./tools/i3c_release.sh -r 1 531

Rebuilds the code in place, compares each output file against the existing tarball, prompts file-by-file for which changes to include, backs up the old tarball with a timestamp, and produces a new tarball with only the approved changes. CVS bookkeeping (CVS/ dirs, .cvsignore) is filtered out of both the diff and the resulting tarball.

A -u (update) mode runs the identical flow but never rebuilds the binaries:

./tools/i3c_release.sh -u 1 531

The existing firmware and CLI artifacts are packaged as-is. Use this for a docs-only release: the firmware embeds a build timestamp, so rebuilding produces a byte-different image even when no source has changed, which shows up as a spurious change in the tarball diff. Both -r and the default mode now prompt before building, defaulting to no.

Build artifacts (.uf2, .elf, .elf.map, sb_i3c) are published to the release-tree root rather than living only under the build directories, so clean no longer disturbs their revision history. The linker map is kept at the root for debugging but is not shipped in the customer tarball.

New in V1528 (carried forward)

Command

Description

private_write [--hold] <da7> <bytes>

I3C Private Write; –hold buffers for burst

private_read <da7> <nbytes>

I3C Private Read (up to 128 bytes)

mctp_read <da7> [pkt_size]

Read all MCTP response packets from drive

mctp_command <da7> [pkt_size]

Burst fire + IBI poll + read (autonomous)

mctp_to_ring <da7> [pkt_size]

Burst fire + IBI poll + read → ring buffer

private_force <on|off>

Force Private mode for all transfers

pio_dump

PIO/bus diagnostic dump

setmwl <0x7E|da7> <len>

Set Maximum Write Length (required, 8..65535). 0x7E = broadcast CCC 0x09; any other address = Direct CCC 0x89. Length is MSB-first. Verified by read-back; non-zero exit if the value did not take.

setmrl <0x7E|da7> <len> [ibi]

Set Maximum Read Length (required, 8..65535). 0x7E = broadcast CCC 0x0A; any other address = Direct CCC 0x8A. Sends a 3-byte frame (length MSB, length LSB, IBI Payload Size) when the target advertises IBI-payload capability, otherwise 2. ibi defaults to 1. Verified by read-back; non-zero exit if the value did not take.

Modified in V1528 (carried forward)

Command

Change

i2c_clk <khz>

Writes to persistent storage only; applied by set_mode

i3c_clk <khz>

Writes to persistent storage only; applied by set_mode

set_mode <i2c|i3c>

Reads and applies stored per-mode clock automatically

Existing Commands (unchanged)

Command

Description

status

Firmware version and device info (V1531 adds ENTDAA Start line)

targetreset

Reset I3C bus

entdaa <da7>

Dynamic address assignment

getpid <da7>

Get Provisional ID

getbcr <da7>

Get Bus Characteristics Register

getdcr <da7>

Get Device Characteristics Register

getstatus <da7>

Get device status

enec <da7>

Enable events (IBI)

disec <da7>

Disable events

set_debug_level <0-3>

Set firmware debug verbosity (was debug in earlier releases)

i3c_sdr_write <da7> <bytes>

SDR write

i3c_sdr_read <da7> <nbytes>

SDR read

i3c_sdr_writeread <da7> <byte> <len>

Write-restart-read (no STOP between phases)

i3c_poll

Poll for IBI events

i3c_err_get

Get error log

i3c_err_clear

Clear error log

scan_i2c

Scan for I2C devices

scan_i3c

Scan for I3C devices

set_termination <0-3>

Set bus termination

set_drive_strength <mA>

Set output drive strength (2–12 mA)

i3c_target_enable

Enable local target mode

i3c_target_disable

Disable local target mode

i3c_target_set_addr <addr>

Set local target address

i3c_target_status

Show local target state

i3c_target_buf_reset

Clear local target ring buffer


13. Test Suite

Run the full test suite:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c_test -d $SLOT
)

Options:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c_test -d $SLOT -t 41        # Run single test
sb_i3c_test -d $SLOT -t 30-52     # Run test range
sb_i3c_test -d $SLOT -v           # Verbose output
sb_i3c_test -d $SLOT -N           # Skip hardware init (caller ensures drive present)
)

V1531: Update EXPECTED_VERSION to 1531 and include 1531 in ACCEPTED_VERSIONS. All V1530 tests run unchanged.

TEST16 note: The 60-byte i2ctransfer target-receive loopback test is host-system-timing sensitive on some configurations. The test may require a second pass; the pass count is noted in the test status line as (passed on attempt N/3) when a retry succeeded. A deterministic root-cause fix is deferred to a future release.

MCTP Tests

Test

Description

38

SETMWL 69 — CCC 0x09 on the wire in a broadcast frame (see command reference)

39

SETMRL 69 — CCC 0x0A on the wire in a broadcast frame (see command reference)

41

MCTP 4K Identify via manual burst write + IBI + mctp_read

50

MCTP discovery (Get Endpoint ID) via mctp_command

51

MCTP 4K Identify via mctp_command (autonomous)

52

MCTP 4K Identify via mctp_to_ring (ring buffer path)

55

Multi-packet MCTP, header-blind read (V1530-compat byte stream)

56

Multi-packet MCTP, framed read (one ring entry per MCTP packet)

Test 52 verifies the complete customer path: burst write → mctp_to_ring → i3c_data_read → verify byte count → confirm ring drains to empty. With V1531 framing in place, i3c_data_read produces the V1530-compatible byte stream unchanged — TEST52 passes without modification.

Tests 55 and 56 verify the per-packet ring-commit behavior introduced in V1531: a 4K Identify Controller response produces ~64 ring entries (one per MCTP packet) when read via ring_read, and the same data concatenated into ~4445 bytes when read via the V1530-compatible i3c_data_read path. Both tests use the drive path’s exact transfer command (mi -T i3c -d $SLOT -t 2 6 1 -c 129 -k -1).

Ring Framing Stress Test (V1531)

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
stress_test_ring -d $SLOT            # standard run
stress_test_ring -d $SLOT -v         # verbose: per-chunk writes + raw ring_read
stress_test_ring -d $SLOT -n 4000    # custom byte target (default 6000)
stress_test_ring -d $SLOT -s 42      # reproducible (fixed PRNG seed)
)

Writes payload to the local target (0x24) in random-sized chunks (1–200 bytes each, default 6 KB total), then drains via ring_read and verifies:

  • Entry count matches the number of chunks written

  • Sum of len= fields matches cumulative bytes written

  • Byte-for-byte content matches the deterministic counter pattern

  • Ring is empty after drain (single-read semantics)

  • Every entry has origin=DATA da=0x24 mdb=0x00

Useful as a regression check after firmware updates. In verbose mode the script prints each chunk in ring_read-style format so writes and reads can be visually compared.


14. Troubleshooting

Intermittent SMBus Errors on Specific Byte Values

If SMBus commands fail intermittently on byte values such as 0xD3, 0xF5, or 0xC1 (even-popcount bytes) but pass consistently on the FT260 path, suspect missing bus termination. Check the drive-side mux jumper. Unterminated bus causes pattern-sensitive ACK failures that mimic firmware bugs.

Drive Not Responding After MCTP Read

After a 4K MCTP transaction, the drive’s MCTP state machine is exhausted. Use double targetreset to recover:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
ENTDAA_DA=$(sb_i3c -d $SLOT status | grep "ENTDAA Start" | gawk -F" " '{ print $3 }')
sb_i3c -d $SLOT targetreset
sleep 0.5
sb_i3c -d $SLOT targetreset
sleep 0.5
sb_i3c -d $SLOT entdaa $ENTDAA_DA
sb_i3c -d $SLOT enec $ENTDAA_DA
sb_i3c -d $SLOT setmwl $ENTDAA_DA 69
sb_i3c -d $SLOT setmrl $ENTDAA_DA 69
)

Hot Join Not Detected

If a drive comes online but no Hot Join announcement appears in ring_read:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT i3c_err_get
# Look for: "hjn_request" — absent means the autonomous IBI handler
# didn't see the request

sb_i3c -d $SLOT pio_dump
# Look for: stuck PIO state machine, signal-integrity issues
)

If hjn_request appears but the announcement still isn’t in the ring, check for hjn_entdaa_failed — drive didn’t accept the ENTDAA. Try set_entdaa_start with a different DA in case 0x1D is contested.

Drive Lands at Wrong DA After Hot Join

set_entdaa_start should be configured before the drive’s Hot Join event. If the value was changed after the drive was already enumerated, the firmware will use the new starting DA only on the next Hot Join (e.g. after a drive power cycle). Verify with sb_i3c -d $SLOT status and confirm the ENTDAA Start: line shows the expected value.

Ring Read Returns Different Entry Counts on Repeated Calls

Each ring_read drains the ring — subsequent reads see only what was committed since the last drain. This is by design (single-read semantics). If you expect entries to be present and the ring is empty, check that the producer (mctp_to_ring, sdr_write, etc.) was issued between reads.

ring_read Output Shows “truncated”

The 4-byte header’s length field claims more payload than is actually in the ring. This usually indicates a corrupted commit (firmware bug). Capture the PIO state with pio_dump, the err log with i3c_err_get, and a debug-level-2 ttyACM capture, then file a support ticket.

I3C Tests Skipping Unexpectedly with -N Flag

If Python test wrappers show “Drive not available in I3C mode” when the drive is present, the i3c_drive_unavailable flag file from a previous failed run may be stale. The V1529 setup_i3c() fix clears this file automatically. If running an older wrapper, clear manually:

rm -f /path/to/luns/1/i3c_drive_unavailable
rm -f /path/to/luns/1/i3c_drive_ready

i2cdetect Shows No Devices

Check for a pending IBI first — this is the most common cause:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT i3c_poll
# Poll repeatedly until: No IBI Bytes Found
)

If IBI is clear, check bus state:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT status
# Look for: Bus State: SDA=1, SCL=1  (both high = idle)
)

If the bus is stuck, power cycle the target device. Remember that mode is not preserved across power cycles — re-establish whichever mode your workflow needs before issuing more bus traffic:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i2c2 -d $SLOT -f power -w 0
sleep 1
sb_i2c2 -d $SLOT -f power -w 1
sleep 3   # let the drive boot — NVMe drives may need 3+ seconds before responding to bus traffic

# Re-establish your operating mode — use i3c OR i2c, not both
sb_i3c -d $SLOT set_mode i3c
# sb_i3c -d $SLOT set_mode i2c
)

Dual-Controller Riser: Wrong /dev/i2c-N After Boot

Risers with two SANBlaze controllers on a shared hub (port 5 = active controller, port 6 = auxiliary) occasionally fail to enumerate port-5 on boot. In V1531, when this happens sb_i3c_find emits a clear error and refuses to write a wrong bus number into /tmp/NVMe/iRiser/<slot>/i3c_dev. To recover:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
# Re-run discovery — port-5 typically comes up on the next bounce
sb_i3c_find_all
cat /tmp/NVMe/iRiser/$SLOT/i3c_dev    # confirm correct bus number

# If port-5 still fails after several attempts, power-cycle the iRiser slot
)

If sb_i3c_find reports that the auxiliary port enumerated but port-5 (the active controller) did NOT, that’s the port-5-enumeration-fail symptom — cache files are wiped automatically so the next healthy invocation re-discovers.

Bus Stuck After Operation

If SDA or SCL is held low after an operation:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT i3c_poll     # Clear any pending IBI
sb_i3c -d $SLOT targetreset  # Reset bus state
sb_i3c -d $SLOT pio_dump     # Inspect PIO state if still stuck
)

burst_addr_nak in the Error Log

The error log context burst_addr_nak indicates the firmware attempted a burst write and read back NAK on the address byte. In V1531 the firmware retries a NAKed packet automatically (Section 9), so isolated burst_addr_nak entries are expected and self-recovering. If you see them persistently, or a fw_download reports unrecovered packets:

  1. Confirm you’re running V1531. Occasional post-flash confusion is the most common cause — a stale firmware binary in place while you believe V1531 is loaded. Verify with sb_i3c -d $SLOT status and check the firmware version line.

  2. Try a slower I3C clock. sb_i3c_init -d $SLOT -p i3c -c 1000 (1 MHz) gives more inter-bit time on the wire and often lets marginal drives keep up. If 1 MHz works and 4 MHz doesn’t for a specific drive, that’s useful data — capture it for SANBlaze support.

  3. Cross-check with I2C mode. Try the same command via sb_i3c -d $SLOT set_mode i2c and re-run. If the command works in I2C and fails in I3C, the drive-side I3C receiver is the likely suspect; if both fail, the issue is upstream (host tool, framing, drive state).

Multi-Chunk MCTP Write Fails on Mid-Stream Chunks

Symptom (seen historically at 4 MHz I3C on rapid MCTP-over-SMBus multi-chunk writes like VPD Write or FW download): first two chunks send cleanly, subsequent chunks fail. V1531’s automatic per-packet NAK retry (Section 9) recovers the great majority of these transparently. If a fw_download still reports unrecovered packets against a specific drive:

  • Re-run the download — the drive’s staging buffer is cleared by a power cycle, and a fresh attempt usually succeeds.

  • Fall back to a slower I3C clock (e.g. -c 4000 or -c 1000 on sb_i3c_init) to give the drive more recovery margin per transaction.

  • Capture an analyzer trace of the failure and contact SANBlaze support; the per-packet retry ceiling and the (default-disabled) fixed pacing floor are both tunable in the firmware but require a rebuild.

Local Target Tests Fail

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
# Ensure target is enabled
sb_i3c -d $SLOT i3c_target_enable

# Verify target address
sb_i3c -d $SLOT i3c_target_set_addr 0x24

# Flush stale data from buffer
sb_i3c -d $SLOT i3c_target_buf_reset
)

MCTP_COMMAND Returns “No response data”

The CLI polls too fast before firmware finishes the bus transaction. V1528+ CLI includes automatic retry (100ms intervals, up to 30 seconds). If still failing, verify the drive was discovered and IBI enabled.

Short MCTP Response (< 4000 bytes)

Drive sent an error packet (SOM=EOM=1 in a single packet). Check byte 3 bit 6 for EOM flag. Common cause: drive MCTP state not reset from previous transaction.

Watchdog Reboot at Idle

V1528 resolved idle watchdog crashes present in intermediate builds. If this recurs, verify firmware version matches the release binary (not an intermediate build). Expected version: 1531.

PIO Dump Shows Stall

If pio_dump shows tx_stall=1 with tx_empty=0, the PIO state machine is blocked waiting for the bus. Run targetreset to clear.

Error Log

Read and clear the firmware error log:

(
: ${SLOT:?Set your slot number with SLOT=N first}
echo "# Executing example on user SLOT=$SLOT"
sb_i3c -d $SLOT i3c_err_get    # Read error log
sb_i3c -d $SLOT i3c_err_clear  # Clear after review
)

15. Support

When reporting an issue, please include:

  • Firmware version: sb_i3c -d $SLOT status

  • Test script output if applicable

  • Error log: sb_i3c -d $SLOT i3c_err_get

  • PIO dump if bus behavior is unexpected: sb_i3c -d $SLOT pio_dump

  • Ring contents if framing-related: sb_i3c -d $SLOT ring_read

  • Debug level 2 log from the firmware’s USB CDC console if SMBus frame assembly is suspected — see Capturing the Debug Log in section 11 for the full procedure

  • The exact command sequence that reproduces the issue

  • Whether the issue reproduces on the FT260 path (slot 2 vs slot 1)


SANBlaze Technology Inc. — Confidential Copyright 2024-2026 SANBlaze