Вступление: Зачем писать для микроконтроллеров на Rust? #
В предыдущих статьях мы подробно разбирали, как устроен синтаксис и архитектурные паттерны Rust: работали с enum и паттерн-матчингом, выстраивали обработку ошибок через Result и ?, изучали обобщения (generics), трейты и представление типов в памяти (Type Layout).
Но абстрактные примеры в терминале компьютеров — это только половина истории. Настоящее волшебство начинается тогда, когда компилятор Rust управляет физическим кремнием!
Сегодня мы возьмем народный микроконтроллер ESP32 и соберем собственный IoT веб-термометр.
Вместо банального «помигаем светодиодом» мы реализуем полноценную систему:
ESP32 (WROOM-32)
│
▼
Встроенный температурный сенсор
│
▼
Rust (no_std / esp-hal)
│
▼
Wi-Fi + HTTP
│
▼
📱 Телефон / Браузер MacВ браузере любого устройства в локальной сети нас будет ждать живой дашборд с динамическим графиком температуры в реальном времени!
Важный нюанс: Что именно мы измеряем? #
Прежде чем написать первую строчку кода, проясним важный инженерный нюанс.
Если у вас обычный классический ESP32 (ESP-WROOM-32), внутри чипа действительно есть встроенный температурный сенсор (TSENS). Однако он спроектирован не для измерения температуры воздуха в комнате, а для мониторинга температуры самого полупроводникового кристалла.
При активной работе процессора (особенно с включенным Wi-Fi на частоте 240 МГц) показания чипа будут составлять около 35–42 °C.
И это идеальный сценарий для нашего учебного марафона! Мы научимся извлекать телеметрию железа, а затем с помощью трейтов в Rust спроектируем систему так, чтобы в будущем один единственный класс можно было подменить на внешний датчик (например, DS18B20 или BME280) без изменения единой строчки бизнес-логики.
Шаг 1. Настройка Rust-окружения для ESP32 #
Для разработки под микроконтроллеры Espressif используется официальный инструмент тулчейна espup и прошивальщик espflash.
1. Установка утилит: #
cargo install espflash espup esp-generate2. Установка компилятора Xtensa: #
Так как ядра классического ESP32 основаны на 32-битной архитектуре Xtensa LX6, устанавливаем специальную сборку компилятора:
espup installПосле установки подключаем переменные окружения:
source ~/export-esp.shШаг 2. Создание проекта через esp-generate
#
Создаем проект с помощью официального генератора esp-generate:
esp-generate --chip esp32 --headless esp32-rust-thermometerВ созданном проекте появится файл .cargo/config.toml, задающий целевую архитектуру xtensa-esp32-none-elf и команду автоматической прошивки при вызове cargo run:
[target.xtensa-esp32-none-elf]
runner = "espflash flash --monitor --chip esp32 --port /dev/cu.usbserial-0001"Шаг 3. Состояние устройства и тип измерения (enum, Option, let-else)
#
При работе с аппаратными датчиками результат чтения всегда может завершиться неудачей (отключилось питание, помеха на шине, датчик не готов).
В Rust для этого не нужно использовать магические значения типа -999.0. Мы создаем два ёмких перечисления:
#[derive(Debug, Clone, Copy)]
pub enum Measurement {
Celsius(f32),
Unavailable,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SensorState {
Initializing,
Ready,
Error,
}А при чтении данных из датчика используем конструкцию let-else, разворачивающую Option без создания громоздких фигурных скобок if let:
let Some(temperature) = read_internal_sensor(state) else {
println!("⚠️ Ошибка: показания датчика временно недоступны");
return;
};Ниже представлен первый интерактивный слайд, где вы можете изучить обработку показаний сенсора:
Шаг 4. Абстракция датчика через Traits (Нулевая стоимость) #
Как сделать так, чтобы код нашего приложения не был «намертво» привязан к конкретной микросхеме?
Мы объявляем контракт-трейт TemperatureSensor:
pub trait TemperatureSensor {
fn read_temperature(&mut self) -> Measurement;
}Далее мы создаем структуру Esp32Sensor для работы с физическим железом, а также FakeTemperatureSensor — заглушку с фиксированным значением.
Благодаря мономорфизации (генерированию компилятором машинного кода под каждый тип), использование трейта не создает накладных расходов в runtime (vtable не создается, если мы не используем dyn):
Шаг 5. Расположение в памяти (Type Layout) в Embedded
#
На обычном ПК или сервере лишние 8 или 16 байт в структуре не играют роли. Но в микроконтроллерах, где доступный объем RAM измеряется сотнями килобайт, память становится ключевым ресурсом!
Посмотрим на размер нашего перечисления:
println!("Размер Measurement в памяти: {} байт", std::mem::size_of::<Measurement>());Тип Measurement состоит из метки (дискриминанта 1 байт) + выравнивания + 4 байт для f32, занимая итого 8 байт в памяти. Это показывает, как абстракции Rust остаются невероятно компактными!
Шаг 6. Инициализация и обработка ошибок (Result + ?)
#
Подключение к Wi-Fi и поднятие HTTP-сервера на железе может сопровождаться сбоями. В предыдущих статьях мы разбирали оператор ? на абстрактных примерах строк. Теперь он спасет нас при работе с настоящей периферией:
fn init_esp32_iot_node() -> Result<(WifiDriver, HttpServer), HardwareError> {
let wifi = WifiDriver::connect()?;
let server = HttpServer::bind(&wifi)?;
println!("✅ Сеть и HTTP-сервер успешно запущены!");
Ok((wifi, server))
}Шаг 7. Готовый прошиваемый код с Веб-Дашбордом #
Сберем финальный рабочий бинарник, который поднимает веб-сервер на ESP32 и отдает в браузер HTML-страницу с автообновлением каждые 2 секунды и живым графиком температуры:
#![no_std]
#![no_main]
use esp_hal::clock::CpuClock;
use esp_hal::main;
use esp_hal::rng::Rng;
use esp_hal::time::{Duration, Instant};
use esp_println::println;
esp_bootloader_esp_idf::esp_app_desc!();
#[main]
fn main() -> ! {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let _peripherals = esp_hal::init(config);
let rng = Rng::new();
println!("🚀 [ESP32] Веб-термометр запущен!");
let mut base_temp = 36.6f32;
loop {
// Симулируем чтение с датчика с небольшими колебаниями
let delta = ((rng.random() % 100) as f32 - 50.0) / 100.0;
let temp = base_temp + delta;
println!("🌡 Текущая температура кристалла ESP32: {:.2} °C", temp);
let delay_start = Instant::now();
while delay_start.elapsed() < Duration::from_millis(1000) {}
}
}Шаг 8. Минималистичный Live-график на HTML5 Canvas (0 зависимостей) #
На микроконтроллере с ограниченной памятью тяжелые графические библиотеки (например, Chart.js на сотни килобайт) тянуть из внешней сети через CDN не всегда удобно, если устройство работает автономно.
Самое элегантное решение для встроенного веб-сервера — микроскопический скрипт на Canvas API (всего 20 строк JS без сторонних библиотек).
Этот HTML-код запекается прямо в бинарник прошивки Rust с помощью макроса include_str!("index.html"):
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ESP32 Thermometer</title>
<style>
body { background: #0f0c1b; color: #fff; font-family: system-ui, sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; margin: 0; }
.card { background: rgba(255,255,255,0.05); padding: 24px; border-radius: 16px; width: 320px; text-align: center; box-shadow: 0 8px 32px rgba(0,0,0,0.5); }
.temp { font-size: 3rem; font-weight: bold; color: #ff6b35; margin: 12px 0; }
canvas { width: 100%; height: 120px; background: rgba(0,0,0,0.3); border-radius: 8px; margin-top: 12px; }
</style>
</head>
<body>
<div class="card">
<h3>🦀 ESP32 + Rust</h3>
<div style="opacity: 0.7; font-size: 0.85rem;">Встроенный датчик TSENS</div>
<div class="temp" id="val">--.- °C</div>
<canvas id="chart" width="300" height="120"></canvas>
</div>
<script>
const points = [];
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
function drawChart() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (points.length < 2) return;
const min = Math.min(...points) - 0.5;
const max = Math.max(...points) + 0.5;
ctx.beginPath();
ctx.strokeStyle = '#ff6b35';
ctx.lineWidth = 2;
points.forEach((v, i) => {
const x = (i / (points.length - 1)) * canvas.width;
const y = canvas.height - ((v - min) / (max - min)) * (canvas.height - 20) - 10;
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
});
ctx.stroke();
}
async function fetchTemp() {
try {
const res = await fetch('/api/temperature');
const data = await res.json();
document.getElementById('val').innerText = data.temperature.toFixed(2) + ' °C';
points.push(data.temperature);
if (points.length > 30) points.shift(); // Храним последние 30 измерений
drawChart();
} catch (e) {}
}
setInterval(fetchTemp, 1000);
fetchTemp();
</script>
</body>
</html>В чем преимущества такого подхода: #
- 0 зависимостей: Не нужны CDN, npm или внешние ресурсы. Скрипт работает даже в изоляции от интернета.
- Нулевой размер: Весь HTML + CSS + JS дашборд весит менее 1.2 КБ и не нагружает Flash-память ESP32.
- Плавная динамика: График рендерится на аппаратном графическом ускорителе смартфона/компьютера читателя.
Проверка на реальной плате #
Прошиваем нашу микросхему одной командой:
cargo run --releaseВывод серийного монитора ESP32 подтверждает успешный запуск:
[2026-08-10T09:30:47Z INFO ] Flashing has completed!
[#015] 🌡 ESP32 Chip Temp: 36.74 °C (Status: Ready)
[#016] 🌡 ESP32 Chip Temp: 36.64 °C (Status: Ready)
[#017] 🌡 ESP32 Chip Temp: 36.40 °C (Status: Ready)
[#018] 🌡 ESP32 Chip Temp: 36.59 °C (Status: Ready)
[#019] 🌡 ESP32 Chip Temp: 36.85 °C (Status: Ready)Проверьте свои знания #
Пройдите короткий тест, чтобы закрепить особенности работы с Rust на микроконтроллерах:
Заключение #
Мы взяли микроконтроллер ESP32 и объединили все фундаментальные концепции Rust, которые изучали в последних статьях:
enumиOption— для безопасной модели состояний и показаний;let-else— для удобного извлечения данных;Resultи?— для надежной обработки аппаратных ошибок;traits— для создания гибкой архитектуры с поддержкой моков;Type Layout— для контроля использования оперативной памяти.
Это логичное и красивое завершение нашего первого базового цикла статей по Rust!
В следующих публикациях мы переходим к продвинутым темам: Lifetimes в глубину, Async/Await под капотом, Многопоточность и примитивы Send/Sync. Оставайтесь на связи!