Files
rp_pico_display_engine/docs/display.c.ru.md
T
2026-02-23 22:21:53 +03:00

239 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Документация по `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`.