‮Сдвиг по фазе (kincajou) wrote,
‮Сдвиг по фазе
kincajou

Category:

worklog: STM32 LTDC, первые шаги.

Поделюсь скромными результатами экспериментов по запуску LCD контроллера, встроенного в STM32F746 и аналогичные ему чипы.

Итак, необходимые исходные данные по плате Awesome Board и дисплейной панели, которую я буду использовать:
  • SDRAM шириной 32 бита, работающая на тактовой 108 МГц
  • 24-битное подключение панели (через SerDes - он работает и совершенно прозрачен с т.з. связки контроллер+панель)
  • 800*480, 24-битный цвет (там, похоже, не 8, а реально всего 6 бит на цветовую компоненту, но щас нас это не волнует)
  • кисельная пиксельная частота в пределах от (???) до 40 МГц, типичное значение 33 МГц
  • геометрические данные и синхроимпульсы, горизонтальные: видимая зона 800 пикс, синхроимпульс 48 пикс, porch (как адекватно перевести? не "крыльцо" же) передний 40, задний 40 пикс - всего 928 пикс
  • та же фигня для вертикали: видимая зона 480 пикс, синхроимпульс 3 пикс, porch передний 13, задний 29 пикс - всего 525 пикс


Сначала прикидываем, какая нам нужна пиксельная частота. Исходим из предположения, что кадровая частота будет порядка 60..70 герц, плюс-минус. Полный размер кадра - 928*525 = 487200 пикселей (видимых и невидимых, считать нужно все), соотв., такое количество пиксельных синхроимпульсов приходится на один кадр. Умножаем сие на 60 герц, получаем 29232000 герц пиксельной частоты. Число неудобное, некруглое, корявое. Подгоняем его туда-сюда, оставаясь в пределах рекомендуемых значений -- останавливаемся на 36 МГц. Очень удобно для канала PLLSAI, конкретные настройки тут не затрагиваются (ибо вариантов больше одного).

Далее определимся с форматом и организацией видеоданных. Контроллер поддерживает т.н. "слои", числом до двух штук - это абстракция, позволяющая логически разделить массив данных в памяти и отображение на экране. Слой может быть равен по размерам или меньше, чем физический экран (я пока не понял, может ли он быть больше - но это тоже отдельная тема). Контроллер читает данные из обоих слоёв, преобразует (при необходимости) формат пикселей, компонует их при помощи альфа-блендинга, подмешивает дизер (если надо) и выдаёт по физическому интерфейсу в панель. Цвет может задаваться непосредственно компонентами или же, как в старые добрые времена, палитрой - в памяти при этом лежат ИНДЕКСЫ цвета. Формат пикселей может иметь альфа-компоненту. Слой так же имеет свою альфа-компоненту. Схема блендинга довольно запутанная, я пока в ней не разбирался.

Важно тут то, что чем шырше пиксел, тем шустрее должна работать память, чтобы успевать прокачивать данные. Т.е. если у нас 32-битные пикселы (8 бит альфа + RGB), то скорость их извлечения из памяти должна быть не меньше, чем пиксельная частота. С одной стороны, говно вопрос - 108 мегагерц на шине как бы дают почти трёхкратный запас. С другой стороны, не стоит забывать, что видеоконтроллер это не единственный потребитель полосы пропускания, плюс к тому есть всякие оверхеды и прочие задержки. Так что 36 МГц пиксельной это уже близко к пределу, если использовать широкий цвет. Для фоторамки, которая ничем особым больше не занята и всю SDRAM использует лишь под картинки, это вполне терпимо. Но естественным образом возникает вопрос, можно ли схалтурить?

Можно! я не зря упомянул палитровый цвет - в таком случае 24-битные пикселы получаются из 8-битных индексов посредством отображения их на Color Look-Up Table. К счастью, этот CLUT находится в совершенно отдельном адресном пространстве и обращение к нему не занимает пропускную способность памяти вовсе. Просто заносим через специальный регистр 256 записей, сопоставляя номер цвета с его фактическим значением, и включаем (потом) режим 8-битного индексированного цвета, и вуаля -- для моей задачи этого будет более чем достаточно.

Слоёв два, CLUT у каждого свой, плюс в этом формате так же можно использовать альфа-блендинг. Это круче, чем VGA!

Так вот, настраиваем тайминги, идя прям по порядку регистров, как они в референс-мануале: LTDC->SSCR, LTDC->BPCR и так далее (без STM32CubeMx - вместо помощи он всё только сильнее запутывает, а понять, как всё это хозяйство работает, можно лишь читая мануалы и документацию на чип). Не забываем, что в эти регистры заносятся не величины всех этих импульсов, а абсолютная "пиксельная координата", т.е. если пишем в регистр, задающий ширину видимой зоны, то заносим туда суммарное значение "синхроимпульс + porch + ширина видимой зоны" (минус 1 пиксель), это т.н. accumulated value. Так же не забываем настроить полярность синхроимпульсов - есть панели, у которых эти сигналы могут иметь инверсию.

