SANBlaze I3C Controller — Release Notes

V1528 (March 2026)

Previous release: V1527

Summary

V1528 adds complete MCTP over I3C support for drive health monitoring. The firmware can now send multi-packet MCTP commands to MCTP capable drives, read full 4096-byte Identify Controller responses across 65 packets, and route the data into the existing target ring buffer for seamless customer application integration. The release also adds I3C Private Transfer support, USB chunking for large payloads, PIO diagnostic tooling, and comprehensive test coverage.


New Features

MCTP over I3C (NVMe-MI)

  • mctp_command: Single CLI command fires buffered MCTP packets, polls for IBI, and reads all response packets autonomously. Returns decoded MCTP packet data with SOM/EOM flags, sequence numbers, and ASCII payload view.

  • mctp_to_ring: Same as mctp_command but routes the complete response into the target ring buffer. Customer applications read data via the existing TARGET_CMD_DATA_READ (0x52) path with zero code changes. Ring buffer reports 0 bytes until the entire response is committed.

  • mctp_read: Firmware-side multi-packet read with exact EOM byte counting. Reads up to 118 packets per transaction with per-packet watchdog updates.

SETMWL / SETMRL (CCC 0x89 / 0x8A)

  • setmwl <da7> <bytes>: Sets Maximum Write Length on the target via direct CCC 0x89. Payload is 2 bytes big-endian. Must be sent after ENTDAA and ENEC, before any MCTP traffic. Required by many I3C MCTP targets to establish maximum transfer unit.

  • setmrl <da7> <bytes>: Sets Maximum Read Length on the target via direct CCC 0x8A. Same timing requirement and payload format as SETMWL.

  • Both commands are visible on the protocol analyzer. Standard practice is to set both to 69 (0x00 0x45) to match the MCTP packet size.

I3C Clock Management

  • Separate clock values are now maintained for I2C and I3C modes. i2c_clk <khz> and i3c_clk <khz> write the desired clock to persistent storage without touching hardware. set_mode i2c|i3c applies the stored clock for that mode automatically. Defaults: I2C 100 kHz, I3C 1250 kHz.

  • This eliminates the previous behavior where switching modes could leave the hardware running at the wrong clock.

I3C Private Transfers

  • private_write [--hold] <da7> <bytes>: I3C Private Write with 0x7E broadcast + Restart preamble. The --hold flag buffers packets in firmware for back-to-back burst transmission (required for multi-packet MCTP commands).

  • private_read <da7> <nbytes>: I3C Private Read with 128-byte chunk support (up from 56 bytes).

  • Burst write system accumulates up to 512 bytes across multiple --hold calls, then fires all packets back-to-back with proper STOP separation.

USB Multi-Chunk Transfers

  • Payloads larger than 56 bytes (USB EP0 limitation) are automatically split into chunks with FIRST/LAST flags and reassembled by firmware. Enables 69-byte MCTP packet writes that previously required workarounds.

PIO Diagnostics

  • pio_dump: Reports bus state (SDA/SCL levels, GPIO functions), PIO state machine status (PC, enabled, FIFO state, stall flags), target state, IRQ flags, and disassembled PIO instructions at current PC. Automatically runs on test failure.

Private Force Mode

  • private_force <on|off>: Forces all subsequent transfers to use I3C Private mode (0x7E broadcast + Restart preamble). Useful for drives that require Private framing for all communication.


Bug Fixes

I2C Clock Set to 10 MHz by TinyDriver

  • Fixed: The Linux i2c-tiny-usb driver sends a SET_DELAY control request on attach. The firmware’s SET_DELAY handler computed clock frequency with a 1000x unit error, converting a 10 kHz intent into a 10 MHz hardware setting. This left the bus running at full RF speed, causing continuous SCL oscillation and bus noise. Fix: removed clock manipulation from the SET_DELAY handler entirely. The stored delay value is preserved but clock is now set exclusively by set_mode i2c|i3c using the separately stored per-mode clock value.

