Skip to content
Snippets Groups Projects
Commit 434f9d6d authored by Per's avatar Per
Browse files

performance

parent 763164d3
No related branches found
No related tags found
No related merge requests found
...@@ -275,20 +275,92 @@ As seen, the implmentation is fairly simple. `ceiling` here is the resource ceil ...@@ -275,20 +275,92 @@ As seen, the implmentation is fairly simple. `ceiling` here is the resource ceil
- `ceiling == max_priority` => here we cannot protect the resource by setting `BASEPRI` (masking priorities), and instead use `atomic` (which executes the closure `|t| f(data, t)` with globally disabled interrupts ( `PRIMASK = true`) - `ceiling == max_priority` => here we cannot protect the resource by setting `BASEPRI` (masking priorities), and instead use `atomic` (which executes the closure `|t| f(data, t)` with globally disabled interrupts ( `PRIMASK = true`)
- `ceiling != max_priority` => here we store the current system ceiling, (`old = basepri::read())`, set the new system ceiling `basepri::write(hw)` execute the closure `ret = f(data, &mut Threshold::new(ceiling))`, restore the system ceiling, `basepri::write(old)` and return the result `ret`. The `PRIMASK` and `BASEPRI` regeisters are located in the `Private Peripheral Bus` memory region, which is `Strongly-ordered` (meaning that accesses are executed in program order). I.e. the next instruction following `basepri::write(hw)` (inside the `claim`) will be protected by the raised system ceiling. [Arm doc - memory barriers](https://static.docs.arm.com/dai0321/a/DAI0321A_programming_guide_memory_barriers_for_m_profile.pdf) - `ceiling != max_priority` => here we store the current system ceiling, (`old = basepri::read())`, set the new system ceiling `basepri::write(hw)` execute the closure `ret = f(data, &mut Threshold::new(ceiling))`, restore the system ceiling, `basepri::write(old)` and return the result `ret`. The `PRIMASK` and `BASEPRI` regeisters are located in the `Private Peripheral Bus` memory region, which is `Strongly-ordered` (meaning that accesses are executed in program order). I.e. the next instruction following `basepri::write(hw)` (inside the `claim`) will be protected by the raised system ceiling. [Arm doc - memory barriers](https://static.docs.arm.com/dai0321/a/DAI0321A_programming_guide_memory_barriers_for_m_profile.pdf)
Race freness at this level can be argued from: Race freeness at this level can be argued from:
- Each *resource* is associated a *ceiling according to SRP - Each *resource* is associated a *ceiling* according to SRP. The `app!` procedural macro computes the ceilings from the tasks defined and the resources (declared and) used. How do we ensure that a task cannot access a resource not declared used in the `app`?
- Accessing a *resource* from *safe* user code can only be done through the `Resource::claim/claim_mut` trait, calling the library `claim`
The only resources accessible is those passed in the argument to the task (e.g., `EXTI0::Resources { mut LOW, mut HIGH }: EXTI0::Resources`). There is also no way in *safe* code to leak a reference to a resource through static (global memory) to another task. Notice though that is perfectly ok pass e.g., `&mut LOW` to a subroutine. In this case the sub routine will execute in task *context*.
Another thing achieved here is that the Rust semantics for non-aliased mutability is ensured. (Essentiall a nested claim to the same resource would be illegal in Rust, since `claim` passes as mutable reference to the *inner* data). This cannot happen as `claim` takes a `mut T`.
```rust
...
LOW.claim_mut_new(b, t, |_low, b, t| {
rtfm::bkpt();
LOW.claim_mut_new(b, t, |_high, _, _| {
rtfm::bkpt();
});
});
...
```
would be rejected
```
error[E0499]: cannot borrow `LOW` as mutable more than once at a time
--> examples/nested_new.rs:100:29
|
100 | LOW.claim_mut_new(b, t, |_low, b, t| {
| --- ^^^^^^^^^^^^ second mutable borrow occurs here
```
Trying to bluntly copy (clone) a resource handler will also fail.
```rust
let mut LOWC = LOW.clone();
error[E0599]: no method named `clone` found for type `_resource::LOW` in the current scope
--> examples/nested_new.rs:100:24
|
100 | let mut LOWC = LOW.clone();
```
- Accessing a *resource* from *safe* user code can only be done through the `Resource::claim/claim_mut` trait, calling the generic library function `claim`
- The `claim` implementation together with the `NVIC`, `BASEPRI` and `PRIMASK` enforces the SRP dispatch policy.
However there is more to it:
What if the user could fake (or alter) the `t` (Threshold). Well in that case the `claim` might give unprotected access. This is prevented by using an *opaque* data type `Threshold` in the `rtfm-core` lib.
```rust
pub struct Threshold {
value: u8,
_not_send: PhantomData<*const ()>,
}
```
The `value` field is not accessible to the user directly (and the user cannot alter or create a new `Threshold`) and the API to `Threshold::new()` is *unsafe*, i.e.,
```rust
...
*_t.value = 72; // attempt to fake Threshodld
let t = Threshold::new(0); // attempt to create a new Threshold
...
```
will render:
```rust
Compiling cortex-m-rtfm v0.2.1 (file:///home/pln/course/nucleo-64-rtfm)
error[E0616]: field `value` of struct `rtfm::Threshold` is private
--> examples/nested_new.rs:135:6
|
135 | *_t.value = 72;
| ^^^^^^^^
|
= note: a method `value` also exists, perhaps you wish to call it
error[E0133]: call to unsafe function requires unsafe function or block
--> examples/nested_new.rs:135:13
|
135 | let t = Threshold::new(0);
| ^^^^^^^^^^^^^^^^^ call to unsafe function
```
## The generated code in detail
Procedural macros in Rust are executed before code generation (causing the argument AST to replaced by a new AST for the remainder of compilation). Procedural macros in Rust are executed before code generation (causing the argument AST to replaced by a new AST for the remainder of compilation).
The intermediate code (AST after expansion) can be exported by the `cargo` sub-command `export`. The intermediate code (AST after expansion) can be exported by the `cargo` sub-command `export`.
```rust
> cargo export examples nested > expanded.rs > cargo export examples nested > expanded.rs
```
or or
```rust
> xargo export examples nested > expanded.rs > xargo export examples nested > expanded.rs
```
Let us study the `nested` example in detail. Let us study the `nested` example in detail.
```rust ```rust
...@@ -322,12 +394,8 @@ app! { ...@@ -322,12 +394,8 @@ app! {
} }
``` ```
---
### Auto generated `main`
The intermediate AST defines the following `main` function. The intermediate AST defines the following `main` function.
```rust ```rust
...@@ -354,14 +422,26 @@ fn main() { ...@@ -354,14 +422,26 @@ fn main() {
idle(); idle();
} }
``` ```
Essentially, the generated code initates the peripheral and resource bindings in an `atomic` section (with the interrupts disabled). The code also sets the interrupt priorities and enables the interrupts. Essentially, the generated code initates the peripheral and resource bindings in an `atomic` section (with the interrupts disabled). Besides first calling the user defined function `init`, the generated code also sets the interrupt priorities and enables the interrupts (tasks).
---
### Allocition of resources
The allocation of memory for the system resources is done using (global) `static mut`, with resource names prepended by `_`. Resources can only by accessed from user code through the `Resource` wrapping, initialized at run time.
```rust
static mut _HIGH: u64 = 0;
static mut _LOW: u64 = 0;
```
---
### Auto generated `init` arguments
All resources and peripherals are passed to the user `init` as defined in the generated `_initResources`. The auto generated code implments a module `init` holding the resource handlers.
```rust ```rust
pub struct _initResources<'a> { pub struct _initResources<'a> {
pub LOW: &'a mut rtfm::Static<u64>, pub LOW: &'a mut rtfm::Static<u64>,
pub HIGH: &'a mut rtfm::Static<u64>, pub HIGH: &'a mut rtfm::Static<u64>,
} }
#[allow(unsafe_code)] #[allow(unsafe_code)]
mod init { mod init {
pub use stm32f40x::Peripherals; pub use stm32f40x::Peripherals;
...@@ -376,25 +456,29 @@ mod init { ...@@ -376,25 +456,29 @@ mod init {
} }
} }
} }
static mut _HIGH: u64 = 0; ```
static mut _LOW: u64 = 0;
---
### Auto generated `task` arguments
A generic resource abstraction is generated in `_resource`.
```rust
mod _resource { mod _resource {
#[allow(non_camel_case_types)]
pub struct HIGH { pub struct HIGH {
_0: (), _0: (),
} }
#[allow(unsafe_code)]
impl HIGH { impl HIGH {
pub unsafe fn new() -> Self { pub unsafe fn new() -> Self {
HIGH { _0: () } HIGH { _0: () }
} }
} }
#[allow(non_camel_case_types)]
pub struct LOW { pub struct LOW {
_0: (), _0: (),
} }
#[allow(unsafe_code)]
impl LOW { impl LOW {
pub unsafe fn new() -> Self { pub unsafe fn new() -> Self {
LOW { _0: () } LOW { _0: () }
...@@ -402,8 +486,6 @@ mod _resource { ...@@ -402,8 +486,6 @@ mod _resource {
} }
} }
``` ```
The allocation of memory for the system resources is done using (global) `static mut`, with resource names prepended by `_`. Resources can only by accessed from user code through the `Resource` wrapping, initialized at run time.
In Rust a `mod` provides a *name space*, thus the statically allocated `HIGH` and `LOW` structs are accessed under the names `_resource::HIGH`, `_resource::LOW` respectively. In Rust a `mod` provides a *name space*, thus the statically allocated `HIGH` and `LOW` structs are accessed under the names `_resource::HIGH`, `_resource::LOW` respectively.
Code is generated for binding the user API `RES::claim`/`RES::claim_mut` to the library implementation of `claim`. For `claim` the reference is passed as `rtfm::Static::ref_(&_HIGH)`, while for `claim_mut` the reference is passed as `rtfm::Static::ref_mut(&_HIGH)`. Recall here that `_HIGH` is the actual resource allocation. Code is generated for binding the user API `RES::claim`/`RES::claim_mut` to the library implementation of `claim`. For `claim` the reference is passed as `rtfm::Static::ref_(&_HIGH)`, while for `claim_mut` the reference is passed as `rtfm::Static::ref_mut(&_HIGH)`. Recall here that `_HIGH` is the actual resource allocation.
...@@ -420,7 +502,7 @@ unsafe impl rtfm::Resource for _resource::HIGH { ...@@ -420,7 +502,7 @@ unsafe impl rtfm::Resource for _resource::HIGH {
unsafe { unsafe {
rtfm::claim( rtfm::claim(
rtfm::Static::ref_(&_HIGH), rtfm::Static::ref_(&_HIGH),
3u8, 3u8, // << computed ceiling value
stm32f40x::NVIC_PRIO_BITS, stm32f40x::NVIC_PRIO_BITS,
t, t,
f, f,
...@@ -434,7 +516,7 @@ unsafe impl rtfm::Resource for _resource::HIGH { ...@@ -434,7 +516,7 @@ unsafe impl rtfm::Resource for _resource::HIGH {
unsafe { unsafe {
rtfm::claim( rtfm::claim(
rtfm::Static::ref_mut(&mut _HIGH), rtfm::Static::ref_mut(&mut _HIGH),
3u8, 3u8, // << computed ceiling value
stm32f40x::NVIC_PRIO_BITS, stm32f40x::NVIC_PRIO_BITS,
t, t,
f, f,
...@@ -523,10 +605,100 @@ impl Threshold { ...@@ -523,10 +605,100 @@ impl Threshold {
} }
} }
``` ```
---
### Interrupt entry points
Each task is mapped to a corresponding entry in the interrupt vector table. An entry point stub is generated for each task, calling the user defined code. Each taks is called with the exact set of resource handlers (and peripherals used), in the above example `EXTI0::Resources`.
```rust
pub unsafe extern "C" fn _EXTI0() {
let f: fn(&mut rtfm::Threshold, EXTI0::Resources) = exti0;
f(
&mut if 1u8 == 1 << stm32f40x::NVIC_PRIO_BITS {
rtfm::Threshold::new(::core::u8::MAX)
} else {
rtfm::Threshold::new(1u8)
},
EXTI0::Resources::new(),
)
}
mod EXTI0 {
pub struct Resources {
pub HIGH: ::_resource::HIGH,
pub LOW: ::_resource::LOW,
}
impl Resources {
pub unsafe fn new() -> Self {
Resources {
HIGH: { ::_resource::HIGH::new() },
LOW: { ::_resource::LOW::new() },
}
}
}
}
```
---
## Performance
As seen there is quite some autogenerated and library code involved for the task and resource management. To our aid here is the Rust memory model allowing for zero cost abstractions.
The `exti0` task:
```rust
fn exti0(
t: &mut Threshold,
EXTI0::Resources { mut LOW, mut HIGH }: EXTI0::Resources,
) {
rtfm::bkpt();
LOW.claim_mut(t, |_low, t| {
rtfm::bkpt();
HIGH.claim_mut(t, |_high, _| {
rtfm::bkpt();
});
});
}
```
Amounts to the following assembly (including the interrupt entry code.)
```rust
Dump of assembler code for function nested_new::_EXTI0:
0x080005a6 <+0>: movs r1, #224 ; 0xe0
=> 0x080005a8 <+2>: bkpt 0x0000
0x080005aa <+4>: mrs r0, BASEPRI
0x080005ae <+8>: movs r2, #208 ; 0xd0
0x080005b0 <+10>: msr BASEPRI, r1
0x080005b4 <+14>: bkpt 0x0000
0x080005b6 <+16>: mrs r1, BASEPRI
0x080005ba <+20>: msr BASEPRI, r2
0x080005be <+24>: bkpt 0x0000
0x080005c0 <+26>: msr BASEPRI, r1
0x080005c4 <+30>: msr BASEPRI, r0
0x080005c8 <+34>: bx lr
```
The worlds fastest preemtive scheduler for tasks with shared resources is at bay! (We challenge anyone to beat RTFM!)
# How low can you go
An observation here is that we read basepri in the inner claim
```
0x080005b6 <+16>: mrs r1, BASEPRI
```
though that we actually know that `BASEPRI` will have the value `r1` in this case.
In an exprimental version of the RTFM implementation this observation has been explointed.
```rust
Dump of assembler code for function nested_new::_EXTI3:
0x080005d0 <+0>: movs r1, #224 ; 0xe0
0x080005d2 <+2>: movs r2, #208 ; 0xd0
=> 0x080005d4 <+4>: bkpt 0x0000
0x080005d6 <+6>: mrs r0, BASEPRI
0x080005da <+10>: msr BASEPRI, r1
0x080005de <+14>: bkpt 0x0000
0x080005e0 <+16>: msr BASEPRI, r2
0x080005e4 <+20>: bkpt 0x0000
0x080005e6 <+22>: msr BASEPRI, r1
0x080005ea <+26>: msr BASEPRI, r0
0x080005ee <+30>: bx lr
```
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment