Book: Major rework for RTIC v2

This commit is contained in:
Per Lindgren 2023-01-28 21:57:43 +01:00 committed by Henrik Tjäder
parent 2a4218c8ff
commit 8d8a1e5afc
40 changed files with 1267 additions and 251 deletions

View file

@ -4,15 +4,13 @@
- [RTIC by example](./by-example.md)
- [The `app`](./by-example/app.md)
- [Hardware tasks & `pend`](./by-example/hardware_tasks.md)
- [Software tasks & `spawn`](./by-example/software_tasks.md)
- [Resources](./by-example/resources.md)
- [The init task](./by-example/app_init.md)
- [The idle task](./by-example/app_idle.md)
- [Defining tasks](./by-example/app_task.md)
- [Hardware tasks](./by-example/hardware_tasks.md)
- [Software tasks & `spawn`](./by-example/software_tasks.md)
- [Message passing & `capacity`](./by-example/message_passing.md)
- [Task priorities](./by-example/app_priorities.md)
- [Monotonic & `spawn_{at/after}`](./by-example/monotonic.md)
- [Channel based communication](./by-example/channel.md)
- [Tasks with delay](./by-example/delay.md)
- [Starting a new project](./by-example/starting_a_project.md)
- [The minimal app](./by-example/app_minimal.md)
- [Tips & Tricks](./by-example/tips.md)
@ -23,13 +21,13 @@
- [Inspecting generated code](./by-example/tips_view_code.md)
- [Running tasks from RAM](./by-example/tips_from_ram.md)
<!-- - [`#[cfg(..)]` support](./by-example/tips.md) -->
- [RTIC vs. the world](./rtic_vs.md)
- [Awesome RTIC examples](./awesome_rtic.md)
- [Migration Guides](./migration.md)
- [v0.5.x to v1.0.x](./migration/migration_v5.md)
- [v0.4.x to v0.5.x](./migration/migration_v4.md)
- [RTFM to RTIC](./migration/migration_rtic.md)
- [Under the hood](./internals.md)
- [Cortex-M architectures](./internals/targets.md)
<!--- [Interrupt configuration](./internals/interrupt-configuration.md)-->
<!--- [Non-reentrancy](./internals/non-reentrancy.md)-->
<!--- [Access control](./internals/access.md)-->
@ -38,3 +36,10 @@
<!--- [Ceiling analysis](./internals/ceilings.md)-->
<!--- [Software tasks](./internals/tasks.md)-->
<!--- [Timer queue](./internals/timer-queue.md)-->
<!-- - [Defining tasks](./by-example/app_task.md) -->
<!-- - [Software tasks & `spawn`](./by-example/software_tasks.md)
- [Message passing & `capacity`](./by-example/message_passing.md)
- [Task priorities](./by-example/app_priorities.md)
- [Monotonic & `spawn_{at/after}`](./by-example/monotonic.md)
-->

View file

@ -1,14 +1,15 @@
# RTIC by example
This part of the book introduces the Real-Time Interrupt-driven Concurrency (RTIC) framework
to new users by walking them through examples of increasing complexity.
This part of the book introduces the RTIC framework to new users by walking them through examples of increasing complexity.
All examples in this part of the book are accessible at the
[GitHub repository][repoexamples].
The examples are runnable on QEMU (emulating a Cortex M3 target),
thus no special hardware required to follow along.
[repoexamples]: https://github.com/rtic-rs/cortex-m-rtic/tree/master/examples
[repoexamples]: https://github.com/rtic-rs/rtic/tree/master/examples
## Running an example
To run the examples with QEMU you will need the `qemu-system-arm` program.
Check [the embedded Rust book] for instructions on how to set up an
@ -28,11 +29,12 @@ $ cargo run --target thumbv7m-none-eabi --example locals
Yields this output:
``` console
{{#include ../../../ci/expected/locals.run}}
{{#include ../../../rtic/ci/expected/locals.run}}
```
> **NOTE**: You can choose target device by passing a target
> triple to cargo (e.g. `cargo run --example init --target thumbv7m-none-eabi`) or
> configure a default target in `.cargo/config.toml`.
>
> For running the examples, we use a Cortex M3 emulated in QEMU, so the target is `thumbv7m-none-eabi`.
> For running the examples, we (typically) use a Cortex M3 emulated in QEMU, so the target is `thumbv7m-none-eabi`.
> Since the M3 architecture is backwards compatible to the M0/M0+ architecture, you may also use the `thumbv6m-none-eabi`, in case you want to inspect generated assembly code for the M0/M0+ architecture.

View file

@ -2,25 +2,31 @@
## Requirements on the `app` attribute
All RTIC applications use the [`app`] attribute (`#[app(..)]`). This attribute
only applies to a `mod`-item containing the RTIC application. The `app`
attribute has a mandatory `device` argument that takes a *path* as a value.
This must be a full path pointing to a
*peripheral access crate* (PAC) generated using [`svd2rust`] **v0.14.x** or
newer.
All RTIC applications use the [`app`] attribute (`#[app(..)]`). This attribute only applies to a `mod`-item containing the RTIC application. The `app` attribute has a mandatory `device` argument that takes a *path* as a value. This must be a full path pointing to a *peripheral access crate* (PAC) generated using [`svd2rust`] **v0.14.x** or newer.
The `app` attribute will expand into a suitable entry point and thus replaces
the use of the [`cortex_m_rt::entry`] attribute.
The `app` attribute will expand into a suitable entry point and thus replaces the use of the [`cortex_m_rt::entry`] attribute.
[`app`]: ../../../api/cortex_m_rtic_macros/attr.app.html
[`svd2rust`]: https://crates.io/crates/svd2rust
[`cortex_m_rt::entry`]: ../../../api/cortex_m_rt_macros/attr.entry.html
## Structure and zero-cost concurrency
An RTIC `app` is an executable system model for since-core applications, declaring a set of `local` and `shared` resources operated on by a set of `init`, `idle`, *hardware* and *software* tasks. In short the `init` task runs before any other task returning the set of `local` and `shared` resources. Tasks run preemptively based on their associated static priority, `idle` has the lowest priority (and can be used for background work, and/or to put the system to sleep until woken by some event). Hardware tasks are bound to underlying hardware interrupts, while software tasks are scheduled by asynchronous executors (one for each software task priority).
At compile time the task/resource model is analyzed under the Stack Resource Policy (SRP) and executable code generated with the following outstanding properties:
- guaranteed race-free resource access and deadlock-free execution on a single-shared stack
- hardware task scheduling is performed directly by the hardware, and
- software task scheduling is performed by auto generated async executors tailored to the application.
Overall, the generated code infers no additional overhead in comparison to a hand-written implementation, thus in Rust terms RTIC offers a zero-cost abstraction to concurrency.
## An RTIC application example
To give a flavour of RTIC, the following example contains commonly used features.
In the following sections we will go through each feature in detail.
``` rust
{{#include ../../../../examples/common.rs}}
{{#include ../../../../rtic/examples/common.rs}}
```

View file

@ -1,52 +1,47 @@
# The background task `#[idle]`
A function marked with the `idle` attribute can optionally appear in the
module. This becomes the special *idle task* and must have signature
`fn(idle::Context) -> !`.
A function marked with the `idle` attribute can optionally appear in the module. This becomes the special *idle task* and must have signature `fn(idle::Context) -> !`.
When present, the runtime will execute the `idle` task after `init`. Unlike
`init`, `idle` will run *with interrupts enabled* and must never return,
as the `-> !` function signature indicates.
When present, the runtime will execute the `idle` task after `init`. Unlike `init`, `idle` will run *with interrupts enabled* and must never return, as the `-> !` function signature indicates.
[The Rust type `!` means “never”][nevertype].
[nevertype]: https://doc.rust-lang.org/core/primitive.never.html
Like in `init`, locally declared resources will have `'static` lifetimes that
are safe to access.
Like in `init`, locally declared resources will have `'static` lifetimes that are safe to access.
The example below shows that `idle` runs after `init`.
``` rust
{{#include ../../../../examples/idle.rs}}
{{#include ../../../../rtic/examples/idle.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example idle
{{#include ../../../../ci/expected/idle.run}}
{{#include ../../../../rtic/ci/expected/idle.run}}
```
By default, the RTIC `idle` task does not try to optimize for any specific targets.
A common useful optimization is to enable the [SLEEPONEXIT] and allow the MCU
to enter sleep when reaching `idle`.
A common useful optimization is to enable the [SLEEPONEXIT] and allow the MCU to enter sleep when reaching `idle`.
>**Caution** some hardware unless configured disables the debug unit during sleep mode.
>**Caution**: some hardware unless configured disables the debug unit during sleep mode.
>
>Consult your hardware specific documentation as this is outside the scope of RTIC.
The following example shows how to enable sleep by setting the
[`SLEEPONEXIT`][SLEEPONEXIT] and providing a custom `idle` task replacing the
default [`nop()`][NOP] with [`wfi()`][WFI].
[`SLEEPONEXIT`][SLEEPONEXIT] and providing a custom `idle` task replacing the default [`nop()`][NOP] with [`wfi()`][WFI].
[SLEEPONEXIT]: https://developer.arm.com/docs/100737/0100/power-management/sleep-mode/sleep-on-exit-bit
[WFI]: https://developer.arm.com/documentation/dui0662/b/The-Cortex-M0--Instruction-Set/Miscellaneous-instructions/WFI
[NOP]: https://developer.arm.com/documentation/dui0662/b/The-Cortex-M0--Instruction-Set/Miscellaneous-instructions/NOP
``` rust
{{#include ../../../../examples/idle-wfi.rs}}
{{#include ../../../../rtic/examples/idle-wfi.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example idle-wfi
{{#include ../../../../ci/expected/idle-wfi.run}}
{{#include ../../../../rtic/ci/expected/idle-wfi.run}}
```
> **Notice**: The `idle` task cannot be used together with *software* tasks running at priority zero. The reason is that `idle` is running as a non-returning Rust function at priority zero. Thus there would be no way for an executor at priority zero to give control to *software* tasks at the same priority.