Drive Lockup Prevention

  • Fixed: Reading past end of EOM packet data (0xFF padding bytes) permanently wedged the MCTP capable drive’s I3C state machine. Root cause: drive interpreted reads beyond real data as protocol violation. Fix: compute exact byte count for EOM packet from NVMe-MI message structure.

Missing STOP Condition

  • Fixed: PIO open-drain STOP was electrically invisible when the MCTP capable drive released SDA/SCL to float (both sides high-impedance with only pullups). Fix: GPIO push-pull STOP sequence after read loop drives definitive low-then-high transitions. Pins restored to PIO0 function after STOP.

Pullup Ordering

  • Fixed: Re-enabling GPIO pullups after PIO STOP meant STOP executed without pullup support, causing ambiguous bus levels. Fix: pullups enabled before STOP opcode.

Stack Overflow

  • Fixed: 384-byte debug arrays (dbg_tbit[128], dbg_raw[128], dbg_byte[128]) on stack corrupted MCTP packet data in adjacent memory. Removed entirely; replaced by computed exact byte count approach.

Watchdog Stability

  • Fixed: Timeout protection macros (PIO_WAIT_TX_EMPTY, PIO_WAIT_IDLE) and defensive code additions caused idle-time watchdog reboots every ~128 seconds. Root cause: added complexity interfered with main loop timing. Fix: clean surgical patch on proven BK16 baseline with only three targeted changes.

USB Deadlock in Debug Output

  • Fixed: CDC debug printf in hot paths could deadlock USB stack when output buffer full. Added per-byte buffering with overflow protection.

Target PIO Re-enable

  • Fixed: Target PIOs were not re-enabled after initiator transactions, causing missed incoming writes. Added target_pio_force_address() to bypass rate limiting on re-enable.

T-bit Overdriven During SDR Read (Critical)

  • Fixed: i3c_sdr_read() in i3c_hl.c was driving SDA with push-pull output during the T-bit clock cycle instead of reading it. The ninth PIO transfer bit was encoded as SDR_WBIT(continuetransfer), meaning the controller actively drove SDA high on every non-last byte, overpowering the target’s T=0 assertion. T=0 was only visible on the last byte of a packet (when continuetransfer=0). Fix: changed the ninth bit to SDR_RBIT(0) so SDA is sampled as input during the T-bit clock cycle. The continuetransfer parameter is now unused and suppressed.

  • Impact: Without this fix, multi-packet reads (4K Identify, 65 packets) never detected T=0 mid-packet and continued clocking the drive after it had finished sending, causing drive lockup requiring targetreset to recover. The math-mode workaround (expected=4096) was masking this bug. Math mode is no longer required; T-bit-only mode (pkt_size=N, expected=0) is now the correct and tested default.

TinyDriver i2c Read Preemption During Active I3C Transaction

  • Fixed: The Linux i2c-tiny-usb driver (TinyDriver) could issue an i2c read request via unified_i2c_read() while firmware was mid-MCTP-read from the drive. This happened because tud_task() is called inside the MCTP read loop to keep USB alive, and TinyDriver’s USB control request could be serviced during that call. The preempting read caused firmware to issue a STOP mid-packet, cutting off the drive response and hanging the bus. Fix: unified_i2c_read() now checks g_initiator_active and returns -1 (NAK) immediately if an I3C initiator transaction is in progress. TinyDriver sees a NACK, stalls, and retries after its own timeout by which time the transaction is complete.

mctp_to_ring Default Expected Bytes

  • Fixed: HOST_CMD_MCTP_TO_RING (0x62) defaulted to expected=4096 when called with no payload (as TinyDriver does: cmd=0x62 len=0). This activated math mode and stopped the read after a computed byte count, truncating short responses. Fix: default changed to expected=0 (T-bit-only mode). Callers that need math mode must pass explicit parameters.


