improve safety
shared-hal-ci / Check build (push) Has been cancelled
shared-hal-ci / Check formatting (push) Has been cancelled
shared-hal-ci / Check Documentation Build (push) Has been cancelled
shared-hal-ci / Clippy (push) Has been cancelled
va108xx-ci / Run Tests (push) Has been cancelled
va108xx-ci / Check formatting (push) Has been cancelled
va108xx-ci / Check Documentation Build (push) Has been cancelled
va108xx-ci / Clippy (push) Has been cancelled
va416xx-ci / Run Tests (push) Has been cancelled
va416xx-ci / Check formatting (push) Has been cancelled
va416xx-ci / Check Documentation Build (push) Has been cancelled
va416xx-ci / Clippy (push) Has been cancelled
shared-hal-ci / Check build (pull_request) Has been cancelled
shared-hal-ci / Check formatting (pull_request) Has been cancelled
shared-hal-ci / Check Documentation Build (pull_request) Has been cancelled
shared-hal-ci / Clippy (pull_request) Has been cancelled
va108xx-ci / Run Tests (pull_request) Has been cancelled
va108xx-ci / Check formatting (pull_request) Has been cancelled
va108xx-ci / Check Documentation Build (pull_request) Has been cancelled
va108xx-ci / Clippy (pull_request) Has been cancelled
va416xx-ci / Run Tests (pull_request) Has been cancelled
va416xx-ci / Check formatting (pull_request) Has been cancelled
va416xx-ci / Check Documentation Build (pull_request) Has been cancelled
va416xx-ci / Clippy (pull_request) Has been cancelled
va108xx-ci / Check build (push) Has been cancelled
va416xx-ci / Check build (push) Has been cancelled
va108xx-ci / Check build (pull_request) Has been cancelled
va416xx-ci / Check build (pull_request) Has been cancelled

