Skip to main content

Exploring the I3C interface on Bosch Sensortec MEMS sensors

August 31, 2026 · 6 min read
Share:
Exploring the I3C interface on Bosch Sensortec MEMS sensors

Look at the interface line in the datasheet of almost any current Bosch Sensortec part and you will find a third entry that was not there a generation ago.

PartFunctionInterfaces
BMI3236-axis IMUI2C, I3C, SPI
BMP581PressureI2C, I3C, SPI
BMP585Pressure, media resistantI2C, I3C, SPI
BMM350MagnetometerI2C, I3C
BMA530AccelerometerI2C, I3C, SPI
BMA580AccelerometerI2C, I3C, SPI

I3C is on all of them, and reaching it takes no configuration. The BMP581 and BMP585 datasheets describe the handover: the part comes up in I2C mode and, the first time it sees the I3C broadcast address 0x7E on the bus, disables its I2C behavior and stays in I3C until a reset or a power cycle. Same two pins, same two nets on the board, and the controller decides which protocol they carry.

We put three of these parts on our bench and went looking at what is actually on the other side of that interface. Not a protocol tutorial: what you see, in what order, when you drive a real sensor over I3C for the first time.

Why a sensor would want I3C at all

Two things, and both of them address problems that show up specifically when the thing on the bus is a sensor.

Addresses are handed out at run time. An I2C sensor has a static address, usually with one pin to choose between two of them: the BMP581 is at 0x46 with SDO low and 0x47 with SDO high, and the BMP585 uses the same pair. Two of those parts on one I2C bus is fine, one at each address. A third is not possible, and neither is a second part that happens to share the pair. On I3C the controller assigns each target an address during enumeration, and how many of them collide on paper stops mattering.

And the sensor can interrupt the host over the same two wires it already uses for data. No additional signal to route, no GPIO allocated, no extra conductor on a connector. That one is worth seeing rather than reading about, so it comes later.

Plug it in and ask who is there

We drove the bus with our Supernova and the Binho I3C Target Board, which takes a Bosch Shuttle 3.0 board directly. Bus initialization sends RSTDAA to clear any address already assigned, then ENTDAA, during which each target arbitrates with its 48-bit provisional identifier and is handed a dynamic address. With one part present, that is 0x08:

$ 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  07 70 10 51 10 00   device id 0x1051
    BCR  0x06   DCR 0x62
    HDR  SDR only   IBI capable
    profile  bmp585

Everything after the address came from the part rather than from a lookup table, and that is the first thing that feels different from I2C. On I2C you write an address you picked in advance and hope something answers. Here the device introduces itself.

Three registers do the introducing. The provisional identifier carries a manufacturer ID and a part number: 0x1051 for the BMP585, 0x1050 for the BMP581, 0x1043 for the BMI323, each embedding the chip ID you would otherwise have read from a register.

The device characteristics register is the one worth pausing on. Both pressure sensors report 0x62, which the MIPI registry assigns to pressure sensor. A controller that has never met this part can tell what class of device it is talking to.

And the bus characteristics register says what the part can do on the bus:

BCR 0x06
  device role                      I3C target
  advanced capabilities (bit 5)    no, so SDR only
  IBI mandatory payload            yes
  IBI capable                      yes
  max data speed limit             no limit

Two useful facts land there before anything has been configured. The part can raise in-band interrupts and will say why when it does. And bit 5 is clear, so it is single-data-rate only, which settles the high-data-rate question from the target itself in one command.

Reading a register, and the first surprise

Getting a value out is a private transfer: send the register address, read the bytes. The framing, though, is per part rather than per vendor.

BMI323BMP581 and BMP585
register address8 bits8 bits
data width16 bits8 bits
dummy bytes before the payload20

The BMI323 inserts two dummy bytes ahead of every read payload, and its datasheet says so. The BMP parts insert none. Carry the assumption from one Bosch part to another and your first read comes back shifted, which looks like a wiring fault and is not.

Worth knowing too: the leading 0x7E broadcast header is optional on these parts, and a read with or without it returns the same value.

Once the framing is right, the data is undramatic, which is rather the point:

