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

}

/// Детали реализации

mod app {

// неинициализированная статическая переменная

static mut x: MaybeUninit<Thing> = MaybeUninit::uninit();

#[no_mangle]

unsafe fn main() -> ! {

cortex_m::interrupt::disable();

// ..

let late = init(..);

// инициализация поздних ресурсов

x.as_mut_ptr().write(late.x);

cortex_m::interrupt::enable(); //~ compiler fence

// исключения, прерывания и задачи могут вытеснить `main` в этой точке

idle(..)

}

#[no_mangle]

unsafe fn UART0() {

foo(foo::Context {

resources: foo::Resources {

// `x` уже инициализирована к этому моменту

x: &mut *x.as_mut_ptr(),

},

// ..

})

}

}

Важная деталь здесь то, что interrupt::enable ведет себя как like a compiler fence, которое не дает компилятору пореставить запись в X после interrupt::enable. Если бы компилятор мог делать такие перестановки появились бы гонки данных между этой записью и любой операцией foo, взаимодействующей с X.

Архитектурам с более сложным конвейером инструкций нужен барьер памяти (atomic::fence) вместо compiler fence для полной очистки операции записи перед включением прерываний. Архитектура ARM Cortex-M не нуждается в барьере памяти в одноядерном контексте.

Когда ресурсы (статические переменные) разделяются между двумя или более задачами, которые выполняются с разными приоритетами, некая форма запрета изменений необходима, чтобы изменять память без гонки данных. В RTIC мы используем основанные на приоритетах критические секции, чтобы гарантировать запрет изменений (см. Протокол немедленного максимального приоритета).

Критическия секция состоит во временном увеличении динамического приоритета задачи. Пока задача находится в критической секции, все другие задачи, которые могут послать запрос переменной не могут запуститься.

Насколько большим должен быть динамический приориткт, чтобы гарантировать запрет изменений определенного ресурса? Анализ приоритетов отвечает на этот вопрос и будет обсужден в следующем разделе. В этом разделе мы сфокусируемся на реализации критической секции.

Для упрощения, давайте взглянем на ресурс, разделяемый двумя задачами, запускаемыми с разными приоритетами. Очевидно, что одна задача может вытеснить другую; чтобы предотвратить гонку данных задача с низким приоритетом должна использовать критическую секцию, когда необходимо изменять разделяемую память. С другой стороны, высокоприоритетная задача может напрямую изменять разделяемую память, поскольку не может быть вытеснена низкоприоритетной задачей. Чтобы заставить использовать критическую секцию на задаче с низким приоритетом, мы предоставляем прокси-ресурсы, в которых мы отдаем уникальную ссылку (&mut-) высокоприоритетной задаче.

Пример ниже показывает разные типы, передаваемые каждой задаче:

#![allow(unused)]

fn main() {

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

mut app {

struct Resources {

#[init(0)]

x: u64,

}

#[interrupt(binds = UART0, priority = 1, resources = [x])]

fn foo(c: foo::Context) {

// прокси-ресурс

let mut x: resources::x = c.resources.x;

x.lock(|x: &mut u64| {

// критическая секция

*x += 1

});

}

#[interrupt(binds = UART1, priority = 2, resources = [x])]

fn bar(c: bar::Context) {

let mut x: &mut u64 = c.resources.x;

*x += 1;

}

// ..

}

}

Теперь давайте посмотрим. как эти типы создаются фреймворком.

#![allow(unused)]

fn main() {

fn foo(c: foo::Context) {

// .. пользовательский код ..

}

fn bar(c: bar::Context) {

// .. пользовательский код ..

}

pub mod resources {

pub struct x {

// ..

}

}

pub mod foo {

pub struct Resources {

pub x: resources::x,