Skip to main content

FMC and SPI Display Interface

The following scenario shows, generally, the steps involved in writing a TouchGFX driver when selecting Custom display interface in the TouchGFX generator using an LCD connected to either an FMC or through SPI.

Further reading
the STM32L496-DISCO Application template available from the designer uses FMC and can be inspected for inspiration on how to implement a TouchGFX display driver.

The process of writing a TouchGFX display driver for MCUs without embedded display controllers over FMC or SPI½ is identical. The scenario described in this section uses an ST7789H2 LCD Controller to exemplify.

Once FMC or SPI is configured according to board specifications in CubeMX the TouchGFX Generator can be used to generate a HAL, selecting the Custom display interface, which allows developers to write custom code to transfer the updated parts 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.

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 its datasheet to find appropriate commands, outlined below, and further details.

TouchGFX Generator Configuration

Generally, for displays with embedded GRAM such as 8080 or SPI displays, the driver works as follows:

  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 a driver for a "Custom" display interface.

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 */
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 Addrses Set */
parameter[0] = 0x00;
parameter[1] = Xpos;
parameter[2] = 0x00;
parameter[3] = Xpos + Width - 1;
ST7789H2_WriteReg(ST7789H2_CASET, parameter, 4);

/* RASET: Row Addrses 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 a private 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) * BSP_LCD_GetXSize();
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;
}

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 immediatly 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
prepare();

//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);
}

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 CubeMX.

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 developers 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.

Selecting a custom 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.