## Design goals
- User doesn't need to know anything about htif/tcif flags
- Runs user code from the interrupt at top priority, doesn't require executor overhead or an additional interrupt executor
- Fully static, no fnptrs
- No cost if you don't use it
- No unsafe user code required
- Overruns are always detectable, and can be handled without panic
- Circular writes (i.e. DAC) should work too with a few changes
## Downsides
- It is possible to run user code at dma irq priority. Does this break any assumptions?
- UB if the future is mem::forgotten (same as any other dma future)
## Example user code
```rust
// CaptureBuffer is part of the business logic of my oscilloscope app.
// It is a circular buffer that looks for a rising edge
struct MyIsr {
capture_buffer: CaptureBuffer,
}
// `run` is called inside the dma interrupt
impl Isr for MyIsr {
type Word = u16;
fn run(&mut self, buffer: Result<DmaBuffer<Self::Word>, OverrunError>) -> ControlFlow<()> {
let buffer = buffer.unwrap();
// this isn't required, but it might lead to better codegen
assert_eq!(buffer.len(), DMA_BUFFER_SIZE / 2);
for sample in buffer {
let sample: u16 = sample.load();
// returns `Break` when the capture buffer is ready to be displayed
self.capture_buffer.write(sample)?;
// TODO: we aren't calling `finish` when we break
// fortunatly, overruns are still checked automatically when the DmaBuffer is dropped
}
// this does the same thing as dropping the DmaBuffer
buffer.finish().unwrap();
ControlFlow::Continue(())
}
}
// this declares the interrupt handler, as well as a static `IsrState<MyIsr>`
bind_custom_interrupt!(MyIsr, DMA1_CH1);
let capture_buffer = CaptureBuffer::new();
let mut my_isr = MyIsr{ capture_buffer };
let mut dma_buffer = [0; DMA_BUFFER_SIZE];
// this checks at compile time to ensure that we registered MyIsr for DMA1_CH1
adc1.circ_dma_read(&mut p.PA0, &mut my_isr, &mut dma_buffer &mut p.DMA1_CH1).await;
display_buffer(&my_isr.capture_buffer);
```
## Possible changes
- Should `run` return a `Poll`? What if the isr was a future? The waker could pend the interrupt
```rust
// This function runs in a dma executor at dma interrupt priority.
// A dma executor is special because it can only run one task, which is known at compile time.
// Each dma channel can have it's own dma executor with a different task.
// This task can be joined from a different executor, and it can return a value.
#[dma_task(DMA1_CH1)]
async fn my_isr(capture_buffer: &mut CaptureBuffer, cancel_token: &CancelToken) -> Result<(), MyError> {
// This part is a bit sketchy, it needs to use the waker to look up the buffer address and size.
// This function would panic if it isn't called from the dma executor.
let buffer_provider = buffer_provider().await;
// This could be a try block.
async fn handle_capture(capture_buffer: &mut CaptureBuffer, buffer_provider: BufferProvider) -> Result<(), MyError> {
loop {
let sample_buffer = buffer_provider.next().await.map_err(|_| MyError::Overrun)?;
let samples = sample_buffer.iter().map(|sample| sample.load()); // impl Iterator<Item = u16>
let is_done = capture_buffer.process_samples(samples);
// If you forget to call finish, the buffer will still
// check for overflow when it is dropped, and panic if there is.
sample_buffer.finish().map_err(|_| MyError::Overrun)?;
if is_done {
return Ok(())
}
}
}
let capture_future = handle_capture(capture_buffer, buffer_provider);
let cancel_future = cancel_token.map(|_| Err(MyError::Cancelled));
(capture_future, cancel_future).race().await
}
// This runs in a normal executor.
async fn my_task(adc1: &mut ADC1, pa0: &mut PA0, dma1_ch1: &mut DMA1_CH1, display: &mut Display, cancel_token: &CancelToken) -> Result<(), MyError> {
let mut capture_buffer = CaptureBuffer::new();
let mut dma_buffer = [0; DMA_BUFFER_SIZE];
adc1.circ_dma_read(pa0, my_isr(&mut capture_buffer, cancel_token), &mut dma_buffer, dma1_ch1).await?;
display.display_buffer(&capture_buffer);
Ok(())
}
```
- Cancellation
- Cooperative cancelation is supported at htif and tcif
- It is currently not possible to cancel before a half is completed
- Dropping the future currently panics
- How to support different handlers at different times on the same channel?
- Put all handlers in an enum, register that as the handler. Generate that enum with a macro? Can we make that enum invisible to the user, or do they need to construct a variant?
- Store the user state directly in the static state?
- User can still impl Isr for &mut MyIsr
- Return a value from the transfer?
- Use a static array for the buffer
- Pros
- User doesn't need to create the buffer
- We don't need to store the len
- The compiler can do better unrolling (the same thing should be possible with an assert)
- Cons
- The user might want to put the state and the buffer in different memory regions
- Options
- User allocated slice
- User allocated array
- Statically allocated array
|| `&mut [T]` | `&mut [T; N]` | `[T; N]` |
|-|-|-|-|
|User doesn't need to create|❌|❌|✅|
|Don't need to store len|❌|✅|✅|
|Good unrolling|❔*|✅|✅|
|Allows dynamic size|✅|❌|❌|
|Allows buffer in different segment|✅|✅|❌|
|Less indirection|❌|❌|✅|
|Can use use buffer elsewhere|✅|✅|❌|
\* User can do `assert_eq(buffer.len(), BUFFER_SIZE/2)`. Also, if the len is maybeuninit, maybe the compiler can still figure it out at compile time?
## Pseudo implementation
```rust
struct OverrunError;
#[repr(transparent)]
struct BufferWord<Word: DmaWord>(Word::Atomic);
impl<Word: DmaWord> BufferWord<Word> {
fn load(&self) -> Word {
self.0.load(Ordering::Relaxed)
}
}
pub struct DmaBuffer<'a, Word: DmaWord> {
buffer: &'a [BufferWord<Word>]
channel: AnyChannel,
}
impl<Word: DmaWord> Deref for DmaBuffer<Word> {
type Target = [BufferWord<Word>]
fn deref(&self) -> &Self::Target {
self.buffer
}
}
impl<Word: DmaWord> Drop for DmaBuffer<Word> {
fn drop(&mut self) {
self.check_overrun().unwrap();
}
}
impl<Word: DmaWord> DmaBuffer<Word> {
fn check_overrun(&self) -> Result<(), OverrunError> {
let flags = self.channel.get_flags();
if flags.has_htif() || flags.has_tcif() {
Err(OverrunError)
} else {
Ok(())
}
}
pub fn finish(self) -> Result<(), OverrunError> {
let this = ManuallyDrop::new(self);
this.check_overrun()
}
}
// This needs to be pub so that the macro can delcare a static of it.
pub struct IsrState<UserState: Isr> {
user_state: AtomicPtr<UserState>,
buffer: UnsafeCell<*mut UserState::Word>;
len: UnsafeCell<u16>;
waker: AtomicWaker,
}
impl IsrState {
pub const fn new() -> Self {
Self{
user_state: AtomicPtr::new(null_mut()),
buffer: UnsafeCell::new(null_mut()),
len: UnsafeCell::new(0),
waker: AtomicWaker::new(),
}
}
unsafe fn on_isr(&self, ch: Channel) {
let user_state = {
let user_state = self.user_state.load(Ordering::Aquire);
if user_state.is_null() {
return;
}
&*user_state
};
let len = *state.len.get();
let half_len = len / 2;
let offset = match ch.check_and_clear_flags() {
Neither => return;
First => Ok(0);
Second => Ok(half_len);
Both => Err(OverrunError);
};
let buffer = offset.map(|offset| {
let buffer: *mut UserState::Word = *state.buffer.get();
let buffer = buffer.add(offset);
let buffer = slice::from_raw_parts(buffer.cast(), half_len);
DmaBuffer::new(buffer, ch);
});
let cf = user_state.run(buffer);
if cf.is_break() {
ch.disable();
Self::state_ptr().store(ptr::null_mut(), Ordering:: Release);
self.waker.wake();
}
}
}
struct CircTransfer<'b, 'u, UserState: Isr> {
state: &'static IsrState<UserState>,
_phantom: PhantomData<(&'b mut [UserState::Word], &'u mut UserState)>,
}
impl<'b, 'u, UserState: Isr> CircTransfer<'b, 'u, UserState> {
fn new(state: &'static IsrState, channel: AnyChannel, buffer: &'a mut [UserState::Word], user_state: &'a mut UserState) -> Self {
let len = buffer.len();
// make divisible by two, rounding down
let len = (len / 2) * 2;
let buffer = buffer.as_ptr();
*state.buffer.get() = buffer;
*state.len.get() = len;
state.user_state.store(user_state, Ordering::Release);
channel.start_circular_dma(buffer, len);
Self{ state, _phantom: PhantomData }
}
}
impl<'b, 'u, UserState: Isr> Future for CircTransfer<'b, 'u, UserState> {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.state.waker.register(cx.waker())
if self.state.user_state.load(Ordering::Aquire).is_null() {
Poll::Ready(())
} else {
Poll::Pending
}
}
}
impl<'b, 'u, UserState: Isr> Drop for CircTransfer<'b, 'u, UserState> {
fn drop(&mut self) {
todo!();
}
}
```