Skip to main content
Associate II
August 7, 2026
Solved

Nucleo H563ZI SPI Comms with an ADC: Works with HAL Interrupt APIs, Not with DMA APIs

  • August 7, 2026
  • 9 replies
  • 155 views

Hi all - I have a problem getting DMA working with SPI, for communication with an MCP3564R ADC as one task in a complex project running on FreeRTOS. I have made a stripped-down version of the project that just does the ADC reading task. It runs on a bare Nucleo board and illustrates the basic problem which shows up from the first transmit, so a connected ADC isn’t needed to see the problem.

The code uses SPI3 at 20MHz (although the problem occurs at lower speeds as well)

The GPIO pins I’m using are not the defaults:

GPDMA1 is configured with two channels for SPI3 TX and RX:

My code can be configured to use the HAL SPI DMA APIs or the interrupt APIs like so:

static uint8_t write_register(const uint8_t * reg_write_cmd_p, uint8_t num_bytes)
{
HAL_StatusTypeDef hal_status;
BaseType_t wait_ret_val;
uint32_t notify_bits;
uint8_t ret_val = 0; /* Failure */

#if (MCP3564_HAL_SPI_MODE == MCP3564_USE_HAL_SPI_DMA_MODE)
hal_status = HAL_SPI_Transmit_DMA(&ADC_SPI_PERIPH_HANDLE, reg_write_cmd_p, num_bytes);
#elif (MCP3564_HAL_SPI_MODE == MCP3564_USE_HAL_SPI_INT_MODE)
hal_status = HAL_SPI_Transmit_IT(&ADC_SPI_PERIPH_HANDLE, reg_write_cmd_p, num_bytes);
#else
#error "unexpected MCP3564_HAL_SPI_MODE"
#endif
if (HAL_OK == hal_status)
{
wait_ret_val = xTaskNotifyWait(0x0, ADC_TASK_NOTIFY_TX_COMPLETE, &notify_bits,
pdMS_TO_TICKS(ADC_HAL_SPI_TIMEOUT_MSEC));
if ((pdTRUE == wait_ret_val) && (notify_bits & ADC_TASK_NOTIFY_TX_COMPLETE))
{
ret_val = 1;
}
}

return ret_val;
}

When I have it building for the interrupt APIs, the first transmission looks just like I expect (note that the pulse edges look more sawtooth than square because I’m getting close to the maximum time resolution of my Analog Discovery USB scope, but it still decodes OK):

When I have it building for the DMA APIs, I’m only seeing zero go out:

I had all this code (and much more) working quite nicely at one time but then the hardware design I’m targeting changed and I had to relocate some pins. DMA hasn’t worked right since despite trying to build an .ioc file back up starting with the basic Nucleo board.

I’ve grilled Google AI about this problem and it has had many suggestions but most of them turned out to be mistaken - for example, it insists that the problem is likely the data cache, although on the STM32H563 family parts my understanding is this could not be an issue due to a difference in the DMA architecture. In any case the cache is not enabled. I worked through many more suggestions from Google AI and they became increasingly far-fetched and incorrect and it stopped answering me.

If anyone has this Nucleo board and would like to look at the project, or step through the code, I have placed the project on GitHub here: https://github.com/paulrpotts/nucleo-h563zi-adc-MCP3564R

Without hardware connected like my hand-built prototype board with the ADC, of course the ADC will not answer and the whole ADC monitoring task will not run, but the failure happens right with the first call to write_register() in task_adc_MCP3564R_test.c line 317. The #define to build for DMA APIs or interrupt APIs is in adc_MCP3564R_config.h at line 100.

Any suggestions for how to get past this non-working DMA appreciated, even just getting one initial write working and seeing the expected data on the SPI bus would be progress.

Thanks,

Paul R. Potts

Best answer by paulrpotts

