Skip to content

I2C DMA timeout does not quiesce the engine #359

Description

@rusty1968

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);
}

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions