Выбрать главу

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

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

#![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.

#![allow(unused)]

fn main() {

mod foo {

pub struct Schedule<'a> {

priority: &'a Cell<u8>,

}

impl<'a> Schedule<'a> {

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

#[doc(hidden)]

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

self.priority

}

}

}

mod app {

type Instant = <path::to::user::monotonic::timer as rtic::Monotonic>::Instant;

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

enum T {

foo,

}

struct NotReady {

index: u8,

instant: Instant,

task: T,

}

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

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` - владеющий укачатель на ячейки в этих буферах

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 {

// Не осталось места, чтобы разместить входные данные / instant

Err(input)

}

}

}

}

}

}

Это очень похоже на реализацию Spawn. На самом деле одни и те же буфер INPUTS и список сободной памяти (FQ) используются совместно интерфейсами spawn и schedule. Главное отличие между ними в том, что schedule также размещает Instant, момент на который задача запланирована на запуск, в отдельном буфере (foo_INSTANTS в нашем случае).

TimerQueue::enqueue_unchecked делает немного больше работы, чем просто добавление записи в min-heap: он также вызывает прерывание системного таймера (SysTick), если новая запись оказывается первой в очереди.

Прерывание системного таймера (SysTick) заботится о двух вещах: передаче задач, которых становятся готовыми из очереди таймера в очередь готовности и установке прерывания истечения времени, когда наступит запланированное время следующей задачи.

Давайте посмотрим на соответствующий код.

#![allow(unused)]

fn main() {

mod app {

#[no_mangle]

fn SysTick() {

const PRIORITY: u8 = 1;

let priority = &Celclass="underline" :new(PRIORITY);

while let Some(ready) = lock(priority, TQ_CEILING, || TQ.dequeue()) {

match ready.task {