From 3726468bb88c86cc98b869962fe5837eb8cb150c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 4 Jun 2026 11:29:06 +0200 Subject: [PATCH 01/10] non blocking embedded slides --- training-slides/src/SUMMARY.md | 1 + .../transaction-non-blocking.drawio.svg | 4 + training-slides/src/non-blocking-embedded.md | 103 ++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 training-slides/src/images/transaction-non-blocking.drawio.svg create mode 100644 training-slides/src/non-blocking-embedded.md diff --git a/training-slides/src/SUMMARY.md b/training-slides/src/SUMMARY.md index a348b3f4..2c1c7496 100644 --- a/training-slides/src/SUMMARY.md +++ b/training-slides/src/SUMMARY.md @@ -98,6 +98,7 @@ Topics about using Rust on ARM Cortex-M Microcontrollers (and similar). Requires * [The Embedded HAL and its implementations](./embedded-hals.md) * [Board Support Crates](./board-support.md) * [Using defmt](./defmt.md) +* [Non-blocking programming and async/await](./non-blocking-embedded.md) ## Under development diff --git a/training-slides/src/images/transaction-non-blocking.drawio.svg b/training-slides/src/images/transaction-non-blocking.drawio.svg new file mode 100644 index 00000000..f4d2a21e --- /dev/null +++ b/training-slides/src/images/transaction-non-blocking.drawio.svg @@ -0,0 +1,4 @@ + + + +
Store Pointer and
Size for Interrupt
Store Pointer and...
Write initial data
Write initial data
Yield to OS
Yield to OS
data tranfers
in interrupts
data tranfers...
Interrupt fires
Interrupt fires
Yes
Yes
No
No
All Data
Written?
All Data...
Notify
Task
Notify...

Interrupt Done

