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

Задачі та 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 це виглядає так:

spawner.spawn(heartbeat().expect("Task pool exhausted"));

Якщо однієї задачі має бути кілька одночасних екземплярів, це потрібно врахувати в її пулі. Не запускайте одну й ту саму нескінченну задачу повторно в кожному кадрі.

Де відбувається очікування

use lilkaoxide::embassy_time::{Duration, Timer};

Timer::after(Duration::from_millis(100)).await;

На час очікування 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, а не намагатись переробити всі його бібліотеки.