From dbe4aab191a395b83885a22f9d1fdae9e2e76945 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 26 Oct 2024 10:51:49 -0700 Subject: [PATCH 01/38] finished first interrupt example --- Cargo.toml | 1 + mdbook/src/14-interrupts/.cargo/config.toml | 8 +++++ mdbook/src/14-interrupts/Cargo.toml | 17 +++++++++ mdbook/src/14-interrupts/Embed.toml | 11 ++++++ mdbook/src/14-interrupts/examples/poke.rs | 40 +++++++++++++++++++++ mdbook/src/14-interrupts/examples/timer.rs | 30 ++++++++++++++++ 6 files changed, 107 insertions(+) create mode 100644 mdbook/src/14-interrupts/.cargo/config.toml create mode 100644 mdbook/src/14-interrupts/Cargo.toml create mode 100644 mdbook/src/14-interrupts/Embed.toml create mode 100644 mdbook/src/14-interrupts/examples/poke.rs create mode 100644 mdbook/src/14-interrupts/examples/timer.rs diff --git a/Cargo.toml b/Cargo.toml index 1bc81f81..548ef719 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "mdbook/src/11-i2c", "mdbook/src/12-led-compass", "mdbook/src/13-punch-o-meter", + "mdbook/src/14-interrupts", "mdbook/src/14-snake-game", "mdbook/src/appendix/3-mag-calibration", "mdbook/src/serial-setup", diff --git a/mdbook/src/14-interrupts/.cargo/config.toml b/mdbook/src/14-interrupts/.cargo/config.toml new file mode 100644 index 00000000..e4ad7f5a --- /dev/null +++ b/mdbook/src/14-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/14-interrupts/Cargo.toml b/mdbook/src/14-interrupts/Cargo.toml new file mode 100644 index 00000000..520c46c5 --- /dev/null +++ b/mdbook/src/14-interrupts/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "interrupts" +version = "0.1.0" +edition = "2021" + +[dependencies] +microbit-v2 = "0.15" +cortex-m-rt = "0.7" +rtt-target = "0.5" +panic-rtt-target = "0.1" +heapless = "0.8" +tiny-led-matrix = "1.0" +embedded-hal = "1.0" + +[dependencies.cortex-m] +version = "0.7" +features = ["critical-section-single-core"] diff --git a/mdbook/src/14-interrupts/Embed.toml b/mdbook/src/14-interrupts/Embed.toml new file mode 100644 index 00000000..21950130 --- /dev/null +++ b/mdbook/src/14-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/14-interrupts/examples/poke.rs b/mdbook/src/14-interrupts/examples/poke.rs new file mode 100644 index 00000000..5e452ddd --- /dev/null +++ b/mdbook/src/14-interrupts/examples/poke.rs @@ -0,0 +1,40 @@ +#![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}, + }, +}; + +#[interrupt] +fn GPIOTE() { + rprintln!("ouch"); + asm::bkpt(); +} + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let button_a = board.buttons.button_a.into_floating_input(); + let gpiote = gpiote::Gpiote::new(board.GPIOTE); + let channel = gpiote.channel0(); + channel + .input_pin(&button_a.degrade()) + .lo_to_hi() + .enable_interrupt(); + channel.reset_events(); + unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) }; + pac::NVIC::unpend(pac::Interrupt::GPIOTE); + loop { + asm::wfe(); + } +} diff --git a/mdbook/src/14-interrupts/examples/timer.rs b/mdbook/src/14-interrupts/examples/timer.rs new file mode 100644 index 00000000..ed5b1dce --- /dev/null +++ b/mdbook/src/14-interrupts/examples/timer.rs @@ -0,0 +1,30 @@ +#![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::hal::{ + gpio, + gpiote, + pac::{self, interrupt}, + timer, +}; + +struct Blinker { + period: u32, + timer: timer::Timer<> + +static BLINKER: Option ! { + +} From cf4c92142b223df78c7d51454001544362aa7218 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 26 Oct 2024 10:52:08 -0700 Subject: [PATCH 02/38] removed unused interrupt example --- mdbook/src/14-interrupts/examples/timer.rs | 30 ---------------------- 1 file changed, 30 deletions(-) delete mode 100644 mdbook/src/14-interrupts/examples/timer.rs diff --git a/mdbook/src/14-interrupts/examples/timer.rs b/mdbook/src/14-interrupts/examples/timer.rs deleted file mode 100644 index ed5b1dce..00000000 --- a/mdbook/src/14-interrupts/examples/timer.rs +++ /dev/null @@ -1,30 +0,0 @@ -#![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::hal::{ - gpio, - gpiote, - pac::{self, interrupt}, - timer, -}; - -struct Blinker { - period: u32, - timer: timer::Timer<> - -static BLINKER: Option ! { - -} From 109eeddc8aa7995ad8a02696d2ccd3f7cbc6e5e7 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 26 Oct 2024 11:18:08 -0700 Subject: [PATCH 03/38] added second example; needs debugging --- mdbook/src/14-interrupts/Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mdbook/src/14-interrupts/Cargo.toml b/mdbook/src/14-interrupts/Cargo.toml index 520c46c5..bda9a586 100644 --- a/mdbook/src/14-interrupts/Cargo.toml +++ b/mdbook/src/14-interrupts/Cargo.toml @@ -6,11 +6,12 @@ edition = "2021" [dependencies] microbit-v2 = "0.15" cortex-m-rt = "0.7" +critical-section = "1" rtt-target = "0.5" panic-rtt-target = "0.1" heapless = "0.8" -tiny-led-matrix = "1.0" -embedded-hal = "1.0" +tiny-led-matrix = "1" +embedded-hal = "1" [dependencies.cortex-m] version = "0.7" From 259531b36b6de16010562f1121de2c722412962a Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 26 Oct 2024 11:20:33 -0700 Subject: [PATCH 04/38] added forgotten interrupt example --- mdbook/src/14-interrupts/examples/pokier.rs | 56 +++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 mdbook/src/14-interrupts/examples/pokier.rs diff --git a/mdbook/src/14-interrupts/examples/pokier.rs b/mdbook/src/14-interrupts/examples/pokier.rs new file mode 100644 index 00000000..2701ad1e --- /dev/null +++ b/mdbook/src/14-interrupts/examples/pokier.rs @@ -0,0 +1,56 @@ +#![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 TE: Mutex>> = + Mutex::new(RefCell::new(None)); + +#[interrupt] +fn GPIOTE() { + rprintln!("ouch"); + critical_section::with(|cs| { + let cell = TE.borrow(cs).borrow(); + let channel = cell.as_ref().unwrap().channel0(); + channel.reset_events(); + }); +} + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let button_a = board.buttons.button_a.into_floating_input(); + let gpiote = gpiote::Gpiote::new(board.GPIOTE); + let channel = gpiote.channel0(); + channel + .input_pin(&button_a.degrade()) + .lo_to_hi() + .enable_interrupt(); + channel.reset_events(); + + critical_section::with(|cs| { + let mut cell = TE.borrow(cs).borrow_mut(); + *cell = Some(gpiote); + }); + unsafe { pac::NVIC::unmask(pac::Interrupt::GPIOTE) }; + pac::NVIC::unpend(pac::Interrupt::GPIOTE); + loop { + asm::wfi(); + rprintln!("got poked"); + } +} From daff3e07dfacd868195133a336a5f987ed5a4020 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 26 Oct 2024 11:44:51 -0700 Subject: [PATCH 05/38] added first-draft text of interrupt chapter --- mdbook/src/14-interrupts/README.md | 385 +++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 mdbook/src/14-interrupts/README.md diff --git a/mdbook/src/14-interrupts/README.md b/mdbook/src/14-interrupts/README.md new file mode 100644 index 00000000..ca2a4f0f --- /dev/null +++ b/mdbook/src/14-interrupts/README.md @@ -0,0 +1,385 @@ +## Interrupts + +So far, we've gone though a fair bunch of topics about +embedded software. We've read out buttons, waited for +timers, done serial communication, and talked to other +things on the Microbit board 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 + +Computation is always contextual: the core always needs memory to load inputs and store outputs to. +Our microcontroller is of what's known as a load-store-architecture, and as such +the core does not store and load it's computation parameters and results in RAM directly. +Instead, our core has access to a small amount scratch pad memory: the core registers. +Note that, confusingly, these core registers are different from the registers we've discussed in chapter 7. + +As far as the core is concerned, all context about the computation that it is doing is stored +in the core registers. If the core is going to switch tasks, it must store the contents +of the core registers somewhere, so that the other task can use them as their own scratchpad memory. +And 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 core registers on the stack. + +The next step is actually jumping to the code that should be run in response to an interrupt. +Interrupt Service Routines (ISRs), often referred to as interrupt handlers, +are special sections in your application code that get executed by the core +in response to specific interrupts. + +## Example with panicking goes here! + +Here's an example of some code that defines an ISR and configures an interrupt: +```rust +/* Timer goes off and program goes BOOM example */ +``` + +In case of our microcontroller, you may +define an ISR that gets executed when I2C is ready, and another one that gets +executed in response to a button press. Inside an ISR you can do pretty much +anything you want, but it's good practice to keep the interrupt handlers +short and quick. + +Once the ISR is done (NOTE: Done automatically on return), the core loads back the original content of its core +registers and returns to the point where it left off, almost as if nothing happened. + +But if the core just goes on with its life after handling an interrupt, how does +your device know that it happened? And seeing as an ISR doesn't have any input parameters, +how can ISR code interact with application code? + +> Note to @hdoordt: Please "hand off"/end here by making the point +> that interrupts don't take/return anything, so `fn() -> ()` or +> `void func(void)`, or let me know so I can change the intro of +> the next section! -James + + +## James: Interlude about sharing + +> Note: Stealing from https://onevariable.com/blog/interrupts-is-threads/ + +As we mentioned in the last section, when an interrupt occurs we aren't passed +any arguments, so how do we get access to things like the peripherals, or other +information we might need? + +### How you do it in desktop rust + +> * Spawn a thread (pass data in) +> * By ownership +> * With Arc +> * With ArcMutex +> * Have some kind of globals +> * With ArcMutex +> * With Lazy + +In "desktop" 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 +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 we want to SHARE something, and still have access to it in the original thread, +we usually can't pass a reference to it. If we 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}"); +} +``` + +We 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 +``` + +We need to **make sure the data lives long enough** for both the current thread and the +new thread we are creating. We can do this by putting it in an `Arc`, or an 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, that we 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! We can now access the data in both the main thread as long as we'd +like. But what if we want to *mutate* the data in both places? + +For this, we usually need some kind of "inner mutability", a type that doesn't +require an `&mut` to modify. On the desktop, we'd typically reach for a type +like a `Mutex`, which requires us to `lock()` it before we can 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 we 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 we run this code, we get: + +```text +hello +hello | thread was here! | +``` + +### Why does desktop 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 the same time + +If Rust allowed us to access data that might not live long enough, like data borrowed +from one thread into another, we might get corrupted data if the original thread +ends or panics, and the second thread tries to access the data that is now invalid. + +If Rust allowed two pieces of code to access the same data at the same, we could have +a data race, or the data could end up corrupted. + +### What's the same in embedded rust? + +In embedded Rust we care about the same things when it comes to sharing data with +interrupts! 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 accessing some data shared with the +interrupt, just to have the interrupt run and ALSO access that data! + +In fact, in embedded Rust, we model interrupts in almost exactly the same way +that threads are modeled in Rust, meaning that the same rules apply, for the +same reasons. + +### What's different in embedded rust? + +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 which point they run, but without access to any context. + +They can also be triggered multiple times, once for each time that the event +occurs. + +Since we can't pass context to interrupts as arguments like a function, we +need to find another place to store that data. + +Additionally, in many cases we don't have access to heap allocations, that +are used by things like `Arc` above to store our data. + +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 an interrupt can +access: `static`s. + +TODO AJM: Next talk about how statics only (safely) allow read access, we need +inner mutability to get write access, show something with a mutex'd integer that +we can init in const context + +TODO AJM: THEN talk about data that doesn't exist at startup, like sticking a +peripheral in after being configured, and how we do that, something like Lazy +Use Bart's crate for now, maybe add Lazy to the Blocking Mutex crate? + +## Working With Interrupts: Blinky Button + +## Under the hood + +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, this is called +the NVIC -- the nested vector interrupt controller. + +> **NOTE** On other microcontroller architectures such as RISC-V, the names and +> details discussed here might differ, but the underlying principles are +> generally very similar. + +The NVIC can receive requests to trigger an interrupt from many peripherals, +and it's even common for a peripheral to have multiple possible interrupts, for +example a GPIO having an interrupt for each pin, or a UART having both a "data +received" and "data finished transmission" interrupt. Its job is to prioritise +these interrupts, remember which ones still need to be procesed, and then cause +the processor to run the relevant interrupt handler code. + +Depending on its configuration, the NVIC can ensure the current interrupt is +fully processed before a new one is executed, or it can stop the processor in +the middle of one interrupt in order to handle another that's higher priority. +This is called "pre-emption" and allows processors to respond very quickly to +critical events. For example, a robot controller might use low-priority +interrupts to keep track sending status information to the operator, but also +have a high-priority interrupt to detect an emergency stop button being pushed +so it can immediately stop moving the motors. You wouldn't want it to wait +until it had finished sending a data packet to get around to stopping! + +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 their priorities, and manually trigger them. Frameworks such as [RTIC] can +handle NVIC configuration for you, taking advantage of its 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 interrupts +in 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 causes our function to be given a +specific name related to the interrupt it handles. Finally, 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 +[Exceptions]: https://docs.rust-embedded.org/book/start/exceptions.html +[Interrupts]: https://docs.rust-embedded.org/book/start/interrupts.html From 5f74b91b4ebd1e7ed76cb989f05ccdecaf9f8132 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 26 Oct 2024 15:20:00 -0700 Subject: [PATCH 06/38] finished initial edit of interrupts, added subsections --- mdbook/src/14-interrupts/README.md | 417 +++--------------- mdbook/src/14-interrupts/examples/poke.rs | 23 +- .../sharing-data-with-globals.md | 203 +++++++++ mdbook/src/14-interrupts/under-the-hood.md | 75 ++++ mdbook/src/SUMMARY.md | 3 + 5 files changed, 349 insertions(+), 372 deletions(-) create mode 100644 mdbook/src/14-interrupts/sharing-data-with-globals.md create mode 100644 mdbook/src/14-interrupts/under-the-hood.md diff --git a/mdbook/src/14-interrupts/README.md b/mdbook/src/14-interrupts/README.md index ca2a4f0f..260fc451 100644 --- a/mdbook/src/14-interrupts/README.md +++ b/mdbook/src/14-interrupts/README.md @@ -1,385 +1,72 @@ ## Interrupts -So far, we've gone though a fair bunch of topics about -embedded software. We've read out buttons, waited for -timers, done serial communication, and talked to other -things on the Microbit board 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. +So far, we've gone though a fair bunch of topics about embedded software. We've read out buttons, +waited for timers, done serial communication, and talked to other things on the Microbit board 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 Computation is always contextual: the core always needs memory to load inputs and store outputs to. -Our microcontroller is of what's known as a load-store-architecture, and as such -the core does not store and load it's computation parameters and results in RAM directly. -Instead, our core has access to a small amount scratch pad memory: the core registers. -Note that, confusingly, these core registers are different from the registers we've discussed in chapter 7. +Our microcontroller is of what's known as a load-store-architecture, and as such the core does not +store and load it's computation parameters and results in RAM directly. Instead, our core has +access to a small amount scratch pad memory: the CPU registers. Confusingly, these CPU registers +are different from the device registers we discussed earlier in the [Registers] chapter. -As far as the core is concerned, all context about the computation that it is doing is stored -in the core registers. If the core is going to switch tasks, it must store the contents -of the core registers somewhere, so that the other task can use them as their own scratchpad memory. -And 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 core registers on the stack. +As far as the core is concerned, all context about the computation that it is doing 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 them as their own scratchpad memory. 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. -Interrupt Service Routines (ISRs), often referred to as interrupt handlers, -are special sections in your application code that get executed by the core -in response to specific interrupts. - -## Example with panicking goes here! - -Here's an example of some code that defines an ISR and configures an interrupt: -```rust -/* Timer goes off and program goes BOOM example */ -``` - -In case of our microcontroller, you may -define an ISR that gets executed when I2C is ready, and another one that gets -executed in response to a button press. Inside an ISR you can do pretty much -anything you want, but it's good practice to keep the interrupt handlers -short and quick. - -Once the ISR is done (NOTE: Done automatically on return), the core loads back the original content of its core -registers and returns to the point where it left off, almost as if nothing happened. - -But if the core just goes on with its life after handling an interrupt, how does -your device know that it happened? And seeing as an ISR doesn't have any input parameters, -how can ISR code interact with application code? - -> Note to @hdoordt: Please "hand off"/end here by making the point -> that interrupts don't take/return anything, so `fn() -> ()` or -> `void func(void)`, or let me know so I can change the intro of -> the next section! -James - - -## James: Interlude about sharing - -> Note: Stealing from https://onevariable.com/blog/interrupts-is-threads/ - -As we mentioned in the last section, when an interrupt occurs we aren't passed -any arguments, so how do we get access to things like the peripherals, or other -information we might need? - -### How you do it in desktop rust - -> * Spawn a thread (pass data in) -> * By ownership -> * With Arc -> * With ArcMutex -> * Have some kind of globals -> * With ArcMutex -> * With Lazy - -In "desktop" 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 -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 we want to SHARE something, and still have access to it in the original thread, -we usually can't pass a reference to it. If we 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}"); -} -``` - -We 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 -``` - -We need to **make sure the data lives long enough** for both the current thread and the -new thread we are creating. We can do this by putting it in an `Arc`, or an 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, that we 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! We can now access the data in both the main thread as long as we'd -like. But what if we want to *mutate* the data in both places? +Interrupt Service Routines (ISRs), often referred to as interrupt handlers, are special functions in +your application code that get called by the core in response to specific interrupts. 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. -For this, we usually need some kind of "inner mutability", a type that doesn't -require an `&mut` to modify. On the desktop, we'd typically reach for a type -like a `Mutex`, which requires us to `lock()` it before we can gain mutable access -to the data. +## Poke The MB2 -That might look something like this: +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 -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 we 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! - } -} +{{#include examples/poke.rs}} ``` -If we run this code, we get: - -```text -hello -hello | thread was here! | -``` - -### Why does desktop 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 the same time - -If Rust allowed us to access data that might not live long enough, like data borrowed -from one thread into another, we might get corrupted data if the original thread -ends or panics, and the second thread tries to access the data that is now invalid. - -If Rust allowed two pieces of code to access the same data at the same, we could have -a data race, or the data could end up corrupted. - -### What's the same in embedded rust? - -In embedded Rust we care about the same things when it comes to sharing data with -interrupts! 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 accessing some data shared with the -interrupt, just to have the interrupt run and ALSO access that data! - -In fact, in embedded Rust, we model interrupts in almost exactly the same way -that threads are modeled in Rust, meaning that the same rules apply, for the -same reasons. - -### What's different in embedded rust? - -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 which point they run, but without access to any context. - -They can also be triggered multiple times, once for each time that the event -occurs. - -Since we can't pass context to interrupts as arguments like a function, we -need to find another place to store that data. - -Additionally, in many cases we don't have access to heap allocations, that -are used by things like `Arc` above to store our data. - -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 an interrupt can -access: `static`s. - -TODO AJM: Next talk about how statics only (safely) allow read access, we need -inner mutability to get write access, show something with a mutex'd integer that -we can init in const context - -TODO AJM: THEN talk about data that doesn't exist at startup, like sticking a -peripheral in after being configured, and how we do that, something like Lazy -Use Bart's crate for now, maybe add Lazy to the Blocking Mutex crate? - -## Working With Interrupts: Blinky Button - -## Under the hood - -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, this is called -the NVIC -- the nested vector interrupt controller. - -> **NOTE** On other microcontroller architectures such as RISC-V, the names and -> details discussed here might differ, but the underlying principles are -> generally very similar. - -The NVIC can receive requests to trigger an interrupt from many peripherals, -and it's even common for a peripheral to have multiple possible interrupts, for -example a GPIO having an interrupt for each pin, or a UART having both a "data -received" and "data finished transmission" interrupt. Its job is to prioritise -these interrupts, remember which ones still need to be procesed, and then cause -the processor to run the relevant interrupt handler code. - -Depending on its configuration, the NVIC can ensure the current interrupt is -fully processed before a new one is executed, or it can stop the processor in -the middle of one interrupt in order to handle another that's higher priority. -This is called "pre-emption" and allows processors to respond very quickly to -critical events. For example, a robot controller might use low-priority -interrupts to keep track sending status information to the operator, but also -have a high-priority interrupt to detect an emergency stop button being pushed -so it can immediately stop moving the motors. You wouldn't want it to wait -until it had finished sending a data packet to get around to stopping! - -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 their priorities, and manually trigger them. Frameworks such as [RTIC] can -handle NVIC configuration for you, taking advantage of its 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 +The ISR handler function is "special". The name `GPIOTE` is required here, and the function must be +decorated with `#[interrupt]` so that it returns using a return-from-interrupt instruction rather +than the normal way. The function may not take arguments and must return `()`. -When describing the NVIC, I said it could "cause the processor to run the -relevant interrupt handler code". But how does that actually work? +There are two steps to configure the interrupt. First, the GPIOTE must be set up to generate an +interrupt when the wire connect 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 a bogus interrupt before you are ready to handle it. -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 interrupts -in 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. +In case of our microcontroller, you may define ISR's 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. -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 causes our function to be given a -specific name related to the interrupt it handles. Finally, 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. +When the ISR function returns (using a magic instruction), the core loads back the original content +of its core registers and returns to the point where it left off, almost as if nothing happened. -For more details on how these interrupt handlers are managed in Rust, see the -[Exceptions] and [Interrupts] chapters in the Embedded Rust Book. +But if the core just goes on with its life after handling an interrupt, how does your device know +that it happened? Seeing as an ISR doesn't have any input parameters or result, how can ISR code +interact with application code? -[Architecture Reference Manual]: https://developer.arm.com/documentation/ddi0403/latest -[`cortex-m-rt`]: https://docs.rs/cortex-m-rt -[Exceptions]: https://docs.rust-embedded.org/book/start/exceptions.html -[Interrupts]: https://docs.rust-embedded.org/book/start/interrupts.html +[Registers]: https://docs.rust-embedded.org/discovery-mb2/07-registers diff --git a/mdbook/src/14-interrupts/examples/poke.rs b/mdbook/src/14-interrupts/examples/poke.rs index 5e452ddd..55ef7370 100644 --- a/mdbook/src/14-interrupts/examples/poke.rs +++ b/mdbook/src/14-interrupts/examples/poke.rs @@ -14,10 +14,12 @@ use microbit::{ }, }; +/// This "function" will be called when an interrupt is received. For now, just +/// report and panic. #[interrupt] fn GPIOTE() { rprintln!("ouch"); - asm::bkpt(); + panic!(); } #[entry] @@ -25,16 +27,23 @@ 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()) - .lo_to_hi() - .enable_interrupt(); - channel.reset_events(); + 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 { - asm::wfe(); + // "wait for interrupt": CPU goes to sleep until an interrupt. + asm::wfi(); } } diff --git a/mdbook/src/14-interrupts/sharing-data-with-globals.md b/mdbook/src/14-interrupts/sharing-data-with-globals.md new file mode 100644 index 00000000..459f0797 --- /dev/null +++ b/mdbook/src/14-interrupts/sharing-data-with-globals.md @@ -0,0 +1,203 @@ +## Sharing Data With Globals + +> **NOTE:** This content is partially taken from +> , which contains more discussion about this +> topic. + +As we mentioned in the last section, when an interrupt occurs we aren't passed any arguments. How do +we get access to things needed in the interrupt handler, such as the peripherals or other main +program state? + +### 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 +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 we want to *share* something, and still have access to it in the original thread, +we usually can't pass a reference to it. If we 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}"); +} +``` + +We 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 +``` + +We need to *make sure the data lives long enough* for both the current thread and the new thread we +are creating. We 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, that we 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! We can now access the data in both the main thread as long as we'd +like. But what if we want to *mutate* the data in both places? + +For this, we usually need some kind of "inner mutability", a type that doesn't +require an `&mut` to modify. On the desktop, we'd typically reach for a type +like a `Mutex`, which requires us to `lock()` it before we can 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 we 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 we run this code, we get: + +```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 the same 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 access 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 accessing some +data shared with the interrupt, just to have the interrupt run and ALSO access that 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 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. + +> **TODO AJM:** Next talk about how statics only (safely) allow read access, we need +inner mutability to get write access, show something with a mutex'd integer that +we can init in const context + +> **TODO AJM:** THEN talk about data that doesn't exist at startup, like sticking a +peripheral in after being configured, and how we do that, something like Lazy +Use Bart's crate for now, maybe add Lazy to the Blocking Mutex crate? diff --git a/mdbook/src/14-interrupts/under-the-hood.md b/mdbook/src/14-interrupts/under-the-hood.md new file mode 100644 index 00000000..aee614b3 --- /dev/null +++ b/mdbook/src/14-interrupts/under-the-hood.md @@ -0,0 +1,75 @@ + +## Under The Hood + +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 prioritise these interrupts, remember which ones still need to be procesed, +and then cause the processor to run the relevant interrupt handler code. + +Depending on its configuration, the NVIC can ensure the current interrupt is fully processed before +a new one is executed, or it can stop the processor in the middle of one interrupt in order to +handle another that's higher priority. This is called "preemption" and allows processors to respond +very quickly to critical events. For example, a robot controller might use low-priority interrupts +to keep track sending status information to the operator, but also have a high-priority interrupt to +detect an emergency stop button being pushed so it can immediately stop moving the motors. You +wouldn't want it to wait until it had finished sending a data packet to get around to stopping! + +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 +[Exceptions]: https://docs.rust-embedded.org/book/start/exceptions.html +[Interrupts]: https://docs.rust-embedded.org/book/start/interrupts.html diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index 9011a49d..b9e79d2c 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -57,6 +57,9 @@ - [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) +- [Interrupts](14-interrupts/README.md) + - [Sharing data with globals](14-interrupts/sharing-data-with-globals.md) + - [Under the hood](14-interrupts/under-the-hood.md) - [Snake game](14-snake-game/README.md) - [Game logic](14-snake-game/game-logic.md) - [Controls](14-snake-game/controls.md) From 529ace178a51c443e6ea6ab94ba86d6ce677af0a Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 26 Oct 2024 15:39:53 -0700 Subject: [PATCH 07/38] cleaned up pokier.rs --- mdbook/src/14-interrupts/Cargo.toml | 8 +++--- mdbook/src/14-interrupts/examples/pokier.rs | 28 +++++++++------------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/mdbook/src/14-interrupts/Cargo.toml b/mdbook/src/14-interrupts/Cargo.toml index bda9a586..5dd6b785 100644 --- a/mdbook/src/14-interrupts/Cargo.toml +++ b/mdbook/src/14-interrupts/Cargo.toml @@ -4,14 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] -microbit-v2 = "0.15" cortex-m-rt = "0.7" critical-section = "1" -rtt-target = "0.5" +critical-section-lock-mut = "0.1" +microbit-v2 = "0.15" panic-rtt-target = "0.1" -heapless = "0.8" -tiny-led-matrix = "1" -embedded-hal = "1" +rtt-target = "0.5" [dependencies.cortex-m] version = "0.7" diff --git a/mdbook/src/14-interrupts/examples/pokier.rs b/mdbook/src/14-interrupts/examples/pokier.rs index 2701ad1e..a6483bf7 100644 --- a/mdbook/src/14-interrupts/examples/pokier.rs +++ b/mdbook/src/14-interrupts/examples/pokier.rs @@ -1,11 +1,9 @@ #![no_main] #![no_std] -use core::cell::RefCell; - use cortex_m::asm; use cortex_m_rt::entry; -use critical_section::Mutex; +use critical_section_lock_mut::LockMut; use panic_rtt_target as _; use rtt_target::{rprintln, rtt_init_print}; @@ -17,17 +15,12 @@ use microbit::{ }, }; -static TE: Mutex>> = - Mutex::new(RefCell::new(None)); +static TE: LockMut = LockMut::new(); #[interrupt] fn GPIOTE() { rprintln!("ouch"); - critical_section::with(|cs| { - let cell = TE.borrow(cs).borrow(); - let channel = cell.as_ref().unwrap().channel0(); - channel.reset_events(); - }); + TE.with_lock(|te| te.channel0().reset_events()); } #[entry] @@ -35,21 +28,24 @@ 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()) - .lo_to_hi() + .hi_to_lo() .enable_interrupt(); channel.reset_events(); - - critical_section::with(|cs| { - let mut cell = TE.borrow(cs).borrow_mut(); - *cell = Some(gpiote); - }); + TE.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(); rprintln!("got poked"); } From 977394b695ade0387c2122d3db49da2fcb75fa8b Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sun, 27 Oct 2024 21:42:58 -0700 Subject: [PATCH 08/38] fixed some text in interrupts --- mdbook/src/14-interrupts/README.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/mdbook/src/14-interrupts/README.md b/mdbook/src/14-interrupts/README.md index 260fc451..6b1c713f 100644 --- a/mdbook/src/14-interrupts/README.md +++ b/mdbook/src/14-interrupts/README.md @@ -58,12 +58,23 @@ interrupt when the wire connect to Button A goes from high to low voltage. Secon configured to allow the interrupt. Order matters a bit: doing things in the "wrong" order may generate a bogus interrupt before you are ready to handle it. -In case of our microcontroller, you may define ISR's 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. - -When the ISR function returns (using a magic instruction), the core loads back the original content -of its core registers and returns to the point where it left off, almost as if nothing happened. +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 GPIOTE records when an interrupt +has been issued, and that record is kept until it is explicitly cleared by the running +program. Without the `panic!()`, when the interrupt handler returns the GPIOTE will re-enable the +interrupt, notice that an interrupt has been issued and not cleared, and run the handler again. This +will continue forever: each time the interrupt handler returns it will be called again. In the next +section we will see how to clear the interrupt indication from within the interrupt handler. + +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. + +When the ISR function returns (using a magic instruction), the CPU looks to see if interrupts have +happened that need to be handled, and if so calls one of the handlers (according to a priority order +set by the NVIC). Otherwise, the CPU restores the CPU registers and returns to the running program +as if nothing has happened. But if the core just goes on with its life after handling an interrupt, how does your device know that it happened? Seeing as an ISR doesn't have any input parameters or result, how can ISR code From 5035fc442076ea80bac4171cd9ca70061ffff1a8 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Mon, 28 Oct 2024 16:08:32 -0700 Subject: [PATCH 09/38] added more content to sharing-data-with-globals --- .../src/14-interrupts/examples/count-once.rs | 58 ++++++++++ .../sharing-data-with-globals.md | 105 +++++++++++++----- 2 files changed, 135 insertions(+), 28 deletions(-) create mode 100644 mdbook/src/14-interrupts/examples/count-once.rs diff --git a/mdbook/src/14-interrupts/examples/count-once.rs b/mdbook/src/14-interrupts/examples/count-once.rs new file mode 100644 index 00000000..68603ffa --- /dev/null +++ b/mdbook/src/14-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!("ouch {}", 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/14-interrupts/sharing-data-with-globals.md b/mdbook/src/14-interrupts/sharing-data-with-globals.md index 459f0797..3960ea21 100644 --- a/mdbook/src/14-interrupts/sharing-data-with-globals.md +++ b/mdbook/src/14-interrupts/sharing-data-with-globals.md @@ -1,20 +1,20 @@ ## Sharing Data With Globals -> **NOTE:** This content is partially taken from -> , which contains more discussion about this +> **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 we mentioned in the last section, when an interrupt occurs we aren't passed any arguments. How do -we get access to things needed in the interrupt handler, such as the peripherals or other main -program state? +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 +### "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 -by ownership. +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 @@ -28,8 +28,8 @@ std::thread::spawn(move || { }); ``` -If we want to *share* something, and still have access to it in the original thread, -we usually can't pass a reference to it. If we do this: +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}; @@ -52,7 +52,7 @@ fn main() { } ``` -We get an error like this: +you'll get an error like this: ```text error[E0597]: `data` does not live long enough @@ -74,8 +74,8 @@ error[E0597]: `data` does not live long enough | - `data` dropped here while still borrowed ``` -We need to *make sure the data lives long enough* for both the current thread and the new thread we -are creating. We can do this by putting it in an `Arc` (Atomically Reference Counted heap +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 @@ -86,7 +86,7 @@ fn main() { let data = Arc::new(String::from("hello")); let handle = spawn({ - // Make a copy of the handle, that we GIVE to the new thread. + // 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(); @@ -102,13 +102,12 @@ fn main() { } ``` -This is great! We can now access the data in both the main thread as long as we'd -like. But what if we want to *mutate* the data in both places? +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, we usually need some kind of "inner mutability", a type that doesn't -require an `&mut` to modify. On the desktop, we'd typically reach for a type -like a `Mutex`, which requires us to `lock()` it before we can gain mutable access -to the data. +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: @@ -127,7 +126,7 @@ fn main() { } let handle = spawn({ - // Make a copy of the handle, that we GIVE to the new thread. + // 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(); @@ -152,7 +151,7 @@ fn main() { } ``` -If we run this code, we get: +If you run this code, you will see: ```text hello @@ -162,13 +161,13 @@ 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 the same time +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 access the same data at the same, we could have a data race, or the data could -end up corrupted. +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 @@ -194,10 +193,60 @@ 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. -> **TODO AJM:** Next talk about how statics only (safely) allow read access, we need -inner mutability to get write access, show something with a mutex'd integer that -we can init in const context +### 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 us the ability to share state between ISRs and the main program +(`examples/count-once.rs`). + +```rust +{{#include examples/count-once.rs}} +``` + +We still cannot safely return from our ISR, but now we are in a position to do something about that: +share the `GPIOTE` with the ISR so that the ISR can clear the interrupt. > **TODO AJM:** THEN talk about data that doesn't exist at startup, like sticking a peripheral in after being configured, and how we do that, something like Lazy Use Bart's crate for now, maybe add Lazy to the Blocking Mutex crate? + +[Interrupts Is Threads]: https://onevariable.com/blog/interrupts-is-threads From 8924ab602a267097a2b9b8d4580264c0bb01abc7 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Fri, 8 Nov 2024 11:16:13 -0800 Subject: [PATCH 10/38] added more content to interrupts chapter --- .../src/14-interrupts/examples/count-once.rs | 2 +- .../examples/{pokier.rs => count-up.rs} | 29 +++++++++++++------ .../sharing-data-with-globals.md | 15 ++++++---- 3 files changed, 30 insertions(+), 16 deletions(-) rename mdbook/src/14-interrupts/examples/{pokier.rs => count-up.rs} (62%) diff --git a/mdbook/src/14-interrupts/examples/count-once.rs b/mdbook/src/14-interrupts/examples/count-once.rs index 68603ffa..43c37976 100644 --- a/mdbook/src/14-interrupts/examples/count-once.rs +++ b/mdbook/src/14-interrupts/examples/count-once.rs @@ -26,7 +26,7 @@ fn GPIOTE() { critical_section::with(|cs| { let mut count = COUNTER.borrow(cs).borrow_mut(); *count += 1; - rprintln!("ouch {}", count); + rprintln!("count: {}", count); }); panic!(); } diff --git a/mdbook/src/14-interrupts/examples/pokier.rs b/mdbook/src/14-interrupts/examples/count-up.rs similarity index 62% rename from mdbook/src/14-interrupts/examples/pokier.rs rename to mdbook/src/14-interrupts/examples/count-up.rs index a6483bf7..940981ac 100644 --- a/mdbook/src/14-interrupts/examples/pokier.rs +++ b/mdbook/src/14-interrupts/examples/count-up.rs @@ -15,12 +15,20 @@ use microbit::{ }, }; -static TE: LockMut = LockMut::new(); +struct Counter { + count: usize, + gpiote: gpiote::Gpiote, +} + +static COUNTER: LockMut = LockMut::new(); #[interrupt] fn GPIOTE() { - rprintln!("ouch"); - TE.with_lock(|te| te.channel0().reset_events()); + COUNTER.with_lock(|counter| { + counter.count += 1; + rprintln!("isr count: {}", counter.count); + counter.gpiote.channel0().reset_events(); + }); } #[entry] @@ -29,8 +37,6 @@ fn main() -> ! { 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 @@ -38,15 +44,20 @@ fn main() -> ! { .hi_to_lo() .enable_interrupt(); channel.reset_events(); - TE.init(gpiote); + let counter = Counter { + count: 0, + gpiote, + }; + COUNTER.init(counter); - // 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(); - rprintln!("got poked"); + + COUNTER.with_lock(|counter| { + rprintln!("host count: {}", counter.count); + }); } } diff --git a/mdbook/src/14-interrupts/sharing-data-with-globals.md b/mdbook/src/14-interrupts/sharing-data-with-globals.md index 3960ea21..bbbf9941 100644 --- a/mdbook/src/14-interrupts/sharing-data-with-globals.md +++ b/mdbook/src/14-interrupts/sharing-data-with-globals.md @@ -235,18 +235,21 @@ it. Instead, you initiate a critical section with a closure that receives a "cri 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 us the ability to share state between ISRs and the main program +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}} ``` -We still cannot safely return from our ISR, but now we are in a position to do something about that: -share the `GPIOTE` with the ISR so that the ISR can clear the interrupt. +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. -> **TODO AJM:** THEN talk about data that doesn't exist at startup, like sticking a -peripheral in after being configured, and how we do that, something like Lazy -Use Bart's crate for now, maybe add Lazy to the Blocking Mutex crate? +### 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 we 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. [Interrupts Is Threads]: https://onevariable.com/blog/interrupts-is-threads From b9919d4991ecff04a188eb9484deaa772c5b4bdb Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Tue, 12 Nov 2024 16:09:16 -0800 Subject: [PATCH 11/38] added count.rs --- mdbook/src/14-interrupts/examples/count.rs | 59 ++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 mdbook/src/14-interrupts/examples/count.rs diff --git a/mdbook/src/14-interrupts/examples/count.rs b/mdbook/src/14-interrupts/examples/count.rs new file mode 100644 index 00000000..576b7bcf --- /dev/null +++ b/mdbook/src/14-interrupts/examples/count.rs @@ -0,0 +1,59 @@ +#![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: AtomicUsize = AtomicUsize::new(0); +static GPIOTE_Peripheral: LockMut = LockMut::new(); + +/// This "function" will be called when an interrupt is received. For now, just +/// report and panic. +#[interrupt] +fn GPIOTE() { + let mut count = COUNTER.fetch_add(1, AcqRel); + rprintln!("ouch {}", count + 1); + GPIOTE_Peripheral.with(|gpiote| { + // TODO + }); + 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(); + } +} From 3848fb39e3c541ff676a6bea90b13e9945dfc9c6 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 16 Nov 2024 11:12:07 -0800 Subject: [PATCH 12/38] finished interrupt count example --- mdbook/src/14-interrupts/examples/count.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mdbook/src/14-interrupts/examples/count.rs b/mdbook/src/14-interrupts/examples/count.rs index 576b7bcf..8b6f1b3f 100644 --- a/mdbook/src/14-interrupts/examples/count.rs +++ b/mdbook/src/14-interrupts/examples/count.rs @@ -1,11 +1,11 @@ #![no_main] #![no_std] -use core::cell::RefCell; +use core::sync::atomic::{AtomicUsize, Ordering::AcqRel}; use cortex_m::asm; use cortex_m_rt::entry; -use critical_section::Mutex; +use critical_section_lock_mut::LockMut; use panic_rtt_target as _; use rtt_target::{rprintln, rtt_init_print}; @@ -18,18 +18,17 @@ use microbit::{ }; static COUNTER: AtomicUsize = AtomicUsize::new(0); -static GPIOTE_Peripheral: LockMut = LockMut::new(); +static GPIOTE_PERIPHERAL: LockMut = LockMut::new(); /// This "function" will be called when an interrupt is received. For now, just /// report and panic. #[interrupt] fn GPIOTE() { - let mut count = COUNTER.fetch_add(1, AcqRel); + let count = COUNTER.fetch_add(1, AcqRel); rprintln!("ouch {}", count + 1); - GPIOTE_Peripheral.with(|gpiote| { - // TODO + GPIOTE_PERIPHERAL.with_lock(|gpiote| { + gpiote.channel0().reset_events(); }); - panic!(); } #[entry] @@ -47,6 +46,7 @@ fn main() -> ! { .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) }; From 0fc56a77595517a709147f4864bc56edb42c26a1 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 16 Nov 2024 11:32:30 -0800 Subject: [PATCH 13/38] finished sharing data with globals subsection --- mdbook/src/14-interrupts/examples/count.rs | 2 -- .../sharing-data-with-globals.md | 19 +++++++++++++++++-- mdbook/src/SUMMARY.md | 3 ++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/mdbook/src/14-interrupts/examples/count.rs b/mdbook/src/14-interrupts/examples/count.rs index 8b6f1b3f..7c290ed5 100644 --- a/mdbook/src/14-interrupts/examples/count.rs +++ b/mdbook/src/14-interrupts/examples/count.rs @@ -20,8 +20,6 @@ use microbit::{ static COUNTER: AtomicUsize = AtomicUsize::new(0); static GPIOTE_PERIPHERAL: LockMut = LockMut::new(); -/// This "function" will be called when an interrupt is received. For now, just -/// report and panic. #[interrupt] fn GPIOTE() { let count = COUNTER.fetch_add(1, AcqRel); diff --git a/mdbook/src/14-interrupts/sharing-data-with-globals.md b/mdbook/src/14-interrupts/sharing-data-with-globals.md index bbbf9941..093dcc5a 100644 --- a/mdbook/src/14-interrupts/sharing-data-with-globals.md +++ b/mdbook/src/14-interrupts/sharing-data-with-globals.md @@ -248,8 +248,23 @@ 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 we want to share 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. +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 +(`examples/count.rs`). Give this example a run and note that 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*. 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/SUMMARY.md b/mdbook/src/SUMMARY.md index b9e79d2c..546ca3c7 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -58,7 +58,8 @@ - [The challenge](13-punch-o-meter/the-challenge.md) - [My solution](13-punch-o-meter/my-solution.md) - [Interrupts](14-interrupts/README.md) - - [Sharing data with globals](14-interrupts/sharing-data-with-globals.md) + - [Sharing data wit globals](14-interrupts/sharing-data-with-globals.md) + - [Debouncing with interrupts](14-interrupts/debouncing-with-interrupts.md) - [Under the hood](14-interrupts/under-the-hood.md) - [Snake game](14-snake-game/README.md) - [Game logic](14-snake-game/game-logic.md) From 78df7df31d938e7d765369eb1dbb3c03113060c7 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sun, 17 Nov 2024 16:02:38 -0800 Subject: [PATCH 14/38] started on interrupt button debounce stuff --- .../examples/{count-up.rs => count-bounce.rs} | 33 ++++---- .../14-interrupts/examples/count-debounce.rs | 78 +++++++++++++++++++ .../sharing-data-with-globals.md | 11 ++- 3 files changed, 99 insertions(+), 23 deletions(-) rename mdbook/src/14-interrupts/examples/{count-up.rs => count-bounce.rs} (56%) create mode 100644 mdbook/src/14-interrupts/examples/count-debounce.rs diff --git a/mdbook/src/14-interrupts/examples/count-up.rs b/mdbook/src/14-interrupts/examples/count-bounce.rs similarity index 56% rename from mdbook/src/14-interrupts/examples/count-up.rs rename to mdbook/src/14-interrupts/examples/count-bounce.rs index 940981ac..527c8a6f 100644 --- a/mdbook/src/14-interrupts/examples/count-up.rs +++ b/mdbook/src/14-interrupts/examples/count-bounce.rs @@ -1,6 +1,8 @@ #![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; @@ -15,19 +17,14 @@ use microbit::{ }, }; -struct Counter { - count: usize, - gpiote: gpiote::Gpiote, -} - -static COUNTER: LockMut = LockMut::new(); +static COUNTER: AtomicUsize = AtomicUsize::new(0); +static GPIOTE_PERIPHERAL: LockMut = LockMut::new(); #[interrupt] fn GPIOTE() { - COUNTER.with_lock(|counter| { - counter.count += 1; - rprintln!("isr count: {}", counter.count); - counter.gpiote.channel0().reset_events(); + let _ = COUNTER.fetch_add(1, AcqRel); + GPIOTE_PERIPHERAL.with_lock(|gpiote| { + gpiote.channel0().reset_events(); }); } @@ -37,6 +34,8 @@ fn main() -> ! { 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 @@ -44,20 +43,16 @@ fn main() -> ! { .hi_to_lo() .enable_interrupt(); channel.reset_events(); - let counter = Counter { - count: 0, - gpiote, - }; - COUNTER.init(counter); + 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(); - - COUNTER.with_lock(|counter| { - rprintln!("host count: {}", counter.count); - }); + let count = COUNTER.load(Acquire); + rprintln!("ouch {}", count); } } diff --git a/mdbook/src/14-interrupts/examples/count-debounce.rs b/mdbook/src/14-interrupts/examples/count-debounce.rs new file mode 100644 index 00000000..33f4e9ec --- /dev/null +++ b/mdbook/src/14-interrupts/examples/count-debounce.rs @@ -0,0 +1,78 @@ +#![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::{ + self, + gpiote, + pac::{self, interrupt}, + }, +}; + +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/14-interrupts/sharing-data-with-globals.md b/mdbook/src/14-interrupts/sharing-data-with-globals.md index 093dcc5a..a4b65be4 100644 --- a/mdbook/src/14-interrupts/sharing-data-with-globals.md +++ b/mdbook/src/14-interrupts/sharing-data-with-globals.md @@ -259,12 +259,15 @@ the `LockMut` type from the `critical-section-lock-mut` crate. This crate is a c 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 -(`examples/count.rs`). Give this example a run and note that the count is bumped up 1 on every push +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. Maybe. Especially if your MB2 is old, you may see a single press bump the counter by several. *This -is not a software bug*. In the next section, I'll talk about what might be going on and how we -should deal with it. +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 From 9046efce17a9236f3dda380db3669930c3fc809a Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Tue, 19 Nov 2024 10:33:14 -0800 Subject: [PATCH 15/38] started on interrupts and debouncing --- mdbook/src/14-interrupts/debouncing.md | 28 +++++++++++++++++++ .../sharing-data-with-globals.md | 11 +++++++- mdbook/src/SUMMARY.md | 4 +-- 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 mdbook/src/14-interrupts/debouncing.md diff --git a/mdbook/src/14-interrupts/debouncing.md b/mdbook/src/14-interrupts/debouncing.md new file mode 100644 index 00000000..b7624d98 --- /dev/null +++ b/mdbook/src/14-interrupts/debouncing.md @@ -0,0 +1,28 @@ +## 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 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. + +> **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/14-interrupts/sharing-data-with-globals.md b/mdbook/src/14-interrupts/sharing-data-with-globals.md index a4b65be4..31c905a0 100644 --- a/mdbook/src/14-interrupts/sharing-data-with-globals.md +++ b/mdbook/src/14-interrupts/sharing-data-with-globals.md @@ -1,6 +1,6 @@ ## Sharing Data With Globals -> **NOTE:** This content is partially taken with permission from the blog post +> **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. @@ -266,6 +266,15 @@ 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. +> **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`). Again, 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. diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index 546ca3c7..944447a9 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -58,8 +58,8 @@ - [The challenge](13-punch-o-meter/the-challenge.md) - [My solution](13-punch-o-meter/my-solution.md) - [Interrupts](14-interrupts/README.md) - - [Sharing data wit globals](14-interrupts/sharing-data-with-globals.md) - - [Debouncing with interrupts](14-interrupts/debouncing-with-interrupts.md) + - [Sharing data with globals](14-interrupts/sharing-data-with-globals.md) + - [Debouncing](14-interrupts/debouncing.md) - [Under the hood](14-interrupts/under-the-hood.md) - [Snake game](14-snake-game/README.md) - [Game logic](14-snake-game/game-logic.md) From 3062d99a9e91a7d621292c050fa7472358b76134 Mon Sep 17 00:00:00 2001 From: arnav Date: Wed, 26 Mar 2025 02:24:51 -0400 Subject: [PATCH 16/38] polling chapter initial draft --- .../examples/delay-print.rs | 1 - .../14-inputs-and-polling/.cargo/config.toml | 8 +++ mdbook/src/14-inputs-and-polling/Cargo.toml | 14 +++++ mdbook/src/14-inputs-and-polling/Embed.toml | 11 ++++ mdbook/src/14-inputs-and-polling/README.md | 24 +++++++++ .../examples/button-a-bsp.rs | 22 ++++++++ .../examples/button-a.rs | 17 ++++++ .../examples/polling-led-toggle.rs | 41 ++++++++++++++ .../src/14-inputs-and-polling/my-solution.md | 7 +++ .../14-inputs-and-polling/polling-sucks.md | 37 +++++++++++++ mdbook/src/14-inputs-and-polling/polling.md | 13 +++++ mdbook/src/14-inputs-and-polling/src/main.rs | 52 ++++++++++++++++++ .../14-inputs-and-polling/the-challenge.md | 15 ++++++ .../14-inputs-and-polling/volatile-reads.md | 54 +++++++++++++++++++ mdbook/src/SUMMARY.md | 21 ++++++-- 15 files changed, 332 insertions(+), 5 deletions(-) create mode 100644 mdbook/src/14-inputs-and-polling/.cargo/config.toml create mode 100644 mdbook/src/14-inputs-and-polling/Cargo.toml create mode 100644 mdbook/src/14-inputs-and-polling/Embed.toml create mode 100644 mdbook/src/14-inputs-and-polling/README.md create mode 100644 mdbook/src/14-inputs-and-polling/examples/button-a-bsp.rs create mode 100644 mdbook/src/14-inputs-and-polling/examples/button-a.rs create mode 100644 mdbook/src/14-inputs-and-polling/examples/polling-led-toggle.rs create mode 100644 mdbook/src/14-inputs-and-polling/my-solution.md create mode 100644 mdbook/src/14-inputs-and-polling/polling-sucks.md create mode 100644 mdbook/src/14-inputs-and-polling/polling.md create mode 100644 mdbook/src/14-inputs-and-polling/src/main.rs create mode 100644 mdbook/src/14-inputs-and-polling/the-challenge.md create mode 100644 mdbook/src/14-inputs-and-polling/volatile-reads.md 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/14-inputs-and-polling/.cargo/config.toml b/mdbook/src/14-inputs-and-polling/.cargo/config.toml new file mode 100644 index 00000000..e4ad7f5a --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/.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-inputs-and-polling/Cargo.toml b/mdbook/src/14-inputs-and-polling/Cargo.toml new file mode 100644 index 00000000..a56252b3 --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/Cargo.toml @@ -0,0 +1,14 @@ +[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" + +[dependencies.cortex-m] +version = "0.7" +features = ["critical-section-single-core"] diff --git a/mdbook/src/14-inputs-and-polling/Embed.toml b/mdbook/src/14-inputs-and-polling/Embed.toml new file mode 100644 index 00000000..21950130 --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/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-inputs-and-polling/README.md b/mdbook/src/14-inputs-and-polling/README.md new file mode 100644 index 00000000..3eacc351 --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/README.md @@ -0,0 +1,24 @@ +# 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. + +## GPIO Inputs + +Recall from the Registers chapter that GPIO pins on the micro:bit are grouped into two ports (P0 and P1), each with its own register block. We've previously manipulated the OUT register to drive pins high or low, activating LEDs. Now, we'll explore another register, IN, which enables us to read the state of input pins. + +### 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.) + +Reading the state of a GPIO input involves checking whether the voltage level at the pin is high (1) or low (0). +In the Registers chapter, we learned how to manipulate GPIO registers directly using a type-safe API, but under the hood, the API is just a wrapper around the raw register values. When the voltage level at the pin is high, the corresponding bit in the IN register is set to 1. Let's now apply this knowledge to reading the state of Button A (connected to P0.14) by accessing the IN register, which reflects the current input state of GPIO pins: + +```rust +{{#include examples/button-a.rs}} +``` + +In this snippet: + +We access the type-safe API provided by the registers module, specifically reading from the IN register of port P0. + +Using pin14().bit_is_clear() conveniently checks if pin 14 reads low (0), indicating Button A is pressed (active-low logic). \ No newline at end of file diff --git a/mdbook/src/14-inputs-and-polling/examples/button-a-bsp.rs b/mdbook/src/14-inputs-and-polling/examples/button-a-bsp.rs new file mode 100644 index 00000000..1aa60c50 --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/examples/button-a-bsp.rs @@ -0,0 +1,22 @@ +#![no_main] +#![no_std] + +use cortex_m_rt::entry; +use microbit::Board; +use rtt_target::{rprintln, rtt_init_print}; + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + + let 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/14-inputs-and-polling/examples/button-a.rs b/mdbook/src/14-inputs-and-polling/examples/button-a.rs new file mode 100644 index 00000000..9f6ac744 --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/examples/button-a.rs @@ -0,0 +1,17 @@ +#![no_main] +#![no_std] + +use registers::{entry, rprintln, rtt_init_print}; + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let (p0, _p1) = registers::init(); + + loop { + // Read pin P0.14 from IN register; active low means pressed when bit is 0. + let button_a_pressed = p0.in_.read().pin14().bit_is_clear(); + + rprintln!("Button A pressed: {}", button_a_pressed); + } +} diff --git a/mdbook/src/14-inputs-and-polling/examples/polling-led-toggle.rs b/mdbook/src/14-inputs-and-polling/examples/polling-led-toggle.rs new file mode 100644 index 00000000..d349add6 --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/examples/polling-led-toggle.rs @@ -0,0 +1,41 @@ +#![no_main] +#![no_std] + +use cortex_m_rt::entry; +use embedded_hal::digital::OutputPin; +use microbit::hal::timer::Timer; +use microbit::{hal::gpio, Board}; +use panic_halt as _; +use rtt_target::{rprintln, rtt_init_print}; + +#[entry] +fn main() -> ! { + rtt_init_print!(); + let board = Board::take().unwrap(); + let mut timer = Timer::new(board.TIMER0); + + // Configure buttons + let button_a = board.buttons.button_a; + let 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 { + if button_a.is_low().unwrap() { + // Button A pressed: LED on + row1.set_high().unwrap(); + } else if button_b.is_low().unwrap() { + // Button B pressed: LED off + row1.set_low().unwrap(); + } + timer.delay_ms(10_u32); + } +} diff --git a/mdbook/src/14-inputs-and-polling/my-solution.md b/mdbook/src/14-inputs-and-polling/my-solution.md new file mode 100644 index 00000000..1f804a1c --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/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/14-inputs-and-polling/polling-sucks.md b/mdbook/src/14-inputs-and-polling/polling-sucks.md new file mode 100644 index 00000000..055401dc --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/polling-sucks.md @@ -0,0 +1,37 @@ +# 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 much less responsive program (try for yourself and see how worse it is). + +A "smarter" program would know that the processor isn't actually doing anything while the blink delay is running, so it could very well do other things while waiting for the delay to finish, namely checking for button presses. + +Doing multiple things at once is called *concurrent* programming, and shows up in many places in programming, but especially in embedded systems. There's a whole host of techniques for implementing systems that concurrently interact with peripherals while maintaining a high degree of responsiveness (e.g. interrupt handling, cooperative multitasking, event-driven super-loops, etc.). We'll explore some of these in later chapters. + +In the next chapter, we'll look at a technique called *interrupts* that is better suited to doing multiple things at once. \ No newline at end of file diff --git a/mdbook/src/14-inputs-and-polling/polling.md b/mdbook/src/14-inputs-and-polling/polling.md new file mode 100644 index 00000000..9143c250 --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/polling.md @@ -0,0 +1,13 @@ +# 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. + +> **Note** The processor can perform this loop much faster than we need it to for our purposes, so we add a small delay that doesn't meaningfully impact the responsiveness of the program, but allows the MCU to take a breath every once in a while. + +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. \ No newline at end of file diff --git a/mdbook/src/14-inputs-and-polling/src/main.rs b/mdbook/src/14-inputs-and-polling/src/main.rs new file mode 100644 index 00000000..6b4031ee --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/src/main.rs @@ -0,0 +1,52 @@ +#![no_main] +#![no_std] + +use cortex_m_rt::entry; +use microbit::{display::blocking::Display, Board}; +use panic_halt as _; +use rtt_target::{rprintln, 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 display = Display::new(board.display_pins); + let button_a = board.buttons.button_a; + let button_b = board.buttons.button_b; + + loop { + if button_a.is_low().unwrap() { + display.show(&LEFT_ARROW); + } else if button_b.is_low().unwrap() { + display.show(&RIGHT_ARROW); + } else { + display.show(&CENTER_LED); + } + timer.delay_ms(10_u32); + } +} diff --git a/mdbook/src/14-inputs-and-polling/the-challenge.md b/mdbook/src/14-inputs-and-polling/the-challenge.md new file mode 100644 index 00000000..c87a4ed4 --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/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 LEDs and 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/14-inputs-and-polling/volatile-reads.md b/mdbook/src/14-inputs-and-polling/volatile-reads.md new file mode 100644 index 00000000..ada8a0d8 --- /dev/null +++ b/mdbook/src/14-inputs-and-polling/volatile-reads.md @@ -0,0 +1,54 @@ +# Volatile Reads + +Reading from registers introduces a subtlety: compiler optimization. Consider this snippet, reading directly from a GPIO register: + +```rust +use core::ptr; + +// Direct memory access to GPIO IN register +const GPIO_P0_IN: u32 = 0x50000510; + +// NOT RECOMMENDED (optimization may break correctness) +fn button_pressed() -> bool { + unsafe { + *(GPIO_P0_IN as *const u32) & (1 << 14) != 0 + } +} + +#[entry] +fn main() -> ! { + let (p0, _p1) = registers::init(); + + loop { + let button_a_pressed = button_pressed(); + if button_a_pressed { + rprintln!("Button A pressed"); + } else { + rprintln!("Button A not pressed"); + } + } +} +``` + +Here we have code that looks like it should work as intended, i.e. it repeatedly reads the state of Button A and prints the result. Unfortunately, as discussed in the "(mis)Optimization" section of Chapter 7, reading or writing directly to memory-mapped registers through address dereferencing is likely to produce incorrect results. The compiler might mistakenly assume these registers are regular memory locations and cache the reads or writes in registers, only reading the value from the register once and using that cached value for all subsequent reads, regardless of the actual state of the register. + +As you may have guessed, we need to do *volatile* reads instead. Here is a better implementation of the `button_pressed` function: + +```rust +fn button_a_pressed() -> bool { + unsafe { + let reg = ptr::read_volatile(GPIO_P0_IN as *const u32); + (reg & (1 << 14)) == 0 // Active-low logic + } +} +``` + +This code performs a *volatile* read from the GPIO IN register, ensuring that every access directly targets the memory-mapped register address and doesn't get optimized away. + +Thankfully, the micro:bit's Board Support Crate (BSP) abstracts away these low-level volatile reads entirely, allowing us to read button states in a simpler way: + +```rust +{{#include examples/button-a-bsp.rs}} +``` + +It's nice when you can work with a higher-level abstraction, and the micro:bit BSP makes this easy. You won't always be so lucky. Hopefully, now you know how to interact with registers directly, and you should be equipped to implement your own higher-level abstraction when none is available. \ No newline at end of file diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index 944447a9..e9203829 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -57,10 +57,23 @@ - [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) -- [Interrupts](14-interrupts/README.md) - - [Sharing data with globals](14-interrupts/sharing-data-with-globals.md) - - [Debouncing](14-interrupts/debouncing.md) - - [Under the hood](14-interrupts/under-the-hood.md) +- [Inputs and Polling](14-inputs-and-polling/README.md) + - [Inputs](14-inputs-and-polling/README.md) + - [Volatile reads](14-inputs-and-polling/volatile-reads.md) + - [Polling](14-inputs-and-polling/polling.md) + - [Turn signaller](14-inputs-and-polling/the-challenge.md) + - [My solution](14-inputs-and-polling/my-solution.md) + - [Polling sucks, actually](14-inputs-and-polling/polling-sucks.md) +- [Interrupts](15-interrupts/README.md) + - [Interrupts and Poking the MB2](15-interrupts/interrupts.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) + - [Under the hood](15-interrupts/under-the-hood.md) + - [Waiting for an interrupt (wfi, wfe, nop)](15-interrupts/waiting-for-an-interrupt.md) + - [Turn signaller revisited](15-interrupts/turn-signaller-revisited.md) + - [My solution](15-interrupts/my-solution.md) + - [Concurrency](15-interrupts/concurrency.md) - [Snake game](14-snake-game/README.md) - [Game logic](14-snake-game/game-logic.md) - [Controls](14-snake-game/controls.md) From fa442397a2a9513cfd4187fc9c765caa40353eeb Mon Sep 17 00:00:00 2001 From: arnav Date: Wed, 26 Mar 2025 02:29:09 -0400 Subject: [PATCH 17/38] small change to volatile reads in polling chapter --- mdbook/src/14-inputs-and-polling/volatile-reads.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mdbook/src/14-inputs-and-polling/volatile-reads.md b/mdbook/src/14-inputs-and-polling/volatile-reads.md index ada8a0d8..d8815c08 100644 --- a/mdbook/src/14-inputs-and-polling/volatile-reads.md +++ b/mdbook/src/14-inputs-and-polling/volatile-reads.md @@ -30,7 +30,7 @@ fn main() -> ! { } ``` -Here we have code that looks like it should work as intended, i.e. it repeatedly reads the state of Button A and prints the result. Unfortunately, as discussed in the "(mis)Optimization" section of Chapter 7, reading or writing directly to memory-mapped registers through address dereferencing is likely to produce incorrect results. The compiler might mistakenly assume these registers are regular memory locations and cache the reads or writes in registers, only reading the value from the register once and using that cached value for all subsequent reads, regardless of the actual state of the register. +Here we have code that looks like it should work as intended, i.e. it repeatedly reads the state of Button A and prints the result. Unfortunately, as discussed in the [(mis)Optimization](../07-registers/misoptimization.md) section of Chapter 7, reading or writing directly to memory-mapped registers through address dereferencing is likely to produce incorrect results. The compiler is likely to mistakenly assume subsequent reads or writes are to regular memory locations and cache the reads or writes in registers. This means that the compiler will only read the value from the register once and use that cached value for the rest of the program, regardless of the actual state of the register. As you may have guessed, we need to do *volatile* reads instead. Here is a better implementation of the `button_pressed` function: @@ -45,7 +45,7 @@ fn button_a_pressed() -> bool { This code performs a *volatile* read from the GPIO IN register, ensuring that every access directly targets the memory-mapped register address and doesn't get optimized away. -Thankfully, the micro:bit's Board Support Crate (BSP) abstracts away these low-level volatile reads entirely, allowing us to read button states in a simpler way: +Thankfully, the micro:bit's Board Support Crate (BSP) abstracts away these low-level volatile reads entirely, allowing us to read button states in a simpler way that still ensures correct behavior under the hood: ```rust {{#include examples/button-a-bsp.rs}} From 06a44bc0c9a3e1e64284a16f56ca4aa78ef7ea3e Mon Sep 17 00:00:00 2001 From: arnav Date: Wed, 26 Mar 2025 02:49:48 -0400 Subject: [PATCH 18/38] small polling ch changes --- mdbook/src/14-inputs-and-polling/volatile-reads.md | 6 ++---- mdbook/src/SUMMARY.md | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/mdbook/src/14-inputs-and-polling/volatile-reads.md b/mdbook/src/14-inputs-and-polling/volatile-reads.md index d8815c08..58ec998e 100644 --- a/mdbook/src/14-inputs-and-polling/volatile-reads.md +++ b/mdbook/src/14-inputs-and-polling/volatile-reads.md @@ -17,8 +17,6 @@ fn button_pressed() -> bool { #[entry] fn main() -> ! { - let (p0, _p1) = registers::init(); - loop { let button_a_pressed = button_pressed(); if button_a_pressed { @@ -43,9 +41,9 @@ fn button_a_pressed() -> bool { } ``` -This code performs a *volatile* read from the GPIO IN register, ensuring that every access directly targets the memory-mapped register address and doesn't get optimized away. +This code performs a *volatile* read from the GPIO IN register, ensuring that every access directly targets the memory-mapped register address and doesn't get optimized away. Using the type-safe `registers` interface, like we showed on the [previous page](./README.md), hides these low-level volatile reads entirely. -Thankfully, the micro:bit's Board Support Crate (BSP) abstracts away these low-level volatile reads entirely, allowing us to read button states in a simpler way that still ensures correct behavior under the hood: +Thankfully, the micro:bit's Board Support Crate (BSP) goes even further and abstracts away register-level operations entirely, allowing us to read button states in a simpler way that still ensures correct behavior under the hood: ```rust {{#include examples/button-a-bsp.rs}} diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index e9203829..0ac8d18c 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -65,7 +65,7 @@ - [My solution](14-inputs-and-polling/my-solution.md) - [Polling sucks, actually](14-inputs-and-polling/polling-sucks.md) - [Interrupts](15-interrupts/README.md) - - [Interrupts and Poking the MB2](15-interrupts/interrupts.md) + - [Interrupts](15-interrupts/interrupts.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) From 42daee02aad69e7c1b6e77738f0ffe89860963f4 Mon Sep 17 00:00:00 2001 From: arnav Date: Wed, 26 Mar 2025 02:51:52 -0400 Subject: [PATCH 19/38] editing ch15 summary --- mdbook/src/SUMMARY.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index 0ac8d18c..1d51be68 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -65,11 +65,10 @@ - [My solution](14-inputs-and-polling/my-solution.md) - [Polling sucks, actually](14-inputs-and-polling/polling-sucks.md) - [Interrupts](15-interrupts/README.md) - - [Interrupts](15-interrupts/interrupts.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) - - [Under the hood](15-interrupts/under-the-hood.md) - [Waiting for an interrupt (wfi, wfe, nop)](15-interrupts/waiting-for-an-interrupt.md) - [Turn signaller revisited](15-interrupts/turn-signaller-revisited.md) - [My solution](15-interrupts/my-solution.md) From fa8dac32d1e27b8fe45ee9dc1ae41b6b956b2d2d Mon Sep 17 00:00:00 2001 From: arnav Date: Wed, 26 Mar 2025 02:57:10 -0400 Subject: [PATCH 20/38] starting ch15 interrupts --- mdbook/src/14-interrupts/under-the-hood.md | 75 --------------- mdbook/src/14-snake-game/.cargo/config.toml | 8 -- mdbook/src/14-snake-game/Embed.toml | 11 --- mdbook/src/14-snake-game/README.md | 25 ----- mdbook/src/14-snake-game/controls.md | 87 ----------------- mdbook/src/14-snake-game/final-assembly.md | 30 ------ mdbook/src/14-snake-game/game-logic.md | 96 ------------------- .../src/14-snake-game/nonblocking-display.md | 68 ------------- mdbook/src/14-snake-game/src/controls.rs | 22 ----- mdbook/src/14-snake-game/src/controls/init.rs | 35 ------- .../14-snake-game/src/controls/interrupt.rs | 25 ----- mdbook/src/14-snake-game/src/display.rs | 22 ----- .../14-snake-game/src/display/interrupt.rs | 13 --- mdbook/src/14-snake-game/src/display/show.rs | 23 ----- mdbook/src/14-snake-game/src/game/coords.rs | 35 ------- mdbook/src/14-snake-game/src/game/movement.rs | 36 ------- mdbook/src/14-snake-game/src/game/rng.rs | 30 ------ mdbook/src/14-snake-game/src/game/snake.rs | 74 -------------- mdbook/src/14-snake-game/src/main.rs | 57 ----------- .../.cargo/config.toml | 0 .../Cargo.toml | 0 .../Embed.toml | 0 .../README.md | 0 .../debouncing.md | 0 .../examples/count-bounce.rs | 0 .../examples/count-debounce.rs | 0 .../examples/count-once.rs | 0 .../examples/count.rs | 0 .../examples/poke.rs | 0 .../sharing-data-with-globals.md | 0 .../Cargo.toml | 0 .../src/game.rs | 0 mdbook/src/SUMMARY.md | 10 +- 33 files changed, 5 insertions(+), 777 deletions(-) delete mode 100644 mdbook/src/14-interrupts/under-the-hood.md delete mode 100644 mdbook/src/14-snake-game/.cargo/config.toml delete mode 100644 mdbook/src/14-snake-game/Embed.toml delete mode 100644 mdbook/src/14-snake-game/README.md delete mode 100644 mdbook/src/14-snake-game/controls.md delete mode 100644 mdbook/src/14-snake-game/final-assembly.md delete mode 100644 mdbook/src/14-snake-game/game-logic.md delete mode 100644 mdbook/src/14-snake-game/nonblocking-display.md delete mode 100644 mdbook/src/14-snake-game/src/controls.rs delete mode 100644 mdbook/src/14-snake-game/src/controls/init.rs delete mode 100644 mdbook/src/14-snake-game/src/controls/interrupt.rs delete mode 100644 mdbook/src/14-snake-game/src/display.rs delete mode 100644 mdbook/src/14-snake-game/src/display/interrupt.rs delete mode 100644 mdbook/src/14-snake-game/src/display/show.rs delete mode 100644 mdbook/src/14-snake-game/src/game/coords.rs delete mode 100644 mdbook/src/14-snake-game/src/game/movement.rs delete mode 100644 mdbook/src/14-snake-game/src/game/rng.rs delete mode 100644 mdbook/src/14-snake-game/src/game/snake.rs delete mode 100644 mdbook/src/14-snake-game/src/main.rs rename mdbook/src/{14-interrupts => 15-interrupts}/.cargo/config.toml (100%) rename mdbook/src/{14-interrupts => 15-interrupts}/Cargo.toml (100%) rename mdbook/src/{14-interrupts => 15-interrupts}/Embed.toml (100%) rename mdbook/src/{14-interrupts => 15-interrupts}/README.md (100%) rename mdbook/src/{14-interrupts => 15-interrupts}/debouncing.md (100%) rename mdbook/src/{14-interrupts => 15-interrupts}/examples/count-bounce.rs (100%) rename mdbook/src/{14-interrupts => 15-interrupts}/examples/count-debounce.rs (100%) rename mdbook/src/{14-interrupts => 15-interrupts}/examples/count-once.rs (100%) rename mdbook/src/{14-interrupts => 15-interrupts}/examples/count.rs (100%) rename mdbook/src/{14-interrupts => 15-interrupts}/examples/poke.rs (100%) rename mdbook/src/{14-interrupts => 15-interrupts}/sharing-data-with-globals.md (100%) rename mdbook/src/{14-snake-game => 16-snake-game}/Cargo.toml (100%) rename mdbook/src/{14-snake-game => 16-snake-game}/src/game.rs (100%) diff --git a/mdbook/src/14-interrupts/under-the-hood.md b/mdbook/src/14-interrupts/under-the-hood.md deleted file mode 100644 index aee614b3..00000000 --- a/mdbook/src/14-interrupts/under-the-hood.md +++ /dev/null @@ -1,75 +0,0 @@ - -## Under The Hood - -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 prioritise these interrupts, remember which ones still need to be procesed, -and then cause the processor to run the relevant interrupt handler code. - -Depending on its configuration, the NVIC can ensure the current interrupt is fully processed before -a new one is executed, or it can stop the processor in the middle of one interrupt in order to -handle another that's higher priority. This is called "preemption" and allows processors to respond -very quickly to critical events. For example, a robot controller might use low-priority interrupts -to keep track sending status information to the operator, but also have a high-priority interrupt to -detect an emergency stop button being pushed so it can immediately stop moving the motors. You -wouldn't want it to wait until it had finished sending a data packet to get around to stopping! - -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 -[Exceptions]: https://docs.rust-embedded.org/book/start/exceptions.html -[Interrupts]: https://docs.rust-embedded.org/book/start/interrupts.html diff --git a/mdbook/src/14-snake-game/.cargo/config.toml b/mdbook/src/14-snake-game/.cargo/config.toml deleted file mode 100644 index e4ad7f5a..00000000 --- a/mdbook/src/14-snake-game/.cargo/config.toml +++ /dev/null @@ -1,8 +0,0 @@ -[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/Embed.toml b/mdbook/src/14-snake-game/Embed.toml deleted file mode 100644 index 21950130..00000000 --- a/mdbook/src/14-snake-game/Embed.toml +++ /dev/null @@ -1,11 +0,0 @@ -[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/14-snake-game/README.md deleted file mode 100644 index 555c7c37..00000000 --- a/mdbook/src/14-snake-game/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Snake game - -We're now going to implement a basic [snake](https://en.wikipedia.org/wiki/Snake_(video_game_genre)) -game that you can play on an MB2 using its 5×5 LED matrix as a display and its two buttons as -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 -us to look at the source code a little at a time. We will build the code bottom-up: we will first -build three modules — `game`, `controls` and `display`, and then compose these to build the final -program. Each module will have a top-level source file and one or more included source files: for -example, the `game` module will consist of `src/game.rs`, `src/game/coords.rs`, -`src/game/movement.rs`, etc. The Rust `mod` statement is used to combine the various components of -the module. *The Rust Programming Language* has a good [description] of Rust's module system. - -[description]: https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html diff --git a/mdbook/src/14-snake-game/controls.md b/mdbook/src/14-snake-game/controls.md deleted file mode 100644 index c9e6fda3..00000000 --- a/mdbook/src/14-snake-game/controls.md +++ /dev/null @@ -1,87 +0,0 @@ -# Controls - -Our protagonist will be controlled by the two buttons on the front of the micro:bit. Button A will -turn to the snake's left, and button B will turn to the snake's right. - -We will use the `microbit::pac::interrupt` macro to handle button presses in a concurrent way. The -interrupt will be generated by the MB2's General Purpose Input/Output Tasks and Events (GPIOTE) -peripheral. - -## The `controls` module - -We will need to keep track of two separate pieces of global mutable state: A reference to the -`GPIOTE` peripheral, and a record of the selected direction to turn next. - -Shared data is wrapped in a `RefCell` to permit interior mutability and locking. You can learn more -about `RefCell` by reading the [RefCell documentation] and the [interior mutability chapter] of the -Rust Book]. The `RefCell` is, in turn, wrapped in a `cortex_m::interrupt::Mutex` to allow safe -access. The Mutex provided by the `cortex_m` crate uses the concept of a [critical section]. Data -in a Mutex can only be accessed from within a function or closure passed to -`cortex_m::interrupt:free` (renamed here to `interrupt_free` for clarity), which ensures that the -code in the function or closure cannot itself be interrupted. - -[RefCell documentation]: https://doc.rust-lang.org/std/cell/struct.RefCell.html -[interior mutability chapter]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html -[critical section]: https://en.wikipedia.org/wiki/Critical_section - -### Initialization - -First, we will initialise the buttons (`src/controls/init.rs`). - -```rust -{{#include src/controls/init.rs}} -``` - -The `GPIOTE` peripheral on the nRF52 has 8 "channels", each of which can be connected to a `GPIO` -pin and configured to respond to certain events, including rising edge (transition from low to high -signal) and falling edge (high to low signal). A button is a `GPIO` pin which has high signal when -not pressed and low signal otherwise. Therefore, a button press is a falling edge. - -Note the awkward use of the function `init_channel()` in initialization to avoid copy-pasting the -button initialization code. The types that the various embedded crates for the MB2 have been hiding -from you are sometimes a bit scary. I would encourage you to explore the type structure of the HAL -and PAC crates at some point, as it is a bit odd and takes getting used to. In particular, note that -each pin on the microbit has *its own unique type.* The purpose of the `degrade()` function in -initialization is to convert these to a common type that can reasonably be used as an argument to -`init_channel()` and thence to `input_pin()`. - -We connect `channel0` to `button_a` and `channel1` to `button_b`. In each case, we set the button up -to generate events on a falling edge (`hi_to_lo`). We store a reference to our `GPIOTE` peripheral -in the `GPIO` Mutex. We then `unmask` `GPIOTE` interrupts, allowing them to be propagated by the -hardware, and call `unpend` to clear any interrupts with pending status (which may have been -generated prior to the interrupts being unmasked). - -### Interrupt handler - -Next, we write the code that handles the interrupt. We use the `interrupt` macro re-exported from -the `nrf52833_hal` crate. We define a function with the same name as the interrupt we want to handle -(you can see them all -[here](https://docs.rs/nrf52833-hal/latest/nrf52833_hal/pac/enum.Interrupt.html)) and annotate it -with `#[interrupt]` (`src/controls/interrupt.rs`). - -```rust -{{#include src/controls/interrupt.rs}} -``` - -When a `GPIOTE` interrupt is generated, we check each button to see whether it has been pressed. If -only button A has been pressed, we record that the snake should turn to the left. If only button B -has been pressed, we record that the snake should turn to the right. In any other case, we record -that the snake should not make any turn. (Having both buttons pressed "at the same time" is -exceedingly unlikely: button presses are noted almost instantly, and this interrupt handler runs -very fast — it would be hard to get both buttons down in time for this to happen. Similarly, it -would be hard to press a button for a short enough time for this code to miss it and report that -neither button is pressed. Still, Rust enforces that you plan for these unexpected cases: the code -will not compile unless you check all the possibilities.) The relevant turn is stored in the `TURN` -Mutex. All of this happens within an `interrupt_free` block, to ensure that we cannot be interrupted -by some other event while handling this interrupt. - -Finally, we expose a simple function to get the next turn (`src/controls.rs`). - -```rust -{{#include src/controls.rs}} -``` - -This function simply returns the current value of the `TURN` Mutex. It takes a single boolean -argument, `reset`. If `reset` is `true`, the value of `TURN` is reset, i.e., set to `Turn::None`. - -Next we will build support for a high-fidelity game display. diff --git a/mdbook/src/14-snake-game/final-assembly.md b/mdbook/src/14-snake-game/final-assembly.md deleted file mode 100644 index b9a5dc29..00000000 --- a/mdbook/src/14-snake-game/final-assembly.md +++ /dev/null @@ -1,30 +0,0 @@ -# Snake game: final assembly - -The code in our `src/main.rs` file brings all the previously-discussed machinery together to make -our final game. - -```rust -{{#include src/main.rs}} -``` - -After initialising the board and its timer and RNG peripherals, we initialise 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 -repeatedly perform the following steps: - -1. Get a 5×5 array of bytes representing the grid. The `Game::get_matrix` method takes three integer - arguments (which should be between 0 and 9, inclusive) which will, eventually, represent how - brightly the head, tail and food should be displayed. - -2. Display the matrix, for an amount of time determined by the `Game::step_len_ms` method. As - currently implemented, this method basically provides for 1 second between steps, reducing by - 200ms every time the player scores 5 points (eating 1 piece of food = 1 point), subject to a - floor of 200ms. - -3. Check the game status. If it is `Ongoing` (which is its initial value), run a step of the game - and update the game state (including its `status` property). Otherwise, the game is over, so - flash the current image three times, then show the player's score (represented as a number of - illuminated LEDs corresponding to the score), and exit the game loop. - -Our main loop just runs the game loop repeatedly, resetting the game's state after each iteration. diff --git a/mdbook/src/14-snake-game/game-logic.md b/mdbook/src/14-snake-game/game-logic.md deleted file mode 100644 index 0023321a..00000000 --- a/mdbook/src/14-snake-game/game-logic.md +++ /dev/null @@ -1,96 +0,0 @@ -# Game logic - -The first module we will build is the game logic. You are probably familiar with [snake] games, but -if not, the basic idea is that the player guides a snake around a 2D grid. At any given time, there -is some "food" at a random location on the grid and the goal of the game is to get the snake to -"eat" as much food as possible. Each time the snake eats food it grows in length. The player loses -if the snake crashes into its own tail. - -[snake]: https://en.wikipedia.org/wiki/Snake_%28video_game_genre%29 - -In some variants of the game, the player also loses if the snake crashes into the edge of the grid, -but given the small size of our grid we are going to implement a "wraparound" rule: if the snake -goes off one edge of the grid, it will continue from the opposite edge. - -## The `game` module - -We will build up the game mechanics in the `game` module. - -### Coordinates - -We start by defining a coordinate system for our game (`src/game/coords.rs`). - -```rust -{{#include src/game/coords.rs}} -``` - -We use a `Coords` struct to refer to a position on the grid. Because `Coords` only contains two -integers, we tell the compiler to derive an implementation of the `Copy` trait for it, so we can -pass around `Coords` structs without having to worry about ownership. - -### Random Number Generation - -We define an associated function, `Coords::random`, which will give us a random position on the -grid. We will use this later to determine where to place the snake's food. - -To generate random coordinates, we need a source of random numbers. The nRF52833 has a hardware -random number generator (HWRNG) peripheral, documented at section 6.19 of the [nRF52833 spec]. The -HAL gives us a simple interface to the HWRNG via the `microbit::hal::rng::Rng` struct. The HWRNG may -not be fast enough for a game; it is also convenient for testing to be able to replicate the -sequence of random numbers produced by the generator between runs, which is impossible for the HWRNG -by design. We thus also define a [pseudo-random] number generator (PRNG). The PRNG uses an -[xorshift] algorithm to generate pseudo-random `u32` values. The algorithm is basic and not -cryptographically secure, but it is efficient, easy to implement and good enough for our humble -snake game. Our `Prng` struct requires an initial seed value, which we do get from the RNG -peripheral. - -[nRF52833 spec]: https://infocenter.nordicsemi.com/pdf/nRF52833_PS_v1.3.pdf -[pseudo-random]: https://en.wikipedia.org/wiki/Pseudorandom_number_generator -[xorshift]: https://en.wikipedia.org/wiki/Xorshift - -All of this makes up `src/game/rng.rs`. - -```rust -{{#include src/game/rng.rs}} -``` - -### Movement - -We also need to define a few `enum`s that help us manage the game's state: direction of movement, -direction to turn, the current game status and the outcome of a particular "step" in the game (ie, a -single movement of the snake). `src/game/movement.rs` contains these. - -```rust -{{#include src/game/movement.rs}} -``` - -### A Snake (*A Snaaake!*) - -Next up we define a `Snake` struct, which keeps track of the coordinates occupied by the snake and -its direction of travel. We use a queue (`heapless::spsc::Queue`) to keep track of the order of -coordinates and a hash set (`heapless::FnvIndexSet`) to allow for quick collision detection. The -`Snake` has methods to allow it to move. `src/game/snake.rs` gets this. - -```rust -{{#include src/game/snake.rs}} -``` - -### Game Module Top-Level - -The `Game` struct keeps track of the game state. It holds a `Snake` object, the current coordinates -of the food, the speed of the game (which is used to determine the time that elapses between each -movement of the snake), the status of the game (whether the game is ongoing or the player has won or -lost) and the player's score. - -This struct contains methods to handle each step of the game, determining the snake's next move and -updating the game state accordingly. It also contains two methods--`game_matrix` and -`score_matrix`--that output 2D arrays of values which can be used to display the game state or the -player score on the LED matrix (as we will see later). - -We put the `Game` struct at the top of the `game` module, in `src/game.rs`. - -```rust -{{#include src/game.rs}} -``` - -Next we will add the ability to control the snake's movements. diff --git a/mdbook/src/14-snake-game/nonblocking-display.md b/mdbook/src/14-snake-game/nonblocking-display.md deleted file mode 100644 index 68603b81..00000000 --- a/mdbook/src/14-snake-game/nonblocking-display.md +++ /dev/null @@ -1,68 +0,0 @@ -# Using the non-blocking display - -We will next display the snake and food on the LEDs of the MB2 screen. So far, we have used the -blocking interface, which provides for LEDs to be either maximally bright or turned off. With this, -a basic functioning snake game would be possible. But you might find that when the snake got a bit -longer, it would be difficult to tell the snake from the food, and to tell which direction the snake -was heading. Let's figure out how to allow the LED brightness to vary: we can make the snake's body -a bit dimmer, which will help sort out the clutter. - -The `microbit` library makes available two different interfaces to the LED matrix. There is the -blocking interface we've already seen in previous chapters. There is also a non-blocking interface -which allows you to customise the brightness of each LED. At the hardware level, each LED is either -"on" or "off", but the `microbit::display::nonblocking` module simulates ten levels of brightness -for each LED by rapidly switching the LED on and off. - -(There is no great reason the two display modes of the `microbit` library crate have to be separate -and use separate code. A more complete design would allow either non-blocking or blocking use of a -single display API with variable brightness levels and refresh rates specified by the user. Never -assume that the stuff you have been handed is perfected, or even close. Always think about what you -might do differently. For now, though, we'll work with what we have, which is adequate for our -immediate purpose.) - -The code to interact with the non-blocking interface (`src/display.rs`) is pretty simple and will -follow a similar structure to the code we used to interact with the buttons. This time we'll start -at the top level. - -## Display module - -```rust -{{#include src/display.rs}} -``` - -First, we initialise 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. - -## Display API - -We then define a couple of convenience functions which allow us to easily set (or unset) the image -to be displayed (`src/display/show.rs`). - -```rust -{{#include src/display/show.rs}} -``` - -`display_image` takes an image and tells the display to show it. Like the `Display::show` method -that it calls, this function takes a struct that implements the `tiny_led_matrix::Render` -trait. That trait ensures that the struct contains the data and methods necessary for the `Display` -to render it on the LED matrix. The two implementations of `Render` provided by the -`microbit::display::nonblocking` module are `BitImage` and `GreyscaleImage`. In a `BitImage`, each -"pixel" (or LED) is either illuminated or not (like when we used the blocking interface), whereas in -a `GreyscaleImage` each "pixel" can have a different brightness. - -`clear_display` does exactly as the name suggests. - -## Display interrupt handling - -Finally, we use the `interrupt` macro to define a handler for the `TIMER1` interrupt. This interrupt -fires many times a second, and this is what allows the `Display` to rapidly cycle the different LEDs -on and off to give the illusion of varying brightness levels. All our handler code does is call the -`Display::handle_display_event` method, which handles this (`src/display/interrupt.rs`). - -```rust -{{#include src/display/interrupt.rs}} -``` - -Now we can understand how our `main` function will do display: we will call `init_display` and use -the new functions we have defined to interact with it. diff --git a/mdbook/src/14-snake-game/src/controls.rs b/mdbook/src/14-snake-game/src/controls.rs deleted file mode 100644 index 7bb5eca3..00000000 --- a/mdbook/src/14-snake-game/src/controls.rs +++ /dev/null @@ -1,22 +0,0 @@ -mod init; -mod interrupt; - -pub use init::init_buttons; - -use crate::game::Turn; -use core::cell::RefCell; -use cortex_m::interrupt::{free as interrupt_free, Mutex}; -use microbit::{board::Buttons, hal::gpiote::Gpiote}; -pub static GPIO: Mutex>> = Mutex::new(RefCell::new(None)); -pub static TURN: Mutex> = Mutex::new(RefCell::new(Turn::None)); - -/// Get the next turn (ie, the turn corresponding to the most recently pressed button). -pub fn get_turn(reset: bool) -> Turn { - interrupt_free(|cs| { - let turn = *TURN.borrow(cs).borrow(); - if reset { - *TURN.borrow(cs).borrow_mut() = Turn::None - } - turn - }) -} diff --git a/mdbook/src/14-snake-game/src/controls/init.rs b/mdbook/src/14-snake-game/src/controls/init.rs deleted file mode 100644 index b5ddb8cc..00000000 --- a/mdbook/src/14-snake-game/src/controls/init.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::{Buttons, GPIO}; - -use cortex_m::interrupt::free as interrupt_free; -use microbit::{ - hal::{ - gpio::{Floating, Input, Pin}, - gpiote::{Gpiote, GpioteChannel}, - }, - pac, -}; - -/// Initialise the buttons and enable interrupts. -pub fn init_buttons(board_gpiote: pac::GPIOTE, board_buttons: Buttons) { - let gpiote = Gpiote::new(board_gpiote); - - fn init_channel(channel: &GpioteChannel<'_>, button: &Pin>) { - channel.input_pin(button).hi_to_lo().enable_interrupt(); - channel.reset_events(); - } - - let channel0 = gpiote.channel0(); - init_channel(&channel0, &board_buttons.button_a.degrade()); - - let channel1 = gpiote.channel1(); - init_channel(&channel1, &board_buttons.button_b.degrade()); - - interrupt_free(move |cs| { - *GPIO.borrow(cs).borrow_mut() = Some(gpiote); - - unsafe { - pac::NVIC::unmask(pac::Interrupt::GPIOTE); - } - pac::NVIC::unpend(pac::Interrupt::GPIOTE); - }); -} diff --git a/mdbook/src/14-snake-game/src/controls/interrupt.rs b/mdbook/src/14-snake-game/src/controls/interrupt.rs deleted file mode 100644 index 608a9d96..00000000 --- a/mdbook/src/14-snake-game/src/controls/interrupt.rs +++ /dev/null @@ -1,25 +0,0 @@ -use super::{Turn, GPIO, TURN}; - -use cortex_m::interrupt::free as interrupt_free; -use microbit::pac::{self, interrupt}; - -#[pac::interrupt] -fn GPIOTE() { - interrupt_free(|cs| { - if let Some(gpiote) = GPIO.borrow(cs).borrow().as_ref() { - let a_pressed = gpiote.channel0().is_event_triggered(); - let b_pressed = gpiote.channel1().is_event_triggered(); - - let turn = match (a_pressed, b_pressed) { - (true, false) => Turn::Left, - (false, true) => Turn::Right, - _ => Turn::None, - }; - - gpiote.channel0().reset_events(); - gpiote.channel1().reset_events(); - - *TURN.borrow(cs).borrow_mut() = turn; - } - }); -} diff --git a/mdbook/src/14-snake-game/src/display.rs b/mdbook/src/14-snake-game/src/display.rs deleted file mode 100644 index a670ace1..00000000 --- a/mdbook/src/14-snake-game/src/display.rs +++ /dev/null @@ -1,22 +0,0 @@ -pub mod interrupt; -pub mod show; - -pub use show::{clear_display, display_image}; - -use core::cell::RefCell; -use cortex_m::interrupt::{free as interrupt_free, Mutex}; -use microbit::display::nonblocking::Display; -use microbit::gpio::DisplayPins; -use microbit::pac; -use microbit::pac::TIMER1; - -static DISPLAY: Mutex>>> = Mutex::new(RefCell::new(None)); - -pub fn init_display(board_timer: TIMER1, board_display: DisplayPins) { - let display = Display::new(board_timer, board_display); - - interrupt_free(move |cs| { - *DISPLAY.borrow(cs).borrow_mut() = Some(display); - }); - unsafe { pac::NVIC::unmask(pac::Interrupt::TIMER1) } -} diff --git a/mdbook/src/14-snake-game/src/display/interrupt.rs b/mdbook/src/14-snake-game/src/display/interrupt.rs deleted file mode 100644 index 2acebd0d..00000000 --- a/mdbook/src/14-snake-game/src/display/interrupt.rs +++ /dev/null @@ -1,13 +0,0 @@ -use super::DISPLAY; - -use cortex_m::interrupt::free as interrupt_free; -use microbit::pac::{self, interrupt}; - -#[pac::interrupt] -fn TIMER1() { - interrupt_free(|cs| { - if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { - display.handle_display_event(); - } - }) -} diff --git a/mdbook/src/14-snake-game/src/display/show.rs b/mdbook/src/14-snake-game/src/display/show.rs deleted file mode 100644 index e929be5e..00000000 --- a/mdbook/src/14-snake-game/src/display/show.rs +++ /dev/null @@ -1,23 +0,0 @@ -use super::DISPLAY; - -use cortex_m::interrupt::free as interrupt_free; - -use tiny_led_matrix::Render; - -/// Display an image. -pub fn display_image(image: &impl Render) { - interrupt_free(|cs| { - if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { - display.show(image); - } - }) -} - -/// Clear the display (turn off all LEDs). -pub fn clear_display() { - interrupt_free(|cs| { - if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { - display.clear(); - } - }) -} diff --git a/mdbook/src/14-snake-game/src/game/coords.rs b/mdbook/src/14-snake-game/src/game/coords.rs deleted file mode 100644 index 33e6da66..00000000 --- a/mdbook/src/14-snake-game/src/game/coords.rs +++ /dev/null @@ -1,35 +0,0 @@ -use super::Prng; - -use heapless::FnvIndexSet; - -/// A single point on the grid. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -pub struct Coords { - // Signed ints to allow negative values (handy when checking if we have gone - // off the top or left of the grid) - pub row: i8, - pub col: i8, -} - -impl Coords { - /// Get random coordinates within a grid. `exclude` is an optional set of - /// coordinates which should be excluded from the output. - pub fn random(rng: &mut Prng, exclude: Option<&FnvIndexSet>) -> Self { - let mut coords = Coords { - row: ((rng.random_u32() as usize) % 5) as i8, - col: ((rng.random_u32() as usize) % 5) as i8, - }; - while exclude.is_some_and(|exc| exc.contains(&coords)) { - coords = Coords { - row: ((rng.random_u32() as usize) % 5) as i8, - col: ((rng.random_u32() as usize) % 5) as i8, - } - } - coords - } - - /// Whether the point is outside the bounds of the grid. - pub fn is_out_of_bounds(&self) -> bool { - self.row < 0 || self.row >= 5 || self.col < 0 || self.col >= 5 - } -} diff --git a/mdbook/src/14-snake-game/src/game/movement.rs b/mdbook/src/14-snake-game/src/game/movement.rs deleted file mode 100644 index 030dd2b4..00000000 --- a/mdbook/src/14-snake-game/src/game/movement.rs +++ /dev/null @@ -1,36 +0,0 @@ -use super::Coords; - -/// Define the directions the snake can move. -pub enum Direction { - Up, - Down, - Left, - Right, -} - -/// What direction the snake should turn. -#[derive(Debug, Copy, Clone)] -pub enum Turn { - Left, - Right, - None, -} - -/// The current status of the game. -pub enum GameStatus { - Won, - Lost, - Ongoing, -} - -/// The outcome of a single move/step. -pub enum StepOutcome { - /// Grid full (player wins) - Full, - /// Snake has collided with itself (player loses) - Collision, - /// Snake has eaten some food - Eat(Coords), - /// Snake has moved (and nothing else has happened) - Move(Coords), -} diff --git a/mdbook/src/14-snake-game/src/game/rng.rs b/mdbook/src/14-snake-game/src/game/rng.rs deleted file mode 100644 index 898deefb..00000000 --- a/mdbook/src/14-snake-game/src/game/rng.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::Rng; - -/// A basic pseudo-random number generator. -pub struct Prng { - value: u32, -} - -impl Prng { - pub fn seeded(rng: &mut Rng) -> Self { - Self::new(rng.random_u32()) - } - - pub fn new(seed: u32) -> Self { - Self { value: seed } - } - - /// Basic xorshift PRNG function: see - fn xorshift32(mut input: u32) -> u32 { - input ^= input << 13; - input ^= input >> 17; - input ^= input << 5; - input - } - - /// Return a pseudo-random u32. - pub fn random_u32(&mut self) -> u32 { - self.value = Self::xorshift32(self.value); - self.value - } -} diff --git a/mdbook/src/14-snake-game/src/game/snake.rs b/mdbook/src/14-snake-game/src/game/snake.rs deleted file mode 100644 index 5387c5c1..00000000 --- a/mdbook/src/14-snake-game/src/game/snake.rs +++ /dev/null @@ -1,74 +0,0 @@ -use super::{Coords, Direction, FnvIndexSet, Turn}; - -use heapless::spsc::Queue; - -pub struct Snake { - /// Coordinates of the snake's head. - pub head: Coords, - /// Queue of coordinates of the rest of the snake's body. The end of the tail is - /// at the front. - pub tail: Queue, - /// A set containing all coordinates currently occupied by the snake (for fast - /// collision checking). - pub coord_set: FnvIndexSet, - /// The direction the snake is currently moving in. - pub direction: Direction, -} - -impl Snake { - pub fn make_snake() -> Self { - let head = Coords { row: 2, col: 2 }; - let initial_tail = Coords { row: 2, col: 1 }; - let mut tail = Queue::new(); - tail.enqueue(initial_tail).unwrap(); - let mut coord_set: FnvIndexSet = FnvIndexSet::new(); - coord_set.insert(head).unwrap(); - coord_set.insert(initial_tail).unwrap(); - Self { - head, - tail, - coord_set, - direction: Direction::Right, - } - } - - /// Move the snake onto the tile at the given coordinates. If `extend` is false, - /// the snake's tail vacates the rearmost tile. - pub fn move_snake(&mut self, coords: Coords, extend: bool) { - // Location of head becomes front of tail - self.tail.enqueue(self.head).unwrap(); - // Head moves to new coords - self.head = coords; - self.coord_set.insert(coords).unwrap(); - if !extend { - let back = self.tail.dequeue().unwrap(); - self.coord_set.remove(&back); - } - } - - fn turn_right(&mut self) { - self.direction = match self.direction { - Direction::Up => Direction::Right, - Direction::Down => Direction::Left, - Direction::Left => Direction::Up, - Direction::Right => Direction::Down, - } - } - - fn turn_left(&mut self) { - self.direction = match self.direction { - Direction::Up => Direction::Left, - Direction::Down => Direction::Right, - Direction::Left => Direction::Down, - Direction::Right => Direction::Up, - } - } - - pub fn turn(&mut self, direction: Turn) { - match direction { - Turn::Left => self.turn_left(), - Turn::Right => self.turn_right(), - Turn::None => (), - } - } -} diff --git a/mdbook/src/14-snake-game/src/main.rs b/mdbook/src/14-snake-game/src/main.rs deleted file mode 100644 index 78456e43..00000000 --- a/mdbook/src/14-snake-game/src/main.rs +++ /dev/null @@ -1,57 +0,0 @@ -#![no_main] -#![no_std] - -mod controls; -mod display; -pub mod game; - -use controls::{get_turn, init_buttons}; -use display::{clear_display, display_image, init_display}; -use game::{Game, GameStatus}; - -use cortex_m_rt::entry; -use embedded_hal::delay::DelayNs; -use microbit::{ - display::nonblocking::{BitImage, GreyscaleImage}, - hal::{Rng, Timer}, - 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).into_periodic(); - let mut rng = Rng::new(board.RNG); - let mut game = Game::new(&mut rng); - - init_buttons(board.GPIOTE, board.buttons); - init_display(board.TIMER1, board.display_pins); - - loop { - loop { - // Game loop - let image = GreyscaleImage::new(&game.game_matrix(6, 3, 9)); - display_image(&image); - timer.delay_ms(game.step_len_ms()); - match game.status { - GameStatus::Ongoing => game.step(get_turn(true)), - _ => { - for _ in 0..3 { - clear_display(); - timer.delay_ms(200u32); - display_image(&image); - timer.delay_ms(200u32); - } - clear_display(); - display_image(&BitImage::new(&game.score_matrix())); - timer.delay_ms(2000u32); - break; - } - } - } - game.reset(); - } -} diff --git a/mdbook/src/14-interrupts/.cargo/config.toml b/mdbook/src/15-interrupts/.cargo/config.toml similarity index 100% rename from mdbook/src/14-interrupts/.cargo/config.toml rename to mdbook/src/15-interrupts/.cargo/config.toml diff --git a/mdbook/src/14-interrupts/Cargo.toml b/mdbook/src/15-interrupts/Cargo.toml similarity index 100% rename from mdbook/src/14-interrupts/Cargo.toml rename to mdbook/src/15-interrupts/Cargo.toml diff --git a/mdbook/src/14-interrupts/Embed.toml b/mdbook/src/15-interrupts/Embed.toml similarity index 100% rename from mdbook/src/14-interrupts/Embed.toml rename to mdbook/src/15-interrupts/Embed.toml diff --git a/mdbook/src/14-interrupts/README.md b/mdbook/src/15-interrupts/README.md similarity index 100% rename from mdbook/src/14-interrupts/README.md rename to mdbook/src/15-interrupts/README.md diff --git a/mdbook/src/14-interrupts/debouncing.md b/mdbook/src/15-interrupts/debouncing.md similarity index 100% rename from mdbook/src/14-interrupts/debouncing.md rename to mdbook/src/15-interrupts/debouncing.md diff --git a/mdbook/src/14-interrupts/examples/count-bounce.rs b/mdbook/src/15-interrupts/examples/count-bounce.rs similarity index 100% rename from mdbook/src/14-interrupts/examples/count-bounce.rs rename to mdbook/src/15-interrupts/examples/count-bounce.rs diff --git a/mdbook/src/14-interrupts/examples/count-debounce.rs b/mdbook/src/15-interrupts/examples/count-debounce.rs similarity index 100% rename from mdbook/src/14-interrupts/examples/count-debounce.rs rename to mdbook/src/15-interrupts/examples/count-debounce.rs diff --git a/mdbook/src/14-interrupts/examples/count-once.rs b/mdbook/src/15-interrupts/examples/count-once.rs similarity index 100% rename from mdbook/src/14-interrupts/examples/count-once.rs rename to mdbook/src/15-interrupts/examples/count-once.rs diff --git a/mdbook/src/14-interrupts/examples/count.rs b/mdbook/src/15-interrupts/examples/count.rs similarity index 100% rename from mdbook/src/14-interrupts/examples/count.rs rename to mdbook/src/15-interrupts/examples/count.rs diff --git a/mdbook/src/14-interrupts/examples/poke.rs b/mdbook/src/15-interrupts/examples/poke.rs similarity index 100% rename from mdbook/src/14-interrupts/examples/poke.rs rename to mdbook/src/15-interrupts/examples/poke.rs diff --git a/mdbook/src/14-interrupts/sharing-data-with-globals.md b/mdbook/src/15-interrupts/sharing-data-with-globals.md similarity index 100% rename from mdbook/src/14-interrupts/sharing-data-with-globals.md rename to mdbook/src/15-interrupts/sharing-data-with-globals.md 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/14-snake-game/src/game.rs b/mdbook/src/16-snake-game/src/game.rs similarity index 100% rename from mdbook/src/14-snake-game/src/game.rs rename to mdbook/src/16-snake-game/src/game.rs diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index 1d51be68..8f6263e0 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -73,11 +73,11 @@ - [Turn signaller revisited](15-interrupts/turn-signaller-revisited.md) - [My solution](15-interrupts/my-solution.md) - [Concurrency](15-interrupts/concurrency.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) +- [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) --- From 56fbdef30304836acb61cbf5668c3e221c211755 Mon Sep 17 00:00:00 2001 From: arnav Date: Wed, 26 Mar 2025 02:58:48 -0400 Subject: [PATCH 21/38] trying again --- mdbook/src/SUMMARY.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index 8f6263e0..f9957423 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -58,14 +58,12 @@ - [The challenge](13-punch-o-meter/the-challenge.md) - [My solution](13-punch-o-meter/my-solution.md) - [Inputs and Polling](14-inputs-and-polling/README.md) - - [Inputs](14-inputs-and-polling/README.md) - [Volatile reads](14-inputs-and-polling/volatile-reads.md) - [Polling](14-inputs-and-polling/polling.md) - [Turn signaller](14-inputs-and-polling/the-challenge.md) - [My solution](14-inputs-and-polling/my-solution.md) - [Polling sucks, actually](14-inputs-and-polling/polling-sucks.md) - [Interrupts](15-interrupts/README.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) From 0ddf7450464cc214e474ce0a41a601082db762ae Mon Sep 17 00:00:00 2001 From: arnav Date: Sat, 29 Mar 2025 05:42:09 -0400 Subject: [PATCH 22/38] finished draft polling chapter --- Cargo.toml | 17 ++- .../.cargo/config.toml | 0 .../Cargo.toml | 0 .../Embed.toml | 0 .../README.md | 0 .../examples/light-it-all.rs | 0 .../my-solution.md | 0 .../src/main.rs | 0 .../templates/solution.rs | 0 .../the-challenge.md | 0 .../.cargo/config.toml | 0 .../Cargo.toml | 1 + .../Embed.toml | 0 mdbook/src/08-inputs-and-outputs/README.md | 17 +++ .../examples/button-a-bsp.rs | 0 .../examples/polling-led-toggle.rs | 0 .../my-solution.md | 0 .../08-inputs-and-outputs/polling-sucks.md | 69 +++++++++ .../polling.md | 4 +- .../src/main.rs | 19 ++- .../the-challenge.md | 2 +- .../.cargo/config.toml | 0 .../{07-registers => 09-registers}/Cargo.toml | 0 .../Embed.toml | 0 .../{07-registers => 09-registers}/README.md | 6 +- .../bad-address.md | 0 .../{07-registers => 09-registers}/embed.gdb | 0 .../examples/bad.rs | 0 .../examples/spooky.rs | 0 .../examples/type-safe.rs | 0 .../examples/volatile.rs | 0 .../misoptimization.md | 0 .../{07-registers => 09-registers}/rtrm.md | 0 .../spooky-action-at-a-distance.md | 0 .../{07-registers => 09-registers}/src/lib.rs | 0 .../src/main.rs | 0 .../type-safe-manipulation.md | 0 .../README.md | 0 .../nix-tooling.md | 0 .../windows-tooling.md | 0 .../{11-i2c => 11-uart}/.cargo/config.toml | 0 mdbook/src/{10-uart => 11-uart}/Cargo.toml | 0 mdbook/src/{10-uart => 11-uart}/Embed.toml | 0 mdbook/src/{10-uart => 11-uart}/README.md | 0 .../src/{10-uart => 11-uart}/echo-server.md | 0 .../examples/naive-send-string.rs | 0 .../examples/receive-byte.rs | 0 .../examples/send-byte.rs | 0 .../examples/send-string.rs | 0 .../src/{10-uart => 11-uart}/my-solution.md | 0 .../naive-approach-write.md | 0 .../receive-a-single-byte.md | 0 .../{10-uart => 11-uart}/reverse-a-string.md | 0 .../send-a-single-byte.md | 0 .../src/{10-uart => 11-uart}/send-a-string.md | 0 mdbook/src/{10-uart => 11-uart}/src/main.rs | 0 .../.cargo/config.toml | 0 mdbook/src/{11-i2c => 12-i2c}/Cargo.toml | 0 mdbook/src/{11-i2c => 12-i2c}/Embed.toml | 0 mdbook/src/{11-i2c => 12-i2c}/README.md | 0 .../{11-i2c => 12-i2c}/examples/chip-id.rs | 0 .../{11-i2c => 12-i2c}/examples/show-accel.rs | 0 mdbook/src/{11-i2c => 12-i2c}/lsm303agr.md | 0 mdbook/src/{11-i2c => 12-i2c}/my-solution.md | 0 .../read-a-single-register.md | 0 mdbook/src/{11-i2c => 12-i2c}/src/main.rs | 0 .../src/{11-i2c => 12-i2c}/the-challenge.md | 0 .../the-general-protocol.md | 0 .../src/{11-i2c => 12-i2c}/using-a-driver.md | 0 .../.cargo/config.toml | 0 .../Cargo.toml | 0 .../Embed.toml | 0 .../README.md | 0 .../examples/magnitude.rs | 0 .../examples/show-mag.rs | 0 .../magnitude.md | 0 .../my-solution.md | 0 .../src/main.rs | 0 .../templates/compass.rs | 0 .../the-challenge.md | 0 mdbook/src/14-inputs-and-polling/README.md | 24 --- .../examples/button-a.rs | 17 --- .../14-inputs-and-polling/polling-sucks.md | 37 ----- .../14-inputs-and-polling/volatile-reads.md | 52 ------- .../.cargo/config.toml | 0 .../Cargo.toml | 0 .../Embed.toml | 0 .../README.md | 0 .../examples/show-accel.rs | 0 .../gravity-is-up.md | 0 .../my-solution.md | 0 .../src/main.rs | 0 .../the-challenge.md | 0 mdbook/src/15-interrupts/concurrency.md | 1 + mdbook/src/15-interrupts/my-solution.md | 1 + .../nvic-and-interrupt-priority.md | 75 ++++++++++ .../15-interrupts/turn-signaller-revisited.md | 1 + .../15-interrupts/waiting-for-an-interrupt.md | 1 + mdbook/src/16-snake-game/.cargo/config.toml | 8 + mdbook/src/16-snake-game/Embed.toml | 11 ++ mdbook/src/16-snake-game/README.md | 25 ++++ mdbook/src/16-snake-game/controls.md | 87 +++++++++++ mdbook/src/16-snake-game/final-assembly.md | 30 ++++ mdbook/src/16-snake-game/game-logic.md | 96 ++++++++++++ .../src/16-snake-game/nonblocking-display.md | 68 +++++++++ mdbook/src/16-snake-game/src/controls.rs | 22 +++ mdbook/src/16-snake-game/src/controls/init.rs | 35 +++++ .../16-snake-game/src/controls/interrupt.rs | 25 ++++ mdbook/src/16-snake-game/src/display.rs | 22 +++ .../16-snake-game/src/display/interrupt.rs | 13 ++ mdbook/src/16-snake-game/src/display/show.rs | 23 +++ mdbook/src/16-snake-game/src/game.rs | 1 + mdbook/src/16-snake-game/src/game/coords.rs | 35 +++++ mdbook/src/16-snake-game/src/game/movement.rs | 36 +++++ mdbook/src/16-snake-game/src/game/rng.rs | 30 ++++ mdbook/src/16-snake-game/src/game/snake.rs | 74 ++++++++++ mdbook/src/16-snake-game/src/main.rs | 57 ++++++++ mdbook/src/SUMMARY.md | 137 +++++++++--------- 118 files changed, 959 insertions(+), 220 deletions(-) rename mdbook/src/{07-registers => 07-led-roulette}/.cargo/config.toml (100%) rename mdbook/src/{08-led-roulette => 07-led-roulette}/Cargo.toml (100%) rename mdbook/src/{07-registers => 07-led-roulette}/Embed.toml (100%) rename mdbook/src/{08-led-roulette => 07-led-roulette}/README.md (100%) rename mdbook/src/{08-led-roulette => 07-led-roulette}/examples/light-it-all.rs (100%) rename mdbook/src/{08-led-roulette => 07-led-roulette}/my-solution.md (100%) rename mdbook/src/{08-led-roulette => 07-led-roulette}/src/main.rs (100%) rename mdbook/src/{08-led-roulette => 07-led-roulette}/templates/solution.rs (100%) rename mdbook/src/{08-led-roulette => 07-led-roulette}/the-challenge.md (100%) rename mdbook/src/{08-led-roulette => 08-inputs-and-outputs}/.cargo/config.toml (100%) rename mdbook/src/{14-inputs-and-polling => 08-inputs-and-outputs}/Cargo.toml (92%) rename mdbook/src/{14-inputs-and-polling => 08-inputs-and-outputs}/Embed.toml (100%) create mode 100644 mdbook/src/08-inputs-and-outputs/README.md rename mdbook/src/{14-inputs-and-polling => 08-inputs-and-outputs}/examples/button-a-bsp.rs (100%) rename mdbook/src/{14-inputs-and-polling => 08-inputs-and-outputs}/examples/polling-led-toggle.rs (100%) rename mdbook/src/{14-inputs-and-polling => 08-inputs-and-outputs}/my-solution.md (100%) create mode 100644 mdbook/src/08-inputs-and-outputs/polling-sucks.md rename mdbook/src/{14-inputs-and-polling => 08-inputs-and-outputs}/polling.md (60%) rename mdbook/src/{14-inputs-and-polling => 08-inputs-and-outputs}/src/main.rs (63%) rename mdbook/src/{14-inputs-and-polling => 08-inputs-and-outputs}/the-challenge.md (92%) rename mdbook/src/{10-uart => 09-registers}/.cargo/config.toml (100%) rename mdbook/src/{07-registers => 09-registers}/Cargo.toml (100%) rename mdbook/src/{08-led-roulette => 09-registers}/Embed.toml (100%) rename mdbook/src/{07-registers => 09-registers}/README.md (80%) rename mdbook/src/{07-registers => 09-registers}/bad-address.md (100%) rename mdbook/src/{07-registers => 09-registers}/embed.gdb (100%) rename mdbook/src/{07-registers => 09-registers}/examples/bad.rs (100%) rename mdbook/src/{07-registers => 09-registers}/examples/spooky.rs (100%) rename mdbook/src/{07-registers => 09-registers}/examples/type-safe.rs (100%) rename mdbook/src/{07-registers => 09-registers}/examples/volatile.rs (100%) rename mdbook/src/{07-registers => 09-registers}/misoptimization.md (100%) rename mdbook/src/{07-registers => 09-registers}/rtrm.md (100%) rename mdbook/src/{07-registers => 09-registers}/spooky-action-at-a-distance.md (100%) rename mdbook/src/{07-registers => 09-registers}/src/lib.rs (100%) rename mdbook/src/{07-registers => 09-registers}/src/main.rs (100%) rename mdbook/src/{07-registers => 09-registers}/type-safe-manipulation.md (100%) rename mdbook/src/{09-serial-communication => 10-serial-communication}/README.md (100%) rename mdbook/src/{09-serial-communication => 10-serial-communication}/nix-tooling.md (100%) rename mdbook/src/{09-serial-communication => 10-serial-communication}/windows-tooling.md (100%) rename mdbook/src/{11-i2c => 11-uart}/.cargo/config.toml (100%) rename mdbook/src/{10-uart => 11-uart}/Cargo.toml (100%) rename mdbook/src/{10-uart => 11-uart}/Embed.toml (100%) rename mdbook/src/{10-uart => 11-uart}/README.md (100%) rename mdbook/src/{10-uart => 11-uart}/echo-server.md (100%) rename mdbook/src/{10-uart => 11-uart}/examples/naive-send-string.rs (100%) rename mdbook/src/{10-uart => 11-uart}/examples/receive-byte.rs (100%) rename mdbook/src/{10-uart => 11-uart}/examples/send-byte.rs (100%) rename mdbook/src/{10-uart => 11-uart}/examples/send-string.rs (100%) rename mdbook/src/{10-uart => 11-uart}/my-solution.md (100%) rename mdbook/src/{10-uart => 11-uart}/naive-approach-write.md (100%) rename mdbook/src/{10-uart => 11-uart}/receive-a-single-byte.md (100%) rename mdbook/src/{10-uart => 11-uart}/reverse-a-string.md (100%) rename mdbook/src/{10-uart => 11-uart}/send-a-single-byte.md (100%) rename mdbook/src/{10-uart => 11-uart}/send-a-string.md (100%) rename mdbook/src/{10-uart => 11-uart}/src/main.rs (100%) rename mdbook/src/{12-led-compass => 12-i2c}/.cargo/config.toml (100%) rename mdbook/src/{11-i2c => 12-i2c}/Cargo.toml (100%) rename mdbook/src/{11-i2c => 12-i2c}/Embed.toml (100%) rename mdbook/src/{11-i2c => 12-i2c}/README.md (100%) rename mdbook/src/{11-i2c => 12-i2c}/examples/chip-id.rs (100%) rename mdbook/src/{11-i2c => 12-i2c}/examples/show-accel.rs (100%) rename mdbook/src/{11-i2c => 12-i2c}/lsm303agr.md (100%) rename mdbook/src/{11-i2c => 12-i2c}/my-solution.md (100%) rename mdbook/src/{11-i2c => 12-i2c}/read-a-single-register.md (100%) rename mdbook/src/{11-i2c => 12-i2c}/src/main.rs (100%) rename mdbook/src/{11-i2c => 12-i2c}/the-challenge.md (100%) rename mdbook/src/{11-i2c => 12-i2c}/the-general-protocol.md (100%) rename mdbook/src/{11-i2c => 12-i2c}/using-a-driver.md (100%) rename mdbook/src/{13-punch-o-meter => 13-led-compass}/.cargo/config.toml (100%) rename mdbook/src/{12-led-compass => 13-led-compass}/Cargo.toml (100%) rename mdbook/src/{12-led-compass => 13-led-compass}/Embed.toml (100%) rename mdbook/src/{12-led-compass => 13-led-compass}/README.md (100%) rename mdbook/src/{12-led-compass => 13-led-compass}/examples/magnitude.rs (100%) rename mdbook/src/{12-led-compass => 13-led-compass}/examples/show-mag.rs (100%) rename mdbook/src/{12-led-compass => 13-led-compass}/magnitude.md (100%) rename mdbook/src/{12-led-compass => 13-led-compass}/my-solution.md (100%) rename mdbook/src/{12-led-compass => 13-led-compass}/src/main.rs (100%) rename mdbook/src/{12-led-compass => 13-led-compass}/templates/compass.rs (100%) rename mdbook/src/{12-led-compass => 13-led-compass}/the-challenge.md (100%) delete mode 100644 mdbook/src/14-inputs-and-polling/README.md delete mode 100644 mdbook/src/14-inputs-and-polling/examples/button-a.rs delete mode 100644 mdbook/src/14-inputs-and-polling/polling-sucks.md delete mode 100644 mdbook/src/14-inputs-and-polling/volatile-reads.md rename mdbook/src/{14-inputs-and-polling => 14-punch-o-meter}/.cargo/config.toml (100%) rename mdbook/src/{13-punch-o-meter => 14-punch-o-meter}/Cargo.toml (100%) rename mdbook/src/{13-punch-o-meter => 14-punch-o-meter}/Embed.toml (100%) rename mdbook/src/{13-punch-o-meter => 14-punch-o-meter}/README.md (100%) rename mdbook/src/{13-punch-o-meter => 14-punch-o-meter}/examples/show-accel.rs (100%) rename mdbook/src/{13-punch-o-meter => 14-punch-o-meter}/gravity-is-up.md (100%) rename mdbook/src/{13-punch-o-meter => 14-punch-o-meter}/my-solution.md (100%) rename mdbook/src/{13-punch-o-meter => 14-punch-o-meter}/src/main.rs (100%) rename mdbook/src/{13-punch-o-meter => 14-punch-o-meter}/the-challenge.md (100%) create mode 100644 mdbook/src/15-interrupts/concurrency.md create mode 100644 mdbook/src/15-interrupts/my-solution.md create mode 100644 mdbook/src/15-interrupts/nvic-and-interrupt-priority.md create mode 100644 mdbook/src/15-interrupts/turn-signaller-revisited.md create mode 100644 mdbook/src/15-interrupts/waiting-for-an-interrupt.md create mode 100644 mdbook/src/16-snake-game/.cargo/config.toml create mode 100644 mdbook/src/16-snake-game/Embed.toml create mode 100644 mdbook/src/16-snake-game/README.md create mode 100644 mdbook/src/16-snake-game/controls.md create mode 100644 mdbook/src/16-snake-game/final-assembly.md create mode 100644 mdbook/src/16-snake-game/game-logic.md create mode 100644 mdbook/src/16-snake-game/nonblocking-display.md create mode 100644 mdbook/src/16-snake-game/src/controls.rs create mode 100644 mdbook/src/16-snake-game/src/controls/init.rs create mode 100644 mdbook/src/16-snake-game/src/controls/interrupt.rs create mode 100644 mdbook/src/16-snake-game/src/display.rs create mode 100644 mdbook/src/16-snake-game/src/display/interrupt.rs create mode 100644 mdbook/src/16-snake-game/src/display/show.rs create mode 100644 mdbook/src/16-snake-game/src/game/coords.rs create mode 100644 mdbook/src/16-snake-game/src/game/movement.rs create mode 100644 mdbook/src/16-snake-game/src/game/rng.rs create mode 100644 mdbook/src/16-snake-game/src/game/snake.rs create mode 100644 mdbook/src/16-snake-game/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index 548ef719..219898b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,14 +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-interrupts", - "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/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/14-inputs-and-polling/Cargo.toml b/mdbook/src/08-inputs-and-outputs/Cargo.toml similarity index 92% rename from mdbook/src/14-inputs-and-polling/Cargo.toml rename to mdbook/src/08-inputs-and-outputs/Cargo.toml index a56252b3..e8dec34e 100644 --- a/mdbook/src/14-inputs-and-polling/Cargo.toml +++ b/mdbook/src/08-inputs-and-outputs/Cargo.toml @@ -8,6 +8,7 @@ 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" diff --git a/mdbook/src/14-inputs-and-polling/Embed.toml b/mdbook/src/08-inputs-and-outputs/Embed.toml similarity index 100% rename from mdbook/src/14-inputs-and-polling/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..2f9f899f --- /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 (1) or low (0). The buttons on the micro:bit are connected to pins; when the buttons are pressed, they pull the voltage at the pin low (to 0V ground). + +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/14-inputs-and-polling/examples/button-a-bsp.rs b/mdbook/src/08-inputs-and-outputs/examples/button-a-bsp.rs similarity index 100% rename from mdbook/src/14-inputs-and-polling/examples/button-a-bsp.rs rename to mdbook/src/08-inputs-and-outputs/examples/button-a-bsp.rs diff --git a/mdbook/src/14-inputs-and-polling/examples/polling-led-toggle.rs b/mdbook/src/08-inputs-and-outputs/examples/polling-led-toggle.rs similarity index 100% rename from mdbook/src/14-inputs-and-polling/examples/polling-led-toggle.rs rename to mdbook/src/08-inputs-and-outputs/examples/polling-led-toggle.rs diff --git a/mdbook/src/14-inputs-and-polling/my-solution.md b/mdbook/src/08-inputs-and-outputs/my-solution.md similarity index 100% rename from mdbook/src/14-inputs-and-polling/my-solution.md rename to mdbook/src/08-inputs-and-outputs/my-solution.md 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..20dccb1b --- /dev/null +++ b/mdbook/src/08-inputs-and-outputs/polling-sucks.md @@ -0,0 +1,69 @@ +# 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 much less responsive program (try for yourself and see how worse it is). + +A "smarter" program would know that the processor isn't actually doing anything while the blink delay is running, so it 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, and compare our current timer counter (if we have one) to a threshold, and transition states if any of the above conditions are met. + +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, and shows up in many places in programming, but especially in embedded systems. There's a whole host of techniques for implementing systems that concurrently 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. + +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/14-inputs-and-polling/polling.md b/mdbook/src/08-inputs-and-outputs/polling.md similarity index 60% rename from mdbook/src/14-inputs-and-polling/polling.md rename to mdbook/src/08-inputs-and-outputs/polling.md index 9143c250..b358fcba 100644 --- a/mdbook/src/14-inputs-and-polling/polling.md +++ b/mdbook/src/08-inputs-and-outputs/polling.md @@ -8,6 +8,6 @@ Now that we've learned how to read GPIO inputs, let's consider how we might use 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. -> **Note** The processor can perform this loop much faster than we need it to for our purposes, so we add a small delay that doesn't meaningfully impact the responsiveness of the program, but allows the MCU to take a breath every once in a while. +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. -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. \ No newline at end of file +>> **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/14-inputs-and-polling/src/main.rs b/mdbook/src/08-inputs-and-outputs/src/main.rs similarity index 63% rename from mdbook/src/14-inputs-and-polling/src/main.rs rename to mdbook/src/08-inputs-and-outputs/src/main.rs index 6b4031ee..711bff73 100644 --- a/mdbook/src/14-inputs-and-polling/src/main.rs +++ b/mdbook/src/08-inputs-and-outputs/src/main.rs @@ -2,8 +2,10 @@ #![no_std] use cortex_m_rt::entry; -use microbit::{display::blocking::Display, Board}; -use panic_halt as _; +use embedded_hal::delay::DelayNs; +use embedded_hal::digital::InputPin; +use microbit::{board::Board, display::blocking::Display, hal::Timer}; +use panic_rtt_target as _; use rtt_target::{rprintln, rtt_init_print}; // Define LED patterns @@ -35,18 +37,19 @@ const CENTER_LED: [[u8; 5]; 5] = [ 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 button_a = board.buttons.button_a; - let button_b = board.buttons.button_b; + 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(&LEFT_ARROW); + display.show(&mut timer, LEFT_ARROW, 10); } else if button_b.is_low().unwrap() { - display.show(&RIGHT_ARROW); + display.show(&mut timer, RIGHT_ARROW, 10); } else { - display.show(&CENTER_LED); + display.show(&mut timer, CENTER_LED, 10); } - timer.delay_ms(10_u32); } } diff --git a/mdbook/src/14-inputs-and-polling/the-challenge.md b/mdbook/src/08-inputs-and-outputs/the-challenge.md similarity index 92% rename from mdbook/src/14-inputs-and-polling/the-challenge.md rename to mdbook/src/08-inputs-and-outputs/the-challenge.md index c87a4ed4..612218d8 100644 --- a/mdbook/src/14-inputs-and-polling/the-challenge.md +++ b/mdbook/src/08-inputs-and-outputs/the-challenge.md @@ -8,7 +8,7 @@ Now it’s your turn to put polling into practice. Your task is to implement a s You'll need to: -- Initialize the LEDs and buttons. +- 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). 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 80% rename from mdbook/src/07-registers/README.md rename to mdbook/src/09-registers/README.md index 8469cd2d..937a058a 100644 --- a/mdbook/src/07-registers/README.md +++ b/mdbook/src/09-registers/README.md @@ -7,9 +7,9 @@ you like. That said, there's a lot of good stuff in here, so I'd recommend you d ----- -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 100% rename from mdbook/src/11-i2c/the-challenge.md rename to mdbook/src/12-i2c/the-challenge.md 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 100% rename from mdbook/src/12-led-compass/README.md rename to mdbook/src/13-led-compass/README.md 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-inputs-and-polling/README.md b/mdbook/src/14-inputs-and-polling/README.md deleted file mode 100644 index 3eacc351..00000000 --- a/mdbook/src/14-inputs-and-polling/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# 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. - -## GPIO Inputs - -Recall from the Registers chapter that GPIO pins on the micro:bit are grouped into two ports (P0 and P1), each with its own register block. We've previously manipulated the OUT register to drive pins high or low, activating LEDs. Now, we'll explore another register, IN, which enables us to read the state of input pins. - -### 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.) - -Reading the state of a GPIO input involves checking whether the voltage level at the pin is high (1) or low (0). -In the Registers chapter, we learned how to manipulate GPIO registers directly using a type-safe API, but under the hood, the API is just a wrapper around the raw register values. When the voltage level at the pin is high, the corresponding bit in the IN register is set to 1. Let's now apply this knowledge to reading the state of Button A (connected to P0.14) by accessing the IN register, which reflects the current input state of GPIO pins: - -```rust -{{#include examples/button-a.rs}} -``` - -In this snippet: - -We access the type-safe API provided by the registers module, specifically reading from the IN register of port P0. - -Using pin14().bit_is_clear() conveniently checks if pin 14 reads low (0), indicating Button A is pressed (active-low logic). \ No newline at end of file diff --git a/mdbook/src/14-inputs-and-polling/examples/button-a.rs b/mdbook/src/14-inputs-and-polling/examples/button-a.rs deleted file mode 100644 index 9f6ac744..00000000 --- a/mdbook/src/14-inputs-and-polling/examples/button-a.rs +++ /dev/null @@ -1,17 +0,0 @@ -#![no_main] -#![no_std] - -use registers::{entry, rprintln, rtt_init_print}; - -#[entry] -fn main() -> ! { - rtt_init_print!(); - let (p0, _p1) = registers::init(); - - loop { - // Read pin P0.14 from IN register; active low means pressed when bit is 0. - let button_a_pressed = p0.in_.read().pin14().bit_is_clear(); - - rprintln!("Button A pressed: {}", button_a_pressed); - } -} diff --git a/mdbook/src/14-inputs-and-polling/polling-sucks.md b/mdbook/src/14-inputs-and-polling/polling-sucks.md deleted file mode 100644 index 055401dc..00000000 --- a/mdbook/src/14-inputs-and-polling/polling-sucks.md +++ /dev/null @@ -1,37 +0,0 @@ -# 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 much less responsive program (try for yourself and see how worse it is). - -A "smarter" program would know that the processor isn't actually doing anything while the blink delay is running, so it could very well do other things while waiting for the delay to finish, namely checking for button presses. - -Doing multiple things at once is called *concurrent* programming, and shows up in many places in programming, but especially in embedded systems. There's a whole host of techniques for implementing systems that concurrently interact with peripherals while maintaining a high degree of responsiveness (e.g. interrupt handling, cooperative multitasking, event-driven super-loops, etc.). We'll explore some of these in later chapters. - -In the next chapter, we'll look at a technique called *interrupts* that is better suited to doing multiple things at once. \ No newline at end of file diff --git a/mdbook/src/14-inputs-and-polling/volatile-reads.md b/mdbook/src/14-inputs-and-polling/volatile-reads.md deleted file mode 100644 index 58ec998e..00000000 --- a/mdbook/src/14-inputs-and-polling/volatile-reads.md +++ /dev/null @@ -1,52 +0,0 @@ -# Volatile Reads - -Reading from registers introduces a subtlety: compiler optimization. Consider this snippet, reading directly from a GPIO register: - -```rust -use core::ptr; - -// Direct memory access to GPIO IN register -const GPIO_P0_IN: u32 = 0x50000510; - -// NOT RECOMMENDED (optimization may break correctness) -fn button_pressed() -> bool { - unsafe { - *(GPIO_P0_IN as *const u32) & (1 << 14) != 0 - } -} - -#[entry] -fn main() -> ! { - loop { - let button_a_pressed = button_pressed(); - if button_a_pressed { - rprintln!("Button A pressed"); - } else { - rprintln!("Button A not pressed"); - } - } -} -``` - -Here we have code that looks like it should work as intended, i.e. it repeatedly reads the state of Button A and prints the result. Unfortunately, as discussed in the [(mis)Optimization](../07-registers/misoptimization.md) section of Chapter 7, reading or writing directly to memory-mapped registers through address dereferencing is likely to produce incorrect results. The compiler is likely to mistakenly assume subsequent reads or writes are to regular memory locations and cache the reads or writes in registers. This means that the compiler will only read the value from the register once and use that cached value for the rest of the program, regardless of the actual state of the register. - -As you may have guessed, we need to do *volatile* reads instead. Here is a better implementation of the `button_pressed` function: - -```rust -fn button_a_pressed() -> bool { - unsafe { - let reg = ptr::read_volatile(GPIO_P0_IN as *const u32); - (reg & (1 << 14)) == 0 // Active-low logic - } -} -``` - -This code performs a *volatile* read from the GPIO IN register, ensuring that every access directly targets the memory-mapped register address and doesn't get optimized away. Using the type-safe `registers` interface, like we showed on the [previous page](./README.md), hides these low-level volatile reads entirely. - -Thankfully, the micro:bit's Board Support Crate (BSP) goes even further and abstracts away register-level operations entirely, allowing us to read button states in a simpler way that still ensures correct behavior under the hood: - -```rust -{{#include examples/button-a-bsp.rs}} -``` - -It's nice when you can work with a higher-level abstraction, and the micro:bit BSP makes this easy. You won't always be so lucky. Hopefully, now you know how to interact with registers directly, and you should be equipped to implement your own higher-level abstraction when none is available. \ No newline at end of file diff --git a/mdbook/src/14-inputs-and-polling/.cargo/config.toml b/mdbook/src/14-punch-o-meter/.cargo/config.toml similarity index 100% rename from mdbook/src/14-inputs-and-polling/.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 100% rename from mdbook/src/13-punch-o-meter/gravity-is-up.md rename to mdbook/src/14-punch-o-meter/gravity-is-up.md 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/concurrency.md b/mdbook/src/15-interrupts/concurrency.md new file mode 100644 index 00000000..5cbbf4f4 --- /dev/null +++ b/mdbook/src/15-interrupts/concurrency.md @@ -0,0 +1 @@ +# Concurrency diff --git a/mdbook/src/15-interrupts/my-solution.md b/mdbook/src/15-interrupts/my-solution.md new file mode 100644 index 00000000..63845e65 --- /dev/null +++ b/mdbook/src/15-interrupts/my-solution.md @@ -0,0 +1 @@ +# My solution 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..aee614b3 --- /dev/null +++ b/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md @@ -0,0 +1,75 @@ + +## Under The Hood + +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 prioritise these interrupts, remember which ones still need to be procesed, +and then cause the processor to run the relevant interrupt handler code. + +Depending on its configuration, the NVIC can ensure the current interrupt is fully processed before +a new one is executed, or it can stop the processor in the middle of one interrupt in order to +handle another that's higher priority. This is called "preemption" and allows processors to respond +very quickly to critical events. For example, a robot controller might use low-priority interrupts +to keep track sending status information to the operator, but also have a high-priority interrupt to +detect an emergency stop button being pushed so it can immediately stop moving the motors. You +wouldn't want it to wait until it had finished sending a data packet to get around to stopping! + +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 +[Exceptions]: https://docs.rust-embedded.org/book/start/exceptions.html +[Interrupts]: https://docs.rust-embedded.org/book/start/interrupts.html diff --git a/mdbook/src/15-interrupts/turn-signaller-revisited.md b/mdbook/src/15-interrupts/turn-signaller-revisited.md new file mode 100644 index 00000000..2d9a521a --- /dev/null +++ b/mdbook/src/15-interrupts/turn-signaller-revisited.md @@ -0,0 +1 @@ +# Turn signaller revisited diff --git a/mdbook/src/15-interrupts/waiting-for-an-interrupt.md b/mdbook/src/15-interrupts/waiting-for-an-interrupt.md new file mode 100644 index 00000000..7a9152ee --- /dev/null +++ b/mdbook/src/15-interrupts/waiting-for-an-interrupt.md @@ -0,0 +1 @@ +# Waiting for an interrupt (wfi, wfe, nop) 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/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/16-snake-game/README.md b/mdbook/src/16-snake-game/README.md new file mode 100644 index 00000000..555c7c37 --- /dev/null +++ b/mdbook/src/16-snake-game/README.md @@ -0,0 +1,25 @@ +# Snake game + +We're now going to implement a basic [snake](https://en.wikipedia.org/wiki/Snake_(video_game_genre)) +game that you can play on an MB2 using its 5×5 LED matrix as a display and its two buttons as +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 +us to look at the source code a little at a time. We will build the code bottom-up: we will first +build three modules — `game`, `controls` and `display`, and then compose these to build the final +program. Each module will have a top-level source file and one or more included source files: for +example, the `game` module will consist of `src/game.rs`, `src/game/coords.rs`, +`src/game/movement.rs`, etc. The Rust `mod` statement is used to combine the various components of +the module. *The Rust Programming Language* has a good [description] of Rust's module system. + +[description]: https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html diff --git a/mdbook/src/16-snake-game/controls.md b/mdbook/src/16-snake-game/controls.md new file mode 100644 index 00000000..c9e6fda3 --- /dev/null +++ b/mdbook/src/16-snake-game/controls.md @@ -0,0 +1,87 @@ +# Controls + +Our protagonist will be controlled by the two buttons on the front of the micro:bit. Button A will +turn to the snake's left, and button B will turn to the snake's right. + +We will use the `microbit::pac::interrupt` macro to handle button presses in a concurrent way. The +interrupt will be generated by the MB2's General Purpose Input/Output Tasks and Events (GPIOTE) +peripheral. + +## The `controls` module + +We will need to keep track of two separate pieces of global mutable state: A reference to the +`GPIOTE` peripheral, and a record of the selected direction to turn next. + +Shared data is wrapped in a `RefCell` to permit interior mutability and locking. You can learn more +about `RefCell` by reading the [RefCell documentation] and the [interior mutability chapter] of the +Rust Book]. The `RefCell` is, in turn, wrapped in a `cortex_m::interrupt::Mutex` to allow safe +access. The Mutex provided by the `cortex_m` crate uses the concept of a [critical section]. Data +in a Mutex can only be accessed from within a function or closure passed to +`cortex_m::interrupt:free` (renamed here to `interrupt_free` for clarity), which ensures that the +code in the function or closure cannot itself be interrupted. + +[RefCell documentation]: https://doc.rust-lang.org/std/cell/struct.RefCell.html +[interior mutability chapter]: https://doc.rust-lang.org/book/ch15-05-interior-mutability.html +[critical section]: https://en.wikipedia.org/wiki/Critical_section + +### Initialization + +First, we will initialise the buttons (`src/controls/init.rs`). + +```rust +{{#include src/controls/init.rs}} +``` + +The `GPIOTE` peripheral on the nRF52 has 8 "channels", each of which can be connected to a `GPIO` +pin and configured to respond to certain events, including rising edge (transition from low to high +signal) and falling edge (high to low signal). A button is a `GPIO` pin which has high signal when +not pressed and low signal otherwise. Therefore, a button press is a falling edge. + +Note the awkward use of the function `init_channel()` in initialization to avoid copy-pasting the +button initialization code. The types that the various embedded crates for the MB2 have been hiding +from you are sometimes a bit scary. I would encourage you to explore the type structure of the HAL +and PAC crates at some point, as it is a bit odd and takes getting used to. In particular, note that +each pin on the microbit has *its own unique type.* The purpose of the `degrade()` function in +initialization is to convert these to a common type that can reasonably be used as an argument to +`init_channel()` and thence to `input_pin()`. + +We connect `channel0` to `button_a` and `channel1` to `button_b`. In each case, we set the button up +to generate events on a falling edge (`hi_to_lo`). We store a reference to our `GPIOTE` peripheral +in the `GPIO` Mutex. We then `unmask` `GPIOTE` interrupts, allowing them to be propagated by the +hardware, and call `unpend` to clear any interrupts with pending status (which may have been +generated prior to the interrupts being unmasked). + +### Interrupt handler + +Next, we write the code that handles the interrupt. We use the `interrupt` macro re-exported from +the `nrf52833_hal` crate. We define a function with the same name as the interrupt we want to handle +(you can see them all +[here](https://docs.rs/nrf52833-hal/latest/nrf52833_hal/pac/enum.Interrupt.html)) and annotate it +with `#[interrupt]` (`src/controls/interrupt.rs`). + +```rust +{{#include src/controls/interrupt.rs}} +``` + +When a `GPIOTE` interrupt is generated, we check each button to see whether it has been pressed. If +only button A has been pressed, we record that the snake should turn to the left. If only button B +has been pressed, we record that the snake should turn to the right. In any other case, we record +that the snake should not make any turn. (Having both buttons pressed "at the same time" is +exceedingly unlikely: button presses are noted almost instantly, and this interrupt handler runs +very fast — it would be hard to get both buttons down in time for this to happen. Similarly, it +would be hard to press a button for a short enough time for this code to miss it and report that +neither button is pressed. Still, Rust enforces that you plan for these unexpected cases: the code +will not compile unless you check all the possibilities.) The relevant turn is stored in the `TURN` +Mutex. All of this happens within an `interrupt_free` block, to ensure that we cannot be interrupted +by some other event while handling this interrupt. + +Finally, we expose a simple function to get the next turn (`src/controls.rs`). + +```rust +{{#include src/controls.rs}} +``` + +This function simply returns the current value of the `TURN` Mutex. It takes a single boolean +argument, `reset`. If `reset` is `true`, the value of `TURN` is reset, i.e., set to `Turn::None`. + +Next we will build support for a high-fidelity game display. diff --git a/mdbook/src/16-snake-game/final-assembly.md b/mdbook/src/16-snake-game/final-assembly.md new file mode 100644 index 00000000..b9a5dc29 --- /dev/null +++ b/mdbook/src/16-snake-game/final-assembly.md @@ -0,0 +1,30 @@ +# Snake game: final assembly + +The code in our `src/main.rs` file brings all the previously-discussed machinery together to make +our final game. + +```rust +{{#include src/main.rs}} +``` + +After initialising the board and its timer and RNG peripherals, we initialise 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 +repeatedly perform the following steps: + +1. Get a 5×5 array of bytes representing the grid. The `Game::get_matrix` method takes three integer + arguments (which should be between 0 and 9, inclusive) which will, eventually, represent how + brightly the head, tail and food should be displayed. + +2. Display the matrix, for an amount of time determined by the `Game::step_len_ms` method. As + currently implemented, this method basically provides for 1 second between steps, reducing by + 200ms every time the player scores 5 points (eating 1 piece of food = 1 point), subject to a + floor of 200ms. + +3. Check the game status. If it is `Ongoing` (which is its initial value), run a step of the game + and update the game state (including its `status` property). Otherwise, the game is over, so + flash the current image three times, then show the player's score (represented as a number of + illuminated LEDs corresponding to the score), and exit the game loop. + +Our main loop just runs the game loop repeatedly, resetting the game's state after each iteration. diff --git a/mdbook/src/16-snake-game/game-logic.md b/mdbook/src/16-snake-game/game-logic.md new file mode 100644 index 00000000..0023321a --- /dev/null +++ b/mdbook/src/16-snake-game/game-logic.md @@ -0,0 +1,96 @@ +# Game logic + +The first module we will build is the game logic. You are probably familiar with [snake] games, but +if not, the basic idea is that the player guides a snake around a 2D grid. At any given time, there +is some "food" at a random location on the grid and the goal of the game is to get the snake to +"eat" as much food as possible. Each time the snake eats food it grows in length. The player loses +if the snake crashes into its own tail. + +[snake]: https://en.wikipedia.org/wiki/Snake_%28video_game_genre%29 + +In some variants of the game, the player also loses if the snake crashes into the edge of the grid, +but given the small size of our grid we are going to implement a "wraparound" rule: if the snake +goes off one edge of the grid, it will continue from the opposite edge. + +## The `game` module + +We will build up the game mechanics in the `game` module. + +### Coordinates + +We start by defining a coordinate system for our game (`src/game/coords.rs`). + +```rust +{{#include src/game/coords.rs}} +``` + +We use a `Coords` struct to refer to a position on the grid. Because `Coords` only contains two +integers, we tell the compiler to derive an implementation of the `Copy` trait for it, so we can +pass around `Coords` structs without having to worry about ownership. + +### Random Number Generation + +We define an associated function, `Coords::random`, which will give us a random position on the +grid. We will use this later to determine where to place the snake's food. + +To generate random coordinates, we need a source of random numbers. The nRF52833 has a hardware +random number generator (HWRNG) peripheral, documented at section 6.19 of the [nRF52833 spec]. The +HAL gives us a simple interface to the HWRNG via the `microbit::hal::rng::Rng` struct. The HWRNG may +not be fast enough for a game; it is also convenient for testing to be able to replicate the +sequence of random numbers produced by the generator between runs, which is impossible for the HWRNG +by design. We thus also define a [pseudo-random] number generator (PRNG). The PRNG uses an +[xorshift] algorithm to generate pseudo-random `u32` values. The algorithm is basic and not +cryptographically secure, but it is efficient, easy to implement and good enough for our humble +snake game. Our `Prng` struct requires an initial seed value, which we do get from the RNG +peripheral. + +[nRF52833 spec]: https://infocenter.nordicsemi.com/pdf/nRF52833_PS_v1.3.pdf +[pseudo-random]: https://en.wikipedia.org/wiki/Pseudorandom_number_generator +[xorshift]: https://en.wikipedia.org/wiki/Xorshift + +All of this makes up `src/game/rng.rs`. + +```rust +{{#include src/game/rng.rs}} +``` + +### Movement + +We also need to define a few `enum`s that help us manage the game's state: direction of movement, +direction to turn, the current game status and the outcome of a particular "step" in the game (ie, a +single movement of the snake). `src/game/movement.rs` contains these. + +```rust +{{#include src/game/movement.rs}} +``` + +### A Snake (*A Snaaake!*) + +Next up we define a `Snake` struct, which keeps track of the coordinates occupied by the snake and +its direction of travel. We use a queue (`heapless::spsc::Queue`) to keep track of the order of +coordinates and a hash set (`heapless::FnvIndexSet`) to allow for quick collision detection. The +`Snake` has methods to allow it to move. `src/game/snake.rs` gets this. + +```rust +{{#include src/game/snake.rs}} +``` + +### Game Module Top-Level + +The `Game` struct keeps track of the game state. It holds a `Snake` object, the current coordinates +of the food, the speed of the game (which is used to determine the time that elapses between each +movement of the snake), the status of the game (whether the game is ongoing or the player has won or +lost) and the player's score. + +This struct contains methods to handle each step of the game, determining the snake's next move and +updating the game state accordingly. It also contains two methods--`game_matrix` and +`score_matrix`--that output 2D arrays of values which can be used to display the game state or the +player score on the LED matrix (as we will see later). + +We put the `Game` struct at the top of the `game` module, in `src/game.rs`. + +```rust +{{#include src/game.rs}} +``` + +Next we will add the ability to control the snake's movements. diff --git a/mdbook/src/16-snake-game/nonblocking-display.md b/mdbook/src/16-snake-game/nonblocking-display.md new file mode 100644 index 00000000..68603b81 --- /dev/null +++ b/mdbook/src/16-snake-game/nonblocking-display.md @@ -0,0 +1,68 @@ +# Using the non-blocking display + +We will next display the snake and food on the LEDs of the MB2 screen. So far, we have used the +blocking interface, which provides for LEDs to be either maximally bright or turned off. With this, +a basic functioning snake game would be possible. But you might find that when the snake got a bit +longer, it would be difficult to tell the snake from the food, and to tell which direction the snake +was heading. Let's figure out how to allow the LED brightness to vary: we can make the snake's body +a bit dimmer, which will help sort out the clutter. + +The `microbit` library makes available two different interfaces to the LED matrix. There is the +blocking interface we've already seen in previous chapters. There is also a non-blocking interface +which allows you to customise the brightness of each LED. At the hardware level, each LED is either +"on" or "off", but the `microbit::display::nonblocking` module simulates ten levels of brightness +for each LED by rapidly switching the LED on and off. + +(There is no great reason the two display modes of the `microbit` library crate have to be separate +and use separate code. A more complete design would allow either non-blocking or blocking use of a +single display API with variable brightness levels and refresh rates specified by the user. Never +assume that the stuff you have been handed is perfected, or even close. Always think about what you +might do differently. For now, though, we'll work with what we have, which is adequate for our +immediate purpose.) + +The code to interact with the non-blocking interface (`src/display.rs`) is pretty simple and will +follow a similar structure to the code we used to interact with the buttons. This time we'll start +at the top level. + +## Display module + +```rust +{{#include src/display.rs}} +``` + +First, we initialise 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. + +## Display API + +We then define a couple of convenience functions which allow us to easily set (or unset) the image +to be displayed (`src/display/show.rs`). + +```rust +{{#include src/display/show.rs}} +``` + +`display_image` takes an image and tells the display to show it. Like the `Display::show` method +that it calls, this function takes a struct that implements the `tiny_led_matrix::Render` +trait. That trait ensures that the struct contains the data and methods necessary for the `Display` +to render it on the LED matrix. The two implementations of `Render` provided by the +`microbit::display::nonblocking` module are `BitImage` and `GreyscaleImage`. In a `BitImage`, each +"pixel" (or LED) is either illuminated or not (like when we used the blocking interface), whereas in +a `GreyscaleImage` each "pixel" can have a different brightness. + +`clear_display` does exactly as the name suggests. + +## Display interrupt handling + +Finally, we use the `interrupt` macro to define a handler for the `TIMER1` interrupt. This interrupt +fires many times a second, and this is what allows the `Display` to rapidly cycle the different LEDs +on and off to give the illusion of varying brightness levels. All our handler code does is call the +`Display::handle_display_event` method, which handles this (`src/display/interrupt.rs`). + +```rust +{{#include src/display/interrupt.rs}} +``` + +Now we can understand how our `main` function will do display: we will call `init_display` and use +the new functions we have defined to interact with it. diff --git a/mdbook/src/16-snake-game/src/controls.rs b/mdbook/src/16-snake-game/src/controls.rs new file mode 100644 index 00000000..7bb5eca3 --- /dev/null +++ b/mdbook/src/16-snake-game/src/controls.rs @@ -0,0 +1,22 @@ +mod init; +mod interrupt; + +pub use init::init_buttons; + +use crate::game::Turn; +use core::cell::RefCell; +use cortex_m::interrupt::{free as interrupt_free, Mutex}; +use microbit::{board::Buttons, hal::gpiote::Gpiote}; +pub static GPIO: Mutex>> = Mutex::new(RefCell::new(None)); +pub static TURN: Mutex> = Mutex::new(RefCell::new(Turn::None)); + +/// Get the next turn (ie, the turn corresponding to the most recently pressed button). +pub fn get_turn(reset: bool) -> Turn { + interrupt_free(|cs| { + let turn = *TURN.borrow(cs).borrow(); + if reset { + *TURN.borrow(cs).borrow_mut() = Turn::None + } + turn + }) +} diff --git a/mdbook/src/16-snake-game/src/controls/init.rs b/mdbook/src/16-snake-game/src/controls/init.rs new file mode 100644 index 00000000..b5ddb8cc --- /dev/null +++ b/mdbook/src/16-snake-game/src/controls/init.rs @@ -0,0 +1,35 @@ +use super::{Buttons, GPIO}; + +use cortex_m::interrupt::free as interrupt_free; +use microbit::{ + hal::{ + gpio::{Floating, Input, Pin}, + gpiote::{Gpiote, GpioteChannel}, + }, + pac, +}; + +/// Initialise the buttons and enable interrupts. +pub fn init_buttons(board_gpiote: pac::GPIOTE, board_buttons: Buttons) { + let gpiote = Gpiote::new(board_gpiote); + + fn init_channel(channel: &GpioteChannel<'_>, button: &Pin>) { + channel.input_pin(button).hi_to_lo().enable_interrupt(); + channel.reset_events(); + } + + let channel0 = gpiote.channel0(); + init_channel(&channel0, &board_buttons.button_a.degrade()); + + let channel1 = gpiote.channel1(); + init_channel(&channel1, &board_buttons.button_b.degrade()); + + interrupt_free(move |cs| { + *GPIO.borrow(cs).borrow_mut() = Some(gpiote); + + unsafe { + pac::NVIC::unmask(pac::Interrupt::GPIOTE); + } + pac::NVIC::unpend(pac::Interrupt::GPIOTE); + }); +} diff --git a/mdbook/src/16-snake-game/src/controls/interrupt.rs b/mdbook/src/16-snake-game/src/controls/interrupt.rs new file mode 100644 index 00000000..608a9d96 --- /dev/null +++ b/mdbook/src/16-snake-game/src/controls/interrupt.rs @@ -0,0 +1,25 @@ +use super::{Turn, GPIO, TURN}; + +use cortex_m::interrupt::free as interrupt_free; +use microbit::pac::{self, interrupt}; + +#[pac::interrupt] +fn GPIOTE() { + interrupt_free(|cs| { + if let Some(gpiote) = GPIO.borrow(cs).borrow().as_ref() { + let a_pressed = gpiote.channel0().is_event_triggered(); + let b_pressed = gpiote.channel1().is_event_triggered(); + + let turn = match (a_pressed, b_pressed) { + (true, false) => Turn::Left, + (false, true) => Turn::Right, + _ => Turn::None, + }; + + gpiote.channel0().reset_events(); + gpiote.channel1().reset_events(); + + *TURN.borrow(cs).borrow_mut() = turn; + } + }); +} diff --git a/mdbook/src/16-snake-game/src/display.rs b/mdbook/src/16-snake-game/src/display.rs new file mode 100644 index 00000000..a670ace1 --- /dev/null +++ b/mdbook/src/16-snake-game/src/display.rs @@ -0,0 +1,22 @@ +pub mod interrupt; +pub mod show; + +pub use show::{clear_display, display_image}; + +use core::cell::RefCell; +use cortex_m::interrupt::{free as interrupt_free, Mutex}; +use microbit::display::nonblocking::Display; +use microbit::gpio::DisplayPins; +use microbit::pac; +use microbit::pac::TIMER1; + +static DISPLAY: Mutex>>> = Mutex::new(RefCell::new(None)); + +pub fn init_display(board_timer: TIMER1, board_display: DisplayPins) { + let display = Display::new(board_timer, board_display); + + interrupt_free(move |cs| { + *DISPLAY.borrow(cs).borrow_mut() = Some(display); + }); + unsafe { pac::NVIC::unmask(pac::Interrupt::TIMER1) } +} diff --git a/mdbook/src/16-snake-game/src/display/interrupt.rs b/mdbook/src/16-snake-game/src/display/interrupt.rs new file mode 100644 index 00000000..2acebd0d --- /dev/null +++ b/mdbook/src/16-snake-game/src/display/interrupt.rs @@ -0,0 +1,13 @@ +use super::DISPLAY; + +use cortex_m::interrupt::free as interrupt_free; +use microbit::pac::{self, interrupt}; + +#[pac::interrupt] +fn TIMER1() { + interrupt_free(|cs| { + if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { + display.handle_display_event(); + } + }) +} diff --git a/mdbook/src/16-snake-game/src/display/show.rs b/mdbook/src/16-snake-game/src/display/show.rs new file mode 100644 index 00000000..e929be5e --- /dev/null +++ b/mdbook/src/16-snake-game/src/display/show.rs @@ -0,0 +1,23 @@ +use super::DISPLAY; + +use cortex_m::interrupt::free as interrupt_free; + +use tiny_led_matrix::Render; + +/// Display an image. +pub fn display_image(image: &impl Render) { + interrupt_free(|cs| { + if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { + display.show(image); + } + }) +} + +/// Clear the display (turn off all LEDs). +pub fn clear_display() { + interrupt_free(|cs| { + if let Some(display) = DISPLAY.borrow(cs).borrow_mut().as_mut() { + display.clear(); + } + }) +} diff --git a/mdbook/src/16-snake-game/src/game.rs b/mdbook/src/16-snake-game/src/game.rs index b55e8950..fb13fc0a 100644 --- a/mdbook/src/16-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/16-snake-game/src/game/coords.rs b/mdbook/src/16-snake-game/src/game/coords.rs new file mode 100644 index 00000000..33e6da66 --- /dev/null +++ b/mdbook/src/16-snake-game/src/game/coords.rs @@ -0,0 +1,35 @@ +use super::Prng; + +use heapless::FnvIndexSet; + +/// A single point on the grid. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct Coords { + // Signed ints to allow negative values (handy when checking if we have gone + // off the top or left of the grid) + pub row: i8, + pub col: i8, +} + +impl Coords { + /// Get random coordinates within a grid. `exclude` is an optional set of + /// coordinates which should be excluded from the output. + pub fn random(rng: &mut Prng, exclude: Option<&FnvIndexSet>) -> Self { + let mut coords = Coords { + row: ((rng.random_u32() as usize) % 5) as i8, + col: ((rng.random_u32() as usize) % 5) as i8, + }; + while exclude.is_some_and(|exc| exc.contains(&coords)) { + coords = Coords { + row: ((rng.random_u32() as usize) % 5) as i8, + col: ((rng.random_u32() as usize) % 5) as i8, + } + } + coords + } + + /// Whether the point is outside the bounds of the grid. + pub fn is_out_of_bounds(&self) -> bool { + self.row < 0 || self.row >= 5 || self.col < 0 || self.col >= 5 + } +} diff --git a/mdbook/src/16-snake-game/src/game/movement.rs b/mdbook/src/16-snake-game/src/game/movement.rs new file mode 100644 index 00000000..030dd2b4 --- /dev/null +++ b/mdbook/src/16-snake-game/src/game/movement.rs @@ -0,0 +1,36 @@ +use super::Coords; + +/// Define the directions the snake can move. +pub enum Direction { + Up, + Down, + Left, + Right, +} + +/// What direction the snake should turn. +#[derive(Debug, Copy, Clone)] +pub enum Turn { + Left, + Right, + None, +} + +/// The current status of the game. +pub enum GameStatus { + Won, + Lost, + Ongoing, +} + +/// The outcome of a single move/step. +pub enum StepOutcome { + /// Grid full (player wins) + Full, + /// Snake has collided with itself (player loses) + Collision, + /// Snake has eaten some food + Eat(Coords), + /// Snake has moved (and nothing else has happened) + Move(Coords), +} diff --git a/mdbook/src/16-snake-game/src/game/rng.rs b/mdbook/src/16-snake-game/src/game/rng.rs new file mode 100644 index 00000000..898deefb --- /dev/null +++ b/mdbook/src/16-snake-game/src/game/rng.rs @@ -0,0 +1,30 @@ +use crate::Rng; + +/// A basic pseudo-random number generator. +pub struct Prng { + value: u32, +} + +impl Prng { + pub fn seeded(rng: &mut Rng) -> Self { + Self::new(rng.random_u32()) + } + + pub fn new(seed: u32) -> Self { + Self { value: seed } + } + + /// Basic xorshift PRNG function: see + fn xorshift32(mut input: u32) -> u32 { + input ^= input << 13; + input ^= input >> 17; + input ^= input << 5; + input + } + + /// Return a pseudo-random u32. + pub fn random_u32(&mut self) -> u32 { + self.value = Self::xorshift32(self.value); + self.value + } +} diff --git a/mdbook/src/16-snake-game/src/game/snake.rs b/mdbook/src/16-snake-game/src/game/snake.rs new file mode 100644 index 00000000..5387c5c1 --- /dev/null +++ b/mdbook/src/16-snake-game/src/game/snake.rs @@ -0,0 +1,74 @@ +use super::{Coords, Direction, FnvIndexSet, Turn}; + +use heapless::spsc::Queue; + +pub struct Snake { + /// Coordinates of the snake's head. + pub head: Coords, + /// Queue of coordinates of the rest of the snake's body. The end of the tail is + /// at the front. + pub tail: Queue, + /// A set containing all coordinates currently occupied by the snake (for fast + /// collision checking). + pub coord_set: FnvIndexSet, + /// The direction the snake is currently moving in. + pub direction: Direction, +} + +impl Snake { + pub fn make_snake() -> Self { + let head = Coords { row: 2, col: 2 }; + let initial_tail = Coords { row: 2, col: 1 }; + let mut tail = Queue::new(); + tail.enqueue(initial_tail).unwrap(); + let mut coord_set: FnvIndexSet = FnvIndexSet::new(); + coord_set.insert(head).unwrap(); + coord_set.insert(initial_tail).unwrap(); + Self { + head, + tail, + coord_set, + direction: Direction::Right, + } + } + + /// Move the snake onto the tile at the given coordinates. If `extend` is false, + /// the snake's tail vacates the rearmost tile. + pub fn move_snake(&mut self, coords: Coords, extend: bool) { + // Location of head becomes front of tail + self.tail.enqueue(self.head).unwrap(); + // Head moves to new coords + self.head = coords; + self.coord_set.insert(coords).unwrap(); + if !extend { + let back = self.tail.dequeue().unwrap(); + self.coord_set.remove(&back); + } + } + + fn turn_right(&mut self) { + self.direction = match self.direction { + Direction::Up => Direction::Right, + Direction::Down => Direction::Left, + Direction::Left => Direction::Up, + Direction::Right => Direction::Down, + } + } + + fn turn_left(&mut self) { + self.direction = match self.direction { + Direction::Up => Direction::Left, + Direction::Down => Direction::Right, + Direction::Left => Direction::Down, + Direction::Right => Direction::Up, + } + } + + pub fn turn(&mut self, direction: Turn) { + match direction { + Turn::Left => self.turn_left(), + Turn::Right => self.turn_right(), + Turn::None => (), + } + } +} diff --git a/mdbook/src/16-snake-game/src/main.rs b/mdbook/src/16-snake-game/src/main.rs new file mode 100644 index 00000000..78456e43 --- /dev/null +++ b/mdbook/src/16-snake-game/src/main.rs @@ -0,0 +1,57 @@ +#![no_main] +#![no_std] + +mod controls; +mod display; +pub mod game; + +use controls::{get_turn, init_buttons}; +use display::{clear_display, display_image, init_display}; +use game::{Game, GameStatus}; + +use cortex_m_rt::entry; +use embedded_hal::delay::DelayNs; +use microbit::{ + display::nonblocking::{BitImage, GreyscaleImage}, + hal::{Rng, Timer}, + 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).into_periodic(); + let mut rng = Rng::new(board.RNG); + let mut game = Game::new(&mut rng); + + init_buttons(board.GPIOTE, board.buttons); + init_display(board.TIMER1, board.display_pins); + + loop { + loop { + // Game loop + let image = GreyscaleImage::new(&game.game_matrix(6, 3, 9)); + display_image(&image); + timer.delay_ms(game.step_len_ms()); + match game.status { + GameStatus::Ongoing => game.step(get_turn(true)), + _ => { + for _ in 0..3 { + clear_display(); + timer.delay_ms(200u32); + display_image(&image); + timer.delay_ms(200u32); + } + clear_display(); + display_image(&BitImage::new(&game.score_matrix())); + timer.delay_ms(2000u32); + break; + } + } + } + game.reset(); + } +} diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index f9957423..62d9312f 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -2,80 +2,79 @@ - [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) -- [Inputs and Polling](14-inputs-and-polling/README.md) - - [Volatile reads](14-inputs-and-polling/volatile-reads.md) - - [Polling](14-inputs-and-polling/polling.md) - - [Turn signaller](14-inputs-and-polling/the-challenge.md) - - [My solution](14-inputs-and-polling/my-solution.md) - - [Polling sucks, actually](14-inputs-and-polling/polling-sucks.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 for an interrupt (wfi, wfe, nop)](15-interrupts/waiting-for-an-interrupt.md) - - [Turn signaller revisited](15-interrupts/turn-signaller-revisited.md) - - [My solution](15-interrupts/my-solution.md) - - [Concurrency](15-interrupts/concurrency.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 for an interrupt (wfi, wfe, nop)](15-interrupts/waiting-for-an-interrupt.md) + - [Turn signaller revisited](15-interrupts/turn-signaller-revisited.md) + - [My solution](15-interrupts/my-solution.md) + - [Concurrency](15-interrupts/concurrency.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) + - [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) --- From 1d9385eae0a17ef2567a066148e6b235b17e1d47 Mon Sep 17 00:00:00 2001 From: arnav Date: Sat, 29 Mar 2025 15:32:07 -0400 Subject: [PATCH 23/38] wrapping up interrupt draft --- mdbook/src/08-inputs-and-outputs/polling-sucks.md | 6 ++++++ mdbook/src/15-interrupts/Cargo.toml | 1 + mdbook/src/15-interrupts/debouncing.md | 4 ++++ .../src/15-interrupts/examples/count-debounce.rs | 10 ++++++---- mdbook/src/15-interrupts/my-solution.md | 1 - .../src/15-interrupts/turn-signaller-revisited.md | 1 - .../src/15-interrupts/waiting-for-an-interrupt.md | 1 - .../src/15-interrupts/waiting-to-be-interrupted.md | 14 ++++++++++++++ mdbook/src/16-snake-game/README.md | 7 ------- mdbook/src/SUMMARY.md | 5 +---- 10 files changed, 32 insertions(+), 18 deletions(-) delete mode 100644 mdbook/src/15-interrupts/my-solution.md delete mode 100644 mdbook/src/15-interrupts/turn-signaller-revisited.md delete mode 100644 mdbook/src/15-interrupts/waiting-for-an-interrupt.md create mode 100644 mdbook/src/15-interrupts/waiting-to-be-interrupted.md diff --git a/mdbook/src/08-inputs-and-outputs/polling-sucks.md b/mdbook/src/08-inputs-and-outputs/polling-sucks.md index 20dccb1b..3e5c75c8 100644 --- a/mdbook/src/08-inputs-and-outputs/polling-sucks.md +++ b/mdbook/src/08-inputs-and-outputs/polling-sucks.md @@ -66,4 +66,10 @@ Superloops work and are often used in embedded systems, but the programmer has t Doing multiple things at once is called *concurrent* programming, and shows up in many places in programming, but especially in embedded systems. There's a whole host of techniques for implementing systems that concurrently 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/15-interrupts/Cargo.toml b/mdbook/src/15-interrupts/Cargo.toml index 5dd6b785..9b61538e 100644 --- a/mdbook/src/15-interrupts/Cargo.toml +++ b/mdbook/src/15-interrupts/Cargo.toml @@ -11,6 +11,7 @@ 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/debouncing.md b/mdbook/src/15-interrupts/debouncing.md index b7624d98..f535b880 100644 --- a/mdbook/src/15-interrupts/debouncing.md +++ b/mdbook/src/15-interrupts/debouncing.md @@ -23,6 +23,10 @@ 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-debounce.rs b/mdbook/src/15-interrupts/examples/count-debounce.rs index 33f4e9ec..780bf8eb 100644 --- a/mdbook/src/15-interrupts/examples/count-debounce.rs +++ b/mdbook/src/15-interrupts/examples/count-debounce.rs @@ -1,7 +1,10 @@ #![no_main] #![no_std] -use core::sync::atomic::{AtomicUsize, Ordering::{Acquire, AcqRel}}; +use core::sync::atomic::{ + AtomicUsize, + Ordering::{AcqRel, Acquire}, +}; use cortex_m::asm; use cortex_m_rt::entry; @@ -10,12 +13,11 @@ use panic_rtt_target as _; use rtt_target::{rprintln, rtt_init_print}; use microbit::{ - Board, hal::{ - self, - gpiote, + self, gpiote, pac::{self, interrupt}, }, + Board, }; static COUNTER: AtomicUsize = AtomicUsize::new(0); diff --git a/mdbook/src/15-interrupts/my-solution.md b/mdbook/src/15-interrupts/my-solution.md deleted file mode 100644 index 63845e65..00000000 --- a/mdbook/src/15-interrupts/my-solution.md +++ /dev/null @@ -1 +0,0 @@ -# My solution diff --git a/mdbook/src/15-interrupts/turn-signaller-revisited.md b/mdbook/src/15-interrupts/turn-signaller-revisited.md deleted file mode 100644 index 2d9a521a..00000000 --- a/mdbook/src/15-interrupts/turn-signaller-revisited.md +++ /dev/null @@ -1 +0,0 @@ -# Turn signaller revisited diff --git a/mdbook/src/15-interrupts/waiting-for-an-interrupt.md b/mdbook/src/15-interrupts/waiting-for-an-interrupt.md deleted file mode 100644 index 7a9152ee..00000000 --- a/mdbook/src/15-interrupts/waiting-for-an-interrupt.md +++ /dev/null @@ -1 +0,0 @@ -# Waiting for an interrupt (wfi, wfe, nop) 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..e1fe7c07 --- /dev/null +++ b/mdbook/src/15-interrupts/waiting-to-be-interrupted.md @@ -0,0 +1,14 @@ +# Waiting for an interrupt (wfi, wfe, nop) + +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 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 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 is still an instruction. It 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. + +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/README.md b/mdbook/src/16-snake-game/README.md index 555c7c37..86d4ab2f 100644 --- a/mdbook/src/16-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/SUMMARY.md b/mdbook/src/SUMMARY.md index 62d9312f..61e311a0 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -66,10 +66,7 @@ - [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 for an interrupt (wfi, wfe, nop)](15-interrupts/waiting-for-an-interrupt.md) - - [Turn signaller revisited](15-interrupts/turn-signaller-revisited.md) - - [My solution](15-interrupts/my-solution.md) - - [Concurrency](15-interrupts/concurrency.md) + - [Waiting to be interrupted](15-interrupts/waiting-to-be-interrupted.md) - [Snake game](16-snake-game/README.md) - [Game logic](16-snake-game/game-logic.md) - [Controls](16-snake-game/controls.md) From 056a56b00bbdb2ec8da9adae952b0366fc46ee8b Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sun, 30 Mar 2025 10:57:37 -0700 Subject: [PATCH 24/38] cleaned up 08 polling-led-toggle.rs --- .../examples/polling-led-toggle.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) 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 index d349add6..07d75977 100644 --- a/mdbook/src/08-inputs-and-outputs/examples/polling-led-toggle.rs +++ b/mdbook/src/08-inputs-and-outputs/examples/polling-led-toggle.rs @@ -29,12 +29,17 @@ fn main() -> ! { .into_push_pull_output(gpio::Level::Low); loop { - if button_a.is_low().unwrap() { - // Button A pressed: LED on - row1.set_high().unwrap(); - } else if button_b.is_low().unwrap() { - // Button B pressed: LED off - row1.set_low().unwrap(); + let on_pressed = button_a.is_low(); + let off_pressed = button_b.is_low(); + 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); } From 747fed64b7119a129ef3fae77aadd687696c4d6f Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sun, 30 Mar 2025 11:31:18 -0700 Subject: [PATCH 25/38] added 08 blink-held example --- .../examples/blink-held.rs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 mdbook/src/08-inputs-and-outputs/examples/blink-held.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); + } +} From 8fb5b3311eb03ab388e3e0eb3a5303c399166034 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sun, 30 Mar 2025 11:40:42 -0700 Subject: [PATCH 26/38] cleaned up issues in 08 code --- .../examples/button-a-bsp.rs | 4 +++- .../examples/polling-led-toggle.rs | 15 ++++++++------- mdbook/src/08-inputs-and-outputs/src/main.rs | 3 +-- 3 files changed, 12 insertions(+), 10 deletions(-) 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 index 1aa60c50..10f737c4 100644 --- a/mdbook/src/08-inputs-and-outputs/examples/button-a-bsp.rs +++ b/mdbook/src/08-inputs-and-outputs/examples/button-a-bsp.rs @@ -2,7 +2,9 @@ #![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] @@ -10,7 +12,7 @@ fn main() -> ! { rtt_init_print!(); let board = Board::take().unwrap(); - let button_a = board.buttons.button_a; + let mut button_a = board.buttons.button_a; loop { if button_a.is_low().unwrap() { 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 index 07d75977..dd38bf00 100644 --- a/mdbook/src/08-inputs-and-outputs/examples/polling-led-toggle.rs +++ b/mdbook/src/08-inputs-and-outputs/examples/polling-led-toggle.rs @@ -2,11 +2,12 @@ #![no_std] use cortex_m_rt::entry; -use embedded_hal::digital::OutputPin; +use embedded_hal::delay::DelayNs; +use embedded_hal::digital::{InputPin, OutputPin}; use microbit::hal::timer::Timer; use microbit::{hal::gpio, Board}; -use panic_halt as _; -use rtt_target::{rprintln, rtt_init_print}; +use panic_rtt_target as _; +use rtt_target::rtt_init_print; #[entry] fn main() -> ! { @@ -15,8 +16,8 @@ fn main() -> ! { let mut timer = Timer::new(board.TIMER0); // Configure buttons - let button_a = board.buttons.button_a; - let button_b = board.buttons.button_b; + 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 @@ -29,8 +30,8 @@ fn main() -> ! { .into_push_pull_output(gpio::Level::Low); loop { - let on_pressed = button_a.is_low(); - let off_pressed = button_b.is_low(); + 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) => (), diff --git a/mdbook/src/08-inputs-and-outputs/src/main.rs b/mdbook/src/08-inputs-and-outputs/src/main.rs index 711bff73..83560783 100644 --- a/mdbook/src/08-inputs-and-outputs/src/main.rs +++ b/mdbook/src/08-inputs-and-outputs/src/main.rs @@ -2,11 +2,10 @@ #![no_std] use cortex_m_rt::entry; -use embedded_hal::delay::DelayNs; use embedded_hal::digital::InputPin; use microbit::{board::Board, display::blocking::Display, hal::Timer}; use panic_rtt_target as _; -use rtt_target::{rprintln, rtt_init_print}; +use rtt_target::rtt_init_print; // Define LED patterns const LEFT_ARROW: [[u8; 5]; 5] = [ From ae0780533b0f5f15c74d423972fadcc81f292e1b Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sun, 30 Mar 2025 11:47:31 -0700 Subject: [PATCH 27/38] made slight change to 15 waiting section --- mdbook/src/15-interrupts/waiting-to-be-interrupted.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mdbook/src/15-interrupts/waiting-to-be-interrupted.md b/mdbook/src/15-interrupts/waiting-to-be-interrupted.md index e1fe7c07..9d5740ae 100644 --- a/mdbook/src/15-interrupts/waiting-to-be-interrupted.md +++ b/mdbook/src/15-interrupts/waiting-to-be-interrupted.md @@ -5,10 +5,10 @@ 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 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 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. +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 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 is still an instruction. It 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. 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. - - From 3ad65052966ee7dd42a458550ba1f0f20c03f5ac Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sun, 30 Mar 2025 11:51:46 -0700 Subject: [PATCH 28/38] removed last trace of 15 concurrency --- mdbook/src/15-interrupts/concurrency.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 mdbook/src/15-interrupts/concurrency.md diff --git a/mdbook/src/15-interrupts/concurrency.md b/mdbook/src/15-interrupts/concurrency.md deleted file mode 100644 index 5cbbf4f4..00000000 --- a/mdbook/src/15-interrupts/concurrency.md +++ /dev/null @@ -1 +0,0 @@ -# Concurrency From 750db856ca72dbb2b29599a4e9930018b0f53e5f Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Mon, 31 Mar 2025 00:59:57 -0700 Subject: [PATCH 29/38] fixed chapter numbering and linking issues --- mdbook/src/09-registers/README.md | 2 +- mdbook/src/12-i2c/the-challenge.md | 14 ++++++++------ mdbook/src/13-led-compass/README.md | 4 ++-- mdbook/src/14-punch-o-meter/gravity-is-up.md | 2 +- mdbook/src/appendix/3-mag-calibration/README.md | 2 +- mdbook/src/explore.md | 4 ++-- 6 files changed, 15 insertions(+), 13 deletions(-) diff --git a/mdbook/src/09-registers/README.md b/mdbook/src/09-registers/README.md index 937a058a..6d818a16 100644 --- a/mdbook/src/09-registers/README.md +++ b/mdbook/src/09-registers/README.md @@ -3,7 +3,7 @@ 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 ----- diff --git a/mdbook/src/12-i2c/the-challenge.md b/mdbook/src/12-i2c/the-challenge.md index d4ae6e1c..d8801499 100644 --- a/mdbook/src/12-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](../10-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/13-led-compass/README.md b/mdbook/src/13-led-compass/README.md index 4df7beec..9a4a54d3 100644 --- a/mdbook/src/13-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/14-punch-o-meter/gravity-is-up.md b/mdbook/src/14-punch-o-meter/gravity-is-up.md index b19892e1..739e7635 100644 --- a/mdbook/src/14-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/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/explore.md b/mdbook/src/explore.md index 7473e39c..beec143e 100644 --- a/mdbook/src/explore.md +++ b/mdbook/src/explore.md @@ -29,7 +29,7 @@ In preemptive multitasking a task that's currently being executed can, at any po *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). +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 @@ -76,7 +76,7 @@ the near future. ### Interrupts -We saw button interrupts briefly in [chapter 14](14-snake-game/controls.html). +We saw button interrupts briefly in [chapter 16](16-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. From 4638f49f99d8aa4575e3ffad6dde76acd67e7625 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 19 Jul 2025 15:10:00 -0700 Subject: [PATCH 30/38] fixed chapter number in reference, again --- mdbook/src/12-i2c/the-challenge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mdbook/src/12-i2c/the-challenge.md b/mdbook/src/12-i2c/the-challenge.md index d8801499..5d4021c3 100644 --- a/mdbook/src/12-i2c/the-challenge.md +++ b/mdbook/src/12-i2c/the-challenge.md @@ -6,7 +6,7 @@ commands "mag" for magnetometer as well as "acc" for accelerometer from the seri 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](../10-uart/index.html) and this chapter. However, here are a few clues: +[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"`. From 09da4f155bc2d7ba1049bc38605b909faff315cb Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 19 Jul 2025 16:32:28 -0700 Subject: [PATCH 31/38] added speaker section to interrupt chapter --- mdbook/src/15-interrupts/Cargo.toml | 1 + .../src/15-interrupts/examples/square-wave.rs | 40 ++++ mdbook/src/15-interrupts/src/main.rs | 40 ++++ mdbook/src/15-interrupts/the-mb2-speaker.md | 31 +++ mdbook/src/SUMMARY.md | 1 + mdbook/src/assets/speaker.svg | 196 ++++++++++++++++++ 6 files changed, 309 insertions(+) create mode 100644 mdbook/src/15-interrupts/examples/square-wave.rs create mode 100644 mdbook/src/15-interrupts/src/main.rs create mode 100644 mdbook/src/15-interrupts/the-mb2-speaker.md create mode 100644 mdbook/src/assets/speaker.svg diff --git a/mdbook/src/15-interrupts/Cargo.toml b/mdbook/src/15-interrupts/Cargo.toml index 9b61538e..3687249d 100644 --- a/mdbook/src/15-interrupts/Cargo.toml +++ b/mdbook/src/15-interrupts/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" 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" 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/src/main.rs b/mdbook/src/15-interrupts/src/main.rs new file mode 100644 index 00000000..bad276c4 --- /dev/null +++ b/mdbook/src/15-interrupts/src/main.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/the-mb2-speaker.md b/mdbook/src/15-interrupts/the-mb2-speaker.md new file mode 100644 index 00000000..75e55e2e --- /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 for five seconds and then stop. It is important to remember that our program +lives in flash on the MB2 — if we let the tone run forever then it will start up again each time we +reset or even power on the MB2. This can rapidly become quite annoying. + +Here's the code (`examples/square-wave.rs`). + +```rust +{{#include examples/square-wave.rs}} +``` diff --git a/mdbook/src/SUMMARY.md b/mdbook/src/SUMMARY.md index 61e311a0..88d260fa 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -67,6 +67,7 @@ - [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) - [Snake game](16-snake-game/README.md) - [Game logic](16-snake-game/game-logic.md) - [Controls](16-snake-game/controls.md) diff --git a/mdbook/src/assets/speaker.svg b/mdbook/src/assets/speaker.svg new file mode 100644 index 00000000..5b78e043 --- /dev/null +++ b/mdbook/src/assets/speaker.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + SPEAKER PIN + GROUND + + + + + + + + + + + + + CONE + PORT + + + + + + + From b7737a3c4e187011635c801e55390420445897b3 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 19 Jul 2025 20:11:08 -0700 Subject: [PATCH 32/38] added interrupt challenge problem --- mdbook/src/15-interrupts/my-solution.md | 15 +++ mdbook/src/15-interrupts/src/main.rs | 148 +++++++++++++++++++--- mdbook/src/15-interrupts/the-challenge.md | 20 +++ mdbook/src/SUMMARY.md | 2 + 4 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 mdbook/src/15-interrupts/my-solution.md create mode 100644 mdbook/src/15-interrupts/the-challenge.md 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/src/main.rs b/mdbook/src/15-interrupts/src/main.rs index bad276c4..c5e59316 100644 --- a/mdbook/src/15-interrupts/src/main.rs +++ b/mdbook/src/15-interrupts/src/main.rs @@ -3,36 +3,152 @@ 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; +use rtt_target::{rtt_init_print, rprintln}; use microbit::{ Board, - hal::{gpio, timer}, + hal::{gpio, timer, pac::{self, interrupt}}, }; -/// 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; +/// 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; -/// Number of cycles for 5 seconds of output. -const CYCLES: u32 = 5000 / PERIOD; +/// 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(); - 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); + // 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/SUMMARY.md b/mdbook/src/SUMMARY.md index 88d260fa..7a73ec56 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -68,6 +68,8 @@ - [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) - [Snake game](16-snake-game/README.md) - [Game logic](16-snake-game/game-logic.md) - [Controls](16-snake-game/controls.md) From 0306bc9c2e874836b966dc5f28190323e20478c6 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 19 Jul 2025 23:15:09 -0700 Subject: [PATCH 33/38] added PWM addendum to interrupts --- mdbook/src/15-interrupts/addendum-pwm.md | 33 ++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 mdbook/src/15-interrupts/addendum-pwm.md 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. From c5dd30f20009b5336fdc29babf2a762d27871eed Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Tue, 22 Jul 2025 13:09:17 -0700 Subject: [PATCH 34/38] Update mdbook/src/08-inputs-and-outputs/polling.md Co-authored-by: Daniel Egger --- mdbook/src/08-inputs-and-outputs/polling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mdbook/src/08-inputs-and-outputs/polling.md b/mdbook/src/08-inputs-and-outputs/polling.md index b358fcba..bb192a3d 100644 --- a/mdbook/src/08-inputs-and-outputs/polling.md +++ b/mdbook/src/08-inputs-and-outputs/polling.md @@ -10,4 +10,4 @@ This method of repeatedly checking inputs in a loop is called polling. When we 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. +>> **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. From 5134ee3d50c32d3fb8f0fcfe8a603b217f572878 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Tue, 22 Jul 2025 13:12:33 -0700 Subject: [PATCH 35/38] Update mdbook/src/15-interrupts/README.md Co-authored-by: Daniel Egger --- mdbook/src/15-interrupts/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mdbook/src/15-interrupts/README.md b/mdbook/src/15-interrupts/README.md index 6b1c713f..32857c1c 100644 --- a/mdbook/src/15-interrupts/README.md +++ b/mdbook/src/15-interrupts/README.md @@ -54,7 +54,7 @@ decorated with `#[interrupt]` so that it returns using a return-from-interrupt i than the normal way. The function may not take arguments and must return `()`. There are two steps to configure the interrupt. First, the GPIOTE must be set up to generate an -interrupt when the wire connect to Button A goes from high to low voltage. Second, the NVIC must be +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 a bogus interrupt before you are ready to handle it. From 90a7fcfddfac650a7f8a7722208ffff21ea61423 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Tue, 29 Jul 2025 13:10:37 -0700 Subject: [PATCH 36/38] rolled up changes suggested by review of PR #56 --- mdbook/src/08-inputs-and-outputs/README.md | 2 +- .../08-inputs-and-outputs/polling-sucks.md | 21 +- mdbook/src/08-inputs-and-outputs/polling.md | 3 +- mdbook/src/15-interrupts/README.md | 93 +++--- .../nvic-and-interrupt-priority.md | 25 +- .../waiting-to-be-interrupted.md | 7 +- mdbook/src/16-snake-game/controls.md | 2 +- mdbook/src/16-snake-game/final-assembly.md | 2 +- .../src/16-snake-game/nonblocking-display.md | 2 +- mdbook/src/SUMMARY.md | 1 + mdbook/src/assets/speaker.svg | 303 +++++++++--------- mdbook/src/explore.md | 145 +++------ 12 files changed, 282 insertions(+), 324 deletions(-) diff --git a/mdbook/src/08-inputs-and-outputs/README.md b/mdbook/src/08-inputs-and-outputs/README.md index 2f9f899f..a68be6c6 100644 --- a/mdbook/src/08-inputs-and-outputs/README.md +++ b/mdbook/src/08-inputs-and-outputs/README.md @@ -8,7 +8,7 @@ The micro:bit v2 has two physical buttons, Button A and Button B, connected to G [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 (1) or low (0). The buttons on the micro:bit are connected to pins; when the buttons are pressed, they pull the voltage at the pin low (to 0V ground). +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). diff --git a/mdbook/src/08-inputs-and-outputs/polling-sucks.md b/mdbook/src/08-inputs-and-outputs/polling-sucks.md index 3e5c75c8..fa32ecca 100644 --- a/mdbook/src/08-inputs-and-outputs/polling-sucks.md +++ b/mdbook/src/08-inputs-and-outputs/polling-sucks.md @@ -28,9 +28,9 @@ 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 much less responsive program (try for yourself and see how worse it is). +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 much less responsive program (try for yourself and see how much worse the interrupt latency is). -A "smarter" program would know that the processor isn't actually doing anything while the blink delay is running, so it could very well do other things while waiting for the delay to finish, namely checking for button presses. +A "smarter" program would know that the processor isn't actually doing anything while the blink delay is running, so it could very well do other things while waiting for the delay to finish, namely checking for button presses. ## Superloops @@ -56,11 +56,22 @@ Since we need to ensure responsiveness, we have to combine these different state 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). +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, and compare our current timer counter (if we have one) to a threshold, and transition states if any of the above conditions are met. +Our main superloop control flow will repeatedly poll the buttons, and compare our current timer counter (if we have one) to a threshold, and transition states if any of the above conditions are met. -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. +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 diff --git a/mdbook/src/08-inputs-and-outputs/polling.md b/mdbook/src/08-inputs-and-outputs/polling.md index bb192a3d..f212f46a 100644 --- a/mdbook/src/08-inputs-and-outputs/polling.md +++ b/mdbook/src/08-inputs-and-outputs/polling.md @@ -10,4 +10,5 @@ This method of repeatedly checking inputs in a loop is called polling. When we 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. +**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/15-interrupts/README.md b/mdbook/src/15-interrupts/README.md index 32857c1c..1bd76e9f 100644 --- a/mdbook/src/15-interrupts/README.md +++ b/mdbook/src/15-interrupts/README.md @@ -1,44 +1,22 @@ ## Interrupts -So far, we've gone though a fair bunch of topics about embedded software. We've read out buttons, -waited for timers, done serial communication, and talked to other things on the Microbit board 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. +So far, we've gone though a fair bunch of topics about embedded software. We've read out buttons, waited for timers, done serial communication, and talked to other things on the Microbit board 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 -Computation is always contextual: the core always needs memory to load inputs and store outputs to. -Our microcontroller is of what's known as a load-store-architecture, and as such the core does not -store and load it's computation parameters and results in RAM directly. Instead, our core has -access to a small amount scratch pad memory: the CPU registers. Confusingly, these CPU registers -are different from the device registers we discussed earlier in the [Registers] chapter. +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".) -As far as the core is concerned, all context about the computation that it is doing 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 them as their own scratchpad memory. 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. +All context 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. -Interrupt Service Routines (ISRs), often referred to as interrupt handlers, are special functions in -your application code that get called by the core in response to specific interrupts. 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. +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 functions 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 @@ -49,35 +27,40 @@ Let's define an ISR and configure an interrupt to "poke" the MB2 when Button A i {{#include examples/poke.rs}} ``` -The ISR handler function is "special". The name `GPIOTE` is required here, and the function must be -decorated with `#[interrupt]` so that it returns using a return-from-interrupt instruction rather -than the normal way. The function may not take arguments and must return `()`. +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 a bogus interrupt before you are ready to handle it. +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 GPIOTE records when an interrupt -has been issued, and that record is kept until it is explicitly cleared by the running -program. Without the `panic!()`, when the interrupt handler returns the GPIOTE will re-enable the -interrupt, notice that an interrupt has been issued and not cleared, and run the handler again. This -will continue forever: each time the interrupt handler returns it will be called again. In the next -section we will see how to clear the interrupt indication from within the interrupt handler. +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. In the next section we will see how to clear the interrupt indication from within the interrupt handler. 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. -When the ISR function returns (using a magic instruction), the CPU looks to see if interrupts have -happened that need to be handled, and if so calls one of the handlers (according to a priority order -set by the NVIC). Otherwise, the CPU restores the CPU registers and returns to the running program -as if nothing has happened. - -But if the core just goes on with its life after handling an interrupt, how does your device know -that it happened? Seeing as an ISR doesn't have any input parameters or result, how can ISR code -interact with application code? +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? -[Registers]: https://docs.rust-embedded.org/discovery-mb2/07-registers +[NVIC and Interrupt Priority]: nvic-and-interrupt-priority.html +[Registers]: ../09-registers/ diff --git a/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md b/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md index aee614b3..94ecbfe8 100644 --- a/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md +++ b/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md @@ -1,5 +1,4 @@ - -## Under The Hood +## 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 @@ -20,16 +19,16 @@ Controller. 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 prioritise these interrupts, remember which ones still need to be procesed, +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. Depending on its configuration, the NVIC can ensure the current interrupt is fully processed before a new one is executed, or it can stop the processor in the middle of one interrupt in order to handle another that's higher priority. This is called "preemption" and allows processors to respond very quickly to critical events. For example, a robot controller might use low-priority interrupts -to keep track sending status information to the operator, but also have a high-priority interrupt to -detect an emergency stop button being pushed so it can immediately stop moving the motors. You -wouldn't want it to wait until it had finished sending a data packet to get around to stopping! +to manage sending status information to the operator, but also have a high-priority interrupt when a +sensor detects an imminent collision so that it can immediately stop moving the motors. You wouldn't +want it to wait until it had finished sending a data packet to get around to stopping! 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 @@ -66,10 +65,16 @@ handler. We do this using the `#[interrupt]` macro, which requires that our func 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. +For more details on how these interrupt handlers are managed in Rust, see the Exceptions and +Interrupts chapters in the [Embedded Rust Book]. + +### Intertupt Priorities + +The NVIC has a settable "priority" for each interrupt. If a higher-priority interrupt happens during the execution of an ISR, that ISR will be paused just as the main program was, and the higher-priority ISR will be run. + +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. Thus, 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 one of the highest-priority ISRs vectored there. Otherwise, the CPU returns to the running program. + [Architecture Reference Manual]: https://developer.arm.com/documentation/ddi0403/latest [`cortex-m-rt`]: https://docs.rs/cortex-m-rt -[Exceptions]: https://docs.rust-embedded.org/book/start/exceptions.html -[Interrupts]: https://docs.rust-embedded.org/book/start/interrupts.html +[Embedded Rust Book]: https://docs.rust-embedded.org/book/ diff --git a/mdbook/src/15-interrupts/waiting-to-be-interrupted.md b/mdbook/src/15-interrupts/waiting-to-be-interrupted.md index 9d5740ae..6a771459 100644 --- a/mdbook/src/15-interrupts/waiting-to-be-interrupted.md +++ b/mdbook/src/15-interrupts/waiting-to-be-interrupted.md @@ -1,11 +1,10 @@ -# Waiting for an interrupt (wfi, wfe, nop) +# 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()`. +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 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. +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 clocks and some 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.
diff --git a/mdbook/src/16-snake-game/controls.md b/mdbook/src/16-snake-game/controls.md index c9e6fda3..b808d6f1 100644 --- a/mdbook/src/16-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/16-snake-game/final-assembly.md b/mdbook/src/16-snake-game/final-assembly.md index b9a5dc29..f65564a8 100644 --- a/mdbook/src/16-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/16-snake-game/nonblocking-display.md b/mdbook/src/16-snake-game/nonblocking-display.md index 68603b81..2c5ab7f9 100644 --- a/mdbook/src/16-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/SUMMARY.md b/mdbook/src/SUMMARY.md index 7a73ec56..b46dbc2c 100644 --- a/mdbook/src/SUMMARY.md +++ b/mdbook/src/SUMMARY.md @@ -70,6 +70,7 @@ - [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) diff --git a/mdbook/src/assets/speaker.svg b/mdbook/src/assets/speaker.svg index 5b78e043..d1e4c617 100644 --- a/mdbook/src/assets/speaker.svg +++ b/mdbook/src/assets/speaker.svg @@ -24,7 +24,7 @@ inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" inkscape:zoom="1.6026445" - inkscape:cx="380.6209" + inkscape:cx="380.93289" inkscape:cy="180.32695" inkscape:window-width="2576" inkscape:window-height="1625" @@ -38,159 +38,170 @@ inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1"> - - - - - - SPEAKER PIN - GROUND - - - - - - + style="fill:#f9f9f9;stroke:none;stroke-width:1.00157;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:2.00314, 4.00628" + id="rect2" + width="497.92703" + height="348.79849" + x="0.62396872" + y="0.62396872" /> + id="g1" + transform="translate(4.3677809,6.8636557)"> + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 102.33087,126.97763 65.82869,65.8287 h 127.6016 l 64.42477,-64.89275" + id="path1" + sodipodi:nodetypes="cccc" /> + + style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 194.13888,192.47321 v 28.67881 h 76.32976 v -28.2376 H 269.145" + id="path2" /> + style="fill:none;stroke:#000000;stroke-width:1.15276;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none" + d="m 211.78738,221.59323 c 0.14707,0.73536 -0.29415,2.35314 0.44121,2.20607 0.73535,-0.14707 -0.44121,-2.95598 -0.44121,-2.20607 0,1.47805 0.31846,2.93919 0.44121,4.41213 0.0244,0.29313 0,0.58828 0,0.88242 0,0.14708 -0.0465,0.30169 0,0.44122 0.10399,0.31198 0.33722,0.57044 0.44121,0.88242 0.0465,0.13953 0,0.29414 0,0.44121 0,0.29415 0,0.58829 0,0.88243 0,0.98733 -0.11223,2.19062 0,3.08849 0.0577,0.46148 0.35,0.86759 0.44121,1.32363 0.0577,0.28843 0,0.58829 0,0.88243 0.14707,0.29414 0.33722,0.57044 0.44122,0.88242 0.14196,0.42588 -0.12267,1.71539 0,2.20607 0.11279,0.45119 0.35,0.86759 0.44121,1.32363 0.11537,0.57686 0,1.17657 0,1.76485 0,0.73536 0,1.47071 0,2.20607 0,0.29414 0.0325,0.59008 0,0.88242 -0.11484,1.03359 -0.33774,2.0537 -0.44121,3.08849 -0.0913,0.91304 0,2.17079 0,3.08849 0,0.44121 0,0.88242 0,1.32363 0,0.44122 -0.0624,0.88686 0,1.32364 0.0857,0.6003 0.39801,1.16 0.44121,1.76485 0.10478,1.46697 -0.0773,2.94345 0,4.41213 0.65235,12.39462 0.44121,-6.72606 0.44121,9.70667 0,1.32364 0,2.64728 0,3.97091 0,0.38235 0.0852,1.50927 0,1.76485 -0.10399,0.31199 -0.29414,0.58829 -0.44121,0.88243 -0.29414,0.58828 -0.58828,1.17657 -0.88243,1.76485 -0.29414,0.58828 -0.53383,1.2071 -0.88242,1.76485 -0.38974,0.62358 -0.89622,1.16647 -1.32364,1.76485 -0.30821,0.4315 -0.6096,0.86893 -0.88242,1.32364 -0.1692,0.28199 -0.2439,0.61933 -0.44122,0.88242 -0.0865,0.11534 -1.47267,1.58955 -1.76485,1.76485 -0.56399,0.3384 -1.21759,0.51759 -1.76485,0.88243 -0.34611,0.23074 -0.54964,0.63283 -0.88242,0.88242 -0.77104,0.57828 -1.31625,0.43752 -2.20606,0.88243 -0.47429,0.23714 -0.86324,0.61933 -1.32364,0.88242 -1.42004,0.81145 -2.80508,1.36309 -4.41213,1.76485 -0.69384,0.17346 -1.50358,-0.14049 -2.20606,0 -0.037,0.007 -3.51729,0.88119 -3.5297,0.88243 -6.11417,0 0.1745,-0.21402 -4.41212,0.44121 -0.43678,0.0624 -0.88243,0 -1.32364,0 -0.29414,0 -0.61934,-0.13154 -0.88243,0 -0.13154,0.0658 0.104,0.33722 0,0.44121 -4.2e-4,4.2e-4 -1.32163,-6.6e-4 -1.32363,0 -0.19732,0.0658 -0.23606,0.40702 -0.44122,0.44121 -0.72535,0.1209 -1.4781,-0.10399 -2.20606,0 -0.32555,0.0465 -0.57044,0.33722 -0.88242,0.44122 -0.28587,0.0953 -1.84485,0 -2.20607,0 -0.88242,0 -1.76485,0 -2.64727,0 -0.44121,0 -0.88843,-0.0725 -1.32364,0 -0.45875,0.0765 -0.86489,0.36475 -1.32364,0.44121 -0.43521,0.0725 -0.88242,0 -1.32363,0 -0.44122,0 -0.88243,0 -1.32364,0 -0.14707,0 -0.33722,-0.104 -0.44121,0 -0.58829,0.58828 0.73535,0.44121 -0.44122,0.44121" + id="path3" /> - - + + SPEAKER PIN + GROUND + + + + + + + + + + + + + CONE - CONE + PORT - - - - + y="55.90226" + id="text7">PORT + + + + + diff --git a/mdbook/src/explore.md b/mdbook/src/explore.md index beec143e..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 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. - -### 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 16](16-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. From 2208e10b2ff60d559147ecaa3cb2af0e08dda8f5 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Tue, 29 Jul 2025 15:27:35 -0700 Subject: [PATCH 37/38] made a final editing pass; believe ready to publish --- .../08-inputs-and-outputs/polling-sucks.md | 12 +++++------ .../08-inputs-and-outputs/the-challenge.md | 2 +- mdbook/src/15-interrupts/README.md | 10 ++++----- mdbook/src/15-interrupts/debouncing.md | 10 ++++----- .../nvic-and-interrupt-priority.md | 21 +++++++------------ .../sharing-data-with-globals.md | 18 ++++++++-------- mdbook/src/15-interrupts/the-mb2-speaker.md | 6 +++--- .../waiting-to-be-interrupted.md | 6 ++---- 8 files changed, 37 insertions(+), 48 deletions(-) diff --git a/mdbook/src/08-inputs-and-outputs/polling-sucks.md b/mdbook/src/08-inputs-and-outputs/polling-sucks.md index fa32ecca..b020e744 100644 --- a/mdbook/src/08-inputs-and-outputs/polling-sucks.md +++ b/mdbook/src/08-inputs-and-outputs/polling-sucks.md @@ -28,9 +28,9 @@ 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 much less responsive program (try for yourself and see how much worse the interrupt latency is). +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, so it could very well do other things while waiting for the delay to finish, namely checking for button presses. +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 @@ -58,11 +58,9 @@ Since we need to ensure responsiveness, we have to combine these different state 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, and compare our current timer counter (if we have one) to a threshold, and transition states if any of the above conditions are met. +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. +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}} @@ -75,7 +73,7 @@ Superloops work and are often used in embedded systems, but the programmer has t ## Concurrency -Doing multiple things at once is called *concurrent* programming, and shows up in many places in programming, but especially in embedded systems. There's a whole host of techniques for implementing systems that concurrently 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. +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. diff --git a/mdbook/src/08-inputs-and-outputs/the-challenge.md b/mdbook/src/08-inputs-and-outputs/the-challenge.md index 612218d8..025edfab 100644 --- a/mdbook/src/08-inputs-and-outputs/the-challenge.md +++ b/mdbook/src/08-inputs-and-outputs/the-challenge.md @@ -12,4 +12,4 @@ You'll need to: - 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. +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/15-interrupts/README.md b/mdbook/src/15-interrupts/README.md index 1bd76e9f..57bed543 100644 --- a/mdbook/src/15-interrupts/README.md +++ b/mdbook/src/15-interrupts/README.md @@ -1,10 +1,10 @@ ## Interrupts -So far, we've gone though a fair bunch of topics about embedded software. We've read out buttons, waited for timers, done serial communication, and talked to other things on the Microbit board 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. +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"! +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. @@ -12,9 +12,9 @@ Interrupts are aptly named: they allow peripherals to actually interrupt the cor 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".) -All context 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. +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 functions 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. +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. @@ -63,4 +63,4 @@ 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/ +[Registers]: ../09-registers/index.html diff --git a/mdbook/src/15-interrupts/debouncing.md b/mdbook/src/15-interrupts/debouncing.md index f535b880..1a3ca197 100644 --- a/mdbook/src/15-interrupts/debouncing.md +++ b/mdbook/src/15-interrupts/debouncing.md @@ -15,10 +15,10 @@ 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 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. +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. @@ -27,6 +27,6 @@ you run the example you should see one count per button press. {{#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 +**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/nvic-and-interrupt-priority.md b/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md index 94ecbfe8..b73a1eb1 100644 --- a/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md +++ b/mdbook/src/15-interrupts/nvic-and-interrupt-priority.md @@ -22,13 +22,13 @@ each pin, or a UART having both a "data received" and "data finished transmissio 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. -Depending on its configuration, the NVIC can ensure the current interrupt is fully processed before -a new one is executed, or it can stop the processor in the middle of one interrupt in order to -handle another that's higher priority. This is called "preemption" and 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 have a high-priority interrupt when a -sensor detects an imminent collision so that it can immediately stop moving the motors. You wouldn't -want it to wait until it had finished sending a data packet to get around to stopping! +### 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 @@ -68,13 +68,6 @@ script to arrange for the address of that function to be placed in the right par For more details on how these interrupt handlers are managed in Rust, see the Exceptions and Interrupts chapters in the [Embedded Rust Book]. -### Intertupt Priorities - -The NVIC has a settable "priority" for each interrupt. If a higher-priority interrupt happens during the execution of an ISR, that ISR will be paused just as the main program was, and the higher-priority ISR will be run. - -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. Thus, 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 one of the highest-priority ISRs vectored there. Otherwise, the CPU returns to the running program. - - [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 index 31c905a0..87317820 100644 --- a/mdbook/src/15-interrupts/sharing-data-with-globals.md +++ b/mdbook/src/15-interrupts/sharing-data-with-globals.md @@ -1,13 +1,13 @@ ## Sharing Data With Globals -> **NOTE** This content is partially taken with permission from the blog post +> **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. +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 @@ -174,15 +174,15 @@ data could end up corrupted. 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 accessing some -data shared with the interrupt, just to have the interrupt run and ALSO access that data! +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 context. + without access to any passed-in context. * Interrupts can be triggered multiple times, once for each time that the event occurs. @@ -275,8 +275,8 @@ main loop, just after the `wfi()` "wait for interrupt". The count will then be r an interrupt handler finishes (`examples/count-bounce.rs`). Again, 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. +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/the-mb2-speaker.md b/mdbook/src/15-interrupts/the-mb2-speaker.md index 75e55e2e..07b1ae82 100644 --- a/mdbook/src/15-interrupts/the-mb2-speaker.md +++ b/mdbook/src/15-interrupts/the-mb2-speaker.md @@ -20,9 +20,9 @@ Let's push the speaker cone out and then in 220 times per second. This will prod 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 for five seconds and then stop. It is important to remember that our program -lives in flash on the MB2 — if we let the tone run forever then it will start up again each time we -reset or even power on the MB2. This can rapidly become quite annoying. +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`). diff --git a/mdbook/src/15-interrupts/waiting-to-be-interrupted.md b/mdbook/src/15-interrupts/waiting-to-be-interrupted.md index 6a771459..50df1edb 100644 --- a/mdbook/src/15-interrupts/waiting-to-be-interrupted.md +++ b/mdbook/src/15-interrupts/waiting-to-be-interrupted.md @@ -4,10 +4,8 @@ You may have wondered why we have been using `asm::wfi()` (wait for instruction) 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 clocks and some 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. +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 is still an instruction. It 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 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. From f5d30f530cb599e0bf9c6cd48430fc1ada7f5ba6 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Wed, 30 Jul 2025 18:50:43 -0700 Subject: [PATCH 38/38] fixed a few more reported mistakes in interrupts chapter --- mdbook/src/15-interrupts/README.md | 10 +++++++++- .../15-interrupts/sharing-data-with-globals.md | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/mdbook/src/15-interrupts/README.md b/mdbook/src/15-interrupts/README.md index 57bed543..7b23f39a 100644 --- a/mdbook/src/15-interrupts/README.md +++ b/mdbook/src/15-interrupts/README.md @@ -54,7 +54,15 @@ 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. In the next section we will see how to clear the interrupt indication from within the interrupt handler. +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 diff --git a/mdbook/src/15-interrupts/sharing-data-with-globals.md b/mdbook/src/15-interrupts/sharing-data-with-globals.md index 87317820..2ba51c25 100644 --- a/mdbook/src/15-interrupts/sharing-data-with-globals.md +++ b/mdbook/src/15-interrupts/sharing-data-with-globals.md @@ -266,17 +266,25 @@ 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`). Again, the count is bumped up 1 on every -push of the MB2 A button. +an interrupt handler finishes (`examples/count-bounce.rs`). + +```rust +{{#include examples/count-bounce.rs}} +``` -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. +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