22а. Графический интерфейс на дисплее: буфер кадра, LTDC, DMA2D и сенсор
Индикатор из раздела 22 показывает число. Дисплей с сенсором показывает интерфейс: кнопки, графики, меню — и здесь микроконтроллер впервые упирается не в периферию, а в память и полосу пропускания. Кадр 240×320 в 16-битном цвете — 153 600 байт, и его надо перерисовывать десятки раз в секунду. Раздел о том, где этот кадр живёт, как он попадает на стекло — через SPI или через контроллер LTDC, — чем помогает ускоритель DMA2D и как в это встраивается сенсор и библиотека интерфейса.
Устройство LTDC и DMA2D, формула смешения и пример таймингов взяты из справочного
руководства на F405/407/427/429 (из пакета Keil); константы и структуры — из
HAL 2.17.1; скорость SPI F411 — из его даташита. Размеры буферов, времена кадра,
тайминги и смешение пересчитаны программой _verify/gui22a/gui_calc.c, собранной
gcc на компьютере. Сверяет _verify/gui22a_check.py.
Как это называется в документации
Оригинал Перевод frame buffer буфер кадра — массив пикселей в памяти LTDC (LCD-TFT display controller) контроллер, который сам выдаёт кадр на параллельный RGB-дисплей DMA2D (Chrom-ART) двумерный DMA: заливка, копирование, преобразование формата, смешение pixel clock (LCD_CLK) тактовая частота пикселей HSYNC, VSYNC, back porch, front porch синхроимпульсы и «поля» до и после активной области layer слой LTDC — до двух, смешиваются снизу вверх PFC (pixel format converter) преобразователь формата пикселя CLUT таблица цветов для индексных форматов line offset, pitch сколько байт от начала одной строки до начала следующей blending, alpha смешение по прозрачности tearing разрыв кадра: показ наполовину старого, наполовину нового
22а.1 Два пути к стеклу
Дисплей со своим контроллером. Небольшие TFT (240×240, 240×320) несут на плате микросхему с собственной памятью кадра. Микроконтроллер по SPI задаёт окно и шлёт пиксели; дальше дисплей показывает их сам, без участия хозяина. Память кадра у микроконтроллера не обязательна — рисовать можно кусками. Это путь F411, F103 и любого камня с SPI (шина — раздел 18, практика — 83.3).
Тот же дисплей со своим контроллером бывает и с параллельной шиной 8080 (8 или 16 бит). Её подключают к FSMC/FMC, и для микроконтроллера дисплей становится внешней памятью: команда и пиксель — запись по одному из двух адресов. Так делают на F103 высокой плотности и F407, у которых LTDC нет.
Дисплей без контроллера — параллельный RGB. Панель 480×272 или 800×480 с 40-контактным шлейфом сама ничего не хранит: ей нужно непрерывно подавать пиксели с тактом, синхроимпульсами и полями, 60 раз в секунду, как монитору. Этим занимается LTDC — он читает буфер кадра из памяти и гонит его на выводы, пока включён. У F411 такого блока нет (в даташите в списке интерфейсов его нет), он есть у F429/F439, F7 и H7 — и у них же есть DMA2D.
Выбор пути делает не библиотека, а размер экрана: до 320×240 обычно SPI, дальше — параллельная шина: 8080 через FSMC к дисплею со своим контроллером или RGB через LTDC к панели без него, потому что SPI не успевает (22а.3). У F469, F769 и H747 есть ещё MIPI DSI — быстрый последовательный интерфейс к панелям.
22а.2 Буфер кадра: сколько памяти
| Экран | Формат | Байт |
|---|---|---|
| 240×240 | RGB565, 2 байта | 115 200 |
| 240×320 | RGB565 | 153 600 |
| 480×272 | RGB565 | 261 120 |
| 480×272 | ARGB8888, 4 байта | 522 240 |
| 800×480 | RGB565 | 768 000 |
| 800×480 | ARGB8888 | 1 536 000 |
У F411 128 КБ ОЗУ — это 131 072 байта: кадр 240×320 не помещается, 240×240 помещается с трудом. У F429 внутренней памяти 256 КБ, из них 64 — только для ядра; кадр 480×272 в 16 битах туда не входит. Поэтому платы с LTDC несут внешнюю SDRAM на шине FMC (раздел 41), а буфер кадра лежит там, и LTDC читает его через шину непрерывно.
Когда буфера нет или он мал, рисуют полосами: библиотека отрисовывает прямоугольник в небольшой буфер — например, десять строк, — и отправляет его дисплею со своим контроллером, затем следующий. Два таких буфера по очереди позволяют DMA слать один, пока процессор рисует второй.
22а.3 Кадр по SPI: сколько это времени
Кадр 240×320 в RGB565 — 1 228 800 бит. У F411 SPI разгоняется до 50 Мбит/с (даташит), но на макетных проводах выше 8–10 МГц не работает (раздел 18):
| SPI | Кадр | Кадров в секунду |
|---|---|---|
| 50 Мбит/с | 24,6 мс | 40,7 |
| 36 Мбит/с | 34,1 мс | 29,3 |
| 25 Мбит/с | 49,2 мс | 20,3 |
| 9 Мбит/с | 136,5 мс | 7,3 |
Отсюда два правила. Первое: перерисовывать только то, что изменилось —
библиотеки интерфейса ведут список «грязных» прямоугольников и шлют дисплею
только их; цифра на часах меняется за доли миллисекунды, а не за 25. Второе:
слать через DMA, иначе процессор всё время кадра занят перекладыванием
байтов и интерфейс перестаёт отвечать на сенсор. И грабля из 83.3 никуда не
делась: дисплей ждёт старший байт первым, а uint16_t в памяти лежит младшим
вперёд — либо 16-битный режим SPI, либо перестановка байтов.
22а.4 LTDC: кадр как у монитора
Параллельной панели нужен не только поток пикселей, но и «рамка» вокруг
каждой строки и каждого кадра — как у аналогового монитора: импульс синхронизации,
заднее поле, активная область, переднее поле. Пример из руководства для 640×480:
HSYNC 8 тактов, заднее поле 7, активных 640, переднее поле 6; по вертикали
VSYNC 4 строки, поле 2, активных 480, поле 2. Всего строка — 661 такт, кадр —
488 строк, 322 568 тактов на кадр; чтобы показывать 60 кадров в секунду,
тактовая частота пикселей должна быть 19,35 МГц. Её даёт отдельная ФАПЧ
PLLSAI.
В регистры пишутся не сами поля, а накопленные суммы минус один — так удобнее счётчику:
LTDC_SSCR = 0x00070003 HSYNC − 1 = 7, VSYNC − 1 = 3
LTDC_BPCR = 0x000E0005 HSYNC + HBP − 1 = 14, VSYNC + VBP − 1 = 5
LTDC_AWCR = 0x028E01E5 … + 640 − 1 = 654, … + 480 − 1 = 485
LTDC_TWCR = 0x029401E7 … + HFP − 1 = 660, … + VFP − 1 = 487
Как и в остальных трёх, ширина и высота лежат в одном регистре: у TWCR
полная ширина — в битах 27:16, полная высота — в битах 10:0. В примере
руководства и на рисунке ниже вторая половина подписана THCR, но
отдельного регистра с таким именем у LTDC нет: следом за TWCR по
смещению 0x18 идёт GCR.
Что умеет контроллер: два слоя, которые смешиваются снизу вверх — сначала слой 1 с фоновым цветом, потом слой 2 с результатом; у каждого слоя своё окно, свой формат пикселя (8 форматов: ARGB8888, RGB888, RGB565, ARGB1555, ARGB4444, L8, AL44, AL88) и своя таблица цветов на 256 записей для индексных форматов; прозрачность — постоянная или попиксельная; цветовой ключ; сглаживание младших разрядов для 18-битных панелей. Часть регистров теневая: новые значения вступают в силу по перезагрузке — сразу или в вертикальном гашении, чтобы переключить буфер кадра между двумя кадрами, а не посреди строки.
Цена: LTDC читает память непрерывно. Панель 480×272 в RGB565 при 60 Гц — 15,7 МБ/с по шине; 800×480 в 32 битах — 92,2 МБ/с. Этот поток идёт по AHB всегда, и с ним конкурируют процессор, DMA2D и все остальные DMA; в SDRAM он упирается первым.
22а.5 DMA2D: рисует не процессор
Заливка прямоугольника, копирование картинки, вывод шрифта — это тысячи одинаковых обращений к памяти, и DMA2D делает их вместо ядра. Четыре режима:
| Режим | Что делает | В HAL |
|---|---|---|
| регистр → память | залить прямоугольник цветом | DMA2D_R2M |
| память → память | скопировать блок пикселей | DMA2D_M2M |
| … с преобразованием формата | RGB565-картинку положить в ARGB8888-буфер | DMA2D_M2M_PFC |
| … с преобразованием и смешением | наложить с прозрачностью на фон | DMA2D_M2M_BLEND |
Входные форматы — те же, что у LTDC, плюс чисто «прозрачностные» A8 и A4:
в них нет цвета, только альфа, а цвет задаётся регистром — так хранят
шрифты: глиф — маска прозрачности, цвет текста — константа. Формат без
альфы (RGB565) получает альфу 255 автоматически; её можно подменить константой
или умножить на неё.
Главная тонкость — смещение строки (line offset, в HAL OutputOffset и
InputOffset): DMA2D рисует прямоугольник шириной Width внутри буфера, у
которого строка длиннее. Смещение — сколько пикселей пропустить в конце каждой
строки, то есть ширина буфера минус ширина прямоугольника. Забыть его — получить
картинку, съезжающую наискось.
hdma2d.Init.Mode = DMA2D_R2M; /* заливка */
hdma2d.Init.ColorMode = DMA2D_OUTPUT_RGB565;
hdma2d.Init.OutputOffset = 480 - 100; /* буфер 480 в ширину, прямоугольник 100 */
HAL_DMA2D_Init(&hdma2d);
HAL_DMA2D_Start(&hdma2d, 0xFFFF0000u, (uint32_t)(fb + y * 480 + x), 100, 40); /* красный, 100×40 */
HAL_DMA2D_PollForTransfer(&hdma2d, 10);
Цвет заливки HAL принимает всегда в ARGB8888 и сам переводит в формат
выхода: из 0xFFFF0000 для RGB565 получается 0xF800. RGB565-значение
сюда передавать нельзя — 0xF800 HAL прочтёт как зелёный 0xF8 и зальёт
прямоугольник цветом 0x07C0. Адрес назначения — число uint32_t,
поэтому указатель на буфер приводят явно.
Смешение считается по формуле из руководства: αMult = αFG · αBG / 255,
αOUT = αFG + αBG − αMult, C = (CFG · αFG + CBG · αBG − CBG · αMult) / αOUT
для каждой из R, G, B. На числах: передний план 200 с альфой 255 поверх фона 50 —
итог 200 (непрозрачный закрывает всё); альфа 0 — итог 50 (фон); альфа 128 поверх
непрозрачного фона — 125, ровно середина; две полупрозрачные (128 и 128) — альфа
192 и цвет 150. Есть и «мёртвое время» между обращениями DMA2D к шине —
регулятор, чтобы ускоритель не съедал всю полосу у LTDC.
22а.6 Сенсор: откуда берутся касания
Резистивный сенсор — четыре провода на АЦП: поочерёдно подать питание на одну пару, измерить напряжение на другой — координата. Дёшево, работает в перчатках, одно касание, нужна калибровка.
Ёмкостный — отдельный контроллер на стекле, по I²C (раздел 17): он сам
считает касания и отдаёт готовые координаты, а вывод INT сообщает, что данные
есть. Опрашивать по прерыванию, а не в цикле: пока пальца нет, шина свободна.
В обоих случаях координаты сенсора — не координаты экрана: оси могут быть перевёрнуты или переставлены относительно буфера кадра, а размеры отличаются. Пересчёт — одно умножение и сложение на ось, и его подбирают на первом же запуске по углам экрана. Касание, дребезг и «отпускание» превращаются в события для библиотеки: нажал, ведёт, отпустил.
22а.7 Библиотека интерфейса и что она просит от вас
Кнопки, списки, шрифты, анимации пишут не сами — берут библиотеку: фирменную от производителя или свободную. Любая из них хочет от программы одного и того же:
- Куда рисовать — буфер кадра целиком (LTDC) или буфер полосы (SPI-дисплей).
- Как отдать нарисованное — функцию «прямоугольник готов»: для SPI-дисплея это отправка через DMA с последующим «готово», для LTDC — переключение адреса слоя в вертикальном гашении.
- Время — вызов раз в миллисекунду, от него живут анимации и таймауты.
- Ввод — функцию «есть ли касание и где».
Под RTOS (часть IV) отрисовку кладут в отдельную задачу с невысоким приоритетом: пусть кадр дорисовывается на 10 мс позже, чем датчик пропустит отсчёт. Сенсор и связь — выше. Между задачами — очередь событий, а не общие переменные (раздел 27).
22а.8 Грабли
| Симптом | Причина |
|---|---|
| цвета «поплыли», красный стал синим | порядок байт RGB565 по SPI (83.3): нужен 16-битный режим или перестановка |
| картинка съехала наискось | не задано смещение строки в DMA2D или длина строки слоя LTDC |
| по экрану бежит горизонтальный разрыв | буфер меняется во время вывода: переключать в вертикальном гашении, второй буфер |
| панель мерцает или не синхронизируется | тактовая пикселей не та: посчитать из полных размеров с полями, как в 22а.4 |
| интерфейс тормозит на F411 | перерисовка всего кадра целиком: только изменившиеся прямоугольники и DMA |
| буфер кадра «не влезает» | 240×320 в 16 битах — 153 600 байт больше 128 КБ: полосы или другой камень |
| на F7/H7 DMA2D рисует старое | кэш данных: чистить строки перед DMA2D и сбрасывать после |
| текст без сглаживания или с фоном | шрифт положен как картинка, а не как маска A8/A4 с цветом из регистра |
| сенсор «нажимает» не туда | оси сенсора не совпадают с осями буфера — пересчёт и калибровка по углам |
| касания теряются при анимации | сенсор опрашивается из задачи отрисовки: отдельная задача и прерывание по INT |
| всё стало медленным после добавления LTDC | его поток 15–90 МБ/с делит шину с остальными: SDRAM и DMA2D под ним |
22а.9 Итоги
- Два пути: дисплей со своей памятью — по SPI (F411, F103) или по шине 8080 через FSMC (F103 высокой плотности, F407) — и параллельная панель через LTDC из буфера кадра (F429, F7, H7).
- Буфер: 240×320 RGB565 — 153 600 байт, не влезает в 128 КБ F411; 480×272 — 261 120, живёт в SDRAM; без буфера — полосы и два буфера под DMA.
- SPI: кадр 240×320 — 24,6 мс на 50 Мбит/с, 136,5 мс на 9; перерисовывать только изменившееся и через DMA.
- LTDC: поля вокруг активной области; 640×480 с полями — 322 568 тактов, 60 Гц требуют 19,35 МГц; регистры хранят накопленные суммы минус один; два слоя, теневые регистры, непрерывный поток 15,7–92,2 МБ/с по шине.
- DMA2D: заливка, копирование, преобразование, смешение; смещение строки = ширина буфера − ширина прямоугольника; шрифты как маски A8/A4; формула смешения с проверенными числами 200/50/125/150.
- Сенсор: резистивный на АЦП или ёмкостный по I²C с выводом INT; пересчёт осей.
- Библиотеке нужны буфер, функция вывода, тик и ввод; отрисовка — отдельная задача с невысоким приоритетом.
Дальше: раздел 22б «Камера по DCMI: кадр в память через DMA» — обратный путь: кадр с камеры в память через DCMI и DMA.