Skip to main content

Flashing STM32 firmware over I3C using Binho Supernova

August 31, 2026 · 7 min read
Share:
Flashing STM32 firmware over I3C using Binho Supernova

Every STM32 MCU ships with a bootloader burned into ROM. You did not write it, you cannot erase it, and it will program flash for you with no debugger attached. Pull one pin high, reset the part, and it comes up listening instead of running your application.

Which interfaces it listens on varies by part, and AN2606 lists them per device. The long-standing entries are UART, I2C, SPI, CAN and USB. On the STM32H5 the list also includes I3C, with the protocol documented in AN5927.

Two properties of I3C are relevant to using it this way. Target addresses are assigned at run time through dynamic address assignment, so several devices can share one bus without strapping resistors or fixture changes between boards. And it is a two-wire bus, so a board already routing I3C for other reasons can carry a firmware update without a dedicated programming header.

We wrote a host implementation against a Supernova and a NUCLEO-H503RB to see what driving the interface is actually like. Erasing, programming and verifying a 122,912-byte image takes 11.1 s.

Two things about the protocol are worth passing on. The first constrains your host stack before you write a line of code.

Peripheral is not the same as bootloader

Worth settling first, because these get conflated. Having an I3C peripheral does not mean the ROM bootloader offers I3C. Here is where it actually stands:

FamilyPartsDevice ID
STM32H5H5030x474
STM32H5H523, H5330x478
STM32H5H562, H563, H5730x484
STM32H7R, H7SH7S780x485
STM32U3U3850x454

The STM32N6 has the peripheral but ST's Open Bootloader ships no I3C interface for it. We only put the H503 on the bench, so treat the rest as "documented, not tested by us", and check AN2606 for your exact part before designing around it.

Read the target's source, not only the app note

The reference document is AN5927, "I3C protocol used in the STM32 bootloader". Start there. It is the right document.

But an application note has a structural problem when you are implementing a wire protocol: it describes the happy path. What you need to know is what the other end does when you get something slightly wrong. Which byte does it check first? Does it NACK, or just go quiet? What state does it land in afterwards?

ST answers all of that in public, and not enough people seem to know it. The Open Bootloader middleware (STMicroelectronics/stm32-mw-openbl on GitHub) is the same protocol implemented in C, in the open. Two files carry the whole story: Modules/I3C/openbl_i3c_cmd.c for command framing and Interfaces/Patterns/I3C/i3c_interface.c for the transport. Reading the code that is going to answer you settles every ambiguity a document leaves open.

That is where we found the thing that changed our design.

The bootloader acknowledges with an interrupt

On the I2C and UART bootloaders, life is simple. You send a command, you read back a status byte. Request, response, repeat. Every STM32 flashing tool ever written has that shape and we assumed we were about to write another one.

The I3C bootloader does not work that way. There is no status register to poll. The STM32 is the I3C target here, with the Supernova as controller, and it reports status by raising an in-band interrupt carrying a single payload byte: 0x79 for ACK and 0x1F for NACK. Those are the only two values we saw. The bootloader core also defines a busy byte and a command-error byte, but the I3C path never sends either: we threw a malformed opcode, an unsupported opcode and a bad address at it, and every one came back 0x1F.

It is right there in ST's transport layer. The target enables IBI, configures exactly one additional data byte, and every acknowledgement for the rest of the session goes out that way:

/* Enable IBI procedure */
LL_I3C_EnableIBI(I3Cx);
/* Configure one additional data byte after IBI acknowledge */
LL_I3C_ConfigNbIBIAddData(I3Cx, LL_I3C_PAYLOAD_1_BYTE);

/* ... and then, for every acknowledgement it ever sends: */
void OPENBL_I3C_SendAcknowledgeByte(uint8_t Acknowledge)
{
  LL_I3C_SetIBIPayload(I3Cx, Acknowledge);
  LL_I3C_TargetHandleMessage(I3Cx, LL_I3C_TARGET_MTYPE_IBI, 1U);
  ...
}

So your host cannot be a write/read loop. Every step of every command is "send bytes, then block until an interrupt arrives".

Check this against your I3C stack before you write anything else. Plenty of convenient high-level wrappers expose IBIs only as fire and forget async events: you get a callback somewhere, with no way to say "now wait for the next one". That is not enough here. We would rather have learned that on day one than after the command layer was written.

The bug that only showed up at scale

Our first read implementation did the obvious thing. One Read Memory command per 256-byte block, each with its own address. Clean, stateless, easy to reason about.

Small reads worked perfectly. Then we asked for 32 KB.

