I2C в Embassy RP
Посмотрим, как инициализировать и использовать I2C с Embassy на Raspberry Pi Pico 2.
Блокирующий режим
Embassy предоставляет простой способ настроить I2C в блокирующем режиме:
#![allow(unused)]
fn main() {
let sda = p.PIN_16;
let scl = p.PIN_17;
info!("set up i2c");
let mut i2c = i2c::I2c::new_blocking(p.I2C0, scl, sda, Config::default());
}
Мы используем метод new_blocking, чтобы создать экземпляр I2C, который ждёт завершения каждой операции, прежде чем продолжить. Сначала выбираем периферию I2C, с которой будем работать, — I2C0 или I2C1. Выбрав периферию, нужно сопоставить её с правильными выводами GPIO для SCL и SDA.
Для конфигурации реализация по умолчанию даёт стандартную скорость обмена 100 кГц и включает внутренние подтягивающие резисторы (pull-up).
Настройка Config
Структура Config позволяет управлять поведением шины I2C. Можно изменить скорость обмена и включить или выключить внутренние подтягивающие резисторы на линиях SDA и SCL.
Чтобы увеличить скорость шины, можно изменить поле frequency:
#![allow(unused)]
fn main() {
let mut config = Config::default();
config.frequency = 400_000;
}
Если в схеме уже есть внешние подтягивающие резисторы, можно отключить внутренние:
#![allow(unused)]
fn main() {
let mut config = Config::default();
config.sda_pullup = false;
config.scl_pullup = false;
}
Отправка данных
Многим устройствам I2C нужно отправлять команды или байты конфигурации. Например, представьте, что мы настраиваем датчик и нужно записать в него два байта:
#![allow(unused)]
fn main() {
const SENSOR_ADDR: u8 = 0x68;
let config_data = [0x6B, 0x00];
i2c.write(SENSOR_ADDR, &config_data)?;
}
Здесь мы отправляем два байта устройству с адресом 0x68. Первый байт 0x6B обычно указывает устройству, в какой регистр мы пишем, а 0x00 — значение, которое нужно записать. Разные устройства используют этот шаблон по-разному, поэтому, чтобы узнать, какие байты отправлять, нужно смотреть даташит вашего устройства.
Чтение из регистра
Большинство устройств I2C хранят данные в регистрах. Чтобы прочитать конкретный регистр, используем write_read. Допустим, мы хотим считать температуру с датчика:
#![allow(unused)]
fn main() {
const TEMP_REGISTER: u8 = 0x41;
let mut buffer = [0u8; 2];
i2c.write_read(SENSOR_ADDR, &[TEMP_REGISTER], &mut buffer)?;
}
Сначала мы сообщаем устройству: «я хочу читать регистр 0x41» (это часть записи), а затем устройство отправляет нам обратно 2 байта данных о температуре (это часть чтения). Метод write_read выполняет обе операции в одной транзакции I2C. После этого в buffer будут лежать сырые байты температуры, которые затем можно преобразовать в реальное значение температуры.
Непрерывное чтение
Некоторые устройства автоматически сдвигают свой внутренний указатель и продолжают выдавать данные. В таких случаях можно использовать простой read:
#![allow(unused)]
fn main() {
let mut buffer = [0u8; 5];
i2c.read(SENSOR_ADDR, &mut buffer)?;
}
Это чтение байтов, начиная с текущей внутренней позиции устройства. Оно используется реже, чем write_read, но полезно для датчиков, которые непрерывно передают данные.
Асинхронный режим
Если мы создаём более сложное приложение, которому нужно выполнять несколько задач одновременно, можно использовать асинхронный режим. Он позволяет программе заниматься другой работой, пока операции I2C завершаются:
#![allow(unused)]
fn main() {
bind_interrupts!(struct Irqs {
I2C0_IRQ => i2c::InterruptHandler<I2C0>;
});
let mut i2c = I2c::new_async(
p.I2C0,
scl,
sda,
Irqs,
I2cConfig::default(),
);
let mut buffer = [0u8; 2];
i2c.write_read(SENSOR_ADDR, &[TEMP_REGISTER], &mut buffer).await?;
}
Режим целевого устройства (slave)
Pico также может работать как целевое устройство I2C (его ещё называют ведомым, slave), которое отвечает на запросы другого контроллера. Однако в большинстве проектов этой книги Pico используется как контроллер, который общается с датчиками и другой периферией, поэтому режим целевого устройства мы здесь не рассматриваем.