Skip to main content
Jack3
Senior
June 30, 2026
Question

STM32U5 ILI9488 connected to 16-bit FMC and using GPDMA in 2D mode

  • June 30, 2026
  • 2 replies
  • 22 views

Hello, I connected an ILI9488 480x320 display on a STM32U5 using FMC in 16-bit mode.

I transfer the framebuffer to the display using GPDMA in 2D mode, and all works fine.
But only as long as I transfer the entire framebuffer or a rectangular region that takes the full width of the screent, where rect_y >= 0 and rect_height < =DISPLAY_SIZE_Y and rect_x = 0 and rect_width = DISPLAY_SIZE_X. This way one line in the frambuffer follows the next line, an easy task for the GPDMA, where 2D mode is inrelevant.
Things don’t work out well when I want rect_x > 0 and rect_width < DISPLAY_SIZE_X, so a random rectangle somewhere from the framebuffer. This way for each new line the GPDMA needs to skip (DISPLAY_SIZE_X - rect_width) pixels to arrive at the next data in the framebuffer. I haven’t been able to configure that. The output image gets extremely slanted and extremely resduced in vertical size if if I make the rectangle 4 pixels more narrow than the display horizontal resolution.

I hope my description is understandable.

This works fine:

GPDMA_FrameBuffer_Transfer_Screen(0, 0, 480, 320);
GPDMA_FrameBuffer_Transfer_Screen(0, 60, 480, 200);

But this results in a slanted and vertically reduced image:

GPDMA_FrameBuffer_Transfer_Screen(0, 0, 476, 320);

My code looks like this:

