498: book: update the resources chapter r=AfoHT a=japaric

see individual commit messages for details.

what's still left to do is adjust the very last section about `#[task_local]` and `#[lock_free]` but I plan to do that as a follow up. I didn't find an in-tree example for those two attributes (are they field attributes? where do they fit in the syntax?); a quick scan of the rtic-syntax crate seems to indicate that `task_local` has been removed (?) and that `lock_free` still exists.

Co-authored-by: Jorge Aparicio <jorge.aparicio@ferrous-systems.com>
This commit is contained in:
bors[bot] 2021-07-22 07:29:35 +00:00 committed by GitHub
commit c62fd967d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 145 additions and 70 deletions

View file

@ -7,22 +7,24 @@ Resources are data visible only to functions declared within the `#[app]`
module. The framework gives the user complete control over which context module. The framework gives the user complete control over which context
can access which resource. can access which resource.
All resources are declared as a single `struct` within the `#[app]` All resources are declared as *two* `struct`s within the `#[app]` module.
module. Each field in the structure corresponds to a different resource. Each field in these structures corresponds to a different resource.
The `struct` must be annotated with the following attribute: `#[resources]`. One `struct` must be annotated with the attribute `#[local]`.
The other `struct` must be annotated with the attribute `#[shared]`.
Resources can optionally be given an initial value using the `#[init]` The difference between these two sets of resources will be covered later.
attribute. Resources that are not given an initial value are referred to as
*late* resources and are covered in more detail in a follow-up section in this
page.
Each context (task handler, `init` or `idle`) must declare the resources it Each context (task handler, `init` or `idle`) must declare the resources it
intends to access in its corresponding metadata attribute using the `resources` intends to access in its corresponding metadata attribute using either the
argument. This argument takes a list of resource names as its value. The listed `local` or `shared` argument. This argument takes a list of resource names as
resources are made available to the context under the `resources` field of the its value. The listed resources are made available to the context under the
`Context` structure. `local` and `shared` fields of the `Context` structure.
The example application shown below contains two interrupt handlers that share access to a resource named `shared`. All resources are initialized at runtime, after the `#[init]` function returns.
The `#[init]` function must return the initial values for all resources; hence its return type includes the types of the `#[shared]` and `#[local]` structs.
Because resources are uninitialized during the execution of the `#[init]` function, they cannot be accessed within the `#[init]` function.
The example application shown below contains two interrupt handlers.
Each handler has access to its own `#[local]` resource.
``` rust ``` rust
{{#include ../../../../examples/resource.rs}} {{#include ../../../../examples/resource.rs}}
@ -33,13 +35,14 @@ $ cargo run --example resource
{{#include ../../../../ci/expected/resource.run}} {{#include ../../../../ci/expected/resource.run}}
``` ```
Note that the `shared` resource cannot be accessed from `idle`. Attempting to do so results in a compile error. A `#[local]` resource cannot be accessed from outside the task it was associated to in a `#[task]` attribute.
Assigning the same `#[local]` resource to more than one task is a compile-time error.
## `lock` ## `lock`
Critical sections are required to access shared mutable data in a data race-free manner. Critical sections are required to access `#[shared]` resources in a data race-free manner.
The `resources` field of the passed `Context` implements the [`Mutex`] trait for each shared resource accessible to the task. The `shared` field of the passed `Context` implements the [`Mutex`] trait for each shared resource accessible to the task.
The only method on this trait, [`lock`], runs its closure argument in a critical section. The only method on this trait, [`lock`], runs its closure argument in a critical section.
@ -52,7 +55,7 @@ The critical section created by the `lock` API is based on dynamic priorities: i
[icpp]: https://en.wikipedia.org/wiki/Priority_ceiling_protocol [icpp]: https://en.wikipedia.org/wiki/Priority_ceiling_protocol
[srp]: https://en.wikipedia.org/wiki/Stack_Resource_Policy [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 the `shared` resource and need to lock the resource for accessing the data. The highest priority handler, which do nat access the `shared` resource, is free to preempt the critical section created by the 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. lowest priority handler.
``` rust ``` rust
@ -72,26 +75,7 @@ As an extension to `lock`, and to reduce rightward drift, locks can be taken as
{{#include ../../../../examples/multilock.rs}} {{#include ../../../../examples/multilock.rs}}
``` ```
## Late resources ## Only shared (`&-`) access
Late resources are resources that are not given an initial value at compile time using the `#[init]` attribute but instead are initialized at runtime using the `init::LateResources` values returned by the `init` function.
Late resources are useful e.g., to *move* (as in transferring the ownership of) peripherals initialized in `init` into tasks.
The example below uses late resources to establish a lockless, one-way channel between the `UART0` interrupt handler and the `idle` task. A single producer single consumer [`Queue`] is used as the channel. The queue is split into consumer and producer end points in `init` and then each end point is stored in a different resource; `UART0` owns the producer resource and `idle` owns the consumer resource.
[`Queue`]: ../../../api/heapless/spsc/struct.Queue.html
``` rust
{{#include ../../../../examples/late.rs}}
```
``` console
$ cargo run --example late
{{#include ../../../../ci/expected/late.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 `resources` list. 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 `resources` list.
@ -113,12 +97,15 @@ $ cargo run --example only-shared-access
## Lock-free resource access of mutable resources ## Lock-free resource access of mutable resources
There exists two other options dealing with 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: if you do use the `lock` API, at runtime the framework will *not* produce a critical section.
* `#[lock_free]`: there might be several tasks with the same priority ``` rust
accessing the resource without critical section. Since tasks with the {{#include ../../../../examples/lock-free.rs}}
same priority never can preempt another task on the same priority ```
this is safe.
* `#[task_local]`: there must be only one task using this resource,
similar to a `static mut` task local resource, but (optionally) set-up by init.
``` console
$ cargo run --example lock-free
{{#include ../../../../ci/expected/lock-free.run}}
```

14
ci/expected/lock-free.run Normal file
View file

@ -0,0 +1,14 @@
GPIOA/start
GPIOA/counter = 1
GPIOA/end
GPIOB/start
GPIOB/counter = 2
GPIOB/end
GPIOA/start
GPIOA/counter = 3
GPIOA/end
GPIOB/start
GPIOB/counter = 4
GPIOB/end
GPIOA/start
GPIOA/counter = 5

View file

@ -1,2 +1,2 @@
UART1: shared = 1 UART1: local_to_uart1 = 1
UART0: shared = 2 UART0: local_to_uart0 = 1

60
examples/lock-free.rs Normal file
View file

@ -0,0 +1,60 @@
//! examples/lock-free.rs
#![deny(unsafe_code)]
#![deny(warnings)]
#![no_main]
#![no_std]
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, init::Monotonics) {
rtic::pend(Interrupt::GPIOA);
(Shared { counter: 0 }, Local {}, init::Monotonics())
}
#[task(binds = GPIOA, shared = [counter])] // <- same priority
fn gpioa(c: gpioa::Context) {
hprintln!("GPIOA/start").unwrap();
rtic::pend(Interrupt::GPIOB);
*c.shared.counter += 1; // <- no lock API required
let counter = *c.shared.counter;
hprintln!(" GPIOA/counter = {}", counter).unwrap();
if counter == 5 {
debug::exit(debug::EXIT_SUCCESS);
}
hprintln!("GPIOA/end").unwrap();
}
#[task(binds = GPIOB, shared = [counter])] // <- same priority
fn gpiob(c: gpiob::Context) {
hprintln!("GPIOB/start").unwrap();
rtic::pend(Interrupt::GPIOA);
*c.shared.counter += 1; // <- no lock API required
let counter = *c.shared.counter;
hprintln!(" GPIOB/counter = {}", counter).unwrap();
if counter == 5 {
debug::exit(debug::EXIT_SUCCESS);
}
hprintln!("GPIOB/end").unwrap();
}
}

View file

@ -13,55 +13,69 @@ mod app {
use lm3s6965::Interrupt; use lm3s6965::Interrupt;
#[shared] #[shared]
struct Shared { struct Shared {}
shared: u32,
}
#[local] #[local]
struct Local {} struct Local {
local_to_uart0: i64,
local_to_uart1: i64,
}
#[init] #[init]
fn init(_: init::Context) -> (Shared, Local, init::Monotonics) { fn init(_: init::Context) -> (Shared, Local, init::Monotonics) {
rtic::pend(Interrupt::UART0); rtic::pend(Interrupt::UART0);
rtic::pend(Interrupt::UART1); rtic::pend(Interrupt::UART1);
(Shared { shared: 0 }, Local {}, init::Monotonics()) (
Shared {},
// initial values for the `#[local]` resources
Local {
local_to_uart0: 0,
local_to_uart1: 0,
},
init::Monotonics(),
)
} }
// `shared` cannot be accessed from this context // `#[local]` resources cannot be accessed from this context
#[idle] #[idle]
fn idle(_cx: idle::Context) -> ! { fn idle(_cx: idle::Context) -> ! {
debug::exit(debug::EXIT_SUCCESS); debug::exit(debug::EXIT_SUCCESS);
// error: no `shared` field in `idle::Context` // error: no `local` field in `idle::Context`
// _cx.shared.shared += 1; // _cx.local.local_to_uart0 += 1;
// error: no `local` field in `idle::Context`
// _cx.local.local_to_uart1 += 1;
loop { loop {
cortex_m::asm::nop(); cortex_m::asm::nop();
} }
} }
// `shared` can be accessed from this context // `local_to_uart0` can only be accessed from this context
// defaults to priority 1 // defaults to priority 1
#[task(binds = UART0, shared = [shared])] #[task(binds = UART0, local = [local_to_uart0])]
fn uart0(mut cx: uart0::Context) { fn uart0(cx: uart0::Context) {
let shared = cx.shared.shared.lock(|shared| { *cx.local.local_to_uart0 += 1;
*shared += 1; let local_to_uart0 = cx.local.local_to_uart0;
*shared
});
hprintln!("UART0: shared = {}", shared).unwrap(); // error: no `local_to_uart1` field in `uart0::LocalResources`
// cx.local.local_to_uart1 += 1;
hprintln!("UART0: local_to_uart0 = {}", local_to_uart0).unwrap();
} }
// `shared` can be accessed from this context // `shared` can only be accessed from this context
// explicitly set to priority 2 // explicitly set to priority 2
#[task(binds = UART1, shared = [shared], priority = 2)] #[task(binds = UART1, local = [local_to_uart1], priority = 2)]
fn uart1(mut cx: uart1::Context) { fn uart1(cx: uart1::Context) {
let shared = cx.shared.shared.lock(|shared| { *cx.local.local_to_uart1 += 1;
*shared += 1; let local_to_uart1 = cx.local.local_to_uart1;
*shared
});
hprintln!("UART1: shared = {}", shared).unwrap(); // error: no `local_to_uart0` field in `uart1::LocalResources`
// cx.local.local_to_uart0 += 1;
hprintln!("UART1: local_to_uart1 = {}", local_to_uart1).unwrap();
} }
} }