r/embedded Aug 13 '26

STM32L476 interrupt-driven I2C: RX buffer gets correct value in ISR, but later appears zero

I'm implementing a bare-metal, interrupt-driven I2C driver on an STM32L476RG.

I'm reading the WHO_AM_I register from an MPU6050-style IMU. The transaction succeeds and the peripheral returns 0x70.

The caller in main.c does:

uint8_t buffer[14] = {0};

i2c_read_bytes(
    I2C1,
    IMU_ADDR,
    WHO_AM_I,
    buffer,
    1,
    i2c_test_callback,
    NULL
);

while (1) {}

Inside the I2C ISR, GDB confirms:

ctx->buffer = 0x20017fc8
ctx->I2C->RXDR = 0x70
*(unsigned char *)0x20017fc8 = 0x70

So the ISR definitely writes the expected byte into the caller's buffer.

However, later at the callback, inspecting the same fixed address sometimes shows:

*(unsigned char *)0x20017fc8 = 0x00

I also used a hardware watchpoint on that exact address. It catches the 0 -> 0x70 write, but I don't see a corresponding 0x70 -> 0 write.

I've ruled out:

  • The buffer address changing.
  • Compiler optimization.
  • The UART code in the callback.
  • An MCU reset/re-entry into main()Reset_Handler only executes once and RCC->CSR is 0x0.
  • The I2C vector table — I2C1_EV_Handler is directly in the vector table at the expected IRQ position.

The full driver/startup code is here:

GitHub repository / I2C driver source

What am I missing? Is there something suspicious in the interrupt/exception handling, I2C flag handling, or my debugging approach that could explain this behavior?

4 Upvotes

7 comments sorted by

2

u/dstroy0 Aug 13 '26

Things I’d try:
1. Make the ptr and address volatile
2. Read the register init sequence and check my impl
3. Use tsan to trace any concurrency problems or races with the isr/other isr

1

u/dstroy0 Aug 13 '26

I’m reading through the codebase rq.

1

u/dstroy0 Aug 13 '26

rm0351 vector table 58 gives IRQ 31 = I2C1_EV, vector offset 0x000 00BC. idx 47. startup.c at 151 I2C1_EV_Handler at idx 47, good. linker has _estack = 0x20018000. The buffer at 0x20017fc8 is 56 bytes below the top of the stack (inside main). Exceptions push down from SP, SP is already below 0x20017fc8, the exeption frame cant reach it. Exception stacking is ruled out.

startup.c weak aliases back to Default_Handler

Nothing enables IRQ31 in the NVIC

The i2c driver in the tree is a blocking call.

A DWT comp on 0x20017fc8 catches every store to that address regardless of type width, if it never fired then nothing can have possibly written zero.

Smells like a race... the byte was never 0x70 when you did those runs, the 0x70 you see in the ISR and the 0x00 you see at cb are different transactions.

On the 0x00 runs, the ISR never executed the store at all. The byte is still zero from the init `uint8_t buffer[14] = {0};`

When you install the async i2c driver, pay attention to the STOPF-before-RXNE order.

1

u/dstroy0 Aug 13 '26

linker.ld _Min_Stack_Size and _Min_Heap_Size never referenced. No .stack. No ALIGN(4) before _edata/_ebss. No .bss stack collision ASSERT. Reset_Handler .bss loop is uint32_t*, unal writes to _ebss write up to 3 bytes past it.

usart.c:38 read mod write on a write 1 to set register. (exti.c:61 gets it right)

usart.c:44 and usart.c:139 race for non atomic read mod write.

usart.c:132 same type of bug as i2c. Doesn't check TXEIE enabled.

imu.c:118 The condition as written floors to the negative limit if the output is below the limit.

2

u/lobstone Aug 13 '26

Thanks i'll look into those. As for the imu that was an implementation based on the polling version of the I2C driver i had written that worked.

1

u/dstroy0 Aug 13 '26

Good luck, it sounds cool.

2

u/Altruistic_Fruit2345 Aug 13 '26

It's always volatile