Функциональность очередь таймера позволяет пользователю планировать задачи на запуск в опреленное время в будущем. Неудивительно, что эта функция также реализуется с помощью очереди: очередь приоритетов, где запланированные задачи сортируются в порядке аозрастания времени. Эта функция требует таймер, способный устанавливать прерывания истечения времени. Таймер используется для пуска прерывания, когда настает запланированное время задачи; в этот момент задача удаляется из очереди таймера и помещается в очередь готовности.
Давайте посмотрим, как это реализовано в коде. Рассмотрим следующую программу:
#![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 {