Упражнение: драйвер 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, если вы хотите получить из этого датчика другие интересные данные.