-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds Shared<T> a wrapper for sharing data between handlers #15
Adds Shared<T> a wrapper for sharing data between handlers #15
Conversation
I absolutely love it. Something I put on the wishlist in rust-embedded/wg#256 (comment) |
I consider multiple instances of (*) e.g.
This sentence worries me. The problem of sharing, and moving, data between main and interrupt handlers already has a zero cost solution in the form of controlled scoping (e.g. RTFM). There are other solutions as well like dynamic interrupt handlers (see embedded-rs and this gist), that although not zero cost at least are non-panicking. Yet people seem to be reaching out for the worst possible solution ( // syntax is a straw man
#[cortex_m_rt::app]
const APP: () = {
// uninitalized static
static mut MOVE: SYST = ();
// NOTE: no RefCell
static mut SHARED: u64 = 0;
#[init]
fn init() { // interrupts disabled
// ..
// initialize static at runtime
MOVE = SYST;
}
#[idle(accesses = [SHARED])]
fn idle() -> ! { // interrupts re-enabled
// ..
loop {
let x = SHARED.lock(|shared: &mut u64| { // same as `interrupt::free`
*shared -= 1;
*shared
});
// .. do stuff with x ..
}
}
#[exception(accesses = [MOVE, SHARED])]
fn SysTick() {
let syst: &mut SYST = MOVE;
// ..
// no critical section required
let shared: &mut u64 = SHARED;
*shared += 1;
}
}; It kind of duplicates what RTFM offers (it doesn't handle priorities and it doesn't allow sharing between interrupts), but if it reduces the use of I should add that I personally would like to eventually deprecate I'd like to hear other people's opinions on this PR, |
Currently it is the only really known and hence standard way of doing this, apart from RTFM. A better standard way would be much appreciated; I don't think a whole lot of people know the required Rust-Fu to make it happen on their own.
The approach is interesting but I have a strong dislike for all those complicated custom notations which desugar into something non-trivial; magic like that breaks far too often in unexpected ways. |
I kinda agree with @therealprof I'd prefer an API that used standard rust structure as much as possible and actively avoided macro driven custom syntax. Possibly something that was more focused towards interrupts as closures as given in the example you linked @japaric. As the major issue here, 'how to transfer data from main to an interrupt handler' is currently driven by the fact that they both exist as free functions floating at the top source level. So perhaps an API that enforces defining interrupt handlers within your init code as closures passed to a function would be useful. Also we would still potentially need something |
I'd love to see a better alternative than the But, I'm not a huge fan of putting basically RTFM-lite into cortex-m-rt, even as an optional API (how optional is it if you pretty much have to use it to safely share state between the main thread and interrupt handlers?). RTFM already exists to provide that type of solution, but I feel like cortex-m-rt should be more general-purpose. As with @therealprof I'd like to minimise custom notation where possible, too. I think if we could make some sort of @japaric could you elaborate on your objection to the |
I will chime in a bit on this, when it comes to For example if this is used in exception/interrupt handlers these systems' execution time analysis becomes tainted and systems such as RTFM should be used in its stead as there are no need for global locks when you have done the analysis. Plus, we can't really do better (from an execution time perspective) than the locks in RTFM so we should rather advocate for the use of that framework. |
If you're not using different priorities to preempt interrupt handling then the execution time of the handler is deterministic/can be reasoned about, conversely if you use different priorities and allow preemption your timing analysis will be off no matter what.
True, but then again most use cases don't actually need stiff hard realtime guarantees so why force the implementation complexity and quirks of RTFM on all users? |
One relevant bit is that in a notable portion of cases this isn't even about 'sharing' data between main and interrupt handlers, it is about being able to transfer ownership of data from main after initialisation into interrupt handler so that it can be used there. Can we think of a way to achieve this that involves minimal intrusion into the users coding? Also what are the active downsides to having runtime registration of interrupt handlers as closures? As to me that seems to solve this side of this issue neatly by allowing you to move values into the closure. |
No can do, unless you want do veneers for every interrupt handler. The entry points of the interrupt handlers need to be known at link time since they have to be placed into the interrupt vector table. I think the only two options here would be veneers or some macro magic so extract the interrupt handler from the |
From my point of view, we should have APIs at all levels that are simple, clear, sound, and have predictable (if possible zero-cost) OH. (These requirements are not unique to embedded, but goes for Rust in general.) RTFM aims at being just that, and with the move to 4.0 we are one step closer to standard Rust. I would like to see some concrete examples of applications written in some existing or potential bare metal fashion that would better match the stated criteria. If we can find that, then we could put the focus on improving the usability (or other aspects) of RTFM. The goal is that RTFM should NEVER stand in your way, even for implementing the simplest type of application. Best regards |
Is the assumption then that we would expect all embedded applications to be built using RTFM? |
Well, our aim is to make RTFM the perfect fit for bare metal applications with static resource and task structure (that is we are NOT aiming at systems where you "load" applications or resources on the fly). In general dynamic systems is out of reach for static analysis (required by RTFM). But we can think about extending RTFM to deal with systems having different operational modes (e.g., a low power mode, running only a subset of tasks), or other modes running another subsets of tasks (e.g., handling the system in a "limp back" state if some fault has been detected). So in the future I foresee we can handle this limited type of dynamic behavior. RTFM already plays well with /Per |
With that said, RTFM does not cover/aim at dynamic applications. However, I do believe such an operating system (supporting dynamic loading) may well be built upon RTFM (but is not our primary goal). Many (or perhaps even most) embedded applications fall into the static task/resource category and hence suitable to SRP based scheduling. Given that the RTFM API does not stand in the way of (any) application development, it can serve as a common platform for application development. Notice, PACs, and HAL-implementations etc. are RTFM agnostic, so there is no loss of generality implied. So in effect, I believe it better spent effort to improve on RTFM than to come up with yet another framework for managing resources. |
No one is trying to write a framework for managing resources. This is all about providing safe low-level primitives to allow writing simple but safe applications. Think of the target community being entry to intermediate level programmers coming from (close to) bare-metal C environments or playgrounds like Arduino with little to no Rust experience. RTFM is (probably) fantastic for mission critical hard real time industrial application but I find the custom DSL boilerplate unidiomatic and not very beginner and/or C-programmer friendly. It's also a huge jump from the content we have in the book to RTFM just for the sake of sharing peripherals with interrupt handlers. |
Yeah, like my principle use case for this right now would be for small code examples in the stm32f0xx-hal, like https://github.com/stm32-rs/stm32f0xx-hal/blob/master/examples/flash_systick.rs |
I've also found myself reaching for the unfortunate global pattern for items that are well described as "late resources" by RTFM. I definitely think that RTFM has solved this problem well - I think the desire is to have a similar functionality to RTFM's resources (statically initialized, as well as early-runtime-initialized), without requiring use of RTFM. In the example above where I would like to share data between a driver instance and an interrupt provided by the driver, it would not make sense to use RTFM at all, as this is a HAL crate, rather than an application. EDIT NOTE: For background, the example I linked above aims to provide "driver managed" async buffering and sending of data via a UART. To achieve this, the driver "takes over" the UART interrupt to service the DMA transactions chunk-at-a-time. The user/application code never "sees" the interrupt, however the memory space backing the queue is provided by the user, so that they can specify the size of the queue used by the driver. This means that the initialization of the driver must happen at runtime. I'm aware that making a generic item (such as |
I am unsure if you meant that in this scenario you can't have RT guarantees, but if you do, I disagree. If you have multiple priorities and preemption you can have a guaranteed WCET since you can compute all the possible latencies/delays visible at your level based on the upper ones. |
This is indeed fundamental, however teaching the mutex dance (or simplifying it, making it easier to use) is strengthening an anti-pattern and should be avoided.
This we can mitigate/solve with documentation and the RTFM book. It's important to see that this is not a trivial problem and that there are frameworks to handle it. @jamesmunns While I agree that there is a real need for a "good enough" solutions, this is not it as it helps making an anti-pattern easier to use (and beginners will always continue what they use the first time) as it does not scale by hurting the entire program. |
eddyp is correct, there are no issues here. For example Klee symbolic execution can give the no. of clock cycles which then can be used for the analysis to give strong guarantees. |
In theory. In practice it can be impractical or even impossible (e.g. if you're processing arbitrary lengths inputs or rely on external timing). Also in many (even hard real-time) applications it is not necessary to guarantee execution WCET across all priority levels but only the highest. Not to mention there're other mechanisms to deal with hard requirements which do not involve the execution units of the MCU/CPU at all. |
I agree. This is exactly what I have been wanting since I've been looking at rust because I want to write a RTOS that can be a drop in replacement for an older C implementation. Ditto for some drivers because the current C implementation has a standard interface. |
I see what you mean, but we're not talking about a bad design which does O(n) processing in the interrupt handler. Anyway, this is already off topic, so I'll stop here. |
Fair enough, I can definitely understand this not being the right solution, my comments were more broad about "this is a problem that needs to be solved", rather than stating "this is how we should solve this problem". If there is a better place for me to provide this feedback, I'm happy to move there :) In general, I would also say many users of Rust at this time are also not considering WCET/RT deadlines at all, as this is generally only required by a select few industries. I would think that many users would prefer something that does work, and is teachable, even if sub-optimal, though I will admit this is just personal opinion. That being said, deadlock is something that is likely to affect even non-RT users, so solutions we present should make that avoidable if possible - either by use of static analysis, or even just providing failable methods ( |
I don't see this as an anti-pattern at all. Raw
For me it's a usability issue. We want to make it easy for beginners and converts to start coding for embedded systems in Rust and once they've understood the basic concept, we can introduce more advanced ones. |
I think we should have a look at what properties are desired and then look into how to solve them for the simple case - attack this issue a bit more pragmatic. |
The properties required are a simple and minimal way to transfer ownership or share ownership of a fixed set of pieces of data between the main function and one or more interrupt handler functions. Particularly one that would be suitable for use inside a HAL. As I find the note about encapsulating interrupt handlers within drivers/hal interfaces a notably compelling one. As this is a situation where you cannot take an approach that defines the whole program structure. |
I think that's quite a big ask and we could probably get away with a lot less. For instance, many use cases are probably OK with only sharing between the main thread and a single interrupt handler -- and now you know the main thread cannot pre-empt that ISR, and can only be pre-empted by a single ISR, so you could imagine the ISR always getting direct and panic-free access, while the main thread only disables a single ISR to access the shared variable. Another use is moving something from main to one ISR once ever and not sharing it thereafter, which again might be simpler to implement. |
I agree. For instance, I see the current Mutex<> implementation analogous to an Rc<>. What we probably need is some conventions and some implementation based on how "wide" is the synchronization mechanism. For instance single core, cluster level in multi core symmetric systems, all cores in the same big.LITTLE configuration, all cores in the system (for hybrid platforms such as Freescale/NXP VF6xx which has an M4 and an A5 core) and so on. It might be that focusing on the naming and conventions would allow us to come to the conclusion we need to define some traits and focus on HAL... |
I've opened an issue on the /wg issue tracker, which might be better suited for discussing these design goals, outside of the scope of this particular PR. |
@HarkonenBade, just for reference, the RTFM version is like this so you can do side by side comparison: #![no_main]
#![no_std]
extern crate panic_halt;
use cortex_m::peripheral::syst::SystClkSource;
use stm32f4::stm32f413::GPIOA;
use rtfm::app;
#[app(device = stm32f4::stm32f413)]
const APP: () = {
// late resorce binding
static mut GPIOA: GPIOA = ();
// init runs in an interrupt free section
#[init]
fn init() {
// configures the system timer to trigger a SysTick exception every second
core.SYST.set_clock_source(SystClkSource::Core);
core.SYST.set_reload(16_000_000); // period = 1s
core.SYST.enable_counter();
core.SYST.enable_interrupt();
// power on GPIOA, RM0368 6.3.11
device.RCC.ahb1enr.modify(|_, w| w.gpioaen().set_bit());
// configure PA5 as output, RM0368 8.4.1
device.GPIOA.moder.modify(|_, w| w.moder5().bits(1));
// pass on late resources
GPIOA = device.GPIOA;
}
#[exception (resources = [GPIOA])]
fn SysTick() {
static mut TOGGLE: bool = false;
if *TOGGLE {
resources.GPIOA.bsrr.write(|w| w.bs5().set_bit());
} else {
resources.GPIOA.bsrr.write(|w| w.br5().set_bit());
}
*TOGGLE = !*TOGGLE;
}
}; |
So looking at the above example, I hope we can waive the conception of RTFM adding complexity, maybe especially for the beginner. No need to think about critical sections, things that may go wrong etc. So in effect it reduces complexity, while giving you all the goodies, deadlock freeness and soundness out the box. And it scales (adding new functionality and requirements, no need to redo the design, just add message passing when you need it...). So maybe its just a matter of communication, documentation and good idiomatic examples. I'm currently giving a course (starting next week) where LTU students will design their own ES, choosing components, designing PCBs, and program their applications. These students are expected 0 prior Rust experience, so they are the ultimate beginners. We ran the course in this format last year (for the first time). All the student groups were able to get mandatory functionality, USART and ITM communication, PWM of some GPIO (e.g. led) , and ADC of some analogue signal, (e.g. battery voltage). Additionally, each group of 4 students selected an application, we had robotic control of a ball-maze game (with led display), a GBA game slot/card emulator (with SD-Card FAT file system implemented in Rust from scratch by the students), a waker watch (using RTC as a programmable wakeup source), a moonshine heater (with LOTS of power electronics), CAN bus based zip game, etc. 7 groups in total, with a lot of diversity. And guess what, students did this in 10 weeks half time studies:
The teaching method was GitGuD, i.e., the students were working hard throughout the whole course challenging their potential. That works since its fun!!! Selecting a project of their own is highly motivating. Doing the impossible even more so ;) And they succeeded. As teachers we were (almost always) available on telegram, answering all questions promptly, students should not get stuck for too long, but the answers were more of hints not solutions. Regarding Rust, only a few lectures actually. But loads of assignments (10) taking them from the simplest bare metal "hello world" to a fully fledged serial terminal (with message parsing etc.). Yes, the assignments cover cortex-m, cortex-m-rt, embedded-hal, and cortex-m-rtfm. I'm now in the process of updating the examples and assignments to RTFM 4. (Message passing will come handy:) ST is sponsoring the course with Nucleo F401re/F411re devkits (each student get one). We also use the So in parallel with waiting for the PCBs to arrive back from fab, we took on Rust using the Nucleos, building experience and a code base that they could re-use in their own firmware. In conclusion, neither Rust not RTFM was a show stopper to success (problems were more related inexperience of PCB design and datasheet reading/understanding). Comments regarding software development was mostly related to the lack of documentation (at that point). We are looking forward to the upcoming installment, now with a more user friendly and capable RTFM4, better docs (the books) and all the great work you all put into making the embedded Rust such a pleasure to work with. |
@perlindgren How would you approach the use of contained interrupt handlers within driver/hal implementations as proposed above? |
@perlindgren at least personally, I am a fan of RTFM, for the reasons you listed. I have used it in the past, and plan to continue recommending it to developers as well as customers of mine. In general, I'd like to make use of RTFM my default for all new embedded rust projects. However the WG in general is supporting all developers of embedded rust, not just users of RTFM. At the moment, we have not made the use of RTFM a core/required part of the ecosystem, at least not as heavily as things such as I would still be interested in hearing your input regarding resource sharing within a driver that I outlined above. |
@jamesmunns ,perhaps the driver impl should export a function (instead of specifying a handler) that you just call from your interrupt handler (defined in #[app]). If your driver should have a task local store, just declare in the interrupt handler/task, and pass it to the driver along with other resources the driver needs. In this way the resource sharing will be sound (no unsafe required). The only difference is that binding the driver to the interrupt vector is done in #[app], not hidden in the driver itself. See the #[app] as being the switchboard (or interaction view) of your system. Functionality can be given in place or factored out. RTFM3, had the notion of paths to the actual implementation, you can still achieve the same thing with a trampoline, and for small enough examples you can write all your code in-place. The main reason for the current choice in RTFM is convenience, we don't need to care about the concrete types of resources. Re-factoring in general is somewhat a bummer in Rust due to the choice of non inference in function signatures. In the future, it is projected that proc macros (like #[app]) can pick up local (or even crate wide) attributes. That would allow drivers to bind resources, interrupts etc. If that is a good thing... well maybe it improves on convenience. However we loose the switchboard view. The main inconvenience is due to the lack of type inference, maybe that restriction can be lifted (which may be a good thing for code re-use in any case, but I don't know what kind of worm-hole that would open up... better keep tho lid on perhaps...) Did that answer your question, or did I completely miss the point? |
@perlindgren, I think that is an acceptable answer, though it makes the trade off of being less convenient in the non-RTFM case, to support the RTFM use case, and may cause unsound behavior if the inner interrupt function should only ever be called in a single interrupt context (not any valid |
@jamesmunns , you mean unsound if you call into the driver code from different call sites (not just the intended interrupt handler)? Well, if the driver can be written in safe code (regarding shared resources), it is not unsound to call it from different call sites. It is the different call sites that are unsound (by grabbing the resources in an unsound manner), it is the price you pay by opting out a safety in the first place. At least it makes it clear that the call site has to take responsibility for the safety. Rust memory model is about memory safety, if code bugs out due to some other reason (like unintended usage), its by definition not a soundness violation. We are intentionally using unsafe in a bit broader than just memory safety. E.g. its unsafe to write a register lacking an enum, just to indicate that the programmer has to take responsibility. With that said, I think +/- all HW access could be regarded unsafe in that sense. Think of it, even in RTFM you can do stupid things in safe code, like changing the clock source to a non existing external xtal, .... bang..... at best you get a hard fault... In RTFM we can do only so much, - we offer soundness to the Rust memory model for concurrent resources. In a hal impl (that takes on setting up clocks and so forth) we can offer a bit more robustness to misuse. Side note: / Per |
@jamesmunns , thinking of it. You may have a cfg feature for opting in the interrupt handler, that does the dirty work, calling into the device driver... Then it would work with or without RTFM. @ALL We are eager to see other examples, where You feel that RTFM falls short, or stand in Your way implementing things. /Per |
1 similar comment
@jamesmunns , thinking of it. You may have a cfg feature for opting in the interrupt handler, that does the dirty work, calling into the device driver... Then it would work with or without RTFM. @ALL We are eager to see other examples, where You feel that RTFM falls short, or stand in Your way implementing things. /Per |
It's not zero cost: it has memory overhead, it can fail at runtime and always In embedded, reliability and resource (e.g. RAM) usage are important metrics, Also, one or two Mutex-es may cut it for a small / hobby project but Mutex is
This is the root issue. RTFM v1 / v2 explored this space and the conclusion was I get that familiarity is important but Rust didn't set up to be just "C with I see global
Then let's cover dynamic handlers first and then procedural macros. There are
Using
Is this likely to occur in practice? Maybe not, but there may be more elaborated I would suggest moving If you want something zero cost then you need a "before, after" constraint, like Thanks for the PR, but I'm not going to accept it. Let's discuss better alternatives |
Huh? Nobody says it has to be zero cost. In fact it is lower cost than what other languages do which is a good thing. Zero cost would be nice but is not a must have if the vast majority of cases. Also if it fails it will fail deterministically which is a good thing!
That is something I agree with. The rest not so much.
This is something I don't think I will ever be able to get behind. The use of macros in Rust is slowly turning into the exact same mess which they are in C/C++: Lots of opaque magic happening behind the facade trying to work around limitations/flaws of the programming language itself. Macros make it hard to reason about the implementation and cause random errors to appear out of nowhere, especially when trying to extend them. Let's do less of those please and make sure that the language itself allows us do stuff.
But they're a lot harder to debug than a deterministic panic which is why I definitely prefer a panic over a deadlock any time of day. |
This adds
Shared<T>
which is a helper for sharing values between the main thread and interrupt handlers. It is essentially a wrapper aroundMutex<RefCell<Option<T>>>
that adds some helper methods to make the standard ways of executing this common pattern less painful.e.g. in one of the examples from the stm32f0xx-hal it makes the following simplifications:
Declaration
to
Loading
to
Referencing
to
There are still some deficiencies that I would like to solve at a future date, but I think it is suitable to be put into use as it is. I'd rather that
get
andget_mut
could returnOption<&T>
andOption<&mut T>
values respectively, but I don't think that can be done without replacing the use ofRefCell
internally. All attempts I've made have run afoul of lifetimes not living long enough to sustain the references passed externally.