View file

@ -1,35 +1,28 @@
# App initialization and the `#[init]` task
An RTIC application requires an `init` task setting up the system. The corresponding `init` function must have the
signature `fn(init::Context) -> (Shared, Local, init::Monotonics)`, where `Shared` and `Local` are resource
signature `fn(init::Context) -> (Shared, Local)`, where `Shared` and `Local` are the resource
structures defined by the user.
The `init` task executes after system reset, [after an optionally defined `pre-init` code section][pre-init] and an always occurring internal RTIC
initialization.
The `init` task executes after system reset (after the optionally defined [pre-init] and internal RTIC
initialization). The `init` task runs *with interrupts disabled* and has exclusive access to Cortex-M (the
`bare_metal::CriticalSection` token is available as `cs`) while device specific peripherals are available through
the `core` and `device` fields of `init::Context`.
[pre-init]: https://docs.rs/cortex-m-rt/latest/cortex_m_rt/attr.pre_init.html
The `init` and optional `pre-init` tasks runs *with interrupts disabled* and have exclusive access to Cortex-M (the
`bare_metal::CriticalSection` token is available as `cs`).
Device specific peripherals are available through the `core` and `device` fields of `init::Context`.
## Example
The example below shows the types of the `core`, `device` and `cs` fields, and showcases the use of a `local`
variable with `'static` lifetime.
Such variables can be delegated from the `init` task to other tasks of the RTIC application.
The example below shows the types of the `core`, `device` and `cs` fields, and showcases the use of a `local` variable with `'static` lifetime. Such variables can be delegated from the `init` task to other tasks of the RTIC application.
The `device` field is only available when the `peripherals` argument is set to the default value `true`.
The `device` field is available when the `peripherals` argument is set to the default value `true`.
In the rare case you want to implement an ultra-slim application you can explicitly set `peripherals` to `false`.
``` rust
{{#include ../../../../examples/init.rs}}
{{#include ../../../../rtic/examples/init.rs}}
```
Running the example will print `init` to the console and then exit the QEMU process.
``` console
$ cargo run --target thumbv7m-none-eabi --example init
{{#include ../../../../ci/expected/init.run}}
{{#include ../../../../rtic/ci/expected/init.run}}
```

View file

@ -3,5 +3,19 @@
This is the smallest possible RTIC application:
``` rust
{{#include ../../../../examples/smallest.rs}}
{{#include ../../../../rtic/examples/smallest.rs}}
```
RTIC is designed with resource efficiency in mind. RTIC itself does not rely on any dynamic memory allocation, thus RAM requirement is dependent only on the application. The flash memory footprint is below 1kB including the interrupt vector table.
For a minimal example you can expect something like:
``` console
$ cargo size --example smallest --target thumbv7m-none-eabi --release
Finished release [optimized] target(s) in 0.07s
text data bss dec hex filename
924 0 0 924 39c smallest
```
<!-- ---
Technically, RTIC will generate a statically allocated future for each *software* task (holding the execution context, including the `Context` struct and stack allocated variables). Futures associated to the same static priority will share an asynchronous stack during execution. -->

View file

