Воспользовавшись Новогодними праздниками, продолжил поднимать элементы на своей плате. Первым делом после того как запустился дисплей провел тест Lvgl графической библиотеки. Результаты показались удовлетворительным. Около 20 FPS. Иногда были просадки но в целом, без использования DMA и контроллера Chrom-ART, который есть на борту картинка плавная. ART использовать не получится, потому что дисплей с интерфейсом SPI. Это было не первое ограничение с которым я столкнулся на пути оптимизации с целью увеличения FPS


Статью скорее надо рассматривать в образовательных или исследовательских целях. Я пришел к выводу, что если разрабатывать устройство, то надо использовать все фичи, а то получится такой испытательный стенд. На котором не работает Chrom-ART

LVGL

Подключить https://lvgl.io было не сложно, тем более есть два порта под Discovery STM32F429:

Выделение полнокадрового буфера у меня не получилось по причине нехватки памяти в одном сегменте. Решил задействовать SDRAM. Тут меня ждало разочарование с передачей буфера экрана за один раз. Экран 320×240×2 = 153600 байт на фрэйм. STM32F429 DMA на SPI имеет ограничения по причине 16 битного внутреннего счетчика. Соответственно, чтобы передать кадр надо выполнить 153600 / 65535 = ~2,3 транзакции. Не очень ситуация. Это и оказалось бутылочным горлышком. Надо ждать когда будет отправлен последний кусок чтобы начинать отрисовывать новый фрэйм:

DMA restriction 16 bit

There's also an underlying hardware issue: the DMA's NDTR (counting) register is 16-bit long.

Ниже то, что получил в ответ на вопрос по проблеме.

В голову приходит решение сделать Double buffer. В одном рисуем, отдаем на отправку и продолжаем рисовать в другом, если он освободился. В свою очередь оправка смотрит, есть ли готовый буфер и отправляет его переходя к другому. ART позволяет это делать почти автоматически. В изначальной реализации это используется

lvgl forum answer

