From 41b813a3b2300b7511b661a8992910779d42b580 Mon Sep 17 00:00:00 2001 From: Bart Massey Date: Sat, 26 Oct 2024 15:20:00 -0700 Subject: [PATCH] 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 ca2a4f0..260fc45 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 5e452dd..55ef737 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 0000000..459f079 --- /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 0000000..aee614b --- /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 e75761d..46316b1 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)