Skip to main content

FMC and SPI Display Interface

The following scenario shows, generally, the steps involved in transferring pixels to an LCD connected to either an FMC or through SPI; Two methods that share some elements. The scenario described in this section uses an ST7789H2 LCD Controller to exemplify.

Once FMC or SPI is configured according to board specifications in STM32CubeMX TouchGFX Generator can be used to generate a TouchGFX HAL allowing developers to write code that transfers updated part of the application framebuffer to a connected display.

The figure below shows a TouchGFX Generator configuration with Custom Display Interface selected. This instructs the TouchGFX Generator that the developer would like to configure and transfer pixels from the framebuffer memory to the display manually and generates the handles to accomplish this.

TouchGFX Generator Configuration

Tip
For displays connected through SPI, custom display interface must be selected

Generally, for displays with embedded GRAM, the code written by the user in the generated TouchGFX HAL should perform the following steps:

  1. Based on the area of the framebuffer to be redrawn, move the "display cursor" and "active window" to a place in GRAM that matches this area.
  2. Prepare to write incoming pixel data to GRAM.
  3. Send pixel data.

Transferring the framebuffer

When an area of the framebuffer has been updated, the TouchGFX Engine calls HAL::flushFrameBuffer(Rect r). This function can be overridden when developers must implement code to transfer pixels to a display manually, as in the case of SPI and FMC. As we'll see, functions to transfer pixels via the FMC Banks are generated by TouchGFX Generator.

Note
The driver code shown in this section for the ST7789H2 would have been developed during the Board Bringup phase and, once working, can more or less be copied to the HAL class generated by the TouchGFX Generator.

The driver must be able to transfer pixels to the display, and to control the memory writing position of the display. Check the datasheet for your display for further details.

void  TouchGFXHAL::flushFrameBuffer(const Rect& rect)
{
/* Set Cursor */
ST7789H2_SetDisplayWindow(rect.x, rect.y, rect.width, rect.height);

/* Prepare to write to LCD RAM */
__ST7789H2_WriteReg(ST7789H2_WRITE_RAM, (uint8_t*)NULL, 0);

/* Send Pixels - User defined function */
this->copyFrameBufferBlockToLCD(rect);
}

The following function ST7789H2_SetDisplayWindow sets the x and y coordinates for the virtual "cursor" in GRAM by writing to specific registers, which is usual for displays using GRAM.

extern "C"
void ST7789H2_SetDisplayWindow(uint16_t Xpos, uint16_t Ypos, uint16_t Width, uint16_t Height)
{
uint8_t parameter[4];

/* CASET: Column Address Set */
parameter[0] = 0x00;
parameter[1] = Xpos;
parameter[2] = 0x00;
parameter[3] = Xpos + Width - 1;
ST7789H2_WriteReg(ST7789H2_CASET, parameter, 4);

/* RASET: Row Address Set */
parameter[0] = 0x00;
parameter[1] = Ypos;
parameter[2] = 0x00;
parameter[3] = Ypos + Height - 1;
ST7789H2_WriteReg(ST7789H2_RASET, parameter, 4);
}

The following function TouchGFXHAL::copyFrameBufferBlockToLCD is, in this example, a user defined function that sends one line of the updated area (Rect) at a time, ensuring to progress the framebuffer pointer accordingly.

void TouchGFXHAL::copyFrameBufferBlockToLCD(const Rect& rect)
{
__IO uint16_t* ptr;
uint32_t height;

// This can be accelerated using regular DMA hardware
for (height = 0; height < rect.height ; height++)
{
ptr = getClientFrameBuffer() + rect.x + (height + rect.y) * HAL::DISPLAY_WIDTH;
LCD_IO_WriteMultipleData((uint16_t*)ptr, rect.width);
}
}

Instead of advancing ptr manually, the TouchGFX Generator will generate a function advanceFrameBufferToRect that advances ptr according to the position of Rect in the framebuffer.

inline uint8_t* TouchGFXGeneratedHAL::advanceFrameBufferToRect(uint8_t* fbPtr, const touchgfx::Rect& rect) const
{
// Advance vertically Advance horizontally
fbPtr += rect.y * lcd().framebufferStride() + rect.x * 2;
return fbPtr;
}

FMC

