OK, I found a solution that works for me. See listing below. All flags but <transfer complete> and <transfer error> don't need to be enabled and should be ignored. Starting point would be void <spi_output_DMA (....)>.
Noteworthy the FIFO error flag is always active although not enabled. While debugging this code I found that clearing this flag leads to stopping the DMA engine while more bytes need to be transferred.
Also, this functional code does not adhere to the recommendations found in the reference manual regarding DMA and SPI.
Thank you all for contributions.
Example of a 5-byte DMA transfer, trace 1 is chip select, trace 2 is SPI clock.

/***********************************************************
************************************************************
** MODULE spi_dma.c **
** PROJECT **
** COMPILER **
** LANGUAGE C **
** DATE 21.10.2025 **
** AUTHOR lucylectric **
** ABSTRACT **
** 21.10.2025 / Creation **
************************************************************
***********************************************************/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "main.h"
#include "stm32f4xx_ll_spi.h"
#include "stm32f4xx_ll_dma.h"
#include "stm32f4xx_ll_gpio.h"
#include "spi_dma.h"
/***********************************************/
#define W5500_CS_LOW LL_GPIO_ResetOutputPin (W5500_CS_GPIO_Port, W5500_CS_Pin);
#define W5500_CS_HIGH LL_GPIO_SetOutputPin (W5500_CS_GPIO_Port, W5500_CS_Pin);
#define W5500_RESET_LOW LL_GPIO_ResetOutputPin (W5500_RESET_GPIO_Port, W5500_RESET_Pin);
#define W5500_RESET_HIGH LL_GPIO_SetOutputPin (W5500_RESET_GPIO_Port, W5500_RESET_Pin);
static volatile uint16_t dma_SPI_write_complete;
static volatile uint16_t dma_SPI_read_complete;
static uint8_t spi_databuff[200] =
{
0x55, 0xAA, 0x81, 0x42, 0x55, 0xAA, 0x81, 0x42,
0x55, 0xAA, 0x81, 0x42, 0x55, 0xAA, 0x81, 0x42,
0x55, 0xAA, 0x81, 0x42, 0x55, 0xAA, 0x81, 0x42,
0x55, 0xAA, 0x81, 0x42, 0x55, 0xAA, 0x81, 0x42
};
/* FUNCTION ************************************/
void SPI1_Init (void)
/* prepare SPI1 & DMA flags
***********************************************/
{
W5500_CS_HIGH;
//LL_SPI_Disable (SPI1); // not required
//LL_DMA_EnableIT_HT (DMA2, LL_DMA_STREAM_3); // half transfer flag
LL_DMA_EnableIT_TE (DMA2, LL_DMA_STREAM_3); // transfer error flag
LL_DMA_EnableIT_TC (DMA2, LL_DMA_STREAM_3); // transfer complete flag
//LL_DMA_EnableIT_DME (DMA2, LL_DMA_STREAM_3); // direct mode error flag
// don't enable FIFO error flag
//LL_DMA_EnableIT_FE (DMA2, LL_DMA_STREAM_3); // FIFO error flag
} /* --- SPI1_Init --- */
/* FUNCTION ************************************/
void DMA2_Stream0_IRQHandler_cont (void)
/* DMA SPI RX - Receive
***********************************************/
{
// LL_DMA_ClearFlag_TC0 transfer complete flag
// LL_DMA_ClearFlag_HT0 half transfer flag
// LL_DMA_ClearFlag_TE0 transfer error flag
// LL_DMA_ClearFlag_DME0 direct mode error flag
// LL_DMA_ClearFlag_FE0 FIFO error flag
if (LL_DMA_IsActiveFlag_TC3 (DMA2))
{
LL_DMA_ClearFlag_TC3 (DMA2); // Transfer complete
}
if (LL_DMA_IsActiveFlag_TE3 (DMA2))
{
LL_DMA_ClearFlag_TE3 (DMA2); // transfer error flag
}
dma_SPI_read_complete = 1; // set ready flag
} /* --- DMA2_Stream0_IRQHandler_cont --- */
/* FUNCTION ************************************/
void DMA2_Stream3_IRQHandler_cont (void)
/* DMA SPI TX - Transmit
***********************************************/
{
// LL_DMA_ClearFlag_TC3 transfer complete flag
// LL_DMA_ClearFlag_HT3 half transfer flag
// LL_DMA_ClearFlag_TE3 transfer error flag
// LL_DMA_ClearFlag_DME3 direct mode error flag
// LL_DMA_ClearFlag_FE3 FIFO error flag
if (LL_DMA_IsActiveFlag_TC3 (DMA2))
{
LL_DMA_ClearFlag_TC3 (DMA2); // Transfer complete
}
if (LL_DMA_IsActiveFlag_TE3 (DMA2))
{
LL_DMA_ClearFlag_TE3 (DMA2); // transfer error flag
}
#if 0 // do not handle these flags & don't clear them
if (LL_DMA_IsActiveFlag_HT3 (DMA2))
{
LL_DMA_ClearFlag_HT3 (DMA2); // half transfer flag
}
if (LL_DMA_IsActiveFlag_DME3 (DMA2))
{
LL_DMA_ClearFlag_DME3 (DMA2); // direct mode error flag
}
if (LL_DMA_IsActiveFlag_FE3 (DMA2))
{
LL_DMA_ClearFlag_FE3 (DMA2); // FIFO error flag
}
#endif
dma_SPI_write_complete = 1; // set ready flag
} /* --- DMA2_Stream3_IRQHandler_cont --- */
/* FUNCTION ************************************/
void SPI1_Write_DMA (uint8_t *buff, uint16_t count)
/*
* Write a complete buffer to SPI with DMA
***********************************************/
{
uint32_t reg_addr;
LL_SPI_Enable (SPI1); // enable SPI engine
// everything else is done in the CubeMX init
// set source and dest address
reg_addr = LL_SPI_DMA_GetRegAddr(SPI1);
LL_DMA_ConfigAddresses (DMA2, LL_DMA_STREAM_3, (uint32_t)buff,
reg_addr, LL_DMA_DIRECTION_MEMORY_TO_PERIPH);
LL_DMA_SetDataLength (DMA2, LL_DMA_STREAM_3, count);
LL_SPI_EnableDMAReq_TX (SPI1); // enable SPI for DMA operation
LL_DMA_EnableStream (DMA2, LL_DMA_STREAM_3); // start DMA mem to periph
} /* --- SPI1_Write_DMA --- */
/* FUNCTION ************************************/
void spi_output_DMA (uint16_t byte_count)
/* SPI data transfer
* - output buffer is static local (test version)
* - prepare DMA flags
* - handle chip select
* - start DMA engine
* - check & wait for complete flags (DMA & SPI)
***********************************************/
{
SPI1_Init ();
dma_SPI_write_complete = 0; // clear DMA finished flag
W5500_CS_LOW; // enable chip select
SPI1_Write_DMA (spi_databuff, byte_count); // start SPI & DMA
while (dma_SPI_write_complete == 0)
{
// wait for DMA finished
}
LL_SPI_DisableDMAReq_TX (SPI1);
while (LL_SPI_IsActiveFlag_BSY(SPI1) != 0)
{
// wait for SPI finished
}
W5500_CS_HIGH; // chip select off
} /* --- spi_output_DMA --- */
/* ----------------------------------------------*/
/* ----------- End of file spi_dma.c --------- */
/* ----------------------------------------------*/