OK, after several answers by people who did not read my original post, or my followup posts or examine the code, and having solved it myself with help from the AI, I am adding a summary of what went wrong in the hopes that if someone else finds this via a search in the future, it will help them solve the problem faster.

  1. I made a project with STM32CubeMX using the template for the Nucleo H563ZI development board, without TrustZone activated. I unchecked “Generate demonstration code” and turned off all the options under “Human Machine Interface.” I set up SPI and DMA as in the project I put on GitHub, linked above. With my application code added, SPI TX and RX via DMA worked fine.
  2. Looking at the FLASH application region, and also RAM application region under Tools/Memory management, RAM defaults to “RW by privileged code only” and flash defaults to “RO by privileged code only.” However, by default, these policies are not enforced because the “apply application regions settings to peripherals” is off and the generated startup code does not limit access to SRAM by the DMA or set flash option bytes. So the DMA, which is “unprivileged,” will read and write to SRAM and read from flash just fine.
  3. Much later I wanted to change the memory map and flash configuration so I could write code to use the last page of user flash for saving calibration data. To do this I shrank the default FLASH application region to 2040KiB from 2048KiB, and set up an application region, “FLASH_APP_DATA_AREA,” one 8KiB flash block at 0x081FE000 (the last page of user flash). I set this flash region so it had access permissions “RW by any privilege level,” code execution “Not Permitted,” Shareability “Non-Shareable,”  and Cacheability “Non-Cacheable” (which I had learned was critical for letting my code write to flash without crashing). I turned on “Apply Application Regions Settings to Peripherals” and “Apply Application Regions Settings to Linker Files.”
  4. After this change, STM32CubeMX now generates MX_GTZC_Init() in main which sets all the privilege bits to 1. This now enforces the privilege protection of SRAM and my SPI reads using DMA cannot transfer data from the SPI bus to RAM (my read buffers remain zero). It also generates MX_FLASH_Init() which sets the volatile block-based protection bits for all regular flash (except the last page). So, my SPI writes using DMA cannot transfer data from my flash data structures to the peripheral (only zeroes are sent on MOSI).
  5. Note that even without adding a special application region for that last page in flash, just turning on the “Apply Application Regions Settings to Peripherals” box will result in the generation of MX_GTZC_Init() and MX_Flash_Init() which effectively now locks the DMA out of being able to access flash _or_ SRAM. This is the unexpected behavior that wound up costing me days of work trying to figure out what was suddenly going wrong and why. It was especially frustrating given that I had not changed the SPI or DMA settings in STM32CubeMX at all, but it appeared that SPI and/or DMA was now broken so that is where I was focusing my debugging efforts at first.
  6. This may not technically be a bug in STM32CubeMX - maybe it is intentional behavior - but it seems strange that the defaults for SRAM and flash are so restrictive that they break DMA access to either, and also that those defaults are not _applied_ by default. If the defaults had not been so restrictive, I would not have run into this problem. If the code had been generated to enforce those defaults, by default, I would have run into this problem when I first tried to use DMA and presumably solved it then. Instead it was sort of a “ticking time bomb” that didn’t cause me any issues until I turned on code generation for the application regions. I was trying to enable write access to one page of flash, and wound up accidentally making it so the DMA engines couldn’t access any flash or SRAM.
  7. So, I would request that ST Micro take a look at this behavior and consider changing it to be more programmer-friendly. There may be a point to locking out DMA access if you are working on some very critical locked-down code, but I don’t think that should be the default, especially since I didn’t even turn on the “TrustZone” features.

Thanks for reading and, like I said, I hope this helps keep someone else from wasting a lot of time trying to solve this like I did.

I would add, just as a matter of message board protocol, that I’m sorry if anyone is offended by this comment, but: I don’t think it is at all helpful to post replies if you haven’t even read the original post. I went to a lot of effort to explain the issue as clearly as I could, with screen shots, and made a demo project available on GitHub. I would hope that people trying to help would put in at least some small fraction of that effort.

 

9 replies

AScha.3
Super User
August 8, 2026

Hi,

i didnt look at all your post, but first things…

  • set in Cube , SPI → master keep io state : enable (to keep the lines active)
  • set pin speed low → medium (to allow 20Mbit signal at all to come out )
  • the DMA can be tricky/complex on H563 , so what you use: linked list or not ?
  • where you start DMA ?

with linked list DMA looks like : (for SAI here, you use ..SPI..  then )

  __HAL_LINKDMA(&hsai_BlockA2, hdmatx, handle_GPDMA1_Channel7);	//Associate the DMA handle /without -> hard fault !
MX_SAIQueue_Config();
HAL_DMAEx_List_SetCircularMode(&SAIQueue);
HAL_DMAEx_List_LinkQ(&handle_GPDMA1_Channel7, &SAIQueue); // Link SAI queue to DMA channel
fresult = HAL_SAI_Transmit_DMA(&hsai_BlockA2, (uint8_t *)playbuf , (sizeof(playbuf))/4);// SAI start

 

If you feel a post has answered your question, please click on " Best Answer ".
Associate II
August 8, 2026

Hi AScha.3,

Thanks for your reply.

The pin speed is a good idea - I had them set to medium in an earlier project but forgot to change it in this .ioc file. It doesn’t fix this problem but it should improve reliability.

