Skip to content

Commit c8ed513

Browse files
committed
Add docs concerning safety considerations when using async+DMA transfers
1 parent 0539540 commit c8ed513

File tree

6 files changed

+319
-14
lines changed

6 files changed

+319
-14
lines changed

hal/src/sercom/i2c.rs

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,9 @@
181181
//! * High-speed mode is not supported.
182182
//! * 4-wire mode is not supported.
183183
//! * 32-bit extension mode is not supported (SAMx5x). If you need to transfer
184-
//! slices, consider using the DMA methods instead . The <span class="stab
184+
//! slices, consider using the DMA methods instead. <span class="stab
185185
//! portability" title="Available on crate feature `dma`
186-
//! only"><code>dma</code></span> Cargo feature must be enabled.
186+
//! only"><code>dma</code></span>
187187
//!
188188
//! # Using I2C with DMA <span class="stab portability" title="Available on crate feature `dma` only"><code>dma</code></span>
189189
//!
@@ -243,6 +243,99 @@
243243
//! `I2cFuture` implements `AsRef<I2c>` and `AsMut<I2c>` so
244244
//! that it can be reconfigured using the regular [`I2c`] methods.
245245
//!
246+
//! ## Considerations when using `async` [`I2c`] with DMA <span class="stab portability" title="Available on crate feature `async` only"><code>async</code></span> <span class="stab portability" title="Available on crate feature `dma` only"><code>dma</code></span>
247+
//!
248+
//! * An [`I2c`] struct must be turned into an [`I2cFuture`] by calling
249+
//! [`I2c::into_future`] before calling `with_dma_channel`. The DMA channel
250+
//! itself must also be configured in async mode by using
251+
//! [`DmaController::into_future`](crate::dmac::DmaController::into_future).
252+
//! If a DMA channel is added to the [`I2c`] struct before it is turned into
253+
//! an [`I2cFuture`], it will not be able to use DMA in async mode.
254+
//!
255+
//! ```
256+
//! // This will work
257+
//! let i2c = i2c.into_future().with_dma_channel(channel);
258+
//!
259+
//! // This won't
260+
//! let i2c = i2c.with_dma_channel(channel).into_future();
261+
//! ```
262+
//!
263+
//! ### Safety considerations
264+
//!
265+
//! In `async` mode, an I2C+DMA transfer does not require `'static` source and
266+
//! destination buffers. This, in theory, makes its use `unsafe`. However it is
267+
//! marked as safe for better ergonomics, and to enable the implementation of
268+
//! the [`embedded_hal_async::i2c::I2c`] trait.
269+
//!
270+
//! This means that, as an user, you **must** ensure that the [`Future`]s
271+
//! returned by the [`embedded_hal_async::i2c::I2c`] methods may never be
272+
//! forgotten through [`forget`] or by wrapping them with a [`ManuallyDrop`].
273+
//!
274+
//! The returned futures implement [`Drop`] and will automatically stop any
275+
//! ongoing transfers; this guarantees that the memory occupied by the
276+
//! now-dropped buffers may not be corrupted by running transfers.
277+
//!
278+
//! This means that using functions like [`futures::select_biased`] to implement
279+
//! timeouts is safe; transfers will be safely cancelled if the timeout expires.
280+
//!
281+
//! This also means that should you [`forget`] this [`Future`] after its
282+
//! first [`poll`] call, the transfer will keep running, ruining the
283+
//! now-reclaimed memory, as well as the rest of your day.
284+
//!
285+
//! * `await`ing is fine: the [`Future`] will run to completion.
286+
//! * Dropping an incomplete transfer is also fine. Dropping can happen, for
287+
//! example, if the transfer doesn't complete before a timeout expires.
288+
//! * Dropping an incomplete transfer *without running its destructor* is
289+
//! **unsound** and will trigger undefined behavior.
290+
//!
291+
//! ```ignore
292+
//! async fn always_ready() {}
293+
//!
294+
//! let mut buffer = [0x00; 10];
295+
//!
296+
//! // This is completely safe
297+
//! i2c.read(&mut buffer).await?;
298+
//!
299+
//! // This is also safe: we launch a transfer, which is then immediately cancelled
300+
//! futures::select_biased! {
301+
//! _ = i2c.read(&mut buffer)?,
302+
//! _ = always_ready(),
303+
//! }
304+
//!
305+
//! // This, while contrived, is also safe.
306+
//! {
307+
//! use core::future::Future;
308+
//!
309+
//! let future = i2c.read(&mut buffer);
310+
//! futures::pin_mut!(future);
311+
//! // Assume ctx is a `core::task::Context` given out by the executor.
312+
//! // The future is polled, therefore starting the transfer
313+
//! future.as_mut().poll(ctx);
314+
//!
315+
//! // Future is dropped here - transfer is cancelled.
316+
//! }
317+
//!
318+
//! // DANGER: This is an example of undefined behavior
319+
//! {
320+
//! use core::future::Future;
321+
//! use core::ops::DerefMut;
322+
//!
323+
//! let future = core::mem::ManuallyDrop::new(i2c.read(&mut buffer));
324+
//! futures::pin_mut!(future);
325+
//! // To actually make this example compile, we would need to wrap the returned
326+
//! // future from `i2c.read()` in a newtype that implements Future, because we
327+
//! // can't actually call as_mut() without being able to name the type we want
328+
//! // to deref to.
329+
//! let future_ref: &mut SomeNewTypeFuture = &mut future.as_mut();
330+
//! future.as_mut().poll(ctx);
331+
//!
332+
//! // Future is NOT dropped here - transfer is not cancelled, resulting un UB.
333+
//! }
334+
//! ```
335+
//!
336+
//! As you can see, unsoundness is relatively hard to come by - however, caution
337+
//! should still be exercised.
338+
//!
246339
//! [`enable`]: Config::enable
247340
//! [`disable`]: I2c::disable
248341
//! [`reconfigure`]: I2c::reconfigure
@@ -262,6 +355,10 @@
262355
//! [`i2c::Read`]: embedded_hal::blocking::i2c::Read
263356
//! [`i2c::WriteRead`]: embedded_hal::blocking::i2c::WriteRead
264357
//! [`async_hal`]: crate::async_hal
358+
//! [`forget`]: core::mem::forget
359+
//! [`ManuallyDrop`]: core::mem::ManuallyDrop
360+
//! [`Future`]: core::future::Future
361+
//! [`poll`]: core::future::Future::poll
265362
266363
use atsamd_hal_macros::hal_module;
267364

