diff --git a/docs/display.c.ru.md b/docs/display.c.ru.md index f4731a5..af41966 100644 --- a/docs/display.c.ru.md +++ b/docs/display.c.ru.md @@ -1,238 +1,65 @@ -# Документация по `src/core/display.c` +# display.c — актуальное описание (без display_poll) -## 1. Назначение модуля +## Коротко -`display.c` реализует низкоуровневый движок вывода кадров на дисплей ST7789 через SPI + DMA на RP2040/RP2350. +Модуль управляет выводом кадров на ST7789 через DMA и работает в двух режимах: +- `DISPLAY_MODE_SAFE` — безопасная буферизация, с новым контрактом `begin/end paint`. +- `DISPLAY_MODE_RAW` — ручное управление буферами и отправкой. -Задачи модуля: +Механизм `display_poll()` и callback завершения кадра удалён. -- хранить 1 или 2 кадровых буфера (RGB565); -- отдавать буфер для рисования; -- запускать передачу кадра в дисплей через DMA; -- сообщать о завершении передачи (через `display_poll()` + callback); -- поддерживать два режима управления буферами: `SAFE` и `RAW`. +## Новый paint-контракт -Публичный API объявлен в `include/display/display.h`. +### `display_begin_paint_try()` +- Неблокирующе пытается начать рисование нового кадра. +- Возвращает `NULL`, если буфер недоступен или paint уже начат. -## 2. Архитектура и данные +### `display_begin_paint_blocking()` +- Блокирующе ждёт доступный буфер и начинает paint-секцию. -Внутри файла есть глобальный контекст `ctx`: +### `display_end_paint()` +- Завершает paint-секцию и отправляет кадр в вывод. +- Возвращает `false`, если нарушен порядок вызовов (`end` без `begin`) или DMA занят. -- `width`, `height` - размер кадра в пикселях; -- `buffer_count` - число буферов (`1` или `2`); -- `mode` - `DISPLAY_MODE_SAFE` или `DISPLAY_MODE_RAW`; -- `dma_busy` - идёт ли сейчас DMA-передача кадра; -- `frame_done_pending` - флаг "DMA уже завершилась, callback ждёт обработки в `display_poll()`"; -- `frame_done_cb` - пользовательский callback завершения кадра; -- `buffers[]` - массив указателей на выделенные буферы; -- `draw_index` - индекс буфера для рисования; -- `scanout_index` - индекс буфера, который будет отправлен/отправляется в дисплей. +## SAFE и RAW -Также хранится `dma_chan` (номер DMA-канала). +### SAFE +- Обычно используется `begin/end paint`. +- Движок сам держит корректный порядок отправки. -## 3. Инициализация дисплея и DMA +### RAW +- Для ручного контроля в RAW используется тот же begin/end-контракт, + а swap выполняется явно: + - `display_begin_paint_try()` / `display_begin_paint_blocking()` + - `display_swap_buffers()` + - `display_end_paint()` -### 3.1 `display_init(const display_config_t* cfg)` - -Последовательность: - -1. Проверки `assert`: - - `cfg != NULL`; - - `buffer_count >= 1`; - - `buffer_count <= DISPLAY_MAX_BUFFERS`. -2. Очистка `ctx`. -3. Копирование параметров (`width/height/mode/callback/...`) в `ctx`. -4. Выделение `buffer_count` буферов через `malloc` размером `width * height * 2` байт. -5. Начальные индексы: - - `draw_index = 0`; - - `scanout_index = 0`. -6. Сброс флагов DMA (`dma_busy = false`, `frame_done_pending = false`). -7. Вызов `hw_init(width, height)`. - -Важно: - -- Освобождения памяти в модуле нет (deinit отсутствует). -- Ошибки аллокации обрабатываются через `assert` (в release-конфигурации поведение зависит от настроек). - -### 3.2 `hw_init(uint16_t width, uint16_t height)` - -Функция поднимает железо: - -- настраивает SPI (порт `spi0` по умолчанию, формат 8-bit, mode 0, MSB first); -- назначает GPIO для MOSI/SCK и управляющих пинов (`CS`, `DC`, `RST`, `BL`); -- выполняет hardware reset ST7789; -- отправляет init-команды ST7789: - - `0x01` (SWRESET), - - `0x11` (SLPOUT), - - `0x36` (MADCTL, параметр `0b10100000`), - - `0x3A` (COLMOD = `0x55`, формат RGB565), - - `0x2A` / `0x2B` (полное окно вывода по X/Y), - - `0x21` (INVON), - - `0x29` (DISPON), - - включение подсветки `BL`. -- конфигурирует DMA-канал: - - размер передачи `DMA_SIZE_8` (SPI работает как поток байтов), - - чтение с инкрементом (из массива пикселей), - - запись без инкремента (в `SPI->dr`), - - DREQ от SPI TX; -- настраивает IRQ `DMA_IRQ_0` на `display_dma_irq_trampoline()`. - -## 4. Передача кадра: полный жизненный цикл - -### 4.1 Запрос буфера для рисования - -`display_get_draw_buffer()` возвращает `ctx.buffers[draw_index]`. - -Спец-случай: - -- если режим `SAFE` и буфер один, функция ждёт (`while (ctx.dma_busy)`) завершения DMA, чтобы вы не рисовали в буфер, который прямо сейчас передаётся. - -### 4.2 Запуск отправки кадра - -`display_submit()`: - -1. Если `dma_busy == true` -> вернуть `false`. -2. В режиме `SAFE` при двух буферах автоматически меняет `draw_index <-> scanout_index` (page flip перед отправкой). -3. Считает число пикселей `width * height`. -4. Ставит `dma_busy = true`. -5. Запускает `hw_start_dma(buffers[scanout_index], pixels)`. -6. Возвращает `true`. - -### 4.3 Что делает `hw_start_dma(...)` - -Перед стартом DMA функция: - -- заново выставляет окно `0x2A/0x2B` на весь экран; -- отправляет `0x2C` (RAMWR); -- переводит линии в поток данных (`DC=1`, `CS=0`); -- задаёт DMA: - - `read_addr = buffer`, - - `trans_count = pixel_count * sizeof(uint16_t)` (в байтах), - - immediate start. - -Почему `DMA_SIZE_8`, но буфер `uint16_t*`: - -- физически в SPI идут байты; -- DMA читает память последовательно по байту; -- порядок байт в потоке определяется endian-представлением в памяти + требованиями ST7789. - -### 4.4 Завершение DMA (IRQ) - -Аппаратный IRQ вызывает `display_dma_irq_handler()` через trampoline. +## Что делает IRQ `display_dma_irq_handler()`: +1. Сбрасывает IRQ-флаг DMA. +2. Завершает SPI-транзакцию (поднимает CS). +3. Снимает флаг `dma_busy`. -1. Сбрасывает IRQ-флаг DMA-канала (`dma_hw->ints0 = 1 << dma_chan`). -2. Поднимает `CS` (`hw_raise_cs()`), завершая SPI-транзакцию. -3. Ставит: - - `dma_busy = false`; - - `frame_done_pending = true`. +Никакой callback из основного цикла больше не нужен. -Сам callback пользователя здесь НЕ вызывается. +## Рекомендуемый цикл 50 Гц -### 4.5 Доставка callback в main loop +```c +if (frame_tick_due) { + frame_tick_due = false; -`display_poll()` должен вызываться в основном цикле. + uint16_t* buf = display_begin_paint_try(); + if (buf) { + // draw + bool ok = display_end_paint(); + hard_assert(ok); + } +} +``` -Если `frame_done_pending == true`: +## Инварианты -- флаг сбрасывается; -- при ненулевом `frame_done_cb` вызывается callback. - -Это безопаснее, чем вызывать пользовательский код прямо из IRQ. - -## 5. Режимы `SAFE` и `RAW` - -### 5.1 `DISPLAY_MODE_SAFE` - -Поведение: - -- при 2 буферах `display_submit()` сам делает swap (рисуете в один, отправляется другой); -- при 1 буфере `display_get_draw_buffer()` блокируется, пока DMA занята; -- API старается исключить конфликт "рисование в передаваемый буфер". - -Плюсы: - -- меньше шансов получить tearing/артефакты; -- меньше ручной координации со стороны приложения. - -Минусы: - -- возможны блокировки (busy-wait); -- в 1-буферном режиме рисование и передача строго последовательно. - -### 5.2 `DISPLAY_MODE_RAW` - -Поведение: - -- движок сам буферы не переставляет; -- пользователь вручную вызывает `display_swap_buffers()`; -- `display_get_draw_buffer()` не блокирует. - -`display_swap_buffers()` вернёт `false`, если: - -- режим не `RAW`; -- буферов меньше двух; -- DMA сейчас активна. - -Плюсы: - -- полный контроль и минимальные накладные ограничения. - -Риски: - -- легко получить tearing или гонки, если swap/submit делаются не в тот момент. - -## 6. Потокобезопасность и ограничения - -Модуль не использует mutex/critical section. Флаги `dma_busy` и `frame_done_pending` объявлены `volatile`, что помогает для простых сценариев "main loop + IRQ", но не является полной синхронизацией для многопоточной модели. - -Практические ограничения: - -- предполагается один управляющий поток приложения; -- `display_poll()` должен вызываться регулярно, иначе callback не сработает; -- `display_wait()` и часть API используют активное ожидание (нагрузка на CPU); -- нет API для освобождения буферов и деинициализации DMA/SPI. - -## 7. Ключевые функции API (кратко) - -- `display_get_draw_buffer()` - получить буфер для рендеринга. -- `display_get_scanout_buffer()` - посмотреть текущий буфер вывода. -- `display_swap_buffers()` - ручной swap (только `RAW` + 2 буфера + DMA idle). -- `display_submit()` - начать отправку кадра. -- `display_ready()` - `true`, если DMA не занята. -- `display_wait()` - блокирующее ожидание конца DMA. -- `display_poll()` - доставка события "кадр отправлен" и вызов callback. -- `display_dma_irq_handler()` - обработчик, который должен быть привязан к DMA IRQ. - -## 8. Рекомендуемые шаблоны использования - -### 8.1 SAFE + 2 буфера (рекомендуемый базовый) - -1. `buf = display_get_draw_buffer()` -2. Рисование в `buf` -3. `display_submit()` -4. В главном цикле постоянно `display_poll()` - -Swap происходит автоматически. - -### 8.2 RAW + 2 буфера (полный ручной контроль) - -1. Рисовать в `display_get_draw_buffer()` -2. Когда `display_ready()`: - - `display_swap_buffers()` - - `display_submit()` -3. Регулярно `display_poll()` - -### 8.3 SAFE + 1 буфер - -1. `display_get_draw_buffer()` может ждать завершения предыдущей передачи; -2. После рисования вызвать `display_submit()`; -3. При необходимости `display_wait()`. - -## 9. Что важно понимать при сопровождении - -- Все аппаратные детали ST7789 и DMA инкапсулированы в `hw_*` функциях внутри `display.c`. -- Политика управления буферами живёт в `display_submit()` / `display_swap_buffers()` / `display_get_draw_buffer()`. -- IRQ только помечает событие; бизнес-логика завершения кадра переносится в `display_poll()`. -- Если понадобится расширение (частичные обновления, тройная буферизация, deinit, sync-примитивы), ядро для этого уже есть в структуре `ctx`. +1. После успешного `begin` обязательно вызвать `display_end_paint()`. +2. Нельзя начинать второй paint, пока первый не завершён. +3. Нельзя вызывать ручной `display_submit()` внутри открытой paint-секции.