Display_result_t GPDMA_Init(void)
{
/* The GPDMA1 Channels 12, 13, 14, and 15 support 2D addressing */
Display_GPDMA.GPDMA_handle = DISPLAY_GPDMA_HANDLE;

Display_GPDMA.GPDMA_handle.Init.Request = DMA_REQUEST_SW; /* Software triggered Memory-to-Memory */
Display_GPDMA.GPDMA_handle.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
Display_GPDMA.GPDMA_handle.Init.Direction = DMA_MEMORY_TO_MEMORY;

/* Initialize GPDMA1, handle_GPDMA1_Channel12, Linked-List Mode, suitable for 2D addressing */
Display_GPDMA.GPDMA_handle.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
Display_GPDMA.GPDMA_handle.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
Display_GPDMA.GPDMA_handle.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0; /* Port 0 */
Display_GPDMA.GPDMA_handle.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
Display_GPDMA.GPDMA_handle.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_NORMAL;

Display_GPDMA.GPDMA_handle.Init.SrcInc = DMA_SINC_INCREMENTED; /* Source increments */
Display_GPDMA.GPDMA_handle.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_HALFWORD; /* 16-bit width, step 2 bytes */
Display_GPDMA.GPDMA_handle.Init.SrcBurstLength = 1;

Display_GPDMA.GPDMA_handle.Init.DestInc = DMA_DINC_FIXED; /* Destination fixed */
Display_GPDMA.GPDMA_handle.Init.DestDataWidth = DMA_DEST_DATAWIDTH_HALFWORD; /* 16-bit pixel width */
Display_GPDMA.GPDMA_handle.Init.DestBurstLength = 1;

Display_GPDMA.GPDMA_handle.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0 | DMA_DEST_ALLOCATED_PORT1;
Display_GPDMA.GPDMA_handle.Init.Mode = DMA_NORMAL;

if (HAL_DMAEx_List_Init(&Display_GPDMA.GPDMA_handle) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

return DISPLAY_OK;
}

Display_result_t GPDMA_FrameBuffer_Transfer_Screen(uint16_t x, uint16_t y, uint16_t w, uint16_t h)
{
DMA_NodeConfTypeDef node_config = { 0 };

/* The GPDMA1 Channels 12, 13, 14, and 15 support 2D addressing */
Display_GPDMA.GPDMA_handle = DISPLAY_GPDMA_HANDLE;

/* The GPDMA1 Channels supporting 2D addressing are 12, 13, 14, or 15 */

/*
* =========================================================================
* NODE 1 & 2: Set Address Windows (Linear Array Incremented -> Fixed FMC CMD)
* =========================================================================
*/

/* Configure General Node Parameters */
memset(&node_config, 0, sizeof(node_config));

/* Clear queue handle using official HAL layout initialization */
Display_GPDMA.Queue.Head = NULL;

/* Set up Universal Node Behavior Fields */
node_config.NodeType = DMA_GPDMA_2D_NODE;

/* Basic Linear Channel Settings */
node_config.Init.Request = DMA_REQUEST_SW; /* Software-triggered */
node_config.Init.Direction = DMA_MEMORY_TO_MEMORY; /* Direction setting */
node_config.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST; /* Basic burst requirement */
node_config.Init.Mode = DMA_NORMAL; /* Normal list execution */

/* Source settings (Reading the framebuffer) Addressing Rules (Source increments, Destination is fixed) */
node_config.Init.SrcInc = DMA_SINC_INCREMENTED; /* Source increments */
node_config.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_HALFWORD; /* 16-bit width, step 2 bytes */
node_config.Init.SrcBurstLength = 1;

/* Destination settings (Writing to stationary FMC address) */
node_config.Init.DestInc = DMA_DINC_FIXED; /* Destination fixed */
node_config.Init.DestDataWidth = DMA_DEST_DATAWIDTH_HALFWORD; /* 16-bit pixel width */
node_config.Init.DestBurstLength = 1;

/* Port Assignment and Event Signals */
node_config.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0 | DMA_DEST_ALLOCATED_PORT1;
node_config.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;

/* Unused default parameters for safe clearing */
node_config.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
node_config.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
node_config.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;

/* Build Node 1 (First 153,600 Bytes / 38,400 Words) */
node_config.DataSize = (uint32_t)w; /* ToDo: Inner loop: Source items in a single horizontal row (16-bit pixels) */
node_config.SrcAddress = (uint32_t)&FrameBuffer[Display.Framebuffer.Show][y][x];
node_config.DstAddress = DISPLAY_16BIT_ADDR_DATA; /* Route to the DATA register (RS High) */

/* Repeated block configuration parameters */
node_config.RepeatBlockConfig.SrcAddrOffset = (DISPLAY_SIZE_X - w); /* Stride skip remaining pixels of line to start of next line in pixels (HALFWORD) */
node_config.RepeatBlockConfig.BlkSrcAddrOffset = 0; /* Force clear uninitialized register properties to prevent memory junk drift */

node_config.RepeatBlockConfig.DestAddrOffset = 0; /* Fixed destination */
node_config.RepeatBlockConfig.BlkDestAddrOffset = 0;

node_config.RepeatBlockConfig.RepeatCount = h * DISPLAY_BYTES_PER_PIXEL; /* ToDo: Total rows (outer loop), entire horizontal block repeat count, Translates to height * 2 bytes */

if (HAL_DMAEx_List_BuildNode(&node_config, &TestNode1) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

/* Insert nodes into queue (NULL acts as tail append) */
if (HAL_DMAEx_List_InsertNode_Tail(&Display_GPDMA.Queue, &TestNode1) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

/* Initialize DMA and Link Queue */
Display_GPDMA.GPDMA_handle.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
Display_GPDMA.GPDMA_handle.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION; /* Process whole list */
Display_GPDMA.GPDMA_handle.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0; /* Access memory list from Port0 */
Display_GPDMA.GPDMA_handle.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;

/* 8. Initialize Channel */
if (HAL_DMAEx_List_Init(&Display_GPDMA.GPDMA_handle) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

/* Link the completed memory list schema queue to the hardware peripheral handle of GPDMA Channel 12 */
if (HAL_DMAEx_List_LinkQ(&Display_GPDMA.GPDMA_handle, &Display_GPDMA.Queue) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

/* Send the window location and size to be updated to the ILI9488 */
Display_16Bit_Set_DisplayWindow(x, y, w, h);

/* Register the callback before starting the interrupt handler */
HAL_DMA_RegisterCallback(&Display_GPDMA.GPDMA_handle, HAL_DMA_XFER_CPLT_CB_ID, My_DMA_Complete_Callback);

if (HAL_DMAEx_List_Start(&Display_GPDMA.GPDMA_handle) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

return DISPLAY_OK;
}

What am I doing wrong?

2 replies

Jack3
Jack3Author
Senior
June 30, 2026

Some spelling mistakes, but there’s not edit option anymore. That sucks, but I hope it’s clear enough.

Jack3
Jack3Author
Senior
July 2, 2026

I used DMA2D to transfer a framebuffer or a rectangular region of it to the 480x320 ILI9488 display connected in 16-bit to the FMC. This works fine.
I can do the same thing using GPDMA in DMA_GPDMA_2D_NODE.
Except the framebuffer block needs to be contigues block of memeory, meaning it has to be the full width of the display.
Can GPDMA used if I want the transfer window is more narrow than the screen width, which means for each line it needs to jump over the remain pixels on the line(s) to arrive at the pixel next line in the window?
I have been struggling, becuase when I do that the output gets slanted and strongly reduced in vertical size.

My code:
 

Display_result_t GPDMA_FrameBuffer_Transfer_Screen(void)
{
DMA_NodeConfTypeDef node_config = { 0 };

Display_GPDMA.GPDMA_handle = DISPLAY_GPDMA_HANDLE;

uint16_t x = 32;
uint16_t y = 0;
uint16_t w = DISPLAY_SIZE_X - 64;
uint16_t h = DISPLAY_SIZE_Y;

/* The GPDMA1 Channels 12, 13, 14, and 15 support 2D addressing */
Display_GPDMA.GPDMA_handle = DISPLAY_GPDMA_HANDLE;

Display_GPDMA.GPDMA_handle.Init.Request = DMA_REQUEST_SW; /* Software triggered Memory-to-Memory */
Display_GPDMA.GPDMA_handle.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
Display_GPDMA.GPDMA_handle.Init.Direction = DMA_MEMORY_TO_MEMORY;

/* Initialize GPDMA1, handle_GPDMA1_Channel12, Linked-List Mode, suitable for 2D addressing */
Display_GPDMA.GPDMA_handle.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
Display_GPDMA.GPDMA_handle.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
Display_GPDMA.GPDMA_handle.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0; /* Port 0 */
Display_GPDMA.GPDMA_handle.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
Display_GPDMA.GPDMA_handle.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_NORMAL;

Display_GPDMA.GPDMA_handle.Init.SrcInc = DMA_SINC_INCREMENTED; /* Source increments */
Display_GPDMA.GPDMA_handle.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_HALFWORD; /* 16-bit width, step 2 bytes */
Display_GPDMA.GPDMA_handle.Init.SrcBurstLength = 16;

Display_GPDMA.GPDMA_handle.Init.DestInc = DMA_DINC_FIXED; /* Destination fixed */
Display_GPDMA.GPDMA_handle.Init.DestDataWidth = DMA_DEST_DATAWIDTH_HALFWORD; /* 16-bit pixel width */
Display_GPDMA.GPDMA_handle.Init.DestBurstLength = 1;

Display_GPDMA.GPDMA_handle.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0 | DMA_DEST_ALLOCATED_PORT1;
Display_GPDMA.GPDMA_handle.Init.Mode = DMA_NORMAL;

if (HAL_DMAEx_List_Init(&Display_GPDMA.GPDMA_handle) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

Display_GPDMA.Queue.Head = NULL;

node_config.NodeType = DMA_GPDMA_2D_NODE;
node_config.Init.Request = DMA_REQUEST_SW; /* Software-triggered */
node_config.Init.Direction = DMA_MEMORY_TO_MEMORY; /* Direction setting */
node_config.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST; /* Basic burst requirement */
node_config.Init.Mode = DMA_NORMAL; /* Normal list execution */
node_config.Init.SrcInc = DMA_SINC_INCREMENTED; /* Source increments */
node_config.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_HALFWORD; /* 16-bit width, step 2 bytes */
node_config.Init.SrcBurstLength = 1;
node_config.Init.DestInc = DMA_DINC_FIXED; /* Destination fixed */
node_config.Init.DestDataWidth = DMA_DEST_DATAWIDTH_HALFWORD; /* 16-bit pixel width */
node_config.Init.DestBurstLength = 1;
node_config.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0 | DMA_DEST_ALLOCATED_PORT1;
node_config.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
node_config.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
node_config.DataSize = (uint32_t)w * DISPLAY_BYTES_PER_PIXEL; /* Inner loop (bytes, not pixels) */
node_config.SrcAddress = (uint32_t)&FrameBuffer[Display.Framebuffer.Show][y][x];
node_config.DstAddress = DISPLAY_16BIT_ADDR_DATA; /* Route to the DATA register (RS High) */
node_config.RepeatBlockConfig.SrcAddrOffset = (DISPLAY_SIZE_X - w); /* Stride skip remaining pixels of line to start of next line in pixels (HALFWORD) */
node_config.RepeatBlockConfig.BlkSrcAddrOffset = 0; /* Force clear uninitialized register properties to prevent memory junk drift */
node_config.RepeatBlockConfig.DestAddrOffset = 0; /* Fixed destination */
node_config.RepeatBlockConfig.BlkDestAddrOffset = 0;

node_config.RepeatBlockConfig.RepeatCount = h; /* Total rows (outer loop), entire horizontal block repeat count */

node_config.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
node_config.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;

if (HAL_DMAEx_List_BuildNode(&node_config, &TestNode1) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

/* Insert nodes into queue (NULL acts as tail append) */
if (HAL_DMAEx_List_InsertNode_Tail(&Display_GPDMA.Queue, &TestNode1) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

/* Initialize DMA and Link Queue */
Display_GPDMA.GPDMA_handle.InitLinkedList.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
Display_GPDMA.GPDMA_handle.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION; /* Process whole list */
Display_GPDMA.GPDMA_handle.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0; /* Access memory list from Port0 */
Display_GPDMA.GPDMA_handle.InitLinkedList.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;

if (HAL_DMAEx_List_Init(&Display_GPDMA.GPDMA_handle) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

if (HAL_DMAEx_List_LinkQ(&Display_GPDMA.GPDMA_handle, &Display_GPDMA.Queue) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

/* Send the window location and size to be updated to the ILI9488 */
Display_16Bit_Set_DisplayWindow(x, y, w, h);

if (HAL_DMAEx_List_Start(&Display_GPDMA.GPDMA_handle) != HAL_OK)
{
if (Display.ErrorCallback != NULL)
{
Display.ErrorCallback(DISPLAY_ERROR, __FILE__, __LINE__);
}

return DISPLAY_ERROR;
}

return DISPLAY_OK;
}

What am I doing wrong?