Исключения
Исключения и прерывания — это аппаратный механизм, с помощью которого процессор обрабатывает асинхронные события и фатальные ошибки (например, выполнение недопустимой инструкции). Исключения подразумевают вытеснение (preemption) и используют обработчики исключений — подпрограммы, которые выполняются в ответ на сигнал, вызвавший событие.
Крейт cortex-m-rt предоставляет атрибут exception, чтобы объявлять обработчики исключений.
// Обработчик исключения для SysTick (системного таймера)
#[exception]
fn SysTick() {
// ..
}
За исключением атрибута exception, обработчики исключений выглядят как обычные функции, но есть одно дополнительное отличие: обработчики exception не могут быть вызваны программно. Продолжая предыдущий пример, инструкция SysTick(); приведёт к ошибке компиляции.
Такое поведение, по сути, намеренное и необходимо для одной возможности: static mut-переменные, объявленные внутри обработчиков exception, безопасно использовать.
#[exception]
fn SysTick() {
static mut COUNT: u32 = 0;
// `COUNT` превратилась в тип `&mut u32`, и её безопасно использовать
*COUNT += 1;
}
Как вы, возможно, знаете, использование static mut-переменных внутри функции делает её не реентерабельной. Вызывать нереентерабельную функцию, напрямую или косвенно, из более чем одного обработчика исключений или прерываний либо из main и одного или нескольких обработчиков исключений или прерываний — неопределённое поведение (UB).
Безопасный Rust никогда не должен приводить к неопределённому поведению, поэтому нереентерабельные функции должны помечаться как unsafe. Однако я только что сказал, что обработчики exception могут безопасно использовать static mut-переменные. Как это возможно? Это возможно потому, что обработчики exception не могут быть вызваны программно, и, значит, реентерабельность невозможна. Такие обработчики вызываются самим оборудованием, которое, как предполагается, физически не допускает параллельности.
Следовательно, в контексте обработчиков исключений во встраиваемых системах отсутствие параллельных вызовов одного и того же обработчика гарантирует отсутствие проблем с реентерабельностью, даже если обработчик использует изменяемые статические переменные.
В многоядерной системе, где несколько ядер процессора выполняют код параллельно, проблемы реентерабельности снова становятся актуальными, даже внутри обработчиков исключений. Хотя у каждого ядра может быть свой набор обработчиков исключений, всё равно возможны ситуации, когда несколько ядер пытаются выполнить один и тот же обработчик одновременно. Чтобы решить эту проблему в многоядерной среде, внутри обработчиков исключений нужно применять подходящие механизмы синхронизации, гарантирующие, что доступ к общим ресурсам корректно координируется между ядрами. Обычно для этого используют такие техники, как блокировки, семафоры или атомарные операции, чтобы предотвратить гонки данных и сохранить целостность данных.
Обратите внимание, что атрибут
exceptionпреобразует определения статических переменных внутри функции, оборачивая их в блокиunsafe, и предоставляет нам новые переменные типа&mutс тем же именем. Поэтому мы можем разыменовать ссылку с помощью*, чтобы получить значения переменных, не оборачивая их в блокunsafe.
Полный пример
Вот пример, который использует системный таймер, чтобы примерно раз в секунду вызывать исключение SysTick. Обработчик исключения SysTick подсчитывает, сколько раз он был вызван, в переменной COUNT, а затем выводит значение COUNT в консоль хоста с помощью semihosting.
ПРИМЕЧАНИЕ: вы можете запустить этот пример на любом устройстве Cortex-M; также его можно запустить на QEMU.
#![deny(unsafe_code)]
#![no_main]
#![no_std]
use panic_halt as _;
use core::fmt::Write;
use cortex_m::peripheral::syst::SystClkSource;
use cortex_m_rt::{entry, exception};
use cortex_m_semihosting::{
debug,
hio::{self, HostStream},
};
#[entry]
fn main() -> ! {
let p = cortex_m::Peripherals::take().unwrap();
let mut syst = p.SYST;
// настраивает системный таймер так, чтобы он вызывал исключение SysTick каждую секунду
syst.set_clock_source(SystClkSource::Core);
// это настроено для LM3S6965, у которого тактовая частота ЦП по умолчанию 12 МГц
syst.set_reload(12_000_000);
syst.clear_current();
syst.enable_counter();
syst.enable_interrupt();
loop {}
}
#[exception]
fn SysTick() {
static mut COUNT: u32 = 0;
static mut STDOUT: Option<HostStream> = None;
*COUNT += 1;
// Ленивая инициализация
if STDOUT.is_none() {
*STDOUT = hio::hstdout().ok();
}
if let Some(hstdout) = STDOUT.as_mut() {
write!(hstdout, "{}", *COUNT).ok();
}
// ВАЖНО: уберите этот блок `if`, если запускаете на реальном оборудовании, иначе
// отладчик окажется в несогласованном состоянии
if *COUNT == 9 {
// Это завершит процесс QEMU
debug::exit(debug::EXIT_SUCCESS);
}
}
tail -n5 Cargo.toml
[dependencies]
cortex-m = "0.5.7"
cortex-m-rt = "0.6.3"
panic-halt = "0.2.0"
cortex-m-semihosting = "0.3.1"
$ cargo run --release
Running `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb (..)
123456789
Если вы запустите это на отладочной плате Discovery, вывод появится в консоли OpenOCD. Кроме того, программа не остановится, когда счётчик дойдёт до 9.
Обработчик исключений по умолчанию
Атрибут exception на самом деле переопределяет обработчик исключения по умолчанию для конкретного исключения. Если вы не переопределяете обработчик для какого-то исключения, его обработает функция DefaultHandler, которая по умолчанию выглядит так:
fn DefaultHandler() {
loop {}
}
Эта функция предоставляется крейтом cortex-m-rt и помечена как #[no_mangle], поэтому на DefaultHandler можно поставить точку останова и перехватывать необработанные исключения.
DefaultHandler можно переопределить с помощью атрибута exception:
#[exception]
fn DefaultHandler(irqn: i16) {
// пользовательский обработчик по умолчанию
}
Аргумент irqn указывает, какое исключение обрабатывается. Отрицательное значение означает, что обрабатывается исключение Cortex-M; нулевое или положительное значение означает, что обрабатывается специфичное для устройства исключение, то есть прерывание.
Обработчик Hard Fault
Исключение HardFault немного особенное. Оно возникает, когда программа попадает в недопустимое состояние, поэтому его обработчик не может вернуть управление, так как это может привести к неопределённому поведению. Кроме того, перед вызовом пользовательского обработчика HardFault крейт рантайма выполняет некоторую работу, чтобы облегчить отладку.
В результате обработчик HardFault должен иметь такую сигнатуру: fn(&ExceptionFrame) -> !. Аргумент обработчика — это указатель на регистры, которые были помещены в стек при исключении. Эти регистры представляют собой снимок состояния процессора в момент возникновения исключения и полезны для диагностики hard fault.
Вот пример, который выполняет недопустимую операцию: чтение из несуществующей области памяти.
ПРИМЕЧАНИЕ: Эта программа не сработает, то есть не упадёт, на QEMU, потому что
qemu-system-arm -machine lm3s6965evbне проверяет загрузки из памяти и спокойно возвращает0при чтении из недопустимой памяти.
#![no_main]
#![no_std]
use panic_halt as _;
use core::fmt::Write;
use core::ptr;
use cortex_m_rt::{entry, exception, ExceptionFrame};
use cortex_m_semihosting::hio;
#[entry]
fn main() -> ! {
// читаем несуществующую область памяти
unsafe {
ptr::read_volatile(0x3FFF_0000 as *const u32);
}
loop {}
}
#[exception]
fn HardFault(ef: &ExceptionFrame) -> ! {
if let Ok(mut hstdout) = hio::hstdout() {
writeln!(hstdout, "{:#?}", ef).ok();
}
loop {}
}
Обработчик HardFault выводит значение ExceptionFrame. Если вы запустите этот код, в консоли OpenOCD вы увидите примерно такой результат:
$ openocd
(..)
ExceptionFrame {
r0: 0x3fff0000,
r1: 0x00000003,
r2: 0x080032e8,
r3: 0x00000000,
r12: 0x00000000,
lr: 0x080016df,
pc: 0x080016e2,
xpsr: 0x61000000,
}
Значение pc — это значение счётчика команд (Program Counter) в момент исключения; оно указывает на инструкцию, которая вызвала исключение.
Если посмотреть на дизассемблирование программы:
$ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex
(..)
ResetTrampoline:
8000942: movw r0, #0xfffe
8000946: movt r0, #0x3fff
800094a: ldr r0, [r0]
800094c: b #-0x4 <ResetTrampoline+0xa>
Значение счётчика команд 0x0800094a можно найти в дизассемблировании. Вы увидите, что исключение вызвала операция загрузки (ldr r0, [r0]). Поле r0 структуры ExceptionFrame покажет, что значение регистра r0 в тот момент было 0x3fff_fffe.