Open cbiffle opened 4 years ago
Discussion from todays meeting: Probably best to toy around with the idea and see how that would work and if people are motivated to see it through. Also if people are interested in working on this, please speak up.
Briefly discussed this in todays meeting. We might come back to this after the next release.
Unnominating for now.
I would fully support extracting the register definitions into a separate crate. Register definitions are pretty much fixed by the hardware and the only reason to change them is due to bugs or a new cpu revision (and corresponding SVD from the vendor). Everything else in the cortex-m crate is essentially open to change for any number of reasons.
When writing very low level code, or when implementing solutions that have very specific requirements (e.g. vector tables, linker scripts, debugging requirements), much of the other stuff that the crate provides either causes problems (even when unused, as compile times are still increased), gets in the way and can cause developer confusion as to which part of the crate can/should be used, or needs working around.
If your first cut at splitting things up a bit was to just extract the register access into a separate crate and make it a dependency, whilst also allowing it to be used directly, that would be awesome.
Discussion from todays meeting: Probably best to toy around with the idea and see how that would work and if people are motivated to see it through. Also if people are interested in working on this, please speak up.
I've just started using rust for embedded and this is something I'm definitely interested in. The current implementation is monolithic and includes much that is not needed or wanted in my project. For many applications it just includes to much extra code, while for many just the registers and other low level code is what they need.
Discussion from todays meeting: Probably best to toy around with the idea and see how that would work and if people are motivated to see it through. Also if people are interested in working on this, please speak up.
I'd be interested in cooking up a prototype if there's interest from upstream. We've been using cortex-m in unprivileged isolated environments for a little over a year now, and just avoiding touching the parts that are unsound there. :-)
I created a for-discussion PR #354 as another alternative to this problem which might be worth also considering.
I may have been unclear -- my initial concerns are not that cortex-m is doing too much, but rather that portions of cortex-m make assumptions about environment and processor state that are incorrect in some cases. For instance, the use of cpsid/cpsie around critical sections is invalid if you're targeting the processor's unprivileged mode, meaning large portions of cortex-m are currently unsound when run unprivileged -- it doesn't look like that PR does anything to address that. My suggestion to divide the crate along functional lines is intended to give people finer-grained tools that they can use to draw these platform distinctions as needed.
FWIW, in this case, I prefer finer-grained crates to larger crates with Cargo features because
cfg
attributes and alternate code paths).However, in the split-crate situation I described initially, I could totally imagine having a cortex-m
facade crate with features for controlling which lower-level crates it pulled in. If the user needed something more complex than that, they'd need to directly depend on the implementation crates, including potentially ones they'd written in-house.
@cbiffle I agree with all of that.
Summary
The
cortex-m
crate provides facilities for using peripherals and special instructions on the ARM Cortex-M series. This makes it useful in a wide range of embedded applications. However, the crate also does a few other things that restrict its applicability. I would like to propose finding a seam in the crate, between its low-level generally-applicable bits and its higher-level bits, and separating it into two.Context
Most Rust applications using the
cortex-m
crate are what I would call monolithic applications: they consist of a single program, compiled/linked together, and do not use memory protection to isolate components, nor the processor's privileged/unprivileged distinction to isolate a kernel.There are (for the sake of this discussion) three other kinds of Cortex-M applications, however: those that use limited memory protection and privilege within a single linked application (FreeRTOS
CM3_MPU
port); those that isolate a kernel and run drivers in privileged mode (Tock; uCLinux and programs running atop it); and those that use memory protection to run drivers in unprivileged mode (so-called "multiserver" systems). The current APIs aren't well suited for any of these applications.As an example, consider issue #223. The memory safety properties of the current
cortex-m
API rely on critical sections, and they turn out to be void in unprivileged mode -- because the library assumes a particular method of implementing critical sections.A taxonomy of
cortex-m
APIsI've been thinking about this a lot recently, while using
cortex-m
in both the kernel and userland of an isolated multiserver operating system (not yet released). I see the following broad groups of functionality. Each of these is probably not a separate crate, to be clear, these are merely categories.The Universally Relevant
Things that make sense to use in a kernel, in userland, with or without protection, etc. Stuff in this category should include:
Operations that are reasonable in both privileged and unprivileged mode. (Note: operations that reliably trap in unprivileged mode are OK. MSR, MRS, and CPS do not trap and should be exposed only very carefully.)
Operations that do not assume a single linked program (e.g. they must not use a
static
to coordinate access to a shared resource).This list may be really short; even with MPU shenanigans, you cannot make the peripherals on the Private Peripheral Bus (which is to say, most of those defined in
cortex-m
) accessible to unprivileged code.At first glance, here are some things that belong in this level:
wfi
/wfe
, probablysev
bkpt
delay
, were it correct (see #236)nop
udf
isb
/dsb
/dmb
apsr
,lr
,pc
Peripheral Register Definitions
The address and layout of memory-mapped peripherals. This information isn't dangerous to expose to unprivileged code, because direct accesses to PPB peripherals from unprivileged code won't work. However, a system might reasonably opt to intercept the MPU or Bus faults and emulate the peripheral.
RegisterBlock
.Handy Algorithms
Pre-built code that implements the "right way" to do certain operations, such as enabling/disabling the caches or adjusting system handler priority.
Ideally, these would be as widely applicable as possible, so that people don't keep reinventing the wheel. (Particularly if the wheel is reinvented incorrectly.) Currently, they contain Concurrency-Model Dependent Bits (below) in some cases, and are tied to Opinionated Peripheral Access in others (farther below).
Concurrency-Model Dependent Bits
interrupt::free
,CriticalSection
, etc. These operations assume a particular concurrency model, namelyThese assumptions aren't general; the first fails on Tock (userland) or uCLinux, and the second fails on many real-time systems, which tend to have a few interrupt priority levels reserved as "never disable" (with attendant safety restrictions).
Opinionated Peripheral Access
take()
and friends. There's a fair amount of surface area incortex-m
devoted to controlling peripheral access and aliasing. This is useful stuff, but it only makes sense in monolithic programs, for two reasons:It relies on the type system for safety. In general, guarantees from the type system cannot be extended to separate programs.
It relies on
static
flags for mutual exclusion. In a system containing multiple separately compiled programs, every program has a taken flag, and the guarantees rapidly fall apart.Proposed crate seam
The split that makes the most sense to me has three parts. We can debate names later; here are placeholder names for the sake of discussion. I'm starting at the current level of abstraction and working down.
cortex-m-monohal
- the Opinionated Peripheral Access and Concurrency-Model Dependent Bits, which provide abstractions over the actual hardware (i.e.take()
is not a hardware operation) and only make sense in monolithic programs. This API would be fairly safe (equivalent to the currentcortex-m
surface area).cortex-m-raw
- the lower level systemsy bits that are mostly needed if you're writing a kernel or driver: Register Definitions and Handy Algorithms. This layer cannot make assumptions about privilege or concurrency model for correctness. As a result, it's going to have a lot ofunsafe
API; themonohal
above it can provide safe wrappers. This crate might get pulled into very strange unprivileged programs that want knowledge of register layouts.cortex-m-intrinsics
- all the Universally Relevant bits for accessingWFI
and the like. This would be as useful in a userland program on uCLinux as it would be in a kernel. This API would be mostly or entirely safe. (It's possible that some of the Handy Algorithms wind up here.)The
monohal
parts would be implemented in terms ofraw
andintrinsics
. An application could usemonohal
and reach intoraw
for particular things, but doing so would potentially violatemonohal
's safety guarantees -- though theraw
bits are likelyunsafe
so that's not surprising.Really, I feel like the
monohal
bits above belong in thecortex-m-rt
crate, along with things like#[exception]
and other niceties for writing safe code that make assumptions about system architecture.NAME
vsname::RegisterBlock
Currently, if you're working in a context where
take()
doesn't make sense, you wind up dealing with types namedRegisterBlock
a lot. This is kind of unfortunate, because it's verbose -- you need to always partially qualify the types (gpioa::RegisterBlock
,scb::RegisterBlock
) to have any idea what's going on. However, the conveniently named types in the monohal (e.g.SCB
) do not havenew()
operations, even unsafe ones, for getting an instance. (Yes, you could do asteal()
of the entire peripheral set and chop it down, but that seems odd.)This is also an issue because some Handy Algorithms are provided on the
NAME
types, and others on thename::RegisterBlock
types -- and the former ones are unavailable to people not using the monohal. (For instance, having a&scb::RegisterBlock
is enough for me to enable the instruction cache, but only if I'm willing to write the code myself -- the canned algorithm is onSCB
.)I bring this up because, in the split I'm proposing, the Handy Algorithms would need to get split up in many cases: a reusable core, likely
unsafe
, that can operate on theRegisterBlock
; and a safer counterpart, using critical sections and the like, in the monohal.I would also like to register a vote for distinct names, instead of having a whole bunch of types named
RegisterBlock
.ScbRegisters
would do in this case. :-)