TouchGFX Generator also supports FMC display interface, if at least one FMC Bank is configured correctly. In this case, the code generated by TouchGFX Generator is similar to that of Custom display interface, except that the function LCD_IO_WriteMultipleData is generated to interact with the FMC bank connected to the display. Revisiting the code presented earlier for the function copyFrameBufferBlockToLCD you'll see that it uses the generated function.

Tip
For both SPI and FMC display interfaces, developers will modify the flushFrameBuffer() function to 1) Set cursor 2) Prepare to write to GRAM 3) transfer the pixels either through a custom SPI display driver or through the generated FMC Bank functions.
    __weak void LCD_IO_WriteMultipleData(uint16_t* pData, uint32_t Size)
{
uint32_t i;

for (i = 0; i < Size; i++)
{
FMC_BANK1_WriteData(pData[i]);
}
}

The following figure shows a valid 16-bit (required) configuration of FMC bank 2 (either can be used).

FMC Bank Configuration

Once a valid configuration has been met, this bank can be selected in TouchGFX Generator. Verify the start Start address of the FMC Bank Register for your MCU.

FMC Interface Selection

TouchGFX Generator validates the configuration of the FMC Banks and reports any issues it may find.

FMC Configuration Error

Returning from HAL::flushFrameBuffer()

Once the function returns TouchGFX Engine continues to draw the rest of the frame. If developers wish to use DMA to transfer pixels to the display, they must ensure that HAL::flushFrameBuffer(Rect& rect) does not return immediately by e.g. waiting on a semaphore signaled by a DMA Completed interrupt.

The following pseudo-code example shows an example of how HAL::flushFrameBuffer() could be structured in case DMA is used. The code uses a FreeRTOS semaphore screen_frame_buffer_sem.

void TouchGFXHAL::flushFrameBuffer(const touchgfx::Rect& rect)
{
uint16_t* fb = HAL::lockFrameBuffer();

//Prepare display: Set cursor, write to display gram as described previously in this scenario

//Try to take a display semaphore - Always free at this point
xSemaphoreTake(screen_frame_buffer_sem, portMAX_DELAY);

//Set up DMA
screenDMAEnable();

// Wait for the DMA transfer to complete
xSemaphoreTake(screen_frame_buffer_sem, portMAX_DELAY);

//Unlock framebuffer and give semaphore back
HAL::unlockFrameBuffer();
xSemaphoreGive(screen_frame_buffer_sem);
}
Caution
The FMC code generated by TouchGFX Generator does not use DMA.

TouchGFX Driver / Tearing Effect Signal

As can be seen in TouchGFX Generator configuration above, the "Application Tick Source" is also set to "Custom", which is general for MCUs without embedded TFT Controllers.

As described in the Abstraction Layer Architecture section, the TouchGFX Engine main loop is unblocked by calling OSWrappers::signalVSync(), usually at the time when a display signals.

For displays with a serial or 8080 display interface, the embedded display controller typically raises a periodic Tearing Effect (TE) signal that can be connected to a GPIO on the MCU. In this case, the MCU is usually configured to raise an interrupt when the GPIO is signalled. This "Tearing Effect" interrupt will then unblock the TouchGFX Engine Main loop to render the next frame. Remember to configure the GPIO to input and enable the external interrupt for the pin in STM32CubeMX.

extern "C"
void TE_Handler(void)
{
...
/* Unblock TouchGFX Engine Main Loop to render next frame */
OSWrappers::signalVSync();
...
}

Conclusion

Selecting Custom Display Interface through the TouchGFX Generator is an expression of a developer's intent to write code to transfer pixels from an application frame buffer to a display, manually.

The TouchGFX Generator will generate a function TouchGFXHAL::flushFrameBuffer(Rect& rect) that is called automatically by TouchGFX after rendering an area of the framebuffer that developers can use to transfer affected pixels to a display, SPI, FMC or otherwise. Regardless, the following steps must be completed in both cases:

  1. Based on the area of the framebuffer to be redrawn, move the "display cursor" and "active window" to a place in GRAM that matches this area.
  2. Prepare to write incoming pixel data to GRAM.
  3. Send pixel data. For FMC display interface, this function is generated for you and can be used inside flushFrameBuffer(Rect& rect) (See earlier in this article).

Selecting the custom or FMC display interface also requires developers to implement a custom TouchGFX Application Tick driver that signals OSWrappers::signalVSync() to unblock the TouchGFX Engine Main loop. Usually, displays used along with MCUs that have no TFT Controllers can provide a Tearing Effect signal that is connected to the MCU.