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

Упражнение: драйвер I²C, простая версия

Мы не будем писать драйвер целиком, только первый шаг: «hello world» написания драйверов, то есть чтение идентификатора устройства датчика. Эта версия называется простой, потому что мы объясняем фрагменты кода, а вам нужно лишь скопировать их в нужное место. Используйте эту версию, если у вас мало опыта работы с Rust, если этот практикум — ваш первый во встраиваемой области или если сложная версия оказалась для вас слишком трудной. Вы можете работать в одном и том же файле с любой из версий.

i2c-driver/src/icm42670p.rs — это текст с пропусками, представляющий собой очень простой драйвер IMU-датчика I²C. Задача — дописать файл так, чтобы запуск main.rs выводил в лог идентификатор устройства драйвера.

i2c-driver/src/icm42670p_solution.rs содержит решение этого упражнения. Чтобы его запустить, нужно поменять импорты в main.rs и lib.rs. Импорты уже есть: достаточно закомментировать текущие импорты и раскомментировать решения, отмеченные комментариями в строках.

API драйвера

Экземпляр датчика

Чтобы работать с периферийным датчиком, сначала нужно получить его экземпляр. Датчик представлен структурой, которая содержит и адрес устройства, и объект, представляющий саму шину I²C. Для этого используются трейты, определённые в крейте embedded-hal. Структура публичная, потому что к ней нужен доступ извне крейта, но её поля приватные.

#![allow(unused)]
fn main() {
#[derive(Debug)]
pub struct ICM42670P<I2C> {
    // The concrete I²C device implementation.
    i2c: I2C,

    // Device address
    address: DeviceAddr,
}
}

Добавляем блок impl, который будет содержать все методы, доступные для экземпляра датчика. В нём также определяется обработка ошибок. В этом же блоке реализуем метод создания экземпляра. Методы могут быть публичными или приватными. Этот метод должен быть доступен извне, поэтому он помечен как pub. Обратите внимание: при такой записи экземпляр датчика становится владельцем шины I²C.

#![allow(unused)]
fn main() {
impl<I2C, E> ICM42670P<I2C>
where
    I2C: i2c::WriteRead<Error = E> + i2c::Write<Error = E>,
{
    /// Creates a new instance of the sensor, taking ownership of the i2c peripheral.
    pub fn new(i2c: I2C, address: DeviceAddr) -> Result<Self, E> {
        Ok(Self { i2c, address })
    }
// ...
}

Адрес устройства

  • Адреса устройства доступны в коде:
#![allow(unused)]
fn main() {
pub enum DeviceAddr {
    /// 0x68
    AD0 = 0b110_1000,
    /// 0x69
    AD1 = 0b110_1001,
}
}
  • У этого устройства I²C два возможных адреса: 0x68 и 0x69. Мы указываем устройству, какой адрес использовать, подавая на вывод AP_AD0 либо 0V, либо 3.3V. При 0V устройство отвечает на адрес 0x68, при 3.3V — на 0x69. Поэтому вывод AP_AD0 можно считать однобитным входом, который задаёт младший бит адреса устройства. Подробнее — в даташите, раздел 9.3.

Представление регистров

Регистры датчика представлены перечислениями. Каждый вариант имеет адрес регистра в качестве значения. Тип Register реализует метод, который возвращает адрес варианта.

#![allow(unused)]
fn main() {
#[derive(Clone, Copy)]
pub enum Register {
    WhoAmI = 0x75,
}

impl Register {
    fn address(&self) -> u8 {
        *self as u8
    }
}

}

read_register() и write_register()

Определим методы чтения и записи на основе методов, которые предоставляет крейт embedded-hal. Они служат вспомогательными средствами для более конкретных методов и абстракцией, адаптированной под датчик с 8-битными регистрами. Обратите внимание, что метод read_register() основан на методе write_read(). Причина — в особенностях протокола I²C: сначала нужно записать по шине команду, которая указывает, из какого регистра мы хотим читать. Вспомогательные методы могут оставаться приватными, так как им не нужен доступ извне крейта.

#![allow(unused)]
fn main() {
impl<I2C, E> ICM42670P<I2C>
where
    I2C: i2c::WriteRead<Error = E> + i2c::Write<Error = E>,
{
    /// Creates a new instance of the sensor, taking ownership of the i2c peripheral.
    pub fn new(i2c: I2C, address: DeviceAddr) -> Result<Self, E> {
        Ok(Self { i2c, address })
    }
    // ...
    /// Writes into a register
    // This method is not public as it is only needed inside this file.
    #[allow(unused)]
    fn write_register(&mut self, register: Register, value: u8) -> Result<(), E> {
        let byte = value;
        self.i2c
            .write(self.address as u8, &[register.address(), byte])
    }

    /// Reads a register using a `write_read` method.
    // This method is not public as it is only needed inside this file.
    fn read_register(&mut self, register: Register) -> Result<u8, E> {
        let mut data = [0];
        self.i2c
            .write_read(self.address as u8, &[register.address()], &mut data)?;
        Ok(u8::from_le_bytes(data))
    }
}

✅ Реализуйте публичный метод, который читает регистр WhoAmI по адресу 0x75. Используйте метод read_register(), описанный выше.

✅ Необязательно: реализуйте дополнительные методы, которые добавляют драйверу возможности. Адреса и описание соответствующих регистров смотрите в документации. 💡 Несколько идей:

  • включение гироскопа или акселерометра;
  • запуск измерений;
  • чтение измерений.

🔎 Общие сведения о регистрах периферии

Регистры могут иметь разный смысл. По сути, это ячейка, которая может хранить значение.

В данном случае мы работаем с внешним устройством (поскольку это датчик, даже если он на той же печатной плате). Оно адресуется по I2C, и мы читаем и записываем его адреса регистров. Каждый адрес указывает на уникальную ячейку, которая содержит какую-то информацию. В данном случае нам нужен адрес ячейки, в которой хранится текущая температура, измеренная датчиком.

Карту регистров ICM-42670 можно найти в разделе 14, если вы хотите получить из этого датчика другие интересные данные.