$ python i3c_mems.py stream --device bmp585 --seconds 2

  temperature    21.583 C   pressure 100351.7 Pa   pressure  1003.52 hPa
  temperature    21.583 C   pressure 100351.7 Pa   pressure  1003.52 hPa

The BMI323 at rest reports an acceleration magnitude of 1.006 g against a configured range of plus or minus 8 g, which is a pleasant property of sensors: gravity is a free calibration check. The two pressure sensors read 1003.52 and 1003.88 hPa in the same room within the hour, two different parts on two different boards agreeing to 0.36 hPa.

The part that is actually different

Here is the reason to care. On the BMI323, the register that routes interrupt sources has an extra destination.

INT_MAP2 at 0x3B holds a field per source. acc_drdy_int sits at bits 11:10 and takes 0b01 for the INT1 pin, 0b10 for INT2, and 0b11 for the I3C in-band interrupt. Write 0x0C00 and the accelerometer's data-ready interrupt stops needing a pin:

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

  IBI from 0x08  MDB 02  sample ready (acc, gyr, temp)=True
  IBI from 0x08  MDB 02  sample ready (acc, gyr, temp)=True

  150 interrupts in 3.00 s = 50.00/s
  configured output data rate 50 Hz (+0.0%)

Fifty per second against a configured 50 Hz, with nothing connected to either interrupt pin. Each interrupt carries a mandatory byte saying which condition fired, so the host knows whether it was woken by new data, a FIFO threshold or something else before it reads a single register.

In a space-constrained design, that is one net you don't have to route and one GPIO pin you do not have to find. On a bench it means you can watch a sensor's interrupt behavior without wiring anything extra to see it.

Three things that may catch you off-guard

Each of these produced a configuration that looked entirely correct and delivered no interrupts.

ENEC is the switch, and nothing else is. Enabling the sensor, routing the interrupt, and telling the controller to accept interrupts all leave the bus silent. Nothing arrives until the ENEC command is sent with its ENINT bit. Measured one step at a time: zero, zero, zero, then fifty per second.

The BMP parts latch by default and deliver exactly one. INT_CONFIG resets with latched mode selected and INT_STATUS is clear-on-read, so the interrupt asserts once and is never re-armed until the host reads the status register. One interrupt then silence looks like broken silicon; it is a two-line fix, either select pulsed mode or read the status register each time, and both then give the configured rate.

Configure a BMP part in standby, not while it is measuring. Writing INT_CONFIG mid-measurement is accepted by the register, which reads back the new value, and ignored by the interrupt block until the next transition from standby into a measurement mode. Every register reads correct and nothing arrives. The datasheets do say to write configuration before entering a measurement mode.

What these parts do not do

I3C has a long feature list, and a sensor implements the part of it a sensor needs. Measured on all three: no high-data-rate modes, no group addressing, no configurable maximum read or write length, no hot-join.

None of that turned out to matter for reading a sensor. Both parts we swept sustained every bus rate the adapter offers, to 12.5 MHz push-pull and 4.17 MHz open drain, over a loop of chip-identifier reads with no errors. Put a number on the demand: three axes at 16 bits, sampled at 50 Hz, is 300 bytes of payload per second. Clocked at 12.5 MHz, and even allowing generously for addressing and framing overhead, that is well under a millisecond of bus time in each of those seconds. Bus headroom is not the constraint for reading a sensor.

The useful habit is to stop guessing which subset a given part implements. A datasheet's command-support table and the status code a transfer returns can each mislead you, in opposite directions, so it is worth establishing a part's real behavior by observing effects: set a value and read it back, move an address and confirm the old one stops answering, count interrupts against a rate you configured.

Go and try it

AN0004 is the full write-up: enumeration and identification in detail, the register framing per part, the measured capability matrix for the BMI323, BMP581 and BMP585 with the evidence behind every row, the interrupt configuration sequences, and the bus rates each part sustained with the method used to establish them.

The Python utility that produced every output above ships with it. Adding a device is one entry in a table: the framing, the expected identity, the writes that start a data stream, and a decoder for the interrupt payload. The I3C Target Board takes all six of the Bosch parts listed at the top of this post, so if yours is one of the three we did not exercise, that table entry is where to start.

Read AN0004 (PDF) or download the utility and assets.

Back to all posts
Share:

Working with I3C?

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