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

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,

}


pub struct Context {

pub resources: Resources,

// ..

}

}


pub mod bar {

pub struct Resources<'a> {

pub x: &'a mut u64,

}


pub struct Context {

pub resources: Resources,

// ..

}

}


mod app {

static mut x: u64 = 0;


impl rtic::Mutex for resources::x {

type T = u64;


fn lock(&mut self, f: impl FnOnce(&mut u64) -> R) -> R {

// мы рассмотрим это детально позднее

}

}


#[no_mangle]

unsafe fn UART0() {

foo(foo::Context {

resources: foo::Resources {

x: resources::x::new(/* .. */),

},

// ..

})

}


#[no_mangle]

unsafe fn UART1() {

bar(bar::Context {

resources: bar::Resources {

x: &mut x,

},

// ..

})

}

}

}

lock

Теперь давайте рассмотрим непосредственно критическую секцию. В этом примере мы должны увеличить динамический приоритет минимум до 2, чтобы избежать гонки данных. В архитектуре Cortex-M динамический приоритет можно изменить записью в регистр BASEPRI.

Семантика регистра BASEPRI такова:

   • Запись 0 в BASEPRI отключает его функциональность.

   • Запись ненулевого значения в BASEPRI изменяет уровень приоритета, требуемого для вытеснения прерывания. Однако, это имеет эффект, только когда записываемое значение меньше, чем уровень приоритета текущего контекста выполнения, но обращаем внимание, что более низкий уровень аппаратного приоритета означает более высокий логический приоритет

Таким образом, динамический приоритет в любой момент времени может быть рассчитан как


#![allow(unused)]

fn main() {

dynamic_priority = max(hw2logical(BASEPRI), hw2logical(static_priority))

}

Где static_priority - приоритет, запрограммированный в NVIC для текущего прерывания, или логический 0, когда текущий контекств - это idle.

В этом конкретном примере мы можем реализовать критическую секцию так:

ПРИМЕЧАНИЕ: это упрощенная реализация


#![allow(unused)]

fn main() {

impl rtic::Mutex for resources::x {

type T = u64;


fn lock(&mut self, f: F) -> R

where

F: FnOnce(&mut u64) -> R,