Перейти до змісту

Пам'ять та володіння

У no_std нема автоматичного heap «десь у системі». Є пам'ять чіпа, стек, статичні дані й те, що ви явно віддали allocator. Якщо не думати про це взагалі, проблема зазвичай з'явиться не на порожньому прикладі, а коли додасте великий кадр або ще одну задачу.

Стек, static та heap

Маленькі локальні змінні зручно тримати на стеку. Але [u8; 134400] — вже не маленька змінна. Такий буфер може вичерпати стек навіть коли загалом у RAM ще достатньо вільного місця.

Для великого буфера з одним власником підходить StaticCell:

use static_cell::StaticCell;

static PIXELS: StaticCell<[u8; 280 * 24 * 2]> = StaticCell::new();

let pixels = PIXELS.init_with(|| [0; 280 * 24 * 2]);

init_with ініціалізує значення на місці; великий проміжний масив не потрібно переносити зі стека. Викликайте це тільки один раз. Повторна ініціалізація того самого StaticCell — помилка, а не спосіб отримати ще один буфер.

Heap потрібен для динамічних алокацій. У прикладах він явно створений через esp_alloc::heap_allocator!. Якщо його не вистачає, збільшення тільки розміру стека проблему Vec не виправить: це різні області й різні причини помилок.

heapless — не безрозмірний

let mut line: heapless::String<32> = heapless::String::new();
if line.push_str("Hello, Lilka!").is_err() {
    // Рядок не вмістився, треба скоротити його або повідомити про помилку.
}

Місткість тут у байтах, не в літерах. Кириличний символ UTF-8 часто займає більше одного байта. heapless::Vec<T, N> так само має верхню межу кількості елементів.

Перевага не в тому, що пам'ять зникла, а в тому, що максимальний розмір відомий. Сам об'єкт все ще лежить на стеку, у static або всередині іншого об'єкта — залежно від того, де ви його створили.

Буфер передачі та framebuffer

display_buffer_1024 не створює повний кадр. У RGB565 один піксель — два байти, тому 280×240 потребує 134400 байтів. Смуга 280×24 — 13440 байтів. Текст і кілька примітивів у blocking можна малювати взагалі без власного framebuffer.

Додайте до цього heap, стеки задач, I2S DMA та дані SD. SDK вже резервує буфери для своєї периферії, тому вся доступна RAM не належить тільки вашому UI. Наявність PSRAM на платі також не переносить туди звичайний локальний масив автоматично.

Чому не можна двічі ініціалізувати Lilka

Lilka::init забирає апаратну периферію та створює драйвери. Викликайте його один раз. Для доступу до дисплея передавайте посилання або передайте сам дисплей потрібному модулю, а не створюйте другу Lilka всередині функції малювання.

let lilka = Lilka::init(Configuration::default()).unwrap();
let mut display = lilka.display;
let controller = lilka.controller;

Це часткове переміщення полів. controller ще можна забрати, але передати всю lilka після переміщення display вже не можна. Драйвери не копіюються, бо в реальному чіпі від цього не з'явиться другий SPI чи дисплей.

У async оберіть одного власника дисплея, а інші задачі нехай надсилають йому дані. Це часто простіше, ніж класти всі об'єкти в один великий mutex.

Чому SD-залежність просить allocator

У нас є дві різні бібліотеки, й тут легко переплутати їхні вимоги:

  • async-embedded-sdmmc 0.3.1 — драйвер SD-карти в SDK. У його src/lib.rs є безумовний extern crate alloc, який не зникає після default-features = false.
  • embedded-sdmmc 0.9.0 — FAT-рівень у файловому прикладі. Він працює з фіксованими буферами та heapless, тому heap для файлової системи не потрібен.

Підключений alloc змушує Rust вимагати глобальний allocator у кінцевій прошивці під час компонування. Але це не означає, що читання сектора чи робота з FAT реально робить алокації. Це особливість підключення поточної версії драйвера, а не апаратна вимога карти й не мінімальний обсяг пам'яті для SD.

Так само feature alloc у SDK лише підключає опціональний esp-alloc. Він не керує всіма extern crate alloc у залежностях і не резервує 16 КіБ автоматично. Цей розмір явно заданий у main навчальних прикладів для простого налаштування; він не був визначений як необхідний обсяг heap для SD.

А якщо зовсім без heap

Можна використовувати статичні буфери й heapless, а вимогу компонування задовольнити глобальним allocator, який завжди повертає null при спробі виділення пам'яті. Він не резервує heap, але й не дає програмі зробити жодну успішну динамічну алокацію.

Це працює тільки поки всі виконувані гілки обходяться без алокацій. Якщо додати код, якому потрібен heap, він отримає відмову; звичайна операція з alloc::Vec чи alloc::String може завершити програму через помилку виділення пам'яті. radio з таким allocator не запускайте: йому heap потрібен.

Інший шлях — виправити або умовно підключати зайвий extern crate alloc у самому SD-драйвері й повторно перевірити збірку. У поточній залежності SDK цього виправлення немає. Саме тому тут окремо описані вимога allocator та використання heap: це різні речі.