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

}

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<R>(&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,

},

// ..

})

}

}

}

Теперь давайте рассмотрим непосредственно критическую секцию. В этом примере мы должны увеличить динамический приоритет минимум до 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<R, F>(&mut self, f: F) -> R

where

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

{

unsafe {

// начать критическую секцию: увеличить динамический приоритет до `2`

asm!("msr BASEPRI, 192" : : : "memory" : "volatile");

// запустить пользовательский код в критической секции

let r = f(&mut x);

// окончить критическую секцию: восстановить динамический приоритет до статического значения (`1`)

asm!("msr BASEPRI, 0" : : : "memory" : "volatile");

r

}

}

}

}

В данном случае важно указать "memory" в блоке asm!. Это не даст компилятору менять местами операции вокруг него. Это важно, поскольку доступ к переменной x вне критической секции привело бы к гонке данных.

Важно отметить, что сигнатура метода lock препятствет его вложенным вызовам. Это необходимо для безопасности памяти, так как вложенные вызовы привели бы к созданию множественных уникальных ссылок (&mut-) на x, ломая правила заимствования Rust. Смотреть ниже:

#![allow(unused)]

fn main() {

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

fn foo(c: foo::Context) {

// resource proxy

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

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

res.lock(|alias: &mut u64| {

//~^ ошибка: `res` уже был заимствован уникально (`&mut-`)

// ..

});

});

}

}

Вложенные вызовы lock на том же ресурсе должны отклоняться компилятором для безопасности памяти, однако вложенные вызовы lock на разных ресурсах - нормальная операция. В этом случае мы хотим убедиться, что вложенные критические секции никогда не приведут к понижению динамического приоритета, так как это плохо, и мы хотим оптимизировать несколько записей в регистр BASEPRI и compiler fences. Чтобы справиться с этим, мы проследим динамический приоритет задачи, с помощью стековой переменной и используем ее, чтобы решить, записывать BASEPRI или нет. На практике, стековая переменная будет соптимизирована компилятором, но все еще будет предоставлять информацию компилятору.