-
Notifications
You must be signed in to change notification settings - Fork 157
[RFC] Peripherals as scoped singletons #157
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
Comments
This is very nice! It fixes my main (and basically only) complaint about rtfm - the need to reconstruct peripherals in every task. Sadly, I haven't had any time to do more with rtfm, but I'll definitely check this out in more depth when I get the time. |
@japaric How do I "move" a peripheral? For some applications it is very important to being able to switch GPIO pins from input to output and back on-the-fly. I do like the idea of the peripheral proxy quite a bit but I'm absolutely not hot about this:
It would be great if there was a simple way to get an abstracted static peripheral proxy object (same for other fixed memory blocks as scratch space, but that is a different topic) as an alternative to obtaining the singleton. |
You write a method that takes
You can continue use type state in straight line code to transition from input mode to output mode. fn foo(pa0: PA0) -> PA0 {
// `as_input` has signature `fn(self) -> Input<Self>`
let input: Input<PA0> = pa0.as_input();
// do input stuff
// `unwrap` has signature `fn(self) -> PA0`
let pa0 = input.unwrap();
// `as_output` has signature `fn(self) -> Output<Self>`
let output: Output<PA0> = pa0.as_output();
// do output stuff
output.unwrap()
} Alternatively you could create an enum Mode {
Input,
Output,
}
struct Io<P> {
pin: P,
mode: Mode,
}
let io: Io<PA0> = pa0.as_io(Mode::Input);
if io.is_high() {
// ..
// automatically changes the mode to `Output`
io.set_low();
// ..
} If your pin is stored in a // `pa0` is stored in a `static` variable (resource) so it can *not* be moved out.
// At best you can get a `&mut` reference to the `static`.
fn bar(resource: &mut Option<PA>) {
// start of the dance
if let Some(pa0) = resource.take() {
// the definition of `foo` is above
let pa0 = foo(pa0);
// end of the dance
*resource = Some(pa0);
} else {
// unreachable because the `resource` is always in the `Some` variant
// we still need to `Option` to temporarily move `PA0` out of the resource
}
}
There is: you use RTFM. |
I have appended an example of how this proposal solves problem (a) to the RFC text (see the issue description). I forgot to cc the embed-rs people. They have been using move semantics / ownership since long ago so maybe they have feedback on this proposal. cc @oli-obk @phil-opp please see the issue description. |
Do I understand correctly that each peripheral has a global flag to check
if it was created or not?
Is `pa0` in your last example a zero sized type?
…On 22 Nov 2017 09:35, "Jorge Aparicio" ***@***.***> wrote:
I have appended an example of how this proposal solves problem (a) to the
RFC text (see the issue description).
I forgot to cc the embed-rs people. They have been using move semantics /
ownership since long ago so maybe they have feedback on this proposal. cc
@oli-obk <https://github.com/oli-obk> @phil-opp
<https://github.com/phil-opp> please see the issue description.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#157 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABgB3FHOhC4vMBo4pn-LiBof_GnfXx4Hks5s48CjgaJpZM4QmGtI>
.
|
Implementation wise, the differences between your proposal and our impl are:
Some thoughts
|
No, there's a single global flag for whether you obtained the handle for the entire hardware. After that, you just zero-cost extract the hardware components that you want. |
Not in the main proposal. In the main proposal there's a single boolean that guards all the peripherals. I initially implemented this using a per peripheral boolean but didn't see much advantage in doing that so that became the alternative to the main proposal (it's under "unresolved questions" in the RFC text). Also note that the boolean never gets cleared when the peripheral is dropped. The boolean is not some sort of lock. (In the f3 implementation there's actually two booleans to guard the peripherals. One for core peripherals and one for device peripherals. It might be possible to just have one boolean but two booleans was easier to implement).
Yes. Thanks for the input.
I think so too but haven't measured. In particular you are guaranteed that
I have yet to look at your interrupt system but I recall that you mentioned during rustfest that your approach doesn't support priority levels (all the interrupts had the same priority). Is that still the case? I'm not 100% sure that reconfigurable interrupt handlers are compatible with RTFM (SRP); RTFM wants everything to be statically (compile time) defined.
Our code is already very bloated in debug mode due to the svd2rust API so I don't think this would help much 😅.
I don't think I fully understand what's the issue here. Could you point me to an example? |
I'm 100% on board with this concept. I'm currently working on a HAL for LPC82x, and what I'm doing is very much in line with what you're suggesting here, with the caveat that I the user must promise not to access the peripheral directly. This RFC would be a huge improvement for me. Thank you, @japaric, this is great work! I have a question about this piece of code from the original issue: pub struct GPIOA { _marker: PhantomData<*const ()> } Why the
Have you considered using type parameters instead of newtypes to encode the type state? Something along those lines: struct PA0<State> {
_state: State,
}
impl<State> PA0<State> {
fn as_output(self) -> PA0<Output> { ... }
}
impl PA0<Output> {
fn set_high(&mut self) { ... }
} I don't know if that's a better solution in this specific case, but in general, I see the following advantages over newtypes:
How does putting a peripheral in a static variable imply that the peripheral becomes globally visible? The static variable can live in a module and not be
I don't care. This is enough of an improvement to be worth the breaking changes.
I don't know the answer to this question, but at least I never had the need for this kind of fine-grained access.
Maybe I don't feel strongly about any of those, just throwing around some suggestions.
I haven't looked at any of the pull requests so far, and I'm busy enough right now that I might not get to it for the foreseeable futures. However, feel free to ping me if you need feedback on something. I'll gladly make time. |
This clears all OIBITS like
Our mutexes don't support priority levels, so all critical sections block all interrupts. This can cause priority inversion. We just haven't gotten around to implementing Mutexes with priority level support into the interrupt system.
Take the simple example of a "Button driver" that's nothing more than a debouncer. We need to implement it to take a specific GPIO pin instead of just "something that we can read a bit from". |
Ah, makes sense. Thank you! |
Thanks for taking the time to comment.
Short answer: no. But I'll explore that option at some point. In general, you shouldn't take the examples in the RFC as "the right way to do it" but rather as a I'm using newtypes instead of the more standard
Right, it may not be global in the literal sense but it's still "global" to the module where it's
If you are OK with using Thanks for the pointers. I'll take a closer look at that example and the interrupt system you are |
@japaric Thanks for the explanation, seems like all bases are covered. |
Thanks, I realize that. I just wanted to make sure you're aware of the possibility, but I guess that whole discussion was a bit off-topic from the beginning. I think we all agree that this RFC allows us to build better abstractions. The details of how those abstractions will look like exactly are probably not that relevant at this point.
I think we agree that this situation is not ideal, but I strongly disagree that it is "just as bad as global visibility". It might be close to as bad, if it's a huge module. But if only 2-3 functions have access to that
Makes sense. In any case, I think the trade-off is worth it. |
This looks like a really nice improvement. I vote for move fast and break things. |
OK. There seems to consensus on this RFC so I'll land the pieces in all the repos in the coming days. There are a few implementation details to iron out:
Finally I have been sketching APIs for more peripherals in this branch (clock configuration, pwm, capture, DMA, etc.) of the blue-pill crate. |
@japaric I've played around a bit with the singletons and it simplifies things quite a bit. I am a bit puzzled about the |
This looks like a nice improvement that will be very helpful today and to what I expect the long term embedded rust will be like. In case my heart emoji didn't express my feelings strongly enough, I'm all for breaking all kinds of things to land this! I've been totally swamped with work the last week and probably will be the next week as well. I will take some time looking at the details as soon as i can. |
For what it's worth, I don't mind having two peripheral sets. Actually, I think it might be a bit nicer, as it makes it more obvious how platform-dependent a given piece of code is. If I remember correctly, the flags might be optimized away anyway. If that's true, I don't really see a reason to try to unify them, if that makes things more complicated.
FYI, I've been working on APIs for clock stuff to, in my LPC82x HAL (which I've published this morning). There are also the beginnings of a generic interface in there, which I plan to submit to embedded-hal once it has matured a little.
I haven't looked at the code yet, but I supect the point is to minimize overhead in release mode, while making mistakes obvious in debug mode. This has precedent in Rust itself, as integer overflow will cause a panic in debug mode and do nothing in release mode, for the same reason. It would be great if there was a way to guarantee at compile-time that the method if only called once. However, I don't know how we could do that reliable. My guess is that it's currently not practical. |
That's actually easy to do as a clippy lint. You strap a Open an issue on the clippy repo if you consider this solution practical |
Well, with embedded projects debug code might not always fit in the flash so sometimes it's simply practical to use it. I never use debug code so I'm not too happy about catching this "problem" only in debug versions. Also if this is called so often that it would become a performance problem, you're very much doing it wrong anyway since this is only supposed to be called once so it shouldn't be a problem, no? ;) |
This is very cool, I like it. The only problem I see in this particular case is that code generated by svd2rust is very clippy unfriendly and requires a lot of adjusting and/or test gating to be usable at all. :( |
You won't be using it on Also you can just turn off all lints except for the |
@oli-obk My own crate contains clippy unfriendly svd2rust generated code. I know I can do some stuff selectively in clippy, but I haven't figured out how to get rid of the specific warnings in the svd2rust code via configuration. Clippy is an incredible tool which I'm using extensively for any projects but the MCU stuff... |
I didn't know this. This is awesome. Thanks!
I presume this is missing "don't"? Otherwise it's confusing :-)
You're right, but you don't have to do a full-blown debug build to get debug assertions. You can enable them separately. See Cargo documentation.
I don't think performance is the issue, but code size could be. Granted, a single |
No. We don't have this lint. Open an issue if you want this lint implemented. |
@hannobraun I'm in general no big fan of |
Edit: Please disregard. While I wrote this, the latest answer by @oli-obk made this irrelevant. |
Oh, good thing I asked then. I've added it to my task list, so I can look into it later. |
I believe multiple crates defining the same peripherals would result in the design laid out in this RFC to provide the guarantees it claims to provide. i.e. One would be able to run I feel like it would be desirable to make it impossible to link multiple versions of the crate defining peripherals in case this is implemented. (A simple way to do so would be to define a global symbol with a well-known name) |
Yes, this what brought up in the pre-RFC (#151). The proposed solution was to add a You can still break singletons if some implements an alternative singleton system for peripherals that uses different names for the
Can clippy trace the call graph then? Can it handle independent call graphs (an interrupt handler is the root of an independent call graph)? Could it get all these: // defined in some other crate
fn call_me_once() { /* .. */ }
fn main() {
call_me_once(); // assume this is the only intended call
call_me_once(); // easy to catch
foo(); // less (?) easy to catch
}
// could be defined in some other crate
fn foo() {
call_me_once();
}
// could be defined in some other crate
// dead code
fn bar() {
call_me_once(); // false positive
}
// could be defined in some other crate
// interrupt handler
#[no_mangle]
pub fn EXTI0() {
call_me_once(); // hard to catch?
} |
all the other functions would be required to also have For the interrupt handler, I'd just assume |
That sounds rather weird to me: Either this tries to catch abuse or it doesn't, just doing this under certain circumstances sounds like a recipe for disaster. As I said, I'm not at all a fan of |
@japaric one alternative would be to use linkonce_odr linkage, but not sure how supported between linkers it is. |
I see. You get a warning if a EDIT: enough for a sanity check. Since the lint won't be a part of the compiler we can't rely on it for safety (i.e. turn an unguarded unsafe function into a safe function). If you, as an end user, want to construct |
@japaric That is fully understood. It is still insane if you develop code with release type builds (like I always do for MCUs for a number of reasons, e.g. because I like looking (and comparing) at the (dis-)assembly) and call If it is enforced it should be always and properly enforced so there's no possible way to miss it. However to me it looks like just documenting why it is unsafe and removing the |
turn peripherals into scoped singletons See this RFC for details: rust-embedded/svd2rust#157
turn peripherals into scoped singletons See this RFC for details: rust-embedded/svd2rust#157
turn peripherals into scoped singletons See this RFC for details: rust-embedded/svd2rust#157
Peripherals as scoped singletons See this RFC for details: #157 With this change device crates will need to depend on a version of the cortex-m crate that includes rust-embedded/cortex-m#65 ### TODO - [x] accept the RFC - [ ] Check that non cortex-m targets still work - [x] decide on better names for `Peripherals::{all,_all}`
Peripherals as scoped singletons See this RFC for details: #157 With this change device crates will need to depend on a version of the cortex-m crate that includes rust-embedded/cortex-m#65 ### TODO - [x] accept the RFC - [ ] Check that non cortex-m targets still work - [x] decide on better names for `Peripherals::{all,_all}`
Peripherals as scoped singletons See this RFC for details: #157 With this change device crates will need to depend on a version of the cortex-m crate that includes rust-embedded/cortex-m#65 ### TODO - [x] accept the RFC - [ ] Check that non cortex-m targets still work - [x] decide on better names for `Peripherals::{all,_all}`
Peripherals as scoped singletons See this RFC for details: rust-embedded/svd2rust#157 - The first commit adapts this crate to the changes in rust-embedded/cortex-m#65 and rust-embedded/svd2rust#158 - ~~The second commit is an alternative implementation of RFC #47 (there's another implementation in #49. This second commit is not required for RFC157 but let us experiment with safe DMA abstractions.~~ postponed ### TODO - [x] un-bless peripherals as resources. Peripherals as resources were special cased: if resource listed in e.g. `app.tasks.FOO.resources` didn't appear in `app.resources` then it was assumed to be a peripheral and special code was generated for it. This is no longer required under RFC157. ~~This depends on PR rtic-rs/rtic-syntax#2~~ postponed
Peripherals as scoped singletons See this RFC for details: #157 With this change device crates will need to depend on a version of the cortex-m crate that includes rust-embedded/cortex-m#65 ### TODO - [x] accept the RFC - [ ] Check that non cortex-m targets still work - [x] decide on better names for `Peripherals::{all,_all}`
All the pieces required to implement this RFC have landed in the master branches of cortex-m, cortex-m-rtfm and svd2rust. 🎉 However, there are quite a few things to do before releasing a new minor version of those crates (e.g. updating the documentation). I have created a milestone for each repo to track the things to do. If you think anything else should be done before the next minor version release now's your chance: open an issue or leave a comment on an existing issue requesting adding it to the milestone.
BTW, the RFC for that change is in japaric/cortex-m-rtfm#59 if you have any thoughts / comments about it. I have run out of embedded Rust time for this week; I'll be back (late) next week. Thanks again everyone for your input. |
Motivation
Today, in svd2rust and cortex-m APIs, peripherals are modeled as global singletons that require
some synchronization, e.g. a critical section like
interrupt::free
, to be modified.The consequences of this choice is that (a) driver APIs are not ergonomic as one would expect, see
below:
Here the
Timer
abstraction has to be reconstructed in each execution context. One would prefer toinstantiate
Timer
as a static variable and then use that from each execution context. However,that's not possible with this
Timer
struct because of the non-static lifetime of the inner field.(It is possible to remove the inner field from
Timer
at the expense of having a critical sectionper method call -- which has worse performance than the non-static lifetime approach).
Even worst is that (b) driver abstractions can be easily broken due to the global visibility
property of peripherals. This means that there's no way to e.g. make sure that
TIM6
is only usedas a
Timer
inmain
andtim6
. Nothing prevents you, or some other crate author, from silentlyusing
TIM6
in other execution context --TIM6
doesn't even have to appear as a function argumentbecause it's always in scope. This issue not only breaks abstractions; you can also have race
conditions on TIM6 -- yes, even with
interrupt::free
you can have race conditions and that'stotally memory safe per Rust definition -- if
Timer
is being used in other execution contextsand you don't know about it.
So, what can we do to fix these issues? We can remove the root of all these problems: global
visibility.
This RFC is proposing to go from global singletons to scoped singletons. Instead of having
peripheral singletons with global visibility you'll have to explicitly import a peripheral
singleton into scope, into the current execution context. Because we are talking about singletons
here you can only import singleton P into scope (execution context) S once. IOW, if you imported P
into an execution context S then you can't import P into another scope S'.
This RFC not only addresses the problems (a) and (b) mentioned above; it also helps us tackle the
problem of compile time checked configuration -- more about that later on.
Detailed design
Zero sized proxies that represent a peripheral register block, as shown below, will be added to
cortex-m
andsvd2rust
generated crates.These proxies will be impossible to directly instantiate. Instead there will be a guarded function
that returns all the proxies only once.
The user will be able to access the proxies like this:
Thus the proxies are singletons: there can only exist a single instance of each of them during the
whole lifetime of the program. The proxies are also scoped, in this case they are tied to
main
, sothey are not visible to other execution contexts, unless they are explicitly moved into another
execution context.
Zero cost
An unsafe, unguarded variant of
Peripherals::all
will also be provided:When only the
unsafe
variant is used the cost of having scoped singletons becomes zero:This program has the same cost as using a global, unsynchronized
GPIOA
register block (which iswhat you see in C HALs).
Sidestepping the proxy
Each peripheral will provide a static method,
ptr
, that returns a raw pointer into the registerblock:
This is useful for implementing safe APIs that perform "unsynchronized" reads on registers that
have no side effect when read:
Enabling new APIs
Scoped singletons effectively give move semantics to peripherals. This enables richer, type safer
APIs than what can be expressed with the current peripheral API. Let's see some examples:
(you can find some initial experiments with these APIs in the
singletons
branch of the f3repository)
Type state as a contract
Digital I/O pins can be configured as inputs, outputs or to some special functionality like serial,
SPI or I2C. In some cases you want to configure a pin to operate in a certain mode for the duration
of the whole program; that is you don't want the pin to be re-configured by mistake.
Type state is a good way to encode this property: a type is used to encode the state of an object.
To transition the object from a state to another it needs to be moved so that the previous state
can no longer be used.
Here's a tentative GPIO API that uses type state:
Here the
Input
andOutput
newtypes are used to encode the type state. The most important parthere is that
GPIOA
is consumed to generate the individual pins and thus it can't no longer beused to configure the pins -- which would break
Input
/Output
contract of "this pin can onlybe used as an input for the rest of the program".
Compile time pin allocation
On modern microcontrollers a single pin can be configured to have one of many functionalities
(Serial, PWM, I2C, etc.). This lets vendor pack more peripherals in a microcontroller without
increasing the number of pins.
A problem that arises from this flexibility is that one might wrongly configure two, or more,
peripherals to use the same pin. With move semantics you can construct an API that ensures that
this can't happen:
Here we have high level abstractions like
Serial
consume the pins they are going to use. This wayonce one such abstraction is created no other abstraction can't use any of the pins the first one is
using.
Non overlapping register access
The "split" technique used for GPIO pins can also be used to split a peripherals in "parts" that (a)
modify different registers and (b) modify non overlapping parts of a same register. This comes in
handy with peripherals like the DMA which usually interacts with several other peripherals.
Vendors usually design their DMA peripherals so that even if the settings related to different
channels are stored in a single register that register can be modified atomically using e.g. "clear
flag" bits (no RMW operation required to clear a flag). If the vendor doesn't provide such mechanism
bit banding can probably be used in its place, if the device has support for it.
RTFM protects peripherals at the register block level. Without move semantics, to clear "transfer
complete" interrupt flags from two interrupts handlers running at different priorities you would
need a lock in the lowest priority handler:
But with move semantics you can split the DMA in channels and assign exclusive access to each
channel to each interrupt handler:
Thus no locking is needed. Each channel will operate on a non-overlapping portion of the shared
IFCR
register.Configuration freezing
In some cases you want to configure the core and peripherals clocks to operate at certain
frequencies during initialization and then make sure that these frequencies are not changed later at
runtime.
Again, move semantics can help here by "discarding" the peripheral in charge of clock configuration
once the clock has been configured:
Here, once the clock is configured, its configuration gets frozen by consuming / discarding the
RCC
peripheral. With this ... move the clock frequencies can no longer be changed. FreezingRCC
returns a
Clocks
struct that contains the frequency of every peripheral bus. This information isrequired to properly configure the operating frequency of each peripheral so it's passed to
peripherals'
init
functions.Drawbacks
Regression when not using RTFM
And you still want to use interrupts.
RTFM supports moving runtime initialized resources into tasks (interrupts) at zero, or very little,
cost but if you are not using RTFM then a
static
variable is required to share a peripheralbetween e.g. main and an interrupt handler. Putting a peripheral singleton in a
static
variablemeans making it globally visible which means you have global singletons again, and all their
disadvantages, but with worse performance (because an
Option
and aRefCell
are needed, seebelow) than what you get with today's API.
With this RFC:
With today's API:
Compare this to RTFM + this RFC:
No
Mutex
, noRefCell
, noOption
and no global visibility. Plus onlyidle
needs a lock toaccess
GPIOA
. Without this RFC, even with RTFM, it's possible to accessGPIOA
froman execution context that's not
idle
orexti0
due to this issue / bug / hole:japaric/cortex-m-rtfm#13.
It breaks the world
Breaking changes in
cortex-m
,svd2rust
andcortex-m-rtfm
are required to implement this.Unresolved questions
Should we have finer grained access to peripherals as in a
GPIOA::take()
that returnsOption<GPIOA>
(in addition toPeripherals::all()
-- one would invalidate the other)?The unsafe variant,
Peripherals::_all
, needs a better name.Implementation bits
Note that the implementation is a bit crude at this point. Expect bugs. Still, I'm posting now to
get feedback and to allow others to experiment.
f3
repo contains a high level API, and examples that use it, based on this RFC.cc @pftbest @thejpster @therealprof @hannobraun @nagisa @kjetilkjeka
EDIT: I realized that it may not be obvious how the RFC solves problem (a) so here's an example:
Let's say that you want to use a serial port in
idle
and in some interrupt handler. You know thatidle
only needs to use the transmitter half the serial port and the interrupt handler only needsto use the receiver part.
With all the moves in
init
you are sure that:No task or crate (dependency) can reconfigure the pins through
GPIOA
, because it (GPIOA
) wascreated (cf. the
init::Peripherals
argument ofinit
) and consumed ininit
.No task or crate (dependency) can reconfigure or drive the pins
PA9
andPA10
, because bothpins were created and consumed in
init
.No task or crate (dependency) can do serial I/O through
USART1
because it (USART1
) was createdand consumed in
init
.No task or crate (dependency) other than
idle
can do serial writes because onlyidle
hasaccess to
Tx
.No task or crate (dependency) other than
exti0
can do serial reads because onlyexti0
hasaccess to
Rx
.Without this RFC you can't have any of these guarantees because the svd2rust API lets you use
GPIOA
,USART1
in any execution context so even a functionfoo
with signaturefn()
(note:no arguments) can reconfigure the pins or start a serial operation.
The text was updated successfully, but these errors were encountered: