Чтение данных с датчика
В этой главе мы инициализируем плату, настроим шину I2C и будем считывать температуру и влажность с датчика SHTC3 на отладочной плате ESP32-C3-DevKit-RUST-2. Каждое успешное измерение выводится с помощью крейта log, а ошибки датчика сообщаются сообщениями уровня error.
Готовый код для этой главы находится в project/part1/.
Инициализация
В начале приложения настраивается логирование, конфигурируется чип и запускается поддержка среды выполнения, которая нужна остальной части приложения. Мы рассмотрим каждый шаг отдельно.
Обработчик паники
В приложении no_std необходимо предоставить обработчик паники. В минимальном приложении из предыдущей главы использовался ручной #[panic_handler], но в этой главе мы будем использовать esp-backtrace:
use esp_backtrace as _;
Feature panic-handler крейта esp-backtrace предоставляет обработчик паники за нас, а также даёт полезную поддержку бэктрейсов, когда что-то идёт не так.
Логгер
Сначала инициализируем логгер:
esp_println::logger::init_logger_from_env();
Это позволяет использовать стандартные макросы log. В этой главе мы используем info! для вывода успешных показаний температуры и влажности, а error! — для сообщений о сбоях при запуске или чтении измерения.
init_logger_from_env() настраивает логирование по переменной окружения ESP_LOG. Формат значения похож на формат фильтров RUST_LOG из env_logger. Переменную можно задать при сборке или запуске приложения:
ESP_LOG=info cargo run --release
Распространённые глобальные уровни: error, warn, info, debug, trace и off. Например, ESP_LOG=warn показывает только предупреждения и ошибки, а ESP_LOG=debug включает также сообщения уровня debug.
Уровень логирования по умолчанию можно задать и в .cargo/config.toml:
[env]
ESP_LOG = "info"
С такой конфигурацией cargo run --release будет использовать ESP_LOG=info, если вы не переопределите значение в оболочке.
Также можно фильтровать отдельные модули:
ESP_LOG=warn,no_std_training=info cargo run --release
Эта команда включает логи уровня info для нашего приложения, а остальные модули оставляет на уровне warn.
esp_hal::init
esp_hal::Config описывает системную конфигурацию, которая применяется при инициализации HAL. Это тип с атрибутом non_exhaustive, поэтому мы создаём его с помощью Config::default(), а затем изменяем нужные параметры. В нашем приложении мы используем with_cpu_clock(CpuClock::max()), чтобы запросить максимальную тактовую частоту ЦП:
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
esp_hal::init применяет эту конфигурацию. Он настраивает тактовую частоту ЦП и сторожевой таймер (watchdog), а затем возвращает периферию и тактовые сигналы, необходимые HAL.
Значение, важное для следующих шагов, — peripherals. Оно содержит отдельное значение Rust для каждого аппаратного блока и каждого вывода. Когда мы позже создадим драйверы, мы передадим эти значения в конструкторы драйверов. Именно передача владения позволяет esp-hal предотвращать случайное двойное использование оборудования: как только peripherals.I2C0 будет передан в драйвер I2C (см. ниже), никакой другой драйвер не сможет использовать тот же блок I2C.
esp_rtos::start
Приложение использует асинхронный Rust через Embassy. esp_rtos::start запускает планировщик, который используют esp-rtos и Embassy. Ему нужны аппаратный таймер и программное прерывание:
let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_interrupt =
esp_hal::interrupt::software::SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start(timg0.timer0, sw_interrupt.software_interrupt0);
Таймер используется для асинхронных операций, основанных на времени, таких как Timer::after(...). Программное прерывание используется средой выполнения, чтобы пробуждать задачи.
Драйверы
Далее создаём драйвер шины I2C для датчика:
let sda = peripherals.GPIO10;
let scl = peripherals.GPIO8;
let i2c = I2c::new(peripherals.I2C0, Config::default())
.expect("Failed to create I2C bus")
.with_sda(sda)
.with_scl(scl)
.into_async();
let mut sht = shtc3(i2c);
I2c::new принимает владение периферией I2C и её конфигурацией. Плата ESP32-C3-DevKit-RUST-2 подключает датчик SHTC3 к GPIO10 (SDA) и GPIO8 (SCL), поэтому мы подключаем эти выводы с помощью методов with_*.
Многие драйверы esp-hal следуют такому шаблону:
- Создать драйвер с аппаратной периферией и конфигурацией.
- Подключить выводы или дополнительные настройки с помощью методов
with_*, напримерwith_sdaиwith_scl. - Выбрать режим выполнения.
Последний вызов, into_async, преобразует драйвер I2C в асинхронный режим. Это позволяет драйверу датчика использовать асинхронные операции I2C, а наша задача может ожидать завершения передач вместо активного ожидания (busy-waiting).
Такое разделение на блокирующий и асинхронный режимы характерно для многих драйверов esp-hal. Типы драйверов параметризуются режимом, а драйверы с поддержкой асинхронности после преобразования используют маркер режима Async. Конкретный способ настройки зависит от периферии, но принцип один и тот же: создать драйвер, настроить его, а затем перевести в асинхронный режим, если он будет использоваться из асинхронного кода.
Наконец, shtc3(i2c) оборачивает шину I2C в драйвер датчика из крейта shtcx2.
Чтение датчика
SHTC3 — датчик температуры и влажности, которым управляют по I2C. В документации SHTC3 описаны набор команд, режимы измерения, требования к времени и формулы преобразования. Реализовывать эти детали вручную нам не нужно: крейт shtcx2 отправляет команды и преобразует необработанные значения.
Приложению всё равно нужно соблюдать последовательность измерения датчика. Показания не возвращаются одной транзакцией I2C; вместо этого мы:
- Запускаем измерение у датчика.
- Ждём, пока измерение завершится.
- Читаем и преобразуем результат.
В коде это выглядит так:
// Read sensor
if let Err(e) = sht.start_measurement(PowerMode::NormalMode).await {
error!("Failed to start measurement: {:?}", e);
Timer::after(Duration::from_secs(1)).await;
continue;
}
// Wait for the maximum measurement duration reported by the sensor driver.
let duration = max_measurement_duration(&sht, PowerMode::NormalMode);
Timer::after(Duration::from_micros(duration.into())).await;
let measurement = match sht.get_measurement_result().await {
Ok(m) => m,
Err(e) => {
error!("Failed to get measurement result: {:?}", e);
Timer::after(Duration::from_secs(1)).await;
continue;
}
};
Мы используем PowerMode::NormalMode — режим с более высокой точностью измерений. После start_measurement(...) SHTC3 занят внутренним преобразованием температуры и влажности. Если попытаться прочитать результат сразу, датчик может ещё не подтвердить чтение по I2C.
Существует несколько способов, которыми датчик может сообщить о готовности измерения. Некоторые датчики имеют вывод прерывания data-ready. SHTC3 на этой плате — 4-контактное I2C-устройство (VDD, SCL, SDA и VSS), поэтому у него нет вывода прерывания, на который наш пример мог бы ждать. SHTC3 также поддерживает clock stretching, но команды измерения, которые использует этот драйвер, его не включают. Поэтому мы ждём максимальную длительность измерения, которую возвращает max_measurement_duration(...), прежде чем пытаться прочитать результат.
Это ожидание отделено от асинхронных передач по I2C. Асинхронный I2C позволяет задаче уступить управление, пока отправляются или принимаются команды I2C; Timer::after(...).await позволяет задаче уступить управление, пока датчик выполняет измерение. В обоих случаях исполнитель может выполнять другую работу, а не простаивать в активном ожидании.
Возвращаемое измерение предоставляет типизированные значения температуры и влажности:
info!(
" {:.2} °C | {:.2} %RH",
measurement.temperature.as_degrees_celsius(),
measurement.humidity.as_percent(),
);
Цикл повторяет эту последовательность раз в секунду и выводит в лог последнее показание.
Запуск приложения
Соберите и прошейте приложение из project/part1/:
cargo run --release
Эта команда делает два действия. Сначала Cargo собирает приложение в режиме release для настроенной целевой платформы ESP32-C3. Затем Cargo запускает раннер, специфичный для целевой платформы, из .cargo/config.toml:
[target.riscv32imc-unknown-none-elf]
runner = "espflash flash --monitor --chip esp32c3"
Раннер вызывает espflash, чтобы прошить бинарный файл на плату. Флаг --monitor оставляет монитор последовательного порта открытым после прошивки, поэтому логи устройства выводятся в том же терминале.
Как только приложение запустится, оно инициализирует плату, настроит асинхронный драйвер I2C и будет многократно считывать датчик SHTC3. На каждом цикле выводится последнее измерение температуры и относительной влажности.
Вы должны увидеть примерно такой вывод:
INFO - 24.31 °C | 42.18 %RH
INFO - 24.30 °C | 42.21 %RH
INFO - 24.32 °C | 42.20 %RH
На этом этапе приложение умеет инициализировать плату, настраивать асинхронный драйвер I2C и считывать данные датчика. В следующей главе мы подключим устройство к Wi-Fi, чтобы эти измерения можно было отправить с платы.