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

Этот пример дает представление о коде, запускаемом фреймворком RTIC:


#![allow(unused)]

fn main() {

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

mod app {

#[init]

fn init(c: init::Context) {

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

}


#[idle]

fn idle(c: idle::Context) -> ! {

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

}


#[interrupt(binds = UART0, priority = 2)]

fn foo(c: foo::Context) {

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

}

}

}

Фреймворк генерирует точку входа в программу, которая выглядит примерно так:

// настоящая точку входа в программу

#[no_mangle]

unsafe fn main() -> ! {

// преобразует логические приоритеты в аппаратные / NVIC приоритеты

fn logical2hw(priority: u8) -> u8 {

use lm3s6965::NVIC_PRIO_BITS;


// NVIC кодирует приоритеты верхними битами

// большие значения обозначают меньший приоритет

((1 << NVIC_PRIORITY_BITS) - priority) << (8 - NVIC_PRIO_BITS)

}


cortex_m::interrupt::disable();


let mut core = cortex_m::Peripheral::steal();


core.NVIC.enable(Interrupt::UART0);


// значение, определенное пользователем

let uart0_prio = 2;


// проверка на этапе компиляции, что определенный приоритет входит в поддерживаемый диапазон

let _ = [(); (1 << NVIC_PRIORITY_BITS) - (uart0_prio as usize)];


core.NVIC.set_priority(Interrupt::UART0, logical2hw(uart0_prio));


// вызов пользовательского кода

init(/* .. */);


// ..


cortex_m::interrupt::enable();


// вызов пользовательского кода

idle(/* .. */)

}

Нереентерабельность

В RTIC задачи-обработчики не могут использоваться повторно. Переиспользование задачи-обработчика может сломать правила заимствования Rust и привести к неопределенному поведению. Задача-обработчик теоретически может быть переиспользована одним из двух способов: программно или аппаратно.

Программно

Чтобы переиспользовать задачу-обработчик программно, назначенный ей обработчик прерывания должен быть вызван с помощью FFI (смотрите пример ниже). FFI требует unsafe код, что уменьшает желание конечных пользователей вызывать обработчик прерывания.


#![allow(unused)]

fn main() {

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

mod app {

#[init]

fn init(c: init::Context) { .. }


#[interrupt(binds = UART0)]

fn foo(c: foo::Context) {

static mut X: u64 = 0;


let x: &mut u64 = X;


// ..


//~ `bar` может вытеснить `foo` в этом месте


// ..

}


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

fn bar(c: foo::Context) {

extern "C" {

fn UART0();

}


// этот обработчик прерывания вызовет задачу-обработчик `foo`, что сломает

// ссылку на статическую переменную `X`

unsafe { UART0() }

}

}

}

Фреймворк RTIC должен сгенерировать код обработчика прерывания, который вызывает определенные пользователем задачи-обработчики. Мы аккуратны в том, чтобы обеспечить невозможность вызова этих обработчиков из пользовательского кода.

Пример выше раскрывается в:


#![allow(unused)]

fn main() {

fn foo(c: foo::Context) {

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

}


fn bar(c: bar::Context) {

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

}


mod app {

// все в этом блоке невидимо для пользовательского кода


#[no_mangle]

unsafe fn USART0() {

foo(..);

}


#[no_mangle]

unsafe fn USART1() {

bar(..);

}

}

}

Аппаратно

Обработчик прерывания также может быть вызван без программного вмешательства. Это может произойти, если один обработчик будет назначен двум или более прерываниям в векторе прерываний, но синтаксиса для такого рода функциональности в RTIC нет.

Контроль доступа

Одна из основ RTIC - контроль доступа. Контроль того, какая часть программы может получить доступ к какой статической переменной - инструмент обеспечения безопасности памяти.

Статические переменные используются для разделения состояний между обработчиками прерываний, или между обработчиком прерывания и нижним контекстом выполнения, main. В обычном Rust коде трудно обеспечить гранулированный контроль за тем, какие функции могут получать доступ к статическим переменным, поскольку к статическим переменным можно получить доступ из любой функции, находящейся в той же области видимости, в которой они определены. Модули дают частичный контроль над доступом к статическим переменным, но они недостаточно гибкие.

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

Затем доступ к ресурсам предоставляется каждой задаче с помощью структуры Resources, чьи поля соответствуют ресурсам, к которым получает доступ задача. Есть лишь одна такая структура на задачу и структура Resources инициализируется либо уникальной ссылкой (&mut-) на статическую переменную, либо с помощью прокси-ресурса (см. раздел критические секции).

Код ниже - пример разных трансформаций структуры кода, происходящих за сценой:


#![allow(unused)]

fn main() {

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

mod app {

static mut X: u64: 0;

static mut Y: bool: 0;


#[init(resources = [Y])]

fn init(c: init::Context) {

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

}


#[interrupt(binds = UART0, resources = [X])]