Запускаем дисплей на 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 я советую вам прочитать несколько документов:

    1. Обзор возможностей LTDC в F4, в нашем F7 все тоже самое
    2. Application note 4861. «LCD-TFT display controller (LTDC) on STM32 MCUs»

    2. А что нам нужно сделать?


    Микроконтроллеры от ST обрели популярность не зря, важнейшим требованием к любым электронным компонентов — это документация, а с ней все хорошо. Сайт конечно ужасный, но на всю документацию я оставлю ссылки. Производитель избавляет нас от мучений и изобретения велосипеда, поэтому на странице 520 в reference manual RM0385 черным по белому расписано по шагам, что нам надо сделать:



    На деле половину из описанного делать не придется: оно либо не нужно для старта, либо уже настроено по умолчанию. Для минимального старта, который нам позволит рисовать пиксели, выводить картинки, графики, текст и прочее достаточно сделать следующее:

    • Включить тактирование модуля LTDC
    • Настроить систему тактирования и частоту вывода данных (pixel clock)
    • Настроить порты ввода-вывода (GPIO) на работу с LTDC
    • Настроить тайминги для нашей модели дисплея
    • Настроить полярность сигналов. Уже сделано по умолчанию
    • Указать цвет фона дисплея. Его пока не увидим, можно оставить «по нулям»
    • Настроить реальный размер видимой зоны дисплея для конкретного слоя
    • Выбрать формат цвета: ARGB8888, RGB 888, RGB565 и т.д.
    • Указать адрес массива, который будет выступать в роли frame buffer-а
    • Указать количество данных в одной линии (длину по ширине)
    • Указать количество линий (высота дисплея)
    • Включить слой с которым мы работаем
    • Включить модуль LTDC

    Страшно? И мне было страшно, а там оказалось работы на 20 минут со всеми разбирательствами. Задача есть, план расписан и остается его всего лишь выполнить.

    3. Настройка системы тактирования


    Первым пунктом нам необходимо подать тактовый сигнал на модуль LTDC, это делается записью в регистр RCC:

    RCC->APB2ENR |= RCC_APB2ENR_LTDCEN;

    Далее необходимо настроить частоту тактирования от внешнего кварца (HSE) на частоту 216 МГц, то есть на максимальную. Первым шагом включаем источник тактирования от кварцевого резонатора и ждем флага готовности:

    
    RCC->CR |= RCC_CR_HSEON;
    while (!(RCC->CR & RCC_CR_HSERDY));
    

    Теперь настраиваем задержку для flash памяти контроллера, т.к. она не умеет работать на частоте ядра. Ее значение как и остальные данные берутся из reference manual:

    
    FLASH->ACR |= FLASH_ACR_LATENCY_5WS;
    

    Теперь для получения искомой частоты я разделю 25 МГц со входа на 25 и получу 1 МГц. Далее просто в PLL умножаю на 432, т.к. в дальнейшем есть делитель частоты с минимальным значением /2 и на него нужно подать удвоенную частоту. После этого подключаем вход PLL к нашему кварцевому резонатору (HSE):

    
    RCC->PLLCFGR |= RCC_PLLCFGR_PLLM_0 | RCC_PLLCFGR_PLLM_3 | RCC_PLLCFGR_PLLM_4;
    RCC->PLLCFGR |= RCC_PLLCFGR_PLLN_4 | RCC_PLLCFGR_PLLN_5 | RCC_PLLCFGR_PLLN_7 | 
    RCC_PLLCFGR_PLLN_8;
    RCC->PLLCFGR |= RCC_PLLCFGR_PLLSRC;
    

    Теперь включаем PLL и ждем флага готовности:

    
    RCC->CR |= RCC_CR_PLLON;
    while((RCC->CR & RCC_CR_PLLRDY) == 0){}
    

    В качестве источника системной частоты назначаем выход нашего PLL и ждем флага готовности:

    
    RCC->CFGR |= RCC_CFGR_SW_PLL;
    while((RCC->CFGR & RCC_CFGR_SWS) != RCC_CFGR_SWS_1) {}
    

    На этом общая настройка тактирования заканчивается и мы переходим к настройке тактовой частоты (PLLSAI) для нашего дисплея (pixel clock). Сигнал для PLLSAI согласно даташиту берется после делителя /25, то есть на входе мы имеем 1 МГц. Нам нужно получить частоту около 9.5 МГц, для этого частоту 1 МГц умножаем на 192, а затем с помощью двух делителей на 5 и на 4 получаем искомое значение PLLSAI = 1 МГц * 192 / 5 /4 = 9,6 МГц:

    
    RCC->PLLSAICFGR |= RCC_PLLSAICFGR_PLLSAIN_6 | RCC_PLLSAICFGR_PLLSAIN_7;
    RCC->PLLSAICFGR |= RCC_PLLSAICFGR_PLLSAIR_0 | RCC_PLLSAICFGR_PLLSAIR_2;
    RCC->DCKCFGR1 	|= RCC_DCKCFGR1_PLLSAIDIVR_0;
    RCC->DCKCFGR1 	&= ~RCC_DCKCFGR1_PLLSAIDIVR_1;
    

    Финальным шагом мы включаем PLLSAI для дисплея и ждем флага готовности к работе:

    
    RCC->CR |= RCC_CR_PLLSAION;
    while ((RCC->CR & RCC_CR_PLLSAIRDY) == 0) {}
    

    На этом базовая настройка системы тактирования завершена, единственное, чтобы не забыть и потом не страдать, давайте включим тактирование на все порты ввода-вывода (GPIO). Питание у нас не батарейное как минимум на отладке, поэтому не экономим:

    
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOCEN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIODEN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOEEN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOFEN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOGEN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOHEN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOJEN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOKEN;
    

    4. Настройка портов ввода-вывода (GPIO)


    С настройкой gpio все очень просто — у нас все ноги шины LTDC должны быть настроены как альтернативный выход и на высокую частоту. Для этого в reference manual на странице 201 у нас есть вот такая подсказка:



    В таблице указано какие биты в регистрах нужно выставить, чтобы получить необходимую настройку. Стоит отметить, что все подтяжки у нас отключены. Где же посмотреть какую альтернативную функцию включить? А для этого идем на страницу 76 в datasheet на наш контроллер и смотрим на такую таблицу:



    Как видите логика таблицы простейшая: находим нужную нам функцию, в нашем случае LTDC B0, далее смотрим на каком GPIO она находится (PE4, например) и наверху смотрим номер альтернативной функции, который будем использоваться для настройки (AF14 у нас). Чтобы настроить наш вывод как push-pull выход с альтернативной функцией LTDC B0 нам нужно написать следующий код:

    
    GPIOE->MODER   &= ~GPIO_MODER_MODER4;
    GPIOE->MODER   |= GPIO_MODER_MODER4_1;
    
    GPIOE->OSPEEDR |= GPIO_OSPEEDER_OSPEEDR4_1;
    
    GPIOE->AFR[0] &= ~GPIO_AFRL_AFRL4_0;
    GPIOE->AFR[0] |= GPIO_AFRL_AFRL4_1 | GPIO_AFRL_AFRL4_2 | GPIO_AFRL_AFRL4_3;
    

    Я привел пример для вывода PE4, который соответствует выводу B0 на шине LTDC, то есть это нулевой бит синего цвета. Для всех остальных выводов настройка идентична, отдельного внимания заслуживают лишь 2 вывода, один из готовых включает дисплей, а другой его подстветку. Настраиваются они как обычный выход push-pull, который все используют для мигания светодиодом. Выглядит настройка вот так:

    
    GPIOK->MODER &= ~GPIO_MODER_MODER3;
    GPIOK->MODER |= GPIO_MODER_MODER3_0;
    

    Данная настройка для вывода PK3, который включает и выключает нашу подсветку. Его кстати можно еще и ШИМовать, чтобы плавно регулировать яркость. Для PI12, который включает дисплей (DISP) все аналогично. Скорость на этих 2-х выводах по умолчанию низкая, т.к. каких-то высокочастотных действий от них не требуется.

    Все остальные порты ввод-вывода вы можете посмотреть самостоятельно на схеме отладочной платы, либо на принципиальную схему собственного устройства.

    5. Тайминги и их настройка


    Тайминги с физический точки зрения представляют из себя обычные задержки. Думаю вы ни раз наблюдали различные извращения типа delay(1), когда смотрели примеры кода на дисплеи с контроллерами SPI/I2C на подобии ILI9341. Там задержка нужна для того, чтобы контроллер, например, успел принять команду, выполнить ее и затем уже что-то делать с данными. В случае с LTDC все примерно так же, только городить костыли мы не будем да и не зачем — наш микроконтроллер сам аппаратно умеет формировать необходимые тайминги. Зачем они нужны на дисплее, где нет контроллера? Да элементарно чтобы после заполнения первой линии пикселей перейти на следующую линию и вернуться в ее начало. Это связано с технологией производства дисплеев, а следовательно у каждой конкретной модели дисплея тайминги могут быть свои.

    Чтобы узнать какие значения нам понадобятся идем на сайт ST и смотрим схему отладочной платы STM32F746-Disco. Там мы можем увидеть, что используется дисплей RK043FN48H-CT672B и документация на него доступна, например, тут. Нас больше всего интересует таблица на странице 13 в разделе 7.3.1:



    Вот они наши значения, которые нам понадобятся при настройке. Так же в документации есть еще много чего интересного, например, диаграммы сигналов на шине и прочее, что может вам понадобиться если вы, например, захотите поднять дисплей на FPGA или CPLD.

    Переходим к настройкам. Первым делом, чтобы не держать в голове данные значения, оформлю их в виде дефайнов:

    
    #define  DISPLAY_HSYNC            ((uint16_t)30)
    #define  DISPLAY_HBP              ((uint16_t)13)
    #define  DISPLAY_HFP              ((uint16_t)32)
    #define  DISPLAY_VSYNC            ((uint16_t)10)
    #define  DISPLAY_VBP              ((uint16_t)2)
    #define  DISPLAY_VFP              ((uint16_t)2)
    

    Тут есть интересная особенность. Тайминг Pulse Width, который именуется у нас DISPLAY_HSYNC, имеет значение в таблице только для частоты pixel clock в 5 МГц, а для 9 и 12 МГц его нет. Данный тайминг необходимо подобрать под свой дисплей, у меня это значение получилось 30, когда в примерах от ST оно было иное. При первом запуске если у вас будет ошибка с его настройкой, то изображение будет сдвинуто или влево или вправо. Если вправо — уменьшаем тайминг, если влево — увеличиваем. По факту он влияет на начало координаты видимой зоны, в чем мы дальше и убедимся. Просто имейте ввиду, а помочь осмыслить весь этот абзац поможет следующая картинка со страницы 24 нашего AN4861:



    Тут удобна небольшая абстракция. У нас есть 2 зоны дисплея: видимая и общая. Видимая зона имеет размеры с заявленным разрешением 480 на 272 пикселя, а общая зона — это видимая + наши тайминги, которых по 3 на каждую сторону. Так же стоит понимать (это уже не абстракция), что один системный тик равен 1 пикселю, поэтому общая зона имеет размер 480 пикселей + HSYNC + HBP + HFP.

    Еще стоит осознать, что чем меньше тайминги, тем лучше — быстрее будет обновляться дисплей и частота кадров немного вырастет. Поэтому после первого запуска поэкспериментируйте с таймингами и уменьшите их насколько возможно при этом сохранив стабильность работы.

    Для настройки таймингов я сделал себе на будущее небольшую «шпаргалку» внутри проекта, она вам тоже поможет понять какую конкретно цифру и куда ее вписывать:

    
    /*
    *************************** Timings for TFT display**********************************
    *
    * HSW = (DISPLAY_HSYNC - 1)
    * VSH = (DISPLAY_VSYNC - 1)
    * AHBP = (DISPLAY_HSYNC + DISPLAY_HBP - 1)
    * AVBP = (DISPLAY_VSYNC + DISPLAY_VBP - 1)
    * AAW = (DISPLAY_HEIGHT + DISPLAY_VSYNC + DISPLAY_VBP - 1)
    * AAH = (DISPLAY_WIDTH + DISPLAY_HSYNC + DISPLAY_HBP - 1)
    * TOTALW = (DISPLAY_HEIGHT + DISPLAY_VSYNC + DISPLAY_VBP + DISPLAY_VFP - 1)
    * TOTALH = (DISPLAY_WIDTH + DISPLAY_HSYNC + DISPLAY_HBP + DISPLAY_HFP - 1)
    *
    */
    

    Откуда собственно эта «шпаргалка» взялась… Во-первых, похожую «формулу» вы видели пару абзацев до этого. Во-вторых, идем на страницу 56 нашего AN4861:



    Правда я надеюсь, что вы поняли физический смысл таймингов еще до появления данной шпаргалки и уверен, что наверняка бы и сами смогли ее составить. Сложно в ней ничего нет, а картинки из RM и AN помогают визуально понять влияние таймингов на процесс формирования изображения.

    Теперь пришла пора написать код, который эти тайминги настроит. В «шпаргалке» указаны бита регистра в которые писать, например, TOTALH и после знака равно формула, дающая на выходе некое число. Понятно? Тогда пишем:

    
    LTDC->SSCR |= ((DISPLAY_HSYNC - 1) << 16 | (DISPLAY_VSYNC - 1));
    
    LTDC->BPCR |= ((DISPLAY_HSYNC+DISPLAY_HBP-1) << 16 | (DISPLAY_VSYNC+DISPLAY_VBP-1));
    
    LTDC->AWCR |= ((DISPLAY_WIDTH + DISPLAY_HSYNC + DISPLAY_HBP - 1) << 16 | 
    (DISPLAY_HEIGHT + DISPLAY_VSYNC + DISPLAY_VBP - 1));
    
    LTDC->TWCR |= ((DISPLAY_WIDTH + DISPLAY_HSYNC + DISPLAY_HBP + DISPLAY_HFP -1)<< 16 |(DISPLAY_HEIGHT + DISPLAY_VSYNC + DISPLAY_VBP + DISPLAY_VFP - 1));
    

    А на этом с таймингами все! В данном разделе можно еще разве, что настроить цвет фона. У меня он по умолчанию черный, поэтому записан нулем. Если вы хотите изменить цвет фонового слоя (background), то можете после равно записать любое значение, например, 0xFFFFFFFF и залить все белым:

    
    LTDC->BCCR = 0; 
    

    Есть в 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 пикселей. Для настройки размера видимой зоны напишем следующий код:

    
    LTDC_Layer2->WHPCR |= (((DISPLAY_WIDTH + DISPLAY_HBP + DISPLAY_HSYNC - 1) << 16) | (DISPLAY_HBP + DISPLAY_HSYNC));
    
    LTDC_Layer2->WVPCR |= (((DISPLAY_HEIGHT + DISPLAY_VSYNC + DISPLAY_VBP - 1) << 16) |(DISPLAY_VSYNC + DISPLAY_VBP));
    

    Как видите тут все просто как и с таймингами. Начальные точки (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 и имеет следующий вид:

    LTDC_Layer2->PFCR = 0;

    Если вам нужен другой формат, отличный от ARGB8888, то идем на страницы 533 и 534 в reference manual-е и выбираем нужный формат из предложенного списка:



    Теперь создадим массив и передадим его адрес в LTDC, он превратится во frame buffer и будет являться «отражением» нашего слоя. Например, вам нужно 1-й пиксель в 1-й строке залить белым цветом, для этого вам достаточно записать значение цвета (0xFFFFFFFF) в первый элемент этого массива. Надо залить 1-й пиксель во 2-й строке? Тогда тоже значение цвета запишем в элемент с номером (480+1). 480 — сделает перенос строки, дальше добавляет номер в нужной нам строке.

    Выглядит это настройка вот так:

    
    #define DISPLAY_WIDTH 			((uint16_t)480)
    #define DISPLAY_HEIGHT			((uint16_t)272)
    
    const uint32_t imageLayer2[DISPLAY_WIDTH * DISPLAY_HEIGHT];
    
    LTDC_Layer2->CFBAR = (uint32_t)imageLayer2;
    

    По хорошему следует после настройки LTDC настроить еще и SDRAM, чтобы убрать модификатор const и получить frame buffer именно в ОЗУ, т.к. собственной ОЗУ МК не хватает даже на один слой при 4 байтах. Хотя это не помешает протестировать правильность настройки периферии.

    Далее необходимо указать значение альфа-слоя, то есть прозрачность для нашего слоя Layer2, для этого записываем значение от 0 до 255, где 0 — полностью прозрачный слой, 255 — полностью непрозрачный, то есть 100% видимый:

    LTDC_Layer2->CACR = 255;

    Согласно нашему плану теперь необходимо записать размеры нашей видимой области дисплея в байтах, для этого записываем в регистры соответствующие значения:

    
    LTDC_Layer2->CFBLR |= (((PIXEL_SIZE * DISPLAY_WIDTH) << 16) | (PIXEL_SIZE * DISPLAY_WIDTH + 3));
    
    LTDC_Layer2->CFBLNR |= DISPLAY_HEIGHT;	
    

    Осталось два последних шага, а именно включение слоя №2 и самого модуля периферии LTDC. Для этого записываем соответствующие биты:

    
    LTDC_Layer2->CR |= LTDC_LxCR_LEN;
    
    LTDC->GCR |= LTDC_GCR_LTDCEN;
    

    На этом настройка нашего модуля закончена и можно работать с нашим дисплеем!

    7. Немного о работе с LTDC


    Вся работа с дисплеем сводится теперь лишь к записи данных в массив imageLayer2, он имеет размер 480 на 272 элемента, что полностью соответствует нашему разрешению и намекает на простую истину — 1 элемент массива = 1 пиксель на дисплее.

    Я в качестве примера записал в массив картинку, которую преобразовал на в программе LCD Image Converter, но на деле вряд ли ваши задачи ограничатся этим. Есть два пути: использование готового GUI и написание его собственноручно. Для относительно простых задач типа вывода текста, построение графиков и подобное советую написать свой GUI, это займет немного времени и даст вам полное понимание его работы. Когда же задача большая и сложная, а времени на разработку своего GUI нет, то советую обратить внимание на готовые решения, например, uGFX и ему подобные.

    Символы текста, линии и прочие элементы по своей сути являются массивами пикселей, соответственно для их реализации вам самостоятельно нужно реализовать логику, но начать стоит с самой базовой функции — «вывод пикселя». Она должна принимать 3 аргумента: координату по Х, координату по Y и соответственно цвет в который данный пиксель окрашивается. Выглядеть это может например так:

    
    typedef enum ColorDisplay {
    	RED = 0xFFFF0000,
    	GREEN = 0xFF00FF00,
    	BLUE = 0xFF0000FF,
    	BLACK = 0xFF000000,
    	WHITE = 0xFFFFFFFF
    } Color;
    
    void SetPixel (uint16_t setX, uint16_t setY, Color Color) {
    
    	uint32_t numBuffer = ((setY - 1) * DISPLAY_WIDTH) + setX;
    
    	imageLayer2[numBuffer] = Color;
    
    }
    

    После того как мы приняли координаты в функцию, мы пересчитываем их в номер массива, который соответствует данной координате и затем записываем принятый цвет в полученный элемент. На основе данной функции дальше можно уже реализовывать функции вывода геометрии, текста и прочие «плюшки» GUI. Думаю идея понятна, а как ее воплотить в жизнь уже на ваше усмотрение.

    Итог


    Как видите реализация даже сложной периферии на регистрах (CMSIS) является не сложной задачей, вам достаточно понять как оно работает внутри. Конечно нынче модно разрабатывать встроенное ПО без понимания происходящего, но это тупиковый путь, если вы планируете стать инженером, а не…

    Если сравнить полученный код с решением на HAL или SPL, то можно заметить, что код написанный на регистрах более компактный. Добавив где нужно пару комментариев и обернув в функции мы получаем читаемость как минимум не хуже, чем у HAL/SPL, а если вспомнив, что reference manual документирует именно регистры, то работа с использованием CMSIS является более удобной.

    1) Проект с исходниками в TrueSTUDIO можно скачать тут

    2) Для тех, кому удобнее посмотреть на GitHub

    3) Утилиту для конвертации изображения в код LCD Image Converter скачиваем тут

    Комментарии 53

      +1
      Просто замечательнейшая статья — жду еще!
      Вы увидите, что поднимать сложную периферию на регистрах так же просто, как и обычный SPI

      Та ну, некоторым и c GPIO сложно работать без RTOS.
      Позвольте поинтересоваться, получается в данном случае, мы ограничены, в плане размера дисплея, лишь размером frame-buffer? Или частоты работы LTDS так же имеет значение?
        +3
        Верно, максимальное разрешение упирается только в размер фрейм буфера и пропускную способность SDRAM, ведь буфер для HD уже явно не упихнуть в память самого контроллера. В аппноуте есть интересная таблица:



        Если судить по ней, то мы можем получить Full HD разрешение при 30 фпс, но для этого нам нужно подключить SDRAM 32-битную и формат данных использовать придется RGB565. Даже в таком случае размер фрейм буфера получается около 4 Мбайт, а это уже не мало.

        Что касается частоты, то pixel clock тоже увеличивается. Для full HD он уже близок к предельному и составляет 74 МГц.
          0
          Спасибо за подробны ответ.
          То есть получается, ничего не мешает использовать ноутбучные, 15 дюймовые квадратики (типа флетрон) матрицы, для своих поделок? Они ведь так же LVDC совместимы.
            +1
            Совсем ничего не мешает)) Единственное не забудьте поставить преобразователь LTDC (RBG24) в этот самый LVDS, например, DS90C365 или любой другой.
        0
        О, похоже на sdr трансивер, выглядит интересно )
          +1
          Он самый, а точнее даже DDC/DUC. На досуге понемногу ковыряю, пока только прием реализовал за 3 месяца)) Может как нибудь и расскажу даже о нем, ибо проект все равно открывать собирался.
            0
            Хороший приём с функционалом это 90% сложности, ващет. Какой АЦП?
              +1
              Изначально был какой-то AD 14-ти битный, в итоге заменил на LTC2165. Примерная структура получилась LTC2165 + Cyclone 10LP + STM32F746. С приемом я как-то проще разобрался, а вот с передачей с наскоку уперся в большие искажения, но там скорее проблема в аналоговом RF тракте. Поменял ОУ на симметрирующий трансформатор (balun), должно стать сильно лучше, но пока руки еще не дошли.
                0
                У меня мечта как-нибудь выкроить время и сделать примерно на такой же связке (+ хорошие полосовые, LNA, att и тд. чем часто не страдают проекты SDR)
                хороший портативный приёмник all in one с максимально низким потреблением. Занимаюсть стихийным DX-ингом, особенно за городом люблю, специально езжу за чистым эфиром )
                  0
                  Мысли о портативном варианте тоже кстати есть, но пока первая ревизия на плате 180х120 мм и кушает до 5 Вт))
              0
              Публикуйте здесь проект обязательно, хотя бы обзорно. Надо как-то поддерживать интерес к радио.
            0
            И получается целый микроконтроллер исключительно для вывода видео?
            Конечно хорошо, но кроме вывода есть же и другие задачи.
              +4
              Откуда такая информация? Вывод данных не затрачивает процессорное время вообще. То есть нагрузка на ядро 0%.
                0
                Стоит учесть что оно затрагивает SDRAM, у меня выходило так, что почти вся пропускная способность работала на захват картинки с камеры и после отображения её на дисплей
                  0
                  Ты пришел записаться в мой фан-клуб?)) Но тут момент — а нужна ли тебе sdram для чего-то еще кроме камеры и дисплея? И конечно от фпс требуемого зависит и разрешения.
                +1
                Я тестировал сколько прямоугольников при разрешении 800х600 (2 байта на пиксель, SDRAM 16bit) смогу отрисовывать через DMA2D в сек. Так вот 138-139 кадров ( и естественно в момент работы DMA2D, микроконтроллер может что-то свое вычислять). Кончено при использовании GUI библиотек показатель ФПС будет ниже, но все же запас более чем хороший, да и не всегда нам нужны 60 кадров в секунду…
                +3
                Спасибо за труды! Такого уровня статей очень не хватает в рунете.
                  +1
                  к сожалению, пример не компилируется

                  cannot open linker script file C:\Users\Ilya\Desktop\Workspace\Firmware\STMicroelectronics\STM32-Lesson\Lesson17\Startup\STM32F746NG_FLASH.ld: Invalid argument Lesson17 C/C++ Problem

                  неверное не стоит указывать в проекте абсолютные пути…

                  />
                    +1
                    Я думаю прописать пути в настройках труда не составит, это задача на 20 секунд. Мне просто удобнее по ряду причин работать именно с абсолютными путями, а не относительными.

                    Если вдруг кто-то не знает где, то подскажу жмем Alt+Enter:

                      0
                      option id=«com.atollic.truestudio.ld.general.scriptfile.569109988» name=«Linker script» superClass=«com.atollic.truestudio.ld.general.scriptfile» useByScannerDiscovery=«false» value=«C:\Users\Ilya\Desktop\Workspace\Firmware\STMicroelectronics\STM32-Lesson\Lesson17\Startup\STM32F746NG_FLASH.ld»
                        +2
                        Я думаю если человек не знает где и как прописывать пути, то вряд ли эта статья ему под силу. Ну и опять же подсказка (кликабельная):

                          +1
                          я думаю, что человек, написавший статью с примером кода, позаботится о читателях, которым будет удобно запускать пример кода не на его, а на своем десктопе…
                            +1
                            Если у человека окажется IAR, то мне еще и под IAR проект пересобрать? А потом еще под десяток ide? Код есть — все остальное мишура же.
                              0
                              речь шла не об IAR и не о «десятках ide», а о запуске в той среде,
                              в которой код написан…
                              если код публикуется для общего доступа, то признаком хорошого тона бедет использование относительных путей, а не абсолютных, хоть это типа «удобно»
                                –2
                                Не скачивайте архив, он с вирусом! Смотрите на кошерный github. Хороший тон — смотреть именно код.
                                  +2
                                  ой какие нервы… «вирусы»...«кошерный github»… указания куда смотреть…
                                  нет, что бы просто признать небольшое неудобство для читателей статьи
                                    –2
                                    Если вам что-то неудобно, то не читайте. Я же вас не заставляю. Мне кажется, что все более чем удобно и понятно, прописать пути это 20 секунд времени, вы же тут настрочили уже…
                      +1
                      Оличная Статья. Она учит как программировать базуруясь только на Appnotes. Я на своем опыте понял, что лучше написать свой код, чем портировать примеры из Cube
                        0
                        Оно конечно здорово — напрямую записывать значения в регистры. Но перебарщивать то-же не слишком хорошо. Проблема в том что значения битов в регистрах имеют очень абстрактное значение — так сказать абсолютный минимум.
                        В этом плане CMSIS имеет проигрыш. Потому как потребность каждый раз заглядывать в документацию -откровенно бесит. Ну вот описаны биты одного значения с номерами 0-1-2-3-4… И естественно их комбинация даёт какое-то осмысленное значение. Но какое именно — становится понятным только после углублённого изучения доков.

                        HAL и LL не является лекарством. Это как обратная сторона CMSIS — концентрация абсолютного зла. Ничего не придумав нового — они навесили новый слой абстракции на существующий CMSIS. Теперь чтобы понять происходящее приходится читать сразу две документации.

                        Есть вариант, точнее идея.
                        Так как совместимость на физическом уровне безбожно потрачена — предлагаю создать совместимость на программном уровне. Те-же самые регистры CMSIS — но уже с осмысленным описанием. С решением конфликтов разнообразия вариантов настройки и простота заполнения конфигов. Под каждый регистр составляется кукла для копирования и редактирования в проекте. Лишнее программист сам удалит, нового не добавит, превысить ограничения полей регистров не сможет. К тому-же вариант оформления кода со статическими константами — намного проще, их относительно мало, и все они собраны в группы — трудно промахнуться.

                        Собственно пример долгостроя. Активная работа начинается с очередным новым проектом. Что-бы не штудировать доки с явной потерей времени — оформляю куклы, почти как дошики — просто долей воды.
                        bitbucket.org/AVI-crak/system_f746/src/default/stm32f7_emus.h
                        Там есть пример оформления стартового файла настройки тактирования, чисто посмотреть.
                        bitbucket.org/AVI-crak/rtos-cortex-m3-gcc/src/default/system_stm32f7xx.h
                          0
                          подключение внешней SDRAM и Flash-памяти по QSPI решают проблемы с памятью

                          А можно поподробнее про внешнюю SDRAM на QSPI( которая, предполагаю, используется как видеопамять) и как она решает проблемы.
                            0
                            Основная проблема, что frame buffer занимает 500+ кБайт, а объем ОЗУ самого МК 240 или 320 кБ, то есть даже один слой нельзя разместить в ОЗУ, а их обычно 2. Подключая SDRAM мы расширяем ОЗУ, т.к. внешняя память воспринимается МК как продолжение собственной памяти, то есть просто добавляются адреса памяти и все. Соответственно 2 frame buffer-а спокойно располагаются в ОЗУ.

                            В реалиях буферов надо как минимум 4, ведь нужно где-то предварительно обрабатывать и готовить кадр. Ну а QSPI просто позволяет хранить множество заготовок слоев, которые заранее отрисовали на этапе разработки дизайна GUI.
                              +1
                              т.е. на QSPI сидит «флешка с картинками»?
                              А видеобуфер где находится?

                              Собственно интересовало: как достаточно медленную последовательную ОЗУ использовать для одновременного рендера картинки и вывода на дисплей( поток, как понял, порядка 30Мбайт/с)?
                                0
                                Ага, в QSPI сидят картинки и по мере необходимости перегружаются в ОЗУ с помощью DMA. Пропускная способность QSPI около 25-35 Мб/с. У SDRAM около 60 Мб/с получалось вытянуть.

                                Видео буфер (frame buffer) находится в ОЗУ, в идеале он должен быть именно там. В примере для статьи я запихал его во flash, чтобы не поднимать SDRAM и не отвлекать читателя от лишних телодвижений.
                                  0
                                  Если не ошибаюсь: контроллер QSPI не поддерживает прямую «аппаратную» запись в маппированное ОЗУ( т.е. аппаратное формирование команды на запись). Соответственно интересно посмотреть на данное решение: как программно? писать в ОЗУ, когда его активно использует LTDC.
                                    0
                                    Один слой это 0.5 Мб, два слоя 1 Мб. Реально необходимый фпс порядка 25-30 и это только в момент взаимодействия пользователя с экраном, то есть при работе с тач-скрином. В оставшееся время данные на дисплее можно и нужно обновлять с частотой 5-10 фпс. Получаем в худшем случае (1% времени работы) около 30 Мб/сек, а в остальное время около 10 Мб/с. Как видите дисплей даже половину полосы не занимает, то есть 70% времени шина будет свободна. В это время хотите пишите из QSPI памяти в SDRAM, хотите готовьте кадры и прочие телодвижения.

                                    Сам контроллер умеет через DMA писать из внешней flash в sdram, это все аппаратная реализация и ядро тут не участвует. Достаточно просто указать адрес исходника и адрес куда переносить данные, дальше все аппаратно.
                                      0
                                      Попытка номер последняя.
                                      Если под SDRAM подразумевается быстрое ОЗУ на параллельной шине, то откуда оно вылезло в данной ситуации( да и вопросов тогда нет)?
                                      Речь идет о системе:
                                      STM32F7( с набортными 200-300кб) + LCD + внешняя память в виде QSPI RAM ( любого обьема)
                                      Собственно и интересует как эту внешнюю память можно использовать как фреймбуфер/боевую видеопамять RW типа?

                                      P.S. Я не троллю. Действительно интересно.
                                        0
                                        SDRAM — это SDRAM. Synchronous Dynamic Random Access Memory… Данный тип памяти априори быстрый, как минимум из-за параллельной шины (16 бит) и частоты в 133 МГц (обычно).

                                        QSPI flash в роли ОЗУ? Странно конечно, но в теории можно. Достаточно указать в линковщике сектор адресов для внешней флешки, создать там видео буфер (массив) и прописать в DMA его адрес. Тогда DMA без проблем сможет пихать этот буфер прямиком в LTDC.
                                        Единственное останется вопрос скорости, на 480х272 думаю без проблем в шину пролезет, а на бОльшие разрешения уже можно и упереться.
                                          0
                                          Виноват
                                          подключение внешней SDRAM и Flash-памяти по QSPI решают проблемы с памятью

                                          я «прочитал» как:
                                          подключение внешней SD(RAM и Flash-памяти по QSPI) решают проблемы с памятью

                                          из за чего начал всю эту ветку
                                            0
                                            Да бывает) Главное, что таки выяснили чего хотели
                            +1
                            Поддерживаю автора. Тоже считаю, что лучше разбираться с даташитами, а не с библиотеками. Ликвидируется лишнее звено в виде стороннего программиста.
                              0
                              В ваших примерах есть огромный косяк, исправьте п-та.
                              LTDC_Layer2->PFCR = 0;
                              

                              — вот такие штуки сносят мозги контроллеру напроч. Например, если включить кеинг, на моей плате исчезает слой.
                              В документации не зря говорится про
                              Reserved, must be kept at reset value.

                              LTDC_Layer2->PFCR &= ~0xFF;

                              Это касается абсолютно всех регистров (не только LTDC) в которых есть неиспользуемые биты.
                              и ссылка на 16й пример, а на гитхабе 15й.
                                0
                                > LTDC_Layer2->PFCR &= ~0xFF;
                                но ведь ~0xFF равно 0x00. А and с нулем эквивалентно присваиванию нуля
                                тогда чем эта строка отличается от
                                LTDC_Layer2->PFCR = 0;?

                                или у армов and и присваивание по разному работают?
                                0
                                А нет случайно статейки о использовании внешнего ОЗУ при работе с дисплеем?
                                Без внешнего ОЗУ вроде все понятно.
                                У меня STM32F429IIT6, никогда ранее с армами не работал, но жизнь заставляет. Пока изучаю к ним подход.
                                В данное время стоит цель заставить работать этот контроллер в связке с дисплеем 800*480 (24 бит цветность).
                                Получается на один буфер надо 1152000 байт памяти, что как бы явно говорит о внешнем ОЗУ.
                                Но вот как-то это все скрестить пока понимания мало.
                                  0
                                  На SPL когда-то видел, если на регистрах, то там 5 строк настройки + в линковщике увеличить размер и длину памяти на размер вешней ОЗУ. Вообще планирую про SDRAM написать.
                                    0
                                    Да, было бы очень интересно почитать Вашу статью. Ждемс )
                                      0
                                      Подскажите, какой средой программирования Вы пользуетесь?
                                      Прошу прощения, если в статье я это упустил.
                                        0
                                        Для данной статьи проект собран в TrueSTUDIO, а вообще для работы VS + visualGDB.
                                    0
                                    Подскажите пожалуйста, никак не могу сообразить, откуда берутся значения в дефайны DISPLAY_HSYNC, DISPLAY_HBP и прочие.
                                    Посмотрел даташит, в регистры LTDC->SSCR, LTDC->BPCR и прочие вносятся пиксели, но никак не могу понять откуда получаются эти цифры. Даже если учесть что частота LTDC у Вас 9.6 МГц, в даташите на дисплей цифры указаны для частоты 9 МГц, все равно у меня ничего не сходится.
                                    Вот у меня допустим LTDC работает на 6.5 МГЦ, в даташите на мой дисплей HSYNC Back-Porch 38 Tosc, т.е 38 периодов частоты 6.5 МГц, это значение и надо ведь вносить в регистр или нет, совсем запутался.
                                    Про HSYNC Pulse Width равно 30 я понял, что Вы это подобрали, а вот почему к примеру DISPLAY_HBP 13, когда в таблице 43, это тоже подборные цифры? DISPLAY_HFP вместо 8 -> 32, видимо тут как-то влияет подборные HSYNC Pulse Width, но получается со всеми этими цифрами надо эксперементировать?
                                      0
                                      Да, тайминги нужно подбирать, цифры в даташите они чисто для ориентирования и по сути задают лишь граничные значения. Я только в документации AIO видел внятное вычисление таймингов в зависимости от частоты, а тут пришлось выставить средние значения, а затем уменьшать их.
                                      Всякие HBP и VBP подбирал уже по самому дисплею. Если значение будет неверное, то картинка просто сместится за границы видимой части дисплея. Двигаете эти переменные пока начало картинки не окажется идеально в видимой части, там в принципе все видно наглядно.
                                        0
                                        Спасибо за ответ. Да, уже разобрался. У меня дисплей WF35LTIACDNN0#. Все тайминги подошли с даташита.
                                        Просто делал все первый раз, боялся ошибиться, но все оказалось очень просто. Конечно благодаря Вашей статье. )
                                        Следующий шаг для меня будет использование внешнего ОЗУ и ПЗУ. Тоже пока темный лес в этом )
                                      0
                                      А можно с помощью настроек LTDC или DMA2D повернуть экран на 180 градусов?
                                        +1
                                        Насколько я помню да, при чем контроллер LTDC умеет это аппаратно делать. У st была pdf-ка с описанием аппаратных фич LTDC, DMA2D и CromeART, там про это можно подробно почитать.
                                          0

                                          Вот не нашёл это. А то есть дисплей, у которого отсутствует пин такой поворота. Если подскажите что за pdf и фича, был бы очень признателен

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                      Самое читаемое