[Roadmap 2017] Needs of no-std / embedded developers

I've been spending my time for the past two weeks exploring options for modeling memory-mapped I/O registers and have come away with some perspective on Rust's features for volatile memory which may be of value to the 2017 Roadmap.

While Rust's read_volatile/write_volatile are usable for modeling volatile memory, Rust does not provide any volatile guarantees, nor does it have rich enough modeling features to properly encapsulate volatile access in a type.

##What Does Volatile Mean? There seems to be several different interpretations on what volatile means. Java's definition is different from C/C++'s definition, and LLVM's and Rust's implementation implies (I couldn't find a formal definition) a different definition still. So I'm going to abandon all current definitions and try obtain understanding independently as it relates to bare-metal programming and embedded systems.

There are generally 3 kinds of memory that the typical microcontroller employs today: ROM, RAM, and memory-mapped I/O.

  • ROM is typically Flash memory where the program is stored so its contents are retained between power cycles. For most applications it is treated as read-only memory, only written to when updating the program. The program typically runs directly out of this Flash memory; there is no need to copy it to RAM for execution.
  • RAM is the memory used for the program's stack, heap, and mutable data. All contents are lost between power cycles.
  • Memory-mapped I/O is the memory used to interact with the hardware peripherals (UART, digital inputs/outputs, analog inputs/outputs, etc...). The fact that this is called memory is actually a misnomer. It's I/O that just happens to be exposed to the programmer as memory. In some cases it can't even store state. It is primarily used to control and acquire data from the hardware.

With RAM, the CPU, for all intents and purposes, is the only reader and writer. With memory-mapped I/O the CPU is not the only reader/writer; the memory is shared with the physical world. For example, when an electrical signal tied to one of the MCU's pins changes, the contents of the memory-mapped I/O corresponding to that pin also changes, regardless of what the CPU is doing. Because this memory's contents can change outside of the control of the CPU, the compiler should never optimize away or re-order any access to a memory-mapped I/O register.

Examples (warning: phony syntax):

#[volatile]
'static mmio: mut u32;

// If the compiler optimized away the second `x = mmio` statement, the 
// program would return the value 0 instead of the correct value 1
fn getValue() -> u32 {
    let mut x = mmio;  // CPU reads value 0
    x = mmio;          // CPU read value 1 because they physical world 
	  	       // changed the value since the previous read
    return x;
}

// The compiler may think that since it already set mmio to 1, there's
// no reason to read it back into x.  This would be incorrect because the
// physical world may have changed the contents of mmio since the CPU last
// wrote the value 1 to it.
fn getValue() -> u32 {
    mmio = 1;
    x = mmio;   
    return x;
}

(There are quite a few other scenarios, but I suspect most reading this thread are already well aware of them.)

So, my definition of volatile is "the memory's contents can change at any time outside of the control or knowledge of the CPU". Therefore, volatility is a property of the memory itself, not the way it is accessed. In fact read_volatile and write_volatile don't read or write memory any differently than any other load/store functions; they just ensure the order and inclusion of instructions is as-the-programmer-wrote-it, so the compiler shouldn't get clever about reordering or optimizing away instructions. One other way to ensure correct treatment of volatile memory is to compile code that accesses it without any optimizations (though that can have other consequences).

Volatile and Memory Safety

Adding volatile accessor functions to the programmer's toolbox does not provide any guarantee that all accesses to volatile memory are done through those functions; it is up to the programmer to get it right. read_volatile and write_volatile provide no more help for programming volatile memory than malloc and free do for dynamic memory allocation and management.

To quote @briansmith 's comment from a prior newsgroup discussion:

A huge part of the value proposition of Rust is that it uses its type system to prevent common types of bugs in error-prone aspects of programming. It seems to me that a useful constraint is "this memory must only be accessed via volatile loads and stores, not normal loads and stores"; that is, there should be some way to use types to force the exclusive use of volatile reads and writes and/or to disable the * dereferencing operator for volatile objects in favor of explicit volatile_load, volatile_store, nonvolatile_load, and nonvolatile_store methods. Yet, the current volatile load/store proposal does not take advantage of Rust's type system to help programmers avoid mistakes at all. This doesn't seem “rustic“ to me.

If we want to include volatile memory safety into Rust's definition of memory safety, we are are forced to admit that Rust is currently not safe.

Learning From Others (The D Programming Language)

Some time ago, the D programming language went through this same consideration. D Improvement Proposal 62 was probably one of the most well-written proposals the D language ever saw. It is well worth the read for anyone seriously researching this topic. To quote:

Volatility is a property of a memory location. For example, if you have a memory mapped register at 0xABCD which represents the current time as a 32bit uint, then every read from that memory will return a different result. The memory at address 0xABCD is volatile, it can change at any time and therefore does not behave like normal memory. The compiler should not optimize reads from this address. All accesses to 0xABCD can't be optimized.

If one reads the formal discussion, you'll find the following comment from Andrei Alexandrescue:

The DIP is correct in mentioning that "volatility" is a property of the memory location so it should apply to the respective variable's type, as opposed to just letting the variable have a regular type and then unsafely relying on calls to e.g. peek and poke.

D ultimately opted for volatile_load and volatile_store intrinsics not because they disagreed with the DIP 62's premise, but because of the complexity it introduces into the type system.

Walter Bright:

Volatile has caused a major increase in the complexity of the C++ type system - a complexity far out of proportion to the value it provides. It would have a similar complex and pervasive effect on the D type system.

Andre Alexandrescue:

Clearly an approach that adds a qualifier would have superior abstraction capabilities, but my experience with C++ (both specification and use) has been that the weight to power ratio of volatile is enormous.

The Purist vs The Practitioner

While I have presented a case here arguing that volatility is a characteristic of the memory itself and not its access, I have to acknowledge Walter Bright's and Andre Alexandrescue's conclusions that such a specialized feature does not justify the complexity it may introduce into the type system. This may also have been why LLVM chose to implement volatile load and store operations over a memory qualifier.

read_volatile and write_volatile do provide the tools needed to implement some abstraction (e.g. VolatileCell) over volatile memory in the same way shared_ptr, unique_ptr, etc... provide abstractions over the management of dynamically allocated memory in C++. volatile-register, Tock, Zinc, have all provided such implementations, and they do allow programmers to write excellent software for their domains. However, they leak their implementation to users in the form of get(), set() or read(), write() methods. This forces the users to trade familiar idioms like a = a + b for the much less ergonomic a.set(a.get() + b.get()) or set(a, get(a) + get(b)).

D, with its assignment operator overloading and property accessor features, can better hide function call syntax from the user, so it was more practical for D to to adopt volatile accessor functions over a volatile memory qualifier.

##Suggestion for the 2017 Roadmap If the 2017 Roadmap will include any progress in defining Rust's memory model, please revisit Rust's definition of volatile and how it can provide guarantees over read_volatile and write_volatile in the same way Rust provides guarantees over malloc, new and free, delete.

2 Likes