wait_completion is a spin-count loop that decrements a counter on every
iteration, calling handle_interrupt() each time. When the counter reaches
zero it returns Err(I2cError::Timeout) unconditionally:
// controller.rs (simplified)
while timeout > 0 && !self.completion {
self.handle_interrupt()?;
timeout = timeout.saturating_sub(1);
(self.yield_ns)(100_000);
}
if self.completion { Ok(()) } else { Err(I2cError::Timeout) }
Neither write_dma_mode nor read_dma_mode writes an abort or soft-reset
register after this error returns. The I2C DMA engine continues executing its
outstanding bus transaction, writing received bytes into the .ram_nc static
buffer whose physical address is still live in i2cm34 (RX DMA address
register).
On the next HAL call, make_driver() calls as_deref_mut() on the same
&'static mut [u8] and immediately begins filling it with new TX data (for a
write) or reading from it (for a read) while the hardware from the previous
timed-out transaction may still be writing to it. This violates the
single-owner invariant in hardware: the CPU and the DMA engine concurrently
access the same physical words. On this SRAM-execution target the engine is an
AHB bus master and can reach the code region without MPU interception —
corrupted transfer data that overlaps a code page becomes a code-injection
primitive.
There is also a secondary issue: timeout = timeout.saturating_sub(1) counts
loop iterations, not real elapsed time. DEFAULT_TIMEOUT_US is treated as a
loop-count, not a microsecond budget, so the actual timeout wall-clock duration
is indeterminate and depends on compiler optimisation of the loop body.
Fix
After wait_completion returns Err, write the controller abort bit and
poll until the engine's idle flag is set before returning the error to the
caller. Example structure for both write_dma_mode and read_dma_mode:
if let Err(e) = self.wait_completion(constants::DEFAULT_TIMEOUT_US) {
// Abort the in-flight DMA transaction and wait for quiescence.
self.regs()
.i2cm00() // consult datasheet for soft-reset / abort bit
.modify(|_, w| w.master_abort().set_bit());
// Spin until the controller reports idle (add a secondary timeout).
let mut abort_timeout = constants::ABORT_TIMEOUT_US;
while abort_timeout > 0 && self.regs().i2cm14().read().bits()
& constants::AST_I2CM_TX_CMD != 0
{
abort_timeout = abort_timeout.saturating_sub(1);
}
return Err(e);
}
wait_completionis a spin-count loop that decrements a counter on everyiteration, calling
handle_interrupt()each time. When the counter reacheszero it returns
Err(I2cError::Timeout)unconditionally:Neither
write_dma_modenorread_dma_modewrites an abort or soft-resetregister after this error returns. The I2C DMA engine continues executing its
outstanding bus transaction, writing received bytes into the
.ram_ncstaticbuffer whose physical address is still live in
i2cm34(RX DMA addressregister).
On the next HAL call,
make_driver()callsas_deref_mut()on the same&'static mut [u8]and immediately begins filling it with new TX data (for awrite) or reading from it (for a read) while the hardware from the previous
timed-out transaction may still be writing to it. This violates the
single-owner invariant in hardware: the CPU and the DMA engine concurrently
access the same physical words. On this SRAM-execution target the engine is an
AHB bus master and can reach the code region without MPU interception —
corrupted transfer data that overlaps a code page becomes a code-injection
primitive.
There is also a secondary issue:
timeout = timeout.saturating_sub(1)countsloop iterations, not real elapsed time.
DEFAULT_TIMEOUT_USis treated as aloop-count, not a microsecond budget, so the actual timeout wall-clock duration
is indeterminate and depends on compiler optimisation of the loop body.
Fix
After
wait_completionreturnsErr, write the controller abort bit andpoll until the engine's idle flag is set before returning the error to the
caller. Example structure for both
write_dma_modeandread_dma_mode: