mirror of
https://github.com/rtic-rs/rtic.git
synced 2024-12-01 16:04:33 +01:00
document internals
note that this assumes that RFC #155 has been implemented
This commit is contained in:
parent
bc024f1979
commit
09ec5a7a41
10 changed files with 1767 additions and 5 deletions
|
@ -10,6 +10,11 @@
|
||||||
- [Starting a new project](./by-example/new.md)
|
- [Starting a new project](./by-example/new.md)
|
||||||
- [Tips & tricks](./by-example/tips.md)
|
- [Tips & tricks](./by-example/tips.md)
|
||||||
- [Under the hood](./internals.md)
|
- [Under the hood](./internals.md)
|
||||||
|
- [Interrupt configuration](./internals/interrupt-configuration.md)
|
||||||
|
- [Non-reentrancy](./internals/non-reentrancy.md)
|
||||||
|
- [Access control](./internals/access.md)
|
||||||
|
- [Late resources](./internals/late-resources.md)
|
||||||
|
- [Critical sections](./internals/critical-sections.md)
|
||||||
- [Ceiling analysis](./internals/ceilings.md)
|
- [Ceiling analysis](./internals/ceilings.md)
|
||||||
- [Task dispatcher](./internals/tasks.md)
|
- [Software tasks](./internals/tasks.md)
|
||||||
- [Timer queue](./internals/timer-queue.md)
|
- [Timer queue](./internals/timer-queue.md)
|
||||||
|
|
|
@ -4,3 +4,8 @@ This section describes the internals of the RTFM framework at a *high level*.
|
||||||
Low level details like the parsing and code generation done by the procedural
|
Low level details like the parsing and code generation done by the procedural
|
||||||
macro (`#[app]`) will not be explained here. The focus will be the analysis of
|
macro (`#[app]`) will not be explained here. The focus will be the analysis of
|
||||||
the user specification and the data structures used by the runtime.
|
the user specification and the data structures used by the runtime.
|
||||||
|
|
||||||
|
We highly suggest that you read the embedonomicon section on [concurrency]
|
||||||
|
before you dive into this material.
|
||||||
|
|
||||||
|
[concurrency]: https://github.com/rust-embedded/embedonomicon/pull/48
|
||||||
|
|
158
book/en/src/internals/access.md
Normal file
158
book/en/src/internals/access.md
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
# Access control
|
||||||
|
|
||||||
|
One of the core foundations of RTFM is access control. Controlling which parts
|
||||||
|
of the program can access which static variables is instrumental to enforcing
|
||||||
|
memory safety.
|
||||||
|
|
||||||
|
Static variables are used to share state between interrupt handlers, or between
|
||||||
|
interrupts handlers and the bottom execution context, `main`. In normal Rust
|
||||||
|
code it's hard to have fine grained control over which functions can access a
|
||||||
|
static variable because static variables can be accessed from any function that
|
||||||
|
resides in the same scope in which they are declared. Modules give some control
|
||||||
|
over how a static variable can be accessed by they are not flexible enough.
|
||||||
|
|
||||||
|
To achieve the fine-grained access control where tasks can only access the
|
||||||
|
static variables (resources) that they have specified in their RTFM attribute
|
||||||
|
the RTFM framework performs a source code level transformation. This
|
||||||
|
transformation consists of placing the resources (static variables) specified by
|
||||||
|
the user *inside* a `const` item and the user code *outside* the `const` item.
|
||||||
|
This makes it impossible for the user code to refer to these static variables.
|
||||||
|
|
||||||
|
Access to the resources is then given to each task using a `Resources` struct
|
||||||
|
whose fields correspond to the resources the task has access to. There's one
|
||||||
|
such struct per task and the `Resources` struct is initialized with either a
|
||||||
|
mutable reference (`&mut`) to the static variables or with a resource proxy (see
|
||||||
|
section on [critical sections](critical-sections.html)).
|
||||||
|
|
||||||
|
The code below is an example of the kind of source level transformation that
|
||||||
|
happens behind the scenes:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
static mut X: u64: 0;
|
||||||
|
static mut Y: bool: 0;
|
||||||
|
|
||||||
|
#[init(resources = [Y])]
|
||||||
|
fn init(c: init::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interrupt(binds = UART0, resources = [X])]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interrupt(binds = UART1, resources = [X, Y])]
|
||||||
|
fn bar(c: bar::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework produces codes like this:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
fn init(c: init::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bar(c: bar::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
pub mod init {
|
||||||
|
pub struct Context<'a> {
|
||||||
|
pub resources: Resources<'a>,
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Resources<'a> {
|
||||||
|
pub Y: &'a mut bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod foo {
|
||||||
|
pub struct Context<'a> {
|
||||||
|
pub resources: Resources<'a>,
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Resources<'a> {
|
||||||
|
pub X: &'a mut u64,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod bar {
|
||||||
|
pub struct Context<'a> {
|
||||||
|
pub resources: Resources<'a>,
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Resources<'a> {
|
||||||
|
pub X: &'a mut u64,
|
||||||
|
pub Y: &'a mut bool,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation details
|
||||||
|
const APP: () = {
|
||||||
|
// everything inside this `const` item is hidden from user code
|
||||||
|
|
||||||
|
static mut X: u64 = 0;
|
||||||
|
static mut Y: bool = 0;
|
||||||
|
|
||||||
|
// the real entry point of the program
|
||||||
|
unsafe fn main() -> ! {
|
||||||
|
interrupt::disable();
|
||||||
|
|
||||||
|
// ..
|
||||||
|
|
||||||
|
// call into user code; pass references to the static variables
|
||||||
|
init(init::Context {
|
||||||
|
resources: init::Resources {
|
||||||
|
X: &mut X,
|
||||||
|
},
|
||||||
|
// ..
|
||||||
|
});
|
||||||
|
|
||||||
|
// ..
|
||||||
|
|
||||||
|
interrupt::enable();
|
||||||
|
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
// interrupt handler that `foo` binds to
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn UART0() {
|
||||||
|
// call into user code; pass references to the static variables
|
||||||
|
foo(foo::Context {
|
||||||
|
resources: foo::Resources {
|
||||||
|
X: &mut X,
|
||||||
|
},
|
||||||
|
// ..
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// interrupt handler that `bar` binds to
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn UART1() {
|
||||||
|
// call into user code; pass references to the static variables
|
||||||
|
bar(bar::Context {
|
||||||
|
resources: bar::Resources {
|
||||||
|
X: &mut X,
|
||||||
|
Y: &mut Y,
|
||||||
|
},
|
||||||
|
// ..
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
|
@ -1,3 +1,80 @@
|
||||||
# Ceiling analysis
|
# Ceiling analysis
|
||||||
|
|
||||||
**TODO**
|
A resource *priority ceiling*, or just *ceiling*, is the dynamic priority that
|
||||||
|
any task must have to safely access the resource memory. Ceiling analysis is
|
||||||
|
relatively simple but critical to the memory safety of RTFM applications.
|
||||||
|
|
||||||
|
To compute the ceiling of a resource we must first collect a list of tasks that
|
||||||
|
have access to the resource -- as the RTFM framework enforces access control to
|
||||||
|
resources at compile time it also has access to this information at compile
|
||||||
|
time. The ceiling of the resource is simply the highest logical priority among
|
||||||
|
those tasks.
|
||||||
|
|
||||||
|
`init` and `idle` are not proper tasks but they can access resources so they
|
||||||
|
need to be considered in the ceiling analysis. `idle` is considered as a task
|
||||||
|
that has a logical priority of `0` whereas `init` is completely omitted from the
|
||||||
|
analysis -- the reason for that is that `init` never uses (or needs) critical
|
||||||
|
sections to access static variables.
|
||||||
|
|
||||||
|
In the previous section we showed that a shared resource may appear as a mutable
|
||||||
|
reference or behind a proxy depending on the task that has access to it. Which
|
||||||
|
version is presented to the task depends on the task priority and the resource
|
||||||
|
ceiling. If the task priority is the same as the resource ceiling then the task
|
||||||
|
gets a mutable reference to the resource memory, otherwise the task gets a
|
||||||
|
proxy -- this also applies to `idle`. `init` is special: it always gets a
|
||||||
|
mutable reference to resources.
|
||||||
|
|
||||||
|
An example to illustrate the ceiling analysis:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
// accessed by `foo` (prio = 1) and `bar` (prio = 2)
|
||||||
|
// CEILING = 2
|
||||||
|
static mut X: u64 = 0;
|
||||||
|
|
||||||
|
// accessed by `idle` (prio = 0)
|
||||||
|
// CEILING = 0
|
||||||
|
static mut Y: u64 = 0;
|
||||||
|
|
||||||
|
#[init(resources = [X])]
|
||||||
|
fn init(c: init::Context) {
|
||||||
|
// mutable reference because this is `init`
|
||||||
|
let x: &mut u64 = c.resources.X;
|
||||||
|
|
||||||
|
// mutable reference because this is `init`
|
||||||
|
let y: &mut u64 = c.resources.Y;
|
||||||
|
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
// PRIORITY = 0
|
||||||
|
#[idle(resources = [Y])]
|
||||||
|
fn idle(c: idle::Context) -> ! {
|
||||||
|
// mutable reference because priority (0) == resource ceiling (0)
|
||||||
|
let y: &'static mut u64 = c.resources.Y;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interrupt(binds = UART0, priority = 1, resources = [X])]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// resource proxy because task priority (1) < resource ceiling (2)
|
||||||
|
let x: resources::X = c.resources.X;
|
||||||
|
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interrupt(binds = UART1, priority = 2, resources = [X])]
|
||||||
|
fn bar(c: foo::Context) {
|
||||||
|
// mutable reference because task priority (2) == resource ceiling (2)
|
||||||
|
let x: &mut u64 = c.resources.X;
|
||||||
|
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
515
book/en/src/internals/critical-sections.md
Normal file
515
book/en/src/internals/critical-sections.md
Normal file
|
@ -0,0 +1,515 @@
|
||||||
|
# Critical sections
|
||||||
|
|
||||||
|
When a resource (static variable) is shared between two, or more, tasks that run
|
||||||
|
at different priorities some form of mutual exclusion is required to access the
|
||||||
|
memory in a data race free manner. In RTFM we use priority-based critical
|
||||||
|
sections to guarantee mutual exclusion (see the [Immediate Priority Ceiling
|
||||||
|
Protocol][ipcp]).
|
||||||
|
|
||||||
|
[ipcp]: https://en.wikipedia.org/wiki/Priority_ceiling_protocol
|
||||||
|
|
||||||
|
The critical section consists of temporarily raising the *dynamic* priority of
|
||||||
|
the task. While a task is within this critical section all the other tasks that
|
||||||
|
may request the resource are *not allowed to start*.
|
||||||
|
|
||||||
|
How high must the dynamic priority be to ensure mutual exclusion on a particular
|
||||||
|
resource? The [ceiling analysis](ceiling-analysis.html) is in charge of
|
||||||
|
answering that question and will be discussed in the next section. This section
|
||||||
|
will focus on the implementation of the critical section.
|
||||||
|
|
||||||
|
## Resource proxy
|
||||||
|
|
||||||
|
For simplicity, let's look at a resource shared by two tasks that run at
|
||||||
|
different priorities. Clearly one of the task can preempt the other; to prevent
|
||||||
|
a data race the *lower priority* task must use a critical section when it needs
|
||||||
|
to modify the shared memory. On the other hand, the higher priority task can
|
||||||
|
directly modify the shared memory because it can't be preempted by the lower
|
||||||
|
priority task. To enforce the use of a critical section on the lower priority
|
||||||
|
task we give it a *resource proxy*, whereas we give a mutable reference
|
||||||
|
(`&mut-`) to the higher priority task.
|
||||||
|
|
||||||
|
The example below shows the different types handed out to each task:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
static mut X: u64 = 0;
|
||||||
|
|
||||||
|
#[interrupt(binds = UART0, priority = 1, resources = [X])]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// resource proxy
|
||||||
|
let mut x: resources::X = c.resources.X;
|
||||||
|
|
||||||
|
x.lock(|x: &mut u64| {
|
||||||
|
// critical section
|
||||||
|
*x += 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interrupt(binds = UART1, priority = 2, resources = [X])]
|
||||||
|
fn bar(c: foo::Context) {
|
||||||
|
let mut x: &mut u64 = c.resources.X;
|
||||||
|
|
||||||
|
*x += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Now let's see how these types are created by the framework.
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bar(c: bar::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod resources {
|
||||||
|
pub struct X {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod foo {
|
||||||
|
pub struct Resources {
|
||||||
|
pub X: resources::X,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
pub resources: Resources,
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod bar {
|
||||||
|
pub struct Resources<'a> {
|
||||||
|
pub X: rtfm::Exclusive<'a, u64>, // newtype over `&'a mut u64`
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context {
|
||||||
|
pub resources: Resources,
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP: () = {
|
||||||
|
static mut X: u64 = 0;
|
||||||
|
|
||||||
|
impl rtfm::Mutex for resources::X {
|
||||||
|
type T = u64;
|
||||||
|
|
||||||
|
fn lock<R>(&mut self, f: impl FnOnce(&mut u64) -> R) -> R {
|
||||||
|
// we'll check this in detail later
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn UART0() {
|
||||||
|
foo(foo::Context {
|
||||||
|
resources: foo::Resources {
|
||||||
|
X: resources::X::new(/* .. */),
|
||||||
|
},
|
||||||
|
// ..
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn UART1() {
|
||||||
|
bar(bar::Context {
|
||||||
|
resources: bar::Resources {
|
||||||
|
X: rtfm::Exclusive(&mut X),
|
||||||
|
},
|
||||||
|
// ..
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## `lock`
|
||||||
|
|
||||||
|
Let's now zoom into the critical section itself. In this example, we have to
|
||||||
|
raise the dynamic priority to at least `2` to prevent a data race. On the
|
||||||
|
Cortex-M architecture the dynamic priority can be changed by writing to the
|
||||||
|
`BASEPRI` register.
|
||||||
|
|
||||||
|
The semantics of the `BASEPRI` register are as follows:
|
||||||
|
|
||||||
|
- Writing a value of `0` to `BASEPRI` disables its functionality.
|
||||||
|
- Writing a non-zero value to `BASEPRI` changes the priority level required for
|
||||||
|
interrupt preemption. However, this only has an effect when the written value
|
||||||
|
is *lower* than the priority level of current execution context, but note that
|
||||||
|
a lower hardware priority level means higher logical priority
|
||||||
|
|
||||||
|
Thus the dynamic priority at any point in time can be computed as
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
dynamic_priority = max(hw2logical(BASEPRI), hw2logical(static_priority))
|
||||||
|
```
|
||||||
|
|
||||||
|
Where `static_priority` is the priority programmed in the NVIC for the current
|
||||||
|
interrupt, or a logical `0` when the current context is `idle`.
|
||||||
|
|
||||||
|
In this particular example we could implement the critical section as follows:
|
||||||
|
|
||||||
|
> **NOTE:** this is a simplified implementation
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
impl rtfm::Mutex for resources::X {
|
||||||
|
type T = u64;
|
||||||
|
|
||||||
|
fn lock<R, F>(&mut self, f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut u64) -> R,
|
||||||
|
{
|
||||||
|
unsafe {
|
||||||
|
// start of critical section: raise dynamic priority to `2`
|
||||||
|
asm!("msr BASEPRI, 192" : : : "memory" : "volatile");
|
||||||
|
|
||||||
|
// run user code within the critical section
|
||||||
|
let r = f(&mut implementation_defined_name_for_X);
|
||||||
|
|
||||||
|
// end of critical section: restore dynamic priority to its static value (`1`)
|
||||||
|
asm!("msr BASEPRI, 0" : : : "memory" : "volatile");
|
||||||
|
|
||||||
|
r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here it's important to use the `"memory"` clobber in the `asm!` block. It
|
||||||
|
prevents the compiler from reordering memory operations across it. This is
|
||||||
|
important because accessing the variable `X` outside the critical section would
|
||||||
|
result in a data race.
|
||||||
|
|
||||||
|
It's important to note that the signature of the `lock` method prevents nesting
|
||||||
|
calls to it. This is required for memory safety, as nested calls would produce
|
||||||
|
multiple mutable references (`&mut-`) to `X` breaking Rust aliasing rules. See
|
||||||
|
below:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[interrupt(binds = UART0, priority = 1, resources = [X])]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// resource proxy
|
||||||
|
let mut res: resources::X = c.resources.X;
|
||||||
|
|
||||||
|
res.lock(|x: &mut u64| {
|
||||||
|
res.lock(|alias: &mut u64| {
|
||||||
|
//~^ error: `res` has already been mutably borrowed
|
||||||
|
// ..
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nesting
|
||||||
|
|
||||||
|
Nesting calls to `lock` on the *same* resource must be rejected by the compiler
|
||||||
|
for memory safety but nesting `lock` calls on *different* resources is a valid
|
||||||
|
operation. In that case we want to make sure that nesting critical sections
|
||||||
|
never results in lowering the dynamic priority, as that would be unsound, and we
|
||||||
|
also want to optimize the number of writes to the `BASEPRI` register and
|
||||||
|
compiler fences. To that end we'll track the dynamic priority of the task using
|
||||||
|
a stack variable and use that to decide whether to write to `BASEPRI` or not. In
|
||||||
|
practice, the stack variable will be optimized away by the compiler but it still
|
||||||
|
provides extra information to the compiler.
|
||||||
|
|
||||||
|
Consider this program:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
static mut X: u64 = 0;
|
||||||
|
static mut Y: u64 = 0;
|
||||||
|
|
||||||
|
#[init]
|
||||||
|
fn init() {
|
||||||
|
rtfm::pend(Interrupt::UART0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interrupt(binds = UART0, priority = 1, resources = [X, Y])]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
let mut x = c.resources.X;
|
||||||
|
let mut y = c.resources.Y;
|
||||||
|
|
||||||
|
y.lock(|y| {
|
||||||
|
*y += 1;
|
||||||
|
|
||||||
|
*x.lock(|x| {
|
||||||
|
x += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
*y += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// mid-point
|
||||||
|
|
||||||
|
x.lock(|x| {
|
||||||
|
*x += 1;
|
||||||
|
|
||||||
|
y.lock(|y| {
|
||||||
|
*y += 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
*x += 1;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interrupt(binds = UART1, priority = 2, resources = [X])]
|
||||||
|
fn bar(c: foo::Context) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interrupt(binds = UART2, priority = 3, resources = [Y])]
|
||||||
|
fn baz(c: foo::Context) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The code generated by the framework looks like this:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
// omitted: user code
|
||||||
|
|
||||||
|
pub mod resources {
|
||||||
|
pub struct X<'a> {
|
||||||
|
priority: &'a Cell<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> X<'a> {
|
||||||
|
pub unsafe fn new(priority: &'a Cell<u8>) -> Self {
|
||||||
|
X { priority }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn priority(&self) -> &Cell<u8> {
|
||||||
|
self.priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// repeat for `Y`
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod foo {
|
||||||
|
pub struct Context {
|
||||||
|
pub resources: Resources,
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Resources<'a> {
|
||||||
|
pub X: resources::X<'a>,
|
||||||
|
pub Y: resources::Y<'a>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP: () = {
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn UART0() {
|
||||||
|
// the static priority of this interrupt (as specified by the user)
|
||||||
|
const PRIORITY: u8 = 1;
|
||||||
|
|
||||||
|
// take a snashot of the BASEPRI
|
||||||
|
let initial: u8;
|
||||||
|
asm!("mrs $0, BASEPRI" : "=r"(initial) : : : "volatile");
|
||||||
|
|
||||||
|
let priority = Cell::new(PRIORITY);
|
||||||
|
foo(foo::Context {
|
||||||
|
resources: foo::Resources::new(&priority),
|
||||||
|
// ..
|
||||||
|
});
|
||||||
|
|
||||||
|
// roll back the BASEPRI to the snapshot value we took before
|
||||||
|
asm!("msr BASEPRI, $0" : : "r"(initial) : : "volatile");
|
||||||
|
}
|
||||||
|
|
||||||
|
// similarly for `UART1`
|
||||||
|
|
||||||
|
impl<'a> rtfm::Mutex for resources::X<'a> {
|
||||||
|
type T = u64;
|
||||||
|
|
||||||
|
fn lock<R>(&mut self, f: impl FnOnce(&mut u64) -> R) -> R {
|
||||||
|
unsafe {
|
||||||
|
// the priority ceiling of this resource
|
||||||
|
const CEILING: u8 = 2;
|
||||||
|
|
||||||
|
let current = self.priority().get();
|
||||||
|
if current < CEILING {
|
||||||
|
// raise dynamic priority
|
||||||
|
self.priority().set(CEILING);
|
||||||
|
let hw = logical2hw(CEILING);
|
||||||
|
asm!("msr BASEPRI, $0" : : "r"(hw) : "memory" : "volatile");
|
||||||
|
|
||||||
|
let r = f(&mut X);
|
||||||
|
|
||||||
|
// restore dynamic priority
|
||||||
|
let hw = logical2hw(current);
|
||||||
|
asm!("msr BASEPRI, $0" : : "r"(hw) : "memory" : "volatile");
|
||||||
|
self.priority().set(current);
|
||||||
|
|
||||||
|
r
|
||||||
|
} else {
|
||||||
|
// dynamic priority is high enough
|
||||||
|
f(&mut X)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// repeat for `Y`
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
At the end the compiler will optimize the function `foo` into something like
|
||||||
|
this:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// NOTE: BASEPRI contains the value `0` (its reset value) at this point
|
||||||
|
|
||||||
|
// raise dynamic priority to `3`
|
||||||
|
unsafe { asm!("msr BASEPRI, 160" : : : "memory" : "volatile") }
|
||||||
|
|
||||||
|
// the two operations on `Y` are merged into one
|
||||||
|
Y += 2;
|
||||||
|
|
||||||
|
// BASEPRI is not modified to access `X` because the dynamic priority is high enough
|
||||||
|
X += 1;
|
||||||
|
|
||||||
|
// lower (restore) the dynamic priority to `1`
|
||||||
|
unsafe { asm!("msr BASEPRI, 224" : : : "memory" : "volatile") }
|
||||||
|
|
||||||
|
// mid-point
|
||||||
|
|
||||||
|
// raise dynamic priority to `2`
|
||||||
|
unsafe { asm!("msr BASEPRI, 192" : : : "memory" : "volatile") }
|
||||||
|
|
||||||
|
X += 1;
|
||||||
|
|
||||||
|
// raise dynamic priority to `3`
|
||||||
|
unsafe { asm!("msr BASEPRI, 160" : : : "memory" : "volatile") }
|
||||||
|
|
||||||
|
Y += 1;
|
||||||
|
|
||||||
|
// lower (restore) the dynamic priority to `2`
|
||||||
|
unsafe { asm!("msr BASEPRI, 192" : : : "memory" : "volatile") }
|
||||||
|
|
||||||
|
// NOTE: it would be sound to merge this operation on X with the previous one but
|
||||||
|
// compiler fences are coarse grained and prevent such optimization
|
||||||
|
X += 1;
|
||||||
|
|
||||||
|
// lower (restore) the dynamic priority to `1`
|
||||||
|
unsafe { asm!("msr BASEPRI, 224" : : : "memory" : "volatile") }
|
||||||
|
|
||||||
|
// NOTE: BASEPRI contains the value `224` at this point
|
||||||
|
// the UART0 handler will restore the value to `0` before returning
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## The BASEPRI invariant
|
||||||
|
|
||||||
|
An invariant that the RTFM framework has to preserve is that the value of the
|
||||||
|
BASEPRI at the start of an *interrupt* handler must be the same value it has
|
||||||
|
when the interrupt handler returns. BASEPRI may change during the execution of
|
||||||
|
the interrupt handler but running an interrupt handler from start to finish
|
||||||
|
should not result in an observable change of BASEPRI.
|
||||||
|
|
||||||
|
This invariant needs to be preserved to avoid raising the dynamic priority of a
|
||||||
|
handler through preemption. This is best observed in the following example:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
static mut X: u64 = 0;
|
||||||
|
|
||||||
|
#[init]
|
||||||
|
fn init() {
|
||||||
|
// `foo` will run right after `init` returns
|
||||||
|
rtfm::pend(Interrupt::UART0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(binds = UART0, priority = 1)]
|
||||||
|
fn foo() {
|
||||||
|
// BASEPRI is `0` at this point; the dynamic priority is currently `1`
|
||||||
|
|
||||||
|
// `bar` will preempt `foo` at this point
|
||||||
|
rtfm::pend(Interrupt::UART1);
|
||||||
|
|
||||||
|
// BASEPRI is `192` at this point (due to a bug); the dynamic priority is now `2`
|
||||||
|
// this function returns to `idle`
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(binds = UART1, priority = 2, resources = [X])]
|
||||||
|
fn bar() {
|
||||||
|
// BASEPRI is `0` (dynamic priority = 2)
|
||||||
|
|
||||||
|
X.lock(|x| {
|
||||||
|
// BASEPRI is raised to `160` (dynamic priority = 3)
|
||||||
|
|
||||||
|
// ..
|
||||||
|
});
|
||||||
|
|
||||||
|
// BASEPRI is restored to `192` (dynamic priority = 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[idle]
|
||||||
|
fn idle() -> ! {
|
||||||
|
// BASEPRI is `192` (due to a bug); dynamic priority = 2
|
||||||
|
|
||||||
|
// this has no effect due to the BASEPRI value
|
||||||
|
// the task `foo` will never be executed again
|
||||||
|
rtfm::pend(Interrupt::UART0);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(binds = UART2, priority = 3, resources = [X])]
|
||||||
|
fn baz() {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
IMPORTANT: let's say we *forget* to roll back `BASEPRI` in `UART1` -- this would
|
||||||
|
be a bug in the RTFM code generator.
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
// code generated by RTFM
|
||||||
|
|
||||||
|
const APP: () = {
|
||||||
|
// ..
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn UART1() {
|
||||||
|
// the static priority of this interrupt (as specified by the user)
|
||||||
|
const PRIORITY: u8 = 2;
|
||||||
|
|
||||||
|
// take a snashot of the BASEPRI
|
||||||
|
let initial: u8;
|
||||||
|
asm!("mrs $0, BASEPRI" : "=r"(initial) : : : "volatile");
|
||||||
|
|
||||||
|
let priority = Cell::new(PRIORITY);
|
||||||
|
bar(bar::Context {
|
||||||
|
resources: bar::Resources::new(&priority),
|
||||||
|
// ..
|
||||||
|
});
|
||||||
|
|
||||||
|
// BUG: FORGOT to roll back the BASEPRI to the snapshot value we took before
|
||||||
|
// asm!("msr BASEPRI, $0" : : "r"(initial) : : "volatile");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The consequence is that `idle` will run at a dynamic priority of `2` and in fact
|
||||||
|
the system will never again run at a dynamic priority lower than `2`. This
|
||||||
|
doesn't compromise the memory safety of the program but affects task scheduling:
|
||||||
|
in this particular case tasks with a priority of `1` will never get a chance to
|
||||||
|
run.
|
74
book/en/src/internals/interrupt-configuration.md
Normal file
74
book/en/src/internals/interrupt-configuration.md
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
# Interrupt configuration
|
||||||
|
|
||||||
|
Interrupts are core to the operation of RTFM applications. Correctly setting
|
||||||
|
interrupt priorities and ensuring they remain fixed at runtime is a requisite
|
||||||
|
for the memory safety of the application.
|
||||||
|
|
||||||
|
The RTFM framework exposes interrupt priorities as something that is declared at
|
||||||
|
compile time. However, this static configuration must be programmed into the
|
||||||
|
relevant registers during the initialization of the application. The interrupt
|
||||||
|
configuration is done before the `init` function runs.
|
||||||
|
|
||||||
|
This example gives you an idea of the code that the RTFM framework runs:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
#[init]
|
||||||
|
fn init(c: init::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[idle]
|
||||||
|
fn idle(c: idle::Context) -> ! {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interrupt(binds = UART0, priority = 2)]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework generates an entry point that looks like this:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
// the real entry point of the program
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn main() -> ! {
|
||||||
|
// transforms a logical priority into a hardware / NVIC priority
|
||||||
|
fn logical2hw(priority: u8) -> u8 {
|
||||||
|
// this value comes from the device crate
|
||||||
|
const NVIC_PRIO_BITS: u8 = ..;
|
||||||
|
|
||||||
|
// the NVIC encodes priority in the higher bits of a bit
|
||||||
|
// also a bigger numbers means lower priority
|
||||||
|
((1 << NVIC_PRIORITY_BITS) - priority) << (8 - NVIC_PRIO_BITS)
|
||||||
|
}
|
||||||
|
|
||||||
|
cortex_m::interrupt::disable();
|
||||||
|
|
||||||
|
let mut core = cortex_m::Peripheral::steal();
|
||||||
|
|
||||||
|
core.NVIC.enable(Interrupt::UART0);
|
||||||
|
|
||||||
|
// value specified by the user
|
||||||
|
let uart0_prio = 2;
|
||||||
|
|
||||||
|
// check at compile time that the specified priority is within the supported range
|
||||||
|
let _ = [(); (1 << NVIC_PRIORITY_BITS) - (uart0_prio as usize)];
|
||||||
|
|
||||||
|
core.NVIC.set_priority(Interrupt::UART0, logical2hw(uart0_prio));
|
||||||
|
|
||||||
|
// call into user code
|
||||||
|
init(/* .. */);
|
||||||
|
|
||||||
|
// ..
|
||||||
|
|
||||||
|
cortex_m::interrupt::enable();
|
||||||
|
|
||||||
|
// call into user code
|
||||||
|
idle(/* .. */)
|
||||||
|
}
|
||||||
|
```
|
115
book/en/src/internals/late-resources.md
Normal file
115
book/en/src/internals/late-resources.md
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
# Late resources
|
||||||
|
|
||||||
|
Some resources are initialized at runtime after the `init` function returns.
|
||||||
|
It's important that these resources (static variables) are fully initialized
|
||||||
|
before tasks are allowed to run, that is they must be initialized while
|
||||||
|
interrupts are disabled.
|
||||||
|
|
||||||
|
The example below shows the kind of code that the framework generates to
|
||||||
|
initialize late resources.
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
// late resource
|
||||||
|
static mut X: Thing = {};
|
||||||
|
|
||||||
|
#[init]
|
||||||
|
fn init() -> init::LateResources {
|
||||||
|
// ..
|
||||||
|
|
||||||
|
init::LateResources {
|
||||||
|
X: Thing::new(..),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(binds = UART0, resources = [X])]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
let x: &mut Thing = c.resources.X;
|
||||||
|
|
||||||
|
x.frob();
|
||||||
|
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The code generated by the framework looks like this:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
fn init(c: init::Context) -> init::LateResources {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public API
|
||||||
|
pub mod init {
|
||||||
|
pub struct LateResources {
|
||||||
|
pub X: Thing,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod foo {
|
||||||
|
pub struct Resources<'a> {
|
||||||
|
pub X: &'a mut Thing,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Context<'a> {
|
||||||
|
pub resources: Resources<'a>,
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implementation details
|
||||||
|
const APP: () = {
|
||||||
|
// uninitialized static
|
||||||
|
static mut X: MaybeUninit<Thing> = MaybeUninit::uninit();
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn main() -> ! {
|
||||||
|
cortex_m::interrupt::disable();
|
||||||
|
|
||||||
|
// ..
|
||||||
|
|
||||||
|
let late = init(..);
|
||||||
|
|
||||||
|
// initialization of late resources
|
||||||
|
X.write(late.X);
|
||||||
|
|
||||||
|
cortex_m::interrupt::enable(); //~ compiler fence
|
||||||
|
|
||||||
|
// exceptions, interrupts and tasks can preempt `main` at this point
|
||||||
|
|
||||||
|
idle(..)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn UART0() {
|
||||||
|
foo(foo::Context {
|
||||||
|
resources: foo::Resources {
|
||||||
|
// `X` has been initialized at this point
|
||||||
|
X: &mut *X.as_mut_ptr(),
|
||||||
|
},
|
||||||
|
// ..
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
An important detail here is that `interrupt::enable` behaves like a *compiler
|
||||||
|
fence*, which prevents the compiler from reordering the write to `X` to *after*
|
||||||
|
`interrupt::enable`. If the compiler were to do that kind of reordering there
|
||||||
|
would be a data race between that write and whatever operation `foo` performs on
|
||||||
|
`X`.
|
||||||
|
|
||||||
|
Architectures with more complex instruction pipelines may need a memory barrier
|
||||||
|
(`atomic::fence`) instead of a compiler fence to fully flush the write operation
|
||||||
|
before interrupts are re-enabled. The ARM Cortex-M architecture doesn't need a
|
||||||
|
memory barrier in single-core context.
|
84
book/en/src/internals/non-reentrancy.md
Normal file
84
book/en/src/internals/non-reentrancy.md
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
# Non-reentrancy
|
||||||
|
|
||||||
|
In RTFM, tasks handlers are *not* reentrant. Reentering a task handler can break
|
||||||
|
Rust aliasing rules and lead to *undefined behavior*. A task handler can be
|
||||||
|
reentered in one of two ways: in software or by hardware.
|
||||||
|
|
||||||
|
## In software
|
||||||
|
|
||||||
|
To reenter a task handler in software its underlying interrupt handler must be
|
||||||
|
invoked using FFI (see example below). FFI requires `unsafe` code so end users
|
||||||
|
are discouraged from directly invoking an interrupt handler.
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
static mut X: u64 = 0;
|
||||||
|
|
||||||
|
#[init]
|
||||||
|
fn init(c: init::Context) { .. }
|
||||||
|
|
||||||
|
#[interrupt(binds = UART0, resources = [X])]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
let x: &mut u64 = c.resources.X;
|
||||||
|
|
||||||
|
*x = 1;
|
||||||
|
|
||||||
|
//~ `bar` can preempt `foo` at this point
|
||||||
|
|
||||||
|
*x = 2;
|
||||||
|
|
||||||
|
if *x == 2 {
|
||||||
|
// something
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[interrupt(binds = UART1, priority = 2)]
|
||||||
|
fn bar(c: foo::Context) {
|
||||||
|
extern "C" {
|
||||||
|
fn UART0();
|
||||||
|
}
|
||||||
|
|
||||||
|
// this interrupt handler will invoke task handler `foo` resulting
|
||||||
|
// in mutable aliasing of the static variable `X`
|
||||||
|
unsafe { UART0() }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The RTFM framework must generate the interrupt handler code that calls the user
|
||||||
|
defined task handlers. We are careful in making these handlers `unsafe` and / or
|
||||||
|
impossible to call from user code.
|
||||||
|
|
||||||
|
The above example expands into:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bar(c: bar::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP: () = {
|
||||||
|
// everything in this block is not visible to user code
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn USART0() {
|
||||||
|
foo(..);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe fn USART1() {
|
||||||
|
bar(..);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## By hardware
|
||||||
|
|
||||||
|
A task handler can also be reentered without software intervention. This can
|
||||||
|
occur if the same handler is assigned to two or more interrupts in the vector
|
||||||
|
table but there's no syntax for this kind of configuration in the RTFM
|
||||||
|
framework.
|
|
@ -1,3 +1,397 @@
|
||||||
# Task dispatcher
|
# Software tasks
|
||||||
|
|
||||||
**TODO**
|
RTFM supports software tasks and hardware tasks. Each hardware task is bound to
|
||||||
|
a different interrupt handler. On the other hand, several software tasks may be
|
||||||
|
dispatched by the same interrupt handler -- this is done to minimize the number
|
||||||
|
of interrupts handlers used by the framework.
|
||||||
|
|
||||||
|
The framework groups `spawn`-able tasks by priority level and generates one
|
||||||
|
*task dispatcher* per priority level. Each task dispatcher runs on a different
|
||||||
|
interrupt handler and the priority of said interrupt handler is set to match the
|
||||||
|
priority level of the tasks managed by the dispatcher.
|
||||||
|
|
||||||
|
Each task dispatcher keeps a *queue* of tasks which are *ready* to execute; this
|
||||||
|
queue is referred to as the *ready queue*. Spawning a software task consists of
|
||||||
|
adding an entry to this queue and pending the interrupt that runs the
|
||||||
|
corresponding task dispatcher. Each entry in this queue contains a tag (`enum`)
|
||||||
|
that identifies the task to execute and a *pointer* to the message passed to the
|
||||||
|
task.
|
||||||
|
|
||||||
|
The ready queue is a SPSC (Single Producer Single Consumer) lock-free queue. The
|
||||||
|
task dispatcher owns the consumer endpoint of the queue; the producer endpoint
|
||||||
|
is treated as a resource shared by the tasks that can `spawn` other tasks.
|
||||||
|
|
||||||
|
## The task dispatcher
|
||||||
|
|
||||||
|
Let's first take a look the code generated by the framework to dispatch tasks.
|
||||||
|
Consider this example:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
// ..
|
||||||
|
|
||||||
|
#[interrupt(binds = UART0, priority = 2, spawn = [bar, baz])]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
foo.spawn.bar().ok();
|
||||||
|
|
||||||
|
foo.spawn.baz(42).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(capacity = 2, priority = 1)]
|
||||||
|
fn bar(c: bar::Context) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(capacity = 2, priority = 1, resources = [X])]
|
||||||
|
fn baz(c: baz::Context, input: i32) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
fn UART1();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The framework produces the following task dispatcher which consists of an
|
||||||
|
interrupt handler and a ready queue:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
fn bar(c: bar::Context) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP: () = {
|
||||||
|
use heapless::spsc::Queue;
|
||||||
|
use cortex_m::register::basepri;
|
||||||
|
|
||||||
|
struct Ready<T> {
|
||||||
|
task: T,
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `spawn`-able tasks that run at priority level `1`
|
||||||
|
enum T1 {
|
||||||
|
bar,
|
||||||
|
baz,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ready queue of the task dispatcher
|
||||||
|
// `U4` is a type-level integer that represents the capacity of this queue
|
||||||
|
static mut RQ1: Queue<Ready<T1>, U4> = Queue::new();
|
||||||
|
|
||||||
|
// interrupt handler chosen to dispatch tasks at priority `1`
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe UART1() {
|
||||||
|
// the priority of this interrupt handler
|
||||||
|
const PRIORITY: u8 = 1;
|
||||||
|
|
||||||
|
let snapshot = basepri::read();
|
||||||
|
|
||||||
|
while let Some(ready) = RQ1.split().1.dequeue() {
|
||||||
|
match ready.task {
|
||||||
|
T1::bar => {
|
||||||
|
// **NOTE** simplified implementation
|
||||||
|
|
||||||
|
// used to track the dynamic priority
|
||||||
|
let priority = Cell::new(PRIORITY);
|
||||||
|
|
||||||
|
// call into user code
|
||||||
|
bar(bar::Context::new(&priority));
|
||||||
|
}
|
||||||
|
|
||||||
|
T1::baz => {
|
||||||
|
// we'll look at `baz` later
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BASEPRI invariant
|
||||||
|
basepri::write(snapshot);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spawning a task
|
||||||
|
|
||||||
|
The `spawn` API is exposed to the user as the methods of a `Spawn` struct.
|
||||||
|
There's one `Spawn` struct per task.
|
||||||
|
|
||||||
|
The `Spawn` code generated by the framework for the previous example looks like
|
||||||
|
this:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
mod foo {
|
||||||
|
// ..
|
||||||
|
|
||||||
|
pub struct Context<'a> {
|
||||||
|
pub spawn: Spawn<'a>,
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Spawn<'a> {
|
||||||
|
// tracks the dyanmic priority of the task
|
||||||
|
priority: &'a Cell<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Spawn<'a> {
|
||||||
|
// `unsafe` and hidden because we don't want the user to tamper with it
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub unsafe fn priority(&self) -> &Cell<u8> {
|
||||||
|
self.priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP: () = {
|
||||||
|
// ..
|
||||||
|
|
||||||
|
// Priority ceiling for the producer endpoint of the `RQ1`
|
||||||
|
const RQ1_CEILING: u8 = 2;
|
||||||
|
|
||||||
|
// used to track how many more `bar` messages can be enqueued
|
||||||
|
// `U2` is the capacity of the `bar` task; a max of two instances can be queued
|
||||||
|
// this queue is filled by the framework before `init` runs
|
||||||
|
static mut bar_FQ: Queue<(), U2> = Queue::new();
|
||||||
|
|
||||||
|
// Priority ceiling for the consumer endpoint of `bar_FQ`
|
||||||
|
const bar_FQ_CEILING: u8 = 2;
|
||||||
|
|
||||||
|
// a priority-based critical section
|
||||||
|
//
|
||||||
|
// this run the given closure `f` at a dynamic priority of at least
|
||||||
|
// `ceiling`
|
||||||
|
fn lock(priority: &Cell<u8>, ceiling: u8, f: impl FnOnce()) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> foo::Spawn<'a> {
|
||||||
|
/// Spawns the `bar` task
|
||||||
|
pub fn bar(&self) -> Result<(), ()> {
|
||||||
|
unsafe {
|
||||||
|
match lock(self.priority(), bar_FQ_CEILING, || {
|
||||||
|
bar_FQ.split().1.dequeue()
|
||||||
|
}) {
|
||||||
|
Some(()) => {
|
||||||
|
lock(self.priority(), RQ1_CEILING, || {
|
||||||
|
// put the taks in the ready queue
|
||||||
|
RQ1.split().1.enqueue_unchecked(Ready {
|
||||||
|
task: T1::bar,
|
||||||
|
// ..
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// pend the interrupt that runs the task dispatcher
|
||||||
|
rtfm::pend(Interrupt::UART0);
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
// maximum capacity reached; spawn failed
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Using `bar_FQ` to limit the number of `bar` tasks that can be spawned may seem
|
||||||
|
like an artificial limitation but it will make more sense when we talk about
|
||||||
|
task capacities.
|
||||||
|
|
||||||
|
## Messages
|
||||||
|
|
||||||
|
We have omitted how message passing actually works so let's revisit the `spawn`
|
||||||
|
implementation but this time for task `baz` which receives a `u64` message.
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
fn baz(c: baz::Context, input: u64) {
|
||||||
|
// .. user code ..
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP: () = {
|
||||||
|
// ..
|
||||||
|
|
||||||
|
// Now we show the full contents of the `Ready` struct
|
||||||
|
struct Ready {
|
||||||
|
task: Task,
|
||||||
|
// message index; used to index the `INPUTS` buffer
|
||||||
|
index: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
// memory reserved to hold messages passed to `baz`
|
||||||
|
static mut baz_INPUTS: [MaybeUninit<u64>; 2] =
|
||||||
|
[MaybeUninit::uninit(), MaybeUninit::uninit()];
|
||||||
|
|
||||||
|
// the free queue: used to track free slots in the `baz_INPUTS` array
|
||||||
|
// this queue is initialized with values `0` and `1` before `init` is executed
|
||||||
|
static mut baz_FQ: Queue<u8, U2> = Queue::new();
|
||||||
|
|
||||||
|
// Priority ceiling for the consumer endpoint of `baz_FQ`
|
||||||
|
const baz_FQ_CEILING: u8 = 2;
|
||||||
|
|
||||||
|
impl<'a> foo::Spawn<'a> {
|
||||||
|
/// Spawns the `baz` task
|
||||||
|
pub fn baz(&self, message: u64) -> Result<(), u64> {
|
||||||
|
unsafe {
|
||||||
|
match lock(self.priority(), baz_FQ_CEILING, || {
|
||||||
|
baz_FQ.split().1.dequeue()
|
||||||
|
}) {
|
||||||
|
Some(index) => {
|
||||||
|
// NOTE: `index` is an ownining pointer into this buffer
|
||||||
|
baz_INPUTS[index as usize].write(message);
|
||||||
|
|
||||||
|
lock(self.priority(), RQ1_CEILING, || {
|
||||||
|
// put the task in the ready queu
|
||||||
|
RQ1.split().1.enqueue_unchecked(Ready {
|
||||||
|
task: T1::baz,
|
||||||
|
index,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// pend the interrupt that runs the task dispatcher
|
||||||
|
rtfm::pend(Interrupt::UART0);
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
// maximum capacity reached; spawn failed
|
||||||
|
Err(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
And now let's look at the real implementation of the task dispatcher:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
const APP: () = {
|
||||||
|
// ..
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe UART1() {
|
||||||
|
const PRIORITY: u8 = 1;
|
||||||
|
|
||||||
|
let snapshot = basepri::read();
|
||||||
|
|
||||||
|
while let Some(ready) = RQ1.split().1.dequeue() {
|
||||||
|
match ready.task {
|
||||||
|
Task::baz => {
|
||||||
|
// NOTE: `index` is an ownining pointer into this buffer
|
||||||
|
let input = baz_INPUTS[ready.index as usize].read();
|
||||||
|
|
||||||
|
// the message has been read out so we can return the slot
|
||||||
|
// back to the free queue
|
||||||
|
// (the task dispatcher has exclusive access to the producer
|
||||||
|
// endpoint of this queue)
|
||||||
|
baz_FQ.split().0.enqueue_unchecked(ready.index);
|
||||||
|
|
||||||
|
let priority = Cell::new(PRIORITY);
|
||||||
|
baz(baz::Context::new(&priority), input)
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::bar => {
|
||||||
|
// looks just like the `baz` branch
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BASEPRI invariant
|
||||||
|
basepri::write(snapshot);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`INPUTS` plus `FQ`, the free queue, is effectively a memory pool. However,
|
||||||
|
instead of using the usual *free list* (linked list) to track empty slots
|
||||||
|
in the `INPUTS` buffer we use a SPSC queue; this lets us reduce the number of
|
||||||
|
critical sections. In fact, thanks to this choice the task dispatching code is
|
||||||
|
lock-free.
|
||||||
|
|
||||||
|
## Queue capacity
|
||||||
|
|
||||||
|
The RTFM framework uses several queues like ready queues and free queues. When
|
||||||
|
the free queue is empty trying to `spawn` a task results in an error; this
|
||||||
|
condition is checked at runtime. Not all the operations performed by the
|
||||||
|
framework on these queues check if the queue is empty / full. For example,
|
||||||
|
returning an slot to the free queue (see the task dispatcher) is unchecked
|
||||||
|
because there's a fixed number of such slots circulating in the system that's
|
||||||
|
equal to the capacity of the free queue. Similarly, adding an entry to the ready
|
||||||
|
queue (see `Spawn`) is unchecked because of the queue capacity chosen by the
|
||||||
|
framework.
|
||||||
|
|
||||||
|
Users can specify the capacity of software tasks; this capacity is the maximum
|
||||||
|
number of messages one can post to said task from a higher priority task before
|
||||||
|
`spawn` returns an error. This user-specified capacity is the capacity of the
|
||||||
|
free queue of the task (e.g. `foo_FQ`) and also the size of the array that holds
|
||||||
|
the inputs to the task (e.g. `foo_INPUTS`).
|
||||||
|
|
||||||
|
The capacity of the ready queue (e.g. `RQ1`) is chosen to be the *sum* of the
|
||||||
|
capacities of all the different tasks managed by the dispatcher; this sum is
|
||||||
|
also the number of messages the queue will hold in the worst case scenario of
|
||||||
|
all possible messages being posted before the task dispatcher gets a chance to
|
||||||
|
run. For this reason, getting a slot from the free queue in any `spawn`
|
||||||
|
operation implies that the ready queue is not yet full so inserting an entry
|
||||||
|
into the ready queue can omit the "is it full?" check.
|
||||||
|
|
||||||
|
In our running example the task `bar` takes no input so we could have omitted
|
||||||
|
both `bar_INPUTS` and `bar_FQ` and let the user post an unbounded number of
|
||||||
|
messages to this task, but if we did that it would have not be possible to pick
|
||||||
|
a capacity for `RQ1` that lets us omit the "is it full?" check when spawning a
|
||||||
|
`baz` task. In the section about the [timer queue](timer-queue.html) we'll see
|
||||||
|
how the free queue is used by tasks that have no inputs.
|
||||||
|
|
||||||
|
## Ceiling analysis
|
||||||
|
|
||||||
|
The queues internally used by the `spawn` API are treated like normal resources
|
||||||
|
and included in the ceiling analysis. It's important to note that these are SPSC
|
||||||
|
queues and that only one of the endpoints is behind a resource; the other
|
||||||
|
endpoint is owned by a task dispatcher.
|
||||||
|
|
||||||
|
Consider the following example:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
#[idle(spawn = [foo, bar])]
|
||||||
|
fn idle(c: idle::Context) -> ! {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task]
|
||||||
|
fn bar(c: bar::Context) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(priority = 2, spawn = [foo])]
|
||||||
|
fn baz(c: baz::Context) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(priority = 3, spawn = [bar])]
|
||||||
|
fn quux(c: quux::Context) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This is how the ceiling analysis would go:
|
||||||
|
|
||||||
|
- `idle` (prio = 0) and `baz` (prio = 2) contend for the consumer endpoint of
|
||||||
|
`foo_FQ`; this leads to a priority ceiling of `2`.
|
||||||
|
|
||||||
|
- `idle` (prio = 0) and `quux` (prio = 3) contend for the consumer endpoint of
|
||||||
|
`bar_FQ`; this leads to a priority ceiling of `3`.
|
||||||
|
|
||||||
|
- `idle` (prio = 0), `baz` (prio = 2) and `quux` (prio = 3) all contend for the
|
||||||
|
producer endpoint of `RQ1`; this leads to a priority ceiling of `3`
|
||||||
|
|
|
@ -1,3 +1,338 @@
|
||||||
# Timer queue
|
# Timer queue
|
||||||
|
|
||||||
**TODO**
|
The timer queue functionality lets the user schedule tasks to run at some time
|
||||||
|
in the future. Unsurprisingly, this feature is also implemented using a queue:
|
||||||
|
a priority queue where the scheduled tasks are kept sorted by earliest scheduled
|
||||||
|
time. This feature requires a timer capable of setting up timeout interrupts.
|
||||||
|
The timer is used to trigger an interrupt when the scheduled time of a task is
|
||||||
|
up; at that point the task is removed from the timer queue and moved into the
|
||||||
|
appropriate ready queue.
|
||||||
|
|
||||||
|
Let's see how this in implemented in code. Consider the following program:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
// ..
|
||||||
|
|
||||||
|
#[task(capacity = 2, schedule = [foo])]
|
||||||
|
fn foo(c: foo::Context, x: u32) {
|
||||||
|
// schedule this task to run again in 1M cycles
|
||||||
|
c.schedule.foo(c.scheduled + Duration::cycles(1_000_000), x + 1).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
fn UART0();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## `schedule`
|
||||||
|
|
||||||
|
Let's first look at the `schedule` API.
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
mod foo {
|
||||||
|
pub struct Schedule<'a> {
|
||||||
|
priority: &'a Cell<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Schedule<'a> {
|
||||||
|
// unsafe and hidden because we don't want the user to tamper with this
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub unsafe fn priority(&self) -> &Cell<u8> {
|
||||||
|
self.priority
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP: () = {
|
||||||
|
use rtfm::Instant;
|
||||||
|
|
||||||
|
// all tasks that can be `schedule`-d
|
||||||
|
enum T {
|
||||||
|
foo,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct NotReady {
|
||||||
|
index: u8,
|
||||||
|
instant: Instant,
|
||||||
|
task: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The timer queue is a binary (min) heap of `NotReady` tasks
|
||||||
|
static mut TQ: TimerQueue<U2> = ..;
|
||||||
|
const TQ_CEILING: u8 = 1;
|
||||||
|
|
||||||
|
static mut foo_FQ: Queue<u8, U2> = Queue::new();
|
||||||
|
const foo_FQ_CEILING: u8 = 1;
|
||||||
|
|
||||||
|
static mut foo_INPUTS: [MaybeUninit<u32>; 2] =
|
||||||
|
[MaybeUninit::uninit(), MaybeUninit::uninit()];
|
||||||
|
|
||||||
|
static mut foo_INSTANTS: [MaybeUninit<Instant>; 2] =
|
||||||
|
[MaybeUninit::uninit(), MaybeUninit::uninit()];
|
||||||
|
|
||||||
|
impl<'a> foo::Schedule<'a> {
|
||||||
|
fn foo(&self, instant: Instant, input: u32) -> Result<(), u32> {
|
||||||
|
unsafe {
|
||||||
|
let priority = self.priority();
|
||||||
|
if let Some(index) = lock(priority, foo_FQ_CEILING, || {
|
||||||
|
foo_FQ.split().1.dequeue()
|
||||||
|
}) {
|
||||||
|
// `index` is an owning pointer into these buffers
|
||||||
|
foo_INSTANTS[index as usize].write(instant);
|
||||||
|
foo_INPUTS[index as usize].write(input);
|
||||||
|
|
||||||
|
let nr = NotReady {
|
||||||
|
index,
|
||||||
|
instant,
|
||||||
|
task: T::foo,
|
||||||
|
};
|
||||||
|
|
||||||
|
lock(priority, TQ_CEILING, || {
|
||||||
|
TQ.enqueue_unchecked(nr);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// No space left to store the input / instant
|
||||||
|
Err(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This looks very similar to the `Spawn` implementation. In fact, the same
|
||||||
|
`INPUTS` buffer and free queue (`FQ`) are shared between the `spawn` and
|
||||||
|
`schedule` APIs. The main difference between the two is that `schedule` also
|
||||||
|
stores the `Instant` at which the task was scheduled to run in a separate buffer
|
||||||
|
(`foo_INSTANTS` in this case).
|
||||||
|
|
||||||
|
`TimerQueue::enqueue_unchecked` does a bit more work that just adding the entry
|
||||||
|
into a min-heap: it also pends the system timer interrupt (`SysTick`) if the new
|
||||||
|
entry ended up first in the queue.
|
||||||
|
|
||||||
|
## The system timer
|
||||||
|
|
||||||
|
The system timer interrupt (`SysTick`) takes cares of two things: moving tasks
|
||||||
|
that have become ready from the timer queue into the right ready queue and
|
||||||
|
setting up a timeout interrupt to fire when the scheduled time of the next task
|
||||||
|
is up.
|
||||||
|
|
||||||
|
Let's see the associated code.
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
const APP: () = {
|
||||||
|
#[no_mangle]
|
||||||
|
fn SysTick() {
|
||||||
|
const PRIORITY: u8 = 1;
|
||||||
|
|
||||||
|
let priority = &Cell::new(PRIORITY);
|
||||||
|
while let Some(ready) = lock(priority, TQ_CEILING, || TQ.dequeue()) {
|
||||||
|
match ready.task {
|
||||||
|
T::foo => {
|
||||||
|
// move this task into the `RQ1` ready queue
|
||||||
|
lock(priority, RQ1_CEILING, || {
|
||||||
|
RQ1.split().0.enqueue_unchecked(Ready {
|
||||||
|
task: T1::foo,
|
||||||
|
index: ready.index,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// pend the task dispatcher
|
||||||
|
rtfm::pend(Interrupt::UART0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This looks similar to a task dispatcher except that instead of running the
|
||||||
|
ready task this only places the task in the corresponding ready queue, that
|
||||||
|
way it will run at the right priority.
|
||||||
|
|
||||||
|
`TimerQueue::dequeue` will set up a new timeout interrupt when it returns
|
||||||
|
`None`. This ties in with `TimerQueue::enqueue_unchecked`, which pends this
|
||||||
|
handler; basically, `enqueue_unchecked` delegates the task of setting up a new
|
||||||
|
timeout interrupt to the `SysTick` handler.
|
||||||
|
|
||||||
|
## Queue capacity
|
||||||
|
|
||||||
|
The capacity of the timer queue is chosen to be the sum of the capacities of all
|
||||||
|
`schedule`-able tasks. Like in the case of the ready queues, this means that
|
||||||
|
once we have claimed a free slot in the `INPUTS` buffer we are guaranteed to be
|
||||||
|
able to insert the task in the timer queue; this lets us omit runtime checks.
|
||||||
|
|
||||||
|
## System timer priority
|
||||||
|
|
||||||
|
The priority of the system timer can't set by the user; it is chosen by the
|
||||||
|
framework. To ensure that lower priority tasks don't prevent higher priority
|
||||||
|
tasks from running we choose the priority of the system timer to be the maximum
|
||||||
|
of all the `schedule`-able tasks.
|
||||||
|
|
||||||
|
To see why this is required consider the case where two previously scheduled
|
||||||
|
tasks with priorities `2` and `3` become ready at about the same time but the
|
||||||
|
lower priority task is moved into the ready queue first. If the system timer
|
||||||
|
priority was, for example, `1` then after moving the lower priority (`2`) task
|
||||||
|
it would run to completion (due to it being higher priority than the system
|
||||||
|
timer) delaying the execution of the higher priority (`3`) task. To prevent
|
||||||
|
scenarios like these the system timer must match the highest priority of the
|
||||||
|
`schedule`-able tasks; in this example that would be `3`.
|
||||||
|
|
||||||
|
## Ceiling analysis
|
||||||
|
|
||||||
|
The timer queue is a resource shared between all the tasks that can `schedule` a
|
||||||
|
task and the `SysTick` handler. Also the `schedule` API contends with the
|
||||||
|
`spawn` API over the free queues. All this must be considered in the ceiling
|
||||||
|
analysis.
|
||||||
|
|
||||||
|
To illustrate, consider the following example:
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
#[rtfm::app(device = ..)]
|
||||||
|
const APP: () = {
|
||||||
|
#[task(priority = 3, spawn = [baz])]
|
||||||
|
fn foo(c: foo::Context) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(priority = 2, schedule = [foo, baz])]
|
||||||
|
fn bar(c: bar::Context) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
#[task(priority = 1)]
|
||||||
|
fn baz(c: baz::Context) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
The ceiling analysis would go like this:
|
||||||
|
|
||||||
|
- `foo` (prio = 3) and `baz` (prio = 1) are `schedule`-able task so the
|
||||||
|
`SysTick` must run at the highest priority between these two, that is `3`.
|
||||||
|
|
||||||
|
- `foo::Spawn` (prio = 3) and `bar::Schedule` (prio = 2) contend over the
|
||||||
|
consumer endpoind of `baz_FQ`; this leads to a priority ceiling of `3`.
|
||||||
|
|
||||||
|
- `bar::Schedule` (prio = 2) has exclusive access over the consumer endpoint of
|
||||||
|
`foo_FQ`; thus the priority ceiling of `foo_FQ` is effectively `2`.
|
||||||
|
|
||||||
|
- `SysTick` (prio = 3) and `bar::Schedule` (prio = 2) contend over the timer
|
||||||
|
queue `TQ`; this leads to a priority ceiling of `3`.
|
||||||
|
|
||||||
|
- `SysTick` (prio = 3) and `foo::Spawn` (prio = 3) both have lock-free access to
|
||||||
|
the ready queue `RQ3`, which holds `foo` entries; thus the priority ceiling of
|
||||||
|
`RQ3` is effectively `3`.
|
||||||
|
|
||||||
|
- The `SysTick` has exclusive access to the ready queue `RQ1`, which holds `baz`
|
||||||
|
entries; thus the priority ceiling of `RQ1` is effectively `3`.
|
||||||
|
|
||||||
|
## Changes in the `spawn` implementation
|
||||||
|
|
||||||
|
When the "timer-queue" feature is enabled the `spawn` implementation changes a
|
||||||
|
bit to track the baseline of tasks. As you saw in the `schedule` implementation
|
||||||
|
there's an `INSTANTS` buffers used to store the time at which a task was
|
||||||
|
scheduled to run; this `Instant` is read in the task dispatcher and passed to
|
||||||
|
the user code as part of the task context.
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
const APP: () = {
|
||||||
|
// ..
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe UART1() {
|
||||||
|
const PRIORITY: u8 = 1;
|
||||||
|
|
||||||
|
let snapshot = basepri::read();
|
||||||
|
|
||||||
|
while let Some(ready) = RQ1.split().1.dequeue() {
|
||||||
|
match ready.task {
|
||||||
|
Task::baz => {
|
||||||
|
let input = baz_INPUTS[ready.index as usize].read();
|
||||||
|
// ADDED
|
||||||
|
let instant = baz_INSTANTS[ready.index as usize].read();
|
||||||
|
|
||||||
|
baz_FQ.split().0.enqueue_unchecked(ready.index);
|
||||||
|
|
||||||
|
let priority = Cell::new(PRIORITY);
|
||||||
|
// CHANGED the instant is passed as part the task context
|
||||||
|
baz(baz::Context::new(&priority, instant), input)
|
||||||
|
}
|
||||||
|
|
||||||
|
Task::bar => {
|
||||||
|
// looks just like the `baz` branch
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BASEPRI invariant
|
||||||
|
basepri::write(snapshot);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Conversely, the `spawn` implementation needs to write a value to the `INSTANTS`
|
||||||
|
buffer. The value to be written is stored in the `Spawn` struct and its either
|
||||||
|
the `start` time of the hardware task or the `scheduled` time of the software
|
||||||
|
task.
|
||||||
|
|
||||||
|
``` rust
|
||||||
|
mod foo {
|
||||||
|
// ..
|
||||||
|
|
||||||
|
pub struct Spawn<'a> {
|
||||||
|
priority: &'a Cell<u8>,
|
||||||
|
// ADDED
|
||||||
|
instant: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Spawn<'a> {
|
||||||
|
pub unsafe fn priority(&self) -> &Cell<u8> {
|
||||||
|
&self.priority
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADDED
|
||||||
|
pub unsafe fn instant(&self) -> Instant {
|
||||||
|
self.instant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP: () = {
|
||||||
|
impl<'a> foo::Spawn<'a> {
|
||||||
|
/// Spawns the `baz` task
|
||||||
|
pub fn baz(&self, message: u64) -> Result<(), u64> {
|
||||||
|
unsafe {
|
||||||
|
match lock(self.priority(), baz_FQ_CEILING, || {
|
||||||
|
baz_FQ.split().1.dequeue()
|
||||||
|
}) {
|
||||||
|
Some(index) => {
|
||||||
|
baz_INPUTS[index as usize].write(message);
|
||||||
|
// ADDED
|
||||||
|
baz_INSTANTS[index as usize].write(self.instant());
|
||||||
|
|
||||||
|
lock(self.priority(), RQ1_CEILING, || {
|
||||||
|
RQ1.split().1.enqueue_unchecked(Ready {
|
||||||
|
task: Task::foo,
|
||||||
|
index,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
rtfm::pend(Interrupt::UART0);
|
||||||
|
}
|
||||||
|
|
||||||
|
None => {
|
||||||
|
// maximum capacity reached; spawn failed
|
||||||
|
Err(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
Loading…
Reference in a new issue