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

11 KiB
Raw Blame History

Документация по 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.