Real-Time Interrupt-driven Concurrency — страница 21 из 23

}


}

}


// инвариант BASEPRI

basepri::write(snapshot);

}

}

}

INPUTS плюс FQ, список свободной памяти равняется эффективному пулу памяти. Однако, вместо того список свободной памяти (связный список), чтобы отслеживать пустые ячейки в буфере INPUTS, мы используем SPSC очередь; это позволяет нам уменьшить количество критических секций. На самом деле благодаря этому выбору код диспетчера задач неблокируемый.

Вместительность очереди

Фреймворк RTIC использует несколько очередей, такие как очереди готовности и списки свободной памяти. Когда список свободной памяти пуст, попытка выызова (spawn) задачи приводит к ошибке; это условие проверяется во время выполнения. Не все операции, произвожимые фреймворком с этими очередями проверяют их пустоту / наличие места. Например, возвращение ячейки списка свободной памяти (см. диспетчер задач) не проверяется, поскольку есть фиксированное количество таких ячеек циркулирующих в системе, равное вместительности списка свободной памяти. Аналогично, добавление записи в очередь готовности (см. Spawn) не проверяется, потому что вместительность очереди выбрана фреймворком.

Пользователи могут задавать вместительность программных задач; эта вместительность - максимальное количество сообщений, которые можно послать указанной задаче от задачи более высоким приоритетом до того, как spawn вернет ошибку. Эта определяемая пользователем иместительность - размер списка свободной памяти задачи (например foo_FQ), а также размер массива, содержащего входные данные для задачи (например foo_INPUTS).

Вместительность очереди готовности (например RQ1) вычисляется как сумма вместительностей всех задач, управляемх диспетчером; эта сумма является также количеством сообщений, которые очередь может хранить в худшем сценарии, когда все возможные сообщения были посланы до того, как диспетчер задач получает шанс на запуск. По этой причине получение ячейки списка свободной памяти при любой операции spawn приводит к тому, что очередь готовности еще не заполнена, поэтому вставка записи в список готовности может пропустить проверку "полна ли очередь?".

В нашем запущенном примере задача bar не принимает входных данных, поэтому мы можем пропустить проверку как bar_INPUTS, так и bar_FQ и позволить пользователю посылать неограниченное число сообщений задаче, но если бы мы сделали это, было бы невозможно превысить вместительность для RQ1, что позволяет нам пропустить проверку "полна ли очередь?" при вызове задачи baz. В разделе о очереди таймера мы увидим как список свободной памяти используется для задач без входных данных.

Анализ приоритетов

Очереди, использемые внутри интерфейса spawn, рассматриваются как обычные ресурсы и для них тоже работает анализ приоритетов. Важно заметить, что это SPSC очереди, и только один из конечных элементов становится ресурсом; другим конечным элементом владеет диспетчер задач.

Рассмотрим следующий пример:


#![allow(unused)]

fn main() {

#[rtic::app(device = ..)]

mod 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) {

// ..

}

}

}

Вот как будет проходить анализ приоритетов:

   • idle (prio = 0) и baz (prio = 2) соревнуются за конечный потребитель foo_FQ; это приводит к максимальному приоритету 2.

   • idle (prio = 0) и quux (prio = 3) соревнуются за конечный потребитель bar_FQ; это приводит к максимальному приоритету 3.

   • idle (prio = 0), baz (prio = 2) и quux (prio = 3) соревнуются за конечный производитель RQ1; это приводит к максимальному приоритету 3

Очередь таймера

Функциональность очередь таймера позволяет пользователю планировать задачи на запуск в опреленное время в будущем. Неудивительно, что эта функция также реализуется с помощью очереди: очередь приоритетов, где запланированные задачи сортируются в порядке аозрастания времени. Эта функция требует таймер, способный устанавливать прерывания истечения времени. Таймер используется для пуска прерывания, когда настает запланированное время задачи; в этот момент задача удаляется из очереди таймера и помещается в очередь готовности.

Давайте посмотрим, как это реализовано в коде. Рассмотрим следующую программу:


#![allow(unused)]

fn main() {

#[rtic::app(device = ..)]

mod app {

// ..


#[task(capacity = 2, schedule = [foo])]

fn foo(c: foo::Context, x: u32) {

// запланировать задачу на повторный запуск через 1 млн. тактов

c.schedule.foo(c.scheduled + Duration::cycles(1_000_000), x + 1).ok();

}


extern "C" {

fn UART0();

}

}

}

schedule

Давайте сначала взглянем на интерфейс schedule.


#![allow(unused)]

fn main() {

mod foo {

pub struct Schedule<'a> {

priority: &'a Cell,

}


impl<'a> Schedule<'a> {

// `unsafe` и спрятано, потому что мы не хотим, чтобы пользовать сюда вмешивался

#[doc(hidden)]

pub unsafe fn priority(&self) ->&Cell {

self.priority

}

}

}


mod app {

type Instant = ::Instant;


// все задачи, которые могут быть запланированы (`schedule`)

enum T {

foo,

}


struct NotReady {

index: u8,

instant: Instant,

task: T,

}


// Очередь таймера - двоичная куча (min-heap) задач `NotReady`

static mut TQ: TimerQueue = ..;

const TQ_CEILING: u8 = 1;


static mut foo_FQ: Queue = Queue::new();

const foo_FQ_CEILING: u8 = 1;


static mut foo_INPUTS: [MaybeUninit