🧠 Как написать нейросеть для ESP32 на Rust


На главную > Блог > Категория > 🧠 Как написать нейросеть для ESP32 на Rust

книги по трейдингу

Как написать нейросеть для ESP32 на Rust

Аннотация. В этой статье мы пройдём полный путь создания нейросетевого приложения для ESP32-S3 на Rust: от выбора архитектуры и компиляции модели до развёртывания на реальном железе и отладки. Всё — с подробным кодом, готовым к использованию.


1. Введение: Почему Rust и ESP32 — идеальная пара

Разработка под микроконтроллеры — это всегда игра в «уложится или не уложится» в жёсткие бюджетные рамки: килобайты RAM, мегагерцы частоты, милливатты питания. Rust здесь даёт уникальное преимущество: безопасность без сборщика мусора и контроль над памятью на этапе компиляции. Для ESP32-S3 это особенно актуально: 512 КБ внутренней SRAM — это не место для экспериментов с Box и Vec в горячем цикле. Rust-компилятор не даст вам случайно выйти за пределы кучи или забыть обработать ошибку.

💡 Заметка. Rust-компилятор ловит множество ошибок, которые иначе проявились бы только на устройстве. Это отлично сочетается с AI-генерируемым кодом: вы получаете быструю итерацию в редакторе и надёжную обратную связь от cargo check, при условии, что модель остаётся в правильной экосистеме (esp-hal 1.x).

2. Архитектурный выбор: Компиляция против интерпретации

Первый и самый важный вопрос: как именно ваша нейросеть будет исполняться на ESP32? Существует два принципиально разных подхода.

2.1. Интерпретаторный путь: TensorFlow Lite Micro

Самый распространённый способ — использовать TensorFlow Lite Micro. Это проверенная, хорошо документированная библиотека, которая интерпретирует модель на лету. Плюсы: гибкость, поддерживается множество операторов, можно менять модель без перекомпиляции прошивки. Минусы: интерпретатор, его операторный реестр и внутренняя бухгалтерия конкурируют за ту же память с тензорным ареной. На ESP32-S3 с его 512 КБ SRAM это критично.

2.2. Компиляторный путь: Микроядро на процедурных макросах

Альтернативный, гораздо более эффективный подход — разрешить граф вычислений на этапе компиляции. Его используют проекты вроде MicroFlow и edgedl. Суть: модель парсится процедурным макросом во время сборки, структура нейросети превращается в статический Rust-код. В рантайме нет ни интерпретатора, ни динамического выделения памяти, ни диспетчеризации — только последовательный вызов заранее скомпилированных функций для каждого слоя. Это даёт максимальную производительность и минимальное потребление памяти.

⚡ Вывод: для production-решений на ESP32-S3 интерпретатор — зло. Выбираем компиляцию.

3. Инструментарий: Краеугольные камни экосистемы

3.1. Основные компоненты

КомпонентКрейт / ПроектНазначение
HALesp-idf-hal или esp-halРабота с GPIO, SPI, I2C, UART, таймерами
Wi-Fi / HTTPesp-idf-svc + embedded-svcПодключение к сети и HTTP-запросы
Инференс (компиляторный подход)edgedl или microflowВыполнение INT8-моделей без кучи
Сериализацияserde_json + allocno_std-совместимая работа с JSON
Дисплейembedded-graphics + mipidsiОтрисовка интерфейса на AMOLED
Сенсорный экранI2C-драйвер (поверх esp-idf-hal)Обработка касаний

3.2. Настройка окружения

Для начала работы с 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.


4. Формат модели: INT8 — единственно верный путь

Для ESP32-S3 существует три класса моделей по размеру и формату:

  1. FP32 (32-битные float). Забудьте. Даже для маленькой сети в 100 КБ параметров это будет 400 КБ только на веса, плюс промежуточные тензоры — вылетаете за 512 КБ SRAM.
  2. FP16 (16-битные float). Уже лучше, но всё ещё не оптимально. Ваш Xtensa LX7 с FPU всё равно считает 32-битные float, так что вы будете платить за конвертацию.
  3. INT8 (8-битные целые). Единственный рабочий вариант для серьёзных задач. Веса в 4 раза меньше, чем FP32. Математика целочисленная, быстрая и предсказуемая. На ESP32-S3 есть аппаратное ускорение для некоторых INT8-операций через расширение TIE728.
📌 Важно: INT8-квантование — это стандарт де-факто для TinyML на ESP32. Проект edgedl поддерживает симметричное INT8-квантование с per-tensor и per-channel шкалами, что даёт гибкость без потери точности.

4.1. Экспорт модели из PyTorch / TensorFlow

