Skip to content

Peripherals Chapter #15

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

Merged
merged 11 commits into from
Sep 26, 2018
9 changes: 6 additions & 3 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ more information and coordination
- [Panics](./start/panics.md)
- [Exceptions](./start/exceptions.md)
- [IO](./start/io.md)
- [Peripherals](./peripherals/peripherals.md)
- [A first attempt in Rust](./peripherals/a-first-attempt.md)
- [The Borrow Checker](./peripherals/borrowck.md)
- [Singletons](./peripherals/singletons.md)
- [Peripherals in Rust](./peripherals/rusty.md)
- [Static Guarantees](./static-guarantees/static-guarantees.md)
<!-- TODO: Define Sections -->
- [Portability](./portability/portability.md)
<!-- TODO: Define Sections -->
- [Singletons](./singletons/singletons.md)
<!-- TODO: Define Sections -->
<!-- TODO: Define more sections -->
- [Concurrency](./concurrency/concurrency.md)
<!-- TODO: Define Sections -->
- [Collections](./collections/collections.md)
Expand Down
2 changes: 2 additions & 0 deletions src/assets/embedded-hal.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/nrf52-memory-map.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/nrf52-spi-frequency-register.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
97 changes: 97 additions & 0 deletions src/peripherals/a-first-attempt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# A First Attempt

## Arbitrary Memory Locations and Rust

Although Rust is capable of interacting with arbitrary memory locations, dereferencing any pointer is considered an `unsafe` operation. The most direct way to expose reading from or writing to a peripheral would look something like this:

```rust
use core::ptr;
const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;

fn read_serial_port_speed() -> u32 {
unsafe { // <-- :(
ptr::read_volatile(SER_PORT_SPEED_REG)
}
}
fn write_serial_port_speed(val: u32) {
unsafe { // <-- :(
ptr::write_volatile(SER_PORT_SPEED_REG, val);
}
}
```

Although this works, it is subjectively a little messy, so the first reaction might be to wrap these related things into a `struct` to better organize them. A second attempt could come up with something like this:

```rust
use core::ptr;

struct SerialPort;

impl SerialPort {
// Private Constants (addresses)
const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;

// Public Constants (enumerated values)
pub const SER_PORT_SPEED_8MBPS: u32 = 0x8000_0000;
pub const SER_PORT_SPEED_125KBPS: u32 = 0x0200_0000;

fn new() -> SerialPort {
SerialPort
}

fn read_speed(&self) -> u32 {
unsafe {
ptr::read_volatile(Self::SER_PORT_SPEED_REG)
}
}

fn write_speed(&mut self, val: u32) {
unsafe {
ptr::write_volatile(Self::SER_PORT_SPEED_REG, val);
}
}
}
```

And this is a little better! We've hidden that random looking memory address, and presented something that feels a little more rusty. We can even use our new interface:

```rust
fn do_something() {
let mut serial = SerialPort::new();

let speed = serial.read_speed();
// Do some work
serial.write_speed(speed * 2);
}
```

But the problem with this is that our `SerialPort` struct could be created anywhere. By creating multiple instances of `SerialPort`, we would create aliased mutable pointers, which are typically avoided in Rust.

Consider the following example:

```rust
fn do_something() {
let mut serial = SerialPort::new();
let speed = serial.read_speed();

// Be careful, we have to go slow!
if speed != SerialPort::SER_PORT_SPEED_LO {
serial.write_speed(SerialPort::SER_PORT_SPEED_LO)
}
// First, send some pre-data
something_else();
// Okay, lets send some slow data
// ...
}

fn something_else() {
let mut serial = SerialPort::new();
// We gotta go fast for this!
serial.write_speed(SerialPort::SER_PORT_SPEED_HI);
// send some data...
}
```

In this case, if we were only looking at the code in `do_something()`, we would think that we are definitely sending our serial data slowly, and would be confused why our embedded code is not working as expected.