Interrupt Don...
Task Context
Task Context
Interrupt Context
Interrupt Context
Write next
data
Write next...
Done
Done
Not Finished
Not Finished
\ No newline at end of file diff --git a/training-slides/src/non-blocking-embedded.md b/training-slides/src/non-blocking-embedded.md new file mode 100644 index 00000000..daadceb7 --- /dev/null +++ b/training-slides/src/non-blocking-embedded.md @@ -0,0 +1,103 @@ +# Non-blocking programming and async/await + +## Non-blocking programming + +In embedded systems, a generic model of how non-blocking programming works +often looks like this: + +
+ +
+ +Note: + +- Technically, yielding is optional and you can do busy-waiting to wait + for an operation to complete. However, allowing the CPU to do other work is + oftentimes the point of non-blocking programming in the first place. + +## Mapping to Embedded Rust + +- How could this be mapped to Embedded Rust? +- The general model implies an existence of a scheduler / OS. Do we want + a non-blocking ecosystem bound to specific schedulers or operating systems? +- `async` / `await` provides a language-level solution, which can even be + scheduling library independent. + +## Async / Await + +- Async / Await works by transforming you code into pollable state machines. +- From a users perspective, you can write code like this + +```rust +let my_async_uart = (...) +let my_data = &[1, 2, 3, 4]; +let result = my_async_uart.write_all(my_data).await; + +let async_delay = Delay; +async_delay.delay_ms(200).await; +``` + +Note: + +- We assume that the async UART driver was written in a non-blocking way. It would + offload work to the hardware, and then detect the completion condition in an + interrupt. + +## `async` executors + +- This is essentially the scheduler of your system, which polls + all `async` tasks. +- Possible implementation: Executor has task queue with a static size and + always polls all active tasks +- If there is nothing to do, the executor might put the system to sleep. This + is one of the few spots where the executor is not architecture independent. + +Note: + +- Popular executors in the Rust ecosystem: RTICv2, embassy +- Show the architecture specific parts of `embassy`, which put the system to sleep. + +## The `Future` trait + +- A `Future` is an operation which can be polled to completion. + +```rust +pub enum Poll { + Ready(T), + Pending, +} + +pub trait Future { + type Output; + + // Required method + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll; +} +``` + +Note: + +- `Pin`: The compiler-generated `Future` may contain self-referential pointers, so `Pin` is + required for safety guarantees. It ensures that the value a pointer refers will not move in + memory while the computation has not completed yet. +- The `Context` can be used to retrieve a waker object. This can be used to implement + the notification mechanism for completion of a future. + +## Wakers + +- Wakers are the reactors of our `async` system. +- We register them inside the initial poll call. +- We wake them inside the interrupt handler, to notify the executor about the completion of an + operation. + +Note: + +- Commonly used waker inside the embedded ecosystem: [`AtomicWaker`](https://docs.rs/futures/latest/futures/task/struct.AtomicWaker.html) +- Usually, a library will have static instances of that waker, tied to a library provided + interrupt handler. + +## An `async` UART driver + +- We have written an `async` UART driver which can be run with QEMU. An example app using it + can be found [here](https://github.com/ferrous-systems/rust-training/blob/main/example-code/qemu-thumbv7em/src/bin/uart_async.rs) +- The driver can be found [here](https://github.com/ferrous-systems/rust-training/blob/main/example-code/qemu-common/src/cmsdk_uart/asynch.rs) From edfa8aaf02e987765466d5e504e1bed3f4b7db71 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 4 Jun 2026 11:36:25 +0200 Subject: [PATCH 02/10] add draw-io files --- .../images/transaction-non-blocking.drawio | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 training-slides/src/images/transaction-non-blocking.drawio diff --git a/training-slides/src/images/transaction-non-blocking.drawio b/training-slides/src/images/transaction-non-blocking.drawio new file mode 100644 index 00000000..08d96b76 --- /dev/null +++ b/training-slides/src/images/transaction-non-blocking.drawio @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From a90c243da90b384b94a4804dd33a89b64f13a53e Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 4 Jun 2026 13:41:35 +0200 Subject: [PATCH 03/10] some minor renaming --- training-slides/src/SUMMARY.md | 2 +- ...blocking-embedded.md => embedded-async.md} | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) rename training-slides/src/{non-blocking-embedded.md => embedded-async.md} (79%) diff --git a/training-slides/src/SUMMARY.md b/training-slides/src/SUMMARY.md index 2c1c7496..111dd17e 100644 --- a/training-slides/src/SUMMARY.md +++ b/training-slides/src/SUMMARY.md @@ -98,7 +98,7 @@ Topics about using Rust on ARM Cortex-M Microcontrollers (and similar). Requires * [The Embedded HAL and its implementations](./embedded-hals.md) * [Board Support Crates](./board-support.md) * [Using defmt](./defmt.md) -* [Non-blocking programming and async/await](./non-blocking-embedded.md) +* [async/await](./embedded-async.md) ## Under development diff --git a/training-slides/src/non-blocking-embedded.md b/training-slides/src/embedded-async.md similarity index 79% rename from training-slides/src/non-blocking-embedded.md rename to training-slides/src/embedded-async.md index daadceb7..5564973a 100644 --- a/training-slides/src/non-blocking-embedded.md +++ b/training-slides/src/embedded-async.md @@ -83,6 +83,23 @@ Note: - The `Context` can be used to retrieve a waker object. This can be used to implement the notification mechanism for completion of a future. +## Mapping to the `async` keyword + +`async` functions are essentially syntactic sugar. An asynchronous function like this + +```rust +async fn my_async_fn() -> u32; +``` + +desugars into this for the compiler + + +```rust +fn my_async_fn() -> impl Future; +``` + +`await`ing a `async` fn is essentially resolving the future it returns to completion. + ## Wakers - Wakers are the reactors of our `async` system. @@ -101,3 +118,10 @@ Note: - We have written an `async` UART driver which can be run with QEMU. An example app using it can be found [here](https://github.com/ferrous-systems/rust-training/blob/main/example-code/qemu-thumbv7em/src/bin/uart_async.rs) - The driver can be found [here](https://github.com/ferrous-systems/rust-training/blob/main/example-code/qemu-common/src/cmsdk_uart/asynch.rs) + +## Under the hood of `embassy-time` + +- `embassy-time` provides a very convenient API. The API is also hardware independent. How does it work? +- We have written an embassy time driver for the simple ARM CMSDK Timer [here](https://github.com/embassy-rs/embassy/blob/main/embassy-time/src/driver_cmsdk/mod.rs) +- Providing `embassy-time` support essential boils down to mapping a timekeeper and an alarm mechanism + to a hardware timer inside a driver and then creating a global instance of that driver. From b08a9708b4e5a69224624b4277b324eec2d0b93c Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Thu, 4 Jun 2026 13:50:00 +0200 Subject: [PATCH 04/10] some improvements --- training-slides/src/embedded-async.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/training-slides/src/embedded-async.md b/training-slides/src/embedded-async.md index 5564973a..cf979bfb 100644 --- a/training-slides/src/embedded-async.md +++ b/training-slides/src/embedded-async.md @@ -98,7 +98,12 @@ desugars into this for the compiler fn my_async_fn() -> impl Future; ``` -`await`ing a `async` fn is essentially resolving the future it returns to completion. +## The `await` keyword + +- `await`ing a `async` fn is essentially resolving the future it returns to completion. +- Every `await` is a point in code where the execution of the future might be paused and the current + execution context needs to be saved. +- Essentially, `await`s are transition points of the compiler generated state machines. ## Wakers @@ -121,7 +126,8 @@ Note: ## Under the hood of `embassy-time` -- `embassy-time` provides a very convenient API. The API is also hardware independent. How does it work? +- `embassy-time` provides a very convenient API. The high-level API for users is also hardware independent.s + How does this work? - We have written an embassy time driver for the simple ARM CMSDK Timer [here](https://github.com/embassy-rs/embassy/blob/main/embassy-time/src/driver_cmsdk/mod.rs) - Providing `embassy-time` support essential boils down to mapping a timekeeper and an alarm mechanism to a hardware timer inside a driver and then creating a global instance of that driver. From 15d0089b55c7250daf71f6a2b31e2e0d3e5385f2 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 5 Jun 2026 00:43:27 +0200 Subject: [PATCH 05/10] improvements --- training-slides/src/embedded-async.md | 35 ++++++++++++------- .../images/transaction-non-blocking.drawio | 4 +-- .../transaction-non-blocking.drawio.svg | 2 +- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/training-slides/src/embedded-async.md b/training-slides/src/embedded-async.md index cf979bfb..06961888 100644 --- a/training-slides/src/embedded-async.md +++ b/training-slides/src/embedded-async.md @@ -1,7 +1,13 @@ -# Non-blocking programming and async/await +# async/await in Rust ## Non-blocking programming +- General goal: Offload work to the hardware, and use some mechanism to allow + the CPU to do otehr work while the hardware does the work +- Interrupts are used to signal progress or completion of an operation + +## Model of task contexts + In embedded systems, a generic model of how non-blocking programming works often looks like this: @@ -37,6 +43,9 @@ let async_delay = Delay; async_delay.delay_ms(200).await; ``` +- The CPU can do other work while it is waiting for the UART transfer to complete + or the delay to elapse. + Note: - We assume that the async UART driver was written in a non-blocking way. It would @@ -45,12 +54,11 @@ Note: ## `async` executors -- This is essentially the scheduler of your system, which polls - all `async` tasks. -- Possible implementation: Executor has task queue with a static size and - always polls all active tasks -- If there is nothing to do, the executor might put the system to sleep. This - is one of the few spots where the executor is not architecture independent. +- This is the scheduler of your system, which polls all `async` tasks. +- Simplified mental model: Executor manages task queue with a static size and + always polls all active tasks. +- If there is nothing to do, the executor might put the system to sleep to save + power. This is one of the few spots where the executor is not architecture independent. Note: @@ -107,10 +115,11 @@ fn my_async_fn() -> impl Future; ## Wakers -- Wakers are the reactors of our `async` system. -- We register them inside the initial poll call. -- We wake them inside the interrupt handler, to notify the executor about the completion of an - operation. +- Wakers are the primary mechanism used to notify the executor of task completion. +- A waker is registered inside the hardware task is started. +- The task is put to sleep until the waker is called. +- Inside an interrupt handler, the `wake` method on the waker is called to notify the executor + about the completion of an operation. Note: @@ -126,8 +135,8 @@ Note: ## Under the hood of `embassy-time` -- `embassy-time` provides a very convenient API. The high-level API for users is also hardware independent.s - How does this work? +- `embassy-time` provides a very convenient API. The high-level API for users is also (seemingly) + hardware independent. How does this work? - We have written an embassy time driver for the simple ARM CMSDK Timer [here](https://github.com/embassy-rs/embassy/blob/main/embassy-time/src/driver_cmsdk/mod.rs) - Providing `embassy-time` support essential boils down to mapping a timekeeper and an alarm mechanism to a hardware timer inside a driver and then creating a global instance of that driver. diff --git a/training-slides/src/images/transaction-non-blocking.drawio b/training-slides/src/images/transaction-non-blocking.drawio index 08d96b76..81f29fd1 100644 --- a/training-slides/src/images/transaction-non-blocking.drawio +++ b/training-slides/src/images/transaction-non-blocking.drawio @@ -1,6 +1,6 @@ - + @@ -16,7 +16,7 @@ - + diff --git a/training-slides/src/images/transaction-non-blocking.drawio.svg b/training-slides/src/images/transaction-non-blocking.drawio.svg index f4d2a21e..201cd79c 100644 --- a/training-slides/src/images/transaction-non-blocking.drawio.svg +++ b/training-slides/src/images/transaction-non-blocking.drawio.svg @@ -1,4 +1,4 @@ -
Store Pointer and
Size for Interrupt
Store Pointer and...
Write initial data
Write initial data
Yield to OS
Yield to OS
data tranfers
in interrupts
data tranfers...
Interrupt fires
Interrupt fires
Yes
Yes
No
No
All Data
Written?
All Data...
Notify
Task
Notify...

