diff --git a/Cargo.toml b/Cargo.toml index 1bc81f81..219898b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,13 +4,15 @@ members = [ "mdbook/src/03-setup", "mdbook/src/05-meet-your-software", "mdbook/src/06-hello-world", - "mdbook/src/07-registers", - "mdbook/src/08-led-roulette", - "mdbook/src/10-uart", - "mdbook/src/11-i2c", - "mdbook/src/12-led-compass", - "mdbook/src/13-punch-o-meter", - "mdbook/src/14-snake-game", + "mdbook/src/07-led-roulette", + "mdbook/src/08-inputs-and-outputs", + "mdbook/src/09-registers", + "mdbook/src/11-uart", + "mdbook/src/12-i2c", + "mdbook/src/13-led-compass", + "mdbook/src/14-punch-o-meter", + "mdbook/src/15-interrupts", + "mdbook/src/16-snake-game", "mdbook/src/appendix/3-mag-calibration", "mdbook/src/serial-setup", ] diff --git a/mdbook/src/05-meet-your-software/examples/delay-print.rs b/mdbook/src/05-meet-your-software/examples/delay-print.rs index d044ee9c..fb624188 100644 --- a/mdbook/src/05-meet-your-software/examples/delay-print.rs +++ b/mdbook/src/05-meet-your-software/examples/delay-print.rs @@ -3,7 +3,6 @@ #![no_std] use cortex_m_rt::entry; -use embedded_hal::delay::DelayNs; use microbit::board::Board; use microbit::hal::timer::Timer; use panic_rtt_target as _; diff --git a/mdbook/src/07-registers/.cargo/config.toml b/mdbook/src/07-led-roulette/.cargo/config.toml similarity index 100% rename from mdbook/src/07-registers/.cargo/config.toml rename to mdbook/src/07-led-roulette/.cargo/config.toml diff --git a/mdbook/src/08-led-roulette/Cargo.toml b/mdbook/src/07-led-roulette/Cargo.toml similarity index 100% rename from mdbook/src/08-led-roulette/Cargo.toml rename to mdbook/src/07-led-roulette/Cargo.toml diff --git a/mdbook/src/07-registers/Embed.toml b/mdbook/src/07-led-roulette/Embed.toml similarity index 100% rename from mdbook/src/07-registers/Embed.toml rename to mdbook/src/07-led-roulette/Embed.toml diff --git a/mdbook/src/08-led-roulette/README.md b/mdbook/src/07-led-roulette/README.md similarity index 100% rename from mdbook/src/08-led-roulette/README.md rename to mdbook/src/07-led-roulette/README.md diff --git a/mdbook/src/08-led-roulette/examples/light-it-all.rs b/mdbook/src/07-led-roulette/examples/light-it-all.rs similarity index 100% rename from mdbook/src/08-led-roulette/examples/light-it-all.rs rename to mdbook/src/07-led-roulette/examples/light-it-all.rs diff --git a/mdbook/src/08-led-roulette/my-solution.md b/mdbook/src/07-led-roulette/my-solution.md similarity index 100% rename from mdbook/src/08-led-roulette/my-solution.md rename to mdbook/src/07-led-roulette/my-solution.md diff --git a/mdbook/src/08-led-roulette/src/main.rs b/mdbook/src/07-led-roulette/src/main.rs similarity index 100% rename from mdbook/src/08-led-roulette/src/main.rs rename to mdbook/src/07-led-roulette/src/main.rs diff --git a/mdbook/src/08-led-roulette/templates/solution.rs b/mdbook/src/07-led-roulette/templates/solution.rs similarity index 100% rename from mdbook/src/08-led-roulette/templates/solution.rs rename to mdbook/src/07-led-roulette/templates/solution.rs diff --git a/mdbook/src/08-led-roulette/the-challenge.md b/mdbook/src/07-led-roulette/the-challenge.md similarity index 100% rename from mdbook/src/08-led-roulette/the-challenge.md rename to mdbook/src/07-led-roulette/the-challenge.md diff --git a/mdbook/src/08-led-roulette/.cargo/config.toml b/mdbook/src/08-inputs-and-outputs/.cargo/config.toml similarity index 100% rename from mdbook/src/08-led-roulette/.cargo/config.toml rename to mdbook/src/08-inputs-and-outputs/.cargo/config.toml diff --git a/mdbook/src/08-inputs-and-outputs/Cargo.toml b/mdbook/src/08-inputs-and-outputs/Cargo.toml new file mode 100644 index 00000000..e8dec34e --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "inputs-and-polling" +version = "0.1.0" +edition = "2021" + +[dependencies] +microbit-v2 = "0.15.0" +cortex-m-rt = "0.7.3" +rtt-target = "0.5.0" +panic-rtt-target = "0.1.3" +embedded-hal = "1.0.0" + +[dependencies.cortex-m] +version = "0.7" +features = ["critical-section-single-core"] diff --git a/mdbook/src/14-snake-game/Embed.toml b/mdbook/src/08-inputs-and-outputs/Embed.toml similarity index 100% rename from mdbook/src/14-snake-game/Embed.toml rename to mdbook/src/08-inputs-and-outputs/Embed.toml diff --git a/mdbook/src/08-inputs-and-outputs/README.md b/mdbook/src/08-inputs-and-outputs/README.md new file mode 100644 index 00000000..a68be6c6 --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/README.md @@ -0,0 +1,17 @@ +# Inputs and Polling + +In earlier chapters, we’ve explored GPIO pins primarily as outputs—driving LEDs on and off. However, GPIO pins can also be configured as inputs, allowing your program to read signals from the physical world, like button presses or switch toggles. In this chapter, we'll learn how to read these input signals and do something useful with them. + +## Reading Button State + +The micro:bit v2 has two physical buttons, Button A and Button B, connected to GPIO pins configured as inputs. Specifically, Button A is connected to pin P0.14, and Button B to pin P0.23. (You can verify this from the official [pinmap table].) + +[pinmap table]: https://tech.microbit.org/hardware/schematic/#v2-pinmap + +Reading the state of a GPIO input involves checking whether the voltage level at the pin is high (3.3V, logic level 1) or low (0V, logic level 0). Each button on the micro:bit is connected to a pin. When the button is *not* pressed, that pin is held high; when the button is pressed, the pin is held low. + +Let's now apply this knowledge to reading the state of Button A by checking if the button is "low" (pressed). + +```rust +{{#include examples/button-a-bsp.rs}} +``` diff --git a/mdbook/src/08-inputs-and-outputs/examples/blink-held.rs b/mdbook/src/08-inputs-and-outputs/examples/blink-held.rs new file mode 100644 index 00000000..5d7ba12f --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/examples/blink-held.rs @@ -0,0 +1,94 @@ +#![no_main] +#![no_std] + +use cortex_m_rt::entry; +use embedded_hal::delay::DelayNs; +use embedded_hal::digital::{InputPin, OutputPin}; +use microbit::hal::timer::Timer; +use microbit::{hal::gpio, Board}; +use panic_rtt_target as _; +use rtt_target::rtt_init_print; + +const ON_TICKS: u16 = 25; +const OFF_TICKS: u16 = 75; + +#[derive(Clone, Copy)] +enum Light { + Lit(u16), + Unlit(u16), +} + +impl Light { + fn flip(self) -> Self { + match self { + Light::Lit(_) => Light::Unlit(OFF_TICKS), + Light::Unlit(_) => Light::Lit(ON_TICKS), + } + } + + fn tick_down(self) -> Self { + match self { + Light::Lit(ticks) => Light::Lit(ticks.max(1) - 1), + Light::Unlit(ticks) => Light::Unlit(ticks.max(1) - 1), + } + } +} + +#[derive(Clone, Copy)] +enum Indicator { + Off, + Blinking(Light), +} + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let mut timer = Timer::new(board.TIMER0); + + // Configure buttons + let mut button_a = board.buttons.button_a; + + // Configure LED (top-left LED at row1, col1) + let mut row1 = board + .display_pins + .row1 + .into_push_pull_output(gpio::Level::Low); + let _col1 = board + .display_pins + .col1 + .into_push_pull_output(gpio::Level::Low); + + let mut state = Indicator::Off; + loop { + let button_pressed = button_a.is_low().unwrap(); + match (button_pressed, state) { + // Turn indicator off when no button. + (false, _) => { + row1.set_low().unwrap(); + state = Indicator::Off; + } + // + (true, Indicator::Off) => { + row1.set_high().unwrap(); + state = Indicator::Blinking(Light::Lit(ON_TICKS)); + } + (true, Indicator::Blinking(light)) => { + match light { + Light::Lit(0) | Light::Unlit(0) => { + let light = light.flip(); + match light { + Light::Lit(_) => row1.set_high().unwrap(), + Light::Unlit(_) => row1.set_low().unwrap(), + } + state = Indicator::Blinking(light); + } + Light::Lit(_) | Light::Unlit(_) => { + state = Indicator::Blinking(light.tick_down()); + } + } + } + } + timer.delay_ms(10_u32); + } +} diff --git a/mdbook/src/08-inputs-and-outputs/examples/button-a-bsp.rs b/mdbook/src/08-inputs-and-outputs/examples/button-a-bsp.rs new file mode 100644 index 00000000..10f737c4 --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/examples/button-a-bsp.rs @@ -0,0 +1,24 @@ +#![no_main] +#![no_std] + +use cortex_m_rt::entry; +use embedded_hal::digital::InputPin; +use microbit::Board; +use panic_rtt_target as _; +use rtt_target::{rprintln, rtt_init_print}; + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + + let mut button_a = board.buttons.button_a; + + loop { + if button_a.is_low().unwrap() { + rprintln!("Button A pressed"); + } else { + rprintln!("Button A not pressed"); + } + } +} diff --git a/mdbook/src/08-inputs-and-outputs/examples/polling-led-toggle.rs b/mdbook/src/08-inputs-and-outputs/examples/polling-led-toggle.rs new file mode 100644 index 00000000..dd38bf00 --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/examples/polling-led-toggle.rs @@ -0,0 +1,47 @@ +#![no_main] +#![no_std] + +use cortex_m_rt::entry; +use embedded_hal::delay::DelayNs; +use embedded_hal::digital::{InputPin, OutputPin}; +use microbit::hal::timer::Timer; +use microbit::{hal::gpio, Board}; +use panic_rtt_target as _; +use rtt_target::rtt_init_print; + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let mut timer = Timer::new(board.TIMER0); + + // Configure buttons + let mut button_a = board.buttons.button_a; + let mut button_b = board.buttons.button_b; + + // Configure LED (top-left LED at row1, col1) + let mut row1 = board + .display_pins + .row1 + .into_push_pull_output(gpio::Level::Low); + let _col1 = board + .display_pins + .col1 + .into_push_pull_output(gpio::Level::Low); + + loop { + let on_pressed = button_a.is_low().unwrap(); + let off_pressed = button_b.is_low().unwrap(); + match (on_pressed, off_pressed) { + // Stay in current state until something is pressed. + (false, false) => (), + // Change to on state. + (false, true) => row1.set_high().unwrap(), + // Change to off state. + (true, false) => row1.set_low().unwrap(), + // Stay in current state until something is released. + (true, true) => (), + } + timer.delay_ms(10_u32); + } +} diff --git a/mdbook/src/08-inputs-and-outputs/my-solution.md b/mdbook/src/08-inputs-and-outputs/my-solution.md new file mode 100644 index 00000000..1f804a1c --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/my-solution.md @@ -0,0 +1,7 @@ +# My solution + +Here's my solution (in `src/main.rs`). Hopefully that was pretty easy. You'll soon see that simple polling like this is not very practical. + +```rust +{{#include src/main.rs}} +``` diff --git a/mdbook/src/08-inputs-and-outputs/polling-sucks.md b/mdbook/src/08-inputs-and-outputs/polling-sucks.md new file mode 100644 index 00000000..b020e744 --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/polling-sucks.md @@ -0,0 +1,84 @@ +# Polling sucks, actually + +Oh yeah, turn signals usually blink, right? How could we extend our program to blink the turn signal LED when a button is pressed. We know how to blink an LED from our Hello World program; we turn on the LED, wait for some time, and then turn it off. But how can we do this in our main loop while also checking for button presses? We could try something like this: + +```rust + loop { + if button_a.is_low().unwrap() { + // Blink left arrow + display.show(&LEFT_ARROW); + timer.delay_ms(500_u32); + display.show(&BLANK); + timer.delay_ms(500_u32); + } else if button_b.is_low().unwrap() { + // Blink right arrow + display.show(&RIGHT_ARROW); + timer.delay_ms(500_u32); + display.show(&BLANK); + timer.delay_ms(500_u32); + } else { + display.show(&BLANK); + } + timer.delay_ms(10_u32); + } +``` + +Can you see the problem? We're trying to do two things at once here: + +1. Check for button presses +2. Blink the LED + +But the processor can only do one thing at a time. If we press a button during the blink delay, the processor won't be able to respond until the delay is over and the loop starts again. As a result, we get a barely-responsive program (try for yourself and see how slow the button is). + +A "smarter" program would know that the processor isn't actually doing anything while the blink delay is running. The program could very well do other things while waiting for the delay to finish — namely, checking for button presses. + +## Superloops + +The term *superloop* in embedded systems is used to refer to a main control loop that does a bunch of things in sequence. It's the natural extension of the simple control flow we've been using so far. To handle logic that could be perceived as mutliple things happening at once, we need to be a bit more clever in how we structure the program so that we can be reasonably responsive to events. + +In the case of our turn signal program, where we want to blink the LEDs when a button is pressed, and be quick to stop blinking when the button is released, we can create a "state machine" to represent the various states of the program. We have three states for the buttons: + +1. No button is pressed +2. Button A is pressed +3. Button B is pressed + +We also have three states for the display: + +1. No LEDs are on +2. We are in the active blink state for the display (the LEDs are on) +3. We are in the inactive blink state for the display (the LEDs are off and waiting to be turned on once the blinking period is over) + +Since we need to ensure responsiveness, we have to combine these different states. To fully represent all states of our program, we would have the following: + +1. No button is pressed +2. Button A is pressed, and we are in the active blink state (the left arrow is showing on the display) +3. Button A is pressed, and we are in the inactive blink state (nothing is showing on the display) +4. Button B is pressed, and we are in the active blink state (the right arrow is showing on the display) +5. Button B is pressed, and we are in the inactive blink state (nothing is showing on the display) + +When either button is first pressed, and we transition from state (1) to either state (2) or (4), we will initialize a timer counter that counts up starting from the moment a button is pressed. When the timer reaches some threshold amount (like half a second) and the buttons are still pressed, we will then transition to state (3) or (5), respectively, and reinitialize the timer counter. When the timer again reaches some threshold amount, we will transition back to state (2) or (4), respectively. If at any time during states (2), (3), (4), or (5) we see that the button is no longer pressed, we transition back to state (1). + +Our main superloop control flow will repeatedly poll the buttons, compare our current timer counter (if we have one) to a threshold, and change states if any of the above conditions are met. + +We have implemented this superloop as a demonstration (`examples/blink-held.rs`), but with the state machine simplified only to blink an LED when button A is held. + +```rust +{{#include examples/blink-held.rs}} +``` + +This is still a bit complex. The 10ms loop delay is more +than adequate to catch button changes. + +Superloops work and are often used in embedded systems, but the programmer has to be careful to maintain a high degree of responsiveness to events. Note how our superloop program is different from the previous simple polling example. Any state transition step in the superloop as written above should take a fairly small amount of time (e.g. we no longer have delays that could block the processor for long periods of time and cause us to miss any events). It's not always easy to transform a simple polling program into a superloop where all state transitions are quick and relatively non-blocking, and in these cases, we will have the rely on alternative techniques for handling the different events being executed at the same time. + +## Concurrency + +Doing multiple things at once is called *concurrent* programming. Concurrency shows up in many places in programming, but especially in embedded systems. There's a whole host of techniques for implementing systems that interact with peripherals while maintaining a high degree of responsiveness (e.g. interrupt handling, cooperative multitasking, event queues, etc.). We'll explore some of these in later chapters. + +There is a good introduction to concurrency in an embedded context [here] that +you might read through before proceeding. + +[here]: https://docs.rust-embedded.org/book/concurrency/index.html + + +For now, let's take a deeper look into what's happening when we call `button_a.is_low()` or `display_pins.row1.set_high()`. diff --git a/mdbook/src/08-inputs-and-outputs/polling.md b/mdbook/src/08-inputs-and-outputs/polling.md new file mode 100644 index 00000000..f212f46a --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/polling.md @@ -0,0 +1,14 @@ +# Polling + +Now that we've learned how to read GPIO inputs, let's consider how we might use these reads practically. Suppose we want our program to turn on an LED when Button A is pressed and turn it off when Button B is pressed. We can do this by polling the state of both buttons in a loop, and responding accordingly when a button is read to be pressed. Here's how we might write this program: + +```rust +{{#include examples/polling-led-toggle.rs}} +``` + +This method of repeatedly checking inputs in a loop is called polling. When we check the state of some input, we say we are *polling* that input. In this case, we are polling both Button A and Button B. + +Polling is simple but allows us to do interesting things based on the external world. For all of our device's inputs, we can "poll" them in a loop, and respond to the results in some way, one by one. This kind of method is very conceptually simple and is a good starting point for many projects. We'll soon find out why polling might not be the best method for all (or even most) cases, but let's try it out first. + +**Note** "Polling" is often used on two levels of granularity. At one level, "polling" is used to refer to asking (once) what the state of an input is. At a higher level, "polling", or perhaps "polling in a loop", is used to refer to asking (repeatedly) what the state of an input is in a simple control flow like the one we used above. This kind of use of the word to refer to a control flow is used only in the simplest of programs, and seldom used in production (it's not practical as we'll soon see), so generally when embedded engineers talk about polling, they mean the former, i.e. to ask (once) what the state of an input is. + diff --git a/mdbook/src/08-inputs-and-outputs/src/main.rs b/mdbook/src/08-inputs-and-outputs/src/main.rs new file mode 100644 index 00000000..83560783 --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/src/main.rs @@ -0,0 +1,54 @@ +#![no_main] +#![no_std] + +use cortex_m_rt::entry; +use embedded_hal::digital::InputPin; +use microbit::{board::Board, display::blocking::Display, hal::Timer}; +use panic_rtt_target as _; +use rtt_target::rtt_init_print; + +// Define LED patterns +const LEFT_ARROW: [[u8; 5]; 5] = [ + [0, 0, 1, 0, 0], + [0, 1, 0, 0, 0], + [1, 1, 1, 1, 1], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], +]; + +const RIGHT_ARROW: [[u8; 5]; 5] = [ + [0, 0, 1, 0, 0], + [0, 0, 0, 1, 0], + [1, 1, 1, 1, 1], + [0, 0, 0, 1, 0], + [0, 0, 1, 0, 0], +]; + +const CENTER_LED: [[u8; 5]; 5] = [ + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], +]; + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let mut timer = Timer::new(board.TIMER0); + + let mut display = Display::new(board.display_pins); + let mut button_a = board.buttons.button_a; + let mut button_b = board.buttons.button_b; + + loop { + if button_a.is_low().unwrap() { + display.show(&mut timer, LEFT_ARROW, 10); + } else if button_b.is_low().unwrap() { + display.show(&mut timer, RIGHT_ARROW, 10); + } else { + display.show(&mut timer, CENTER_LED, 10); + } + } +} diff --git a/mdbook/src/08-inputs-and-outputs/the-challenge.md b/mdbook/src/08-inputs-and-outputs/the-challenge.md new file mode 100644 index 00000000..025edfab --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/the-challenge.md @@ -0,0 +1,15 @@ +# The challenge + +Now it’s your turn to put polling into practice. Your task is to implement a simple program that uses button polling to display directional arrows based on user input: + +- If Button A is pressed, display a left arrow (←) on the LED matrix. +- If Button B is pressed, display a right arrow (→) on the LED matrix. +- If neither button is pressed, display a single lit LED at the center of the matrix. + +You'll need to: + +- Initialize the variables for the LED and the buttons. +- Continuously poll Button A and Button B. +- Update the LED display according to the button state with a clear indication of each state (left, right, or neutral). + +I hope you don't mess up! It's *so* hard to share the road with people who don't use their turn signals properly. diff --git a/mdbook/src/10-uart/.cargo/config.toml b/mdbook/src/09-registers/.cargo/config.toml similarity index 100% rename from mdbook/src/10-uart/.cargo/config.toml rename to mdbook/src/09-registers/.cargo/config.toml diff --git a/mdbook/src/07-registers/Cargo.toml b/mdbook/src/09-registers/Cargo.toml similarity index 100% rename from mdbook/src/07-registers/Cargo.toml rename to mdbook/src/09-registers/Cargo.toml diff --git a/mdbook/src/08-led-roulette/Embed.toml b/mdbook/src/09-registers/Embed.toml similarity index 100% rename from mdbook/src/08-led-roulette/Embed.toml rename to mdbook/src/09-registers/Embed.toml diff --git a/mdbook/src/07-registers/README.md b/mdbook/src/09-registers/README.md similarity index 78% rename from mdbook/src/07-registers/README.md rename to mdbook/src/09-registers/README.md index 8469cd2d..6d818a16 100644 --- a/mdbook/src/07-registers/README.md +++ b/mdbook/src/09-registers/README.md @@ -3,13 +3,13 @@ This chapter is a technical deep-dive. You can safely [skip it] for now and come back to it later if you like. That said, there's a lot of good stuff in here, so I'd recommend you dive in. -[skip it]: ../08-led-roulette/index.html +[skip it]: ../10-serial-communication/index.html ----- -It's time to explore what calling `display_pins.row1.set_high()` does under the hood. +It's time to explore what calling `display_pins.row1.set_high()` or `button_a_pin.is_high()` does under the hood. -In a nutshell, it just writes to some special memory regions. Go into the `07-registers` directory +In a nutshell, calling `display_pins.row1.set_high()` just writes to some special memory regions. Go into the `07-registers` directory and let's run the starter code statement by statement (`src/main.rs`). ``` rust @@ -56,6 +56,8 @@ whereas a "low" (voltage) level will turn it off. These "low" and "high" states map directly to the concept of digital logic. "low" is `0` or `false` and "high" is `1` or `true`. This is why this pin configuration is known as digital output. +The opposite of a digital output is a digital input. In the same way that a digital output can be either `0` or `1`, a digital input can be either `0` or `1`. The difference is that digital outputs can drive a voltages, but digital inputs *read* a voltage. When the microcontroller reads a voltage level above a high threshold, it will interpret that as a `1` and when it reads a voltage level below a low threshold, it will interpret that as a `0`. + ----- OK. But how can one find out what this register does? Time to RTRM (Read the Reference Manual)! diff --git a/mdbook/src/07-registers/bad-address.md b/mdbook/src/09-registers/bad-address.md similarity index 100% rename from mdbook/src/07-registers/bad-address.md rename to mdbook/src/09-registers/bad-address.md diff --git a/mdbook/src/07-registers/embed.gdb b/mdbook/src/09-registers/embed.gdb similarity index 100% rename from mdbook/src/07-registers/embed.gdb rename to mdbook/src/09-registers/embed.gdb diff --git a/mdbook/src/07-registers/examples/bad.rs b/mdbook/src/09-registers/examples/bad.rs similarity index 100% rename from mdbook/src/07-registers/examples/bad.rs rename to mdbook/src/09-registers/examples/bad.rs diff --git a/mdbook/src/07-registers/examples/spooky.rs b/mdbook/src/09-registers/examples/spooky.rs similarity index 100% rename from mdbook/src/07-registers/examples/spooky.rs rename to mdbook/src/09-registers/examples/spooky.rs diff --git a/mdbook/src/07-registers/examples/type-safe.rs b/mdbook/src/09-registers/examples/type-safe.rs similarity index 100% rename from mdbook/src/07-registers/examples/type-safe.rs rename to mdbook/src/09-registers/examples/type-safe.rs diff --git a/mdbook/src/07-registers/examples/volatile.rs b/mdbook/src/09-registers/examples/volatile.rs similarity index 100% rename from mdbook/src/07-registers/examples/volatile.rs rename to mdbook/src/09-registers/examples/volatile.rs diff --git a/mdbook/src/07-registers/misoptimization.md b/mdbook/src/09-registers/misoptimization.md similarity index 100% rename from mdbook/src/07-registers/misoptimization.md rename to mdbook/src/09-registers/misoptimization.md diff --git a/mdbook/src/07-registers/rtrm.md b/mdbook/src/09-registers/rtrm.md similarity index 100% rename from mdbook/src/07-registers/rtrm.md rename to mdbook/src/09-registers/rtrm.md diff --git a/mdbook/src/07-registers/spooky-action-at-a-distance.md b/mdbook/src/09-registers/spooky-action-at-a-distance.md similarity index 100% rename from mdbook/src/07-registers/spooky-action-at-a-distance.md rename to mdbook/src/09-registers/spooky-action-at-a-distance.md diff --git a/mdbook/src/07-registers/src/lib.rs b/mdbook/src/09-registers/src/lib.rs similarity index 100% rename from mdbook/src/07-registers/src/lib.rs rename to mdbook/src/09-registers/src/lib.rs diff --git a/mdbook/src/07-registers/src/main.rs b/mdbook/src/09-registers/src/main.rs similarity index 100% rename from mdbook/src/07-registers/src/main.rs rename to mdbook/src/09-registers/src/main.rs diff --git a/mdbook/src/07-registers/type-safe-manipulation.md b/mdbook/src/09-registers/type-safe-manipulation.md similarity index 100% rename from mdbook/src/07-registers/type-safe-manipulation.md rename to mdbook/src/09-registers/type-safe-manipulation.md diff --git a/mdbook/src/09-serial-communication/README.md b/mdbook/src/10-serial-communication/README.md similarity index 100% rename from mdbook/src/09-serial-communication/README.md rename to mdbook/src/10-serial-communication/README.md diff --git a/mdbook/src/09-serial-communication/nix-tooling.md b/mdbook/src/10-serial-communication/nix-tooling.md similarity index 100% rename from mdbook/src/09-serial-communication/nix-tooling.md rename to mdbook/src/10-serial-communication/nix-tooling.md diff --git a/mdbook/src/09-serial-communication/windows-tooling.md b/mdbook/src/10-serial-communication/windows-tooling.md similarity index 100% rename from mdbook/src/09-serial-communication/windows-tooling.md rename to mdbook/src/10-serial-communication/windows-tooling.md diff --git a/mdbook/src/11-i2c/.cargo/config.toml b/mdbook/src/11-uart/.cargo/config.toml similarity index 100% rename from mdbook/src/11-i2c/.cargo/config.toml rename to mdbook/src/11-uart/.cargo/config.toml diff --git a/mdbook/src/10-uart/Cargo.toml b/mdbook/src/11-uart/Cargo.toml similarity index 100% rename from mdbook/src/10-uart/Cargo.toml rename to mdbook/src/11-uart/Cargo.toml diff --git a/mdbook/src/10-uart/Embed.toml b/mdbook/src/11-uart/Embed.toml similarity index 100% rename from mdbook/src/10-uart/Embed.toml rename to mdbook/src/11-uart/Embed.toml diff --git a/mdbook/src/10-uart/README.md b/mdbook/src/11-uart/README.md similarity index 100% rename from mdbook/src/10-uart/README.md rename to mdbook/src/11-uart/README.md diff --git a/mdbook/src/10-uart/echo-server.md b/mdbook/src/11-uart/echo-server.md similarity index 100% rename from mdbook/src/10-uart/echo-server.md rename to mdbook/src/11-uart/echo-server.md diff --git a/mdbook/src/10-uart/examples/naive-send-string.rs b/mdbook/src/11-uart/examples/naive-send-string.rs similarity index 100% rename from mdbook/src/10-uart/examples/naive-send-string.rs rename to mdbook/src/11-uart/examples/naive-send-string.rs diff --git a/mdbook/src/10-uart/examples/receive-byte.rs b/mdbook/src/11-uart/examples/receive-byte.rs similarity index 100% rename from mdbook/src/10-uart/examples/receive-byte.rs rename to mdbook/src/11-uart/examples/receive-byte.rs diff --git a/mdbook/src/10-uart/examples/send-byte.rs b/mdbook/src/11-uart/examples/send-byte.rs similarity index 100% rename from mdbook/src/10-uart/examples/send-byte.rs rename to mdbook/src/11-uart/examples/send-byte.rs diff --git a/mdbook/src/10-uart/examples/send-string.rs b/mdbook/src/11-uart/examples/send-string.rs similarity index 100% rename from mdbook/src/10-uart/examples/send-string.rs rename to mdbook/src/11-uart/examples/send-string.rs diff --git a/mdbook/src/10-uart/my-solution.md b/mdbook/src/11-uart/my-solution.md similarity index 100% rename from mdbook/src/10-uart/my-solution.md rename to mdbook/src/11-uart/my-solution.md diff --git a/mdbook/src/10-uart/naive-approach-write.md b/mdbook/src/11-uart/naive-approach-write.md similarity index 100% rename from mdbook/src/10-uart/naive-approach-write.md rename to mdbook/src/11-uart/naive-approach-write.md diff --git a/mdbook/src/10-uart/receive-a-single-byte.md b/mdbook/src/11-uart/receive-a-single-byte.md similarity index 100% rename from mdbook/src/10-uart/receive-a-single-byte.md rename to mdbook/src/11-uart/receive-a-single-byte.md diff --git a/mdbook/src/10-uart/reverse-a-string.md b/mdbook/src/11-uart/reverse-a-string.md similarity index 100% rename from mdbook/src/10-uart/reverse-a-string.md rename to mdbook/src/11-uart/reverse-a-string.md diff --git a/mdbook/src/10-uart/send-a-single-byte.md b/mdbook/src/11-uart/send-a-single-byte.md similarity index 100% rename from mdbook/src/10-uart/send-a-single-byte.md rename to mdbook/src/11-uart/send-a-single-byte.md diff --git a/mdbook/src/10-uart/send-a-string.md b/mdbook/src/11-uart/send-a-string.md similarity index 100% rename from mdbook/src/10-uart/send-a-string.md rename to mdbook/src/11-uart/send-a-string.md diff --git a/mdbook/src/10-uart/src/main.rs b/mdbook/src/11-uart/src/main.rs similarity index 100% rename from mdbook/src/10-uart/src/main.rs rename to mdbook/src/11-uart/src/main.rs diff --git a/mdbook/src/12-led-compass/.cargo/config.toml b/mdbook/src/12-i2c/.cargo/config.toml similarity index 100% rename from mdbook/src/12-led-compass/.cargo/config.toml rename to mdbook/src/12-i2c/.cargo/config.toml diff --git a/mdbook/src/11-i2c/Cargo.toml b/mdbook/src/12-i2c/Cargo.toml similarity index 100% rename from mdbook/src/11-i2c/Cargo.toml rename to mdbook/src/12-i2c/Cargo.toml diff --git a/mdbook/src/11-i2c/Embed.toml b/mdbook/src/12-i2c/Embed.toml similarity index 100% rename from mdbook/src/11-i2c/Embed.toml rename to mdbook/src/12-i2c/Embed.toml diff --git a/mdbook/src/11-i2c/README.md b/mdbook/src/12-i2c/README.md similarity index 100% rename from mdbook/src/11-i2c/README.md rename to mdbook/src/12-i2c/README.md diff --git a/mdbook/src/11-i2c/examples/chip-id.rs b/mdbook/src/12-i2c/examples/chip-id.rs similarity index 100% rename from mdbook/src/11-i2c/examples/chip-id.rs rename to mdbook/src/12-i2c/examples/chip-id.rs diff --git a/mdbook/src/11-i2c/examples/show-accel.rs b/mdbook/src/12-i2c/examples/show-accel.rs similarity index 100% rename from mdbook/src/11-i2c/examples/show-accel.rs rename to mdbook/src/12-i2c/examples/show-accel.rs diff --git a/mdbook/src/11-i2c/lsm303agr.md b/mdbook/src/12-i2c/lsm303agr.md similarity index 100% rename from mdbook/src/11-i2c/lsm303agr.md rename to mdbook/src/12-i2c/lsm303agr.md diff --git a/mdbook/src/11-i2c/my-solution.md b/mdbook/src/12-i2c/my-solution.md similarity index 100% rename from mdbook/src/11-i2c/my-solution.md rename to mdbook/src/12-i2c/my-solution.md diff --git a/mdbook/src/11-i2c/read-a-single-register.md b/mdbook/src/12-i2c/read-a-single-register.md similarity index 100% rename from mdbook/src/11-i2c/read-a-single-register.md rename to mdbook/src/12-i2c/read-a-single-register.md diff --git a/mdbook/src/11-i2c/src/main.rs b/mdbook/src/12-i2c/src/main.rs similarity index 100% rename from mdbook/src/11-i2c/src/main.rs rename to mdbook/src/12-i2c/src/main.rs diff --git a/mdbook/src/11-i2c/the-challenge.md b/mdbook/src/12-i2c/the-challenge.md similarity index 62% rename from mdbook/src/11-i2c/the-challenge.md rename to mdbook/src/12-i2c/the-challenge.md index d4ae6e1c..5d4021c3 100644 --- a/mdbook/src/11-i2c/the-challenge.md +++ b/mdbook/src/12-i2c/the-challenge.md @@ -1,11 +1,13 @@ # The challenge -The challenge for this chapter is, to build a small application that communicates with the outside -world via the serial interface introduced in the last chapter. It should be able to receive the -commands "mag" for magnetometer as well as "acc" for accelerometer. It should then print the -corresponding sensor data to the serial port in response. This time no template code will be -provided since all you need is already provided in the [UART](../10-uart/index.html) and this -chapter. However, here are a few clues: +The challenge for this chapter is to build a small application that communicates with the outside +world via the serial interface introduced in the last chapter. It should expect to receive the +commands "mag" for magnetometer as well as "acc" for accelerometer from the serial port. It should +then be able to send the corresponding sensor data to the serial port in response. + +This time no template code will be provided since all you need is already provided in the +[UART](../11-uart/index.html) and this chapter. However, here are a few clues: - You might be interested in `core::str::from_utf8` to convert the bytes in the buffer to a `&str`, since we need to compare with `"mag"` and `"acc"`. + - You will have to read the documentation for the magnetometer API and functionality. While the `lsm303agr` crate provides the API interface, the [LSM303AGR datasheet](https://www.st.com/resource/en/datasheet/lsm303agr.pdf) details the sensor's magnetic field measurement parameters. See pages 13-15 for sensor characteristics and, importantly, pages 66-67 for the output register format. diff --git a/mdbook/src/11-i2c/the-general-protocol.md b/mdbook/src/12-i2c/the-general-protocol.md similarity index 100% rename from mdbook/src/11-i2c/the-general-protocol.md rename to mdbook/src/12-i2c/the-general-protocol.md diff --git a/mdbook/src/11-i2c/using-a-driver.md b/mdbook/src/12-i2c/using-a-driver.md similarity index 100% rename from mdbook/src/11-i2c/using-a-driver.md rename to mdbook/src/12-i2c/using-a-driver.md diff --git a/mdbook/src/13-punch-o-meter/.cargo/config.toml b/mdbook/src/13-led-compass/.cargo/config.toml similarity index 100% rename from mdbook/src/13-punch-o-meter/.cargo/config.toml rename to mdbook/src/13-led-compass/.cargo/config.toml diff --git a/mdbook/src/12-led-compass/Cargo.toml b/mdbook/src/13-led-compass/Cargo.toml similarity index 100% rename from mdbook/src/12-led-compass/Cargo.toml rename to mdbook/src/13-led-compass/Cargo.toml diff --git a/mdbook/src/12-led-compass/Embed.toml b/mdbook/src/13-led-compass/Embed.toml similarity index 100% rename from mdbook/src/12-led-compass/Embed.toml rename to mdbook/src/13-led-compass/Embed.toml diff --git a/mdbook/src/12-led-compass/README.md b/mdbook/src/13-led-compass/README.md similarity index 97% rename from mdbook/src/12-led-compass/README.md rename to mdbook/src/13-led-compass/README.md index 4df7beec..9a4a54d3 100644 --- a/mdbook/src/12-led-compass/README.md +++ b/mdbook/src/13-led-compass/README.md @@ -20,7 +20,7 @@ field strengths are components of the magnetic field vector.

