From d92fc0dffbfde18f303001e0e37cffe7a9474cdc Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 8 Jun 2026 12:46:41 +0200 Subject: [PATCH] Async SPI future API is unsafe now --- vorago-shared-hal/CHANGELOG.md | 6 +- vorago-shared-hal/src/spi/asynch.rs | 96 ++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 9 deletions(-) diff --git a/vorago-shared-hal/CHANGELOG.md b/vorago-shared-hal/CHANGELOG.md index 97efcbc..af37fbc 100644 --- a/vorago-shared-hal/CHANGELOG.md +++ b/vorago-shared-hal/CHANGELOG.md @@ -8,9 +8,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [unreleased] +### Added + +- Async SPI now exposes methods which return optional futures. + ## [v0.4.0] 2026-05-19 -## Changed +### Changed - Naming improvements for UART register module - Improved UART Async TX module. Only enable TX below threshold interrupts if the FIFO diff --git a/vorago-shared-hal/src/spi/asynch.rs b/vorago-shared-hal/src/spi/asynch.rs index a90a8cb..2ff8eda 100644 --- a/vorago-shared-hal/src/spi/asynch.rs +++ b/vorago-shared-hal/src/spi/asynch.rs @@ -589,7 +589,16 @@ impl SpiAsync { Self(spi) } - fn read(&mut self, words: &mut [u8]) -> Option> { + /// Future which read `words` from the slave. + /// + /// Returns [None] if the provided buffer is empty. + /// + /// # Safety + /// + /// This function stores the raw pointer of the passed data buffer. The user MUST ensure + /// that the slice outlives the data structure. If the passed slice is stack-allocated, + /// the user also MUST ensure that the `Drop` method runs on transfer cancellation. + pub unsafe fn read(&mut self, words: &mut [u8]) -> Option> { if words.is_empty() { return None; } @@ -597,7 +606,16 @@ impl SpiAsync { Some(SpiFuture::new_for_read(&mut self.0, id, words)) } - fn write(&mut self, words: &[u8]) -> Option> { + /// Future which writes `words` to the slave, ignoring all the incoming words. + /// + /// Returns [None] if the provided buffer is empty. + /// + /// # Safety + /// + /// This function stores the raw pointer of the passed data. The user MUST ensure + /// that the slice outlives the data structure. If the passed slice is stack-allocated, + /// the user also MUST ensure that the `Drop` method runs on transfer cancellation. + pub unsafe fn write(&mut self, words: &[u8]) -> Option> { if words.is_empty() { return None; } @@ -605,7 +623,22 @@ impl SpiAsync { Some(SpiFuture::new_for_write(&mut self.0, id, words)) } - fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Option> { + /// Future which writes and reads simultaneously. `write` is written to the slave on MOSI and + /// words received on MISO are stored in `read`. + /// + /// It is allowed for `read` and `write` to have different lengths, even zero length. + /// The transfer runs for `max(read.len(), write.len())` words. If `read` is shorter, + /// incoming words after `read` has been filled will be discarded. If `write` is shorter, + /// the value of words sent in MOSI after all `write` has been sent is 0. + /// + /// Returns [None] if either of the provided buffers is empty. + /// + /// # Safety + /// + /// This function stores the raw pointer of the passed slices. The user MUST ensure + /// that the slice outlives the data structure. If the passed slice is stack-allocated, + /// the user also MUST ensure that the `Drop` method runs on transfer cancellation. + pub unsafe fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Option> { if read.is_empty() || write.is_empty() { return None; } @@ -613,7 +646,18 @@ impl SpiAsync { Some(SpiFuture::new_for_transfer(&mut self.0, id, read, write)) } - fn transfer_in_place(&mut self, words: &mut [u8]) -> Option> { + /// Future which writes and reads simultaneously. The contents of `words` are + /// written to the slave, and the received words are stored into the same + /// `words` buffer, overwriting it. + /// + /// Returns [None] if the provided buffer is empty. + /// + /// # Safety + /// + /// This function stores the raw pointer of the passed slice. The user MUST ensure + /// that the slice outlives the data structure. If the passed slice is stack-allocated, + /// the user also MUST ensure that the `Drop` method runs on transfer cancellation. + pub unsafe fn transfer_in_place(&mut self, words: &mut [u8]) -> Option> { if words.is_empty() { return None; } @@ -627,32 +671,68 @@ impl embedded_hal_async::spi::ErrorType for SpiAsync { } impl embedded_hal_async::spi::SpiBus for SpiAsync { + /// Read `words` from the slave. + // + /// # Safety + /// + /// This function stores the raw pointer of the passed data buffer. The user MUST ensure + /// that the slice outlives the data structure. If the passed slice is stack-allocated, + /// the user also MUST ensure that the `Drop` method runs on transfer cancellation. async fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { if words.is_empty() { return Ok(()); } - self.read(words).unwrap().await + unsafe { self.read(words).unwrap().await } } + /// Write `words` to the slave, ignoring all the incoming words. + /// + /// # Safety + /// + /// This function stores the raw pointer of the passed data. The user MUST ensure + /// that the slice outlives the data structure. If the passed slice is stack-allocated, + /// the user also MUST ensure that the `Drop` method runs on transfer cancellation. async fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> { if words.is_empty() { return Ok(()); } - self.write(words).unwrap().await + unsafe { self.write(words).unwrap().await } } + /// Write and read simultaneously. `write` is written to the slave on MOSI and + /// words received on MISO are stored in `read`. + /// + /// It is allowed for `read` and `write` to have different lengths, even zero length. + /// The transfer runs for `max(read.len(), write.len())` words. If `read` is shorter, + /// incoming words after `read` has been filled will be discarded. If `write` is shorter, + /// the value of words sent in MOSI after all `write` has been sent is 0. + /// + /// # Safety + /// + /// This function stores the raw pointer of the passed slices. The user MUST ensure + /// that the slice outlives the data structure. If the passed slice is stack-allocated, + /// the user also MUST ensure that the `Drop` method runs on transfer cancellation. async fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> { if read.is_empty() && write.is_empty() { return Ok(()); } - self.transfer(read, write).unwrap().await + unsafe { self.transfer(read, write).unwrap().await } } + /// Write and read simultaneously. The contents of `words` are + /// written to the slave, and the received words are stored into the same + /// `words` buffer, overwriting it. + /// + /// # Safety + /// + /// This function stores the raw pointer of the passed slice. The user MUST ensure + /// that the slice outlives the data structure. If the passed slice is stack-allocated, + /// the user also MUST ensure that the `Drop` method runs on transfer cancellation. async fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { if words.is_empty() { return Ok(()); } - self.transfer_in_place(words).unwrap().await + unsafe { self.transfer_in_place(words).unwrap().await } } async fn flush(&mut self) -> Result<(), Self::Error> {