@ -4,23 +4,18 @@
The `priority` argument declares the static priority of each `task`.
For Cortex-M, tasks can have priorities in the range `1..=(1 << NVIC_PRIO_BITS)`
where `NVIC_PRIO_BITS` is a constant defined in the `device` crate.
For Cortex-M, tasks can have priorities in the range `1..=(1 << NVIC_PRIO_BITS)` where `NVIC_PRIO_BITS` is a constant defined in the `device` crate.
Omitting the `priority` argument the task priority defaults to `1`.
The `idle` task has a non-configurable static priority of `0`, the lowest priority.
Omitting the `priority` argument the task priority defaults to `1`. The `idle` task has a non-configurable static priority of `0`, the lowest priority.
> A higher number means a higher priority in RTIC, which is the opposite from what
> Cortex-M does in the NVIC peripheral.
> Explicitly, this means that number `10` has a **higher** priority than number `9`.
The highest static priority task takes precedence when more than one
task are ready to execute.
The highest static priority task takes precedence when more than one task are ready to execute.
The following scenario demonstrates task prioritization:
Spawning a higher priority task A during execution of a lower priority task B suspends
task B. Task A has higher priority thus preempting task B which gets suspended
until task A completes execution. Thus, when task A completes task B resumes execution.
Spawning a higher priority task A during execution of a lower priority task B pends task A. Task A has higher priority thus preempting task B which gets suspended until task A completes execution. Thus, when task A completes task B resumes execution.
```text
Task Priority
@ -39,23 +34,17 @@ Task Priority
The following example showcases the priority based scheduling of tasks:
``` rust
{{#include ../../../../examples/preempt.rs}}
{{#include ../../../../rtic/examples/preempt.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example preempt
{{#include ../../../../ci/expected/preempt.run}}
{{#include ../../../../rtic/ci/expected/preempt.run}}
```
Note that the task `bar` does *not* preempt task `baz` because its priority
is the *same* as `baz`'s. The higher priority task `bar` runs before `foo`
when `baz`returns. When `bar` returns `foo` can resume.
Note that the task `bar` does *not* preempt task `baz` because its priority is the *same* as `baz`'s. The higher priority task `bar` runs before `foo` when `baz`returns. When `bar` returns `foo` can resume.
One more note about priorities: choosing a priority higher than what the device
supports will result in a compilation error.
The error is cryptic due to limitations in the Rust language
if `priority = 9` for task `uart0_interrupt` in `example/common.rs` this looks like:
One more note about priorities: choosing a priority higher than what the device supports will result in a compilation error. The error is cryptic due to limitations in the Rust language, if `priority = 9` for task `uart0_interrupt` in `example/common.rs` this looks like:
```text
error[E0080]: evaluation of constant value failed
@ -68,5 +57,4 @@ if `priority = 9` for task `uart0_interrupt` in `example/common.rs` this looks l
```
The error message incorrectly points to the starting point of the macro, but at least the
value subtracted (in this case 9) will suggest which task causes the error.
The error message incorrectly points to the starting point of the macro, but at least the value subtracted (in this case 9) will suggest which task causes the error.

View file

@ -1,21 +1,18 @@
<!-- Should probably be removed -->
# Defining tasks with `#[task]`
Tasks, defined with `#[task]`, are the main mechanism of getting work done in RTIC.
Tasks can
* Be spawned (now or in the future, also by themselves)
* Receive messages (passing messages between tasks)
* Be prioritized, allowing preemptive multitasking
* Be spawned (now or in the future)
* Receive messages (message passing)
* Prioritized allowing preemptive multitasking
* Optionally bind to a hardware interrupt
RTIC makes a distinction between “software tasks” and “hardware tasks”.
RTIC makes a distinction between “software tasks” and “hardware tasks”. Hardware tasks are tasks that are bound to a specific interrupt vector in the MCU while software tasks are not.
*Hardware tasks* are tasks that are bound to a specific interrupt vector in the MCU while software tasks are not.
This means that if a hardware task is bound to, lets say, a UART RX interrupt, the task will be run every
time that interrupt triggers, usually when a character is received.
*Software tasks* are explicitly spawned in a task, either immediately or using the Monotonic timer mechanism.
This means that if a hardware task is bound to an UART RX interrupt the task will run every time this interrupt triggers, usually when a character is received.
In the coming pages we will explore both tasks and the different options available.

View file

@ -0,0 +1,112 @@
# Communication over channels.
Channels can be used to communicate data between running *software* tasks. The channel is essentially a wait queue, allowing tasks with multiple producers and a single receiver. A channel is constructed in the `init` task and backed by statically allocated memory. Send and receive endpoints are distributed to *software* tasks:
```rust
...
const CAPACITY: usize = 5;
#[init]
fn init(_: init::Context) -> (Shared, Local) {
let (s, r) = make_channel!(u32, CAPACITY);
receiver::spawn(r).unwrap();
sender1::spawn(s.clone()).unwrap();
sender2::spawn(s.clone()).unwrap();
...
```
In this case the channel holds data of `u32` type with a capacity of 5 elements.
## Sending data
The `send` method post a message on the channel as shown below:
```rust
#[task]
async fn sender1(_c: sender1::Context, mut sender: Sender<'static, u32, CAPACITY>) {
hprintln!("Sender 1 sending: 1");
sender.send(1).await.unwrap();
}
```
## Receiving data
The receiver can `await` incoming messages:
```rust
#[task]
async fn receiver(_c: receiver::Context, mut receiver: Receiver<'static, u32, CAPACITY>) {
while let Ok(val) = receiver.recv().await {
hprintln!("Receiver got: {}", val);
...
}
}
```
For a complete example:
``` rust
{{#include ../../../../rtic/examples/async-channel.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example async-channel --features test-critical-section
{{#include ../../../../rtic/ci/expected/async-channel.run}}
```
Also sender endpoint can be awaited. In case there the channel capacity has not been reached, `await` the sender can progress immediately, while in the case the capacity is reached, the sender is blocked until there is free space in the queue. In this way data is never lost.
In the below example the `CAPACITY` has been reduced to 1, forcing sender tasks to wait until the data in the channel has been received.
``` rust
{{#include ../../../../rtic/examples/async-channel-done.rs}}
```
Looking at the output, we find that `Sender 2` will wait until the data sent by `Sender 1` as been received.
> **NOTICE** *Software* tasks at the same priority are executed asynchronously to each other, thus **NO** strict order can be assumed. (The presented order here applies only to the current implementation, and may change between RTIC framework releases.)
``` console
$ cargo run --target thumbv7m-none-eabi --example async-channel-done --features test-critical-section
{{#include ../../../../rtic/ci/expected/async-channel-done.run}}
```
## Error handling
In case all senders have been dropped `await` on an empty receiver channel results in an error. This allows to gracefully implement different types of shutdown operations.
``` rust
{{#include ../../../../rtic/examples/async-channel-no-sender.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example async-channel-no-sender --features test-critical-section
{{#include ../../../../rtic/ci/expected/async-channel-no-sender.run}}
```
Similarly, `await` on a send channel results in an error in case the receiver has been dropped. This allows to gracefully implement application level error handling.
The resulting error returns the data back to the sender, allowing the sender to take appropriate action (e.g., storing the data to later retry sending it).
``` rust
{{#include ../../../../rtic/examples/async-channel-no-receiver.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example async-channel-no-receiver --features test-critical-section
{{#include ../../../../rtic/ci/expected/async-channel-no-receiver.run}}
```
## Try API
In cases you wish the sender to proceed even in case the channel is full. To that end, a `try_send` API is provided.
``` rust
{{#include ../../../../rtic/examples/async-channel-try.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example async-channel-try --features test-critical-section
{{#include ../../../../rtic/ci/expected/async-channel-try.run}}
```

View file

@ -0,0 +1,116 @@
# Tasks with delay
A convenient way to express *miniminal* timing requirements is by means of delaying progression.
This can be achieved by instantiating a monotonic timer:
```rust
...
rtic_monotonics::make_systick_timer_queue!(TIMER);
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
let systick = Systick::start(cx.core.SYST, 12_000_000);
TIMER.initialize(systick);
...
```
A *software* task can `await` the delay to expire:
```rust
#[task]
async fn foo(_cx: foo::Context) {
...
TIMER.delay(100.millis()).await;
...
```
Technically, the timer queue is implemented as a list based priority queue, where list-nodes are statically allocated as part of the underlying task `Future`. Thus, the timer queue is infallible at run-time (its size and allocation is determined at compile time).
For a complete example:
``` rust
{{#include ../../../../rtic/examples/async-delay.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example async-delay --features test-critical-section
{{#include ../../../../rtic/ci/expected/async-delay.run}}
```
## Timeout
Rust `Futures` (underlying Rust `async`/`await`) are composable. This makes it possible to `select` in between `Futures` that have completed.
A common use case is transactions with associated timeout. In the examples shown below, we introduce a fake HAL device which performs some transaction. We have modelled the time it takes based on the input parameter (`n`) as `350ms + n * 100ms)`.
Using the `select_biased` macro from the `futures` crate it may look like this:
```rust
// Call hal with short relative timeout using `select_biased`
select_biased! {
v = hal_get(&TIMER, 1).fuse() => hprintln!("hal returned {}", v),
_ = TIMER.delay(200.millis()).fuse() => hprintln!("timeout", ), // this will finish first
}
```
Assuming the `hal_get` will take 450ms to finish, a short timeout of 200ms will expire.
```rust
// Call hal with long relative timeout using `select_biased`
select_biased! {
v = hal_get(&TIMER, 1).fuse() => hprintln!("hal returned {}", v), // hal finish first
_ = TIMER.delay(1000.millis()).fuse() => hprintln!("timeout", ),
}
```
By extending the timeout to 1000ms, the `hal_get` will finish first.
Using `select_biased` any number of futures can be combined, so its very powerful. However, as the timeout pattern is frequently used, it is directly supported by the RTIC [rtc-monotonics] and [rtic-time] crates. The second example from above using `timeout_after`:
```rust
// Call hal with long relative timeout using monotonic `timeout_after`
match TIMER.timeout_after(1000.millis(), hal_get(&TIMER, 1)).await {
Ok(v) => hprintln!("hal returned {}", v),
_ => hprintln!("timeout"),
}
```
In cases you want exact control over time without drift. For this purpose we can use exact points in time using `Instance`, and spans of time using `Duration`. Operations on the `Instance` and `Duration` types are given by the [fugit] crate.
[fugit]: https://crates.io/crates/fugit
```rust
// get the current time instance
let mut instant = TIMER.now();
// do this 3 times
for n in 0..3 {
// exact point in time without drift
instant += 1000.millis();
TIMER.delay_until(instant).await;
// exact point it time for timeout
let timeout = instant + 500.millis();
hprintln!("now is {:?}, timeout at {:?}", TIMER.now(), timeout);
match TIMER.timeout_at(timeout, hal_get(&TIMER, n)).await {
Ok(v) => hprintln!("hal returned {} at time {:?}", v, TIMER.now()),
_ => hprintln!("timeout"),
}
}
```
`instant = TIMER.now()` gives the baseline (i.e., the exact current point in time). We want to call `hal_get` after 1000ms relative to this exact point in time. This can be accomplished by `TIMER.delay_until(instant).await;`. We define the absolute point in time for the `timeout`, and call `TIMER.timeout_at(timeout, hal_get(&TIMER, n)).await`. For the first loop iteration `n == 0`, and the `hal_get` will take 350ms (and finishes before the timeout). For the second iteration `n == 1`, and `hal_get` will take 450ms (and again succeeds to finish before the timeout). For the third iteration `n == 2` (`hal_get` will take 5500ms to finish). In this case we will run into a timeout.
The complete example:
``` rust
{{#include ../../../../rtic/examples/async-timeout.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example async-timeout --features test-critical-section
{{#include ../../../../rtic/ci/expected/async-timeout.run}}
```

View file

@ -1,39 +1,27 @@
# Hardware tasks
At its core RTIC is using a hardware interrupt controller ([ARM NVIC on cortex-m][NVIC])
to schedule and start execution of tasks. All tasks except `pre-init`, `#[init]` and `#[idle]`
run as interrupt handlers.
At its core RTIC is using the hardware interrupt controller ([ARM NVIC on cortex-m][NVIC]) to perform scheduling and executing tasks, and all (*hardware*) tasks except `#[init]` and `#[idle]` run as interrupt handlers. This also means that you can manually bind tasks to interrupt handlers.
Hardware tasks are explicitly bound to interrupt handlers.
To bind an interrupt use the `#[task]` attribute argument `binds = InterruptName`. This task becomes the interrupt handler for this hardware interrupt vector.
To bind a task to an interrupt, use the `#[task]` attribute argument `binds = InterruptName`.
This task then becomes the interrupt handler for this hardware interrupt vector.
All tasks bound to an explicit interrupt are *hardware tasks* since they start execution in reaction to a hardware event (interrupt).
All tasks bound to an explicit interrupt are called *hardware tasks* since they
start execution in reaction to a hardware event.
Specifying a non-existing interrupt name will cause a compilation error. The interrupt names are commonly defined by [PAC or HAL][pacorhal] crates.
Specifying a non-existing interrupt name will cause a compilation error. The interrupt names
are commonly defined by [PAC or HAL][pacorhal] crates.
Any available interrupt vector should work, but different hardware might have added special properties to select interrupt priority levels, such as the [nRF “softdevice”](https://github.com/rtic-rs/cortex-m-rtic/issues/434).
Any available interrupt vector should work. Specific devices may bind
specific interrupt priorities to specific interrupt vectors outside
user code control. See for example the
[nRF “softdevice”](https://github.com/rtic-rs/cortex-m-rtic/issues/434).
Beware of using interrupt vectors that are used internally by hardware features;
RTIC is unaware of such hardware specific details.
Beware of re-purposing interrupt vectors used internally by hardware features, RTIC is unaware of such hardware specific details.
[pacorhal]: https://docs.rust-embedded.org/book/start/registers.html
[NVIC]: https://developer.arm.com/documentation/100166/0001/Nested-Vectored-Interrupt-Controller/NVIC-functional-description/NVIC-interrupts
The example below demonstrates the use of the `#[task(binds = InterruptName)]` attribute to declare a
hardware task bound to an interrupt handler.
The example below demonstrates the use of the `#[task(binds = InterruptName)]` attribute to declare a hardware task bound to an interrupt handler. In the example the interrupt triggering task execution is manually pended (`rtic::pend(Interrupt::UART0)`). However, in the typical case, interrupts are pended by the hardware peripheral. RTIC does not interfere with mechanisms for clearing peripheral interrupts, so any hardware specific implementation is completely up to the implementer.
``` rust
{{#include ../../../../examples/hardware.rs}}
{{#include ../../../../rtic/examples/hardware.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example hardware
{{#include ../../../../ci/expected/hardware.run}}
{{#include ../../../../rtic/ci/expected/hardware.run}}
```

View file

@ -1,176 +1,138 @@
# Resource usage
The RTIC framework manages shared and task local resources allowing persistent data
storage and safe accesses without the use of `unsafe` code.
The RTIC framework manages shared and task local resources allowing persistent data storage and safe accesses without the use of `unsafe` code.
RTIC resources are visible only to functions declared within the `#[app]` module and the framework
gives the user complete control (on a per-task basis) over resource accessibility.
RTIC resources are visible only to functions declared within the `#[app]` module and the framework gives the user complete control (on a per-task basis) over resource accessibility.
Declaration of system-wide resources is done by annotating **two** `struct`s within the `#[app]` module
with the attribute `#[local]` and `#[shared]`.
Each field in these structures corresponds to a different resource (identified by field name).
The difference between these two sets of resources will be covered below.
Declaration of system-wide resources is done by annotating **two** `struct`s within the `#[app]` module with the attribute `#[local]` and `#[shared]`. Each field in these structures corresponds to a different resource (identified by field name). The difference between these two sets of resources will be covered below.
Each task must declare the resources it intends to access in its corresponding metadata attribute
using the `local` and `shared` arguments. Each argument takes a list of resource identifiers.
The listed resources are made available to the context under the `local` and `shared` fields of the
`Context` structure.
Each task must declare the resources it intends to access in its corresponding metadata attribute using the `local` and `shared` arguments. Each argument takes a list of resource identifiers. The listed resources are made available to the context under the `local` and `shared` fields of the `Context` structure.
The `init` task returns the initial values for the system-wide (`#[shared]` and `#[local]`)
resources, and the set of initialized timers used by the application. The monotonic timers will be
further discussed in [Monotonic & `spawn_{at/after}`](./monotonic.md).
The `init` task returns the initial values for the system-wide (`#[shared]` and `#[local]`) resources.
<!-- and the set of initialized timers used by the application. The monotonic timers will be
further discussed in [Monotonic & `spawn_{at/after}`](./monotonic.md). -->
## `#[local]` resources
`#[local]` resources are locally accessible to a specific task, meaning that only that task can
access the resource and does so without locks or critical sections. This allows for the resources,
commonly drivers or large objects, to be initialized in `#[init]` and then be passed to a specific
task.
`#[local]` resources accessible only to a single task. This task is given unique access to the resource without the use of locks or critical sections.
Thus, a task `#[local]` resource can only be accessed by one singular task.
Attempting to assign the same `#[local]` resource to more than one task is a compile-time error.
This allows for the resources, commonly drivers or large objects, to be initialized in `#[init]` and then be passed to a specific task. (Thus, a task `#[local]` resource can only be accessed by one single task.) Attempting to assign the same `#[local]` resource to more than one task is a compile-time error.
Types of `#[local]` resources must implement a [`Send`] trait as they are being sent from `init`
to a target task, crossing a thread boundary.
Types of `#[local]` resources must implement [`Send`] trait as they are being sent from `init` to the target task and thus crossing the *thread* boundary.
[`Send`]: https://doc.rust-lang.org/stable/core/marker/trait.Send.html
The example application shown below contains two tasks where each task has access to its own
`#[local]` resource; the `idle` task has its own `#[local]` as well.
The example application shown below contains three tasks `foo`, `bar` and `idle`, each having access to its own `#[local]` resource.
``` rust
{{#include ../../../../examples/locals.rs}}
{{#include ../../../../rtic/examples/locals.rs}}
```
Running the example:
``` console
$ cargo run --target thumbv7m-none-eabi --example locals
{{#include ../../../../ci/expected/locals.run}}
{{#include ../../../../rtic/ci/expected/locals.run}}
```
Local resources in `#[init]` and `#[idle]` have `'static`
lifetimes. This is safe since both tasks are not re-entrant.
### Task local initialized resources
Local resources can also be specified directly in the resource claim like so:
`#[task(local = [my_var: TYPE = INITIAL_VALUE, ...])]`; this allows for creating locals which do no need to be
initialized in `#[init]`.
A special use-case of local resources are the ones specified directly in the task declaration, `#[task(local = [my_var: TYPE = INITIAL_VALUE, ...])]`. This allows for creating locals which do no need to be initialized in `#[init]`. Moreover, local resources in `#[init]` and `#[idle]` have `'static` lifetimes, this is safe since both are not re-entrant.
Types of `#[task(local = [..])]` resources have to be neither [`Send`] nor [`Sync`] as they
are not crossing any thread boundary.
Types of `#[task(local = [..])]` resources have to be neither [`Send`] nor [`Sync`] as they are not crossing any thread boundary.
[`Sync`]: https://doc.rust-lang.org/stable/core/marker/trait.Sync.html
In the example below the different uses and lifetimes are shown:
``` rust
{{#include ../../../../examples/declared_locals.rs}}
{{#include ../../../../rtic/examples/declared_locals.rs}}
```
<!-- ``` console
$ cargo run --target thumbv7m-none-eabi --example declared_locals
{{#include ../../../../ci/expected/declared_locals.run}}
``` -->
You can run the application, but as the example is designed merely to showcase the lifetime properties there is no output (it suffices to build the application).
``` console
$ cargo build --target thumbv7m-none-eabi --example declared_locals
```
<!-- {{#include ../../../../rtic/ci/expected/declared_locals.run}} -->
## `#[shared]` resources and `lock`
Critical sections are required to access `#[shared]` resources in a data race-free manner and to
achieve this the `shared` field of the passed `Context` implements the [`Mutex`] trait for each
shared resource accessible to the task. This trait has only one method, [`lock`], which runs its
closure argument in a critical section.
Critical sections are required to access `#[shared]` resources in a data race-free manner and to achieve this the `shared` field of the passed `Context` implements the [`Mutex`] trait for each shared resource accessible to the task. This trait has only one method, [`lock`], which runs its closure argument in a critical section.
[`Mutex`]: ../../../api/rtic/trait.Mutex.html
[`lock`]: ../../../api/rtic/trait.Mutex.html#method.lock
The critical section created by the `lock` API is based on dynamic priorities: it temporarily
raises the dynamic priority of the context to a *ceiling* priority that prevents other tasks from
preempting the critical section. This synchronization protocol is known as the
[Immediate Ceiling Priority Protocol (ICPP)][icpp], and complies with
[Stack Resource Policy (SRP)][srp] based scheduling of RTIC.
The critical section created by the `lock` API is based on dynamic priorities: it temporarily raises the dynamic priority of the context to a *ceiling* priority that prevents other tasks from preempting the critical section. This synchronization protocol is known as the [Immediate Ceiling Priority Protocol (ICPP)][icpp], and complies with [Stack Resource Policy (SRP)][srp] based scheduling of RTIC.
[icpp]: https://en.wikipedia.org/wiki/Priority_ceiling_protocol
[srp]: https://en.wikipedia.org/wiki/Stack_Resource_Policy
In the example below we have three interrupt handlers with priorities ranging from one to three.
The two handlers with the lower priorities contend for a `shared` resource and need to succeed in locking the
resource in order to access its data. The highest priority handler, which does not access the `shared`
resource, is free to preempt a critical section created by the lowest priority handler.
In the example below we have three interrupt handlers with priorities ranging from one to three. The two handlers with the lower priorities contend for the `shared` resource and need to lock the resource for accessing the data. The highest priority handler, which do not access the `shared` resource, is free to preempt the critical section created by the lowest priority handler.
``` rust
{{#include ../../../../examples/lock.rs}}
{{#include ../../../../rtic/examples/lock.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example lock
{{#include ../../../../ci/expected/lock.run}}
{{#include ../../../../rtic/ci/expected/lock.run}}
```
Types of `#[shared]` resources have to be [`Send`].
## Multi-lock
As an extension to `lock`, and to reduce rightward drift, locks can be taken as tuples. The
following examples show this in use:
As an extension to `lock`, and to reduce rightward drift, locks can be taken as tuples. The following examples show this in use:
``` rust
{{#include ../../../../examples/multilock.rs}}
{{#include ../../../../rtic/examples/multilock.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example multilock
{{#include ../../../../ci/expected/multilock.run}}
{{#include ../../../../rtic/ci/expected/multilock.run}}
```
## Only shared (`&-`) access
By default, the framework assumes that all tasks require exclusive access (`&mut-`) to resources,
but it is possible to specify that a task only requires shared access (`&-`) to a resource using the
`&resource_name` syntax in the `shared` list.
By default, the framework assumes that all tasks require exclusive mutable access (`&mut-`) to resources, but it is possible to specify that a task only requires shared access (`&-`) to a resource using the `&resource_name` syntax in the `shared` list.
The advantage of specifying shared access (`&-`) to a resource is that no locks are required to
access the resource even if the resource is contended by more than one task running at different
priorities. The downside is that the task only gets a shared reference (`&-`) to the resource,
limiting the operations it can perform on it, but where a shared reference is enough this approach
reduces the number of required locks. In addition to simple immutable data, this shared access can
be useful where the resource type safely implements interior mutability, with appropriate locking
or atomic operations of its own.
The advantage of specifying shared access (`&-`) to a resource is that no locks are required to access the resource even if the resource is contended by more than one task running at different priorities. The downside is that the task only gets a shared reference (`&-`) to the resource, limiting the operations it can perform on it, but where a shared reference is enough this approach reduces the number of required locks. In addition to simple immutable data, this shared access can be useful where the resource type safely implements interior mutability, with appropriate locking or atomic operations of its own.
Note that in this release of RTIC it is not possible to request both exclusive access (`&mut-`)
and shared access (`&-`) to the *same* resource from different tasks. Attempting to do so will
result in a compile error.
Note that in this release of RTIC it is not possible to request both exclusive access (`&mut-`) and shared access (`&-`) to the *same* resource from different tasks. Attempting to do so will result in a compile error.
In the example below a key (e.g. a cryptographic key) is loaded (or created) at runtime and then
used from two tasks that run at different priorities without any kind of lock.
In the example below a key (e.g. a cryptographic key) is loaded (or created) at runtime (returned by `init`) and then used from two tasks that run at different priorities without any kind of lock.
``` rust
{{#include ../../../../examples/only-shared-access.rs}}
{{#include ../../../../rtic/examples/only-shared-access.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example only-shared-access
{{#include ../../../../ci/expected/only-shared-access.run}}
{{#include ../../../../rtic/ci/expected/only-shared-access.run}}
```
## Lock-free resource access of shared resources
## Lock-free access of shared resources
A critical section is *not* required to access a `#[shared]` resource that's only accessed by tasks
running at the *same* priority. In this case, you can opt out of the `lock` API by adding the
`#[lock_free]` field-level attribute to the resource declaration (see example below). Note that
this is merely a convenience to reduce needless resource locking code, because even if the
A critical section is *not* required to access a `#[shared]` resource that's only accessed by tasks running at the *same* priority. In this case, you can opt out of the `lock` API by adding the `#[lock_free]` field-level attribute to the resource declaration (see example below).
<!-- Note that this is merely a convenience to reduce needless resource locking code, because even if the
`lock` API is used, at runtime the framework will **not** produce a critical section due to how
the underlying resource-ceiling preemption works.
the underlying resource-ceiling preemption works. -->
Also worth noting: using `#[lock_free]` on resources shared by
tasks running at different priorities will result in a *compile-time* error -- not using the `lock`
API would be a data race in that case.
To adhere to the Rust [aliasing] rule, a resource may be either accessed through multiple immutable references or a singe mutable reference (but not both at the same time).
[aliasing]: https://doc.rust-lang.org/nomicon/aliasing.html
Using `#[lock_free]` on resources shared by tasks running at different priorities will result in a *compile-time* error -- not using the `lock` API would violate the aforementioned alias rule. Similarly, for each priority there can be only a single *software* task accessing a shared resource (as an `async` task may yield execution to other *software* or *hardware* tasks running at the same priority). However, under this single-task restriction, we make the observation that the resource is in effect no longer `shared` but rather `local`. Thus, using a `#[lock_free]` shared resource will result in a *compile-time* error -- where applicable, use a `#[local]` resource instead.
``` rust
{{#include ../../../../examples/lock-free.rs}}
{{#include ../../../../rtic/examples/lock-free.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example lock-free
{{#include ../../../../ci/expected/lock-free.run}}
{{#include ../../../../rtic/ci/expected/lock-free.run}}
```

View file

@ -1,47 +1,99 @@
# Software tasks & spawn
The RTIC concept of a software task shares a lot with that of [hardware tasks](./hardware_tasks.md)
with the core difference that a software task is not explicitly bound to a specific
interrupt vector, but rather bound to a “dispatcher” interrupt vector running
at the intended priority of the software task (see below).
The RTIC concept of a *software* task shares a lot with that of [hardware tasks](./hardware_tasks.md) with the core difference that a software task is not explicitly bound to a specific
interrupt vector, but rather to a “dispatcher” interrupt vector running at the same priority as the software task.
Thus, software tasks are tasks which are not *directly* bound to an interrupt vector.
Similarly to *hardware* tasks, the `#[task]` attribute used on a function declare it as a task. The absence of a `binds = InterruptName` argument to the attribute declares the function as a *software task*.
The `#[task]` attributes used on a function determine if it is
software tasks, specifically the absence of a `binds = InterruptName`
argument to the attribute definition.
The static method `task_name::spawn()` spawns (starts) a software task and given that there are no higher priority tasks running the task will start executing directly.
The static method `task_name::spawn()` spawns (schedules) a software
task by registering it with a specific dispatcher. If there are no
higher priority tasks available to the scheduler (which serves a set
of dispatchers), the task will start executing directly.
The *software* task itself is given as an `async` Rust function, which allows the user to optionally `await` future events. This allows to blend reactive programming (by means of *hardware* tasks) with sequential programming (by means of *software* tasks).
All software tasks at the same priority level share an interrupt handler bound to their dispatcher.
What differentiates software and hardware tasks is the usage of either a dispatcher or a bound interrupt vector.
Whereas, *hardware* tasks are assumed to run-to-completion (and return), *software* tasks may be started (`spawned`) once and run forever, with the side condition that any loop (execution path) is broken by at least one `await` (yielding operation).
The interrupt vectors used as dispatchers cannot be used by hardware tasks.
All *software* tasks at the same priority level shares an interrupt handler acting as an async executor dispatching the software tasks.
Availability of a set of “free” (not in use by hardware tasks) and usable interrupt vectors allows the framework
to dispatch software tasks via dedicated interrupt handlers.
This list of dispatchers, `dispatchers = [FreeInterrupt1, FreeInterrupt2, ...]` is an argument to the `#[app]` attribute, where you define the set of free and usable interrupts.
This set of dispatchers, `dispatchers = [FreeInterrupt1, FreeInterrupt2, ...]` is an
argument to the `#[app]` attribute.
Each interrupt vector acting as dispatcher gets assigned to one priority level meaning that the list of dispatchers need to cover all priority levels used by software tasks.
Each interrupt vector acting as dispatcher gets assigned to a unique priority level meaning that
the list of dispatchers needs to cover all priority levels used by software tasks.
Example: The `dispatchers =` argument needs to have at least 3 entries for an application using three different priorities for software tasks.
Example: The `dispatchers =` argument needs to have at least 3 entries for an application using
three different priorities for software tasks.
The framework will give a compilation error if there are not enough dispatchers provided.
The framework will give a compilation error if there are not enough dispatchers provided, or if a clash occurs between the list of dispatchers and interrupts bound to *hardware* tasks.
See the following example:
``` rust
{{#include ../../../../examples/spawn.rs}}
{{#include ../../../../rtic/examples/spawn.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example spawn
{{#include ../../../../ci/expected/spawn.run}}
{{#include ../../../../rtic/ci/expected/spawn.run}}
```
You may `spawn` a *software* task again, given that it has run-to-completion (returned).
In the below example, we `spawn` the *software* task `foo` from the `idle` task. Since the default priority of the *software* task is 1 (higher than `idle`), the dispatcher will execute `foo` (preempting `idle`). Since `foo` runs-to-completion. It is ok to `spawn` the `foo` task again.
Technically the async executor will `poll` the `foo` *future* which in this case leaves the *future* in a *completed* state.
``` rust
{{#include ../../../../rtic/examples/spawn_loop.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example spawn_loop
{{#include ../../../../rtic/ci/expected/spawn_loop.run}}
```
An attempt to `spawn` an already spawned task (running) task will result in an error. Notice, the that the error is reported before the `foo` task is actually run. This is since, the actual execution of the *software* task is handled by the dispatcher interrupt (`SSIO`), which is not enabled until we exit the `init` task. (Remember, `init` runs in a critical section, i.e. all interrupts being disabled.)
Technically, a `spawn` to a *future* that is not in *completed* state is considered an error.
``` rust
{{#include ../../../../rtic/examples/spawn_err.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example spawn_err
{{#include ../../../../rtic/ci/expected/spawn_err.run}}
```
## Passing arguments
You can also pass arguments at spawn as follows.
``` rust
{{#include ../../../../rtic/examples/spawn_arguments.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example spawn_arguments
{{#include ../../../../rtic/ci/expected/spawn_arguments.run}}
```
## Priority zero tasks
In RTIC tasks run preemptively to each other, with priority zero (0) the lowest priority. You can use priority zero tasks for background work, without any strict real-time requirements.
Conceptually, one can see such tasks as running in the `main` thread of the application, thus the resources associated are not required the [Send] bound.
[Send]: https://doc.rust-lang.org/nomicon/send-and-sync.html
``` rust
{{#include ../../../../rtic/examples/zero-prio-task.rs}}
```
``` console
$ cargo run --target thumbv7m-none-eabi --example zero-prio-task
{{#include ../../../../rtic/ci/expected/zero-prio-task.run}}
```
> **Notice**: *software* task at zero priority cannot co-exist with the [idle] task. The reason is that `idle` is running as a non-returning Rust function at priority zero. Thus there would be no way for an executor at priority zero to give control to *software* tasks at the same priority.
---
Application side safety: Technically, the RTIC framework ensures that `poll` is never executed on any *software* task with *completed* future, thus adhering to the soundness rules of async Rust.

View file

@ -10,6 +10,8 @@ If you are targeting ARMv6-M or ARMv8-M-base architecture, check out the section
This will give you an RTIC application with support for RTT logging with [`defmt`] and stack overflow
protection using [`flip-link`]. There is also a multitude of examples provided by the community:
For inspiration you may look at the below resources. For now they cover RTIC 1.0.x, but will be updated with RTIC 2.0.x examples over time.
- [`rtic-examples`] - Multiple projects
- [https://github.com/kalkyl/f411-rtic](https://github.com/kalkyl/f411-rtic)
- ... More to come

View file

@ -1,7 +1,7 @@
<div align="center"><img width="300" height="300" src="RTIC.svg"></div>
<div style="font-size: 6em; font-weight: bolder;" align="center">RTIC</div>
<h1 align="center">Real-Time Interrupt-driven Concurrency</h1>
<h1 align="center">The Embedded Rust RTOS</h1>
<p align="center">A concurrency framework for building real-time systems</p>
@ -10,29 +10,160 @@
This book contains user level documentation for the Real-Time Interrupt-driven Concurrency
(RTIC) framework. The API reference is available [here](../../api/).
Formerly known as Real-Time For the Masses.
<!-- Formerly known as Real-Time For the Masses. -->
<!--There is a translation of this book in [Russian].-->
<!--[Russian]: ../ru/index.html-->
This is the documentation of v1.0.x of RTIC; for the documentation of version
This is the documentation of v2.0.x (pre-release) of RTIC 2.
## RTIC - The Past, current and Future
This section gives a background to the RTIC model. Feel free to skip to section [RTIC the model](preface.md#rtic-the-model) for a TL;DR.
The RTIC framework takes the outset from real-time systems research at Luleå University of Technology (LTU) Sweden. RTIC is inspired by the concurrency model of the [Timber] language, the [RTFM-SRP] based scheduler, the [RTFM-core] language and [Abstract Timer] implementation. For a full list of related research see [TODO].
[Timber]: https://timber-lang.org/
[RTFM-SRP]: https://www.diva-portal.org/smash/get/diva2:1005680/FULLTEXT01.pdf
[RTFM-core]: https://ltu.diva-portal.org/smash/get/diva2:1013248/FULLTEXT01.pdf
[AbstractTimer]: https://ltu.diva-portal.org/smash/get/diva2:1013030/FULLTEXT01.pdf
## Stack Resource Policy based Scheduling
Stack Resource Policy (SRP) based concurrency and resource management is at heart of the RTIC framework. The [SRP] model itself extends on [Priority Inheritance Protocols], and provides a set of outstanding properties for single core scheduling. To name a few:
- preemptive deadlock and race-free scheduling
- resource efficiency
- tasks execute on a single shared stack
- tasks run-to-completion with wait free access to shared resources
- predictable scheduling, with bounded priority inversion by a single (named) critical section
- theoretical underpinning amenable to static analysis (e.g., for task response times and overall schedulability)
SRP comes with a set of system wide requirements:
- each task is associated a static priority,
- tasks execute on a single-core,
- tasks must be run-to-completion, and
- resources must be claimed/locked in LIFO order.
[SRP]: https://link.springer.com/article/10.1007/BF00365393
[Priority Inheritance Protocols]: https://ieeexplore.ieee.org/document/57058
## SRP analysis
SRP based scheduling requires the set of static priority tasks and their access to shared resources to be known in order to compute a static *ceiling* (𝝅) for each resource. The static resource *ceiling* 𝝅(r) reflects the maximum static priority of any task that accesses the resource `r`.
### Example
Assume two tasks `A` (with priority `p(A) = 2`) and `B` (with priority `p(B) = 4`) both accessing the shared resource `R`. The static ceiling of `R` is 4 (computed from `𝝅(R) = max(p(A) = 2, p(B) = 4) = 4`).
A graph representation of the example:
```mermaid
graph LR
A["p(A) = 2"] --> R
B["p(A) = 4"] --> R
R["𝝅(R) = 4"]
```
## RTIC the hardware accelerated real-time scheduler
SRP itself is compatible both to dynamic and static priority scheduling. For the implementation of RTIC we leverage on the underlying hardware for accelerated static priority scheduling.
In the case of the `ARM Cortex-M` architecture, each interrupt vector entry `v[i]` is associated a function pointer (`v[i].fn`), and a static priority (`v[i].priority`), an enabled- (`v[i].enabled`) and a pending-bit (`v[i].pending`).
An interrupt `i` is scheduled (run) by the hardware under the conditions:
1. is `pended` and `enabled` and has a priority higher than the (optional `BASEPRI`) register, and
1. has the highest priority among interrupts meeting 1.
The first condition (1) can be seen a filter allowing RTIC to take control over which tasks should be allowed to start (and which should be prevented from starting).
The SPR model for single-core static scheduling on the other hand states that a task should be scheduled (run) under the conditions:
1. it is `requested` to run and has a static priority higher than the current system ceiling (𝜫)
1. it has the highest static priority among tasks meeting 1.
The similarities are striking and it is not by chance/luck/coincidence. The hardware was cleverly designed with real-time scheduling in mind.
In order to map the SRP scheduling onto the hardware we need to have a closer look on the system ceiling (𝜫). Under SRP 𝜫 is computed as the maximum priority ceiling of the currently held resources, and will thus change dynamically during the system operation.
## Example
Assume the task model above. Starting from an idle system, 𝜫 is 0, (no task is holding any resource). Assume that `A` is requested for execution, it will immediately be scheduled. Assume that `A` claims (locks) the resource `R`. During the claim (lock of `R`) any request `B` will be blocked from starting (by 𝜫 = `max(𝝅(R) = 4) = 4`, `p(B) = 4`, thus SRP scheduling condition 1 is not met).
## Mapping
The mapping of static priority SRP based scheduling to the Cortex M hardware is straightforward:
- each task `t` are mapped to an interrupt vector index `i` with a corresponding function `v[i].fn = t` and given the static priority `v[i].priority = p(t)`.
- the current system ceiling is mapped to the `BASEPRI` register or implemented through masking the interrupt enable bits accordingly.
## Example
For the running example, a snapshot of the ARM Cortex M [NVIC] may have the following configuration (after task `A` has been pended for execution.)
| Index | Fn | Priority | Enabled | Pended |
| ----- | --- | -------- | ------- | ------ |
| 0 | A | 2 | true | true |
| 1 | B | 4 | true | false |
[NVIC]: https://developer.arm.com/documentation/ddi0337/h/nested-vectored-interrupt-controller/about-the-nvic
(As discussed later, the assignment of interrupt and exception vectors is up to the user.)
A claim (lock(r)) will change the current system ceiling (𝜫) and can be implemented as a *named* critical section:
- old_ceiling = 𝜫, 𝜫 = 𝝅(r)
- execute code within critical section
- old_ceiling = 𝜫
This amounts to a resource protection mechanism requiring only two machine instructions on enter and one on exit the critical section for managing the `BASEPRI` register. For architectures lacking `BASEPRI`, we can implement the system ceiling through a set of machine instructions for disabling/enabling interrupts on entry/exit for the named critical section. The number of machine instructions vary depending on the number of mask registers that needs to be updated (a single machine operation can operate on up to 32 interrupts, so for the M0/M0+ architecture a single instruction suffice). RTIC will determine the ceiling values and masking constants at compile time, thus all operations is in Rust terms zero-cost.
In this way RTIC fuses SRP based preemptive scheduling with a zero-cost hardware accelerated implementation, resulting in "best in class" guarantees and performance.
Given that the approach is dead simple, how come SRP and hardware accelerated scheduling is not adopted by any other mainstream RTOS?
The answer is simple, the commonly adopted threading model does not lend itself well to static analysis - there is no known way to extract the task/resource dependencies from the source code at compile time (thus ceilings cannot be efficiently computed and the LIFO resource locking requirement cannot be ensured). Thus SRP based scheduling is in the general case out of reach for any thread based RTOS.
## RTIC into the Future
Asynchronous programming in various forms are getting increased popularity and language support. Rust natively provides an `async`/`await` API for cooperative multitasking and the compiler generates the necessary boilerplate for storing and retrieving execution contexts (i.e., managing the set of local variables that spans each `await`).
The Rust standard library provides collections for dynamically allocated data-structures (useful to manage execution contexts at run-time. However, in the setting of resource constrained real-time systems, dynamic allocations are problematic (both regarding performance and reliability - Rust runs into a *panic* on an out-of-memory condition). Thus, static allocation is king!
RTIC provides a mechanism for `async`/`await` that relies solely on static allocations. However, the implementation relies on the `#![feature(type_alias_impl_trait)]` (TAIT) which is undergoing stabilization (thus RTIC 2.0.x currently requires a *nightly* toolchain). Technically, using TAIT, the compiler determines the size of each execution context allowing static allocation.
From a modelling perspective `async/await` lifts the run-to-completion requirement of SRP, and each section of code between two yield points (`await`s) can be seen as an individual task. The compiler will reject any attempt to `await` while holding a resource (not doing so would break the strict LIFO requirement on resource usage under SRP).
So with the technical stuff out of the way, what does `async/await` bring to the RTIC table?
The answer is - improved ergonomics! In cases you want a task to perform a sequence of requests (and await their results in order to progress). Without `async`/`await` the programmer would be forced to split the task into individual sub-tasks and maintain some sort of state encoding (and manually progress by selecting sub-task). Using `async/await` each yield point (`await`) essentially represents a state, and the progression mechanism is built automatically for you at compile time by means of `Futures`.
Rust `async`/`await` support is still incomplete and/or under development (e.g., there are no stable way to express `async` closures, precluding use in iterator patterns). Nevertheless, Rust `async`/`await` is production ready and covers most common use cases.
An important property is that futures are composable, thus you can await either, all, or any combination of possible futures (allowing e.g., timeouts and/or asynchronous errors to be promptly handled). For more details and examples see Section [todo].
## RTIC the model
An RTIC `app` is a declarative and executable system model for single-core applications, defining a set of (`local` and `shared`) resources operated on by a set of (`init`, `idle`, *hardware* and *software*) tasks. In short the `init` task runs before any other task returning a set of resources (`local` and `shared`). Tasks run preemptively based on their associated static priority, `idle` has the lowest priority (and can be used for background work, and/or to put the system to sleep until woken by some event). Hardware tasks are bound to underlying hardware interrupts, while software tasks are scheduled by asynchronous executors (one for each software task priority).
At compile time the task/resource model is analyzed under SRP and executable code generated with the following outstanding properties:
- guaranteed race-free resource access and deadlock-free execution on a single-shared stack (thanks to SRP)
- hardware task scheduling is performed directly by the hardware, and
- software task scheduling is performed by auto generated async executors tailored to the application.
The RTIC API design ensures that both SRP requirements and Rust soundness rules are upheld at all times, thus the executable model is correct by construction. Overall, the generated code infers no additional overhead in comparison to a hand-written implementation, thus in Rust terms RTIC offers a zero-cost abstraction to concurrency.
<!--
For the documentation older versions, see;
* v1.0.x go [here](/1.0).
* v0.5.x go [here](/0.5).
* v0.4.x go [here](/0.4).
## Is RTIC an RTOS?
A common question is whether RTIC is an RTOS or not, and depending on your background the
answer may vary. From RTIC's developers point of view; RTIC is a hardware accelerated
RTOS that utilizes the NVIC in Cortex-M MCUs to perform scheduling, rather than the more
classical software kernel.
Another common view from the community is that RTIC is a concurrency framework as there
is no software kernel and that it relies on external HALs.
---
* v0.4.x go [here](/0.4). -->
<!--
{{#include ../../../README.md:7:47}}
{{#include ../../../README.md:48:}}
-->

31
book/en/src/rtic_vs.md Normal file
View file

@ -0,0 +1,31 @@
# RTIC vs. the world
RTIC aims to provide the lowest level of abstraction needed for developing robust and reliable embedded software.
It provides a minimal set of required mechanisms for safe sharing of mutable resources among interrupts and asynchronously executing tasks. The scheduling primitives leverages on the underlying hardware for unparalleled performance and predictability, in effect RTIC provides in Rust terms a zero-cost abstraction to concurrent real-time programming.
## Comparison regarding safety and security
Comparing RTIC to traditional a Real-Time Operating System (RTOS) is hard. Firstly, a traditional RTOS typically comes with no guarantees regarding system safety, even the most hardened kernels like the formally verified [seL4] kernel. Their claims to integrity, confidentiality, and availability regards only the kernel itself (under additional assumptions its configuration and environment). They even state:
"An OS kernel, verified or not, does not automatically make a system secure. In fact, any system, no matter how secure, can be used in insecure ways."
[seL4]: https://sel4.systems/
### Security by design
In the world of information security we commonly find:
- confidentiality, protecting the information from being exposed to an unauthorized party,
- integrity, referring to accuracy and completeness of data, and
- availability, referring to data being accessible to authorized users.
Obviously, a traditional OS can guarantee neither confidentiality nor integrity, as both requires the security critical code to be trusted. Regarding availability, this typically boils down to the usage of system resources. Any OS that allows for dynamic allocation of resources, relies on that the application correctly handles allocations/de-allocations, and cases of allocation failures.
Thus their claim is correct, security is completely out of hands for the OS, the best we can hope for is that it does not add further vulnerabilities.
RTIC on the other hand holds your back. The declarative system wide model gives you a static set of tasks and resources, with precise control over what data is shared and between which parties. Moreover, Rust as a programming language comes with strong properties regarding integrity (compile time aliasing, mutability and lifetime guarantees, together with ensured data validity).
Using RTIC these properties propagate to the system wide model, without interference of other applications running. The RTIC kernel is internally infallible without any need of dynamically allocated data.

View file

@ -10,7 +10,19 @@ categories = ["concurrency", "embedded", "no-std", "asynchronous"]
description = "Real-Time Interrupt-driven Concurrency (RTIC): a concurrency framework for building real-time systems"
documentation = "https://rtic.rs/"
edition = "2021"
keywords = ["arm", "cortex-m", "risc-v", "embedded", "async", "runtime", "futures", "await", "no-std", "rtos", "bare-metal"]
keywords = [
"arm",
"cortex-m",
"risc-v",
"embedded",
"async",
"runtime",
"futures",
"await",
"no-std",
"rtos",
"bare-metal",
]
license = "MIT OR Apache-2.0"
name = "rtic"
readme = "README.md"
@ -31,6 +43,7 @@ bare-metal = "1.0.0"
#portable-atomic = { version = "0.3.19" }
atomic-polyfill = "1"
[build-dependencies]
version_check = "0.9"
@ -42,6 +55,11 @@ rtic-time = { path = "../rtic-time" }
rtic-channel = { path = "../rtic-channel" }
rtic-monotonics = { path = "../rtic-monotonics" }
[dev-dependencies.futures]
version = "0.3.26"
default-features = false
features = ["async-await"]
[dev-dependencies.panic-semihosting]
features = ["exit"]
version = "0.6.0"

View file

@ -0,0 +1,9 @@
Sender 1 sending: 1
Sender 1 done
Sender 2 sending: 2
Sender 3 sending: 3
Receiver got: 1
Sender 2 done
Receiver got: 2
Sender 3 done
Receiver got: 3

View file

@ -0,0 +1 @@
Sender 1 sending: 1 Err(NoReceiver(1))

View file

@ -0,0 +1 @@
Receiver got: Err(NoSender)

View file

@ -0,0 +1,2 @@
Sender 1 sending: 1
Sender 1 try sending: 2 Err(Full(2))

View file

@ -3,4 +3,4 @@ Sender 2 sending: 2
Sender 3 sending: 3
Receiver got: 1
Receiver got: 2
Receiver got: 5
Receiver got: 3

View file

@ -1,5 +1,16 @@
init
hello from bar
hello from foo
foo no timeout
bar timeout
the hal takes a duration of Duration { ticks: 450 }
timeout
the hal takes a duration of Duration { ticks: 450 }
hal returned 5
the hal takes a duration of Duration { ticks: 450 }
hal returned 5
now is Instant { ticks: 2102 }, timeout at Instant { ticks: 2602 }
the hal takes a duration of Duration { ticks: 350 }
hal returned 5 at time Instant { ticks: 2452 }
now is Instant { ticks: 3102 }, timeout at Instant { ticks: 3602 }
the hal takes a duration of Duration { ticks: 450 }
hal returned 5 at time Instant { ticks: 3552 }
now is Instant { ticks: 4102 }, timeout at Instant { ticks: 4602 }
the hal takes a duration of Duration { ticks: 550 }
timeout

View file

@ -0,0 +1,3 @@
bar: local_to_bar = 1
foo: local_to_foo = 1
idle: local_to_idle = 1

View file

@ -0,0 +1,3 @@
init
Cannot spawn a spawned (running) task!
foo

View file

@ -0,0 +1,7 @@
init
foo
idle
foo
idle
foo
idle

View file

@ -0,0 +1,65 @@
//! examples/async-channel-done.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
use rtic_channel::*;
#[shared]
struct Shared {}
#[local]
struct Local {}
const CAPACITY: usize = 1;
#[init]
fn init(_: init::Context) -> (Shared, Local) {
let (s, r) = make_channel!(u32, CAPACITY);
receiver::spawn(r).unwrap();
sender1::spawn(s.clone()).unwrap();
sender2::spawn(s.clone()).unwrap();
sender3::spawn(s).unwrap();
(Shared {}, Local {})
}
#[task]
async fn receiver(_c: receiver::Context, mut receiver: Receiver<'static, u32, CAPACITY>) {
while let Ok(val) = receiver.recv().await {
hprintln!("Receiver got: {}", val);
if val == 3 {
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}
}
#[task]
async fn sender1(_c: sender1::Context, mut sender: Sender<'static, u32, CAPACITY>) {
hprintln!("Sender 1 sending: 1");
sender.send(1).await.unwrap();
hprintln!("Sender 1 done");
}
#[task]
async fn sender2(_c: sender2::Context, mut sender: Sender<'static, u32, CAPACITY>) {
hprintln!("Sender 2 sending: 2");
sender.send(2).await.unwrap();
hprintln!("Sender 2 done");
}
#[task]
async fn sender3(_c: sender3::Context, mut sender: Sender<'static, u32, CAPACITY>) {
hprintln!("Sender 3 sending: 3");
sender.send(3).await.unwrap();
hprintln!("Sender 3 done");
}
}

View file

@ -0,0 +1,40 @@
//! examples/async-channel-no-receiver.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
use rtic_channel::*;
#[shared]
struct Shared {}
#[local]
struct Local {}
const CAPACITY: usize = 1;
#[init]
fn init(_: init::Context) -> (Shared, Local) {
let (s, _r) = make_channel!(u32, CAPACITY);
sender1::spawn(s.clone()).unwrap();
(Shared {}, Local {})
}
#[task]
async fn sender1(_c: sender1::Context, mut sender: Sender<'static, u32, CAPACITY>) {
hprintln!("Sender 1 sending: 1 {:?}", sender.send(1).await);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}

View file

@ -0,0 +1,40 @@
//! examples/async-channel-no-sender.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
use rtic_channel::*;
#[shared]
struct Shared {}
#[local]
struct Local {}
const CAPACITY: usize = 1;
#[init]
fn init(_: init::Context) -> (Shared, Local) {
let (_s, r) = make_channel!(u32, CAPACITY);
receiver::spawn(r).unwrap();
(Shared {}, Local {})
}
#[task]
async fn receiver(_c: receiver::Context, mut receiver: Receiver<'static, u32, CAPACITY>) {
hprintln!("Receiver got: {:?}", receiver.recv().await);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}

View file

@ -0,0 +1,48 @@
//! examples/async-channel-try.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
use rtic_channel::*;
#[shared]
struct Shared {}
#[local]
struct Local {}
const CAPACITY: usize = 1;
#[init]
fn init(_: init::Context) -> (Shared, Local) {
let (s, r) = make_channel!(u32, CAPACITY);
receiver::spawn(r).unwrap();
sender1::spawn(s.clone()).unwrap();
(Shared {}, Local {})
}
#[task]
async fn receiver(_c: receiver::Context, mut receiver: Receiver<'static, u32, CAPACITY>) {
while let Ok(val) = receiver.recv().await {
hprintln!("Receiver got: {}", val);
}
}
#[task]
async fn sender1(_c: sender1::Context, mut sender: Sender<'static, u32, CAPACITY>) {
hprintln!("Sender 1 sending: 1");
sender.send(1).await.unwrap();
hprintln!("Sender 1 try sending: 2 {:?}", sender.try_send(2));
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}

View file

@ -19,9 +19,10 @@ mod app {
#[local]
struct Local {}
const CAPACITY: usize = 5;
#[init]
fn init(_: init::Context) -> (Shared, Local) {
let (s, r) = make_channel!(u32, 5);
let (s, r) = make_channel!(u32, CAPACITY);
receiver::spawn(r).unwrap();
sender1::spawn(s.clone()).unwrap();
@ -32,30 +33,30 @@ mod app {
}
#[task]
async fn receiver(_c: receiver::Context, mut receiver: Receiver<'static, u32, 5>) {
async fn receiver(_c: receiver::Context, mut receiver: Receiver<'static, u32, CAPACITY>) {
while let Ok(val) = receiver.recv().await {
hprintln!("Receiver got: {}", val);
if val == 5 {
if val == 3 {
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}
}
#[task]
async fn sender1(_c: sender1::Context, mut sender: Sender<'static, u32, 5>) {
async fn sender1(_c: sender1::Context, mut sender: Sender<'static, u32, CAPACITY>) {
hprintln!("Sender 1 sending: 1");
sender.send(1).await.unwrap();
}
#[task]
async fn sender2(_c: sender2::Context, mut sender: Sender<'static, u32, 5>) {
async fn sender2(_c: sender2::Context, mut sender: Sender<'static, u32, CAPACITY>) {
hprintln!("Sender 2 sending: 2");
sender.send(2).await.unwrap();
}
#[task]
async fn sender3(_c: sender3::Context, mut sender: Sender<'static, u32, 5>) {
async fn sender3(_c: sender3::Context, mut sender: Sender<'static, u32, CAPACITY>) {
hprintln!("Sender 3 sending: 3");
sender.send(5).await.unwrap();
sender.send(3).await.unwrap();
}
}

View file

@ -1,3 +1,5 @@
// examples/async-delay.rs
//
#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]

View file

@ -0,0 +1,87 @@
// examples/async-timeout.rs
//
#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]
use cortex_m_semihosting::{debug, hprintln};
use panic_semihosting as _;
use rtic_monotonics::systick_monotonic::*;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0, UART0], peripherals = true)]
mod app {
use super::*;
use futures::{future::FutureExt, select_biased};
rtic_monotonics::make_systick_timer_queue!(TIMER);
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
hprintln!("init");
let systick = Systick::start(cx.core.SYST, 12_000_000);
TIMER.initialize(systick);
foo::spawn().ok();
(Shared {}, Local {})
}
#[task]
async fn foo(_cx: foo::Context) {
// Call hal with short relative timeout using `select_biased`
select_biased! {
v = hal_get(&TIMER, 1).fuse() => hprintln!("hal returned {}", v),
_ = TIMER.delay(200.millis()).fuse() => hprintln!("timeout", ), // this will finish first
}
// Call hal with long relative timeout using `select_biased`
select_biased! {
v = hal_get(&TIMER, 1).fuse() => hprintln!("hal returned {}", v), // hal finish first
_ = TIMER.delay(1000.millis()).fuse() => hprintln!("timeout", ),
}
// Call hal with long relative timeout using monotonic `timeout_after`
match TIMER.timeout_after(1000.millis(), hal_get(&TIMER, 1)).await {
Ok(v) => hprintln!("hal returned {}", v),
_ => hprintln!("timeout"),
}
// get the current time instance
let mut instant = TIMER.now();
// do this 3 times
for n in 0..3 {
// exact point in time without drift
instant += 1000.millis();
TIMER.delay_until(instant).await;
// exact point it time for timeout
let timeout = instant + 500.millis();
hprintln!("now is {:?}, timeout at {:?}", TIMER.now(), timeout);
match TIMER.timeout_at(timeout, hal_get(&TIMER, n)).await {
Ok(v) => hprintln!("hal returned {} at time {:?}", v, TIMER.now()),
_ => hprintln!("timeout"),
}
}
debug::exit(debug::EXIT_SUCCESS);
}
}
// Emulate some hal
async fn hal_get(timer: &'static SystickTimerQueue, n: u32) -> u32 {
// emulate some delay time dependent on n
let d = 350.millis() + n * 100.millis();
hprintln!("the hal takes a duration of {:?}", d);
timer.delay(d).await;
// emulate some return value
5
}

86
rtic/examples/common.rs Normal file
View file

@ -0,0 +1,86 @@
//! examples/common.rs
#![feature(type_alias_impl_trait)]
#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(warnings)]
#![no_main]
#![no_std]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [UART0, UART1])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {}
#[local]
struct Local {
local_to_foo: i64,
local_to_bar: i64,
local_to_idle: i64,
}
// `#[init]` cannot access locals from the `#[local]` struct as they are initialized here.
#[init]
fn init(_: init::Context) -> (Shared, Local) {
foo::spawn().unwrap();
bar::spawn().unwrap();
(
Shared {},
// initial values for the `#[local]` resources
Local {
local_to_foo: 0,
local_to_bar: 0,
local_to_idle: 0,
},
)
}
// `local_to_idle` can only be accessed from this context
#[idle(local = [local_to_idle])]
fn idle(cx: idle::Context) -> ! {
let local_to_idle = cx.local.local_to_idle;
*local_to_idle += 1;
hprintln!("idle: local_to_idle = {}", local_to_idle);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
// error: no `local_to_foo` field in `idle::LocalResources`
// _cx.local.local_to_foo += 1;
// error: no `local_to_bar` field in `idle::LocalResources`
// _cx.local.local_to_bar += 1;
loop {
cortex_m::asm::nop();
}
}
// `local_to_foo` can only be accessed from this context
#[task(local = [local_to_foo])]
async fn foo(cx: foo::Context) {
let local_to_foo = cx.local.local_to_foo;
*local_to_foo += 1;
// error: no `local_to_bar` field in `foo::LocalResources`
// cx.local.local_to_bar += 1;
hprintln!("foo: local_to_foo = {}", local_to_foo);
}
// `local_to_bar` can only be accessed from this context
#[task(local = [local_to_bar])]
async fn bar(cx: bar::Context) {
let local_to_bar = cx.local.local_to_bar;
*local_to_bar += 1;
// error: no `local_to_foo` field in `bar::LocalResources`
// cx.local.local_to_foo += 1;
hprintln!("bar: local_to_bar = {}", local_to_bar);
}
}

View file

@ -0,0 +1,50 @@
//! examples/lock-free.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965)]
mod app {
use cortex_m_semihosting::{debug, hprintln};
use lm3s6965::Interrupt;
#[shared]
struct Shared {
#[lock_free] // <- lock-free shared resource
counter: u64,
}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
rtic::pend(Interrupt::UART0);
(Shared { counter: 0 }, Local {})
}
#[task(binds = UART0, shared = [counter])] // <- same priority
fn foo(c: foo::Context) {
rtic::pend(Interrupt::UART1);
*c.shared.counter += 1; // <- no lock API required
let counter = *c.shared.counter;
hprintln!(" foo = {}", counter);
}
#[task(binds = UART1, shared = [counter])] // <- same priority
fn bar(c: bar::Context) {
rtic::pend(Interrupt::UART0);
*c.shared.counter += 1; // <- no lock API required
let counter = *c.shared.counter;
hprintln!(" bar = {}", counter);
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}

View file

@ -0,0 +1,40 @@
//! examples/spawn.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![deny(missing_docs)]
#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
hprintln!("init");
foo::spawn().unwrap();
match foo::spawn() {
Ok(_) => {}
Err(()) => hprintln!("Cannot spawn a spawned (running) task!"),
}
(Shared {}, Local {})
}
#[task]
async fn foo(_: foo::Context) {
hprintln!("foo");
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
}
}

View file

@ -0,0 +1,42 @@
//! examples/spawn.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![deny(missing_docs)]
#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]
use panic_semihosting as _;
#[rtic::app(device = lm3s6965, dispatchers = [SSI0])]
mod app {
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(_: init::Context) -> (Shared, Local) {
hprintln!("init");
(Shared {}, Local {})
}
#[idle]
fn idle(_: idle::Context) -> ! {
for _ in 0..3 {
foo::spawn().unwrap();
hprintln!("idle");
}
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
loop {}
}
#[task]
async fn foo(_: foo::Context) {
hprintln!("foo");
}
}

View file

@ -0,0 +1,66 @@
//! examples/zero-prio-task.rs
#![no_main]
#![no_std]
#![feature(type_alias_impl_trait)]
#![deny(missing_docs)]
use core::marker::PhantomData;
use panic_semihosting as _;
/// Does not impl send
pub struct NotSend {
_0: PhantomData<*const ()>,
}
#[rtic::app(device = lm3s6965, peripherals = true)]
mod app {
use super::NotSend;
use core::marker::PhantomData;
use cortex_m_semihosting::{debug, hprintln};
#[shared]
struct Shared {
x: NotSend,
}
#[local]
struct Local {
y: NotSend,
}
#[init]
fn init(_cx: init::Context) -> (Shared, Local) {
hprintln!("init");
async_task::spawn().unwrap();
async_task2::spawn().unwrap();
(
Shared {
x: NotSend { _0: PhantomData },
},
Local {
y: NotSend { _0: PhantomData },
},
)
}
#[task(priority = 0, shared = [x], local = [y])]
async fn async_task(_: async_task::Context) {
hprintln!("hello from async");
}
#[task(priority = 0, shared = [x])]
async fn async_task2(_: async_task2::Context) {
hprintln!("hello from async2");
}
#[idle(shared = [x])]
fn idle(_: idle::Context) -> ! {
hprintln!("hello from idle");
debug::exit(debug::EXIT_SUCCESS); // Exit QEMU simulator
loop {}
}
}