Your buffer setting (NULL, LV_HOR_RES_MAX * LV_VER_RES_MAX) is too high (as you already noticed by yourself 

Normally the buffer is set around to be LVHORRESMAX * 20 (or 40, or more or less).You are using DMA. DMA in STM32F4/7 or STM32FH7 can only handle up buffer length up to 16 bit (means 65535 bytes).

You set your buffer to LVHORRESMAX * LVVERRES_MAX. For a 320 x 240 that is 76 800.
As you have a 16-bit color values, it means 76800 * 2 = 153600 bytes. That’s more than double of that what the DMA can handle at once.
As only less than a half of the necessary data is transferred to the display, frame rate gets more than doubled.

This is true if lvgl is updating the entire (or nearly the entire) display at once.
When only smaller parts of the display will be redrawn everything is ok. As the used buffer is smaller (less than the 16-bit limit of DMA).

You can of course change your flush function to split the display transfer into smaller parts,
but that will not change the overall time you need for transfer all the display data over SPI.
The bottleneck is SPI.

But with the method to have the buffer within the SDRAM (external memory), I think you will slow down lvgl.

Using the external SDRAM would make sense only when using it as a real frame buffer for display, in the case the STM32F4/7 would use it directly for (parallel) display driving.

Как приготовить Doom в Кубе

За основу взят порт:

По файловой системе изменений почти никаких. Надо только смонтировать диск. Подключаем по SDIO через 4е линии. Не забываем ставить делитель хотя бы на 4. Без него не проходит инициализация флэшки. Я ставлю на 10. После инициализации, впрочем, можно вернуть частоту назад.

 FRESULT res = f_mount(&SDFatFs, (TCHAR const*)SDPath, 1);

Добавляем секцию sdram. На ней будет организована куча. Так же обозначим для каких файлов будем использовать sdram для статических переменных. Еще я задействовал CCMRAM под стек

/* Highest address of the user mode stack */
_estack = ORIGIN(CCMRAM) + LENGTH(CCMRAM);	/* end of "RAM" Ram type memory */

/* Internal Memory Map*/
MEMORY
{
  CCMRAM    (xrw)    : ORIGIN = 0x10000000,   LENGTH = 64K
	rom (rx)	: ORIGIN = 0x08000000, LENGTH = 2048K
	ram (rwx)   : ORIGIN = 0x20000000, LENGTH = 192K
	sdram (rwx) : ORIGIN = 0xD004B000, LENGTH = 7892K /* first 300K is used as LCD frame buffer */
}

SECTIONS 
{	
	/* external SDRAM */
	.sdram (NOLOAD) :
	{
		. = ALIGN(4);
		*(.sdram .sdram.*)
		bin/chocdoom/i_video.o(COMMON)
		bin/chocdoom/r_bsp.o(COMMON)
		bin/chocdoom/w_wad.o(COMMON)
		bin/chocdoom/r_main.o(COMMON)
		bin/chocdoom/r_plane.o(COMMON)
	} > sdram
...

Теперь подправим метод _sbrk для управления динамической памятью кучи через sdram. Реализация sbrkr на ваше усмотрение

#define HEAP_SIZE		0x00700000 // 7 MB

/* heap */
__attribute__ ((section(".sdram")))
static char heap[HEAP_SIZE];

caddr_t _sbrk_r (struct _reent* r, int incr)
{
...

Верхушка памяти отведена под два буфера экрана

    sdram (rwx) : ORIGIN = 0xС004B000, LENGTH = 7892K /* first 300K is used as LCD frame buffer */

Напоминаю, что FMC может управлять до двух банков, соответственно адреса банков

  1. 0xС0000000

  2. 0xD0000000

Отрисовка экрана находится в файле i_video.c

Надо еще было развернуть байты, но я это сделал в инициализации палитры

// swapped = (num>>8) | (num<<8)

Первый эксперимент был по отрисовке линии, второй - всего буфера

void I_FinishUpdate (void)
{
        int x, y;
        byte index;

        lcd_vsync = false;

        for (y = 0; y < SCREENHEIGHT; y++)
        {
                for (x = 0; x < SCREENWIDTH; x++)
                {
                        index = I_VideoBuffer[y * SCREENWIDTH + x];

                        ((uint16_t*)lcd_frame_buffer)[y * SCREENWIDTH + x] = rgb565_palette[index];
                        // swapped = (num>>8) | (num<<8);
                }
//              lcd_draw_line (lcd_frame_buffer, y);
        }

        lcd_draw_buff (lcd_frame_buffer);
        lcd_refresh ();
        lcd_vsync = true;
}

После вывода линий стал подключать RTOS и уперся в выделение памяти. Тюнингом удалось дойти до заставки. Но потом сваливался в static void HardFault_Handler(void).

Еще момент в том, что автор немного подправил реализацию Chocdoom. Были падения по выходам за границы массива при внутреннем построении сцены. Как известно STM32F429 Discovery имеет 2.4" QVGA TFT LCD дисплей. Я же взял 320x240 SPI.

Если бы не ограничение в передаче через DMA в устройство подключенное на SPI можно было бы обойтись без многопоточности. Просто отсылать после отрисовки буфер. И возможно ждать перед началом поставки следующего. Но STM32F4 может пересылать 0xFFFF байт за раз, а это при экране 320x240 и 2 байта на пиксель надо делать более двух раз.

Первый и последний эксперимент был сделан с посылкой и ожиданием в 3 этапа. Ниже функция которая это делает.

void fill_display(SPI_HandleTypeDef *hspi,
                DMA_HandleTypeDef *hdma_spi_tx,
                uint8_t *buff,
                uint32_t size){

        uint16_t max_transaction_size = 0xFFFF;
        uint16_t send_size;
        uint32_t offset = 0;
        uint8_t done = 0;

        HAL_GPIO_WritePin(GPIOC, DC_Pin, GPIO_PIN_SET);
        HAL_GPIO_WritePin(GPIOC, CS_Pin, GPIO_PIN_RESET);

        do {

                if (offset + max_transaction_size <= size){
                        send_size = max_transaction_size;
                } else {
                        send_size = size - offset;
                        done = 1;
                }

                HAL_SPI_Transmit_DMA(hspi, buff + offset, send_size);
                while (HAL_DMA_STATE_BUSY == HAL_DMA_GetState(hdma_spi_tx))
                    { continue; }
                offset += max_transaction_size;

        } while(done == 0);

        HAL_GPIO_WritePin(GPIOC, DC_Pin, GPIO_PIN_RESET);

}

Сделал ради эксперимента. Скорость впечатлила. По сравнению с ESP32 из моих прошлых экспериментов https://habr.com/ru/post/512130/ картинка побежала явно быстрее. ESP32 использовал RTOS но имел на борты PSRAM против SDRAM у STM32F429. Последовательный интерфейс против параллельного. Два ядра у ESP32 против одного у STM32F429. STM32F429 "немного" дороже раза этак в два.

В дополнении я начал прикручивать кнопки и внезапно Doom стал работать задом наперед. Демка крутилась в обратную сторону. Открывать двери надо было спиной...

На этом моменте эксперимент был закончен.

Материалы по теме

В планах

В планах собрать STM32F750x8. На борту у нее всего 64K flash, но можно исполнять инструкции из внешней flash памяти. Расширяемый бюджетный STM32F7

3.9 Quad-SPI memory interface (QUADSPI)

All devices embed a Quad-SPI memory interface, which is a specialized communication interface targeting Single, Dual or Quad-SPI Flash memories. It can work in:

• Direct mode through registers.

• External flash status register polling mode.

• Memory mapped mode. Up to 256 Mbytes external flash are memory mapped, supporting 8, 16 and 32-bit access. Code execution is supported. The opcode and the frame format are fully programmable. Communication can be either in Single Data Rate or Dual Data Rate.

Code execution is supported - Hello ESP32!

Схема всего этого поделья