Настройка проекта
Создание проекта
Начнём с создания минимального проекта в качестве отправной точки. Для таких случаев мы предоставляем инструмент esp-generate. Его можно установить командой:
cargo install --locked esp-generate
После установки esp-generate создадим пустой проект:
esp-generate --headless --chip=esp32c3 -o unstable-hal -o embassy no_std-training
Поскольку мы работаем с отладочной платой ESP32-C3-DevKit-RUST-2, выбираем чип ESP32-C3. Мы будем использовать асинхронную поддержку через Embassy, поэтому нужны обе опции: embassy и unstable.
Теперь в текущем каталоге должен появиться каталог no_std-training/.
Структура проекта
Сгенерированный проект должен выглядеть так:
no_std-training/
├─ .cargo/
│ └─ config.toml
├─ .clippy.toml
├─ build.rs
├─ Cargo.toml
├─ rust-toolchain.toml
└─ src/
├─ bin/
│ └─ main.rs
└─ lib.rs
Рассмотрим, за что отвечает каждый из этих файлов:
.cargo/config.tomlсодержит конфигурацию Cargo.- Указана целевая платформа, в данном случае
riscv32imc-unknown-none-elf. - Переданы флаги компилятора Rust, которые принудительно включают frame pointers. Это необходимо для корректной работы бэктрейсов. Если вы не используете крейт
esp-backtrace, это не требуется. - Кроме того, мы настраиваем espflash как раннер Cargo для проекта.
- Указана целевая платформа, в данном случае
.clippy.tomlсодержит конфигурацию Clippy, линтера для кода на Rust.build.rsнастраивает компилятор Rust так, чтобы подключались необходимые скрипты компоновщика.- Это можно было бы задать и в
.cargo/config.toml, но такой подход даёт более понятные диагностические сообщения при ошибках сборки.
- Это можно было бы задать и в
Cargo.toml— манифест Cargo, в котором хранятся метаданные проекта, зависимости и другая информация.rust-toolchain.tomlзадаёт, какой тулчейн Rust использовать, а также указывает нужные компоненты и целевую платформу.src/lib.rsсодержит библиотечный код приложения. Сейчас он пуст, но будет наполнен в следующих главах.src/bin/main.rs— точка входа в приложение. Здесь будет находиться большая часть логики.
Зависимости
Сгенерированный Cargo.toml уже содержит ряд зависимостей. Их назначение кратко описано ниже:
esp-hal— слой абстракции аппаратного обеспечения (HAL). Предоставляет доступ к встроенной периферии через высокоуровневые API.esp-rtos— поддержка среды выполнения, необходимая для запуска асинхронного кода поверхesp-hal.esp-bootloader-esp-idf— поддержка загрузчика ESP-IDF.embassy-executor— асинхронный исполнитель для встраиваемых устройств. Позволяет запускать одну или несколько асинхронных задач.embassy-time— асинхронная работа со временем: задержки и таймауты.critical-section— критические секции для встраиваемых устройств.static_cell— статически выделяемая ячейка, которая инициализируется во время выполнения.
Дополнительные зависимости будут добавлены и в последующих главах.
Минимальное приложение
Файл src/bin/main.rs содержит шаблонный код минимального приложения:
#![no_std]
#![no_main]
#![deny(
clippy::mem_forget,
reason = "mem::forget is generally not safe to do with esp_hal types, especially those \
holding buffers for the duration of a data transfer."
)]
#![deny(clippy::large_stack_frames)]
use embassy_executor::Spawner;
use embassy_time::{Duration, Timer};
use esp_hal::clock::CpuClock;
use esp_hal::timer::timg::TimerGroup;
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
loop {}
}
// Создаёт дескриптор приложения по умолчанию, который требуется загрузчику ESP-IDF.
// Подробнее см.: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32c3/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!();
#[allow(
clippy::large_stack_frames,
reason = "it's not unusual to allocate larger buffers etc. in main"
)]
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config);
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);
// TODO: запустить задачи
let _ = spawner;
loop {
Timer::after(Duration::from_secs(1)).await;
}
// для вдохновения посмотрите примеры по адресу https://github.com/esp-rs/esp-hal/tree/main/examples
}
В целом код понятен, однако есть несколько интересных моментов, которые мы кратко объясним.
Application Description
Загрузчик ESP-IDF ожидает, что структура Application Description находится по определённому адресу. Если её не добавить, вы получите запутанные ошибки загрузчика.
Application Description создаётся с помощью такого вызова макроса:
#![allow(unused)]
fn main() {
esp_bootloader_esp_idf::esp_app_desc!();
}
Больше ничего делать не нужно. Подробнее см. в документации ESP-IDF.
Макрос #[esp_rtos::main]
Макрос #[esp_rtos::main] определяет точку входа приложения, обычно это функция main:
#[esp_rtos::main]
async fn main(spawner: Spawner) -> ! {
// ...
}
Этот макрос создаёт новый экземпляр исполнителя Embassy Executor и запускает функцию main как асинхронную задачу.