Запускаем дисплей на STM32 через LTDC… на регистрах

в 10:21, , рубрики: CMSIS, Cortex, LTDC, mcu, stm32, tft, программирование микроконтроллеров, Производство и разработка электроники, схемотехника, Электроника для начинающих

Приветствую! Недавно для проекта потребовалось запустить дисплей, который имел интерфейс LVDS. Для реализации задачи был выбран контроллер STM32F746, т.к. я с ним уже достаточно много работал и у него есть модуль LTDC, который позволяет работать напрямую с дисплеем без контроллера. В данном случае контроллер реализован уже внутри микроконтроллера. Так же не последним доводом было то, что на данном камне есть отладка STM32F746-Disco, которая у меня была под рукой, а значит я мог начинать работу над проектом не дожидаясь пока ко мне приедет плата, компоненты и прочее.

Сегодня я расскажу как запустить модуль LTDC, работая с регистрами (CMSIS). HAL и прочие библиотеки не люблю и не использую по религиозным убеждениям, но в этом и интерес. Вы увидите, что поднимать сложную периферию на регистрах так же просто, как и обычный SPI. Интересно? Тогда поехали!

Запускаем дисплей на STM32 через LTDC… на регистрах - 1

1. Немного о LTDC

Данный модуль периферии по своей сути является контроллером, который обычно стоит на стороне дисплея, например, SSD1963 и ему подобные. Если посмотрим на структуру LTDC, то увидим, что физически это параллельная шина на 24 бита + аппаратный ускоритель графики + массив данных в ОЗУ, который является по факту буфером дисплея (frame buffer).

Запускаем дисплей на STM32 через LTDC… на регистрах - 2

На выходе мы имеем обычную параллельную шину, которая в себе содержит 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 черным по белому расписано по шагам, что нам надо сделать:

Запускаем дисплей на STM32 через LTDC… на регистрах - 3

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

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

Запускаем дисплей на STM32 через LTDC… на регистрах - 4

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

Запускаем дисплей на STM32 через LTDC… на регистрах - 5

Как видите логика таблицы простейшая: находим нужную нам функцию, в нашем случае 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:

Запускаем дисплей на STM32 через LTDC… на регистрах - 6

Вот они наши значения, которые нам понадобятся при настройке. Так же в документации есть еще много чего интересного, например, диаграммы сигналов на шине и прочее, что может вам понадобиться если вы, например, захотите поднять дисплей на 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:

Запускаем дисплей на STM32 через LTDC… на регистрах - 7

Тут удобна небольшая абстракция. У нас есть 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:

Запускаем дисплей на STM32 через LTDC… на регистрах - 8

Правда я надеюсь, что вы поняли физический смысл таймингов еще до появления данной шпаргалки и уверен, что наверняка бы и сами смогли ее составить. Сложно в ней ничего нет, а картинки из 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 фпс при множестве других задач:

Запускаем дисплей на STM32 через LTDC… на регистрах - 9

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-е и выбираем нужный формат из предложенного списка:

Запускаем дисплей на STM32 через LTDC… на регистрах - 10

Теперь создадим массив и передадим его адрес в 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 скачиваем тут

Автор: NordicEnergy

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js