ESP32-S3 читал старый код Linux после записи во flash
У маленького ESP32-S3 есть два ядра и один физический flash. Одно ядро в этом проекте запускает Linux, второе остаётся в ESP-IDF/FreeRTOS и выполняет сервисные задачи, включая операции с flash. Такая асимметричная система позволила загрузить нативный Linux 6.11 на Xtensa LX7 в модуле N16R8 с 16 МиБ flash и 8 МиБ octal PSRAM. Затем машина начала иногда разрушать собственную раннюю загрузку.
Один flash, две версии реальности
Проблема проявлялась после записи JFFS2. Linux передавал запрос прошивке на соседнем ядре, та физически стирала или переписывала область flash, а общий кэш продолжал содержать старые строки по тем же адресам. Последующее чтение могло вернуть уже не существующую версию страницы. Среди таких страниц оказывался и код ядра, исполняемый из flash.
В патче прошивки после команд erase и write добавлен вызов ROM-функции Espressif `Cache_Invalidate_Addr()`. Ей передают виртуальный адрес области Linux во flash и длину изменённого диапазона. Это делает изменение содержимого flash видимым для ядра, которое продолжает исполнять Linux. Сам вызов существует в ROM-интерфейсе ESP32-S3; его символ опубликован в linker description ESP-IDF по адресу `0x400016b0`.
UART молчал и считался успешным boot
Ранний тест долго скрывал масштаб дефекта. Его логика принимала отсутствие дальнейшего вывода UART за успешный boot. Но kernel fault или зависание способны оборвать сам канал отчёта. Новый runner дожидался login prompt, читал `dmesg` и проверял, что `/proc/sys/kernel/tainted` остаётся нулевым. До исправления автор получил 10 отказов в 17 завершённых прогонах и ещё три зависания. После исправления отчёт фиксирует 25 успешных factory boot подряд, затем отдельный финальный soak на 10 из 10 загрузок.
Это убедительное контролируемое сравнение для конкретной платы и образа, однако оно не доказывает отсутствие всех будущих сбоев. Тесты не включали длительный flash reclaim, Wi‑Fi traffic и BLE provisioning. Агрегированная цифра автора в 55 чистых boot пока прослеживается по опубликованным отчётам лишь частично: напрямую документированы 35 успешных итераций.
Исправление сузило проблему, но не отменило границы NOMMU
После устранения порчи загрузки проект получил и более экономный путь `fork()`: измеренный peak fork shadow для Bash, Dash, MicroPython и socat сократился ровно вдвое. В финальном образе осталось 3744 кБ `MemAvailable` перед board suite. Но это по-прежнему NOMMU Linux: аппаратной изоляции процессов и copy-on-write у ESP32-S3 нет, а переключение процесса способно блокировать прерывания примерно на 21.5 мс.
История ценна тем, что ошибка оказалась физически понятной: один исполнитель изменил носитель, второй сохранил старую картину этого носителя в кэше. В системах CPU+DMA, FPGA+CPU или двухъядерных MCU адреса, формат буфера и владение периферией описывают лишь часть контракта. В него должна входить и явная судьба кэшированных данных после каждого обмена.