mcp2221a

Overview

The Microchip MCP2221A USB-to-I2C bridge, driven over HID through hidapi (hidraw on Linux, the darwin backend on macOS). It exists for the hosts with no kernel driver for the chip, which is macOS; on Linux hid_mcp2221 exposes the same part as a normal I2C adapter and i2c_bus prefers that path. Byte layouts are from the datasheet, DS20005565E.

It is the chip, not an abstraction: status, speed, cancel, one write, one read, a scan and a reset. The bus interface other code is written against is i2c::Bus in i2c_bus, which wraps this. The chip’s GPIO, ADC, DAC and UART functions are not implemented.

Public headers

Header  
mcp2221a/mcp2221a.h MCP2221A (open, get_status, set_i2c_speed, cancel, i2c_write, i2c_read, scan_i2c_bus), MCP2221AStatus, I2CState, the command opcodes.

Using it

Link the CMake target mcp2221a. The demo executable mcp2221a_i2c_scan is the whole API in use:

MCP2221A mcp;
if (!mcp.open()) return 1;
if (!mcp.set_i2c_speed(100000)) return 1;

for (uint8_t address : mcp.scan_i2c_bus()) {
    SPDLOG_INFO(" - 0x{:02x}", address);
}

i2c_bus’s HID backend does the same and then forwards write, read and probe.

Behaviour worth knowing

address_acked is byte 20 bit 6 of the status response and only that bit. The datasheet marks bit 7 and bits 5 to 0 “don’t care”, and on real hardware they are not zero, so comparing the whole byte to zero read as NACK always, which is why scan_i2c_bus() never found anything on a working bus. The sense is inverted on the wire and the field is the negation.

The report builders are named functions with concrete parameter types rather than template specialisations, because the specialisations were quietly skipped for the entire life of the driver: call sites passed promoted ints, the primary template packed the arguments consecutively, the address landed in the length field, and every transfer went to the general-call address. A mismatched argument to a named function is a conversion, not a silent change of overload.

The I2C engine latches. A NACKed address, or a cancel issued with nothing in progress, leaves it in StopTimeout (0x62), every parameter change is refused while it persists, and the state survives process exit because nothing power-cycles the chip. Cancel does not clear it; five consecutive cancels were acknowledged on hardware and left it at 0x62. Only a device reset does, so cancel() treats “already idle” as success and does not touch the bus.

open() follows a successful hid_open() with a real status query, because the handle can land on a node about to disappear. If the engine is found wedged it resets the device and waits for it to re-enumerate, measured at about 6 s through VMware USB passthrough and far quicker on bare metal; the node path is recycled, so each candidate handle has to prove itself with a transaction. That reset is conditional on the engine being wedged rather than run on every open.

The speed divider is 12000000 / speed_hz - 3 and is only consumed when a speed change is requested, so the division is guarded: callers that only cancel or only read status pass zero, and dividing by it traps on x86 and yields garbage on AArch64. A write command being accepted means only that the engine started; the address-ACK bit is checked afterwards, or a write to an empty address “succeeds” and surfaces later as an unexplained read error, which is how the MFi coprocessor’s silence used to present. A sleeping client NACKs its first access and wakes on it, so that case is logged at debug. Reads are fetched in 60-byte chunks with retries while the transfer is still in progress.

Tests

None registered. mcp2221a_i2c_scan prints and returns 0, which makes it a demo rather than a test, and everything above marked “on hardware” was observed with a bridge attached and is not reproduced by the build.


This site uses Just the Docs, a documentation theme for Jekyll.