Description
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
APIs
I'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
astatic
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 (seecortex_m::asm::delay
is wrong on anything more complex than an M4 #236)nop
udf
isb
/dsb
/dmb
- ITM output support (ITM ports can be set to unprivileged)
- The user-visible register accessors
apsr
,lr
,pc
- Access to the STIR.
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.
- All peripheral layouts currently named
RegisterBlock
. - All register access types (W/R proxies and friends if you're using svd2rust).
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, namely
- All code is privileged.
- Critical sections are imposed by stopping all interrupts.
These 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 in cortex-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; the
monohal
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 of raw
and intrinsics
. An
application could use monohal
and reach into raw
for particular things, but
doing so would potentially violate monohal
's safety guarantees -- though the
raw
bits are likely unsafe
so that's not surprising.
Really, I feel like the monohal
bits above belong in the cortex-m-rt
crate,
along with things like #[exception]
and other niceties for writing safe code
that make assumptions about system architecture.
NAME
vs name::RegisterBlock
Currently, if you're working in a context where take()
doesn't make sense, you
wind up dealing with types named RegisterBlock
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 have new()
operations, even unsafe ones, for getting an instance. (Yes,
you could do a steal()
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 the name::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 on SCB
.)
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 the RegisterBlock
; 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. :-)