Open japaric opened 8 years ago
Here one way to handle interrupts that I have been thinking about:
It's basically an event loop.
static EVENTS: Queue<Event> = Queue::new();
enum Event {
AdcConversionDone,
DmaTransferDone,
UartTransferDone,
// ...
}
fn main() {
// Omitted: Initialize peripherals, state machine, etc.
loop {
match EVENTS.dequeue() {
Some(event) => {
match event {
// Use a state machine to decide what to do when `event` happens
}
},
None => {
// No event, sleep until next interrupt happens (AKA `asm!("wfi)`)
sleep();
}
}
}
}
// Interrupt handler
fn dma() {
// Omitted: clear interrupt flag
EVENTS.queue(DmaTransferDone);
}
// Interrupt handler
fn adc() {
// Omitted: clear interrupt flag
EVENTS.queue(AdcConversionDone);
}
Notes:
Queue
is synchronized. Under the hood, Queue
temporarily disables interrupts during the queue and dequeue operations.Feedback on @goandylok's idea
To do that we can spawn() an interruption, move register or a module shown above into it and send data via channel.
I'm not sure what spawning an interrupt means, I suppose you mean registering an interrupt handler (from main
)? I have seen this functionality on the RustyGecko thesis (section 7.2); it looks like this:
fn main() {
let mut adc = adc0();
let buffer = CircularBuffer::new();
let mut ch = buffer.in();
// registers this closure as the interrupt handler of `adc0`s interrupt. This closure
// takes ownership of `ch`.
adc.on_single(move |sample| ch.send(sample));
loop {}
}
Which I like because it's a nice use of Rust's ownership concept. A few concerns:
main
function, otherwise the captures may become invalidated (stack frame may be deallocated). If that's the case, then on_single
should be an unsafe
method, or there should be a custom lint (defaulted to deny
) to check that on_single
and similar methods are only ever be called from main
.Box<Fn(..)>
before being stored (in some static
variable). This means the application will require an allocator. Requiring an allocator is not inherently bad but it's one more requirement that must be considered.That's what I mean.
an interrupt handler is only safe if done from the main function
Actually, wherever we register a handler without box, the environment of closure will be deallocated. Because they are moved into a registering function, for example the on_single()
, and returned soon. Heap allocations is necessary for this solution.
@goandylok
Hmm, my understanding is that the boxing is required only to make the callback constant in size (Box<Fn(..)>
is always the size of two pointers -- i.e. 8 bytes on ARM) so it can be stored in a static variable. Unboxed closures (F: Fn(..)
) can't be stored in a static because they have different (anonymous) types and sizes.
Heap allocations is necessary for this solution.
A crazy idea to make this work without heap allocations is to change the signature of on_single
from fn (_: Box<Fn(..)>)
to fn (_: &'static Fn(..))
. Now, in the current state of the language, this change would make on_single
fairly useless because (a), AFAIK, there's no way to create a &'static Fn(..)
trait object and (b) even if (a) was possible, that closure could only capture static variables and not stack variables because those always have a lifetime 'a
smaller than 'static
.
Here comes the crazy part: Change the lifetimes of stack variables allocated in a function that never returns from 'a
to 'static
. This would make the following example possible, safe and heap-allocation free:
impl Adc {
// ..
fn on_single<F: Fn(u8) + 'static>(callback: F) { /* .. */ }
}
fn main() -> ! {
let mut adc = adc0();
let buffer = CircularBuffer::new();
let mut ch = buffer.in();
// Note: Captures everything by reference! All those references have lifetime `'static`
let closure = |sample| ch.send(sample);
// Coerce to trait object
let callback: &'static Fn(u8) = &closure;
adc.on_single(callback);
// `main` is known to never return
// Therefore its stack variables are never deallocated and have `'static` lifetime
loop {}
}
Wonderful idea!!! But rustc does not seem to be so cleaver. Am I wrong?
fn main() {
start();
}
fn start() -> ! {
let closure = || ();
let callback: &'static Fn() = &closure;
on_single(callback);
loop {}
}
fn on_single<F: Fn() + 'static>(callback: F) { /* .. */}
{ crazy } HEAD » cargo build /cygdrive/k/Andy/Rust/Workspace/crazy 101
Compiling crazy v0.1.0 (file:///K:/Andy/Rust/Workspace/crazy)
src\main.rs:7:36: 7:43 error: `closure` does not live long enough
src\main.rs:7 let callback: &'static Fn() = &closure;
^~~~~~~
src\main.rs:7:36: 7:43 note: reference must be valid for the static lifetime...
src\main.rs:6:25: 10:2 note: ...but borrowed value is only valid for the block suffix following statement 0 at 6:24
src\main.rs:6 let closure = || ();
^
error: aborting due to previous error
error: Could not compile `crazy`.
To learn more, run the command again with --verbose.
But rustc does not seem to be so cleaver. Am I wrong?
Right, when I said "Change the lifetimes of stack variables allocated in a function that never returns from 'a to 'static.", I meant it as a change in the Rust language. It would have to go through the RFC process to make sure it doesn't have any safety holes. In particular, some questions I have: Does this invariant (stack frame is never popped) on all divergent functions? Or do we need a new type signature for, specifically, functions that never return? Is this safe/sane when panic/unwinding occurs?
Ok, I got your point. Are you willing to submit it?
Ok, I got your point. Are you willing to submit it?
I'd start with a pre-RFC on the internals forum, but I don't have much time this week -- maybe next week!
I don't think the invariant hold for all divergent functions. There is at least two case where it doesn't hold:
Instead of a closure, we could use a simple fn
which wont need to be boxed. But then there is the question of capturing the channel (ch
). For that I see tow ways_
1) This could be done by passing it to adc.on_single
. Then, it would pass it to the function as an extra parameter on each call.
2) if the channel is thread safe and statically allocated, it could be in a global static variable and don't need to be captured at all.
I'l add that if you use an RTOS, you want to use queues provided by the RTOS because they provide integration with the RTOS like allowing to run other thread when a thread is waiting on an empty queue. This should be kept in mind.
Also you said:
Peripherals are not accessed in the interrupt handlers (module clearing the interrupt flags)
I don't think its always practical, and as such its not an universal solution (a solution don't need to be universal, but its limitation must be known and understood).
I found a paper that is relevant to the subject: http://amitlevy.com/papers/tock-plos2015.pdf. Its about Tock, an embedded operating system written in rust. It talks about closures in sections 3.2 and 3.3. The code is on https://github.com/helena-project/tock.
There are two presentation pdfs linked on http://www.amitlevy.com/#talks.
(I found this linked on https://users.rust-lang.org/t/ics-scada-rtos-rust-project-brainstorming/6516)
There is at least two case where it doesn't hold:
as you mentioned: unwinding
Do you know if unwinding pops stack frames or simply walks over the stack frames? If the later then I think this 'static
stack variables invariant will hold during unwinding.
the thread is cancelled, either by itself or another thread
There's no safe way to cancel a thread, right? At least, AFAIK, not in std
. Got an example where cancelling a thread, assuming 'static
stack variables, leads to memory unsafety (e.g. using dangling pointers)? I got an example for the unwinding case but it depends on how the unwinding mechanism is implemented.
Instead of a closure, we could use a simple fn which wont need to be boxed. But then there is the question of capturing the channel (ch). For that I see tow ways_
This could be done by passing it to adc.on_single. Then, it would pass it to the function as an extra parameter on each call.
This would require "fixing" the type of the extra parameter, right? Or in other words, you can't have a generic fn on_single<T>(closure: _, extra_arg: T)
because you need to store the callback in a static
and that static
can't be a generic.
2) if the channel is thread safe and statically allocated, it could be in a global static variable and don't need to be captured at all.
This is starting to sound like my proposal -- my Queue
is also thread safe and statically allocated. A issue that I'm starting to see is that a global static channel can be accessed from any interrupt handler, and you probably want to limit which interrupt handlers can access it but you really can't.
I don't think its always practical, and as such its not an universal solution
In the book I want to list several methods one can use to handle interrupts along with their tradeoffs. I don't think we have arrived at an universal solution (if it exists) yet.
I found a paper that is relevant to the subject:
This paper was discussed on reddit and on the rust-users forum last year, look for the past discussion. IIRC, the authors didn't know/think about using Cell
, which allows mutation via a &-
reference (within a single thread), to handle shared mutability of registers. The slides also mention a re-design, so we should check out their repository to see what approach they are using today.
Embedded software must deal with several external signal/events (interrupts!). How should we structure our program to deal with these interrupts which may happen at any time (i.e. they are asynchronous events)?
There are many ways to tackle this problem. This issue is an space to discuss possible solutions and the abstractions these solutions require.