-
Notifications
You must be signed in to change notification settings - Fork 242
Description
Background
I was revisiting the original use case that @pftbest once told me about -- a lock free single
producer single consumer ring buffer -- and that led me to submit #37 and noticed that RTFM still
can't fulfill that use case in 100% safe code. (I hope I'm wrong)
Below is a program that explains the use case:
#![deny(warnings)]
#![feature(proc_macro)]
#![no_std]
extern crate blue_pill;
extern crate cortex_m_rtfm as rtfm;
// https://github.com/japaric/spscrb
extern crate spscrb;
use blue_pill::stm32f103xx::{self, Interrupt};
use rtfm::{app, Threshold};
use spscrb::{Consumer, Producer, RingBuffer};
app! {
device: stm32f103xx,
resources: {
static PRODUCER: Producer<u32, [u32; 32]>;
static CONSUMER: Consumer<u32, [u32; 32]>;
},
tasks: {
EXTI0: {
path: exti0,
resources: [PRODUCER],
priority: 1,
},
EXTI1: {
path: exti1,
resources: [CONSUMER],
priority: 2,
},
},
}
fn init(_p: init::Peripherals) -> init::LateResourceValues {
let rb: &'static mut RingBuffer<u32, [u32; 32]>;
// this part needs to be hidden from the user, or rather the user shouldn't need to do this
rb = {
static mut RB: RingBuffer<u32, [u32; 32]> = RingBuffer::new();
unsafe { &mut RB }
};
// NOTE this method *consumes* a `&'static mut` reference
let (p, c) = rb.spsc();
// let (p2, c2) = rb.spsc();
// ^ this would be an error
init::LateResourceValues { PRODUCER: p, CONSUMER: c }
}
fn idle() -> ! {
rtfm::set_pending(Interrupt::EXTI0);
rtfm::set_pending(Interrupt::EXTI1);
loop {
rtfm::wfi();
}
}
fn exti0(_t: &mut Threshold, r: EXTI0::Resources) {
// lock-free operation
r.PRODUCER.enqueue(0xdead_beef).unwrap();
}
fn exti1(_t: &mut Threshold, r: EXTI1::Resources) {
// lock-free operation
r.CONSUMER.dequeue().unwrap();
}Basically you have a statically allocated ring buffer which is hidden from the user. To enqueue and
dequeue elements into the buffer you have to use the "producer" and "consumer" end points,
respectively. These end points can be used from different execution contexts, which can run at
different priorities, in a lock free manner. This is memory safe, without locking, because the
producer and the consumer, each, own different cursors into the ring buffer and because the cursors
are word sized (they can be atomically read) -- check the implementation for details.
To construct the producer and consumer there's this spsc method with signature fn(&'static mut RingBuffer<_, _>) -> ... This signature ensures that (a) the ring buffer is statically allocated,
and thus it will never be destroyed, and (b) that once you have called this method the reference to
the ring buffer becomes invalidated (required to avoid mutable aliasing).
Thanks to #43 (thanks @jonas-schievink!) we can defer initialization of resources and initialize the
producer and consumer end points in init (see example above). However, one problem remains:
there's no way to safely get / create a &'static mut RingBuffer<_, _> in init. The only place
where &'static mut - references are available right now is in idle but that's too late for
initialization of resources (without wrapping them in Option).
Here's my proposal for solving this:
Proposal
Add a roots field to app!. This field contains a list of static variables with initial values.
The syntax of its contents is similar to the contents of resources.
app! {
roots: {
static RB: RingBuffer<u32, [u32; 32]> = RingBuffer::new();
},
}The user will get a list of &'static mut- references to these roots in init:
// auto-generated: struct Roots { RB: &'static mut RingBuffer<u32, [u32; 32]>, }
fn init(roots: init::Roots, ..) {
let (p, c) = roots.RB.spsc();
// ..
}Implementation
This can be implemented by creating a static mut variable for each root.
// auto-generated
fn main() {
interrupt::free(|_| {
static mut RB: RingBuffer<u32, [u32; 32]> = RingBuffer::new();
init(init::Roots { RB: &mut RB }, ..);
});
}Alternatively, the roots could be allocated in the lowest / first stack frame (hence "rooting" in
the proposal name), which is known to never be deallocated: (I haven't tested if this works)
// auto-generated
fn main() {
interrupt::free(|_| {
let mut RB: RingBuffer<u32, [u32; 32]> = RingBuffer::new();
let roots = init::Roots { RB: &mut *(&mut RB as *mut _) };
mem::forget(RB); // ???
init(roots, ..);
// ..
});
// idle is a divergent function
idle();
}Other use cases
Another use case for having &'static mut- references in init is being able to set up periodic
DMA transfers in init. An example of this use case is setting up a circular DMA transfer that
reads ADC or Serial data:
app! {
roots: {
static BUFFER: [[u16; 64]; 2] = [[0; 64]; 2];
},
resources: {
// cf. japaric/embedded-hal#14 and the blue-pill `dma` module
static BUFFER: dma::CircBuffer<[u16; 64]>;
}
}
fn init(roots: init::Roots, ..) -> init::LateResourceValues {
let buffer = adc1.start(dma1, roots.BUFFER);
init::LateResourceValues {
BUFFER: buffer,
}
}Thoughts? Can these use cases be safely achieved through some other means?