Запускаем дисплей на 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 скачиваем тут
    Поделиться публикацией
    Комментарии 41
      +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й.

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

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