mirror of
https://github.com/stasenso/rp_pico_display_engine.git
synced 2026-06-26 21:32:41 +03:00
docs(display): rewrite display.c docs for paint lifecycle
This commit is contained in:
+46
-219
@@ -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);
|
## Новый paint-контракт
|
||||||
- отдавать буфер для рисования;
|
|
||||||
- запускать передачу кадра в дисплей через DMA;
|
|
||||||
- сообщать о завершении передачи (через `display_poll()` + callback);
|
|
||||||
- поддерживать два режима управления буферами: `SAFE` и `RAW`.
|
|
||||||
|
|
||||||
Публичный 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` - размер кадра в пикселях;
|
## SAFE и RAW
|
||||||
- `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-канала).
|
### 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)`
|
## Что делает IRQ
|
||||||
|
|
||||||
Последовательность:
|
|
||||||
|
|
||||||
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()`:
|
`display_dma_irq_handler()`:
|
||||||
|
1. Сбрасывает IRQ-флаг DMA.
|
||||||
|
2. Завершает SPI-транзакцию (поднимает CS).
|
||||||
|
3. Снимает флаг `dma_busy`.
|
||||||
|
|
||||||
1. Сбрасывает IRQ-флаг DMA-канала (`dma_hw->ints0 = 1 << dma_chan`).
|
Никакой callback из основного цикла больше не нужен.
|
||||||
2. Поднимает `CS` (`hw_raise_cs()`), завершая SPI-транзакцию.
|
|
||||||
3. Ставит:
|
|
||||||
- `dma_busy = false`;
|
|
||||||
- `frame_done_pending = true`.
|
|
||||||
|
|
||||||
Сам 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`:
|
## Инварианты
|
||||||
|
|
||||||
- флаг сбрасывается;
|
1. После успешного `begin` обязательно вызвать `display_end_paint()`.
|
||||||
- при ненулевом `frame_done_cb` вызывается callback.
|
2. Нельзя начинать второй paint, пока первый не завершён.
|
||||||
|
3. Нельзя вызывать ручной `display_submit()` внутри открытой paint-секции.
|
||||||
Это безопаснее, чем вызывать пользовательский код прямо из 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`.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user