cancel
Showing results for 
Search instead for 
Did you mean: 

Help needed - STM32L562E-DK - Correct way to efficiently write to TFT LCD-Display, ST7789H2 driver

MPetr.3
Associate III

Hello community,

I am trying to efficiently display graphics on my STM32L562E-DK board with low-level code (only drivers and util-methods) without using any big external software like TouchGFX, Embedded Wizard, etc.

In short: I am not achieving the same frame rate / update effect with my code as i.e. Embedded Wizard does. They have a high frame rate and no visible screen update effect.

(I am just beginning with using displays on microcontrollers.)

Additional Board information: The display is connected via a 29-pin FCP connector to FMC of the microcontroller.

After inspecting the official Github-Democode for the STM32L562E-DK, I grabbed myself the classes:

 "stm32l562e_discovery_lcd.h"
 "lcd.h"
 "st7789h2.h"
"st7789h2_reg.h"
"stm32l552e_eval_lcd.h"
"../Components/hx8347i/hx8347i.h"
// also included its associated .c files

Those files work perfectly fine, I can for example use this function to display stuff on the display:

static void Display_DemoDescription(void)
{
  char desc[60];
 
  /* Set font */
  UTIL_LCD_SetFont(&Font24);
 
  /* Clear the LCD */
  UTIL_LCD_Clear(UTIL_LCD_COLOR_WHITE);
  
  /* Set the LCD Text Color */
  UTIL_LCD_SetTextColor(UTIL_LCD_COLOR_DARKBLUE);
  UTIL_LCD_SetBackColor(UTIL_LCD_COLOR_WHITE);
  
  /* Display LCD messages */
  UTIL_LCD_DisplayStringAt(0, 10, (uint8_t *)"STM32L552-EV BSP", CENTER_MODE);
  UTIL_LCD_DisplayStringAt(0, 35, (uint8_t *)"drivers example", CENTER_MODE);
 
  /* Draw Bitmap */
  UTIL_LCD_DrawBitmap(120, 65, (uint8_t *)stlogo);
  
  UTIL_LCD_SetFont(&Font12);
  UTIL_LCD_DisplayStringAt(0, 220, (uint8_t *)"Copyright (c) STMicroelectronics 2019", CENTER_MODE);
  
  UTIL_LCD_SetFont(&Font16);
  UTIL_LCD_FillRect(0, 150, 320, 50, UTIL_LCD_COLOR_BLUE);
  UTIL_LCD_SetTextColor(UTIL_LCD_COLOR_WHITE);
  UTIL_LCD_SetBackColor(UTIL_LCD_COLOR_BLUE);
  UTIL_LCD_DisplayStringAt(0, 150, (uint8_t *)"Press Wakeup push-button", CENTER_MODE);
  UTIL_LCD_DisplayStringAt(0, 165, (uint8_t *)"to start :", CENTER_MODE);
  sprintf(desc,"%s example", BSP_examples[DemoIndex].DemoName);
  UTIL_LCD_DisplayStringAt(0, 180, (uint8_t *)desc, CENTER_MODE);
}

But here comes the problem: The screen update is so slow, so that I can even see the pixels of a rect or oval building up in a short flash, or I see the stlogo getting drawn from top to bottom in approx. 50milliseconds or so.

I'm now asking for help to correctly display graphics on the screen with a fast frame rate and without a visible display update effect.

My suspected possible causes:

I think, the above code and its util-functions like fillrect(..) send each drawn pixel individually to the display.

I know that for a smooth view you need to use framebuffers. They are (as far as I see) not utilized in the above code. But I'm also wondering where to store the framebuffer. 256kB of Ram available, 240*240 pixels *4bytes/pixel = 230kB Ram for framebuffer.

I really would appreciate help on this topic on how to really draw efficiently on the display. If also anyone knows how Embedded Wizard is doing it, I would really appreciate help.

Thanks in advance.

5 REPLIES 5

ST's code for lines/circles is quite slow, plotting a pixel at a time. I'm sure that TouchGFX has much more optimal coding, and likely uses secondary frame buffer, and is aware of the vertical fresh.

Tips, Buy me a coffee, or three.. PayPal Venmo
Up vote any posts that you find helpful, it shows what's working..
MPetr.3
Associate III

Thanks for your answer. So you're saying ST's code plots circles pixel by pixel, which I also suspected. But is there an alternative way to display forms like that? Is there a more advanced communication to not only write pixel by pixel individually but blocks of pixel data in one shot?

To additionally avoid the flickering a frame buffer has to be used I think. Is there then a method to send the framebuffer data (if stored on the uC) to the display in one operation?

With a secondary framebuffer (which is stored on the display/driver-chip, is this correct?), there must be a method to switch the "active layer" to this frame buffer, so no flickering occurs. Does this method exist? I have not found it in the ST's drivers or code.

Thanks in advance.

Back when I was doing such things, the trick was typically to understand where neighbouring pixels were in the memory buffer, and to manage things in the memory address domain, rather than the X,Y.

Optimize horizontal/vertical line draws, fills.