This commit is contained in:
Robin Mueller
2026-06-08 20:30:26 +02:00
parent 272a0dd3fc
commit 4e16344c70
7 changed files with 89 additions and 124 deletions
@@ -62,7 +62,8 @@ async fn main(_spawner: Spawner) {
InterruptConfig::new(pac::Interrupt::OC2, true, true),
);
let (tx, _rx) = uarta.split();
let mut async_tx = TxAsync::new(tx);
// Safety: We do not cancel futures.
let mut async_tx = unsafe { TxAsync::new(tx) };
let mut ticker = Ticker::every(Duration::from_secs(1));
let mut idx = 0;
loop {
@@ -71,12 +72,10 @@ async fn main(_spawner: Spawner) {
led1.toggle();
led2.toggle();
// Safety: We are sending static lifetime slices, and not cancelling the futures.
unsafe {
async_tx
.write_all(STR_LIST[idx].as_bytes())
.await
.expect("writing failed");
}
async_tx
.write_all(STR_LIST[idx].as_bytes())
.await
.expect("writing failed");
idx += 1;
if idx == STR_LIST.len() {
idx = 0;
+3 -2
View File
@@ -101,7 +101,8 @@ mod app {
tc_handler::spawn().unwrap();
tm_tx_handler::spawn().unwrap();
let tx_async = TxAsync::new(tx);
// Safety: We do not cancel futures.
let tx_async = unsafe { TxAsync::new(tx) };
static TC_PIPE: static_cell::ConstStaticCell<
embassy_sync::pipe::Pipe<CriticalSectionRawMutex, TC_PIPE_SIZE>,
@@ -285,7 +286,7 @@ mod app {
loop {
let read_len = cx.local.tm_rx.read(&mut buf).await;
// Safety: The buffer outlives the UART TX structure.
if let Err(e) = unsafe { cx.local.uart_tx.write_all(&buf[0..read_len]).await } {
if let Err(e) = cx.local.uart_tx.write_all(&buf[0..read_len]).await {
defmt::warn!("UART TX overrun error: {}", e);
}
}
@@ -68,19 +68,18 @@ async fn main(_spawner: Spawner) {
let uart_config = uart::Config::new_with_clock_config(clock_config);
let uarta = uart::Uart::new_for_uart0(dp.uart0, pinsg.pg0, pinsg.pg1, uart_config);
let (tx, _rx) = uarta.split();
let mut async_tx = TxAsync::new(tx);
// Safety: We do not cancel futures.
let mut async_tx = unsafe { TxAsync::new(tx) };
let mut ticker = Ticker::every(Duration::from_secs(1));
let mut idx = 0;
loop {
defmt::println!("Current time: {}", Instant::now().as_secs());
led.toggle();
// Safety: We are sending static lifetime slices, and not cancelling the futures.
unsafe {
async_tx
.write_all(STR_LIST[idx].as_bytes())
.await
.expect("writing failed");
}
async_tx
.write_all(STR_LIST[idx].as_bytes())
.await
.expect("writing failed");
idx += 1;
if idx == STR_LIST.len() {
idx = 0;
+5 -1
View File
@@ -10,11 +10,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Changed
- Async TX UART functions are explicitely marked `unsafe`.
- Async TX UART and Async SPI driver constructor is explicitely marked `unsafe`.
- Async TX UART `write` now returns a `TxFuture`
- Empty async TX write resolves to `Poll::Ready(0)` immediately.
- Async SPI API now always returns futures instead of optional futures.
### Fixed
- Asynch drivers now borrow the buffers properly for the lifetime of the future.
## [v0.4.0] 2026-05-19
### Changed
+45 -78
View File
@@ -319,21 +319,27 @@ impl TransferContext {
}
}
pub struct SpiFuture<'spi> {
pub struct SpiFuture<'spi, 'read, 'write> {
bank: super::Bank,
spi: &'spi mut super::Spi<u8>,
empty_buffer: bool,
finished_regularly: core::cell::Cell<bool>,
phantom_read: core::marker::PhantomData<(&'read (), &'write ())>,
}
impl<'spi> SpiFuture<'spi> {
fn new_for_read(spi: &'spi mut super::Spi<u8>, bank: super::Bank, words: &mut [u8]) -> Self {
impl<'spi, 'read, 'write> SpiFuture<'spi, 'read, 'write> {
fn new_for_read(
spi: &'spi mut super::Spi<u8>,
bank: super::Bank,
words: &'read mut [u8],
) -> Self {
if words.is_empty() {
return Self {
bank,
spi,
empty_buffer: true,
finished_regularly: core::cell::Cell::new(false),
phantom_read: core::marker::PhantomData,
};
}
Self::generic_init_transfer(spi, bank);
@@ -367,16 +373,22 @@ impl<'spi> SpiFuture<'spi> {
spi,
empty_buffer: false,
finished_regularly: core::cell::Cell::new(false),
phantom_read: core::marker::PhantomData,
}
}
fn new_for_write(spi: &'spi mut super::Spi<u8>, bank: super::Bank, words: &[u8]) -> Self {
fn new_for_write(
spi: &'spi mut super::Spi<u8>,
bank: super::Bank,
words: &'write [u8],
) -> Self {
if words.is_empty() {
return Self {
bank,
spi,
empty_buffer: true,
finished_regularly: core::cell::Cell::new(false),
phantom_read: core::marker::PhantomData,
};
}
let index = bank as usize;
@@ -402,14 +414,15 @@ impl<'spi> SpiFuture<'spi> {
spi,
empty_buffer: false,
finished_regularly: core::cell::Cell::new(false),
phantom_read: core::marker::PhantomData,
}
}
fn new_for_transfer(
spi: &'spi mut super::Spi<u8>,
bank: super::Bank,
read: &mut [u8],
write: &[u8],
read: &'read mut [u8],
write: &'write [u8],
) -> Self {
if read.is_empty() || write.is_empty() {
return Self {
@@ -417,6 +430,7 @@ impl<'spi> SpiFuture<'spi> {
spi,
empty_buffer: true,
finished_regularly: core::cell::Cell::new(false),
phantom_read: core::marker::PhantomData,
};
}
let index = bank as usize;
@@ -453,13 +467,14 @@ impl<'spi> SpiFuture<'spi> {
spi,
empty_buffer: false,
finished_regularly: core::cell::Cell::new(false),
phantom_read: core::marker::PhantomData,
}
}
fn new_for_transfer_in_place(
spi: &'spi mut super::Spi<u8>,
bank: super::Bank,
words: &mut [u8],
words: &'read mut [u8],
) -> Self {
if words.is_empty() {
return Self {
@@ -467,6 +482,7 @@ impl<'spi> SpiFuture<'spi> {
spi,
empty_buffer: true,
finished_regularly: core::cell::Cell::new(false),
phantom_read: core::marker::PhantomData,
};
}
let write_idx = Self::generic_init_transfer_write_transfer_in_place(spi, bank, words);
@@ -491,6 +507,7 @@ impl<'spi> SpiFuture<'spi> {
spi,
empty_buffer: false,
finished_regularly: core::cell::Cell::new(false),
phantom_read: core::marker::PhantomData,
}
}
@@ -540,7 +557,7 @@ impl<'spi> SpiFuture<'spi> {
}
}
impl<'spi> Future for SpiFuture<'spi> {
impl<'spi> Future for SpiFuture<'spi, '_, '_> {
type Output = Result<(), RxOverrunError>;
fn poll(
@@ -570,7 +587,7 @@ impl<'spi> Future for SpiFuture<'spi> {
}
}
impl<'spi> Drop for SpiFuture<'spi> {
impl<'spi> Drop for SpiFuture<'spi, '_, '_> {
fn drop(&mut self) {
if !self.finished_regularly.get() && !self.empty_buffer {
// It might be sufficient to disable and enable the SPI.. But this definitely
@@ -591,7 +608,13 @@ impl<'spi> Drop for SpiFuture<'spi> {
pub struct SpiAsync(pub super::Spi<u8>);
impl SpiAsync {
pub fn new(
/// Construct an asynchronous SPI driver for the given SPI peripheral.
///
/// # Safety
///
/// The user MUST ensure that the `Drop` method of all futures generated with this driver
/// is called on transfer cancellation. By default, this does not require any special handling.
pub unsafe fn new(
mut spi: super::Spi<u8>,
#[cfg(feature = "vor1x")] opt_irq_cfg: Option<crate::InterruptConfig>,
) -> Self {
@@ -620,13 +643,7 @@ impl SpiAsync {
/// 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]) -> SpiFuture<'_> {
pub fn read<'read>(&mut self, words: &'read mut [u8]) -> SpiFuture<'_, 'read, '_> {
let id = self.0.id;
SpiFuture::new_for_read(&mut self.0, id, words)
}
@@ -634,13 +651,7 @@ impl SpiAsync {
/// 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]) -> SpiFuture<'_> {
pub fn write<'write>(&mut self, words: &'write [u8]) -> SpiFuture<'_, '_, 'write> {
let id = self.0.id;
SpiFuture::new_for_write(&mut self.0, id, words)
}
@@ -654,13 +665,11 @@ impl SpiAsync {
/// 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]) -> SpiFuture<'_> {
pub fn transfer<'read, 'write>(
&mut self,
read: &'read mut [u8],
write: &'write [u8],
) -> SpiFuture<'_, 'read, 'write> {
let id = self.0.id;
SpiFuture::new_for_transfer(&mut self.0, id, read, write)
}
@@ -670,13 +679,7 @@ impl SpiAsync {
/// `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]) -> SpiFuture<'_> {
pub fn transfer_in_place<'read>(&mut self, words: &'read mut [u8]) -> SpiFuture<'_, 'read, '_> {
let id = self.0.id;
SpiFuture::new_for_transfer_in_place(&mut self.0, id, words)
}
@@ -687,68 +690,32 @@ 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(());
}
unsafe { self.read(words).await }
self.read(words).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(());
}
unsafe { self.write(words).await }
self.write(words).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(());
}
unsafe { self.transfer(read, write).await }
self.transfer(read, write).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(());
}
unsafe { self.transfer_in_place(words).await }
self.transfer_in_place(words).await
}
async fn flush(&mut self) -> Result<(), Self::Error> {
+7 -2
View File
@@ -1249,8 +1249,13 @@ impl Tx {
self.regs.write_data(Data::new_with_raw_value(data));
}
pub fn into_async(self) -> TxAsync {
TxAsync::new(self)
/// Create an asynchronous UART driver.
///
/// # Safety
///
/// See [TxAsync::new] for details.
pub unsafe fn into_async(self) -> TxAsync {
unsafe { TxAsync::new(self) }
}
}
+17 -27
View File
@@ -113,23 +113,26 @@ impl TxContext {
}
#[derive(Debug)]
pub struct TxFuture {
pub struct TxFuture<'buf> {
id: Bank,
empty_buffer: bool,
// Phantom used to borrow the buffer for the lifetime of the future.
phantom: core::marker::PhantomData<&'buf ()>,
}
impl TxFuture {
impl<'buf> TxFuture<'buf> {
/// # Safety
///
/// This function stores the raw pointer of the passed data slice. The user MUST ensure
/// that the slice outlives the data structure.
pub unsafe fn new(tx: &mut Tx, data: &[u8]) -> Self {
pub unsafe fn new(tx: &mut Tx, data: &'buf [u8]) -> Self {
if data.is_empty() {
// We can just return a dummy future which is immediately ready, no need to set up
// interrupts etc.
return Self {
id: tx.id,
empty_buffer: true,
phantom: core::marker::PhantomData,
};
}
TX_DONE[tx.id as usize].store(false, core::sync::atomic::Ordering::Relaxed);
@@ -160,11 +163,12 @@ impl TxFuture {
Self {
id: tx.id,
empty_buffer: false,
phantom: core::marker::PhantomData,
}
}
}
impl Future for TxFuture {
impl Future for TxFuture<'_> {
type Output = Result<usize, TxOverrunError>;
fn poll(
@@ -189,7 +193,7 @@ impl Future for TxFuture {
///
/// It is imperative that this `Drop` method is executed to avoid undefined behaviour on
/// transfer. Do *NOT* use `core::mem::forget` on the `TxFuture`.
impl Drop for TxFuture {
impl Drop for TxFuture<'_> {
fn drop(&mut self) {
let mut reg_block = unsafe { self.id.steal_regs() };
if !TX_DONE[self.id as usize].load(core::sync::atomic::Ordering::Relaxed)
@@ -205,7 +209,11 @@ impl Drop for TxFuture {
pub struct TxAsync(Tx);
impl TxAsync {
pub fn new(tx: Tx) -> Self {
/// # Safety
///
/// The user MUST ensure that the `Drop` method of all futures generated with this driver
/// is called on transfer cancellation. By default, this does not require any special handling.
pub unsafe fn new(tx: Tx) -> Self {
Self(tx)
}
@@ -218,12 +226,7 @@ impl TxAsync {
///
/// This implementation is not side effect free, and a started future might have already
/// written part of the passed buffer.
///
/// # Safety
///
/// This function stores the raw pointer of the passed data slice. The user MUST ensure
/// that the slice outlives the data structure.
pub unsafe fn write(&mut self, buf: &[u8]) -> TxFuture {
pub fn write<'buf>(&mut self, buf: &'buf [u8]) -> TxFuture<'buf> {
unsafe { TxFuture::new(&mut self.0, buf) }
}
@@ -234,12 +237,7 @@ impl TxAsync {
///
/// This function is not side-effect-free on cancel (AKA "cancel-safe"), i.e. if you cancel (drop) a returned
/// future that hasn't completed yet, some bytes might have already been written.
///
/// # Safety
///
/// This function stores the raw pointer of the passed data slice. The user MUST ensure
/// that the slice outlives the data structure.
pub async unsafe fn write_all(&mut self, buf: &[u8]) -> Result<(), TxOverrunError> {
pub async fn write_all(&mut self, buf: &[u8]) -> Result<(), TxOverrunError> {
let fut = <Self as embedded_io_async::Write>::write_all(self, buf);
fut.await
}
@@ -274,16 +272,8 @@ impl Write for TxAsync {
///
/// This implementation is not side effect free, and a started future might have already
/// written part of the passed buffer.
///
/// # Safety
///
/// This function is not `unsafe` due to the trait definition.
/// This function stores the raw pointer of the passed data slice. The user MUST ensure
/// that the slice outlives the data structure.
async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
// Safety: We documented the safety contract. Not much else we can do here as we are bound
// by the trait definition.
unsafe { self.write(buf).await }
self.write(buf).await
}
async fn flush(&mut self) -> Result<(), Self::Error> {