Первая попытка
Регистры
Рассмотрим периферийное устройство 'SysTick' — простой таймер, который поставляется с каждым процессорным ядром Cortex-M. Обычно вы ищете информацию об этом в техническом описании микросхемы или Справочном руководстве, но данный пример общий для всех ядер ARM Cortex-M, поэтому обратимся к [Справочному руководству ARM]. Мы видим, что есть четыре регистра:
| Смещение | Имя | Описание | Ширина |
|---|---|---|---|
| 0x00 | SYST_CSR | Регистр управления и состояния | 32 бита |
| 0x04 | SYST_RVR | Регистр значения перезагрузки | 32 бита |
| 0x08 | SYST_CVR | Регистр текущего значения | 32 бита |
| 0x0C | SYST_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 };
Волатильные доступы
В приведенном выше подходе есть несколько проблем.
- Нам приходится использовать
unsafeкаждый раз, когда мы хотим получить доступ к нашему периферийному устройству. - У нас нет способа указать, какие регистры предназначены только для чтения, а какие — для чтения и записи.
Чтобы решить эти проблемы, мы можем использовать крейт volatile-register, который предоставляет типы RO (только для чтения), WO (только для записи) и RW (чтение/запись). Это позволяет нам определить, какие операции безопасны, и избежать случайной записи в регистр только для чтения.
Кроме того, нам нужно использовать волатильные операции для доступа к памяти, чтобы гарантировать, что компилятор не оптимизирует наши операции чтения или записи. Это достигается с помощью методов read и write из крейта volatile-register.
Обертка в стиле Rust
Нам нужно обернуть эту struct в API более высокого уровня, который безопасен для вызова пользователями. Как автор драйвера, мы вручную проверяем, что небезопасный код корректен, а затем предоставляем безопасный 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!("Time is now 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, который указывает на то же самое периферийное устройство! Код, написанный в таком стиле, будет работать, если автор достаточно внимателен, чтобы заметить все эти "дублирующиеся" экземпляры драйвера, но как только код распространяется по нескольким модулям, драйверам, разработчикам и дням, такие ошибки становятся все проще совершать.