Probably two host side buffers, changing the LTDC buffer address at the frame refresh.

Tips, Buy me a coffee, or three.. PayPal Venmo
Up vote any posts that you find helpful, it shows what's working..

I just took a look into the touchGFX lib used at the demo-software at github, and it seems like they use DMA and a framebuffer with 16bit/Pixel.

I'm looking for a library like the one TouchGFX uses, but they kind of modified everything.

Do you or anybody else knows if a library exists which takes care of framebuffers and DMA etc.?

Since there is probably no such thing, I wonder how I can use TouchGFX for my STM32L5 Discovery Board to generate a small project and extract the nessecary code. The Demo uses it but there is no "official way" to make a TouchGFX Project for this board. It's officially not supported in STM32CubeIDE or TouchGFX Designer, yet it has been done for the demo somehow.

Does anybody know a way of creating a TouchGFX Project for the Demo-Board? Maybe create a Project for a supported board and after that change the configuration to match the L5-Board?

Thanks.

MPetr.3
Associate III

Hello,

I found a good solution I want to share with you.

After inspecting other third-party libs it, as expected, boiled down to pretty much one function, which transfers a frame buffer to the lcd display. This function was just not included in the official ST library. I would not recommend using the non-framebuffer function there.

The code to transfer a buffer from SRAM to the LCD display via the CPU is the following:

// rx=rect.x, ry=rect.y, rw=rect.width, rh=rect.height
 
void copyFrameBufferBlockToLCD(uint32_t rx, uint32_t ry, uint32_t rw, uint32_t rh, uint16_t *frameBuffer/*)
{
    uint16_t tmp;
 
    uint16_t *ptr = frameBuffer + rx + ry * DISPLAY_WIDTH;
 
    /* Set Cursor */
    __ST7789H2_SetDisplayWindow(rx, ry, rw, rh);
 
    /* Prepare to write */
    __ST7789H2_WriteReg(ST7789H2_WRITE_RAM, (uint16_t*)NULL, 0);
 
    /* Read dummy data */
    __ST7789H2_ReadData(&tmp, 1);
 
  
    for (uint32_t h = 0; h < rh ; h++)
    {
        __ST7789H2_WriteData((ptr + h * DISPLAY_WIDTH), rw);
    }
}

I used a full-size frame buffer (not partial), because it did fit into the RAM. The color format is RGB565.

A transfer at full speed takes 21ms.

If you want to further advance the code and use the DMA instead of the CPU to save all the CPU time spend on the transfer only, then the code is as follows:

Initialize the DMA:

DMA_HandleTypeDef hdma;
void MX_DMA_Init(uint32_t alignement)
{
    static uint32_t DmaIntialized = 0;
    static uint32_t DmaMemDataAlignment= 0;
 
    if (DmaMemDataAlignment == 0) /* The very first init is to be performed */
    {
        /* DMA controller clock enable once */
        __HAL_RCC_DMAMUX1_CLK_ENABLE();
        __HAL_RCC_DMA1_CLK_ENABLE();
        __HAL_DMA_RESET_HANDLE_STATE(&hdma);
    }
 
    if (DmaMemDataAlignment != alignement)
    {
        if (DmaIntialized)
        {
            HAL_DMA_DeInit(&hdma);
            __HAL_DMA_RESET_HANDLE_STATE(&hdma);
            DmaIntialized = 0;
        }
        DmaMemDataAlignment = alignement;
    }
 
    if (!DmaIntialized)
    {
        DmaIntialized = 1;
 
        /* Configure DMA request hdma on DMA1_Channel1 */
        hdma.Instance = DMA1_Channel1;
        hdma.Init.Request = DMA_REQUEST_MEM2MEM;
        hdma.Init.Direction = DMA_MEMORY_TO_MEMORY;
        //hdma.Init.Direction = /*DMA_MEMORY_TO_MEMORY*/DMA_MEMORY_TO_PERIPH;
        hdma.Init.PeriphInc = DMA_MINC_DISABLE;//DMA_PINC_ENABLE
        hdma.Init.MemInc = DMA_MINC_DISABLE;//DMA_PINC_ENABLE
        if (DmaMemDataAlignment == 1)
        {
            hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
            hdma.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
        }
        else
        {
            hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
            hdma.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
        }
        hdma.Init.Mode = DMA_NORMAL;
        hdma.Init.Priority = DMA_PRIORITY_HIGH;
        if (HAL_DMA_Init(&hdma) != HAL_OK)
        {
            while(1);
        }
    }
}

Copy frame buffer via DMA to LCD:

