worklog: STM32 LTDC, первые шаги.
Сначала прикидываем, какая нам нужна пиксельная частота. Исходим из предположения, что кадровая частота будет порядка 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
Не забываем ещё физически включить дисплей (некоторые модели — в частности, используемый мной — требуют дополнительного сигнала разрешения работы) и подсветку.
Если мы настроили всё верно и прописали цвет фона, то дисплей включится, на управляющих пинах появятся синхроимпульсы и поверхность засветится равномерной заливкой — контроллер начал что-то выводить. Ура!
Тут заливка не очень-то равномерная — у дисплейчка довольно-таки печально всё с углами обзора. Зато дёшево!
Теперь надо прикинуть, собсно, какие слои мы будем использовать и куда их запихнуть.
Сейчас мне представляется так: слой 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, при рестарте процессора само не обнуляется!)
Запускаем дисплей на STM32 через LTDC… на регистрах
Приветствую! Недавно для проекта потребовалось запустить дисплей, который имел интерфейс LVDS. Для реализации задачи был выбран контроллер STM32F746, т.к. я с ним уже достаточно много работал и у него есть модуль LTDC, который позволяет работать напрямую с дисплеем без контроллера. В данном случае контроллер реализован уже внутри микроконтроллера. Так же не последним доводом было то, что на данном камне есть отладка STM32F746-Disco, которая у меня была под рукой, а значит я мог начинать работу над проектом не дожидаясь пока ко мне приедет плата, компоненты и прочее.
Сегодня я расскажу как запустить модуль LTDC, работая с регистрами (CMSIS). HAL и прочие библиотеки не люблю и не использую по религиозным убеждениям, но в этом и интерес. Вы увидите, что поднимать сложную периферию на регистрах так же просто, как и обычный SPI. Интересно? Тогда поехали!
1. Немного о LTDC
Данный модуль периферии по своей сути является контроллером, который обычно стоит на стороне дисплея, например, SSD1963 и ему подобные. Если посмотрим на структуру LTDC, то увидим, что физически это параллельная шина на 24 бита + аппаратный ускоритель графики + массив данных в ОЗУ, который является по факту буфером дисплея (frame buffer).
На выходе мы имеем обычную параллельную шину, которая в себе содержит 24 бита цвета (по 8 бит на цвет модели RGB), линии синхронизации, линия включения/отключения дисплея и pixel clock. Последний по факту является сигналом тактирования по которому загружаются пиксели в дисплей, то есть если частота у нас 9.5 МГц, то за 1 секунду мы можем загружать 9.5 млн. пикселей. Это в теории разумеется, на практике цифры несколько скромнее из-за таймингов и прочего.
Для более подробного ознакомления с LTDC я советую вам прочитать несколько документов:
2. А что нам нужно сделать?
Микроконтроллеры от ST обрели популярность не зря, важнейшим требованием к любым электронным компонентов — это документация, а с ней все хорошо. Сайт конечно ужасный, но на всю документацию я оставлю ссылки. Производитель избавляет нас от мучений и изобретения велосипеда, поэтому на странице 520 в reference manual RM0385 черным по белому расписано по шагам, что нам надо сделать:
На деле половину из описанного делать не придется: оно либо не нужно для старта, либо уже настроено по умолчанию. Для минимального старта, который нам позволит рисовать пиксели, выводить картинки, графики, текст и прочее достаточно сделать следующее:
- Включить тактирование модуля LTDC
- Настроить систему тактирования и частоту вывода данных (pixel clock)
- Настроить порты ввода-вывода (GPIO) на работу с LTDC
- Настроить тайминги для нашей модели дисплея
Настроить полярность сигналов. Уже сделано по умолчаниюУказать цвет фона дисплея. Его пока не увидим, можно оставить «по нулям»- Настроить реальный размер видимой зоны дисплея для конкретного слоя
- Выбрать формат цвета: ARGB8888, RGB 888, RGB565 и т.д.
- Указать адрес массива, который будет выступать в роли frame buffer-а
- Указать количество данных в одной линии (длину по ширине)
- Указать количество линий (высота дисплея)
- Включить слой с которым мы работаем
- Включить модуль LTDC
3. Настройка системы тактирования
Первым пунктом нам необходимо подать тактовый сигнал на модуль LTDC, это делается записью в регистр RCC:
Далее необходимо настроить частоту тактирования от внешнего кварца (HSE) на частоту 216 МГц, то есть на максимальную. Первым шагом включаем источник тактирования от кварцевого резонатора и ждем флага готовности:
Теперь настраиваем задержку для flash памяти контроллера, т.к. она не умеет работать на частоте ядра. Ее значение как и остальные данные берутся из reference manual:
Теперь для получения искомой частоты я разделю 25 МГц со входа на 25 и получу 1 МГц. Далее просто в PLL умножаю на 432, т.к. в дальнейшем есть делитель частоты с минимальным значением /2 и на него нужно подать удвоенную частоту. После этого подключаем вход PLL к нашему кварцевому резонатору (HSE):
Теперь включаем PLL и ждем флага готовности:
В качестве источника системной частоты назначаем выход нашего PLL и ждем флага готовности:
На этом общая настройка тактирования заканчивается и мы переходим к настройке тактовой частоты (PLLSAI) для нашего дисплея (pixel clock). Сигнал для PLLSAI согласно даташиту берется после делителя /25, то есть на входе мы имеем 1 МГц. Нам нужно получить частоту около 9.5 МГц, для этого частоту 1 МГц умножаем на 192, а затем с помощью двух делителей на 5 и на 4 получаем искомое значение PLLSAI = 1 МГц * 192 / 5 /4 = 9,6 МГц:
Финальным шагом мы включаем PLLSAI для дисплея и ждем флага готовности к работе:
На этом базовая настройка системы тактирования завершена, единственное, чтобы не забыть и потом не страдать, давайте включим тактирование на все порты ввода-вывода (GPIO). Питание у нас не батарейное как минимум на отладке, поэтому не экономим:
4. Настройка портов ввода-вывода (GPIO)
С настройкой gpio все очень просто — у нас все ноги шины LTDC должны быть настроены как альтернативный выход и на высокую частоту. Для этого в reference manual на странице 201 у нас есть вот такая подсказка:
В таблице указано какие биты в регистрах нужно выставить, чтобы получить необходимую настройку. Стоит отметить, что все подтяжки у нас отключены. Где же посмотреть какую альтернативную функцию включить? А для этого идем на страницу 76 в datasheet на наш контроллер и смотрим на такую таблицу:
Как видите логика таблицы простейшая: находим нужную нам функцию, в нашем случае LTDC B0, далее смотрим на каком GPIO она находится (PE4, например) и наверху смотрим номер альтернативной функции, который будем использоваться для настройки (AF14 у нас). Чтобы настроить наш вывод как push-pull выход с альтернативной функцией LTDC B0 нам нужно написать следующий код:
Я привел пример для вывода PE4, который соответствует выводу B0 на шине LTDC, то есть это нулевой бит синего цвета. Для всех остальных выводов настройка идентична, отдельного внимания заслуживают лишь 2 вывода, один из готовых включает дисплей, а другой его подстветку. Настраиваются они как обычный выход push-pull, который все используют для мигания светодиодом. Выглядит настройка вот так:
Данная настройка для вывода PK3, который включает и выключает нашу подсветку. Его кстати можно еще и ШИМовать, чтобы плавно регулировать яркость. Для PI12, который включает дисплей (DISP) все аналогично. Скорость на этих 2-х выводах по умолчанию низкая, т.к. каких-то высокочастотных действий от них не требуется.
Все остальные порты ввод-вывода вы можете посмотреть самостоятельно на схеме отладочной платы, либо на принципиальную схему собственного устройства.
5. Тайминги и их настройка
Тайминги с физический точки зрения представляют из себя обычные задержки. Думаю вы ни раз наблюдали различные извращения типа delay(1), когда смотрели примеры кода на дисплеи с контроллерами SPI/I2C на подобии ILI9341. Там задержка нужна для того, чтобы контроллер, например, успел принять команду, выполнить ее и затем уже что-то делать с данными. В случае с LTDC все примерно так же, только городить костыли мы не будем да и не зачем — наш микроконтроллер сам аппаратно умеет формировать необходимые тайминги. Зачем они нужны на дисплее, где нет контроллера? Да элементарно чтобы после заполнения первой линии пикселей перейти на следующую линию и вернуться в ее начало. Это связано с технологией производства дисплеев, а следовательно у каждой конкретной модели дисплея тайминги могут быть свои.
Чтобы узнать какие значения нам понадобятся идем на сайт ST и смотрим схему отладочной платы STM32F746-Disco. Там мы можем увидеть, что используется дисплей RK043FN48H-CT672B и документация на него доступна, например, тут. Нас больше всего интересует таблица на странице 13 в разделе 7.3.1:
Вот они наши значения, которые нам понадобятся при настройке. Так же в документации есть еще много чего интересного, например, диаграммы сигналов на шине и прочее, что может вам понадобиться если вы, например, захотите поднять дисплей на FPGA или CPLD.
Переходим к настройкам. Первым делом, чтобы не держать в голове данные значения, оформлю их в виде дефайнов:
Тут есть интересная особенность. Тайминг Pulse Width, который именуется у нас DISPLAY_HSYNC, имеет значение в таблице только для частоты pixel clock в 5 МГц, а для 9 и 12 МГц его нет. Данный тайминг необходимо подобрать под свой дисплей, у меня это значение получилось 30, когда в примерах от ST оно было иное. При первом запуске если у вас будет ошибка с его настройкой, то изображение будет сдвинуто или влево или вправо. Если вправо — уменьшаем тайминг, если влево — увеличиваем. По факту он влияет на начало координаты видимой зоны, в чем мы дальше и убедимся. Просто имейте ввиду, а помочь осмыслить весь этот абзац поможет следующая картинка со страницы 24 нашего AN4861:
Тут удобна небольшая абстракция. У нас есть 2 зоны дисплея: видимая и общая. Видимая зона имеет размеры с заявленным разрешением 480 на 272 пикселя, а общая зона — это видимая + наши тайминги, которых по 3 на каждую сторону. Так же стоит понимать (это уже не абстракция), что один системный тик равен 1 пикселю, поэтому общая зона имеет размер 480 пикселей + HSYNC + HBP + HFP.
Еще стоит осознать, что чем меньше тайминги, тем лучше — быстрее будет обновляться дисплей и частота кадров немного вырастет. Поэтому после первого запуска поэкспериментируйте с таймингами и уменьшите их насколько возможно при этом сохранив стабильность работы.
Для настройки таймингов я сделал себе на будущее небольшую «шпаргалку» внутри проекта, она вам тоже поможет понять какую конкретно цифру и куда ее вписывать:
Откуда собственно эта «шпаргалка» взялась… Во-первых, похожую «формулу» вы видели пару абзацев до этого. Во-вторых, идем на страницу 56 нашего AN4861:
Правда я надеюсь, что вы поняли физический смысл таймингов еще до появления данной шпаргалки и уверен, что наверняка бы и сами смогли ее составить. Сложно в ней ничего нет, а картинки из RM и AN помогают визуально понять влияние таймингов на процесс формирования изображения.
Теперь пришла пора написать код, который эти тайминги настроит. В «шпаргалке» указаны бита регистра в которые писать, например, TOTALH и после знака равно формула, дающая на выходе некое число. Понятно? Тогда пишем:
А на этом с таймингами все! В данном разделе можно еще разве, что настроить цвет фона. У меня он по умолчанию черный, поэтому записан нулем. Если вы хотите изменить цвет фонового слоя (background), то можете после равно записать любое значение, например, 0xFFFFFFFF и залить все белым:
Есть в reference manual-е замечательная иллюстрация, которая наглядно демонстрирует, что у нас имеется по сути 3 слоя: background, layer 1 и layer 2. Фоновый слой «кастрирован» и умеет лишь заливаться одним конкретным цветом, но тоже бывает невероятно полезен при реализации дизайн будущего GUI. Так же данная иллюстрация наглядно демонстрирует приоритет слоев из чего следует, что мы увидим цвет заливки на background, только когда на остальные слои или пустые или прозрачные.
В качестве примера покажу одну из страниц проекта, где в процессе реализации шаблона background был залит один цветом и контроллер перерисовывал не страницу целиком, а только отдельные сектора, что позволяло получать около 50-60 фпс при множестве других задач:
6. Финальная часть настройки LTDC
Настройки LTDC делятся на 2 раздела: первые являются общими для всего модуля LTDC и находятся в группе регистров LTDC, а вторые настраивают один из двух слоев и находятся в группе LTDC_Layer1 и LTDC_Layer2.
Общие настройки мы выполнили в предыдущем пункте, к ним относится настройка таймингов, фонового слоя. Теперь переходим к настройке слоев и по нашему списку необходимо реальный размер видимой зоны слоя, который описывается в виде 4-х координат (x0, y0, x1, y2), позволяющие получить размеры прямоугольника. Размер видимого слоя может быть меньше разрешения дисплея, никто не мешает сделать размер слоя 100 на 100 пикселей. Для настройки размера видимой зоны напишем следующий код:
Как видите тут все просто как и с таймингами. Начальные точки (x0, y0) видимой зоны состоят из суммы двух таймингов: HSYNC + HBP и VSYNC + VBP. Для вычисления координат конечной точки (x1, y1) к данным значения просто добавляется ширина и высота в пикселях.
Теперь необходимо настроить формат принимаемых данных. Максимальное качество получается при использовании формата ARGB8888, но при этом мы получаем и максимальный объем занимаемой памяти. Один пиксель занимаем 32 бита или 4 байта, а значит весь экран занимает 4*480*272 = 522 240 байт, то есть половину flash-памяти нашего не самого слабого контроллера. Пугаться не стоит — подключение внешней SDRAM и Flash-памяти по QSPI решают проблемы с памятью и ограничений на данный формат нет, радуемся хорошему качеству. Если вы хотите сэкономить место или у вас дисплей не поддерживает формат 24 бита, то для этого используются более подходящие модели, например, RGB565. Очень популярный формат как для дисплеев, так и для камер, а главное при его использовании 1 пиксель занимаем всего 5+6+5 = 16 бит или 2 байта. Соответственно объем памяти, занимаемый слоем, будет в 2 раза меньше. По умолчанию в контроллере уже настроен формат ARGB8888 и имеет следующий вид:
Если вам нужен другой формат, отличный от ARGB8888, то идем на страницы 533 и 534 в reference manual-е и выбираем нужный формат из предложенного списка:
Теперь создадим массив и передадим его адрес в LTDC, он превратится во frame buffer и будет являться «отражением» нашего слоя. Например, вам нужно 1-й пиксель в 1-й строке залить белым цветом, для этого вам достаточно записать значение цвета (0xFFFFFFFF) в первый элемент этого массива. Надо залить 1-й пиксель во 2-й строке? Тогда тоже значение цвета запишем в элемент с номером (480+1). 480 — сделает перенос строки, дальше добавляет номер в нужной нам строке.
Выглядит это настройка вот так:
По хорошему следует после настройки LTDC настроить еще и SDRAM, чтобы убрать модификатор const и получить frame buffer именно в ОЗУ, т.к. собственной ОЗУ МК не хватает даже на один слой при 4 байтах. Хотя это не помешает протестировать правильность настройки периферии.
Далее необходимо указать значение альфа-слоя, то есть прозрачность для нашего слоя Layer2, для этого записываем значение от 0 до 255, где 0 — полностью прозрачный слой, 255 — полностью непрозрачный, то есть 100% видимый:
Согласно нашему плану теперь необходимо записать размеры нашей видимой области дисплея в байтах, для этого записываем в регистры соответствующие значения:
Осталось два последних шага, а именно включение слоя №2 и самого модуля периферии LTDC. Для этого записываем соответствующие биты:
На этом настройка нашего модуля закончена и можно работать с нашим дисплеем!
7. Немного о работе с LTDC
Вся работа с дисплеем сводится теперь лишь к записи данных в массив imageLayer2, он имеет размер 480 на 272 элемента, что полностью соответствует нашему разрешению и намекает на простую истину — 1 элемент массива = 1 пиксель на дисплее.
Я в качестве примера записал в массив картинку, которую преобразовал на в программе LCD Image Converter, но на деле вряд ли ваши задачи ограничатся этим. Есть два пути: использование готового GUI и написание его собственноручно. Для относительно простых задач типа вывода текста, построение графиков и подобное советую написать свой GUI, это займет немного времени и даст вам полное понимание его работы. Когда же задача большая и сложная, а времени на разработку своего GUI нет, то советую обратить внимание на готовые решения, например, uGFX и ему подобные.
Символы текста, линии и прочие элементы по своей сути являются массивами пикселей, соответственно для их реализации вам самостоятельно нужно реализовать логику, но начать стоит с самой базовой функции — «вывод пикселя». Она должна принимать 3 аргумента: координату по Х, координату по Y и соответственно цвет в который данный пиксель окрашивается. Выглядеть это может например так:
После того как мы приняли координаты в функцию, мы пересчитываем их в номер массива, который соответствует данной координате и затем записываем принятый цвет в полученный элемент. На основе данной функции дальше можно уже реализовывать функции вывода геометрии, текста и прочие «плюшки» GUI. Думаю идея понятна, а как ее воплотить в жизнь уже на ваше усмотрение.
Как видите реализация даже сложной периферии на регистрах (CMSIS) является не сложной задачей, вам достаточно понять как оно работает внутри. Конечно нынче модно разрабатывать встроенное ПО без понимания происходящего, но это тупиковый путь, если вы планируете стать инженером, а не…
Если сравнить полученный код с решением на HAL или SPL, то можно заметить, что код написанный на регистрах более компактный. Добавив где нужно пару комментариев и обернув в функции мы получаем читаемость как минимум не хуже, чем у HAL/SPL, а если вспомнив, что reference manual документирует именно регистры, то работа с использованием CMSIS является более удобной.
1) Проект с исходниками в TrueSTUDIO можно скачать тут
2) Для тех, кому удобнее посмотреть на GitHub
3) Утилиту для конвертации изображения в код LCD Image Converter скачиваем тут
STM32F746GDISCOVERY. Продолжение индустриального графического интерфейса
Эта отладочная плата была приобретена в направлении других горизонтов развития, однако, как и прежде, с чего-то нужно оживлять железо. Эти отладочные платы с мониторчиками и Toush Screen – прежде всего прекрасный набор кнопок (черная и голубая в набор не входят).
Чего не хватало в предыдущем проекте калькулятора? – В основном памяти для картинок. Здесь в полном распоряжении 16 Мбайт QSPI. Можно ни на чем не экономить.
При попытке использования более-менее свежего железа всегда возникают неожиданности.
Не смотря на то, что MXCube предлагает выбор версии IAR для генерации кода, 7-я версия выдала кучу ошибок, устранить которые я не смог.
8-я версия IAR выдала ошибки инициализации DMA2D в режиме DMA2D_M2M_BLEND, обругав 2 параметра. Поскольку эти параметры мне были особо не нужны, я их просто убил.
В IAR есть кнопочки Download and Debug и Debug without Downloading
Так вот, если в программе оставить только функции инициализации и ничего значимого не делать, то при нажатии Download and Debug IAR отрабатывает Debug without Downloading. То есть загрузчик не отрабатывает и, соответственно, данные во flash память не грузятся! В примере грузятся, у Вас не грузятся. Можно хорошо скоротать несколько дней!
SDRAM
Несколько озадачило начало работы. Через программатор все работает и светится. Однако, если отключить питание и включить автономно на экране начинает все дрожать, а то появляется и совсем не пойми что. Если понажимать RESET, то вроде как все встает на свои места и начинает работать. Но со стороны комично. В STM32F429i_Discovery такого не было.
Серьезные пацаны подметили, что если включить правильное питание (жрет она 0,6 А), то не дрожит. Все, что нашел в интернете, так у кого-то были плохие конденсаторы возле SDRAM. Посмотрел через хорошую лупу – у меня точно не треснувшие!
Проблема все-таки в SDRAM. При инициализации дисплея, мы сразу привязываем его к определенным адресам буферов. А вот что на момент инициализации в этих адресах лежит не всегда понятно. При серьезном питании память, видимо, сбрасывается. При питании от USB компьютера не всегда.
Помогло! Нажимать смущенно черную кнопку после этого не нужно! Особенно я бы обратил внимание на pLayerCfg1.FBStartAdress. Поскольку уровень по цвету урезанный, то его использование не всегда очевидно, как и аппаратные телодвижения с ним разработчиков.
MPU (memory-protection-unit)
В LCD-TFT display controller (LTDC) on STM32 MCUs есть пример настройки MPU для нашей отладочной платы.
Данные в SDRAM там буферизованы для использования DMA2D,
MPU_InitStruct.IsBufferable = MPU_ACCESS_BUFFERABLE;
поэтому после использования функций библиотеки stm32746g_discovery_lcd.c вперемешку с DMA2D нужно выдавать команду SCB_CleanDCache();
Рассуждения на эту тему можно почитать в обсуждении:
https://kincajou.livejournal.com/4160197.html
Переводы AN4838, AN4839:
http://microsin.net/programming/arm/an4838-managing-memory-protection-unit-stm32.html
http://microsin.net/programming/arm/an4839-level-1-cache-stm32f7.html
QSPI
В CubeMX лапки выставить как в проекте STM32F746GDISCOVERY по умолчанию.
Настройку параметров лучше подсмотреть в примере QSPI_perfs
…Repository\STM32Cube_FW_F7_V1.15.0\Projects\STM32746G-Discovery\Applications\QSPI\QSPI_perfs\EWARM
поскольку в последнем показывается крутость производительности.
Кто не любит EWARM, нашел случайно ссылку по работе с QSPI по VisualGDB
https://sysprogs.com/w/forums/topic/using-the-quadspi-on-stm32f7-discovery/
В примере разработчики пишут в QSPI данные в формате unsigned char. Если работать со словами данных – все тоже получается. (См. STM32CubeExpansion_AN4749_F7_V1.0.0). Однако при подготовке файлов для вывода на LCD через DMA2D в формате байтов а не слов, есть нюанс.
В примере QSPI_perfs добавлена куча настроек IAR
Это линкер c секцией QSPI. Файл в текстовом формате. Можно посмотреть, можно и редактировать. Секцию .textqspi можно назвать по любому и без точки. Однако .text символизирует код и данные.
Дальше начинается самое интересное…
В примере QSPI_perfs есть файл main_images.c.
В описании AN4760 (раздел. 5.1.1 Генерация содержимого буфера кадра из памяти QSPI): «Все изображения и значки, необходимые для приложения, должны храниться во внешней памяти QSPI. В проекте доступно шесть изображений и три иконки, все они включены в файл «main_images.c», где каждое из них определяется как константа в отдельном заголовочном файле.»
Вроде бы все понятно… В stm32f746xx_QSPI_flash.icf мы создаем (уже создан разработчиками STM) раздел для памяти QSPI и директивой #pragma location = «.textqspi» мы обозначаем, что эти константы в этот раздел грузим.
Удобная штука. Все, что в «main_images.c» прописано, начинает грузиться в QSPI. Можно использовать в отдельном проекте, ничего из периферии не описывая. (Такой проект у меня вложен в основной.) Если изменить имя файла, то грузиться не будет. При этом нужно использовать линкер stm32f746xx_QSPI_flash.icf и загрузчик FlashSTM32F7xx_STM32F746G-DISCO.board. Пустую функцию dummy_init в файле «main_images.c» я тоже нигде не нашел. Возможно это намек, что именно так грузить нужно не только данные, но и код.
В следующем разделе AN4760 (5.1.2 Отображение изображений непосредственно из памяти QSPI). На этот пример (AN4749 поставляется с программным пакетом X-CUBE-LPDEMO-F7) я уже ссылался. Точно так же описываются данные. Файла main_images.c уже нет, зато в файле «nenu.c» есть функции WM_HWIN Createloadpicturefrom(void) библиотеки emWin version 5.28, описания которых я тоже поверхностно не нашел.
То есть по файлу «main_images.c» все гуманно! Работает, но не понятно как. Если кто-нибудь проникся, найдите время, пожалуйста, ПОДЕЛИТЕСЬ!
В описании AN4760 есть еще интересный подраздел «Как осуществлять программирование QSPI Flash memory только один раз».
В оригинале QUADSPI AN4760 pg. 58
How to proceed to program QSPI Flash memory only once… for IAR EWARM
Долго пытался разобраться. Так и не понял…
В Гугле ничего не нашел. Задал вопрос на форуме STMicroelectronics. Получил следующий ответ:
clive1 (NFA Crew) (Community Member)
2 months ago
If they are not part of your application image, I’d suggest writing some code to combine them, providing a table the app can access, and output them as a .HEX file, with an address starting at 0x90000000
Write that image with the STM32 Cube Programmer, using the External Loader for the QSPI memory (N25Q128 as I recall), and don’t tell IAR about it. Have you application set up the QSPI memory, and then access your table/images from there using normal memory access, ie pointers, structures, etc.
Что, собственно, и реализовано в файле QSPI_Addr_Table.h с использованием препроцессора …#ifdef IMG_NO_DATA
Но мне кажется, что это не изящно. Если кто-нибудь разобрался, пожалуйста, ПОДЕЛИТЕСЬ!
Touch panel FT5336
К stm32746g_discovery_ts.c как-то сразу возникло много вопросов…
Например функция BSP_TS_GetState, которая возвращает в строке 357 в качестве статуса
ts_status = BSP_TS_Get_GestureId(TS_State);
GestureId ну просто не работает и возвращает всегда ноль. Это не мешает разработчикам, поскольку в дефайнах статус «ОК» тоже ноль.
Строка 415 BSP_TS_ITClear с комментарием /* Clear TS IT pending bits */
Смотрим ft5336.c строка 425 /* Nothing to be done here for FT5336*/
Запросто включают и выключают режим прерываний, который не выключается, а переключается из Interrupt polling mode в Interrupt trigger mode и обратно.
Понимание обработки событий после некоторой исследовательской части окончательно у меня так и не сложилось. В проекте закомментированы функции, отображающие на экране счетчики этих самых событий. Кому интересно, можно посмотреть.
Подключение интерфейса к вашей системе. Примеры touchGFX stm32
В большинстве приложений пользовательский интерфейс должен быть как-то связан с остальной частью вашей системы, а также отправлять и получать данные. Это может быть взаимодействие с аппаратными периферийными устройствами (данные датчиков, аналого-цифровые преобразования, последовательная связь и т. Д.) Или взаимодействие с другими программными модулями.
В этой статье описываются рекомендуемые решения для реализации этого подключения.
Первый метод является «быстрым и грязным» подходом, в первую очередь предназначенным для создания прототипов, тогда как второй метод является архитектурно обоснованным способом правильного соединения пользовательского интерфейса с остальными компонентами в реальном приложении.
В конце этой статьи мы даем ссылку на примеры использования обоих методов.
Модельный класс
Все приложения TouchGFX имеют класс Model, который помимо хранения информации о состоянии пользовательского интерфейса также предназначен для функционирования в качестве интерфейса с окружающей системой. Под этим мы имеем в виду как аппаратную периферию, так и общение с другими задачами ОС в вашей системе. Обычно это плохой дизайн для доступа к другим программным модулям или аппаратным средствам в отдельных классах View.
Класс Model хорошо подходит для размещения любого такого кода интерфейса, потому что:
- Класс Model имеет функцию tick (), которая автоматически вызывается для каждого кадра и может быть реализована для поиска и реагирования на события из других подмодулей.
- Класс Model имеет указатель на вашего в настоящее время активного Presenter, чтобы иметь возможность уведомлять пользовательский интерфейс о входящих событиях.
Аппаратное взаимодействие
Метод 1: Выборка непосредственно из задачи GUI
Лучший способ взаимодействия с аппаратным обеспечением зависит от того, как часто вам нужно выполнять выборку, сколько времени это занимает и насколько критично время. Если ваши требования в этом отношении снисходительны, самый простой подход — просто выбрать аппаратное обеспечение непосредственно в Model::tick функции. Если выборка происходит реже, чем ваша частота кадров (обычно около 60 Гц), вы можете просто добавить счетчик и выполнять выборку только каждый N-й такт. Когда это сделано таким образом, ваша операция сэмплирования должна быть несколько быстрой (обычно 1 мс или меньше), иначе ваша частота кадров начнет страдать, так как сэмплирование выполняется в контексте задачи с графическим интерфейсом и будет задерживать рисование кадра.
Метод 2: Выборка из вторичной задачи
В качестве альтернативы, если нежелательно помещать взаимодействие с оборудованием непосредственно в контекст GUITask, вы можете создать новую задачу ОС, отвечающую за выборку. Вы можете настроить выполнение этой задачи с точными временными интервалами, необходимыми для вашего конкретного сценария. Также в зависимости от ваших потребностей эта новая задача может иметь более низкий или более высокий приоритет, чем задача с графическим интерфейсом. Если он имеет более высокий приоритет, то вы гарантированно выполняете его в указанное вами время, независимо от того, что выполняет задача с графическим интерфейсом. Это имеет тот недостаток, что, если это процесс, потребляющий ЦП, это может повлиять на частоту кадров пользовательского интерфейса. Если, с другой стороны, выборка не является критичной по времени, вы можете назначить задаче более низкий приоритет, чем задаче с графическим интерфейсом, чтобы аппаратная выборка никогда не влияла на частоту кадров пользовательского интерфейса.
Если вы используете подход вторичной задачи, мы рекомендуем вам воспользоваться системой обмена сообщениями между задачами, которая предоставляется вашей ОСРВ. Большинство, если не все, ОСРВ имеют механизм очереди / почты, который позволяет отправлять данные (обычно определяемые пользователем структуры C, байтовые массивы или простые целые числа) из одной задачи в другую. Чтобы передать новые данные в задачу GUI, настройте почтовый ящик или очередь сообщений для теста пользовательского интерфейса и отправьте данные в задачу GUI с помощью этой системы обмена сообщениями. Затем вы можете Model::tick опросить почтовый ящик задачи с графическим интерфейсом, чтобы проверить, поступили ли какие-либо новые данные. В случае, прочитайте данные и обновите пользовательский интерфейс соответственно.
Распространение данных в пользовательском интерфейсе
Независимо от того, используете ли вы метод 1 или метод 2, Model::tick функция — это место, где GUITask узнает о новых данных, отображаемых в пользовательском интерфейсе. Помимо работы в качестве интерфейса к вашей окружающей системе, вспомните ранее, что Model класс также отвечает за хранение данных о состоянии, поэтому могут быть некоторые переменные состояния, которые также необходимо обновить.
Давайте рассмотрим простой пример, когда датчик температуры подключен к системе и текущая температура должна отображаться в пользовательском интерфейсе. В процессе подготовки мы добавим класс Model для поддержки этого:
С учетом вышесказанного вы Presenters можете спросить модель о текущей температуре, что позволяет докладчику установить это значение в пользовательском интерфейсе (представление) при входе в экран, отображающий температуру. Что нам нужно сделать сейчас, это иметь возможность снова обновлять пользовательский интерфейс при получении новой информации о температуре. Для этого мы используем тот факт, что в модели есть указатель на вашего в настоящее время активного докладчика. Тип этого указателя — интерфейс ( ModelListener ), который вы можете изменить, чтобы отразить соответствующие события приложения:
Теперь, когда мы подключили этот интерфейс, осталось только выполнить фактическую выборку входящих событий «новой температуры» в Model::tick
Подход выше обеспечивает две вещи:
- Переменная currentTempera всегда актуальна, так что ваш Presenter может в любой момент получить текущую температуру.
- Ведущий немедленно уведомляется об изменениях температуры и может принять соответствующие меры.
Одним из преимуществ шаблона MVP является то, что вы получаете отдельную обработку уведомлений в зависимости от того, на каком экране вы находитесь в данный момент. Предположим, например, что событие изменения температуры происходит при отображении какого-либо меню настроек (например, MainMenuPresenter / MainMenuView активен), где текущая температура не имеет значения.
Поскольку функция notifyTemperaCCanged имеет пустую реализацию по умолчанию, это уведомление просто игнорируется MainMenuPresenter. С другой стороны, если у вас есть TemperatureControlPresenter, вы можете в этом презентаторе переопределить функцию notifyTeurationChanged и сообщить в View, что он должен отображать обновленную температуру:
Передача данных из пользовательского интерфейса в окружающую систему
Обратное направление, в котором данные / события передаются из пользовательского интерфейса в окружающую систему, осуществляется через модель практически таким же образом. Продолжая приведенный выше пример, если нам нужно добавить возможность настройки нового заданного значения (целевой температуры), мы добавим в модель следующее:
В случае, если пользователь устанавливает новую целевую температуру в пользовательском интерфейсе, представление может информировать презентатора, который содержит указатель на объект модели и, следовательно, может вызывать setNewTargetTemperature функцию.
Примеры
Способ 1 — из задачи графического интерфейса
Загрузите эту ссылку, чтобы найти рабочий пример для STM32F746, показывающий, как создать образец кнопки и управлять светодиодом непосредственно в классе Model. В примере используется архитектура MVP для передачи значений и событий между двумя представлениями и классом Model. Класс Model выполняет выборку кнопки и обновляет светодиод в соответствии с состоянием приложения.
Загрузите эту ссылку, чтобы найти рабочий пример для STM32F429, показывающий, как создать образец кнопки в классе Model. В примере используется архитектура MVP для передачи события кнопки в представление.
Способ 2 — из другого задания
Способ 3 — из нескольких задач (4.9.3)
Этот рабочий пример был продемонстрирован на вебинаре TouchGFX «Интеграция с вашим оборудованием» от 28 мая 2018 года.
Приложение было разработано для платы STM32F769-DISCO и взаимодействует со светодиодом и кнопкой пользователя, чтобы показать, как интегрировать как код C, так и аппаратные периферийные устройства в ваше приложение TouchGFX.
Приложение настраивает кнопку в режиме GPIO. Поведение заключается в том, чтобы определить состояние кнопки в btntask.c и пропустить сообщение через очередь сообщений GUI, если кнопка нажата. Это позволяет нам продвигать анимацию в приложении, удерживая кнопку нажатой.
Приложение использует три задачи FreeRTOS. Один для графического интерфейса, один для каждого периферийного устройства (светодиод и кнопка USER).
Метод 4 — Из задачи и внешней линии прерывания (4.9.3)
Этот рабочий пример был продемонстрирован на вебинаре TouchGFX «Интеграция с вашим оборудованием» от 28 мая 2018 года.
Приложение было разработано для платы STM32F769-DISCO и взаимодействует со светодиодом и кнопкой пользователя, чтобы показать, как интегрировать как код C, так и аппаратные периферийные устройства в ваше приложение TouchGFX.
Это приложение настраивает кнопку в режиме EXTI (внешняя линия прерывания 0). Поведение заключается в получении прерывания при нажатии кнопки, после которого прерывание очищается. Это не допускает того же поведения, что и в GPIO, но вместо этого мы будем пошагово выполнять анимацию, потому что сообщение отправляется только через очередь сообщений графического интерфейса всякий раз, когда получено прерывание.
Приложение использует две задачи FreeRTOS. Один для графического интерфейса, один для светодиода. (Задача Button из метода 3 остается активной в этом приложении, чтобы проиллюстрировать, что периферийный код взаимодействия был перемещен в обработчик прерываний).