Bulk Transfers

Why bulk exists

Every command travels in HID reports, which cap one transfer at the report size: 64 bytes on Supernova, 1024 bytes on Pulsar. That is enough for register-level traffic, but a page-sized payload — a flash image, an EEPROM dump, a large sensor FIFO — pays the HID framing cost many times over, and the ordinary calls stop at MAX_I2C_TRANSFER_LENGTH and MAX_SPI_TRANSFER_LENGTH (1024 bytes).

Bulk transfers add a second, larger pipe for payload bytes only, over the device’s vendor bulk endpoints. Nothing else changes route.

How it works: the command and the payload travel separately

  • The request (command code, lengths, addresses, sub-addresses) and the reply (result code, announced payload length) travel over HID exactly like every other command.

  • Only the data bytes — what a write clocks out, or what a read brings back — move over the vendor bulk endpoints.

So if the host cannot claim the vendor endpoints, every HID command keeps working, including the ordinary i2cWrite, i2cRead and spiTransfer. Only the bulk calls become unavailable, and they say so with SDK_ERROR_BULK_UNAVAILABLE (0xC000) instead of hanging.

Which calls use it

Protocol

Controller

Target

I2C

i2cWriteBulk, i2cReadBulk

i2cTargetWriteMemory, i2cTargetReadMemory

SPI

spiWriteBulk, spiReadBulk, spiTransferBulk

spiTargetWriteMemory, spiTargetReadMemory

I3C

i3cSdrWriteBulk, i3cSdrReadBulk, i3cHdrDdrWriteBulk, i3cHdrDdrReadBulk, i3cLegacyI2cWriteBulk, i3cLegacyI2cReadBulk

none — i3cTargetWriteMemory / i3cTargetReadMemory use HID (see Target Mode)

Each has the same name, prefixed with CosmicSDK_, in the C API. The asynchronous C++ API has the same calls, but does not check the device’s capability record before sending (see below).

Layer by layer

  • Blocking C++ API (CosmicSDK) — the intended entry point. Every bulk call checks both the host and the device before it sends anything, then moves the payload and returns an ordinary result code.

  • C API — mirrors the blocking C++ API one to one. CosmicSDK_GetBulkCapabilities fills a caller-supplied CosmicSDK_BulkGroup array.

  • Asynchronous C++ API (CosmicAsyncSDK) — checks only that the host claimed the vendor endpoints. Query getBulkCapabilities yourself before sending a bulk command there, or a device that does not implement it costs a full timeout.

  • Python, Java and C# bindings — wrap the C API, with the same calls.

  • WebAssembly — the bulk entry points exist for parity and always answer 0xC000; the vendor endpoints are not reachable from the browser build.

When to use it

  • Use a bulk call when the payload exceeds what the ordinary call accepts, or when throughput matters: flash programming, memory dumps, streaming a target arena.

  • Keep the ordinary call for register-sized traffic. It works on every host, needs no vendor endpoint, and has no per-connection capability query.

  • I3C HDR-DDR gains nothing today: it is capped at I3C_HDR_DDR_MAX_PAYLOAD (1024 bytes), which the ordinary call already reaches. SDR is the I3C mode where bulk buys a larger transfer.

  • The SDK does not split transfers. Anything larger than one call’s ceiling is several calls.

group Bulk transfers: the four checks

What every *Bulk and bulk-backed *Memory call needs before it moves a byte.

A bulk call carries its request and reply over HID like every other command, and moves only the payload over the device’s vendor bulk endpoints. Two independent parties have to agree before that works – this host and the connected device – and the SDK checks both locally, returning SDK_ERROR_BULK_UNAVAILABLE (0xC000) without a round trip when either says no:

if (sdk.isBulkTransportAvailable()) {                        // host: vendor interface claimed?
const auto& caps = sdk.getBulkCapabilities();              // device: one query per connection
if (caps.supportsCommand(CMD_I2C_CONTROLLER_READ_BULK))    // per command
    n = std::min(n, caps.maxPayloadFor(GROUP_I2C));          // per-group ceiling
}
rc = sdk.i2cReadBulk(bus, address, subAddress, n, out);      // otherwise SDK_ERROR_BULK_UNAVAILABLE

The C API has the same two halves: CosmicSDK_IsBulkTransportAvailable() and CosmicSDK_GetBulkCapabilities(), which fills a caller-supplied CosmicSDK_BulkGroup array.

  • Size against the device, not a constant. The *_BULK_MAX_PAYLOAD constants are only compile-time upper bounds; the real ceiling differs by product and firmware build. A request over the device’s ceiling returns SDK_ERROR_WRONG_REQUEST.

  • HDR-DDR is capped at I3C_HDR_DDR_MAX_PAYLOAD (1024 bytes) regardless of what the device advertises for GROUP_I3C.

  • The SDK does not split transfers. A transfer larger than one call’s ceiling is several calls.

  • Windows: the vendor interface must be bound to WinUSB. The firmware declares it WinUSB-compatible, so Windows 8.1 and later bind it with no driver install; if another driver has been associated with that interface, the host cannot claim it and every bulk call returns 0xC000. On Linux, install the udev rules shipped with the SDK so a non-root user can claim it. Either way, getLastBulkError() (C: CosmicSDK_GetLastBulkError()) says why.

  • WebAssembly always answers 0xC000. The bulk entry points are exported for parity, but the vendor endpoints are not reachable from the browser build.

  • i3cTargetWriteMemory() / i3cTargetReadMemory() are not bulk calls: the 1024-byte I3C target arena fits in a HID request, so they need none of the above.