The “Master Keep Io State” setting: I have never seen that setting mentioned in any examples or documentation. I tried setting it to “Master Keep Io State Enable” but that change didn’t make any difference.

My DMA settings are as shown: for channel 0 (RX):

For channel 1 (TX):

My peripheral initialization is not modified from the code generated by STM32CubeMX in spi.c. See:

https://github.com/paulrpotts/nucleo-h563zi-adc-MCP3564R/blob/trunk/Core/Src/spi.c

I’m not using a linked list or circular mode.

Re: your question as to where I start DMA: I am using the Cube MX-generated code in main.c and spi.c and HAL APIs HAL_SPI_Transmit_DMA() and HAL_SPI_TransmitReceive_DMA(). Is there some other API call I need to allow the DMA to run that is not in the generated code? I have looked at a number of examples and don’t remember seeing another API call used.
 

Associate II
August 8, 2026

It appears I have a clue. It looks like initializing the flash peripheral at startup, but never explicitly using it, is interfering with the correct functioning of the DMA.

My .ioc file defines special memory areas to allow my code to access the highest/last page of user flash either through the regular user flash addresses or through the high cycle data area addresses:

and

This results in the flash peripheral enabled:

And also the creation of MPU_Config() in main.c, and calling it at the very start of main(), although this specifically does not seem to be the cause.

If I turn off this setting:

Then I can turn off the Enable checkbox under FLASH.

Cube MX then no longer generates flash.c and flash.h and no longer calls MX_Flash_Init() from main(). The MPU_Config() is not changed and is still called at the start of main(). But without the call to MX_Flash_Init() at startup, the SPI DMA transmit works correctly.

So, it appears that initializing the flash peripheral is interfering with the correct functioning of the DMA, even though the code in this project is not actually using those flash pages. So I will have to figure out what the conflict is. My full application uses that flash page to store calibration data so I can’t just disable it completely.

It doesn’t seem to be caused by the MMU configuration, since that is still being generated in main.c and that did not change.

I’ll have to look at this more on Monday. If anyone wants to take a look, here is the branch with the working code:

https://github.com/paulrpotts/nucleo-h563zi-adc-MCP3564R/tree/disable-reserved-high-flash-page

To see a diff:

https://github.com/paulrpotts/nucleo-h563zi-adc-MCP3564R/compare/disable-reserved-high-flash-page?expand=1

Note that most of the diff is .mxproject and .ioc files due to the removal of flash.c and flash.h. The actual code change is no longer calling MX_Flash_Init().

Associate II
August 8, 2026

OK, with the assistance of the STM32 “Sidekick” AI, I have worked out that I needed to set the access permission for the Application Region “FLASH” to “RO by any privilege level.” If it isn’t set this way, the generated MX_FLASH_Init() sets the FLASH_BB_PRIV bits to 1 except for the separate Application Region I defined as the last page of flash. Then when the DMA attempts to read const data that the compiler put into flash, it gets a privilege violation and reads only zero bits.


Changing this results in the generated MX_FLASH_Init() function containing _only_ Flash_Unlock() and Flash_Lock(), which pointless, so I turned off generation of MX_FLASH_Init() under Project Manager/Advanced Settings, since it is not needed.

I will leave this here since this solution did not seem obvious - Google AI in particular never came up with any suggestions having to do with the access permissions coming from FLASH_BB_PRIV bits interfering with the DMA’s ability to read from flash, but not the CPU’s. Some kind of option on the DMA configuration pages, to enable reading from flash, might also have been useful.

Associate II
August 8, 2026

And there’s a similar issue in SRAM that is fixed in a similar way. All this came about behind the scenes because I wanted my code to be able to write to a single page of flash.

 

cartergray733
Explorer II
August 9, 2026

Since SPI works with interrupts, focus on the GPDMA configuration. Check the SPI3 TX request mapping, source/destination addresses, increment settings, and GPIO alternate functions. Sending only zeros strongly suggests the DMA isn’t reading the transmit buffer correctly.

ST Technical Moderator
August 10, 2026
In order to give better visibility on the answered topics, please click on 'Best answer' on the reply which solved your issue or answered your question. Saket_Om
paulrpottsAuthorBest answer
Associate II
August 10, 2026

