11 KiB
Документация по 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)
Последовательность:
- Проверки
assert:cfg != NULL;buffer_count >= 1;buffer_count <= DISPLAY_MAX_BUFFERS.
- Очистка
ctx. - Копирование параметров (
width/height/mode/callback/...) вctx. - Выделение
buffer_countбуферов черезmallocразмеромwidth * height * 2байт. - Начальные индексы:
draw_index = 0;scanout_index = 0.
- Сброс флагов DMA (
dma_busy = false,frame_done_pending = false). - Вызов
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():
- Если
dma_busy == true-> вернутьfalse. - В режиме
SAFEпри двух буферах автоматически меняетdraw_index <-> scanout_index(page flip перед отправкой). - Считает число пикселей
width * height. - Ставит
dma_busy = true. - Запускает
hw_start_dma(buffers[scanout_index], pixels). - Возвращает
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():
- Сбрасывает IRQ-флаг DMA-канала (
dma_hw->ints0 = 1 << dma_chan). - Поднимает
CS(hw_raise_cs()), завершая SPI-транзакцию. - Ставит:
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 буфера (рекомендуемый базовый)
buf = display_get_draw_buffer()- Рисование в
buf display_submit()- В главном цикле постоянно
display_poll()
Swap происходит автоматически.
8.2 RAW + 2 буфера (полный ручной контроль)
- Рисовать в
display_get_draw_buffer() - Когда
display_ready():display_swap_buffers()display_submit()
- Регулярно
display_poll()
8.3 SAFE + 1 буфер
display_get_draw_buffer()может ждать завершения предыдущей передачи;- После рисования вызвать
display_submit(); - При необходимости
display_wait().
9. Что важно понимать при сопровождении
- Все аппаратные детали ST7789 и DMA инкапсулированы в
hw_*функциях внутриdisplay.c. - Политика управления буферами живёт в
display_submit()/display_swap_buffers()/display_get_draw_buffer(). - IRQ только помечает событие; бизнес-логика завершения кадра переносится в
display_poll(). - Если понадобится расширение (частичные обновления, тройная буферизация, deinit, sync-примитивы), ядро для этого уже есть в структуре
ctx.