Skip to main content

Exploring the I3C interface on TDK InvenSense MEMS sensors

September 2, 2026 · 6 min read
Share:
Exploring the I3C interface on TDK InvenSense MEMS sensors

We ran a TDK InvenSense ICM-45686 six-axis IMU over I3C from a Binho Supernova and the Binho mikroBUS Adapter Board. The sensor is on a 6DOF IMU 27 Click, which drops into the mikroBUS socket. The only connections are clock, data, ground and a 3.3 V supply.

Most of what follows is the same work any I3C sensor needs: enumerate, identify, read registers, stream data, take interrupts. The part that is not routine is HDR-DDR, and that is most of this post.

Enumeration

Bus initialization is two common command codes. RSTDAA clears any dynamic address already assigned. ENTDAA then runs dynamic address assignment, with each target arbitrating on its 48-bit provisional identifier and receiving an address.

$ python i3c_mems.py scan

1 target(s) on the bus at 3300 mV, PUSH_PULL_5_MHZ_50_DC, OPEN_DRAIN_1_MHZ

  dynamic address 0x08
    PID  04 6A 00 00 00 11   device id 0x0000
    BCR  0x27   DCR 0x44
    HDR  claimed   IBI capable
    profile  icm45686

No address was configured on the host, and the part's static address was not used.

Identity

The device characteristics register reports 0x44, which the MIPI registry assigns to a 6-axis IMU. The identity register WHO_AM_I at 0x72 reads 0xE9.

The provisional identifier is worth a note. Its device id field reads 0x0000. On many sensors that field carries the same value the part reports in its identity register, which lets a host name the part from enumeration alone. This part leaves it empty, so a host that identifies parts that way will not recognise it and has to read the register instead.

The bus characteristics register reads 0x27:

BCR 0x27
  device role                      I3C target
  advanced capabilities (bit 5)    yes
  IBI mandatory payload            yes
  IBI capable                      yes
  max data speed limit             yes

Bit 5 is set, which means the target claims high data rate transfers. GETHDRCAP answers 0x01, the bit the MIPI specification assigns to HDR-DDR. That claim is what the rest of this post is about.

Registers and data

Register access is a private transfer. The part uses an 8-bit register address with 8-bit data and no dummy bytes, and the address auto-increments on a multi-byte read.

Two writes start the accelerometer. ACCEL_CONFIG0 at 0x1B sets the full scale and the output data rate, and PWR_MGMT0 at 0x10 puts it in low noise mode.

$ python i3c_mems.py stream --device icm45686 --seconds 5

  x     0.021 g   y    -0.028 g   z     1.005 g   magnitude     1.006 g
  x     0.020 g   y    -0.028 g   z     1.005 g   magnitude     1.005 g
  x     0.021 g   y    -0.028 g   z     1.006 g   magnitude     1.006 g

The magnitude column is a useful check. At rest an accelerometer measures gravity, so the magnitude should read 1 g. That single number verifies the register framing, byte order, sign handling and scale factor together.

It is also how we settled the byte order without opening the datasheet. Only one combination of byte order and full-scale factor gives 1.000 g at rest, and on this part that combination is little endian at ±32 g.

Two things here cost us time and will cost anyone else the same.

FIFO_DATA at 0x14 does not auto-increment. It returns the FIFO, which is correct for a FIFO register, and it sits early enough in the map that a first exploratory read is likely to land on it. With the FIFO empty, a four-byte read returns the same byte four times, which looks like an interface that cannot walk its own register file. Every other register advances normally.

The gyroscope's reset output data rate is 800 Hz. The two sensors have separate rate fields, so enabling both puts data-ready on the bus at the faster of the two. With the accelerometer at 50 Hz and the gyroscope left at its default, the interrupt stream swamped the bus and the run ended in a timeout part way through a three second window.

Interrupts

Data-ready is routed by INT1_CONFIG0 at 0x16, bit 2. Nothing arrives until ENEC is sent with its ENINT bit.

$ python i3c_mems.py ibi --device icm45686 --seconds 3

  IBI from 0x08  MDB 01
  IBI from 0x08  MDB 01

  152 interrupts in 3.00 s = 50.60/s
  configured output data rate 50 Hz (+1.2%)