Большинство существующих проектов используют формат .tflite (TensorFlow Lite) как промежуточное звено. Для экспорта INT8-модели необходимо:

  1. Обучить модель с симуляцией INT8-квантования (QAT — Quantization-Aware Training).
  2. Экспортировать в TFLite с INT8-квантованием.
  3. Сконвертировать в формат, понятный вашему Rust-движку (например, .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 на начальном этапе.


5. План «Б»: Серверная логика — мозг вне микроконтроллера

Если ваша задача — не распознавание жестов или сегментация изображений, а общение с пользователем на естественном языке, то инференс LLM прямо на ESP32 — это заведомо проигрышная стратегия. Даже самая маленькая языковая модель (TinyStories-33M) требует ~132 МБ только для весов в FP32. У вас есть 512 КБ SRAM и 8 МБ PSRAM.

Золотое правило: Микроконтроллер — это не мозг, а рефлекторная дуга. Он отвечает за быструю классификацию, ввод/вывод и локальный кеш. Тяжёлая генерация — удел сервера.

Вот как должна выглядеть архитектура:

  1. На ESP32: Лёгкий классификатор (Naive Bayes на основе хешей). Его задача — определить категорию вопроса («шашлык» → «кулинария») за микросекунды. Веса обновляются с сервера раз в сутки.
  2. Сервер (Rust + Actix Web + MySQL): Принимает категорию и ключевые слова, делает полнотекстовый поиск по базе знаний.
  3. Если сервер не нашёл: Делает запрос к внешней LLM, генерирует ответ, сохраняет в MySQL и отдаёт ESP32.
  4. Самообучение: Пользователь ставит лайк/дизлайк. ESP32 шлёт фидбек на сервер. Сервер обновляет веса слов и готовит новый словарь.

6. Работа с дисплеем и сенсором: Оживляем устройство

Ваш ESP32-S3 оснащён 1.64-дюймовым AMOLED-экраном (QSPI) и ёмкостным сенсором (I2C). Это не просто «красивая картинка», а основной интерфейс взаимодействия.

6.1. Подключение и драйверы

В 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()
}

6.2. Отображение чата

Для вывода текста используйте embedded-graphics с поддержкой шрифтов. Минимальный интерфейс — это два поля: вопрос пользователя (сверху) и ответ бота (снизу).


// Отображение статуса "Думаю..."
display.clear(Rgb565::BLACK);
display.draw_circle(120, 120, 20, Rgb565::YELLOW);
display.draw_text(10, 220, "Бот печатает...", Rgb565::GRAY);

6.3. Сенсорное управление

Сенсорный экран по I2C — это простой метод получения координат касания. Его можно использовать для:


7. Код: Сервер на Actix Web

Серверная часть принимает запросы от 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)),
    }
}

8. Код: Прошивка ESP32 (Клиент)

Фрагмент 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 {}
}

9. Отладка: Трассировка и железный подход

Отладка на микроконтроллерах всегда сложнее, чем на сервере. Есть несколько проверенных методов:

  1. Трассировка через UART. Используйте println! или log::info! для вывода отладочных сообщений. Подключите ESP32 через USB-конвертер и смотрите вывод в screen или minicom.
  2. Проверка на хосте (Host Testing). Тестируйте математику и логику инференса на вашем компьютере до прошивки на устройство.
  3. Hardware-in-the-loop (HIL). Для сложных моделей используйте автоматизированные тесты, которые прошивают устройство и прогоняют набор тестовых примеров.
🧪 Совет: LLM хорошо читают исходный код, но они гораздо хуже представляют, что на самом деле делал чип во вторник. Мы активно использовали трассировку во время выполнения: log::info! по UART — это не ностальгия, это общее доказательство между разработчиком и ассистентом.

10. Выводы и дорожная карта

Мы рассмотрели полный цикл создания нейросетевого устройства на базе ESP32-S3 и Rust. Ключевые выводы:

  1. Выбирайте компиляторный подход к инференсу (MicroFlow, edgedl). Избегайте интерпретаторов на устройстве.
  2. INT8 — единственный формат для хранения и вычислений на ESP32.
  3. Разделяйте ответственность: ESP32 — это быстрый интерфейс и локальный кеш, сервер — это умная генерация и память.
  4. Отладка через трассировку — ваш основной инструмент.

Дорожная карта на неделю:

  1. День 1–2: Настройка окружения, прошивка тестового «blinky». Сборка и запуск edgedl или microflow примера.
  2. День 3–4: Написание сервера на Actix Web, развёртывание MySQL, наполнение тестовыми данными.
  3. День 5: Интеграция: ESP32 запрашивает сервер по Wi-Fi и выводит ответ в консоль.
  4. День 6–7: Добавление дисплея (AMOLED) и сенсора. Отрисовка простого интерфейса чата.

Удачи! И помните: даже самые сложные системы строятся из простых, правильно скомпонованных частей.

 

Дата размещения статьи: 06-08-2026 в 16:15:12