japaric-archived / cu

Testing ground for the Copper book (http://japaric.github.io/copper/).
Apache License 2.0
58 stars 1 forks source link

Handling interrupts #35

Open japaric opened 8 years ago

japaric commented 8 years ago

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.

japaric commented 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:

japaric commented 8 years ago

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:

andylokandy commented 8 years ago

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.

japaric commented 8 years ago

@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 {}
}
andylokandy commented 8 years ago

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.
japaric commented 8 years ago

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?

andylokandy commented 8 years ago

Ok, I got your point. Are you willing to submit it?

japaric commented 8 years ago

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!

dzamlo commented 8 years ago

I don't think the invariant hold for all divergent functions. There is at least two case where it doesn't hold:

dzamlo commented 8 years ago

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).

dzamlo commented 8 years ago

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)

japaric commented 8 years ago

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.