&mut-) на память ресурса, в противном случае задача получает прокси -- это также касается idle. init особеннвй: он всегда получает уникальные ссылки (&mut-) на ресурсы.
Пример для иллюстрации анализа приоритетов:
#![allow(unused)]
fn main() {
#[rtic::app(device = ..)]
mod app {
struct Resources {
// доступен из `foo` (prio = 1) и `bar` (prio = 2)
// -> CEILING = 2
#[init(0)]
x: u64,
// доступен из `idle` (prio = 0)
// -> CEILING = 0
#[init(0)]
y: u64,
}
#[init(resources = [x])]
fn init(c: init::Context) {
// уникальная ссылка, потому что это `init`
let x: &mut u64 = c.resources.x;
// уникальная ссылка, потому что это `init`
let y: &mut u64 = c.resources.y;
// ..
}
// PRIORITY = 0
#[idle(resources = [y])]
fn idle(c: idle::Context) -> ! {
// уникальная ссылка, потому что
// приоритет (0) == максимальному приоритету ресурса (0)
let y: &'static mut u64 = c.resources.y;
loop {
// ..
}
}
#[interrupt(binds = UART0, priority = 1, resources = [x])]
fn foo(c: foo::Context) {
// прокси-ресурс, потому что
// приоритет задач (1) < максимальному приоритету ресурса (2)
let x: resources::x = c.resources.x;
// ..
}
#[interrupt(binds = UART1, priority = 2, resources = [x])]
fn bar(c: foo::Context) {
// уникальная ссылка, потому что
// приоритет задачи (2) == максимальному приоритету ресурса (2)
let x: &mut u64 = c.resources.x;
// ..
}
// ..
}
}
Программные задачи
RTIC поддерживает программные и аппаратные задачи. Каждая аппаратная задача назначается на отдельный обработчик прерывания. С другой стороны, несколько программных задач могут управляться одним обработчиком прерывания -- это сделано, чтобы минимизировать количество обработчиков прерывания, используемых фреймворком.
Фреймворк группирует задачи, для которых вызывается spawn по уровню приоритета, и генерирует один диспетчер задачи для каждого уровня приоритета. Каждый диспетчер запускается на отдельном обработчике прерывания, а приоритет этого обработчика прерывания устанавливается так, чтобы соответствовать уровню приоритета задач, управляемых диспетчером.
Каждый диспетчер задач хранит очередь задач, готовых к выполнению; эта очередь называется очередью готовности. Вызов программной задачи состоит из добавления записи в очередь и вызова прерывания, который запускает соответствующий диспетчер задач. Каждая запись в эту очередь содержит метку (enum), которая идентифицирует задачу, которую необходимо выполнить и указатель на сообщение, передаваемое задаче.
Очередь готовности - неблокируемая очередь типа SPSC (один производитель - один потребитель). Диспетчер задач владеет конечным потребителем в очереди; конечным производителем считается ресурс, за который соперничают задачи, которые могут вызывать (spawn) другие задачи.
Дисметчер задач
Давайте сначала глянем на код, генерируемый фреймворком для диспетчеризации задач. Рассмотрим пример:
#![allow(unused)]
fn main() {
#[rtic::app(device = ..)]
mod 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();
}
}
}
Фреймворк создает следующий диспетчер задач, состоящий из обработчика прерывания и очереди готовности:
#![allow(unused)]
fn main() {
fn bar(c: bar::Context) {
// .. пользовательский код ..
}
mod app {
use heapless::spsc::Queue;
use cortex_m::register::basepri;
struct Ready {
task: T,
// ..
}
/// вызываемые (`spawn`) задачи, выполняющиеся с уровнем приоритета `1`
enum T1 {
bar,
baz,
}
// очередь готовности диспетчера задач
// `U4` - целое число, представляющее собой емкость этой очереди
static mut RQ1: Queue, U4> = Queue::new();
// обработчик прерывания, выбранный для диспетчеризации задач с приоритетом `1`
#[no_mangle]
unsafe UART1() {
// приоритет данного обработчика прерывания
const PRIORITY: u8 = 1;
let snapshot = basepri::read();
while let Some(ready) = RQ1.split().1.dequeue() {
match ready.task {
T1::bar => {
// **ПРИМЕЧАНИЕ** упрощенная реализация
// используется для отслеживания динамического приоритета
let priority = Cell::new(PRIORITY);
// вызов пользовательского кода
bar(bar::Context::new(&priority));
}
T1::baz => {
// рассмотрим `baz` позднее
}
}
}
// инвариант BASEPRI
basepri::write(snapshot);
}
}
}
Вызов задачи
Интерфейс spawn предоставлен пользователю как методы структурв Spawn. Для каждой задачи существует своя структура Spawn.
Код Spawn, генерируемый фреймворком для предыдущего примера выглядит так:
#![allow(unused)]
fn main() {
mod foo {