diff --git a/docs/display.c.ru.md b/docs/display.c.ru.md new file mode 100644 index 0000000..f4731a5 --- /dev/null +++ b/docs/display.c.ru.md @@ -0,0 +1,238 @@ +# Документация по `src/core/display.c` + +## 1. Назначение модуля + +`display.c` реализует низкоуровневый движок вывода кадров на дисплей ST7789 через SPI + DMA на RP2040/RP2350. + +Задачи модуля: + +- хранить 1 или 2 кадровых буфера (RGB565); +- отдавать буфер для рисования; +- запускать передачу кадра в дисплей через DMA; +- сообщать о завершении передачи (через `display_poll()` + callback); +- поддерживать два режима управления буферами: `SAFE` и `RAW`. + +Публичный API объявлен в `include/display/display.h`. + +## 2. Архитектура и данные + +Внутри файла есть глобальный контекст `ctx`: + +- `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` - индекс буфера, который будет отправлен/отправляется в дисплей. + +Также хранится `dma_chan` (номер DMA-канала). + +## 3. Инициализация дисплея и DMA + +### 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. + +`display_dma_irq_handler()`: + +1. Сбрасывает IRQ-флаг DMA-канала (`dma_hw->ints0 = 1 << dma_chan`). +2. Поднимает `CS` (`hw_raise_cs()`), завершая SPI-транзакцию. +3. Ставит: + - `dma_busy = false`; + - `frame_done_pending = true`. + +Сам callback пользователя здесь НЕ вызывается. + +### 4.5 Доставка callback в main loop + +`display_poll()` должен вызываться в основном цикле. + +Если `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`.