Skip to main content
Harvey White
Senior III
July 5, 2026
Question

U5 DMA not setting interrupt or executing callback, stays busy

  • July 5, 2026
  • 5 replies
  • 101 views

Using the latest CubeMXIDE, CubeIDE, and drivers.
Custom hardware driving an ILI9341 through SPI1
 

Driver (attached) writes column and page  registers, writes RAMWRITE command, and then transfers in one of three methods:  blocking, IRQ, and DMA.

blocking works fine, so the hardware and commands are proper.

IRQ and DMA do not work.  Working specifically on DMA at the moment.

Software for the driver next:

 

case ROTATE_180:

{

// checked valid



// new design driver, landscape mode, draws left to right



// note: DISPLAY IS IN native landscape MODE

// range is 0-319 and 0-239





// reverse X if not commented out

// data.x0 = 319-data.x0;

// data.x1 = 319-data.x1;



// reverse Y if not commented out

// data.y0 = 239-data.y0;

// data.y1 = 239-data.y1;



minmax (data.x0, 0, 319);

minmax (data.x1, 0, 319);



minmax (data.y0, 0, 239);

minmax (data.y1, 0, 239);



if (data.x1 < data.x0) swap(data.x0,data.x1);

if (data.y1 < data.y0) swap(data.y0,data.y1);





#ifdef _LVGL

// com_addr[0] = ILI9341_PASET;

// com_data[0] = (319-data.x1) >> 8; // MSB LOWER

// com_data[1] = (319-data.x1) & 0xFF; // LSB LOWER

// com_data[2] = (319-data.x0) >> 8; // MSB UPPER

// com_data[3] = (319-data.x0) & 0xFF; // LSB UPPER

#else

com_addr[0] = ILI9341_PASET;

com_data[0] = (data.x0) >> 8; // MSB LOWER

com_data[1] = (data.x0) & 0xFF; // LSB LOWER

com_data[2] = (data.x1) >> 8; // MSB UPPER

com_data[3] = (data.x1) & 0xFF; // LSB UPPER

#endif



// command sequence is send command with A0 down, then data with A0 up.

// CS must remain active low throughout the transaction



#ifdef _SIMPLE

// command pointer says to send data with A0 down

if (A0.port != nullptr) A0.reset();

status = HAL_SPI_Transmit(SPI_interface, (unsigned char*) &com_addr[0], 1, 2);



// data pointer says to send data with A0 up

if (A0.port != nullptr) A0.set();

status = HAL_SPI_Transmit( SPI_interface,&com_data[0],4, 2);

#else

ILI9341_spi->Send(&com_addr[0], 1, &com_data[0], 4, CS_INACTIVE);

#endif



dx = abs(data.x1 - data.x0);

// dy = data.y1 - data.y0;



// build QVGA_image from QVGA, DMA2D can be called from RGB565 buffer



RGB565_BUFFER(translate, page, 0, 0, 320, 240); // QVGA_image now regenerated from main image in RGB565



for (int y = (data.y0); y <= (data.y1); y++)

{

// legal values are 0 to 239 because of rotation

// reverse Y direction to go top down on physical display

// for smart light board with LED strip at bottom



com_addr[0] = ILI9341_CASET;

com_data[0] = (y) >> 8; // MSB LOWER

com_data[1] = (y) & 0xFF; // LSB LOWER

com_data[2] = (y) >> 8; // MSB UPPER

com_data[3] = (y) & 0xFF; // LSB UPPER



// CASET writes from y0 to y1 inclusive, so y1 does not need to be incremented

#ifdef _SIMPLE

// command pointer says to send data with A0 down

if (A0.port != nullptr) A0.reset();

status = HAL_SPI_Transmit(SPI_interface, (unsigned char*) &com_addr[0], 1, 2);



// data pointer says to send data with A0 up

if (A0.port != nullptr) A0.set();

status = HAL_SPI_Transmit( SPI_interface,&com_data[0],4, 2);

#else

ILI9341_spi->Send(&com_addr[0], 1, &com_data[0], 4, CS_INACTIVE);

#endif





// fill line buffer with one line of data

// in RGB565 format

com_addr[0] = ILI9341_RAMWR;



#ifdef _SIMPLE

// command pointer says to send data with A0 down

if (A0.port != nullptr) A0.reset();

status = HAL_SPI_Transmit(SPI_interface, (unsigned char*) &com_addr[0], 1, 2);



#else

status = ILI9341_spi->Send(&com_addr[0], 1,

nullptr, 0, CS_INACTIVE);

#endif



if (A0.port != nullptr) A0.set();

#ifdef _SIMPLE

// data pointer says to send data with A0 up

// dx is low by one. i.e. start at 0 and stop at 1 is two rows in terms of input to routine

// transfer is in bytes because command is a byte, and SPI interface is set to bytes



#if (defined _ILI9341_DMA)



// RTOS_ILI9341[interface].tag = false;

// taskENTER_CRITICAL();

// status = HAL_SPI_Abort(SPI_interface);

// status =HAL_SPI_DMAStop(&hspi1);

// while (SPI_interface->State == HAL_SPI_STATE_BUSY_TX); // wait end of transfer

// result = HAL_SPI_Transmit_DMA(SPI_interface, (unsigned char*)&line_buffer[data.x0],(dx+1));



result = HAL_SPI_Transmit_DMA(SPI_interface, (unsigned char*)&QVGA_image.Lpixel[y][data.x0],(dx+1));



// taskEXIT_CRITICAL();

// while (hSPI1.State == HAL_SPI_STATE_); // wait end of transfer

// while (SPI_interface->State != HAL_SPI_STATE_READY); // wait end of transfer

//// status =HAL_SPI_DMAStop(&hspi1);

// result = HAL_SPI_Transmit_IT(SPI_interface, (unsigned char*)&line_buffer[data.x0],(dx+1)); // 2 bytes/pixel

// while (RTOS_ILI9341[interface].tag == false);



status = HAL_DMA_PollForTransfer(&handle_GPDMA1_Channel0, HAL_DMA_FULL_TRANSFER, 2);



// ensure DMA transfer is complete

while (status != HAL_OK)

{

status = HAL_DMA_PollForTransfer(&handle_GPDMA1_Channel0, HAL_DMA_FULL_TRANSFER, 2);

}

// AL_StatusTypeDef HAL_SPI_Abort_IT(SPI_HandleTypeDef *hspi)



status = HAL_DMA_Abort_IT(&handle_GPDMA1_Channel0); // stop DMA so that next use of SPI is valid and does not return HAL_BUSY



#elif (defined _ILI9341_IRQ)

// if IRQ is not generated, then this method does not work and hangs waiting for first

// interrupt.

// taskEXIT_CRITICAL();

RTOS_ILI9341[interface].tag = false;

// result = HAL_SPI_Transmit_IT(SPI_interface, (unsigned char*)&line_buffer[data.x0],(dx+1)); // 2 bytes/pixel

result = HAL_SPI_Transmit_IT(SPI_interface, (unsigned char*)&QVGA_image.Lpixel[y][data.x0],(dx)); // 2 bytes/pixel

while (RTOS_ILI9341[interface].tag == false);

#else





// failsafe blocking transfer.

// HAL_SPI_Transmit(SPI_interface, (unsigned char*)&line_buffer[data.x0],2*(dx+1), 2); // 2 bytes/pixel

// for (int jj = 0; jj < 2*(dx+1); jj+=10)

// for (int jj = 0; jj < 2*(dx+1); jj+=10)

// {

// HAL_SPI_Transmit(SPI_interface, (unsigned char*)&line_buffer[data.x0 + jj],10,2);

// HAL_SPI_Transmit(SPI_interface, (unsigned char*)&line_buffer[xx],1,2);

// vTaskDelay(10);

// }

// if translate is true, then RGB565 is called, and QVGA_image is the source

// if translate is false, then RGB565 is not called, and QVGA_image is the source

HAL_SPI_Transmit(SPI_interface, (unsigned char*)&QVGA_image.Lpixel[y][data.x0],2*(dx+1), 10); // 2 bytes/pixel

#endif

// }

#else

// status = ILI9341_spi->Send(nullptr, 0

// ,(uint8_t*) &line_buffer[data.x0],2*(dx+1), CS_INACTIVE);

#endif

} // end loop

break;

 

 

Since this is writing to a display, and the display address set is the whole width, line by line, I can see what happens on the display.

Programmed I/O works.  

The first DMA transaction works, Poll for transfer returns HAL_OK. 

Looping back to the first programmed transfer returns HAL_ERROR.  Following the code through indicates that the State of the SPI interface is HAL_BUSY. 

I’m missing something here, I think.

 

5 replies

ST Employee
July 6, 2026

Hi Harvey,

First DMA transfer OK, then SPI stuck at HAL_BUSY with no interrupt/callback usually means the GPDMA channel completion IRQ never reaches the HAL, so the SPI state never returns to READY and the next call fails. A few things to check:

1.Is the channel IRQ enabled and routed? 

HAL_NVIC_EnableIRQ(GPDMA1_Channel0_IRQn); in your MSP init
In stm32u5xx_it.c: GPDMA1_Channel0_IRQHandler() calling HAL_DMA_IRQHandler(&handle_GPDMA1_Channel0);
To test it, put a breakpoint in the channel IRQ handler and in HAL_SPI_TxCpltCallback(). If neither hits, that's it.

 

2. Is DMA linked to the TX handle? __HAL_LINKDMA(&hspi1, hdmatx, handle_GPDMA1_Channel0);

3. Channel params for memory→peripheral? Direction = DMA_MEMORY_TO_PERIPH, Request = GPDMA1_REQUEST_SPI1_TX, memory increment on / peripheral increment off, data widths matching your frame size.

 

Could you share your MX_GPDMA1_Init/channel config, the relevant stm32u5xx_it.c, and how you're polling? 

 

Kind regards, 

DHIF Khaled

"Please mark my answer as best by clicking on the “Accept as solution"" button if it fully answered your question. This will help other users find this solution faster.​"
Harvey White
Senior III
July 7, 2026

Thank you for your response.  Point by point:

1.Is the channel IRQ enabled and routed? 

HAL_NVIC_EnableIRQ(GPDMA1_Channel0_IRQn); in your MSP init
In stm32u5xx_it.c: GPDMA1_Channel0_IRQHandler() calling HAL_DMA_IRQHandler(&handle_GPDMA1_Channel0);
To test it, put a breakpoint in the channel IRQ handler and in HAL_SPI_TxCpltCallback(). If neither hits, that's it.

 

Neither hits.

2. Is DMA linked to the TX handle? __HAL_LINKDMA(&hspi1, hdmatx, handle_GPDMA1_Channel0);

 

.Looks as if it were an LL routine, I use higher level MX routines.  I think it is properly linked.

3. Channel params for memory→peripheral? Direction = DMA_MEMORY_TO_PERIPH, Request = GPDMA1_REQUEST_SPI1_TX, memory increment on / peripheral increment off, data widths matching your frame size.

These look appropriate.

It looks as if the interrupts do not fire.

I’m attaching the IOC file, and the bits and pieces of the initialization code

/**

* @brief GPDMA1 Initialization Function

* @param None

* @retval None

*/

static void MX_GPDMA1_Init(void)

{



/* USER CODE BEGIN GPDMA1_Init 0 */



/* USER CODE END GPDMA1_Init 0 */



/* Peripheral clock enable */

__HAL_RCC_GPDMA1_CLK_ENABLE();



/* GPDMA1 interrupt Init */

HAL_NVIC_SetPriority(GPDMA1_Channel0_IRQn, 5, 0);

HAL_NVIC_EnableIRQ(GPDMA1_Channel0_IRQn);



/* USER CODE BEGIN GPDMA1_Init 1 */



/* USER CODE END GPDMA1_Init 1 */

/* USER CODE BEGIN GPDMA1_Init 2 */



/* USER CODE END GPDMA1_Init 2 */





/**

* @brief NVIC Configuration.

* @retval None

*/

static void MX_NVIC_Init(void)

{

/* SPI1_IRQn interrupt configuration */

HAL_NVIC_SetPriority(SPI1_IRQn, 5, 0);

HAL_NVIC_EnableIRQ(SPI1_IRQn);





/**

* @brief SPI1 Initialization Function

* @param None

* @retval None

*/

static void MX_SPI1_Init(void)

{



/* USER CODE BEGIN SPI1_Init 0 */



/* USER CODE END SPI1_Init 0 */



SPI_AutonomousModeConfTypeDef HAL_SPI_AutonomousMode_Cfg_Struct = {0};



/* USER CODE BEGIN SPI1_Init 1 */



/* USER CODE END SPI1_Init 1 */

/* SPI1 parameter configuration*/

hspi1.Instance = SPI1;

hspi1.Init.Mode = SPI_MODE_MASTER;

hspi1.Init.Direction = SPI_DIRECTION_2LINES_TXONLY;

hspi1.Init.DataSize = SPI_DATASIZE_8BIT;

hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;

hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;

hspi1.Init.NSS = SPI_NSS_SOFT;

hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;

hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;

hspi1.Init.TIMode = SPI_TIMODE_DISABLE;

hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;

hspi1.Init.CRCPolynomial = 0x7;

hspi1.Init.NSSPMode = SPI_NSS_PULSE_ENABLE;

hspi1.Init.NSSPolarity = SPI_NSS_POLARITY_LOW;

hspi1.Init.FifoThreshold = SPI_FIFO_THRESHOLD_01DATA;

hspi1.Init.MasterSSIdleness = SPI_MASTER_SS_IDLENESS_00CYCLE;

hspi1.Init.MasterInterDataIdleness = SPI_MASTER_INTERDATA_IDLENESS_00CYCLE;

hspi1.Init.MasterReceiverAutoSusp = SPI_MASTER_RX_AUTOSUSP_DISABLE;

hspi1.Init.MasterKeepIOState = SPI_MASTER_KEEP_IO_STATE_ENABLE;

hspi1.Init.IOSwap = SPI_IO_SWAP_DISABLE;

hspi1.Init.ReadyMasterManagement = SPI_RDY_MASTER_MANAGEMENT_INTERNALLY;

hspi1.Init.ReadyPolarity = SPI_RDY_POLARITY_HIGH;

if (HAL_SPI_Init(&hspi1) != HAL_OK)

{

Error_Handler();

}

HAL_SPI_AutonomousMode_Cfg_Struct.TriggerState = SPI_AUTO_MODE_DISABLE;

HAL_SPI_AutonomousMode_Cfg_Struct.TriggerSelection = SPI_GRP1_GPDMA_CH0_TCF_TRG;

HAL_SPI_AutonomousMode_Cfg_Struct.TriggerPolarity = SPI_TRIG_POLARITY_RISING;

if (HAL_SPIEx_SetConfigAutonomousMode(&hspi1, &HAL_SPI_AutonomousMode_Cfg_Struct) != HAL_OK)

{

Error_Handler();

}

/* USER CODE BEGIN SPI1_Init 2 */



/* USER CODE END SPI1_Init 2 */

 

 

 

It looks to me as if the interrupts are not firing.

I get the same action on anything with an interrupt, except that the timers fire interrupts.

SPI, DMA, and DMA2D, no….

Thanks for the help.

 

ST Employee
July 7, 2026

Thanks ​@Harvey White ,

The timers working while SPI, DMA and DMA2D stay quiet feels like the useful clue, when a whole group of interrupts is silent even with the NVIC enable in place, it usually points to something above the individual peripherals rather than a per-peripheral mistake.

A couple of things that might be worth a look:

  • One is whether the interrupts are being masked. It could be worth checking if BASEPRI or PRIMASK is non-zero around the point you'd expect the IRQ. A lingering critical section could hold off your priority-5 interrupts while a higher-priority timer still slips through. I noticed some RTOS bits (`RTOS_ILI9341`, `taskENTER_CRITICAL`), are you running an RTOS? If so, running the transfer before the scheduler starts might tell you a lot.
  • The other, since it's a U5, is TrustZone. If it's enabled, it might be worth confirming SPI1/GPDMA1/DMA2D aren't landing on the secure side (GTZC / NVIC `ITNS`) while your code runs non-secure the ISR would vector to the other state and never reach you. Timers being non-secure could explain the difference.
  • One more thing: `HAL_SPI_Transmit_DMA()` and `HAL_DMA_PollForTransfer()` may be working against each other, since the poll clears the TC flag itself, that could be why the callback never runs and `hspi1.State` doesn't return to `READY`, which lines up with the `HAL_BUSY` you're seeing.

Would you be able to say whether you're running an RTOS and whether TrustZone is enabled? That'd help narrow it down.

Kind regards,
DHIF Khaled

"Please mark my answer as best by clicking on the “Accept as solution"" button if it fully answered your question. This will help other users find this solution faster.​"
Harvey White
Senior III
July 7, 2026

OK, did some more debugging:  Using QSPI memory, 64 MBit maps as 2M * 32 bit.  Not used.

I am using FreeRTOS, latest version.  

A.Trust zone is not enabled.  Data appears on the SPI1 register, not the SPI1_SEC.  In fact, having “all registers” open, and having the SPI1_sec tab expanded gives an internal error.  You may want to fix that.

B. This ought to help item #2.

For Item #1

  1. PRIMASK is set to 0 through the entire experience.
  2. BASEPRI is set to 0x80 by FreeRTOS.  That does not change during the routine
  3. taskENTER_CRITICAL and taskEXIT_CRITICAL are not used, The task exit was put in to make sure that interrupts were enabled.  That didn’t work.
  4. The RTOS_ILI9341 is a bit complicated, but works out.
    1. I want to be able to go to another operating system without having to rewrite all of the code.
    2. FreeRTOS calls are done through a call to a routine dealing with a data structure (I’ll attach code a little later).
    3. The structure supports (this is a full device set, not all need to be used)
      1. 3 semaphores, busy, done, and dma_done
      2. 2  queues, send_queue, receive queue
      3. 2 tasks, send task and receive task.
      4. tag (boolean flag)
    4. SEMAPHORE_GET needs a pointer to the semaphore structure and returns set, not set, or an error code.
    5. It has yet to be expanded into EmDOS.  Due to severe limitations in AZURE (my opinion), it won’t go there.
  5. In this case, the tag is simply a binary variable.  It could be replaced with a FreeRTOS semaphore or mutex.
  6.  

The following section is the complete polling code

#if (defined _ILI9341_DMA)

 

RTOS_ILI9341[interface].tag = false;

result = HAL_SPI_Transmit_DMA(SPI_interface, (unsigned char*)&QVGA_image.Lpixel[y][data.x0],(dx+1));

while (RTOS_ILI9341[interface].tag == false);

 

and to repeat the callback:

/* USER CODE BEGIN 1 */

void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)

// void DMA_XferCpltCallback(DMA_HandleTypeDef &handle_GPDMA1_Channel0)

{

#ifdef _SPI1

// if (_ILI9341 == 1) _SEMAPHORE_RELEASE(RTOS_ILI9341[1].dma_done);

if (_ILI9341 == 1) RTOS_ILI9341[_ILI9341].tag = true;

#endif

 

#ifdef _SPI2

// if (_ILI9341 == 2) _SEMAPHORE_RELEASE(RTOS_ILI9341[2].dma_done);

// if (_ILI9341 == 2) RTOS_ILI9341[_ILI9341].tag = true;

#endif

 

#ifdef _SPI3

// if (_ILI9341 == 3) _SEMAPHORE_RELEASE(RTOS_ILI9341[3].dma_done);

#endif

 

#ifdef _SPI4

// if (_ILI9341 == 4) _SEMAPHORE_RELEASE(RTOS_ILI9341[4].dma_done);

#endif

}

 

 

/* USER CODE END 1 */

 

So not using poll for transfer (which didn’t work anyway)

Thanks….

 

Harvey White
Senior III
July 20, 2026

I’ve possibly found the cause.  It’s related to GNU tools 14.3.rel1.  I’m going to file a bug report on it.  It’s solved by going back to 13.3.rel1.  

Details there.  PS: FreeRTOS forum was of no help.