Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Исключения

Исключения и прерывания — это аппаратный механизм, с помощью которого процессор обрабатывает асинхронные события и фатальные ошибки (например, выполнение недопустимой инструкции). Исключения подразумевают вытеснение (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.