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

Первая попытка

Регистры

Посмотрим на периферию «SysTick» — простой таймер, который есть в каждом ядре процессора Cortex-M. Обычно такие вещи ищут в техническом описании (data sheet) производителя чипа или в техническом справочном руководстве (Technical Reference Manual), но этот пример общий для всех ядер ARM Cortex-M, поэтому заглянем в справочное руководство ARM. Там мы видим четыре регистра:

СмещениеИмяОписаниеШирина
0x00SYST_CSRРегистр управления и состояния32 бита
0x04SYST_RVRРегистр перезагрузочного значения32 бита
0x08SYST_CVRРегистр текущего значения32 бита
0x0CSYST_CALIBРегистр калибровочного значения32 бита

Подход C

В Rust мы можем представить набор регистров точно так же, как в C, — с помощью struct.

#[repr(C)]
struct SysTick {
    pub csr: u32,
    pub rvr: u32,
    pub cvr: u32,
    pub calib: u32,
}

Атрибут #[repr(C)] говорит компилятору Rust расположить эту структуру в памяти так же, как это делает компилятор C. Это очень важно, так как Rust позволяет переупорядочивать поля структуры, а C — нет. Представьте, сколько отладки потребовалось бы, если бы компилятор молча переставлял эти поля! С этим атрибутом у нас есть четыре 32-битных поля, которые соответствуют таблице выше. Но сама по себе эта struct бесполезна — нужна переменная.

let systick = 0xE000_E010 as *mut SysTick;
let time = unsafe { (*systick).cvr };

Летучие (volatile) обращения

У описанного выше подхода есть несколько проблем.

  1. Каждый раз, когда мы хотим обратиться к нашей периферии, нужно использовать unsafe.
  2. У нас нет способа указать, какие регистры доступны только для чтения, а какие — для чтения и записи.
  3. Любой фрагмент кода в любом месте программы может обратиться к аппаратуре через эту структуру.
  4. Самое важное: это на самом деле не работает…

Проблема в том, что компиляторы умные. Если вы дважды подряд запишете значение в одно и то же место ОЗУ, компилятор может это заметить и просто пропустит первую запись. В C переменные можно пометить как volatile, чтобы каждое чтение и запись выполнялись именно так, как задумано. В Rust же помечают не переменную, а обращения к ней как volatile.

let systick = unsafe { &mut *(0xE000_E010 as *mut SysTick) };
let time = unsafe { core::ptr::read_volatile(&mut systick.cvr) };

Итак, мы исправили одну из четырёх проблем, но теперь у нас ещё больше кода с unsafe! К счастью, есть сторонний крейт, который может помочь — volatile_register.

use volatile_register::{RW, RO};

#[repr(C)]
struct SysTick {
    pub csr: RW<u32>,
    pub rvr: RW<u32>,
    pub cvr: RW<u32>,
    pub calib: RO<u32>,
}

fn get_systick() -> &'static mut SysTick {
    unsafe { &mut *(0xE000_E010 as *mut SysTick) }
}

fn get_time() -> u32 {
    let systick = get_systick();
    systick.cvr.read()
}

Теперь volatile-обращения выполняются автоматически через методы read и write. Запись по-прежнему требует unsafe, но справедливости ради скажем, что аппаратура — это набор изменяемого состояния, и у компилятора нет способа узнать, безопасна ли такая запись на самом деле, поэтому это хорошая позиция по умолчанию.

Удобная обёртка на Rust

Нам нужно обернуть эту struct в API более высокого уровня, который пользователи смогут безопасно вызывать. Как автор драйвера, мы вручную проверяем, что код с unsafe корректен, а затем предоставляем пользователям безопасный API, чтобы им не нужно было об этом беспокоиться (при условии, что они доверяют нам и мы всё сделали правильно!).

Один из вариантов может выглядеть так:

use volatile_register::{RW, RO};

pub struct SystemTimer {
    p: &'static mut RegisterBlock
}

#[repr(C)]
struct RegisterBlock {
    pub csr: RW<u32>,
    pub rvr: RW<u32>,
    pub cvr: RW<u32>,
    pub calib: RO<u32>,
}

impl SystemTimer {
    pub fn new() -> SystemTimer {
        SystemTimer {
            p: unsafe { &mut *(0xE000_E010 as *mut RegisterBlock) }
        }
    }

    pub fn get_time(&self) -> u32 {
        self.p.cvr.read()
    }

    pub fn set_reload(&mut self, reload_value: u32) {
        unsafe { self.p.rvr.write(reload_value) }
    }
}

pub fn example_usage() -> String {
    let mut st = SystemTimer::new();
    st.set_reload(0x00FF_FFFF);
    format!("Время сейчас 0x{:08x}", st.get_time())
}

Проблема этого подхода в том, что следующий код для компилятора совершенно допустим:

fn thread1() {
    let mut st = SystemTimer::new();
    st.set_reload(2000);
}

fn thread2() {
    let mut st = SystemTimer::new();
    st.set_reload(1000);
}

Аргумент &mut self функции set_reload проверяет, что на эту конкретную структуру SystemTimer нет других ссылок, но они не мешают пользователю создать вторую SystemTimer, которая указывает на ту же самую периферию! Код, написанный таким образом, будет работать, если автор достаточно внимателен, чтобы заметить все такие «дублирующиеся» экземпляры драйвера. Но как только код распределяется по нескольким модулям, драйверам, разработчикам и дням, такие ошибки становится всё легче допустить.