Skip to content

Commit 96e5a91

Browse files
bors[bot]jamesmunns
andcommitted
Merge #15
15: Peripherals Chapter r=jamesmunns a=jamesmunns * Still needs heavy expansion and editing * Move "singleton" to "peripherals" Co-authored-by: James Munns <[email protected]> Co-authored-by: James Munns <[email protected]>
2 parents daeb4b0 + 3746885 commit 96e5a91

10 files changed

+316
-8
lines changed

src/SUMMARY.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@ more information and coordination
2424
- [Panics](./start/panics.md)
2525
- [Exceptions](./start/exceptions.md)
2626
- [IO](./start/io.md)
27+
- [Peripherals](./peripherals/peripherals.md)
28+
- [A first attempt in Rust](./peripherals/a-first-attempt.md)
29+
- [The Borrow Checker](./peripherals/borrowck.md)
30+
- [Singletons](./peripherals/singletons.md)
31+
- [Peripherals in Rust](./peripherals/rusty.md)
2732
- [Static Guarantees](./static-guarantees/static-guarantees.md)
2833
<!-- TODO: Define Sections -->
2934
- [Portability](./portability/portability.md)
30-
<!-- TODO: Define Sections -->
31-
- [Singletons](./singletons/singletons.md)
32-
<!-- TODO: Define Sections -->
35+
<!-- TODO: Define more sections -->
3336
- [Concurrency](./concurrency/concurrency.md)
3437
<!-- TODO: Define Sections -->
3538
- [Collections](./collections/collections.md)

src/assets/embedded-hal.svg

Lines changed: 2 additions & 0 deletions
Loading

src/assets/nrf52-memory-map.png

140 KB
Loading
68.7 KB
Loading

src/peripherals/a-first-attempt.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# A First Attempt
2+
3+
## Arbitrary Memory Locations and Rust
4+
5+
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:
6+
7+
```rust
8+
use core::ptr;
9+
const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;
10+
11+
fn read_serial_port_speed() -> u32 {
12+
unsafe { // <-- :(
13+
ptr::read_volatile(SER_PORT_SPEED_REG)
14+
}
15+
}
16+
fn write_serial_port_speed(val: u32) {
17+
unsafe { // <-- :(
18+
ptr::write_volatile(SER_PORT_SPEED_REG, val);
19+
}
20+
}
21+
```
22+
23+
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:
24+
25+
```rust
26+
use core::ptr;
27+
28+
struct SerialPort;
29+
30+
impl SerialPort {
31+
// Private Constants (addresses)
32+
const SER_PORT_SPEED_REG: *mut u32 = 0x4000_1000 as _;
33+
34+
// Public Constants (enumerated values)
35+
pub const SER_PORT_SPEED_8MBPS: u32 = 0x8000_0000;
36+
pub const SER_PORT_SPEED_125KBPS: u32 = 0x0200_0000;
37+
38+
fn new() -> SerialPort {
39+
SerialPort
40+
}
41+
42+
fn read_speed(&self) -> u32 {
43+
unsafe {
44+
ptr::read_volatile(Self::SER_PORT_SPEED_REG)
45+
}
46+
}
47+
48+
fn write_speed(&mut self, val: u32) {
49+
unsafe {
50+
ptr::write_volatile(Self::SER_PORT_SPEED_REG, val);
51+
}
52+
}
53+
}
54+
```
55+
56+
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:
57+
58+
```rust
59+
fn do_something() {
60+
let mut serial = SerialPort::new();
61+
62+
let speed = serial.read_speed();
63+
// Do some work
64+
serial.write_speed(speed * 2);
65+
}
66+
```
67+
68+
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.
69+
70+
Consider the following example:
71+
72+
```rust
73+
fn do_something() {
74+
let mut serial = SerialPort::new();
75+
let speed = serial.read_speed();
76+
77+
// Be careful, we have to go slow!
78+
if speed != SerialPort::SER_PORT_SPEED_LO {
79+
serial.write_speed(SerialPort::SER_PORT_SPEED_LO)
80+
}
81+
// First, send some pre-data
82+
something_else();
83+
// Okay, lets send some slow data
84+
// ...
85+
}
86+
87+
fn something_else() {
88+
let mut serial = SerialPort::new();
89+
// We gotta go fast for this!
90+
serial.write_speed(SerialPort::SER_PORT_SPEED_HI);
91+
// send some data...
92+
}
93+
```
94+
95+
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.
96+
97+
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.

src/peripherals/borrowck.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Mutable Global State
2+
3+
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.
4+
5+
## What should our rules be?
6+
7+
How can we reliably interact with these peripherals?
8+
9+
1. Always use `volatile` methods to read or write to peripheral memory, as it can change at any time
10+
2. In software, we should be able to share any number of read-only accesses to these peripherals
11+
3. If some software should have read-write access to a peripheral, it should hold the only reference to that peripheral
12+
13+
## The Borrow Checker
14+
15+
The last two of these rules sound suspiciously similar to what the Borrow Checker does already!
16+
17+
Imagine if we could pass around ownership of these peripherals, or offer immutable or mutable references to them?
18+
19+
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?

src/peripherals/peripherals.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Peripherals
2+
3+
## What are Peripherals?
4+
5+
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.
6+
7+
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.
8+
9+
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.
10+
11+
## Linear and Real Memory Space
12+
13+
On a microcontroller, writing some data to an arbitrary address, such as `0x4000_0000` or `0x0000_0000`, may be a completely valid action.
14+
15+
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.
16+
17+
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.
18+
19+
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:
20+
21+
![](./../assets/nrf52-memory-map.png)
22+
23+
[Nordic nRF52832 Datasheet (pdf)]
24+
25+
## Memory Mapped Peripherals
26+
27+
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.
28+
29+
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:
30+
31+
![](./../assets/nrf52-spi-frequency-register.png)
32+
33+
[Nordic nRF52832 Datasheet (pdf)]
34+
35+
This interface is how interactions with the hardware are made, no matter what language is used, whether that language is Assembly, C, or Rust.
36+
37+
[Nordic nRF52832 Datasheet (pdf)]: http://infocenter.nordicsemi.com/pdf/nRF52832_PS_v1.1.pdf

src/peripherals/rusty.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Peripherals in Rust

0 commit comments

Comments
 (0)