void copyFrameBufferBlockToLCDDMA(uint32_t rx, uint32_t ry, uint32_t rw, uint32_t rh, uint16_t *frameBuffer)
{
    uint16_t tmp;
    uint16_t *ptr = frameBuffer + rx + ry * DISPLAY_WIDTH;
 
    /* Set Cursor */
    __ST7789H2_SetDisplayWindow(rx, ry, rw, rh);
 
    /* Prepare to write */
    __ST7789H2_WriteReg(ST7789H2_WRITE_RAM, (uint16_t*)NULL, 0);
 
    /* Read dummy data */
    __ST7789H2_ReadData(&tmp, 1);
 
    int factor = 40; //adds multiple lines in one command
    for (uint32_t h = 0; h < rh ; h+=factor)
    {
        MX_DMA_Init(2);
        HAL_DMA_Start_IT(&hdma, (uint32_t)((ptr + h * DISPLAY_WIDTH)), (uint32_t)LCD_DATA_ADDR, rw*factor);// (2 * rect.width was original but was 2x too much)
        HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, 100); 
    }
}

As you noticed, I combined multiple line writes in a single DMA transfer too further save minimal overhead.

In the following section are the few functions you need too, just some standard read write register functions, nothing special. It is not really nice formated and some stuff is commented out to just make the default implementation easier:

void __ST7789H2_WriteReg(uint16_t Reg, uint16_t *pData, uint16_t Length)
{
    uint16_t i = 0;
    /* Write register address */
    *(uint16_t *)LCD_REGISTER_ADDR = Reg;
    while (i < Length)
    {
        /* Write data value */
        *(uint16_t *)LCD_DATA_ADDR = pData[i++];
    }
}
 
void __ST7789H2_ReadData(uint16_t *pData, uint16_t Length)
{
    uint16_t i = 0;
    while (i < Length)
    {
        /* Read value */
        pData[i++] = *(uint16_t *)LCD_DATA_ADDR;
    }
}
 
 
void __ST7789H2_WriteData(uint16_t *pData, uint16_t Length)
{
     uint16_t i = 0;
     while (i < Length)
     {
         /* Write data value */
         *(uint16_t *)LCD_DATA_ADDR = pData[i++];
     }
}
 
//Rect refreshRect = {0, 0, 0, 0};
uint16_t        DISPLAY_WIDTH = 240;         ///< The width of the LCD display in pixels.
uint16_t        DISPLAY_HEIGHT = 240;
 
 
void __ST7789H2_SetDisplayWindow(uint16_t Xpos, uint16_t Ypos, uint16_t Width, uint16_t Height)
{
    //if (refreshRect != Rect(Xpos, Ypos, Width, Height))
    //{
        // Check ST7789H2_DrawBitmap() implementation
        uint8_t  parameter[8];
        uint32_t Xstart, Xstop, Ystart, Ystop;
        //static uint32_t Orientation = __ST7789H2_GetOrientation(); // Fixed Orientation
 
        /* Compute new Y start and stop values */
//        if (Orientation == LCD_ORIENTATION_PORTRAIT)
//        {
            Ystart = Ypos;
            Ystop  = Ypos + Height - 1;
            Xstart = Xpos;
            Xstop  = Xpos + Width - 1;
//        }
//        else if (Orientation == LCD_ORIENTATION_LANDSCAPE)
//        {
//            Ystart = Ypos;
//            Ystop  = Ypos + Height - 1;
//            Xstart = Xpos + 0x50U;
//            Xstop  = Xpos + Width - 1 + 0x50U;
//        }
//        else if (Orientation == LCD_ORIENTATION_PORTRAIT_ROT180)
//        {
//            Ystart = Ypos + 0x50U;
//            Ystop  = Ypos + Height - 1 + 0x50U;
//            Xstart = Xpos;
//            Xstop  = Xpos + Width - 1;
//        }
//        else if (Orientation == LCD_ORIENTATION_LANDSCAPE_ROT180)
//        {
//            Ystart = Ypos;
//            Ystop  = Ypos + Height - 1;
//            Xstart = Xpos;
//            Xstop  = Xpos + Width - 1;
//        }
 
        /* CASET: Column Address Set */
        parameter[0] = (uint8_t)(Xstart >> 8);           /* XS[15:8] */
        parameter[1] = 0x00;
        parameter[2] = (uint8_t) Xstart;                 /* XS[7:0] */
        parameter[3] = 0x00;
        parameter[4] = (uint8_t)(Xstop >> 8);            /* XE[15:8] */
        parameter[5] = 0x00;
        parameter[6] = (uint8_t) Xstop;                  /* XE[7:0] */
        parameter[7] = 0x00;
        __ST7789H2_WriteReg(ST7789H2_CASET, (uint16_t *)parameter, 4);
 
        /* RASET: Row Address Set */
        parameter[0] = (uint8_t)(Ystart >> 8);           /* YS[15:8] */
        parameter[1] = 0x00;
        parameter[2] = (uint8_t) Ystart;                 /* YS[7:0] */
        parameter[3] = 0x00;
        parameter[4] = (uint8_t)(Ystop >> 8);            /* YE[15:8] */
        parameter[5] = 0x00;
        parameter[6] = (uint8_t) Ystop;                  /* YE[7:0] */
        parameter[7] = 0x00;
        __ST7789H2_WriteReg(ST7789H2_RASET, (uint16_t *)parameter, 4);
 
      //  refreshRect = Rect(Xpos, Ypos, Width, Height);
    //}
}

If you have any questions to this feel free to ask me or reply to that thread.