rust-embedded / cortex-m

Low level access to Cortex-M processors
Apache License 2.0
812 stars 146 forks source link

Proposal for separating cortex-m into smaller parts #239

Open cbiffle opened 4 years ago

cbiffle commented 4 years ago

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:

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

  2. 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:

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.

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

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:

  1. It relies on the type system for safety. In general, guarantees from the type system cannot be extended to separate programs.

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

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

therealprof commented 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.

therealprof commented 3 years ago

Briefly discussed this in todays meeting. We might come back to this after the next release.

Unnominating for now.

hydra commented 3 years ago

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.

Quick-Flash commented 3 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.

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.

cbiffle commented 3 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.

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

hydra commented 3 years ago

I created a for-discussion PR #354 as another alternative to this problem which might be worth also considering.

cbiffle commented 3 years ago

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

  1. Cargo features complicate the code they control (with cfg attributes and alternate code paths).
  2. They restrict users to swapping between the options the crate maintainer considered, whereas finer-grained crates would let us swap in a different implementation of something.

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.

hydra commented 3 years ago

@cbiffle I agree with all of that.