Test Suite Updates

  • Test 38: SETMWL 69 — verifies direct CCC 0x89 accepted by drive after ENTDAA/ENEC

  • Test 39: SETMRL 69 — verifies direct CCC 0x8A accepted by drive after ENTDAA/ENEC

  • Test 41: MCTP 4K Identify via manual burst write + IBI poll + mctp_read

  • Test 50: MCTP discovery via mctp_command (single-packet Get Endpoint ID)

  • Test 51: MCTP 4K Identify via mctp_command (autonomous firmware path)

  • Test 52: MCTP 4K Identify via mctp_to_ring (ring buffer customer path). Verifies committed byte count, reads data via i3c_data_read, confirms ring buffer drains to empty.

  • Tests 42, 61, 62 removed (functionality covered by other tests or deferred).

  • PIO dump automatically captured on test failure for post-mortem analysis

  • SETMWL/SETMRL now issued as preamble in all MCTP tests (38–52)


TinyDriver (sb_i2c) Validation

MCTP over I3C has been validated end-to-end via the Linux i2c-tiny-usb driver path using sb_i2c. TinyDriver sends the MCTP command via the existing I3C private write path (cmd 0x35) and fires mctp_to_ring (cmd 0x62) with no payload. Firmware routes the drive response into the ring buffer; the driver reads it back via TARGET_CMD_DATA_READ (0x52).

Example command and response (Get Endpoint ID, MCTP discovery):

sb_i2c -n 1 -d 2 -V -0 5,1 -z -w 84 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 e2 00 06 07

smbus_rsp[0]:
3b 01 00 00 c5 84 88 00 00 00 20 00 00 01 02 00
01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 9c 10 9f
07 11

The response is 49 bytes of real data (T=0 at byte 48), no 0xFF padding, no drive lockup, and no targetreset required between successive commands.


CLI Changes (sb_i3c)

New commands:

Command

Description

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

I3C Private Write, optional burst buffering

private_read <da7> <nbytes>

I3C Private Read (up to 128 bytes)

mctp_read <da7> [pkt_size]

Read all MCTP response packets

mctp_command <da7> [pkt_size]

Fire burst + IBI + read (autonomous)

mctp_to_ring <da7> [pkt_size]

Fire burst + IBI + read → ring buffer

private_force <on|off>

Force Private mode for all transfers

pio_dump

Dump PIO/bus diagnostic state

setmwl <da7> <bytes>

Set Maximum Write Length (CCC 0x89, direct)

setmrl <da7> <bytes>

Set Maximum Read Length (CCC 0x8A, direct)

i3c_data_dump [N]

Binary ring buffer read (u16 status; u16 len; data)

Modified commands:

Command

Change

i3c_sdr_write

Multi-chunk support for payloads > 56 bytes

i3c_sdr_read

128-byte chunk support for Private reads

i2c_clk <khz>

Now writes to persistent storage only; hardware applied by set_mode

i3c_clk <khz>

Now writes to persistent storage only; hardware applied by set_mode

set_mode <i2c|i3c>

Now reads and applies the stored per-mode clock automatically


Firmware Command Reference

Cmd

Hex

Direction

Description

HOST_CMD_MCTP_READ_ALL

0x61

OUT/IN

Read all MCTP packets (firmware-side)

HOST_CMD_MCTP_TO_RING

0x62

OUT/IN

MCTP command → ring buffer

HOST_CMD_MCTP_COMMAND

0x63

OUT/IN

MCTP command → USB retrieval

HOST_CMD_PIO_DUMP

0x5D

IN

PIO diagnostic dump

HOST_CMD_SET_PRIVATE_FORCE

0x5E

OUT

Private mode force on/off

CHUNK_FLAG_PRIVATE

0x08

I3C Private transfer flag

CHUNK_FLAG_HOLD

0x10

Burst buffer accumulation flag


Upgrade Notes

  • Firmware binary replaces V1527 with no configuration changes.

  • CLI binary (sb_i3c) must be updated to match firmware for new command support.

  • Test script (sb_i3c_test.sh) updated with tests 50-52; existing tests unchanged.

  • Existing customer applications using TARGET_CMD_DATA_READ require no changes when using the mctp_to_ring path. Only the driver layer needs to send the new setup commands (see MCTP Ring Buffer Integration Guide).