From ff5f5021fb4f469f72b3645760238ebe1c4d99af Mon Sep 17 00:00:00 2001 From: Ulf Lilleengen Date: Wed, 10 Jan 2024 09:48:09 +0100 Subject: [PATCH] cleanup docs and add channel synchronization example --- docs/README.md | 4 + docs/modules/ROOT/nav.adoc | 6 +- docs/modules/ROOT/pages/delaying_a_task.adoc | 28 ------ .../modules/ROOT/pages/project_structure.adoc | 10 +-- .../ROOT/pages/sharing_peripherals.adoc | 86 +++++++++++++++---- docs/modules/ROOT/pages/time_keeping.adoc | 60 +++++++++++++ examples/rp/.cargo/config.toml | 2 +- examples/rp/src/bin/blinky_two_channels.rs | 47 ++++++++++ 8 files changed, 187 insertions(+), 56 deletions(-) create mode 100644 docs/README.md delete mode 100644 docs/modules/ROOT/pages/delaying_a_task.adoc create mode 100644 docs/modules/ROOT/pages/time_keeping.adoc create mode 100644 examples/rp/src/bin/blinky_two_channels.rs diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..0bf3a6c89 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,4 @@ +# embassy docs + +The documentation hosted at [https://embassy.dev/book](https://embassy.dev/book). Building the documentation requires +cloning the [embassy-book](https://github.com/embassy-rs/embassy-book) repository and following the instructions. diff --git a/docs/modules/ROOT/nav.adoc b/docs/modules/ROOT/nav.adoc index b692c44e1..44b0eddb9 100644 --- a/docs/modules/ROOT/nav.adoc +++ b/docs/modules/ROOT/nav.adoc @@ -3,11 +3,11 @@ ** xref:project_structure.adoc[Project Structure] ** xref:new_project.adoc[Starting a new Embassy project] ** xref:best_practices.adoc[Best Practices] -* xref:layer_by_layer.adoc[Bare metal to async] * xref:runtime.adoc[Executor] -* xref:delaying_a_task.adoc[Delaying a Task] -* xref:sharing_peripherals.adoc[Sharing peripherals between tasks] +* xref::time_keeping.adoc[Time-keeping] +* xref:sharing_peripherals.adoc[Sharing peripherals] * xref:hal.adoc[HAL] +** xref:layer_by_layer.adoc[Anatomy of an async HAL] ** xref:nrf.adoc[nRF] ** xref:stm32.adoc[STM32] * xref:bootloader.adoc[Bootloader] diff --git a/docs/modules/ROOT/pages/delaying_a_task.adoc b/docs/modules/ROOT/pages/delaying_a_task.adoc deleted file mode 100644 index 3171e3515..000000000 --- a/docs/modules/ROOT/pages/delaying_a_task.adoc +++ /dev/null @@ -1,28 +0,0 @@ -= Delaying a Task - -In an embedded program, delaying a task is one of the most common actions taken. In an event loop, delays will need to be inserted to ensure -that other tasks have a chance to run before the next iteration of the loop is called, if no other I/O is performed. Embassy provides an abstraction -to delay the current task for a specified interval of time. - -Timing is serviced by the `embassy::time::Timer` struct, which provides two timing methods. - -`Timer::at` creates a future that completes at the specified `Instant`, relative to the system boot time. -`Timer::after` creates a future that completes after the specified `Duration`, relative to when the future was created. - -An example of a delay is provided as follows: - -[,rust] ----- -use embassy::executor::{task, Executor}; -use embassy::time::{Duration, Timer}; - -#[task] -/// Task that ticks periodically -async fn tick_periodic() -> ! { - loop { - rprintln!("tick!"); - // async sleep primitive, suspends the task for 500ms. - Timer::after(Duration::from_millis(500)).await; - } -} ----- \ No newline at end of file diff --git a/docs/modules/ROOT/pages/project_structure.adoc b/docs/modules/ROOT/pages/project_structure.adoc index 3e6008ec4..61ffd05a6 100644 --- a/docs/modules/ROOT/pages/project_structure.adoc +++ b/docs/modules/ROOT/pages/project_structure.adoc @@ -18,7 +18,7 @@ my-project |- rust-toolchain.toml ---- -=== .cargo/config.toml +== .cargo/config.toml This directory/file describes what platform you're on, and configures link:https://github.com/probe-rs/probe-rs[probe-rs] to deploy to your device. @@ -36,17 +36,17 @@ target = "thumbv6m-none-eabi" # <-change for your platform DEFMT_LOG = "trace" # <- can change to info, warn, or error ---- -=== build.rs +== build.rs This is the build script for your project. It links defmt (what is defmt?) and the `memory.x` file if needed. This file is pretty specific for each chipset, just copy and paste from the corresponding link:https://github.com/embassy-rs/embassy/tree/main/examples[example]. -=== Cargo.toml +== Cargo.toml This is your manifest file, where you can configure all of the embassy components to use the features you need. TODO: someone should exhaustively describe every feature for every component! -=== memory.x +== memory.x This file outlines the flash/ram usage of your program. It is especially useful when using link:https://github.com/embassy-rs/nrf-softdevice[nrf-softdevice] on an nRF5x. @@ -63,7 +63,7 @@ MEMORY } ---- -=== rust-toolchain.toml +== rust-toolchain.toml This file configures the rust version and configuration to use. diff --git a/docs/modules/ROOT/pages/sharing_peripherals.adoc b/docs/modules/ROOT/pages/sharing_peripherals.adoc index 41f467942..fcba0e27b 100644 --- a/docs/modules/ROOT/pages/sharing_peripherals.adoc +++ b/docs/modules/ROOT/pages/sharing_peripherals.adoc @@ -1,6 +1,12 @@ = Sharing peripherals between tasks -Often times, more than one task needs access to the same resource (pin, communication interface, etc.). The following example shows how to use the on-board LED on a Raspberry Pi Pico board by two tasks simultaneously. +Often times, more than one task needs access to the same resource (pin, communication interface, etc.). Embassy provides many different synchronization primitives in the link:https://crates.io/crates/embassy-sync[embassy-sync] crate. + +The following examples shows different ways to use the on-board LED on a Raspberry Pi Pico board by two tasks simultaneously. + +== Sharing using a Mutex + +Using mutual exclusion is the simplest way to share a peripheral. [,rust] ---- @@ -29,13 +35,12 @@ async fn main(spawner: Spawner) { let dt = 100 * 1_000_000; let k = 1.003; - unwrap!(spawner.spawn(toggle(&LED, Duration::from_nanos(dt)))); - unwrap!(spawner.spawn(toggle_slightly_slower( - &LED, - Duration::from_nanos((dt as f64 * k) as u64) - ))); + unwrap!(spawner.spawn(toggle_led(&LED, Duration::from_nanos(dt)))); + unwrap!(spawner.spawn(toggle_led(&LED, Duration::from_nanos((dt as f64 * k) as u64)))); } +// A pool size of 2 means you can spawn two instances of this task. +#[embassy_executor::task(pool_size = 2)] async fn toggle_led(led: &'static LedType, delay: Duration) { let mut ticker = Ticker::every(delay); loop { @@ -48,31 +53,74 @@ async fn toggle_led(led: &'static LedType, delay: Duration) { ticker.next().await; } } -#[embassy_executor::task] -async fn toggle(led: &'static LedType, delay: Duration) { - toggle_led(led, delay).await -} - -#[embassy_executor::task] -async fn toggle_slightly_slower(led: &'static LedType, delay: Duration) { - toggle_led(led, delay).await -} ---- The structure facilitating access to the resource is the defined `LedType`. -== Why so complicated +=== Why so complicated Unwrapping the layers gives insight into why each one is needed. -=== `Mutex` +==== `Mutex` The mutex is there so if one task gets the resource first and begins modifying it, all other tasks wanting to write will have to wait (the `led.lock().await` will return immediately if no task has locked the mutex, and will block if it is accessed somewhere else). -=== `Option` +==== `Option` The `LED` variable needs to be defined outside the main task as references accepted by tasks need to be `'static`. However, if it is outside the main task, it cannot be initialised to point to any pin, as the pins themselves are not initialised. Thus, it is set to `None`. -=== `Output` +==== `Output` To indicate that the pin will be set to an Output. The `AnyPin` could have been `embassy_rp::peripherals::PIN_25`, however this option lets the `toggle_led` function be more generic. + +== Sharing using a Channel + +A channel is another way to ensure exclusive access to a resource. Using a channel is great in the cases where the access can happen at a later point in time, allowing you to enqueue operations and do other things. + +[,rust] +---- +use defmt::*; +use embassy_executor::Spawner; +use embassy_rp::gpio; +use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; +use embassy_sync::channel::{Channel, Sender}; +use embassy_time::{Duration, Ticker}; +use gpio::{AnyPin, Level, Output}; +use {defmt_rtt as _, panic_probe as _}; + +enum LedState { + Toggle, +} +static CHANNEL: Channel = Channel::new(); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + let mut led = Output::new(AnyPin::from(p.PIN_25), Level::High); + + let dt = 100 * 1_000_000; + let k = 1.003; + + unwrap!(spawner.spawn(toggle_led(CHANNEL.sender(), Duration::from_nanos(dt)))); + unwrap!(spawner.spawn(toggle_led(CHANNEL.sender(), Duration::from_nanos((dt as f64 * k) as u64)))); + + loop { + match CHANNEL.receive().await { + LedState::Toggle => led.toggle(), + } + } +} + +// A pool size of 2 means you can spawn two instances of this task. +#[embassy_executor::task(pool_size = 2)] +async fn toggle_led(control: Sender<'static, ThreadModeRawMutex, LedState, 64>, delay: Duration) { + let mut ticker = Ticker::every(delay); + loop { + control.send(LedState::Toggle).await; + ticker.next().await; + } +} +---- + +This example replaces the Mutex with a Channel, and uses another task (the main loop) to drive the LED. The advantage of this approach is that only a single task references the peripheral, separating concerns. However, using a Mutex has a lower overhead and might be necessary if you need to ensure +that the operation is ecompleted before continuing to do other work in your task. diff --git a/docs/modules/ROOT/pages/time_keeping.adoc b/docs/modules/ROOT/pages/time_keeping.adoc new file mode 100644 index 000000000..5068216ed --- /dev/null +++ b/docs/modules/ROOT/pages/time_keeping.adoc @@ -0,0 +1,60 @@ += Time-keeping + +In an embedded program, delaying a task is one of the most common actions taken. In an event loop, delays will need to be inserted to ensure +that other tasks have a chance to run before the next iteration of the loop is called, if no other I/O is performed. Embassy provides abstractions +to delay the current task for a specified interval of time. + +The interface for time-keeping in Embassy is handled by the link:https://crates.io/crates/embassy-time[embassy-time] crate. The types can be used with the internal +timer queue in link:https://crates.io/crates/embassy-executor[embassy-executor] or a custom timer queue implementation. + +== Timer + +The `embassy::time::Timer` type provides two timing methods. + +`Timer::at` creates a future that completes at the specified `Instant`, relative to the system boot time. +`Timer::after` creates a future that completes after the specified `Duration`, relative to when the future was created. + +An example of a delay is provided as follows: + +[,rust] +---- +use embassy::executor::{task, Executor}; +use embassy::time::{Duration, Timer}; + +#[task] +/// Task that ticks periodically +async fn tick_periodic() -> ! { + loop { + rprintln!("tick!"); + // async sleep primitive, suspends the task for 500ms. + Timer::after(Duration::from_millis(500)).await; + } +} +---- + +== Delay + +The `embassy::time::Delay` type provides an implementation of the link:https://docs.rs/embedded-hal/1.0.0/embedded_hal/delay/index.html[embedded-hal] and +link:https://docs.rs/embedded-hal-async/latest/embedded_hal_async/delay/index.html[embedded-hal-async] traits. This can be used for drivers +that expect a generic delay implementation to be provided. + +An example of how this can be used: + +[,rust] +---- +use embassy::executor::{task, Executor}; + +#[task] +/// Task that ticks periodically +async fn tick_periodic() -> ! { + loop { + rprintln!("tick!"); + // async sleep primitive, suspends the task for 500ms. + generic_delay(embassy::time::Delay).await + } +} + +async fn generic_delay(delay: D) { + delay.delay_ms(500).await; +} +---- diff --git a/examples/rp/.cargo/config.toml b/examples/rp/.cargo/config.toml index 3d7d61740..04490c789 100644 --- a/examples/rp/.cargo/config.toml +++ b/examples/rp/.cargo/config.toml @@ -1,5 +1,5 @@ [target.'cfg(all(target_arch = "arm", target_os = "none"))'] -runner = "probe-rs run --chip RP2040" +runner = "probe-rs run --chip RP2040 --probe 1209:4853:0e0039001450563641333620" [build] target = "thumbv6m-none-eabi" # Cortex-M0 and Cortex-M0+ diff --git a/examples/rp/src/bin/blinky_two_channels.rs b/examples/rp/src/bin/blinky_two_channels.rs new file mode 100644 index 000000000..6179dc260 --- /dev/null +++ b/examples/rp/src/bin/blinky_two_channels.rs @@ -0,0 +1,47 @@ +#![no_std] +#![no_main] +/// This example demonstrates how to access a given pin from more than one embassy task +/// The on-board LED is toggled by two tasks with slightly different periods, leading to the +/// apparent duty cycle of the LED increasing, then decreasing, linearly. The phenomenon is similar +/// to interference and the 'beats' you can hear if you play two frequencies close to one another +/// [Link explaining it](https://www.physicsclassroom.com/class/sound/Lesson-3/Interference-and-Beats) +use defmt::*; +use embassy_executor::Spawner; +use embassy_rp::gpio; +use embassy_sync::blocking_mutex::raw::ThreadModeRawMutex; +use embassy_sync::channel::{Channel, Sender}; +use embassy_time::{Duration, Ticker}; +use gpio::{AnyPin, Level, Output}; +use {defmt_rtt as _, panic_probe as _}; + +enum LedState { + Toggle, +} +static CHANNEL: Channel = Channel::new(); + +#[embassy_executor::main] +async fn main(spawner: Spawner) { + let p = embassy_rp::init(Default::default()); + let mut led = Output::new(AnyPin::from(p.PIN_25), Level::High); + + let dt = 100 * 1_000_000; + let k = 1.003; + + unwrap!(spawner.spawn(toggle_led(CHANNEL.sender(), Duration::from_nanos(dt)))); + unwrap!(spawner.spawn(toggle_led(CHANNEL.sender(), Duration::from_nanos((dt as f64 * k) as u64)))); + + loop { + match CHANNEL.receive().await { + LedState::Toggle => led.toggle(), + } + } +} + +#[embassy_executor::task(pool_size = 2)] +async fn toggle_led(control: Sender<'static, ThreadModeRawMutex, LedState, 64>, delay: Duration) { + let mut ticker = Ticker::every(delay); + loop { + control.send(LedState::Toggle).await; + ticker.next().await; + } +}