На главную > Блог > Категория > 🧠 Как написать нейросеть для ESP32 на Rust
Аннотация. В этой статье мы пройдём полный путь создания нейросетевого приложения для ESP32-S3 на Rust: от выбора архитектуры и компиляции модели до развёртывания на реальном железе и отладки. Всё — с подробным кодом, готовым к использованию.
Разработка под микроконтроллеры — это всегда игра в «уложится или не уложится» в жёсткие бюджетные рамки: килобайты RAM, мегагерцы частоты, милливатты питания. Rust здесь даёт уникальное преимущество: безопасность без сборщика мусора и контроль над памятью на этапе компиляции. Для ESP32-S3 это особенно актуально: 512 КБ внутренней SRAM — это не место для экспериментов с Box и Vec в горячем цикле. Rust-компилятор не даст вам случайно выйти за пределы кучи или забыть обработать ошибку.
cargo check, при условии, что модель остаётся в правильной экосистеме (esp-hal 1.x).
Первый и самый важный вопрос: как именно ваша нейросеть будет исполняться на ESP32? Существует два принципиально разных подхода.
Самый распространённый способ — использовать TensorFlow Lite Micro. Это проверенная, хорошо документированная библиотека, которая интерпретирует модель на лету. Плюсы: гибкость, поддерживается множество операторов, можно менять модель без перекомпиляции прошивки. Минусы: интерпретатор, его операторный реестр и внутренняя бухгалтерия конкурируют за ту же память с тензорным ареной. На ESP32-S3 с его 512 КБ SRAM это критично.
Альтернативный, гораздо более эффективный подход — разрешить граф вычислений на этапе компиляции. Его используют проекты вроде MicroFlow и edgedl. Суть: модель парсится процедурным макросом во время сборки, структура нейросети превращается в статический Rust-код. В рантайме нет ни интерпретатора, ни динамического выделения памяти, ни диспетчеризации — только последовательный вызов заранее скомпилированных функций для каждого слоя. Это даёт максимальную производительность и минимальное потребление памяти.
| Компонент | Крейт / Проект | Назначение |
|---|---|---|
| HAL | esp-idf-hal или esp-hal | Работа с GPIO, SPI, I2C, UART, таймерами |
| Wi-Fi / HTTP | esp-idf-svc + embedded-svc | Подключение к сети и HTTP-запросы |
| Инференс (компиляторный подход) | edgedl или microflow | Выполнение INT8-моделей без кучи |
| Сериализация | serde_json + alloc | no_std-совместимая работа с JSON |
| Дисплей | embedded-graphics + mipidsi | Отрисовка интерфейса на AMOLED |
| Сенсорный экран | I2C-драйвер (поверх esp-idf-hal) | Обработка касаний |
Для начала работы с ESP32-S3 под Rust выполните инструкцию из официальной книги:
# Установка espup
cargo install espup
espup install
# Настройка переменных окружения
source $HOME/export-esp.sh
# Создание шаблонного проекта
cargo generate --git https://github.com/esp-rs/esp-idf-template cargo
При создании проекта выберите esp32s3 в качестве чипа и укажите использование esp-idf-hal.
Для ESP32-S3 существует три класса моделей по размеру и формату:
edgedl поддерживает симметричное INT8-квантование с per-tensor и per-channel шкалами, что даёт гибкость без потери точности.
Большинство существующих проектов используют формат .tflite (TensorFlow Lite) как промежуточное звено. Для экспорта INT8-модели необходимо:
.espdl для edgedl или особый бинарный блоб для microflow).Рекомендация для старта: Проще всего начать с проекта Nano-U. Его авторы предоставляют не только обученную INT8-модель, но и полный pipeline для её запуска на ESP32-S3, включая build.rs скрипт, который запаковывает веса прямо в прошивку на этапе компиляции.
// build.rs (фрагмент из Nano-U)
// Запаковываем модель в бинарник
let model_path = env::var("MODELS_DIR").unwrap();
// ... парсим .tflite и _quant_params.json ...
// Генерируем Rust-код с весами
Это избавляет от необходимости реализовывать загрузчик моделей с SD-карты или по Wi-Fi на начальном этапе.
Если ваша задача — не распознавание жестов или сегментация изображений, а общение с пользователем на естественном языке, то инференс LLM прямо на ESP32 — это заведомо проигрышная стратегия. Даже самая маленькая языковая модель (TinyStories-33M) требует ~132 МБ только для весов в FP32. У вас есть 512 КБ SRAM и 8 МБ PSRAM.
Золотое правило: Микроконтроллер — это не мозг, а рефлекторная дуга. Он отвечает за быструю классификацию, ввод/вывод и локальный кеш. Тяжёлая генерация — удел сервера.
Вот как должна выглядеть архитектура:
Ваш ESP32-S3 оснащён 1.64-дюймовым AMOLED-экраном (QSPI) и ёмкостным сенсором (I2C). Это не просто «красивая картинка», а основной интерфейс взаимодействия.
В Cargo.toml добавьте:
[dependencies]
embedded-graphics = "0.8"
mipidsi = "0.8" # Для AMOLED через QSPI
Инициализация дисплея на базе esp-idf-hal:
use esp_idf_hal::prelude::*;
use esp_idf_hal::spi::SpiDeviceDriver;
use mipidsi::Display;
fn init_display(spi: SpiDeviceDriver) -> Display<SpiDeviceDriver, mipidsi::models::ILI9341> {
Display::new(spi, Some(dc_pin), Some(cs_pin), Some(rst_pin))
.init()
.unwrap()
}
Для вывода текста используйте embedded-graphics с поддержкой шрифтов. Минимальный интерфейс — это два поля: вопрос пользователя (сверху) и ответ бота (снизу).
// Отображение статуса "Думаю..."
display.clear(Rgb565::BLACK);
display.draw_circle(120, 120, 20, Rgb565::YELLOW);
display.draw_text(10, 220, "Бот печатает...", Rgb565::GRAY);
Сенсорный экран по I2C — это простой метод получения координат касания. Его можно использовать для:
Серверная часть принимает запросы от ESP32, ищет в базе данных и возвращает ответ. Используем actix-web и sqlx для MySQL.
use actix_web::{post, web, HttpResponse, Responder};
use serde::Deserialize;
use sqlx::MySqlPool;
#[derive(Deserialize)]
struct AskRequest {
text: String,
}
#[post("/ask")]
async fn ask(pool: web::Data<MySqlPool>, req: web::Json<AskRequest>) -> impl Responder {
let result = sqlx::query!(
r#"
SELECT answer FROM knowledge_base
WHERE MATCH(question) AGAINST(? IN BOOLEAN MODE)
LIMIT 1
"#,
req.text
)
.fetch_optional(pool.get_ref())
.await;
match result {
Ok(Some(record)) => HttpResponse::Ok().json(record.answer),
Ok(None) => HttpResponse::NotFound().body("Я ещё учусь."),
Err(e) => HttpResponse::InternalServerError().body(format!("Ошибка: {}", e)),
}
}
Фрагмент main.rs для ESP32-S3, реализующий подключение к Wi-Fi, отправку HTTP-запроса на сервер и печать ответа в консоль (UART).
#![no_std]
#![no_main]
use esp_idf_hal::prelude::*;
use esp_idf_svc::http::client::EspHttpClient;
use esp_idf_svc::wifi::EspWifi;
use embedded_svc::http::client::Client;
use embedded_svc::wifi::*;
use serde_json::json;
#[entry]
fn main() -> anyhow::Result<()> {
esp_idf_svc::sys::link_patches();
let peripherals = Peripherals::take().unwrap();
let sys_loop = EspSystemEventLoop::take()?;
// 1. Wi-Fi
let mut wifi = EspWifi::new(peripherals.modem, sys_loop, None)?;
wifi.set_configuration(&Configuration::Client(ClientConfiguration {
ssid: "MY_WIFI".try_into().unwrap(),
password: "MY_PASS".try_into().unwrap(),
..Default::default()
}))?;
wifi.start()?;
wifi.connect()?;
wifi.wait_netif_up()?;
// 2. HTTP-клиент
let client = EspHttpClient::new(&wifi)?;
// 3. Запрос к серверу
let request_body = json!({ "text": "Как сделать шашлык?" }).to_string();
let request = client.post("http://192.168.1.10:8080/ask")?
.header("Content-Type", "application/json")
.body(&request_body)?;
let response = request.submit()?;
let status = response.status();
if status.is_success() {
let bytes = response.body()?;
let answer = core::str::from_utf8(&bytes)?;
println!("Ответ: {}", answer);
} else {
println!("Ошибка: {}", status);
}
loop {}
}
Отладка на микроконтроллерах всегда сложнее, чем на сервере. Есть несколько проверенных методов:
println! или log::info! для вывода отладочных сообщений. Подключите ESP32 через USB-конвертер и смотрите вывод в screen или minicom.log::info! по UART — это не ностальгия, это общее доказательство между разработчиком и ассистентом.
Мы рассмотрели полный цикл создания нейросетевого устройства на базе ESP32-S3 и Rust. Ключевые выводы:
edgedl). Избегайте интерпретаторов на устройстве.edgedl или microflow примера.Удачи! И помните: даже самые сложные системы строятся из простых, правильно скомпонованных частей.
Дата размещения статьи: 06-08-2026 в 16:15:12