Задачі та async
Async потрібен, коли програма має чекати кілька незалежних речей: таймер, передачу, дані від іншої задачі. Якщо вся робота — раз на секунду намалювати число, blocking теж буде нормально працювати. Обирати треба за потребою, а не тому що .await виглядає сучасніше.
Хто що запускає
Для поточного SDK використовуйте #[esp_rtos::main]. Він дає async-точку входу та Spawner. Усередині викликайте Lilka::init(config).await: SDK забирає периферію, запускає свій RTOS runtime й налаштовує драйвери.
Не додавайте поряд ще один ручний esp_rtos::start або esp_hal::init. SDK вже робить це зі своїми TIMG0 та software interrupt. Поле lilka.rtos.is_started() можна використати як перевірку завершеної ініціалізації.
Повна робоча зв'язка залежностей і точки входу є в async-прикладі. Її варто спочатку зібрати як є, а вже потім переносити задачі у свій проєкт.
Задачі Embassy
Функція з #[embassy_executor::task] має свій пул екземплярів. Виклик створює token, а Spawner передає задачу executor. У використаній Embassy 0.10 це виглядає так:
Якщо однієї задачі має бути кілька одночасних екземплярів, це потрібно врахувати в її пулі. Не запускайте одну й ту саму нескінченну задачу повторно в кожному кадрі.
Де відбувається очікування
На час очікування executor може виконувати інші готові задачі. Звичайний loop з довгими обчисленнями без .await продовжує займати поточний executor. Якщо обробляєте багато даних, розбийте роботу на порції або винесіть її в відповідний контекст виконання.
Timer::after додає затримку після попередньої роботи. Для періодичного оновлення з рівнішим інтервалом зручний Ticker:
use lilkaoxide::embassy_time::{Duration, Ticker};
let mut ticker = Ticker::every(Duration::from_millis(100));
loop {
ticker.next().await;
// Одна коротка порція роботи.
}
Як передавати дані
Для «ось останнє значення, старе вже не цікаве» підходить embassy_sync::signal::Signal. Для черги подій, які не можна губити, — embassy_sync::channel::Channel. Тип синхронізації обирається під контексти, з яких ці об'єкти використовуються.
Зручна схема для UI: одна задача володіє дисплеєм, інші надсилають їй стан або події. Не треба, щоб задача кнопок, задача мережі та задача SD по черзі намагались домалювати щось у той самий екран.
Якщо все ж використовуєте mutex, не тримайте його довше потрібного. Особливо не затискайте всі дані програми одним lock на час повної передачі кадру чи читання файла.
Blocking-бібліотека всередині async
Наявність async SDK не робить кожну сторонню бібліотеку асинхронною. FAT-приклад на embedded-sdmmc використовує blocking BlockDevice; його не можна просто підставити замість async SD, дописавши .await у середині синхронного трейта.
Так само embedded-graphics малює у синхронний DrawTarget. У нашому прикладі це RAM-буфер, а вже передача на дисплей асинхронна. Це нормальний поділ: коротке малювання в пам'ять, потім очікування SPI.
Перш ніж переносити blocking-код, перевірте сигнатури залежності. Іноді правильне рішення — залишити маленький застосунок blocking, а не намагатись переробити всі його бібліотеки.