Включаем контроллер: LTDC->GCR |= LTDC_GCR_LTDCEN
Не забываем ещё физически включить дисплей (некоторые модели - в частности, используемый мной - требуют дополнительного сигнала разрешения работы) и подсветку.
Если мы настроили всё верно и прописали цвет фона, то дисплей включится, на управляющих пинах появятся синхроимпульсы и поверхность засветится равномерной заливкой - контроллер начал что-то выводить. Ура!
DSCN2160
Тут заливка не очень-то равномерная - у дисплейчка довольно-таки печально всё с углами обзора. Зато дёшево!


Теперь надо прикинуть, собсно, какие слои мы будем использовать и куда их запихнуть.
Сейчас мне представляется так: слой 1 (физически он всегда внизу, под слоем 2) будет использоваться под GUI. Формат пикселей 8 бит CLUT без альфы (L8), размер совпадает с отображаемой зоной на дисплее, т.е. 800*480. Т.е. требуется один байт на пиксель.
Слой 2, который наверху, будет использоваться под активно обновляемое изображение - спектр, панорамный индикатор, осциллограмма или что-то ещё. Формат пикселей 8-бит CLUT с альфа-каналом (AL88) - два байта на пиксель. Размер этого "окна" будет меньше физического дисплея. Скажем, 500*200. За счёт альфа-блендинга можно будет под этот слой подложить условно-статичный фон, например, с какой-нибудь шкалой.

Прикидываем требуемые объёмы памяти. Вспоминаем, что есть такая штука, как двойная буферизация - пока рисуется один фреймбуффер, процессор занят работой с другим (если надо) и наоборот. Подменять их удобно, когда идёт период "обратного луча" (тот самый porch), для чего предусмотрено прерывание по номеру строки: записываем в регистр LTDC->LIPCR строчку, которая находится в "тёмной" зоне, влючаем прерывание Line interrupt и вуаля, соответствующий обработчик может жонглировать адресом фреймбуффера.

Исходя из нужных нам объёмов и зная, по каким адресам у нас сидит память, совершенно произвольно назначаем стартовый адрес фреймбуфера.
Заносим параметры его логической геометрии в соответвующие регистры слоя. Там есть масса нюансов, связанных с кэшированием и предсказывающим чтением из памяти, так же с 64-байтовым бурстом чтения и read boundary - это пока не важно, можно подкрутить потом.

В регистр WHPCR (Window Horizontal Start Position) заносим значение (Horz.SyncWidth + Horz.BackPorch) в поле WHSTPOS и (Horz.SyncWidth + Horz.BackPorch + Horz.DisplayArea - 1) в поле WHSPPOS, аналогично для регистра WVPCR (только вертикальное измерение). Первое значение задаёт нулевой ЛОГИЧЕСКИЙ пиксел окна относительно ФИЗИЧЕСКОГО пиксела на дисплее по соответвующей оси, второе значение задаёт последний логический пиксел. Таким образом, когда мы пишем в слой по координатам {0,0}, электроника преобразует это в {(Horz.SyncWidth + Horz.BackPorch), (Vert.SyncWidth + Vert.BackPorch)}, то есть этот "нулевой" пиксел попадёт прям в самое начало видимой зоны - туда, где уже закончились синхроимпульсы.

Так же настраиваем альфу для всего слоя (0 - полностью прозрачный, 255 - полностью непрозрачный), задаём цвет заливки ЗА пределами слоя (т.е. если он занимает лишь часть площади экрана, то вокруг него будет эта заливка), специальные константы, устанавливающие режим блендинга, а так же ОТСТУП строки (параметр, определяющий промежуток между началом одной строки и началом следующей) и ДЛИНУ строки. Это две разные, хоть и очень тесно связанные, сущности - нужные как раз для преодоления сложностей с read boundary и т.п. В простейшем случае эти тонкости можно проигнорировать и записать отступ строки (в байтах), равный, собсно, длине строки. Длина строки заносится с поправкой +3 байта (т.е. если у нас 800 пикселей и 1 байт на пиксел, то надо записать 803).
Записываем количество строк. Я не знаю, почему это сделано так, ведь количество строк уже как бы известно из содержимого регистра WVPCR - достаточно от координаты последней строки отнять координату первой.. ну да ладно, надо так надо.
Записываем физический адрес начала рабочего фреймбуфера (как уже написал, более-менее произвольная вещь и его можно переписывать прям во время работы, подменяя один буфер другим): LTDC_Layer1->CFBAR = (uint32_t)FB1_Start;
Включаем слой и, одновременно, работу с CLUT: LTDC_Layer1->CR = LTDC_LxCR_CLUTEN | LTDC_LxCR_LEN;

А самое главное - указываем, что обновление регистров должно сработать во время vertical blanking: LTDC->SRCR = LTDC_SRCR_VBR

Если этого не сделать, то все вышеобозначенные значения останутся сидеть в т.н. "теневых регистрах" (они нужны для синхронизации между сверхбыстрой внутренней шиной микроконтроллера и довольно-таки тормозными структурами внутри видеоблока).

Всё, теперь можно навалить пикселей во фреймбуфер и наслаждаться результатом (если на дисплее вы видите кашу из цветных пикселов, то вспомните, что содержимое оперативной памяти, даже SDRAM, при рестарте процессора само не обнуляется!)
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 3 comments