Interrupt Done

Interrupt Don...
Task Context
Task Context
Interrupt Context
Interrupt Context
Write next
data
Write next...
Done
Done
Not Finished
Not Finished
\ No newline at end of file +
Store context for Interrupt
Write initial data
Yield to OS
data tranfers
in interrupts
Interrupt fires
Yes
No
All Data
Written?
Notify
Task

Interrupt Done

Task Context
Interrupt Context
Write next
data
Done
Not Finished
\ No newline at end of file From bd0c5c13bdd9c8fe9d0667037be370e57197a097 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 5 Jun 2026 00:44:42 +0200 Subject: [PATCH 06/10] improvements --- training-slides/src/embedded-async.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/training-slides/src/embedded-async.md b/training-slides/src/embedded-async.md index 06961888..7e3ddfaa 100644 --- a/training-slides/src/embedded-async.md +++ b/training-slides/src/embedded-async.md @@ -138,5 +138,5 @@ Note: - `embassy-time` provides a very convenient API. The high-level API for users is also (seemingly) hardware independent. How does this work? - We have written an embassy time driver for the simple ARM CMSDK Timer [here](https://github.com/embassy-rs/embassy/blob/main/embassy-time/src/driver_cmsdk/mod.rs) -- Providing `embassy-time` support essential boils down to mapping a timekeeper and an alarm mechanism - to a hardware timer inside a driver and then creating a global instance of that driver. +- Providing `embassy-time` support boils down to mapping a timekeeper and an scheduling / alarm + mechanism to a hardware timer inside a driver and then creating a global instance of that driver. From 6628c8307b2317b71fd8786f47652fd90c97c998 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 5 Jun 2026 18:58:26 +0200 Subject: [PATCH 07/10] some more improvements --- training-slides/src/embedded-async.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/training-slides/src/embedded-async.md b/training-slides/src/embedded-async.md index 7e3ddfaa..9ddee9bc 100644 --- a/training-slides/src/embedded-async.md +++ b/training-slides/src/embedded-async.md @@ -1,9 +1,9 @@ -# async/await in Rust +# async/await in Embedded Rust ## Non-blocking programming - General goal: Offload work to the hardware, and use some mechanism to allow - the CPU to do otehr work while the hardware does the work + the CPU to do other work while the hardware does the work - Interrupts are used to signal progress or completion of an operation ## Model of task contexts @@ -93,7 +93,7 @@ Note: ## Mapping to the `async` keyword -`async` functions are essentially syntactic sugar. An asynchronous function like this +`async` functions are syntactic sugar. An asynchronous function like this ```rust async fn my_async_fn() -> u32; @@ -108,7 +108,7 @@ fn my_async_fn() -> impl Future; ## The `await` keyword -- `await`ing a `async` fn is essentially resolving the future it returns to completion. +- `await`ing a `async` fn is resolving the future it returns to completion. - Every `await` is a point in code where the execution of the future might be paused and the current execution context needs to be saved. - Essentially, `await`s are transition points of the compiler generated state machines. @@ -137,6 +137,7 @@ Note: - `embassy-time` provides a very convenient API. The high-level API for users is also (seemingly) hardware independent. How does this work? -- We have written an embassy time driver for the simple ARM CMSDK Timer [here](https://github.com/embassy-rs/embassy/blob/main/embassy-time/src/driver_cmsdk/mod.rs) +- We have written an embassy time driver for the simple ARM CMSDK Timer + [here](https://github.com/embassy-rs/embassy/blob/main/embassy-time/src/driver_cmsdk/mod.rs) - Providing `embassy-time` support boils down to mapping a timekeeper and an scheduling / alarm mechanism to a hardware timer inside a driver and then creating a global instance of that driver. From bf0db8c9c49781a2eed2472b60d33859ae47407b Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Fri, 5 Jun 2026 19:01:59 +0200 Subject: [PATCH 08/10] try to fix CI --- training-slides/src/embedded-async.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/training-slides/src/embedded-async.md b/training-slides/src/embedded-async.md index 9ddee9bc..72762038 100644 --- a/training-slides/src/embedded-async.md +++ b/training-slides/src/embedded-async.md @@ -34,7 +34,8 @@ Note: - Async / Await works by transforming you code into pollable state machines. - From a users perspective, you can write code like this -```rust +```rust,no_run +// Some HAL specific constructor. let my_async_uart = (...) let my_data = &[1, 2, 3, 4]; let result = my_async_uart.write_all(my_data).await; From 9d6fd3f6a61dd98b2620dea35574bb7a276655fb Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 13 Jul 2026 16:45:37 +0200 Subject: [PATCH 09/10] simplify slides --- training-slides/src/embedded-async.md | 63 ++------------------------- 1 file changed, 4 insertions(+), 59 deletions(-) diff --git a/training-slides/src/embedded-async.md b/training-slides/src/embedded-async.md index 72762038..5afafd9c 100644 --- a/training-slides/src/embedded-async.md +++ b/training-slides/src/embedded-async.md @@ -55,68 +55,13 @@ Note: ## `async` executors -- This is the scheduler of your system, which polls all `async` tasks. -- Simplified mental model: Executor manages task queue with a static size and - always polls all active tasks. -- If there is nothing to do, the executor might put the system to sleep to save - power. This is one of the few spots where the executor is not architecture independent. - -Note: - +- Embedded specific executors are usually static - Popular executors in the Rust ecosystem: RTICv2, embassy -- Show the architecture specific parts of `embassy`, which put the system to sleep. - -## The `Future` trait - -- A `Future` is an operation which can be polled to completion. - -```rust -pub enum Poll { - Ready(T), - Pending, -} - -pub trait Future { - type Output; - - // Required method - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll; -} -``` - -Note: - -- `Pin`: The compiler-generated `Future` may contain self-referential pointers, so `Pin` is - required for safety guarantees. It ensures that the value a pointer refers will not move in - memory while the computation has not completed yet. -- The `Context` can be used to retrieve a waker object. This can be used to implement - the notification mechanism for completion of a future. - -## Mapping to the `async` keyword - -`async` functions are syntactic sugar. An asynchronous function like this - -```rust -async fn my_async_fn() -> u32; -``` - -desugars into this for the compiler - - -```rust -fn my_async_fn() -> impl Future; -``` - -## The `await` keyword - -- `await`ing a `async` fn is resolving the future it returns to completion. -- Every `await` is a point in code where the execution of the future might be paused and the current - execution context needs to be saved. -- Essentially, `await`s are transition points of the compiler generated state machines. +- Only architecture specific parts that an executor might have: Putting the system to sleep + when there is nothing to do. -## Wakers +## Wakers in embedded `async` -- Wakers are the primary mechanism used to notify the executor of task completion. - A waker is registered inside the hardware task is started. - The task is put to sleep until the waker is called. - Inside an interrupt handler, the `wake` method on the waker is called to notify the executor From 1377112a09ff0f0d63dac579702ff7e48b069367 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Mon, 13 Jul 2026 16:55:28 +0200 Subject: [PATCH 10/10] fix pseudocode --- training-slides/src/embedded-async.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/training-slides/src/embedded-async.md b/training-slides/src/embedded-async.md index 5afafd9c..05bc22d6 100644 --- a/training-slides/src/embedded-async.md +++ b/training-slides/src/embedded-async.md @@ -34,7 +34,7 @@ Note: - Async / Await works by transforming you code into pollable state machines. - From a users perspective, you can write code like this -```rust,no_run +```rust,ignore // Some HAL specific constructor. let my_async_uart = (...) let my_data = &[1, 2, 3, 4];