You should already be able to write a program that continuously prints the magnetometer data on the -RTT console from the [I2C chapter](../11-i2c/index.md). After you write that program +RTT console from the [I2C chapter](../12-i2c/index.md). After you write that program (`examples/show-mag.rs`), locate where north is at your current location. Then line up your micro:bit with that direction and observe how the sensor's X and Y measurements look. @@ -35,7 +35,7 @@ you see this time? Then rotate it 90 degrees again. What values do you see? > a lot for an introductory guide to embedded systems. If you have only one MB2 and it doesn't seem > to be working, you may just want to skip to the [next chapter]. Cheap hardware: whatcha gonna do? -[next chapter]: ../13-punch-o-meter/index.html +[next chapter]: ../14-punch-o-meter/index.html The Earth's magnetic north is a fickle thing: it differs from true north in most places on Earth, sometimes substantially. It can point down into the ground quite a bit. It changes over time. diff --git a/mdbook/src/12-led-compass/examples/magnitude.rs b/mdbook/src/13-led-compass/examples/magnitude.rs similarity index 100% rename from mdbook/src/12-led-compass/examples/magnitude.rs rename to mdbook/src/13-led-compass/examples/magnitude.rs diff --git a/mdbook/src/12-led-compass/examples/show-mag.rs b/mdbook/src/13-led-compass/examples/show-mag.rs similarity index 100% rename from mdbook/src/12-led-compass/examples/show-mag.rs rename to mdbook/src/13-led-compass/examples/show-mag.rs diff --git a/mdbook/src/12-led-compass/magnitude.md b/mdbook/src/13-led-compass/magnitude.md similarity index 100% rename from mdbook/src/12-led-compass/magnitude.md rename to mdbook/src/13-led-compass/magnitude.md diff --git a/mdbook/src/12-led-compass/my-solution.md b/mdbook/src/13-led-compass/my-solution.md similarity index 100% rename from mdbook/src/12-led-compass/my-solution.md rename to mdbook/src/13-led-compass/my-solution.md diff --git a/mdbook/src/12-led-compass/src/main.rs b/mdbook/src/13-led-compass/src/main.rs similarity index 100% rename from mdbook/src/12-led-compass/src/main.rs rename to mdbook/src/13-led-compass/src/main.rs diff --git a/mdbook/src/12-led-compass/templates/compass.rs b/mdbook/src/13-led-compass/templates/compass.rs similarity index 100% rename from mdbook/src/12-led-compass/templates/compass.rs rename to mdbook/src/13-led-compass/templates/compass.rs diff --git a/mdbook/src/12-led-compass/the-challenge.md b/mdbook/src/13-led-compass/the-challenge.md similarity index 100% rename from mdbook/src/12-led-compass/the-challenge.md rename to mdbook/src/13-led-compass/the-challenge.md diff --git a/mdbook/src/14-snake-game/.cargo/config.toml b/mdbook/src/14-punch-o-meter/.cargo/config.toml similarity index 100% rename from mdbook/src/14-snake-game/.cargo/config.toml rename to mdbook/src/14-punch-o-meter/.cargo/config.toml diff --git a/mdbook/src/13-punch-o-meter/Cargo.toml b/mdbook/src/14-punch-o-meter/Cargo.toml similarity index 100% rename from mdbook/src/13-punch-o-meter/Cargo.toml rename to mdbook/src/14-punch-o-meter/Cargo.toml diff --git a/mdbook/src/13-punch-o-meter/Embed.toml b/mdbook/src/14-punch-o-meter/Embed.toml similarity index 100% rename from mdbook/src/13-punch-o-meter/Embed.toml rename to mdbook/src/14-punch-o-meter/Embed.toml diff --git a/mdbook/src/13-punch-o-meter/README.md b/mdbook/src/14-punch-o-meter/README.md similarity index 100% rename from mdbook/src/13-punch-o-meter/README.md rename to mdbook/src/14-punch-o-meter/README.md diff --git a/mdbook/src/13-punch-o-meter/examples/show-accel.rs b/mdbook/src/14-punch-o-meter/examples/show-accel.rs similarity index 100% rename from mdbook/src/13-punch-o-meter/examples/show-accel.rs rename to mdbook/src/14-punch-o-meter/examples/show-accel.rs diff --git a/mdbook/src/13-punch-o-meter/gravity-is-up.md b/mdbook/src/14-punch-o-meter/gravity-is-up.md similarity index 96% rename from mdbook/src/13-punch-o-meter/gravity-is-up.md rename to mdbook/src/14-punch-o-meter/gravity-is-up.md index b19892e1..739e7635 100644 --- a/mdbook/src/13-punch-o-meter/gravity-is-up.md +++ b/mdbook/src/14-punch-o-meter/gravity-is-up.md @@ -5,7 +5,7 @@ What's the first thing we'll do? Perform a sanity check! You should already be able to write a program that continuously prints the accelerometer data on the -RTT console from the [I2C chapter](../11-i2c/index.md). Mine is in `examples/show-accel.rs`. Do you +RTT console from the [I2C chapter](../12-i2c/index.md). Mine is in `examples/show-accel.rs`. Do you observe something interesting even when holding the board parallel to the floor with the back side facing up? (Remember that the accelerometer is mounted on the back of the board, so holding it upside-down like this makes the Z axis point up.) diff --git a/mdbook/src/13-punch-o-meter/my-solution.md b/mdbook/src/14-punch-o-meter/my-solution.md similarity index 100% rename from mdbook/src/13-punch-o-meter/my-solution.md rename to mdbook/src/14-punch-o-meter/my-solution.md diff --git a/mdbook/src/13-punch-o-meter/src/main.rs b/mdbook/src/14-punch-o-meter/src/main.rs similarity index 100% rename from mdbook/src/13-punch-o-meter/src/main.rs rename to mdbook/src/14-punch-o-meter/src/main.rs diff --git a/mdbook/src/13-punch-o-meter/the-challenge.md b/mdbook/src/14-punch-o-meter/the-challenge.md similarity index 100% rename from mdbook/src/13-punch-o-meter/the-challenge.md rename to mdbook/src/14-punch-o-meter/the-challenge.md diff --git a/mdbook/src/15-interrupts/.cargo/config.toml b/mdbook/src/15-interrupts/.cargo/config.toml new file mode 100644 index 00000000..e4ad7f5a --- /dev/null +++ b/mdbook/src/15-interrupts/.cargo/config.toml @@ -0,0 +1,8 @@ +[build] +target = "thumbv7em-none-eabihf" + +[target.thumbv7em-none-eabihf] +runner = "probe-rs run --chip nRF52833_xxAA" +rustflags = [ + "-C", "linker=rust-lld", +] diff --git a/mdbook/src/15-interrupts/Cargo.toml b/mdbook/src/15-interrupts/Cargo.toml new file mode 100644 index 00000000..3687249d --- /dev/null +++ b/mdbook/src/15-interrupts/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "interrupts" +version = "0.1.0" +edition = "2021" + +[dependencies] +cortex-m-rt = "0.7" +critical-section = "1" +critical-section-lock-mut = "0.1" +embedded-hal = "1.0.0" +microbit-v2 = "0.15" +panic-rtt-target = "0.1" +rtt-target = "0.5" + + +[dependencies.cortex-m] +version = "0.7" +features = ["critical-section-single-core"] diff --git a/mdbook/src/15-interrupts/Embed.toml b/mdbook/src/15-interrupts/Embed.toml new file mode 100644 index 00000000..21950130 --- /dev/null +++ b/mdbook/src/15-interrupts/Embed.toml @@ -0,0 +1,11 @@ +[default.general] +chip = "nrf52833_xxAA" # micro:bit V2 + +[default.reset] +halt_afterwards = false + +[default.rtt] +enabled = true + +[default.gdb] +enabled = false diff --git a/mdbook/src/15-interrupts/README.md b/mdbook/src/15-interrupts/README.md new file mode 100644 index 00000000..7b23f39a --- /dev/null +++ b/mdbook/src/15-interrupts/README.md @@ -0,0 +1,74 @@ +## Interrupts + +So far, we've touched a bunch of hardware on the MB2. We've read out buttons, waited for timers, done serial communication, and talked to devices using I2C. Each of these things involved waiting for one or more peripherals to become ready. So far, our waiting was by "polling": repeatedly asking the peripheral if it's done yet, until it is. + +Seeing as our microcontroller only has a single CPU core, it cannot do anything else while it waits. On top of that, a CPU core continuously polling a peripheral wastes power, and in a lot of applications, we can't have that. Can we do better? + +Luckily, we can! While our little microcontroller can't compute things in parallel, it can easily switch between different tasks during execution, responding to events from the outside world. This switching is done using a feature called "interrupts". + +Interrupts are aptly named: they allow peripherals to actually interrupt the core program execution at any point in time. On our MB2's nRF52833, peripherals are connected to the core's Nested Vectored Interrupt Controller (NVIC). The NVIC can stop the CPU in its tracks, instruct it to go do something else, and once that's done, get the CPU back to what it was doing before it was interrupted. We'll cover the Nested and Vectored parts of the interrupt controller later: let's first focus on how the core switches tasks. + +### Handling Interrupts + +The model of computation used by our NRF52833 is the one used by almost every modern CPU. Inside the CPU are "scratch-pad" storage locations known as "CPU registers". (Confusingly, these CPU registers are different from the "device registers" we discussed earlier in the [Registers] chapter.) To carry out a computation, the CPU typically loads values from memory to CPU registers, performs the computation using the register values, then stores the result back to memory. (This is known as a "load-store architecture".) + +Everything about the computation the CPU is currently running is stored in the CPU registers. If the core is going to switch tasks, it must store the contents of the CPU registers somewhere so that the new task can use the registers as its own scratch-pad. When the new task is complete the CPU can then restore the register values and restart the old computation. Sure enough, that is exactly the first thing the core does in response to an interrupt request: it stops what it's doing immediately and stores the contents of the CPU registers on the stack. + +The next step is actually jumping to the code that should be run in response to an interrupt. An Interrupt Service Routines (ISR), often referred to as an interrupt "handler", is a special function in your application code that gets called by the core in response to interrupts. An "interrupt table" in memory contains an "interrupt vector" for every possible interrupt: the interrupt vector indicates what ISR to call when a specific interrupt is received. We describe the details of ISR vectoring in the [NVIC and Interrupt Priority] section. + +An ISR function "returns" using a special return-from-interrupt machine instruction that causes the CPU to restore the CPU registers and jump back to where it was before the ISR was called. + +## Poke The MB2 + +Let's define an ISR and configure an interrupt to "poke" the MB2 when Button A is pressed +(`examples/poke.rs`). The board will respond by saying "ouch" and panicking. + +```rust +{{#include examples/poke.rs}} +``` + +The ISR handler function is "special". The name `GPIOTE` is required here, indicating +that this ISR should be stored at the entry for the `GPIOTE` interrupt in the interrupt table. + +The `#[interrupt]` decoration is used at compile time to mark a function to be treated specially as +an ISR. (This is a "proc macro", in case you feel like exploring that concept.) + +Marking a function with `#[interrupt]` implies several special things about the function: + +* The compiler will check that the function takes no arguments and returns no value. The CPU has no + arguments to provide to an ISR, and no place to put a return value from the ISR. + +* The compiler will place a vector to this function at the location in the interrupt table + implied by the function's name. + +* The function will be compiled to finishing by using a return-from-interrupt instruction rather + than the normal function return instruction. + +* Since the function finishes in a non-standard way, the compiler will understand not to allow + directly calling the ISR from normal code. + +There are two steps to configure the interrupt. First, the GPIOTE must be set up to generate an +interrupt when the pin connected to Button A goes from high to low voltage. Second, the NVIC must be +configured to allow the interrupt. Order matters a bit: doing things in the "wrong" order may +generate an interrupt before you are ready to handle it. + +**Note** As with most microcontrollers, there is a lot of flexibility in when the GPIOTE can generate an interrupt. Interrupts can be generated on low-to-high pin transition, high-to-low (as here), any change ("edge"), when low, or when high. On the nRF52833, interrupts generate an event that must be manually cleared in the ISR to ensure that the ISR is not called a second time for the same interrupt. Other microcontrollers may work a little differently — you should read Rust crate and microcontroller documentation to understand the details on a different board. + +When you push the A Button, you will see an "ouch" message and then a panic. Why does the interrupt +handler call `panic!()`? Try commenting the `panic!()` call out and see what happens when you push +the button. You will see "ouch" messages scroll off the screen. The NVIC records when an interrupt +has been issued: that "event" is kept until it is explicitly cleared by the running program. Without +the `panic!()`, when the interrupt handler returns the NVIC will (in this case) re-enable the +interrupt, notice that there is still an interrupt event pending, and run the handler again. This +will continue forever: each time the interrupt handler returns it will be called again. As we will +see in a bit, the interrupt indication can be cleared from within the interrupt handler using the +`reset_event()` peripheral method. + +You may define ISRs for many different interrupt sources: when I2C is ready, when a timer expires, +and on and on. Inside an ISR you can do pretty much anything you want, but it's good practice to +keep the interrupt handlers short and quick. + +Normally, once an ISR is complete the main program continues running just as it would have if the interrupt had not happened. This is a bit of a problem, though: how does your application notice that the ISR has run and done things? Seeing as an ISR doesn't have any input parameters or result, how can ISR code interact with application code? + +[NVIC and Interrupt Priority]: nvic-and-interrupt-priority.html +[Registers]: ../09-registers/index.html diff --git a/mdbook/src/15-interrupts/addendum-pwm.md b/mdbook/src/15-interrupts/addendum-pwm.md new file mode 100644 index 00000000..8f95536c --- /dev/null +++ b/mdbook/src/15-interrupts/addendum-pwm.md @@ -0,0 +1,33 @@ +# Addendum: PWM + +One last note before we move on. + +Interrupts are kind of expensive. The processor must finish +or abort the currently-running instruction, then save enough +state to restart execution, then call an interrupt +handler. All this takes a few CPU cycles of precious +runtime. + +The way the solution of the previous section is written, it +will take two interrupts per cycle of speaker output. That's +something like 1000 interrupts per second. On a processor +like our nRF52833, that works fine. + +The nRF52833 does have an on-board peripheral that could cut +our siren's interrupt rate way down. The Pulse-Width +Modulation (PWM) unit can, among other things, generate +cycles on the speaker pin at a rate controlled by a PWM +register. This could be used to generate the basic square +wave used for our siren. We would still need an interrupt +every time we wanted to change the frequency, but this might +be more like 10 interrupts per second than 1000. + +I did not use the PWM unit in my solution. This was partly +because I wanted to focus on interrupts. Another big reason, +though, was that the nRF52833 PWM unit is pretty complicated +and hard to understand. Getting something working a simple +way in the tight bare-metal environment is always +attractive. + +If you are up for a challenge, I would encourage you to try +using the PWM unit for your siren. diff --git a/mdbook/src/15-interrupts/debouncing.md b/mdbook/src/15-interrupts/debouncing.md new file mode 100644 index 00000000..1a3ca197 --- /dev/null +++ b/mdbook/src/15-interrupts/debouncing.md @@ -0,0 +1,32 @@ +## Debouncing + +As I mentioned in the last section, hardware can be a little… special. This is definitely the case +for the buttons on the MB2, and really for almost any pushbutton or switch in almost any system. If +you are seeing several interrupts for a single keypress, it is probably the result of what is known +as switch "bouncing". This is literally what the name implies: as the electrical contacts of the +switch come together, they may bounce apart and then recontact several times rather quickly before +establishing a solid connection. Unfortunately, our microprocessor is *very* fast by mechanical +standards: each one of these bounces makes a new interrupt. + +To "debounce" the switch, you need to *not* process button press interrupts for a short time after +you receive one. 50-100ms is typically a good debounce interval. Debounce timing seems hard: you +definitely don't want to spin in an interrupt handler, and yet it would be hard to deal with this in +the main program. + +The solution comes through another form of hardware concurrency: the `TIMER` peripheral we have used +a bunch already. You can set the timer when a "good" button interrupt is received, and not respond +to further interrupts for that button until the timer peripheral has counted enough time off. The +timers in `nrf-hal` come configured with a 32-bit count value and a "tick rate" of 1 MHz: a million +ticks per second. For a 100ms debounce, just let the timer count off 100,000 ticks. Anytime the +button interrupt handler sees that the timer is running, it can just do nothing. + +The implementation of all this can be seen in the next example (`examples/count-debounce.rs`). When +you run the example you should see one count per button press. + +```rust +{{#include examples/count-debounce.rs}} +``` + +**NOTE** The buttons on the MB2 are a little fiddly: it's pretty easy to push one down enough to +feel a "click" but not enough to actually make contact with the switch. I recommend using a +fingernail to press the button when testing. diff --git a/mdbook/src/15-interrupts/examples/count-bounce.rs b/mdbook/src/15-interrupts/examples/count-bounce.rs new file mode 100644 index 00000000..527c8a6f --- /dev/null +++ b/mdbook/src/15-interrupts/examples/count-bounce.rs @@ -0,0 +1,58 @@ +#![no_main] +#![no_std] + +use core::sync::atomic::{AtomicUsize, Ordering::{Acquire, AcqRel}}; + +use cortex_m::asm; +use cortex_m_rt::entry; +use critical_section_lock_mut::LockMut; +use panic_rtt_target as _; +use rtt_target::{rprintln, rtt_init_print}; + +use microbit::{ + Board, + hal::{ + gpiote, + pac::{self, interrupt}, + }, +}; + +static COUNTER: AtomicUsize = AtomicUsize::new(0); +static GPIOTE_PERIPHERAL: LockMut = LockMut::new(); + +#[interrupt] +fn GPIOTE() { + let _ = COUNTER.fetch_add(1, AcqRel); + GPIOTE_PERIPHERAL.with_lock(|gpiote| { + gpiote.channel0().reset_events(); + }); +} + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let button_a = board.buttons.button_a.into_floating_input(); + + // Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO + // wire goes low). + let gpiote = gpiote::Gpiote::new(board.GPIOTE); + let channel = gpiote.channel0(); + channel + .input_pin(&button_a.degrade()) + .hi_to_lo() + .enable_interrupt(); + channel.reset_events(); + GPIOTE_PERIPHERAL.init(gpiote); + + // Set up the NVIC to handle GPIO interrupts. + unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) }; + pac::NVIC::unpend(pac::Interrupt::GPIOTE); + + loop { + // "wait for interrupt": CPU goes to sleep until an interrupt. + asm::wfi(); + let count = COUNTER.load(Acquire); + rprintln!("ouch {}", count); + } +} diff --git a/mdbook/src/15-interrupts/examples/count-debounce.rs b/mdbook/src/15-interrupts/examples/count-debounce.rs new file mode 100644 index 00000000..780bf8eb --- /dev/null +++ b/mdbook/src/15-interrupts/examples/count-debounce.rs @@ -0,0 +1,80 @@ +#![no_main] +#![no_std] + +use core::sync::atomic::{ + AtomicUsize, + Ordering::{AcqRel, Acquire}, +}; + +use cortex_m::asm; +use cortex_m_rt::entry; +use critical_section_lock_mut::LockMut; +use panic_rtt_target as _; +use rtt_target::{rprintln, rtt_init_print}; + +use microbit::{ + hal::{ + self, gpiote, + pac::{self, interrupt}, + }, + Board, +}; + +static COUNTER: AtomicUsize = AtomicUsize::new(0); +static GPIOTE_PERIPHERAL: LockMut = LockMut::new(); +static DEBOUNCE_TIMER: LockMut> = LockMut::new(); + +// 100ms at 1MHz count rate. +const DEBOUNCE_TIME: u32 = 100 * 1_000_000 / 1000; + +#[interrupt] +fn GPIOTE() { + DEBOUNCE_TIMER.with_lock(|debounce_timer| { + if debounce_timer.read() == 0 { + let _ = COUNTER.fetch_add(1, AcqRel); + debounce_timer.start(DEBOUNCE_TIME); + } + }); + GPIOTE_PERIPHERAL.with_lock(|gpiote| { + gpiote.channel0().reset_events(); + }); +} + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let button_a = board.buttons.button_a.into_floating_input(); + + // Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO + // wire goes low). + let gpiote = gpiote::Gpiote::new(board.GPIOTE); + let channel = gpiote.channel0(); + channel + .input_pin(&button_a.degrade()) + .hi_to_lo() + .enable_interrupt(); + channel.reset_events(); + GPIOTE_PERIPHERAL.init(gpiote); + + // Set up the debounce timer. + let mut debounce_timer = hal::Timer::new(board.TIMER0); + debounce_timer.disable_interrupt(); + debounce_timer.reset_event(); + DEBOUNCE_TIMER.init(debounce_timer); + + // Set up the NVIC to handle interrupts. + unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) }; + pac::NVIC::unpend(pac::Interrupt::GPIOTE); + + let mut cur_count = 0; + loop { + // "wait for interrupt": CPU goes to sleep until an interrupt. + asm::wfi(); + let count = COUNTER.load(Acquire); + if count > cur_count { + rprintln!("ouch {}", count); + cur_count = count; + } + } +} diff --git a/mdbook/src/15-interrupts/examples/count-once.rs b/mdbook/src/15-interrupts/examples/count-once.rs new file mode 100644 index 00000000..43c37976 --- /dev/null +++ b/mdbook/src/15-interrupts/examples/count-once.rs @@ -0,0 +1,58 @@ +#![no_main] +#![no_std] + +use core::cell::RefCell; + +use cortex_m::asm; +use cortex_m_rt::entry; +use critical_section::Mutex; +use panic_rtt_target as _; +use rtt_target::{rprintln, rtt_init_print}; + +use microbit::{ + Board, + hal::{ + gpiote, + pac::{self, interrupt}, + }, +}; + +static COUNTER: Mutex> = Mutex::new(RefCell::new(0)); + +/// This "function" will be called when an interrupt is received. For now, just +/// report and panic. +#[interrupt] +fn GPIOTE() { + critical_section::with(|cs| { + let mut count = COUNTER.borrow(cs).borrow_mut(); + *count += 1; + rprintln!("count: {}", count); + }); + panic!(); +} + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let button_a = board.buttons.button_a.into_floating_input(); + + // Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO + // wire goes low). + let gpiote = gpiote::Gpiote::new(board.GPIOTE); + let channel = gpiote.channel0(); + channel + .input_pin(&button_a.degrade()) + .hi_to_lo() + .enable_interrupt(); + channel.reset_events(); + + // Set up the NVIC to handle GPIO interrupts. + unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) }; + pac::NVIC::unpend(pac::Interrupt::GPIOTE); + + loop { + // "wait for interrupt": CPU goes to sleep until an interrupt. + asm::wfi(); + } +} diff --git a/mdbook/src/15-interrupts/examples/count.rs b/mdbook/src/15-interrupts/examples/count.rs new file mode 100644 index 00000000..7c290ed5 --- /dev/null +++ b/mdbook/src/15-interrupts/examples/count.rs @@ -0,0 +1,57 @@ +#![no_main] +#![no_std] + +use core::sync::atomic::{AtomicUsize, Ordering::AcqRel}; + +use cortex_m::asm; +use cortex_m_rt::entry; +use critical_section_lock_mut::LockMut; +use panic_rtt_target as _; +use rtt_target::{rprintln, rtt_init_print}; + +use microbit::{ + Board, + hal::{ + gpiote, + pac::{self, interrupt}, + }, +}; + +static COUNTER: AtomicUsize = AtomicUsize::new(0); +static GPIOTE_PERIPHERAL: LockMut = LockMut::new(); + +#[interrupt] +fn GPIOTE() { + let count = COUNTER.fetch_add(1, AcqRel); + rprintln!("ouch {}", count + 1); + GPIOTE_PERIPHERAL.with_lock(|gpiote| { + gpiote.channel0().reset_events(); + }); +} + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let button_a = board.buttons.button_a.into_floating_input(); + + // Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO + // wire goes low). + let gpiote = gpiote::Gpiote::new(board.GPIOTE); + let channel = gpiote.channel0(); + channel + .input_pin(&button_a.degrade()) + .hi_to_lo() + .enable_interrupt(); + channel.reset_events(); + GPIOTE_PERIPHERAL.init(gpiote); + + // Set up the NVIC to handle GPIO interrupts. + unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) }; + pac::NVIC::unpend(pac::Interrupt::GPIOTE); + + loop { + // "wait for interrupt": CPU goes to sleep until an interrupt. + asm::wfi(); + } +} diff --git a/mdbook/src/15-interrupts/examples/poke.rs b/mdbook/src/15-interrupts/examples/poke.rs new file mode 100644 index 00000000..55ef7370 --- /dev/null +++ b/mdbook/src/15-interrupts/examples/poke.rs @@ -0,0 +1,49 @@ +#![no_main] +#![no_std] + +use cortex_m::asm; +use cortex_m_rt::entry; +use panic_rtt_target as _; +use rtt_target::{rprintln, rtt_init_print}; + +use microbit::{ + Board, + hal::{ + gpiote, + pac::{self, interrupt}, + }, +}; + +/// This "function" will be called when an interrupt is received. For now, just +/// report and panic. +#[interrupt] +fn GPIOTE() { + rprintln!("ouch"); + panic!(); +} + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let button_a = board.buttons.button_a.into_floating_input(); + + // Set up the GPIOTE to generate an interrupt when Button A is pressed (GPIO + // wire goes low). + let gpiote = gpiote::Gpiote::new(board.GPIOTE); + let channel = gpiote.channel0(); + channel + .input_pin(&button_a.degrade()) + .hi_to_lo() + .enable_interrupt(); + channel.reset_events(); + + // Set up the NVIC to handle GPIO interrupts. + unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) }; + pac::NVIC::unpend(pac::Interrupt::GPIOTE); + + loop { + // "wait for interrupt": CPU goes to sleep until an interrupt. + asm::wfi(); + } +} diff --git a/mdbook/src/15-interrupts/examples/square-wave.rs b/mdbook/src/15-interrupts/examples/square-wave.rs new file mode 100644 index 00000000..bad276c4 --- /dev/null +++ b/mdbook/src/15-interrupts/examples/square-wave.rs @@ -0,0 +1,40 @@ +#![no_main] +#![no_std] + +use cortex_m::asm; +use cortex_m_rt::entry; +use embedded_hal::{delay::DelayNs, digital::OutputPin}; +use panic_rtt_target as _; +use rtt_target::rtt_init_print; + +use microbit::{ + Board, + hal::{gpio, timer}, +}; + +/// The "period" is the time per cycle. It is +/// 1/f where f is the frequency in Hz. In this +/// case we measure time in milliseconds. +const PERIOD: u32 = 1000 / 220; + +/// Number of cycles for 5 seconds of output. +const CYCLES: u32 = 5000 / PERIOD; + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let mut speaker_pin = board.speaker_pin.into_push_pull_output(gpio::Level::Low); + let mut timer = timer::Timer::new(board.TIMER0); + + for _ in 0..CYCLES { + speaker_pin.set_high().unwrap(); + timer.delay_ms(PERIOD / 2); + speaker_pin.set_low().unwrap(); + timer.delay_ms(PERIOD / 2); + } + + loop { + asm::wfi(); + } +} diff --git a/mdbook/src/15-interrupts/my-solution.md b/mdbook/src/15-interrupts/my-solution.md new file mode 100644 index 00000000..410d275b --- /dev/null +++ b/mdbook/src/15-interrupts/my-solution.md @@ -0,0 +1,15 @@ +# My Solution + +I found it a bit tricky to figure out how the interrupt +handler should calculate the next interrupt time to keep +the siren going. I ended up with a couple of state variables +to keep track of whether the speaker pin was on or off +(could have checked the hardware) and to keep track of what +time the siren was at in its up-down cycle. + +My code contains all the details (`src/main.rs`). + + +```rust +{{#include src/main.rs}} +``` diff --git a/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md b/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md new file mode 100644 index 00000000..b73a1eb1 --- /dev/null +++ b/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md @@ -0,0 +1,73 @@ +## NVIC and Interrupt Priority + +We've seen that interrupts make our processor immediately jump to another function in the code, but +what's going on behind the scenes to allow this to happen? In this section we'll cover some +technical details that won't be necessary for the rest of the book, so feel free to skip ahead if +you're not interested. + +### The Interrupt Controller + +Interrupts allow the processor to respond to peripheral events such as a GPIO input pin changing +state, a timer completing its cycle, or a UART receiving a new byte. The peripheral contains +circuitry that notices the event and informs a dedicated interrupt-handling peripheral. On Arm +processors, the interrupt-handling peripheral is called the NVIC — the Nested Vector Interrupt +Controller. + +> **NOTE** On other microcontroller architectures such as RISC-V the names and details discussed +> here will differ, but the underlying principles are generally very similar. + +The NVIC can receive requests to trigger an interrupt from many peripherals. It's even common for a +peripheral to have multiple possible interrupts, for example a GPIO port having an interrupt for +each pin, or a UART having both a "data received" and "data finished transmission" interrupt. The +job of the NVIC is to prioritize these interrupts, remember which ones still need to be processed, +and then cause the processor to run the relevant interrupt handler code. + +### Interrupt Priorities + +The NVIC has a settable "priority" for each interrupt. Depending on its configuration, the NVIC can ensure the current interrupt is fully processed before a new one is executed, or it can "preempt" the processor in the middle of one interrupt in order to handle another that's higher priority. + +Preemption allows processors to respond very quickly to critical events. For example, a robot controller might use low-priority interrupts to manage sending status information to the operator, but also take a high-priority interrupt when a sensor detects an imminent collision so that it can immediately stop moving the motors. You wouldn't want the robot to wait until it had finished sending a data packet to get around to stopping! + +If an equal-priority or lower-priority interrupt occurs during an ISR, it will be "pended": the NVIC will remember the new interrupt and run its ISR sometime after the current ISR completes. When an ISR function returns the NVIC looks to see if, while the ISR was running, other interrupts have happened that need to be handled. If so, the NVIC checks the interrupt table and calls the highest-priority ISR vectored there. Otherwise, the CPU returns to the running program. + +In embedded Rust, we can program the NVIC using the [`cortex-m`] crate, which provides methods to +enable and disable (called `unmask` and `mask`) interrupts, set interrupt priorities, and trigger +interrupts from software. Frameworks such as [RTIC] can handle NVIC configuration for you, taking +advantage of the NVIC's flexibility to provide convenient resource sharing and task management. + +You can read more information about the NVIC in [Arm's documentation]. + +[`cortex-m`]: https://docs.rs/cortex-m/latest/cortex_m/peripheral/struct.NVIC.html +[RTIC]: https://rtic.rs/ +[Arm's documentation]: https://developer.arm.com/documentation/ddi0337/e/Nested-Vectored-Interrupt-Controller/About-the-NVIC + +### The vector table + +When describing the NVIC, I said it could "cause the processor to run the relevant interrupt handler +code". But how does that actually work? + +First, we need some way for the processor to know which code to run for each interrupt. On Cortex-M +processors, this involves a part of memory called the vector table. It is typically located at the +very start of the flash memory that contains our code, which is reprogrammed every time we upload +new code to our processor, and contains a list of addresses -- the locations in memory of every +interrupt function. The specific layout of the start of memory is defined by Arm in the +[Architecture Reference Manual]; for our purposes the important part is that bytes 64 through to 256 +contain the addresses of all 48 interrupt handlers for the nRF processor we use, four bytes per +address. Each interrupt has a number, from 0 to 47. For example, `TIMER0` is interrupt number 8, and +so bytes 96 to 100 contain the four-byte address of its interrupt handler. When the NVIC tells the +processor to handle interrupt number 8, the CPU reads the address stored in those bytes and jumps +execution to it. + +How is this vector table generated in our code? We use the [`cortex-m-rt`] crate which handles this +for us. It provides a default interrupt for every unused position (since every position must be +filled) and allows our code to override this default whenever we want to specify our own interrupt +handler. We do this using the `#[interrupt]` macro, which requires that our function be given a +specific name related to the interrupt it handles. Then the `cortex-m-rt` crate uses its linker +script to arrange for the address of that function to be placed in the right part of memory. + +For more details on how these interrupt handlers are managed in Rust, see the Exceptions and +Interrupts chapters in the [Embedded Rust Book]. + +[Architecture Reference Manual]: https://developer.arm.com/documentation/ddi0403/latest +[`cortex-m-rt`]: https://docs.rs/cortex-m-rt +[Embedded Rust Book]: https://docs.rust-embedded.org/book/ diff --git a/mdbook/src/15-interrupts/sharing-data-with-globals.md b/mdbook/src/15-interrupts/sharing-data-with-globals.md new file mode 100644 index 00000000..2ba51c25 --- /dev/null +++ b/mdbook/src/15-interrupts/sharing-data-with-globals.md @@ -0,0 +1,290 @@ +## Sharing Data With Globals + +> **NOTE** This content is partially taken (with permission) from the blog post +> *[Interrupts Is Threads]* by James Munns, which contains more discussion about this +> topic. + +As I mentioned in the last section, when an interrupt occurs we aren't passed any arguments and +cannot return any result. This makes it hard for our program interact with peripherals and other +main program state. Before worrying about this bare-metal embedded problem, it is likely worth +thinking about threads in "std" Rust. + +### "std" Rust: Sharing Data With A Thread + +In "std" Rust, we also have to think about sharing data when we do things like +spawn a thread. + +When you want to *give* something to a thread, you might pass it into a closure by ownership. + +```rust +// Create a string in our current thread +let data = String::from("hello"); + +// Now spawn a new thread, and GIVE it ownership of the string +// that we just created +std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(1000)); + println!("{data}"); +}); +``` + +If you want to *share* something, and still have access to it in the original thread, you usually +can't pass a reference to it. If you do this: + +```rust +use std::{thread::{sleep, spawn}, time::Duration}; + +fn main() { + // Create a string in our current thread + let data = String::from("hello"); + + // make a reference to pass along + let data_ref = &data; + + // Now spawn a new thread, and GIVE it ownership of the string + // that we just created + spawn(|| { + sleep(Duration::from_millis(1000)); + println!("{data_ref}"); + }); + + println!("{data_ref}"); +} +``` + +you'll get an error like this: + +```text +error[E0597]: `data` does not live long enough + --> src/main.rs:6:20 + | +3 | let data = String::from("hello"); + | ---- binding `data` declared here +... +6 | let data_ref = &data; + | ^^^^^ borrowed value does not live long enough +... +10 | / spawn(|| { +11 | | sleep(Duration::from_millis(1000)); +12 | | println!("{data_ref}"); +13 | | }); + | |______- argument requires that `data` is borrowed for `'static` +... +16 | } + | - `data` dropped here while still borrowed +``` + +You need to *make sure the data lives long enough* for both the current thread and the new thread +you are creating. You can do this by putting it in an `Arc` (Atomically Reference Counted heap +allocation) like this: + +```rust +use std::{sync::Arc, thread::{sleep, spawn}, time::Duration}; + +fn main() { + // Create a string in our current thread + let data = Arc::new(String::from("hello")); + + let handle = spawn({ + // Make a copy of the handle to GIVE to the new thread. + // Both `data` and `new_thread_data` are pointing at the + // same string! + let new_thread_data = data.clone(); + move || { + sleep(Duration::from_millis(1000)); + println!("{new_thread_data}"); + } + }); + + println!("{data}"); + // wait for the thread to stop + let _ = handle.join(); +} +``` + +This is great! You can now access the data in both the main thread as long as you'd +like. But what if you want to *mutate* the data in both places? + +For this, you will usually need some kind of "inner mutability" — a type that doesn't require an +`&mut` to modify. On the desktop, you'd typically reach for a type like `Mutex`, `lock()`-ing it to +gain mutable access to the data. + +That might look something like this: + +```rust +use std::{sync::{Arc, Mutex}, thread::{sleep, spawn}, time::Duration}; + +fn main() { + // Create a string in our current thread + let data = Arc::new(Mutex::new(String::from("hello"))); + + // lock it from the original thread + { + let guard = data.lock().unwrap(); + println!("{guard}"); + // the guard is dropped here at the end of the scope! + } + + let handle = spawn({ + // Make a copy of the handle, that you GIVE to the new thread. + // Both `data` and `new_thread_data` are pointing at the + // same `Mutex`! + let new_thread_data = data.clone(); + move || { + sleep(Duration::from_millis(1000)); + { + let mut guard = new_thread_data.lock().unwrap(); + // we can modify the data! + guard.push_str(" | thread was here! |"); + // the guard is dropped here at the end of the scope! + } + } + }); + + // wait for the thread to stop + let _ = handle.join(); + { + let guard = data.lock().unwrap(); + println!("{guard}"); + // the guard is dropped here at the end of the scope! + } +} +``` + +If you run this code, you will see: + +```text +hello +hello | thread was here! | +``` + +Why does "std" Rust make us do this? Rust is helping us out by making us think about two things: + +1. The data lives long enough (potentially "forever"!) +2. Only one piece of code can mutably access the data at a time + +If Rust allowed us to access data that might not live long enough, like data borrowed from one +thread into another, things might go wrong. We might get corrupted data if the original thread ends +or panics and then the second thread tries to access the data that is now invalid. If Rust allowed +two pieces of code to try to mutate the same data at the same, we could have a data race, or the +data could end up corrupted. + +### Embedded Rust: Sharing Data With An ISR + +In embedded Rust we care about the same things when it comes to sharing data with interrupt +handlers! Similar to threads, interrupts can occur at any time, sort of like a thread waking up and +accessing some shared data. This means that the data we share with an interrupt must live long +enough, and we must be careful to ensure that our main code isn't in the middle of working with some +data shared with an ISR when that ISR gets run and *also* tries to work with the data! + +In fact, in embedded Rust, we model interrupts in a similar way that we model threads in Rust: the +same rules apply, for the same reasons. However, in embedded Rust, we have some crucial differences: + +* Interrupts don't work exactly like threads: we set them up ahead of time, and they wait until some + event happens (like a button being pressed, or a timer expiring). At that point they run, but + without access to any passed-in context. + +* Interrupts can be triggered multiple times, once for each time that the event occurs. + +Since we can't pass context to interrupts as function arguments, we need to find another place to +store that data. In "bare metal" embedded Rust we don't have access to heap allocations: thus `Arc` +and similar are not possibilities for us. + +Without the ability to pass things by value, and without a heap to store data, that leaves us with +one place to put our shared data that our ISR can access: `static` globals. + +### Embedded Rust ISR Data Sharing: The "Standard Method" + +Global variables are very much second-class citizens in Rust, with many limitations compared to +local variables. You can declare a global state variable like this: + +```rust +static COUNTER: usize = 0; +``` + +Of course, this isn't super-useful: you want to be able to mutate the `COUNTER`. You can +say + +```rust +static mut COUNTER: usize = 0; +``` + +but now all accesses will be unsafe. + +```rust +unsafe { COUNTER += 1 }; +``` + +The unsafety here is for a reason: imagine that in the middle of updating `COUNTER` an interrupt +handler runs and also tries to update `COUNTER`. The usual chaos will ensue. Clearly some kind of +locking is in order. + +The `critical-section` crate provides a sort of `Mutex` type, but with an unusual API and unusual +operations. Examining the `Cargo.toml` for this chapter, you will see the feature +`critical-section-single-core` on the `cortex-m` crate enabled. This feature asserts that there is +only one processor core in this system, and that thus synchronization can be performed by simply +*disabling interrupts* across the critical section. If not in an interrupt, this will ensure that +only the main program has access to the global. If in an interrupt, this will ensure that the main +program cannot be accessing the global (program control is in the interrupt handler) and that no +other higher-priority interrupt handler can fire. + +`critical_section::Mutex` is a bit weird in that it gives mutual exclusion but does not itself give +mutability. To make the data mutable, you will need to protect an interior-mutable type — usually +`RefCell` — with the mutex. This `Mutex` is also a bit weird in that you don't `.lock()` +it. Instead, you initiate a critical section with a closure that receives a "critical section token" +certifying that other program execution is prevented. This token can be passed to the `Mutex`'s +`borrow()` method to allow access. + +Putting it all together gives you the ability to share state between ISRs and the main program +(`examples/count-once.rs`). + +```rust +{{#include examples/count-once.rs}} +``` + +You still cannot safely return from your ISR, but now you are in a position to do something about +that: share the `GPIOTE` with the ISR so that the ISR can clear the interrupt. + +### Sharing Peripherals (etc) With Globals + +There's one more problem yet to solve: Rust globals must be initialized statically — before the +program starts. For the counter that was easy — just initialize it to 0. If you want to share the +`GPIOTE` peripheral, though, that won't work. The peripheral must be retrieved from the `Board` +struct and set up once the program has started: there is no `const` initializer for this (nor can +there reasonably be). + +Let's rewrite the button counter a bit. First, move the actual count to be an `AtomicUsize`. This is +a more natural type for this global anyhow. Next, add a global `GPIOTE_PERIPHERAL` variable using +the `LockMut` type from the `critical-section-lock-mut` crate. This crate is a convenient wrapper +for the pattern of the last section. + +Now that the main program can set up the GPIOTE peripheral and then make it available to the +interrupt handler, you can quit panicking and let the counter bump up on every button press. Move +the count display into the main loop, to show that the count is shared between the interrupt handler +and the rest of the program. + +Give this example (`examples/count.rs`) a run and note that the count is bumped up 1 on every push +of the MB2 A button. + +```rust +{{#include examples/count.rs}} +``` + +> **NOTE** It is always a good idea to compile examples involving interrupt handling with +> `--release`. Long interrupt handlers can lead to a lot of confusion. + +Really, though, that `rprintln!()` in the interrupt handler is bad practice: while the interrupt +handler is running the printing code, nothing else can move forward. Let's move the reporting to the +main loop, just after the `wfi()` "wait for interrupt". The count will then be reported every time +an interrupt handler finishes (`examples/count-bounce.rs`). + +```rust +{{#include examples/count-bounce.rs}} +``` + +In this example the count is bumped up 1 on every push of the MB2 A button. Maybe. Especially if +your MB2 is old (!), you may see a single press bump the counter by several. *This is not a software +bug.* Mostly. In the next section, I'll talk about what might be going on and how we should deal +with it. + +[Interrupts Is Threads]: https://onevariable.com/blog/interrupts-is-threads diff --git a/mdbook/src/15-interrupts/src/main.rs b/mdbook/src/15-interrupts/src/main.rs new file mode 100644 index 00000000..c5e59316 --- /dev/null +++ b/mdbook/src/15-interrupts/src/main.rs @@ -0,0 +1,156 @@ +#![no_main] +#![no_std] + +use cortex_m::asm; +use cortex_m_rt::entry; +use critical_section_lock_mut::LockMut; +use embedded_hal::{delay::DelayNs, digital::OutputPin}; +use panic_rtt_target as _; +use rtt_target::{rtt_init_print, rprintln}; + +use microbit::{ + Board, + hal::{gpio, timer, pac::{self, interrupt}}, +}; + +/// Base siren frequency in Hz. +const BASE_FREQ: u32 = 440; +/// Max rise in siren frequency in Hz. +const FREQ_RISE: u32 = 220; +/// Time for one full cycle in µs. +const RISE_TIME: u32 = 500_000; + +/// These convenience types make life easier. +type SpeakerPin = gpio::Pin>; +type SirenTimer = timer::Timer; + +/// The current state of the siren. Updated by the interrupt +/// handler when running. +struct Siren { + /// The timer being used by the siren. + timer: SirenTimer, + /// The MB2 speaker pin. Needs to be owned + /// here for the interrupt handler. + speaker_pin: SpeakerPin, + /// Is the speaker pin currently high or low? + pin_high: bool, + /// Time in µs since the start of the current siren cycle. + cur_time: u32, +} + +impl Siren { + /// Make a new siren with the given peripherals. + fn new(speaker_pin: SpeakerPin, timer: SirenTimer) -> Self { + Self { + timer, + speaker_pin, + pin_high: false, + cur_time: 0, + } + } + + /// Start the siren running. + fn start(&mut self) { + self.speaker_pin.set_low().unwrap(); + self.pin_high = false; + self.cur_time = 0; + self.timer.enable_interrupt(); + // The timer interval is in ticks. + // The [nrf52833_hal] timer is hard-wired to 1M ticks/sec. + self.timer.start(1_000_000 / BASE_FREQ); + } + + /// Stop the siren. + fn stop(&mut self) { + self.timer.disable_interrupt(); + } + + /// Step the siren to the current speaker state change. + /// This is normally called from the timer interrupt. + fn step(&mut self) { + // Flip the speaker pin. + if self.pin_high { + self.speaker_pin.set_low().unwrap(); + self.pin_high = false; + } else { + self.speaker_pin.set_high().unwrap(); + self.pin_high = true; + } + + // Figure out the next period. The math is a little + // special here. + + // First, wrap to the next siren cycle if needed. + while self.cur_time >= 2 * RISE_TIME { + self.cur_time -= 2 * RISE_TIME; + } + // Next, figure out where we are in the current siren cycle. + let cycle_time = if self.cur_time < RISE_TIME { + self.cur_time + } else { + 2 * RISE_TIME - self.cur_time + }; + // Finally, calculate the frequency and period. + let frequency = BASE_FREQ + FREQ_RISE * cycle_time / RISE_TIME; + let period = 1_000_000 / frequency; + + // Anticipate the time of the next interrupt. + self.cur_time += period / 2; + + // Make sure to clear the current interrupt before + // starting the next one, else you might get interrupted + // again immediately. + self.timer.reset_event(); + self.timer.start(period / 2); + } +} + +/// The siren. Accessible from both the interrupt handler +/// and the main program. +static SIREN: LockMut = LockMut::new(); + +/// The timer interrupt for the siren. Just steps the siren. +#[interrupt] +fn TIMER0() { + SIREN.with_lock(|siren| siren.step()); +} + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + // It is convenient to use a `degrade()`ed pin + // to avoid having to deal with the type of the + // speaker pin, rather than looking it up: + // the pin is stored globally in `SIREN`, so its + // size must be known. + // + // This does lose type safety, but that is unlikely + // to matter after this point. + let speaker_pin = board.speaker_pin + .into_push_pull_output(gpio::Level::Low) + .degrade(); + let timer0 = timer::Timer::new(board.TIMER0); + let mut timer1 = timer::Timer::new(board.TIMER1); + + // Set up the NVIC to handle interrupts. + unsafe { pac::NVIC::unmask(pac::Interrupt::TIMER0) }; + pac::NVIC::unpend(pac::Interrupt::TIMER0); + + // Place the siren struct where the interrupt handler can find it. + let siren = Siren::new(speaker_pin, timer0); + SIREN.init(siren); + + // Start the siren and do the countdown. + SIREN.with_lock(|siren| siren.start()); + for t in (1..=10).rev() { + rprintln!("{}", t); + timer1.delay_ms(1_000); + } + rprintln!("launch!"); + SIREN.with_lock(|siren| siren.stop()); + + loop { + asm::wfi(); + } +} diff --git a/mdbook/src/15-interrupts/the-challenge.md b/mdbook/src/15-interrupts/the-challenge.md new file mode 100644 index 00000000..d22d277f --- /dev/null +++ b/mdbook/src/15-interrupts/the-challenge.md @@ -0,0 +1,20 @@ +# The Challenge + +Let's make the MB2 into a siren! But not just any siren — an +interrupt-driven siren. That way we can turn the siren on +and the rest of our program can run on, ignoring it. + +Make your siren sweep the pitch from 220Hz to 440Hz and back +over a one-second period. The main program should start the +siren, then print a ten-second countdown from 10 to 1, then +stop the siren and print "launch!". The main program should +not mess with the siren during countdown — it should just be +interrupt-driven. + +*Hint:* I found it easiest to use a global locked `Siren` +struct that owned the state of the siren and the peripherals +it needed to operate. + +This is a fancy program that introduces a lot of new +ideas. Don't be surprised if it takes you a bit to figure it +out. diff --git a/mdbook/src/15-interrupts/the-mb2-speaker.md b/mdbook/src/15-interrupts/the-mb2-speaker.md new file mode 100644 index 00000000..07b1ae82 --- /dev/null +++ b/mdbook/src/15-interrupts/the-mb2-speaker.md @@ -0,0 +1,31 @@ +# The MB2 Speaker + +Your MB2 has a built-in speaker — the large black square device labeled "SPEAKER" in the middle of +the back of the board. + +The speaker works by moving air in response to a GPIO pin: when the speaker pin is high (3.3V) a +diaphragm inside — the "speaker cone" — is pushed all the way out; when the speaker pin is low (GND) +it is pulled all the way back in. As air is pushed out and sucked back in, it flows in and out of +the tiny rectangular hole — the "speaker port" — on the side of the device. Do this fast enough, +and the pressure changes will make a sound. + + + +With the right hardware driving it, this speaker cone could actually be moved to any position in its +range with an appropriate current. This would allow fairly good reproduction of any sound, like a +"normal" speaker. Unfortunately, limitations in the MB2 hardware controlling the speaker mean that +only the full-in and full-out positions are readily available. + +Let's push the speaker cone out and then in 220 times per second. This will produce a "square" +220-cycles-per-second pressure wave. The unit "cycles-per-second" is Hertz; we will be producing a +220Hz tone (a musical "A3"), which is not unpleasant on this shrill speaker. + +We'll make our tone play for five seconds and then stop. It is important to remember that our +program lives in flash on the MB2 — the tone will start up again each time we reset or even power on +the MB2. If we let the tone run forever, this behavior can rapidly become quite annoying. + +Here's the code (`examples/square-wave.rs`). + +```rust +{{#include examples/square-wave.rs}} +``` diff --git a/mdbook/src/15-interrupts/waiting-to-be-interrupted.md b/mdbook/src/15-interrupts/waiting-to-be-interrupted.md new file mode 100644 index 00000000..50df1edb --- /dev/null +++ b/mdbook/src/15-interrupts/waiting-to-be-interrupted.md @@ -0,0 +1,11 @@ +# Waiting for an interrupt + +You may have wondered why we have been using `asm::wfi()` (wait for instruction) in our main loop instead of something like `asm::nop()`. + +As discussed before, `asm::nop()` means no-op(eration), and is an instruction that the CPU executes without doing anything . We definitely could have used `asm::nop()` in our main loop instead, and the program would have behaved the same way. The microcontroller, on the other hand, would behave differently. + +Calling `asm::wfi()` puts the CPU into "Wait For Interrupt" (WFI) mode. When the CPU is in WFI mode, it will sleep until an interrupt wakes it up. During sleep, the CPU will stop fetching instructions, turn off some clocks and peripherals, and enter a low-power state, but still keep the core running. When an interrupt occurs, the CPU will wake up and execute as normal. + +The main difference between `asm::wfi()` and `asm::nop()` is that the NOP instruction completes immediately, and will thus be run repeatedly in a loop. The NOP still needs to be fetched from the program memory and be executed even though the execution doesn't do anything. Most microcontrollers you'll find out there have a low-power mode (some even have several, each with varying things staying on and each with different power consumption characteristics) that can (and should in a lot of cases) be used to save power. The WFI instruction halts execution *in a low-power mode* until an interrupt is received. + +You'll find some interrupt-driven programs that consist of nothing but `asm::wfi()` in the main loop, with all program logic being implemented in the interrupt handlers. diff --git a/mdbook/src/16-snake-game/.cargo/config.toml b/mdbook/src/16-snake-game/.cargo/config.toml new file mode 100644 index 00000000..e4ad7f5a --- /dev/null +++ b/mdbook/src/16-snake-game/.cargo/config.toml @@ -0,0 +1,8 @@ +[build] +target = "thumbv7em-none-eabihf" + +[target.thumbv7em-none-eabihf] +runner = "probe-rs run --chip nRF52833_xxAA" +rustflags = [ + "-C", "linker=rust-lld", +] diff --git a/mdbook/src/14-snake-game/Cargo.toml b/mdbook/src/16-snake-game/Cargo.toml similarity index 100% rename from mdbook/src/14-snake-game/Cargo.toml rename to mdbook/src/16-snake-game/Cargo.toml diff --git a/mdbook/src/16-snake-game/Embed.toml b/mdbook/src/16-snake-game/Embed.toml new file mode 100644 index 00000000..21950130 --- /dev/null +++ b/mdbook/src/16-snake-game/Embed.toml @@ -0,0 +1,11 @@ +[default.general] +chip = "nrf52833_xxAA" # micro:bit V2 + +[default.reset] +halt_afterwards = false + +[default.rtt] +enabled = true + +[default.gdb] +enabled = false diff --git a/mdbook/src/14-snake-game/README.md b/mdbook/src/16-snake-game/README.md similarity index 74% rename from mdbook/src/14-snake-game/README.md rename to mdbook/src/16-snake-game/README.md index 555c7c37..86d4ab2f 100644 --- a/mdbook/src/14-snake-game/README.md +++ b/mdbook/src/16-snake-game/README.md @@ -5,13 +5,6 @@ game that you can play on an MB2 using its 5×5 LED matrix as a display and its controls. In doing so, we will build on some of the concepts covered in the earlier chapters of this book, and also learn about some new peripherals and concepts. -In particular, we will be using the concept of hardware interrupts to allow our program to interact -with multiple peripherals at once. Interrupts are a common way to implement concurrency in embedded -contexts. There is a good introduction to concurrency in an embedded context [here] that -you might read through before proceeding. - -[here]: https://docs.rust-embedded.org/book/concurrency/index.html - ## Modularity The source code here is more modular than it probably should be. This fine-grained modularity allows diff --git a/mdbook/src/14-snake-game/controls.md b/mdbook/src/16-snake-game/controls.md similarity index 98% rename from mdbook/src/14-snake-game/controls.md rename to mdbook/src/16-snake-game/controls.md index c9e6fda3..b808d6f1 100644 --- a/mdbook/src/14-snake-game/controls.md +++ b/mdbook/src/16-snake-game/controls.md @@ -26,7 +26,7 @@ code in the function or closure cannot itself be interrupted. ### Initialization -First, we will initialise the buttons (`src/controls/init.rs`). +First, we will initialize the buttons (`src/controls/init.rs`). ```rust {{#include src/controls/init.rs}} diff --git a/mdbook/src/14-snake-game/final-assembly.md b/mdbook/src/16-snake-game/final-assembly.md similarity index 93% rename from mdbook/src/14-snake-game/final-assembly.md rename to mdbook/src/16-snake-game/final-assembly.md index b9a5dc29..f65564a8 100644 --- a/mdbook/src/14-snake-game/final-assembly.md +++ b/mdbook/src/16-snake-game/final-assembly.md @@ -7,7 +7,7 @@ our final game. {{#include src/main.rs}} ``` -After initialising the board and its timer and RNG peripherals, we initialise a `Game` struct and a +After initializing the board and its timer and RNG peripherals, we initialize a `Game` struct and a `Display` from the `microbit::display::blocking` module. In our "game loop" (which runs inside of the "main loop" we place in our `main` function), we diff --git a/mdbook/src/14-snake-game/game-logic.md b/mdbook/src/16-snake-game/game-logic.md similarity index 100% rename from mdbook/src/14-snake-game/game-logic.md rename to mdbook/src/16-snake-game/game-logic.md diff --git a/mdbook/src/14-snake-game/nonblocking-display.md b/mdbook/src/16-snake-game/nonblocking-display.md similarity index 98% rename from mdbook/src/14-snake-game/nonblocking-display.md rename to mdbook/src/16-snake-game/nonblocking-display.md index 68603b81..2c5ab7f9 100644 --- a/mdbook/src/14-snake-game/nonblocking-display.md +++ b/mdbook/src/16-snake-game/nonblocking-display.md @@ -30,7 +30,7 @@ at the top level. {{#include src/display.rs}} ``` -First, we initialise a `microbit::display::nonblocking::Display` struct representing the LED +First, we initialize a `microbit::display::nonblocking::Display` struct representing the LED display, passing it the board's `TIMER1` and `DisplayPins` peripherals. Then we store the display in a Mutex. Finally, we unmask the `TIMER1` interrupt. diff --git a/mdbook/src/14-snake-game/src/controls.rs b/mdbook/src/16-snake-game/src/controls.rs similarity index 100% rename from mdbook/src/14-snake-game/src/controls.rs rename to mdbook/src/16-snake-game/src/controls.rs diff --git a/mdbook/src/14-snake-game/src/controls/init.rs b/mdbook/src/16-snake-game/src/controls/init.rs similarity index 100% rename from mdbook/src/14-snake-game/src/controls/init.rs rename to mdbook/src/16-snake-game/src/controls/init.rs diff --git a/mdbook/src/14-snake-game/src/controls/interrupt.rs b/mdbook/src/16-snake-game/src/controls/interrupt.rs similarity index 100% rename from mdbook/src/14-snake-game/src/controls/interrupt.rs rename to mdbook/src/16-snake-game/src/controls/interrupt.rs diff --git a/mdbook/src/14-snake-game/src/display.rs b/mdbook/src/16-snake-game/src/display.rs similarity index 100% rename from mdbook/src/14-snake-game/src/display.rs rename to mdbook/src/16-snake-game/src/display.rs diff --git a/mdbook/src/14-snake-game/src/display/interrupt.rs b/mdbook/src/16-snake-game/src/display/interrupt.rs similarity index 100% rename from mdbook/src/14-snake-game/src/display/interrupt.rs rename to mdbook/src/16-snake-game/src/display/interrupt.rs diff --git a/mdbook/src/14-snake-game/src/display/show.rs b/mdbook/src/16-snake-game/src/display/show.rs similarity index 100% rename from mdbook/src/14-snake-game/src/display/show.rs rename to mdbook/src/16-snake-game/src/display/show.rs diff --git a/mdbook/src/14-snake-game/src/game.rs b/mdbook/src/16-snake-game/src/game.rs similarity index 99% rename from mdbook/src/14-snake-game/src/game.rs rename to mdbook/src/16-snake-game/src/game.rs index b55e8950..fb13fc0a 100644 --- a/mdbook/src/14-snake-game/src/game.rs +++ b/mdbook/src/16-snake-game/src/game.rs @@ -194,3 +194,4 @@ impl Game { values } } + diff --git a/mdbook/src/14-snake-game/src/game/coords.rs b/mdbook/src/16-snake-game/src/game/coords.rs similarity index 100% rename from mdbook/src/14-snake-game/src/game/coords.rs rename to mdbook/src/16-snake-game/src/game/coords.rs diff --git a/mdbook/src/14-snake-game/src/game/movement.rs b/mdbook/src/16-snake-game/src/game/movement.rs similarity index 100% rename from mdbook/src/14-snake-game/src/game/movement.rs rename to mdbook/src/16-snake-game/src/game/movement.rs diff --git a/mdbook/src/14-snake-game/src/game/rng.rs b/mdbook/src/16-snake-game/src/game/rng.rs similarity index 100% rename from mdbook/src/14-snake-game/src/game/rng.rs rename to mdbook/src/16-snake-game/src/game/rng.rs diff --git a/mdbook/src/14-snake-game/src/game/snake.rs b/mdbook/src/16-snake-game/src/game/snake.rs similarity index 100% rename from mdbook/src/14-snake-game/src/game/snake.rs rename to mdbook/src/16-snake-game/src/game/snake.rs diff --git a/mdbook/src/14-snake-game/src/main.rs b/mdbook/src/16-snake-game/src/main.rs similarity index 100% rename from mdbook/src/14-snake-game/src/main.rs rename to mdbook/src/16-snake-game/src/main.rs diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index 9011a49d..b46dbc2c 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -2,66 +2,80 @@ - [Background](01-background/README.md) - [Hardware/knowledge requirements](02-requirements/README.md) - [Setting up a development environment](03-setup/README.md) - - [Linux](03-setup/linux.md) - - [Windows](03-setup/windows.md) - - [macOS](03-setup/macos.md) - - [Verify the installation](03-setup/verify.md) - - [Setting up your IDE](03-setup/IDE.md) + - [Linux](03-setup/linux.md) + - [Windows](03-setup/windows.md) + - [macOS](03-setup/macos.md) + - [Verify the installation](03-setup/verify.md) + - [Setting up your IDE](03-setup/IDE.md) - [Meet your hardware](04-meet-your-hardware/README.md) - - [micro:bit v2](04-meet-your-hardware/microbit-v2.md) - - [Rust Embedded terminology](04-meet-your-hardware/terminology.md) + - [micro:bit v2](04-meet-your-hardware/microbit-v2.md) + - [Rust Embedded terminology](04-meet-your-hardware/terminology.md) - [Meet your software](05-meet-your-software/README.md) - - [Build it](05-meet-your-software/build-it.md) - - [Flash it](05-meet-your-software/flash-it.md) - - [Debug it](05-meet-your-software/debug-it.md) - - [Light it up](05-meet-your-software/light-it-up.md) + - [Build it](05-meet-your-software/build-it.md) + - [Flash it](05-meet-your-software/flash-it.md) + - [Debug it](05-meet-your-software/debug-it.md) + - [Light it up](05-meet-your-software/light-it-up.md) - [Hello World](06-hello-world/README.md) - - [Toggle it](06-hello-world/toggle-it.md) - - [Spin wait](06-hello-world/spin-wait.md) - - [NOP](06-hello-world/nop.md) - - [Timers](06-hello-world/timers.md) - - [Portability](06-hello-world/portability.md) - - [Board support crate](06-hello-world/board-support-crate.md) -- [Registers](07-registers/README.md) - - [RTRM](07-registers/rtrm.md) - - [(mis)Optimization](07-registers/misoptimization.md) - - [`0xBAAAAAAD` address](07-registers/bad-address.md) - - [Spooky action at a distance](07-registers/spooky-action-at-a-distance.md) - - [Type safe manipulation](07-registers/type-safe-manipulation.md) -- [LED roulette](08-led-roulette/README.md) - - [The challenge](08-led-roulette/the-challenge.md) - - [My solution](08-led-roulette/my-solution.md) -- [Serial communication](09-serial-communication/README.md) - - [\*nix tooling](09-serial-communication/nix-tooling.md) - - [Windows tooling](09-serial-communication/windows-tooling.md) -- [UART](10-uart/README.md) - - [Send a single byte](10-uart/send-a-single-byte.md) - - [Send a string](10-uart/send-a-string.md) - - [Naive approach and `write!`](10-uart/naive-approach-write.md) - - [Receive a single byte](10-uart/receive-a-single-byte.md) - - [Echo server](10-uart/echo-server.md) - - [Reverse a string](10-uart/reverse-a-string.md) - - [My solution](10-uart/my-solution.md) -- [I2C](11-i2c/README.md) - - [The general protocol](11-i2c/the-general-protocol.md) - - [LSM303AGR](11-i2c/lsm303agr.md) - - [Read a single register](11-i2c/read-a-single-register.md) - - [Using a driver](11-i2c/using-a-driver.md) - - [The challenge](11-i2c/the-challenge.md) - - [My solution](11-i2c/my-solution.md) -- [LED compass](12-led-compass/README.md) - - [Magnitude](12-led-compass/magnitude.md) - - [The challenge](12-led-compass/the-challenge.md) - - [My solution](12-led-compass/my-solution.md) -- [Punch-o-meter](13-punch-o-meter/README.md) - - [Gravity is up?](13-punch-o-meter/gravity-is-up.md) - - [The challenge](13-punch-o-meter/the-challenge.md) - - [My solution](13-punch-o-meter/my-solution.md) -- [Snake game](14-snake-game/README.md) - - [Game logic](14-snake-game/game-logic.md) - - [Controls](14-snake-game/controls.md) - - [Non-blocking display](14-snake-game/nonblocking-display.md) - - [Final assembly](14-snake-game/final-assembly.md) + - [Toggle it](06-hello-world/toggle-it.md) + - [Spin wait](06-hello-world/spin-wait.md) + - [NOP](06-hello-world/nop.md) + - [Timers](06-hello-world/timers.md) + - [Portability](06-hello-world/portability.md) + - [Board support crate](06-hello-world/board-support-crate.md) +- [LED roulette](07-led-roulette/README.md) + - [The challenge](07-led-roulette/the-challenge.md) + - [My solution](07-led-roulette/my-solution.md) +- [Inputs and Outputs](08-inputs-and-outputs/README.md) + - [Polling](08-inputs-and-outputs/polling.md) + - [Turn signaller](08-inputs-and-outputs/the-challenge.md) + - [My solution](08-inputs-and-outputs/my-solution.md) + - [Polling sucks, actually](08-inputs-and-outputs/polling-sucks.md) +- [Registers](09-registers/README.md) + - [RTRM](09-registers/rtrm.md) + - [(mis)Optimization](09-registers/misoptimization.md) + - [`0xBAAAAAAD` address](09-registers/bad-address.md) + - [Spooky action at a distance](09-registers/spooky-action-at-a-distance.md) + - [Type safe manipulation](09-registers/type-safe-manipulation.md) +- [Serial communication](10-serial-communication/README.md) + - [\*nix tooling](10-serial-communication/nix-tooling.md) + - [Windows tooling](10-serial-communication/windows-tooling.md) +- [UART](11-uart/README.md) + - [Send a single byte](11-uart/send-a-single-byte.md) + - [Send a string](11-uart/send-a-string.md) + - [Naive approach and `write!`](11-uart/naive-approach-write.md) + - [Receive a single byte](11-uart/receive-a-single-byte.md) + - [Echo server](11-uart/echo-server.md) + - [Reverse a string](11-uart/reverse-a-string.md) + - [My solution](11-uart/my-solution.md) +- [I2C](12-i2c/README.md) + - [The general protocol](12-i2c/the-general-protocol.md) + - [LSM303AGR](12-i2c/lsm303agr.md) + - [Read a single register](12-i2c/read-a-single-register.md) + - [Using a driver](12-i2c/using-a-driver.md) + - [The challenge](12-i2c/the-challenge.md) + - [My solution](12-i2c/my-solution.md) +- [LED compass](13-led-compass/README.md) + - [Magnitude](13-led-compass/magnitude.md) + - [The challenge](13-led-compass/the-challenge.md) + - [My solution](13-led-compass/my-solution.md) +- [Punch-o-meter](14-punch-o-meter/README.md) + - [Gravity is up?](14-punch-o-meter/gravity-is-up.md) + - [The challenge](14-punch-o-meter/the-challenge.md) + - [My solution](14-punch-o-meter/my-solution.md) +- [Interrupts](15-interrupts/README.md) + - [NVIC and interrupt priority](15-interrupts/nvic-and-interrupt-priority.md) + - [Sharing data with globals](15-interrupts/sharing-data-with-globals.md) + - [Debouncing](15-interrupts/debouncing.md) + - [Waiting to be interrupted](15-interrupts/waiting-to-be-interrupted.md) + - [The MB2 speaker](15-interrupts/the-mb2-speaker.md) + - [The challenge](15-interrupts/the-challenge.md) + - [My solution](15-interrupts/my-solution.md) + - [Addendum: PWM](15-interrupts/addendum-pwm.md) +- [Snake game](16-snake-game/README.md) + - [Game logic](16-snake-game/game-logic.md) + - [Controls](16-snake-game/controls.md) + - [Non-blocking display](16-snake-game/nonblocking-display.md) + - [Final assembly](16-snake-game/final-assembly.md) - [What's left for you to explore](explore.md) --- diff --git a/mdbook/src/appendix/3-mag-calibration/README.md b/mdbook/src/appendix/3-mag-calibration/README.md index 2bc1690b..d72dcf75 100644 --- a/mdbook/src/appendix/3-mag-calibration/README.md +++ b/mdbook/src/appendix/3-mag-calibration/README.md @@ -42,4 +42,4 @@ Note that the calibration matrix is printed by the demo program. This matrix can a program such as the [chapter 12] compass program (or stored in flash somewhere somehow) to avoid the need to recalibrate every time the user runs the program. -[chapter 12]: ../../12-led-compass/index.html +[chapter 13]: ../../13-led-compass/index.html diff --git a/mdbook/src/assets/speaker.svg b/mdbook/src/assets/speaker.svg new file mode 100644 index 00000000..d1e4c617 --- /dev/null +++ b/mdbook/src/assets/speaker.svg @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + SPEAKER PIN + GROUND + + + + + + + + + + + + + CONE + PORT + + + + + + + + diff --git a/mdbook/src/explore.md b/mdbook/src/explore.md index 7473e39c..d94b02c2 100644 --- a/mdbook/src/explore.md +++ b/mdbook/src/explore.md @@ -13,52 +13,12 @@ explore. [open an issue]: https://github.com/rust-embedded/discovery-mb2/issues/new -## Topics about embedded software +## More of the MB2 -These topics discuss strategies for writing embedded software. Although many -problems can be solved in different ways, these sections talk about some -strategies, and when they make sense (or don't make sense) to use. +We touched most of the hardware on the MB2 in the course of this book. That said, there's still a +few MB2 topics left to explore. -### Multitasking - -Most of our programs executed a single task. How could we achieve multitasking in a system with no -OS, and thus no threads? There are two main approaches to multitasking: preemptive multitasking and -cooperative multitasking. - -In preemptive multitasking a task that's currently being executed can, at any point in time, be -*preempted* (interrupted) by another task. On preemption, the first task will be suspended and the -processor will instead execute the second task. At some point the first task will be resumed. -Microcontrollers provide hardware support for preemption in the form of *interrupts*. We were -introduced to interrupts when we built our snake game in [chapter 14](14-snake-game/index.md). - -In cooperative multitasking a task that's being executed will run until it reaches a *suspension -point*. When the processor reaches that suspension point it will stop executing the current task and -instead go and execute a different task. At some point the first task will be resumed. The main -difference between these two approaches to multitasking is that in cooperative multitasking *yields* -execution control at *known* suspension points instead of being forcefully preempted at any point of -its execution. - -### Sleeping - -All our programs have been continuously polling peripherals to see if there's anything that needs to -be done. However, sometimes there's nothing to be done! At those times, the microcontroller should -"sleep". - -When the processor sleeps, it stops executing instructions and this saves power. It's almost always -a good idea to save power so your microcontroller should be sleeping as much as possible. But, how -does it know when it has to wake up to perform some action? Interrupts are one of the events that -wake up the microcontroller but there are others. The Arm machine instructions `wfi` and `wfe` are -the instructions that make the processor "sleep" waiting for an interrupt or event. - -## Topics related to microcontroller capabilities - -Microcontrollers (like our nRF52/nRF51) have many capabilities. However, many share similar -capabilities that can be used to solve all sorts of different problems. - -These topics discuss some of those capabilities, and how they can be used effectively -in embedded development. - -### Direct Memory Access (DMA). +## Direct Memory Access (DMA). Some peripherals have DMA, a kind of *asynchronous* `memcpy` that allows the peripheral to move data into or out of memory without the CPU being involved. @@ -74,48 +34,12 @@ transfer and do other work while the transfer is ongoing. The details of low-level DMA can be a bit tricky. We hope to add a chapter covering this topic in the near future. -### Interrupts - -We saw button interrupts briefly in [chapter 14](14-snake-game/controls.html). -This introduced the key idea: in order to interact with the real world, it is often necessary for -the microcontroller to respond *immediately* when some kind of event occurs. - -Microcontrollers have the ability to be interrupted, meaning when a certain event -occurs, it will stop whatever it is doing at the moment, to instead respond to that -event. This can be very useful when we want to stop a motor when a button is pressed, -or measure a sensor when a timer finishes counting down. - -Although these interrupts can be very useful, they can also be a bit difficult -to work with properly. We want to make sure that we respond to events quickly, -but also allow other work to continue as well. - -In Rust, we model interrupts similar to the concept of threading on desktop Rust -programs. This means we also must think about the Rust concepts of `Send` and `Sync` -when sharing data between our main application, and code that executes as part of -handling an interrupt event. - -### Pulse Width Modulation (PWM) - -In a nutshell, PWM is turning on something and then turning it off periodically -while keeping some proportion ("duty cycle") between the "on time" and the "off -time". When used on a LED with a sufficiently high frequency, this can be used -to dim the LED. A low duty cycle, say 10% on time and 90% off time, will make -the LED very dim wheres a high duty cycle, say 90% on time and 10% off time, -will make the LED much brighter (almost as if it were fully powered). - -In general, PWM can be used to control how much *power* is given to some -electric device. With proper (power) electronics between a microcontroller and -an electrical motor, PWM can be used to control how much power is given to the -motor thus it can be used to control its torque and speed. Then you can add an -angular position sensor and you got yourself a closed loop controller that can -control the position of the motor at different loads. - There are some abstraction for working with PWM in the `embedded-hal` [`pwm` module] and you will find implementations of these traits in `nrf52833-hal`. [`pwm` module]: https://docs.rs/embedded-hal/latest/embedded_hal/pwm/index.html -### Digital inputs and outputs +## Digital inputs and outputs We have used the microcontroller pins as digital outputs, to drive LEDs. When building our snake game, we also caught a glimpse of how these pins can be configured as digital inputs. As digital @@ -129,7 +53,7 @@ Digital inputs and outputs are abstracted within the `embedded-hal` [`digital` m [`digital` module]: https://docs.rs/embedded-hal/latest/embedded_hal/digital/index.html -### Analog-to-Digital Converters (ADC) +## Analog-to-Digital Converters (ADC) There are a lot of digital sensors out there. You can use a protocol like I2C and SPI to read them. But analog sensors also exist! These sensors just output a reading to the CPU of the voltage @@ -144,7 +68,7 @@ nRF52833. [issue #377]: https://github.com/rust-embedded/embedded-hal/issues/377 -### Digital-to-Analog Converters (DAC) +## Digital-to-Analog Converters (DAC) As you might expect a DAC is exactly the opposite of ADC. You can write some digital number into a register to produce a specific voltage on some analog output pin. When this analog output pin is @@ -155,7 +79,7 @@ Neither the nRF52833 nor the MB2 board has a dedicated DAC. One typically gets a by outputting PWM and using a bit of electronics on the output (RC filter) to "smooth" out the PWM waveform. -### Real Time Clock +## Real Time Clock A Real-Time Clock peripheral keeps track of time under its own power, usually in "human format": seconds, minutes, hours, days, months and years. Some Real-Time Clocks even handle leap years and @@ -169,22 +93,27 @@ an on-board battery, the RTC should be able to run for a long time (possibly yea plugged into the battery port on the MB2 (for example, the battery pack provided with the micro::bit Go kit). -### Other communication protocols +## Other communication protocols -- I2C: discussed in earlier chapters of this book -- SPI: abstracted within the [`embedded-hal` `spi` module] and implemented by the [`nrf52-hal`] -- I2S: currently not abstracted within the `embedded-hal` but implemented by the [`nrf52-hal`] +- SPI: The "Serial Peripheral Interface" is a high-speed communications interface similar in some + ways to I2C. SPI is abstracted within the [`embedded-hal` `spi` module] and implemented by + [`nrf52-hal`]. +- I2S: The "Inter-IC Sound" protocol is a variant of I2C customized for audio transmission. + I2C is currently not abstracted within `embedded-hal`, but is implemented by [`nrf52-hal`]. - Ethernet: there does exist a small TCP/IP stack named [`smoltcp`] which is implemented for some chips. The MB2 does not have an Ethernet peripheral -- USB: there is some experimental work on this, for example with the [`usb-device`] crate -- Bluetooth: the `nrf-softdevice` wrapper provided by the `Embassy` MB2 runtime is probably the - easiest entry into MB2 Bluetooth right now +- USB: there is some experimental work on this, for example with the [`usb-device`] crate. For + the MB2, the USB port is managed by the interface MCU rather than the host MCU, making + it difficult to do custom USB things. +- Bluetooth: the `nrf-softdevice` wrapper provided by the [Embassy] MB2 runtime is probably the + easiest entry into MB2 Bluetooth. Embassy also sports the Rust-native [`TrouBLE`] BLE host crate. - CAN, SMBUS, IrDA, etc: All kinds of specialty interfaces exist in the world; Rust sometimes has support for them. Please investigate the current situation for the interface you need [`embedded-hal` `spi` module]: https://docs.rs/embedded-hal/0.2.6/embedded_hal/spi/index.html [`smoltcp`]: https://github.com/smoltcp-rs/smoltcp [`usb-device`]: https://github.com/mvirkkunen/usb-device +[`TrouBLE`]: https://crates.io/crates/trouble-host Different applications use different communication protocols. User facing applications usually have a USB connector because USB is a ubiquitous protocol in PCs and smartphones. Whereas inside cars @@ -198,10 +127,30 @@ built the stuff from above. ## General Embedded-Relevant Topics These topics cover items that are not specific to our device, or the hardware on it. Instead, they -discuss useful techniques that could be used on embedded systems. Most of what we will discuss here -is not available on the MB2 — but most of it could easily be added by connecting a cheap piece of -hardware to the MB2 edge-card connector, either driving it directly or using something like SPI or -I2C to control it. +discuss useful techniques that could be used on embedded systems. + +Most of the hardware we will discuss here is not available on the MB2 — but much of it could easily +be added by connecting a cheap piece of hardware to the MB2 edge-card connector, either driving it +directly or using something like SPI or I2C to control it. + +### Multitasking + +Most of our programs executed a single task. How could we achieve multitasking in a system with no +OS, and thus no threads? There are two main approaches to multitasking: preemptive multitasking and +cooperative multitasking. + +In preemptive multitasking a task that's currently being executed can, at any point in time, be +*preempted* (interrupted) by another task. On preemption, the first task will be suspended and the +processor will instead execute the second task. At some point the first task will be resumed. +Microcontrollers provide hardware support for preemption in the form of *interrupts*. We were +introduced to interrupts when we built our snake game in [chapter 16](16-snake-game/index.md). + +In cooperative multitasking a task that's being executed will run until it reaches a *suspension +point*. When the processor reaches that suspension point it will stop executing the current task and +instead go and execute a different task. At some point the first task will be resumed. The main +difference between these two approaches to multitasking is that in cooperative multitasking *yields* +execution control at *known* suspension points instead of being forcefully preempted at any point of +its execution. ### Gyroscopes @@ -270,7 +219,5 @@ There are many other options: [`embedded-hal`]: https://github.com/rust-embedded/embedded-hal -- You could try running Rust on a different development board. The easiest way to get started is to - use the [`cortex-m-quickstart`] Cargo project template. - -[`cortex-m-quickstart`]: https://docs.rs/cortex-m-quickstart/0.3.1/cortex_m_quickstart/ +- You could try running Rust on a different development board. Popular boards such as the ESP-32, + Raspberry Pi, or Arduino have their own active Rust developer communities.