run 1: FAIL at chunk 27  (0x08001B00)
run 2: FAIL at chunk 79  (0x08004F00)
run 3: OK   32768 bytes, 128 chunks, 2.19s
run 4: FAIL at chunk 31  (0x08001F00)
run 5: FAIL at chunk 15  (0x08000F00)

A different offset every run, and one run in five that sailed through clean. That is the worst possible failure signature, because it is exactly the shape that makes you suspect your own checksum arithmetic. We did, for a while.

The failures were not as random as they looked. They always landed in the same place: waiting for the acknowledgement of a newly issued command opcode. Never mid-block. Never on the address. Always on the first two bytes of the next command.

That is a turnaround problem, not a data problem. When the target finishes sending a block it exits its transfer loop and walks back through the command dispatcher to wait for the next opcode. Write into that window and your bytes arrive while the receive path is not ready. The command is dropped, no IBI is ever raised, and the host sits waiting for an answer that is not coming.

The tempting fix is a delay. We tried it. It is not a fix.

ApproachDelay between commandsSuccessful runsAverage
One command per block0 ms1 of 32.17 s
One command per block1 ms2 of 32.38 s
One command per block2 ms0 of 3n/a
One command per block5 ms3 of 32.91 s
Transfer loop0 ms3 of 31.39 s
Transfer loop1 ms3 of 31.57 s

Look at the 2 ms row going 0 for 3. With a hazard like this, a better number in a small sample is noise, not progress. Padding with delays moves the failure rate around without removing the cause, which is a great way to ship something that fails one board in twenty on a production line.

The protocol already has the answer. Both Read Memory and Write Memory carry a size field whose bottom bit means "more blocks follow". Set it and the target stays in its transfer loop, advancing the address itself, so you send the address once for the whole image. We read that as a throughput optimization. It is a correctness requirement. Staying in the loop is both reliable and about 35% faster, and six consecutive 128 KB reads afterwards came back byte identical.

Proving it actually took

One thing worth being pedantic about. The tool reads back every byte it wrote and compares, which proves flash contains the image you handed it. It does not prove the part is running that image. A target still sitting in the bootloader, or one who's reset never released, passes a read-back check perfectly.

So, we built two images of under 900 bytes that differ only in what they say. Each prints a banner at reset and then one line a second over the ST-LINK virtual COM port, which is already on the USB cable you have plugged in:

========================================
  Binho  |  AN0001
  Programming STM32 microcontrollers
  over I3C with the Binho Supernova
========================================
  Running: IMAGE A
  Kernel clock: 32 MHz
  LED blink: 1 Hz
========================================
  IMAGE A  alive, 1 s
  IMAGE A  alive, 2 s

Flash the other one and it says IMAGE B, and the LED goes from 1 Hz to 5 Hz so you can tell them apart with no terminal at all. That transition is the actual end to end result: erased, programmed and verified over I3C, and the part is running what you sent it.

The heartbeat line exists because a banner printed only at reset is missed if you open the terminal a second too late, which is exactly what happened to us the first time we captured this.

One detail from writing them that is worth stealing. The images do not touch the clock tree, and they read HSIDIV at run time to work out the baud divisor rather than assuming a frequency. On this part the reset default is a divide by two, so it runs at 32 MHz and not the 64 MHz the HSI itself does. Hardcode 64 and your baud rate is out by exactly a factor of two, and the only symptom is garbled output rather than no output, which is a much worse thing to debug.

Source, makefile and both prebuilt binaries ship in the assets archive. They are bare metal, no HAL and no CubeMX, so the whole thing is one readable file.

Is it worth it?

Our opinion, and it is an opinion: if your board already has SWD wired up, no. Nothing here beats a debugger you have already paid for and connected.

The case that matters is the board that has an I3C bus on it for other reasons and no room, no connector or no budget for a programming header. There, two wires you already routed become a full firmware update path: identify the part, erase it, program it, verify every byte, boot the application, no debugger anywhere. And because it is I3C, several targets can share the bus and be selected by dynamic address at flash time, with no strapping and no fixture changes between boards.

Mostly we came away thinking more people should know this interface exists. It has been sitting in ROM on every H5 since launch.

Get the details

The full procedure, the wire level protocol, the connection tables and the measured results are in AN0001. The reference utility is a single Python file whose only dependency is the Supernova SDK, and it does info, flash, read, erase and reset, takes .hex or .bin, and can drive BOOT0 and NRST over the Supernova's GPIO so bootloader entry needs no hands.

Read AN0001 (PDF)
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.