@@ -471,7 +568,8 @@ where
471568
D: crate::dmac::AnyChannel<Status = S>,
472569
S: crate::dmac::ReadyChannel,
473570
{
474-
/// Reclaim the DMA channel. Any subsequent I2C operations will no longer use DMA.
571+
/// Reclaim the DMA channel. Any subsequent I2C operations will no longer
572+
/// use DMA.
475573
pub fn take_dma_channel(self) -> (I2c<C, crate::typelevel::NoneT>, D) {
476574
(
477575
I2c {

hal/src/sercom/i2c/async_api.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,8 @@ mod dma {
324324
C: AnyConfig,
325325
D: AnyChannel<Status = ReadyFuture>,
326326
{
327-
/// Reclaim the DMA channel. Any subsequent I2C operations will no longer use DMA.
327+
/// Reclaim the DMA channel. Any subsequent I2C operations will no
328+
/// longer use DMA.
328329
pub fn take_dma_channel(self) -> (I2cFuture<C, crate::typelevel::NoneT>, D) {
329330
let (i2c, channel) = self.i2c.take_dma_channel();
330331
(I2cFuture { i2c }, channel)

hal/src/sercom/spi.rs

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,99 @@
322322
//! `SpiFuture` implements `AsRef<Spi>` and `AsMut<Spi>` so
323323
//! that it can be reconfigured using the regular [`Spi`] methods.
324324
//!
325+
//! ## Considerations when using `async` [`Spi`] with DMA <span class="stab portability" title="Available on crate feature `async` only"><code>async</code></span> <span class="stab portability" title="Available on crate feature `dma` only"><code>dma</code></span>
326+
//!
327+
//! * An [`Spi`] struct must be turned into an [`SpiFuture`] by calling
328+
//! [`Spi::into_future`] before calling `with_dma_channel`. The DMA channel
329+
//! itself must also be configured in async mode by using
330+
//! [`DmaController::into_future`](crate::dmac::DmaController::into_future).
331+
//! If a DMA channel is added to the [`Spi`] struct before it is turned into
332+
//! an [`SpiFuture`], it will not be able to use DMA in async mode.
333+
//!
334+
//! ```
335+
//! // This will work
336+
//! let spi = spi.into_future().with_dma_channels(rx_channel, tx_channel);
337+
//!
338+
//! // This won't
339+
//! let spi = spi.with_dma_channels(rx_channel, tx_channel).into_future();
340+
//! ```
341+
//!
342+
//! ### Safety considerations
343+
//!
344+
//! In `async` mode, an SPI+DMA transfer does not require `'static` source and
345+
//! destination buffers. This, in theory, makes its use `unsafe`. However it is
346+
//! marked as safe for better ergonomics, and to enable the implementation of
347+
//! the [`embedded_hal_async::spi::SpiBus`] trait.
348+
//!
349+
//! This means that, as an user, you **must** ensure that the [`Future`]s
350+
//! returned by the [`embedded_hal_async::spi::SpiBus`] methods may never be
351+
//! forgotten through [`forget`] or by wrapping them with a [`ManuallyDrop`].
352+
//!
353+
//! The returned futures implement [`Drop`] and will automatically stop any
354+
//! ongoing transfers; this guarantees that the memory occupied by the
355+
//! now-dropped buffers may not be corrupted by running transfers.
356+
//!
357+
//! This means that using functions like [`futures::select_biased`] to implement
358+
//! timeouts is safe; transfers will be safely cancelled if the timeout expires.
359+
//!
360+
//! This also means that should you [`forget`] this [`Future`] after its
361+
//! first [`poll`] call, the transfer will keep running, ruining the
362+
//! now-reclaimed memory, as well as the rest of your day.
363+
//!
364+
//! * `await`ing is fine: the [`Future`] will run to completion.
365+
//! * Dropping an incomplete transfer is also fine. Dropping can happen, for
366+
//! example, if the transfer doesn't complete before a timeout expires.
367+
//! * Dropping an incomplete transfer *without running its destructor* is
368+
//! **unsound** and will trigger undefined behavior.
369+
//!
370+
//! ```ignore
371+
//! async fn always_ready() {}
372+
//!
373+
//! let mut buffer = [0x00; 10];
374+
//!
375+
//! // This is completely safe
376+
//! spi.read(&mut buffer).await?;
377+
//!
378+
//! // This is also safe: we launch a transfer, which is then immediately cancelled
379+
//! futures::select_biased! {
380+
//! _ = spi.read(&mut buffer)?,
381+
//! _ = always_ready(),
382+
//! }
383+
//!
384+
//! // This, while contrived, is also safe.
385+
//! {
386+
//! use core::future::Future;
387+
//!
388+
//! let future = spi.read(&mut buffer);
389+
//! futures::pin_mut!(future);
390+
//! // Assume ctx is a `core::task::Context` given out by the executor.
391+
//! // The future is polled, therefore starting the transfer
392+
//! future.as_mut().poll(ctx);
393+
//!
394+
//! // Future is dropped here - transfer is cancelled.
395+
//! }
396+
//!
397+
//! // DANGER: This is an example of undefined behavior
398+
//! {
399+
//! use core::future::Future;
400+
//! use core::ops::DerefMut;
401+
//!
402+
//! let future = core::mem::ManuallyDrop::new(spi.read(&mut buffer));
403+
//! futures::pin_mut!(future);
404+
//! // To actually make this example compile, we would need to wrap the returned
405+
//! // future from `i2c.read()` in a newtype that implements Future, because we
406+
//! // can't actually call as_mut() without being able to name the type we want
407+
//! // to deref to.
408+
//! let future_ref: &mut SomeNewTypeFuture = &mut future.as_mut();
409+
//! future.as_mut().poll(ctx);
410+
//!
411+
//! // Future is NOT dropped here - transfer is not cancelled, resulting un UB.
412+
//! }
413+
//! ```
414+
//!
415+
//! As you can see, unsoundness is relatively hard to come by - however, caution
416+
//! should still be exercised.
417+
//!
325418
//! [`enable`]: Config::enable
326419
//! [`gpio`]: crate::gpio
327420
//! [`Pin`]: crate::gpio::pin::Pin
@@ -330,6 +423,10 @@
330423
//! [`embedded_hal::spi::SpiBus`]: crate::ehal::spi::SpiBus
331424
//! [`embedded_hal::spi::SpiDevice`]: crate::ehal::spi::SpiDevice
332425
//! [`async_hal`]: crate::async_hal
426+
//! [`forget`]: core::mem::forget
427+
//! [`ManuallyDrop`]: core::mem::ManuallyDrop
428+
//! [`Future`]: core::future::Future
429+
//! [`poll`]: core::future::Future::poll
333430
334431
use core::marker::PhantomData;
335432

@@ -1430,7 +1527,8 @@ where
14301527
TxDma: crate::dmac::AnyChannel<Status = S>,
14311528
S: crate::dmac::ReadyChannel,
14321529
{
1433-
/// Reclaim the DMA channels. Any subsequent SPI transaction will not use DMA.
1530+
/// Reclaim the DMA channels. Any subsequent SPI transaction will not use
1531+
/// DMA.
14341532
pub fn take_dma_channels(self) -> (Spi<C, D, NoneT, NoneT>, RxDma, TxDma) {
14351533
(
14361534
Spi {
@@ -1500,7 +1598,8 @@ where
15001598
R: crate::dmac::AnyChannel<Status = S>,
15011599
S: crate::dmac::ReadyChannel,
15021600
{
1503-
/// Reclaim the Rx DMA channel. Any subsequent SPI transaction will not use DMA.
1601+
/// Reclaim the Rx DMA channel. Any subsequent SPI transaction will not use
1602+
/// DMA.
15041603
#[cfg(feature = "dma")]
15051604
pub fn take_rx_channel(self) -> (Spi<C, D, NoneT, T>, R) {
15061605
(
@@ -1546,7 +1645,8 @@ where
15461645
T: crate::dmac::AnyChannel<Status = S>,
15471646
S: crate::dmac::ReadyChannel,
15481647
{
1549-
/// Reclaim the DMA channel. Any subsequent SPI transaction will not use DMA.
1648+
/// Reclaim the DMA channel. Any subsequent SPI transaction will not use
1649+
/// DMA.
15501650
pub fn take_tx_channel(self) -> (Spi<C, D, R, NoneT>, T) {
15511651
(
15521652
Spi {

hal/src/sercom/spi/async_api.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,8 @@ mod dma {
404404
RxDma: AnyChannel<Status = ReadyFuture>,
405405
TxDma: AnyChannel<Status = ReadyFuture>,
406406
{
407-
/// Reclaim the DMA channels. Any subsequent SPI transaction will not use DMA.
407+
/// Reclaim the DMA channels. Any subsequent SPI transaction will not
408+
/// use DMA.
408409
pub fn take_dma_channels(self) -> (SpiFuture<C, D, NoneT, NoneT>, RxDma, TxDma) {
409410
let (spi, rx, tx) = self.spi.take_dma_channels();
410411
(SpiFuture { spi }, rx, tx)
@@ -417,7 +418,8 @@ mod dma {
417418
D: Receive,
418419
R: AnyChannel<Status = ReadyFuture>,
419420
{
420-
/// Reclaim the Rx DMA channel. Any subsequent SPI transaction will not use DMA.
421+
/// Reclaim the Rx DMA channel. Any subsequent SPI transaction will not
422+
/// use DMA.
421423
pub fn take_rx_channel(self) -> (SpiFuture<C, D, NoneT, T>, R) {
422424
let (spi, channel) = self.spi.take_rx_channel();
423425
(SpiFuture { spi }, channel)
@@ -430,7 +432,8 @@ mod dma {
430432
D: Capability,
431433
T: AnyChannel<Status = ReadyFuture>,
432434
{
433-
/// Reclaim the DMA channel. Any subsequent SPI transaction will not use DMA.
435+
/// Reclaim the DMA channel. Any subsequent SPI transaction will not use
436+
/// DMA.
434437
pub fn take_tx_channel(self) -> (SpiFuture<C, D, R, NoneT>, T) {
435438
let (spi, channel) = self.spi.take_tx_channel();
436439
(SpiFuture { spi }, channel)

0 commit comments

Comments
 (0)