INT1_STATUS0 at 0x19 is read to clear, and the host has to read it after each interrupt. Without that the data-ready condition stays asserted and the stream stops after the first one.

The mandatory data byte reads 0x01 on every interrupt with data-ready as the only enabled source. MIPI reserves bit 7 of that byte for a pending read and leaves the rest device-specific. What this part encodes in the remaining bits we did not establish, so the utility reports the byte rather than interpreting it.

HDR-DDR

High data rate double data rate transfers move data on both clock edges, doubling the signalling rate without raising the clock. The BCR says this part supports them, and a claim in a register is not a working transfer. The test is to read one register two ways and compare.

HDR-DDR against SDR    register 0x32 reads 6C 36 02 00 over SDR
                       and 6C 36 02 00 over HDR-DDR,
                       the same bytes by both routes

bus after HDR-DDR      SDR transfers still work, so the exit pattern
                       returned the bus to single data rate

Getting there took most of a session. Three things stand between a target claiming DDR and a host reading from it, and none of them produces an error.

The command byte is not a register address. A DDR read carries a command code, and the obvious thing to put there is the register you want. Reads issued that way return zeros. So do reads on every other command code: we swept all 128 read commands from 0x80 to 0xFF and got nothing but zeros and padding, with a successful result reported each time.

The register comes from the target's internal address pointer. A DDR read returns bytes from wherever that pointer sits, so the pointer has to be positioned before the read.

An SDR read positions the pointer without changing anything. Reading register N over SDR leaves the pointer at N+1. A DDR write also moves the pointer, but it writes as it goes, so it cannot aim a read at a register whose contents matter.

Reading register 0x32 onward over DDR is therefore three steps: read 0x31 over SDR, issue the DDR read, then trigger the HDR exit pattern to put the bus back in single data rate.

SDR read of 0x32, four bytes          ->  6C 36 02 00
SDR read of 0x31, then HDR-DDR read   ->  6C 36 02 00

One more constraint. DDR moves 16-bit words, so a payload is always an even number of bytes and an odd length is rejected outright. A two-byte read came back empty on our setup while four bytes worked, so four is the practical minimum.

What this result says is that the target and the controller complete a double data rate transfer, agree on the bytes, and return the bus cleanly to SDR afterwards. It says nothing about throughput. Doubling the signalling rate on the wire is not doubling the rate at which a sensor produces data, and reading four bytes out of a register file measures neither.

What the part implements

We establish support from observable effects rather than result codes, because a target can accept a command it does not implement. Each verdict below needed something to change: a value set and read back, an address moved and the old one confirmed silent, interrupts counted against a configured rate.

This part implements dynamic address assignment and reassignment, the identity commands, interrupt enable and disable, configurable maximum read and write lengths, timing control, and HDR-DDR. Group addressing is not implemented: a write through an assigned group address is refused before, during and after assignment. No hot-join request was seen at any point.

RSTDAA is worth a note. It is accepted every time, and on most runs the target releases its dynamic address and the old address stops answering. On roughly one run in eight it returns a successful result and keeps answering at the address it already had, across three consecutive attempts. A host that resets dynamic addresses should confirm the result rather than assume it.

On bus rates, the part passed every rate the adapter offers, push-pull to 12.5 MHz and open drain to 4.17 MHz, over a hundred consecutive register reads with no errors. That is a read loop rather than a throughput benchmark.

The application note

AN0006 has the detail this post skipped: the identity fields decoded one by one, the register map, a figure pairing the three interrupt steps with the register behind each, the capability matrix with the evidence for every verdict, the bus rate method, and a practical-notes section for the per-part behaviour that costs bench time.

The Python utility that produced every output above ships with it. Adding a part is one entry in a profile table: how its registers are framed, what identity to expect, the writes that start a data stream, the writes that route an interrupt onto the bus, and a decoder for the interrupt payload.

AN0006 (PDF) · Utility and assets (ZIP)

Back to all posts
Share:

Working with I3C?

Binho builds the industry's most comprehensive I3C test and measurement tools.