2026-05-03 11:37 PM
Hi all !
I'm trying to write a driver for a LevelX system to talk to a QSPI chip over the OCTOSPI1 peripheral (STM32H563). I got the chip to respond to most commands, set it up in reading / protected mode.
But, later, I've spotted an issue : I cannot sent over DMA two buffer, one after the other (as I did, two call to the Transmit_DMA function). Or, the LevelX API provide me two buffer, that may not be contigous. That's the issue, how to do ?
I reminded that this chip has Linked List functionnality, which does match what I want (two DMA transfer from / to two different memory space, but, within one frame (Only one NCS being low).
And, I cannot find a way to make them work...
From a general perspective, the code is build like that :
if (xfer_size > GD25_DMA_THRESHOLD) {
/*
* Clean the semaphore, to ensure we restart from 0.
*/
while (tx_semaphore_get(&flash_dma_done, TX_NO_WAIT) == TX_SUCCESS)
;
/*
* Ensure that all the data is into the RAM.
*/
if (main_buffer)
HAL_DCACHE_CleanByAddr(&hdcache1, (uint32_t *)main_buffer, main_size);
if (spare_buffer)
HAL_DCACHE_CleanByAddr(&hdcache1, (uint32_t *)spare_buffer, spare_size);
/*
* Build the linked list for the DMA
*/
if (STM32H563_prepare_dma_xfer(main_buffer, main_size, spare_buffer, spare_size, true) != LX_SUCCESS)
return LX_ERROR;
/*
* Start the transfer
*/
if (HAL_DMAEx_List_Start(&handle_GPDMA1_octospiTX) != HAL_OK)
return LX_ERROR;
if (HAL_XSPI_Command(&hospi1, &cmd, HAL_MAX_DELAY) != HAL_OK)
return LX_ERROR;
if (tx_semaphore_get(&flash_dma_done, TX_WAIT_FOREVER) != TX_SUCCESS)
return LX_ERROR;
} else {
/*
* Build the buffer
*/
uint8_t buf[GD25_DMA_THRESHOLD + 2] = {0};
if (main_buffer) {
memcpy(buf, main_buffer, main_size);
}
if (spare_buffer) {
memcpy(buf + main_size, spare_buffer, spare_size);
}
/*
* Transfer from the local buffer
*/
if (HAL_XSPI_Command(&hospi1, &cmd, HAL_MAX_DELAY) != HAL_OK)
return LX_ERROR;
HAL_XSPI_Transmit(&hospi1, buf, HAL_MAX_DELAY);
} if (xfer_size > GD25_DMA_THRESHOLD) {
/*
* Clean the semaphore, to ensure we restart from 0.
*/
while (tx_semaphore_get(&flash_dma_done, TX_NO_WAIT) == TX_SUCCESS)
;
/*
* Ensure that all the data is into the RAM.
*/
if (main_buffer)
HAL_DCACHE_CleanByAddr(&hdcache1, (uint32_t *)main_buffer, main_size);
if (spare_buffer)
HAL_DCACHE_CleanByAddr(&hdcache1, (uint32_t *)spare_buffer, spare_size);
/*
* Build the linked list for the DMA
*/
if (STM32H563_prepare_dma_xfer(main_buffer, main_size, spare_buffer, spare_size, true) != LX_SUCCESS)
return LX_ERROR;
/*
* Start the transfer
*/
if (HAL_DMAEx_List_Start(&handle_GPDMA1_octospiTX) != HAL_OK)
return LX_ERROR;
if (HAL_XSPI_Command(&hospi1, &cmd, HAL_MAX_DELAY) != HAL_OK)
return LX_ERROR;
if (tx_semaphore_get(&flash_dma_done, TX_WAIT_FOREVER) != TX_SUCCESS)
return LX_ERROR;
} else {
/*
* Build the buffer
*/
uint8_t buf[GD25_DMA_THRESHOLD + 2] = {0};
if (main_buffer) {
memcpy(buf, main_buffer, main_size);
}
if (spare_buffer) {
memcpy(buf + main_size, spare_buffer, spare_size);
}
/*
* Transfer from the local buffer
*/
if (HAL_XSPI_Command(&hospi1, &cmd, HAL_MAX_DELAY) != HAL_OK)
return LX_ERROR;
HAL_XSPI_Transmit(&hospi1, buf, HAL_MAX_DELAY);
}(I've stripped the working part, for clarity). Before there's just configuration of the cmd struct ( XSPI_RegularCmdTypeDef ). This code as a variant for the writes, which does the same (and... is in the same issue). Not showing it to let the code short, there's a github at the bottom for full code.
And the second usefull code section : (This function is called from the read and write procedure, hence the isTx flag that is passed).
UINT STM32H563_prepare_dma_xfer(UCHAR *main_buffer, ULONG main_size, UCHAR *spare_buffer, ULONG spare_size, bool isTx) {
/*
* Clearing the list
*/
if (HAL_DMAEx_List_ResetQ(&dma_xfer) != HAL_OK)
return LX_ERROR;
/*
* First, configure the common node to be passed to the GPDMA :
*/
DMA_NodeConfTypeDef node_config = {0};
// General settings about the DMA.
node_config.NodeType = DMA_GPDMA_LINEAR_NODE;
node_config.Init.Request = GPDMA1_REQUEST_OCTOSPI1;
node_config.Init.Direction = (isTx) ? DMA_MEMORY_TO_PERIPH : DMA_PERIPH_TO_MEMORY;
node_config.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_BYTE;
node_config.Init.DestDataWidth = DMA_DEST_DATAWIDTH_BYTE;
if (isTx) {
node_config.Init.SrcBurstLength = 64;
node_config.Init.DestBurstLength = 1;
} else {
node_config.Init.SrcBurstLength = 1;
node_config.Init.DestBurstLength = 64;
}
node_config.Init.Mode = DMA_NORMAL;
if (isTx) {
node_config.Init.SrcInc = DMA_SINC_INCREMENTED;
node_config.Init.DestInc = DMA_DINC_FIXED;
} else {
node_config.Init.SrcInc = DMA_SINC_FIXED;
node_config.Init.DestInc = DMA_DINC_INCREMENTED;
}
// As the datasheet recommand !
if (isTx) {
node_config.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT1 | DMA_DEST_ALLOCATED_PORT0;
} else {
node_config.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0 | DMA_DEST_ALLOCATED_PORT1;
}
/*
* Build the first node. For the main buffer.
*/
if (main_buffer) {
node_config.DataSize = main_size;
if (spare_buffer) {
node_config.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
} else {
node_config.Init.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
}
if (isTx) {
node_config.SrcAddress = (uint32_t)main_buffer;
node_config.DstAddress = (uint32_t)&OCTOSPI1->DR;
} else {
node_config.SrcAddress = (uint32_t)&OCTOSPI1->DR;
node_config.DstAddress = (uint32_t)main_buffer;
}
if (HAL_DMAEx_List_BuildNode(&node_config, &master_xfer) != HAL_OK)
return LX_ERROR;
if ((main_buffer) && (main_size > 0)) {
if (HAL_DMAEx_List_InsertNode_Tail(&dma_xfer, &master_xfer) != HAL_OK)
return LX_ERROR;
}
/*
* Push the variable into RAM.
*/
HAL_DCACHE_CleanByAddr(&hdcache1, (uint32_t *)&master_xfer, sizeof(DMA_NodeTypeDef));
}
/*
* Build the second node. For the spare buffer.
*/
if (spare_buffer) {
node_config.DataSize = spare_size;
node_config.Init.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
if (isTx) {
node_config.SrcAddress = (uint32_t)spare_buffer;
node_config.DstAddress = (uint32_t)&OCTOSPI1->DR;
} else {
node_config.SrcAddress = (uint32_t)&OCTOSPI1->DR;
node_config.DstAddress = (uint32_t)spare_buffer;
}
if (HAL_DMAEx_List_BuildNode(&node_config, &slave_xfer) != HAL_OK)
return LX_ERROR;
if ((spare_buffer) && (spare_size > 0)) {
if (HAL_DMAEx_List_InsertNode_Tail(&dma_xfer, &slave_xfer) != HAL_OK)
return LX_ERROR;
}
/*
* Push the variable into the RAM.
*/
HAL_DCACHE_CleanByAddr(&hdcache1, (uint32_t *)&slave_xfer, sizeof(DMA_NodeTypeDef));
}
/*
* Push the whole transfer into the RAM.
*/
HAL_DCACHE_CleanByAddr(&hdcache1, (uint32_t *)&dma_xfer, sizeof(DMA_QListTypeDef));
if (isTx) {
if (HAL_DMAEx_List_LinkQ(&handle_GPDMA1_octospiTX, &dma_xfer) != HAL_OK)
return LX_ERROR;
} else {
if (HAL_DMAEx_List_LinkQ(&handle_GPDMA1_octospiRX, &dma_xfer) != HAL_OK)
return LX_ERROR;
}
return LX_SUCCESS;
}UINT STM32H563_prepare_dma_xfer(UCHAR *main_buffer, ULONG main_size, UCHAR *spare_buffer, ULONG spare_size, bool isTx) {
/*
* Clearing the list
*/
if (HAL_DMAEx_List_ResetQ(&dma_xfer) != HAL_OK)
return LX_ERROR;
/*
* First, configure the common node to be passed to the GPDMA :
*/
DMA_NodeConfTypeDef node_config = {0};
// General settings about the DMA.
node_config.NodeType = DMA_GPDMA_LINEAR_NODE;
node_config.Init.Request = GPDMA1_REQUEST_OCTOSPI1;
node_config.Init.Direction = (isTx) ? DMA_MEMORY_TO_PERIPH : DMA_PERIPH_TO_MEMORY;
node_config.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_BYTE;
node_config.Init.DestDataWidth = DMA_DEST_DATAWIDTH_BYTE;
if (isTx) {
node_config.Init.SrcBurstLength = 64;
node_config.Init.DestBurstLength = 1;
} else {
node_config.Init.SrcBurstLength = 1;
node_config.Init.DestBurstLength = 64;
}
node_config.Init.Mode = DMA_NORMAL;
if (isTx) {
node_config.Init.SrcInc = DMA_SINC_INCREMENTED;
node_config.Init.DestInc = DMA_DINC_FIXED;
} else {
node_config.Init.SrcInc = DMA_SINC_FIXED;
node_config.Init.DestInc = DMA_DINC_INCREMENTED;
}
// As the datasheet recommand !
if (isTx) {
node_config.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT1 | DMA_DEST_ALLOCATED_PORT0;
} else {
node_config.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0 | DMA_DEST_ALLOCATED_PORT1;
}
/*
* Build the first node. For the main buffer.
*/
if (main_buffer) {
node_config.DataSize = main_size;
if (spare_buffer) {
node_config.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
} else {
node_config.Init.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
}
if (isTx) {
node_config.SrcAddress = (uint32_t)main_buffer;
node_config.DstAddress = (uint32_t)&OCTOSPI1->DR;
} else {
node_config.SrcAddress = (uint32_t)&OCTOSPI1->DR;
node_config.DstAddress = (uint32_t)main_buffer;
}
if (HAL_DMAEx_List_BuildNode(&node_config, &master_xfer) != HAL_OK)
return LX_ERROR;
if ((main_buffer) && (main_size > 0)) {
if (HAL_DMAEx_List_InsertNode_Tail(&dma_xfer, &master_xfer) != HAL_OK)
return LX_ERROR;
}
/*
* Push the variable into RAM.
*/
HAL_DCACHE_CleanByAddr(&hdcache1, (uint32_t *)&master_xfer, sizeof(DMA_NodeTypeDef));
}
/*
* Build the second node. For the spare buffer.
*/
if (spare_buffer) {
node_config.DataSize = spare_size;
node_config.Init.TransferEventMode = DMA_TCEM_LAST_LL_ITEM_TRANSFER;
if (isTx) {
node_config.SrcAddress = (uint32_t)spare_buffer;
node_config.DstAddress = (uint32_t)&OCTOSPI1->DR;
} else {
node_config.SrcAddress = (uint32_t)&OCTOSPI1->DR;
node_config.DstAddress = (uint32_t)spare_buffer;
}
if (HAL_DMAEx_List_BuildNode(&node_config, &slave_xfer) != HAL_OK)
return LX_ERROR;
if ((spare_buffer) && (spare_size > 0)) {
if (HAL_DMAEx_List_InsertNode_Tail(&dma_xfer, &slave_xfer) != HAL_OK)
return LX_ERROR;
}
/*
* Push the variable into the RAM.
*/
HAL_DCACHE_CleanByAddr(&hdcache1, (uint32_t *)&slave_xfer, sizeof(DMA_NodeTypeDef));
}
/*
* Push the whole transfer into the RAM.
*/
HAL_DCACHE_CleanByAddr(&hdcache1, (uint32_t *)&dma_xfer, sizeof(DMA_QListTypeDef));
if (isTx) {
if (HAL_DMAEx_List_LinkQ(&handle_GPDMA1_octospiTX, &dma_xfer) != HAL_OK)
return LX_ERROR;
} else {
if (HAL_DMAEx_List_LinkQ(&handle_GPDMA1_octospiRX, &dma_xfer) != HAL_OK)
return LX_ERROR;
}
return LX_SUCCESS;
}What I'm getting for now is that screen (large timebase, show all from reset). In theses IO, flash is working as QSPI, so, exact values decoded by the scope are irrelevant).
[ image linked in attachements ]
1st block : configuration commands.
2nd block : polling to wait until ready. (In that case, waiting for the page to be loaded).
There shall be a third block for the transfer, which... is not happening. And, I don't know why.
Examples from ST are using the same function call, shall be working for them, and, not mine.
To be noted : I must send the command bytes somewhere, so, I must call the HAL_XSPI_Command(...) somewhere. But, how to fire the linked list GPDMA engine ? And, sync the OCTOSPI peripheral ? That's the question.
Thanks for any help ! I'm struggling on that point for the last 10 days, starting to be long...
---
Here the link for the full code access : https://github.com/lheywang/EtherBench/tree/embedded