In this case, it is easy to see where the error was introduced. However, once this code is spread out over multiple modules, drivers, developers, and days, it gets easier and easier to make these kinds of mistakes.
19 changes: 19 additions & 0 deletions src/peripherals/borrowck.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## Mutable Global State

Unfortunately, hardware is basically nothing but mutable global state, which can feel very frightening for a Rust developer. Hardware exists independently from the structures of the code we write, and can be modified at any time by the real world.

## What should our rules be?

How can we reliably interact with these peripherals?

1. Always use `volatile` methods to read or write to peripheral memory, as it can change at any time
2. In software, we should be able to share any number of read-only accesses to these peripherals
3. If some software should have read-write access to a peripheral, it should hold the only reference to that peripheral

## The Borrow Checker

The last two of these rules sound suspiciously similar to what the Borrow Checker does already!

Imagine if we could pass around ownership of these peripherals, or offer immutable or mutable references to them?

Well, we can, but for the Borrow Checker, we need to have exactly one instance of each peripheral, so Rust can handle this correctly. Well, luckliy in the hardware, there is only one instance of any given peripheral, but how can we expose that in the structure of our code?
37 changes: 37 additions & 0 deletions src/peripherals/peripherals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Peripherals

## What are Peripherals?

Most Microcontrollers have more than just a CPU, RAM, or Flash Memory - they contain sections of silicon which are used for interacting with systems outside of the microcontroller, as well as directly and indirectly interacting with their surroundings in the world via sensors, motor controllers, or human interfaces such as a display or keyboard. These components are collectively known as Peripherals.

These peripherals are useful because they allow a developer to offload processing to them, avoiding having to handle everything in software. Similar to how a desktop developer would offload graphics processing to a video card, embedded developers can offload some tasks to peripherals allowing the CPU to spend it's time doing something else important, or doing nothing in order to save power.

However, unlike graphics cards, which typically have a Software API like Vulkan, Metal, or OpenGL, peripherals are exposed to our Microcontroller with a hardware interface, which is mapped to a chunk of the memory.

## Linear and Real Memory Space

On a microcontroller, writing some data to an arbitrary address, such as `0x4000_0000` or `0x0000_0000`, may be a completely valid action.

On a desktop system, access to memory is tightly controlled by the MMU, or Memory Management Unit. This component has two major responsibilities: enforcing access permission to sections of memory (preventing one thread from reading or modifying the memory of another thread); and re-mapping segments of the physical memory to virtual memory ranges used in software. Microcontrollers do not typically have an MMU, and instead only use real physical addresses in software.

Although 32 bit microcontrollers have a real and linear address space from `0x0000_0000`, and `0xFFFF_FFFF`, they generally only use a few hundred kilobytes of that range for actual memory. This leaves a significant amount of address space remaining.

Rather than ignore that remaining space, Microcontroller designers instead mapped the interface for peripherals in certain memory locations. This ends up looking something like this:

![](./../assets/nrf52-memory-map.png)

[Nordic nRF52832 Datasheet (pdf)]

## Memory Mapped Peripherals

Interaction with these peripherals is simple at a first glance - write the right data to the correct address. For example, sending a 32 bit word over a serial port could be as direct as writing that 32 bit word to a certain memory address. The Serial Port Peripheral would then take over and send out the data automatically.

Configuration of these peripherals works similarly. Instead of calling a function to configure a peripheral, a chunk of memory is exposed which serves as the hardware API. Write `0x8000_0000` to a SPI Frequency Configuration Register, and the SPI port will send data at 8 Megabits per second. Write `0x0200_0000` to the same address, and the SPI port will send data at 125 Kilobits per second. These configuration registers look a little bit like this:

![](./../assets/nrf52-spi-frequency-register.png)

[Nordic nRF52832 Datasheet (pdf)]

This interface is how interactions with the hardware are made, no matter what language is used, whether that language is Assembly, C, or Rust.

[Nordic nRF52832 Datasheet (pdf)]: http://infocenter.nordicsemi.com/pdf/nRF52832_PS_v1.1.pdf
1 change: 1 addition & 0 deletions src/peripherals/rusty.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Peripherals in Rust
Loading