OK, after several answers by people who did not read my original post, or my followup posts or examine the code, and having solved it myself with help from the AI, I am adding a summary of what went wrong in the hopes that if someone else finds this via a search in the future, it will help them solve the problem faster.

  1. I made a project with STM32CubeMX using the template for the Nucleo H563ZI development board, without TrustZone activated. I unchecked “Generate demonstration code” and turned off all the options under “Human Machine Interface.” I set up SPI and DMA as in the project I put on GitHub, linked above. With my application code added, SPI TX and RX via DMA worked fine.
  2. Looking at the FLASH application region, and also RAM application region under Tools/Memory management, RAM defaults to “RW by privileged code only” and flash defaults to “RO by privileged code only.” However, by default, these policies are not enforced because the “apply application regions settings to peripherals” is off and the generated startup code does not limit access to SRAM by the DMA or set flash option bytes. So the DMA, which is “unprivileged,” will read and write to SRAM and read from flash just fine.
  3. Much later I wanted to change the memory map and flash configuration so I could write code to use the last page of user flash for saving calibration data. To do this I shrank the default FLASH application region to 2040KiB from 2048KiB, and set up an application region, “FLASH_APP_DATA_AREA,” one 8KiB flash block at 0x081FE000 (the last page of user flash). I set this flash region so it had access permissions “RW by any privilege level,” code execution “Not Permitted,” Shareability “Non-Shareable,”  and Cacheability “Non-Cacheable” (which I had learned was critical for letting my code write to flash without crashing). I turned on “Apply Application Regions Settings to Peripherals” and “Apply Application Regions Settings to Linker Files.”
  4. After this change, STM32CubeMX now generates MX_GTZC_Init() in main which sets all the privilege bits to 1. This now enforces the privilege protection of SRAM and my SPI reads using DMA cannot transfer data from the SPI bus to RAM (my read buffers remain zero). It also generates MX_FLASH_Init() which sets the volatile block-based protection bits for all regular flash (except the last page). So, my SPI writes using DMA cannot transfer data from my flash data structures to the peripheral (only zeroes are sent on MOSI).
  5. Note that even without adding a special application region for that last page in flash, just turning on the “Apply Application Regions Settings to Peripherals” box will result in the generation of MX_GTZC_Init() and MX_Flash_Init() which effectively now locks the DMA out of being able to access flash _or_ SRAM. This is the unexpected behavior that wound up costing me days of work trying to figure out what was suddenly going wrong and why. It was especially frustrating given that I had not changed the SPI or DMA settings in STM32CubeMX at all, but it appeared that SPI and/or DMA was now broken so that is where I was focusing my debugging efforts at first.
  6. This may not technically be a bug in STM32CubeMX - maybe it is intentional behavior - but it seems strange that the defaults for SRAM and flash are so restrictive that they break DMA access to either, and also that those defaults are not _applied_ by default. If the defaults had not been so restrictive, I would not have run into this problem. If the code had been generated to enforce those defaults, by default, I would have run into this problem when I first tried to use DMA and presumably solved it then. Instead it was sort of a “ticking time bomb” that didn’t cause me any issues until I turned on code generation for the application regions. I was trying to enable write access to one page of flash, and wound up accidentally making it so the DMA engines couldn’t access any flash or SRAM.
  7. So, I would request that ST Micro take a look at this behavior and consider changing it to be more programmer-friendly. There may be a point to locking out DMA access if you are working on some very critical locked-down code, but I don’t think that should be the default, especially since I didn’t even turn on the “TrustZone” features.

Thanks for reading and, like I said, I hope this helps keep someone else from wasting a lot of time trying to solve this like I did.

I would add, just as a matter of message board protocol, that I’m sorry if anyone is offended by this comment, but: I don’t think it is at all helpful to post replies if you haven’t even read the original post. I went to a lot of effort to explain the issue as clearly as I could, with screen shots, and made a demo project available on GitHub. I would hope that people trying to help would put in at least some small fraction of that effort.

 

TDK
August 10, 2026

I would add, just as a matter of message board protocol, that I’m sorry if anyone is offended by this comment, but: I don’t think it is at all helpful to post replies if you haven’t even read the original post.

I think your opinion here is reasonable, but pointing to examples can be helpful. Some don’t know they exist. Comparing your code to the example would have exposed some of the issues here.

One of the replies was 99% likely a bot (firstnamelastname1234 plus profile picture). So no help expected there.

The other comment saying you should be using medium speed instead of low wasn’t the solution, but clearly it’s valid and could have been a problem and was worth putting out there.

 

The complexity in the H5 is off the charts and solving problems like these can often be a bit above what can be expected from a free forum.

Thanks for coming back with the solution.

"If you feel a post has answered your question, please click ""Accept as Solution""."