Portenta H7 ADC DMA first steps

Hello everyone.

I know, this topic has already been mentioned here, but no working solution was found so far.

Brief summary: The Portenta H7 has an advertized ADC sampling rate of 3.6 Msps. However, timing the analogRead() function results in a sobering 20 kHz rate, i.e. one ADC read takes about 50 us.

The solution is to include DMA in the ADC process. I found one site, which explains it well for the STM32 (even with a video on YouTube):

Two examples are provided. The first shows a single ADC reading, the second introduces DMA. As a start, I wanted to test the single ADC read, because the DMA example is based on it.

The code can be found on the site, however, the HAL code and the Init() code is missing. Therefore I opened CubeMX for the STM32H743 Nucleo (since the Portenta H7cannot be included into CubeMX) and assigned the ADC1 as shown in the digikey Video.

I compared the generated code with another ADC DMA Stm32 example:

and found that its nearly equal. Now the Arduino code:

#include "stm32h7xx_hal_gpio.h"
#include "stm32h7xx_hal.h"
#include "stm32h7xx_hal_adc.h"

int     RxPin           = 0;            
int     ADC_raw     = 0;
const int TxPin     = 0; 
uint16_t raw        = 0;

ADC_HandleTypeDef hadc1;

void setup() {
  // put your setup code here, to run once:

  MX_GPIO_H15_Init();       // Initialize GPIO-H for VLC
  MX_adc1_Init();                // Initialize ADC1

  
  
  Serial.begin(115200);
  //analogReadResolution(16);
  pinMode(RxPin, INPUT);
  pinMode(TxPin, OUTPUT);

  
}



void loop() {

    delay(500);

// Test: 
    HAL_GPIO_WritePin(GPIOH, GPIO_PIN_15, GPIO_PIN_SET);      // SET Pin PH15 HIGH
    delay(1);
    HAL_GPIO_WritePin(GPIOH, GPIO_PIN_15, GPIO_PIN_RESET);    // SET Pin PH15 LOW
    delay(1);
    HAL_GPIO_WritePin(GPIOH, GPIO_PIN_15, GPIO_PIN_SET);      // SET Pin PH15 HIGH
    
   
    HAL_ADC_Start(&hadc1);                                    // Get ADC value
    HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);         //wait until ADC conversion completes.... this is where the code hangs, see oscilloscope pic
    raw = HAL_ADC_GetValue(&hadc1);

    
    HAL_GPIO_WritePin(GPIOH, GPIO_PIN_15, GPIO_PIN_RESET); // SET Pin PH15 LOW

  Serial.print(raw);
  
delay(1);

}





 static void MX_adc1_Init(void)
{


  ADC_ChannelConfTypeDef sConfig = {0};


  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
  hadc1.Init.Resolution = ADC_RESOLUTION_16B;
  hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;       //also possible: ADC_SCAN_ENABLE; 
  hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
  hadc1.Init.LowPowerAutoWait = DISABLE;
  hadc1.Init.ContinuousConvMode = DISABLE;
  hadc1.Init.NbrOfConversion = 1;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DR;
  hadc1.Init.Overrun = ADC_OVR_DATA_PRESERVED;
  hadc1.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE;
  hadc1.Init.OversamplingMode = DISABLE;
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    Error_Handler();
  }
//  __HAL_RCC_ADC1_CLK_ENABLE();                    // necessary? -> found in https://www.st.com/resource/en/user_manual/um1905-description-of-stm32f7-hal-and-lowlayer-drivers-stmicroelectronics.pdf

  sConfig.Channel = ADC_CHANNEL_1;                  // 'Maybe this is the problem? Which channel should I choose for Pin A0 on the arduino portenta?'
  sConfig.Rank = ADC_REGULAR_RANK_1;
  sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
  sConfig.SingleDiff = ADC_SINGLE_ENDED;
  sConfig.OffsetNumber = ADC_OFFSET_NONE;
  sConfig.Offset = 0;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }


}



// -------------------------------- GPIO Pin "PH15" Initialization for STM32 ---------------------------------
void MX_GPIO_H15_Init()
{
  /* GPIO Ports Clock Enable */
 __HAL_RCC_GPIOH_CLK_ENABLE();

 GPIO_InitTypeDef GPIO_InitStruct; 

  /*Configure GPIO pin : PH15 */
 GPIO_InitStruct.Pin = GPIO_PIN_15;       // Pin15
 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // digital Output, push-pull configuration
 GPIO_InitStruct.Speed = GPIO_SPEED_HIGH;
 HAL_GPIO_Init(GPIOH, &GPIO_InitStruct);  // GPIO-H
}
// ----------------------------------------------------------------------------------------------------------

void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */

  /* USER CODE END Error_Handler_Debug */
}

What it does, is setting pin PH15 high, low, high and then start one AD read and wait until it is finished. Only then, PH15 is set low again. Unfortunately, HAL_ADC_PollForConversion() is where the code hangs and waits, as can be seen in the oscilloscope picture provided in the comments.

The reason might be that adc1_Init() was not adapted to the Portenta H7 and that some settings are incorrect, because it is not included in CubeMX.

Does anyone get along with the settings in adc1_Init()? If we could make this example work, the next step would be to include DMA....

Thank You!

Hi,

this is exactly what i need for my project! Since my portenta is broken, I can't make any experiments right now. But i have to do it in the next few weeks. I also need the 3.6MSps in combination with the DMA.
I hope i can make it work.
I will get back to you when i find out something!

And thanks for the links!

Try to generate the Initialisation with CubeMX for a STM32H747XI since the Portenta is based on that STM. To me this way worked fine.

here is an example of the ADC-Init from my Project:

/**
  * @brief ADC1 Initialization Function
  * @param None
  * @retval None
  */
static void MX_ADC1_Init(void)
{

  /* USER CODE BEGIN ADC1_Init 0 */

  /* USER CODE END ADC1_Init 0 */

  ADC_MultiModeTypeDef multimode = {0};
  ADC_ChannelConfTypeDef sConfig = {0};

  /* USER CODE BEGIN ADC1_Init 1 */

  /* USER CODE END ADC1_Init 1 */
  /** Common config
  */
  hadc1.Instance = ADC1; //ADC1 Channel_0 is connected to A0 Pin of the Portenta
  hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV256; //dont use ADC_CLOCK_ASYNC_DIV1
  hadc1.Init.Resolution = ADC_RESOLUTION_16B;
  hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;
  hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
  hadc1.Init.LowPowerAutoWait = DISABLE;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.NbrOfConversion = 1;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DR;
  hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
  hadc1.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE;
  hadc1.Init.OversamplingMode = DISABLE;
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    Error_Handler();
  }
  /** Configure the ADC multi-mode
  */
  multimode.Mode = ADC_MODE_INDEPENDENT;
  if (HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode) != HAL_OK)
  {
    Error_Handler();
  }
  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_0; //A0 Pin of the Portenta
  sConfig.Rank = ADC_REGULAR_RANK_1;
  sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
  sConfig.SingleDiff = ADC_SINGLE_ENDED;
  sConfig.OffsetNumber = ADC_OFFSET_NONE;
  sConfig.Offset = 0;
  sConfig.OffsetSignedSaturation = DISABLE;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
}

But me im also trying to set up an ADC with DMA for fast multichannel reading! I ll let u know if i have any success in doing so
It looks to me like we have to self Code on it via HAL library, like there is no support from Arduino for setting up the Portenta in such detailed (Hardware close) settings (at least for now. I hope the best, that we will get some soon).

1 Like

Hello alexbau,

thanks for the ADC-Init code. I copied it into my example and found that it still hangs at

HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);

Of course, if I reduce the timeout from HAL_MAX_DELAY to anything smaller, eventually GPIO_PIN_15 is reset, but no AD read is carried out. Serial.print() just prints a 0...

Did you use PollForConversion in your code? And if so .... was it any faster than analogRead()?

no i dont use PollForConversion in my code. in my setup i use ContinuosConversion (as u can read in my ADC Init;)) and i start the ADC and just read the ADC value (HAL_ADC_GetValue()) in a timer interrupt to have a dedicated samplerate. Since the ADC Conversion is much faster then my Timer intervall it has always a new Value when the timer interrupt occurs. Its a relativly dirty Setup i know, but so far it works well enought for me XD
i achive a Sampling time of about 5us. with analogRead() from arduino i just come down to 2ms per reading
important for that setup is, that u have set the Overrun-Setting to OVERWRITTEN

To ur questions in the Code-Comment:
ADC_SCAN_ENABLE in ScanConvMode is just needed when u want to read multiple channels of the same ADC

__HAL_RCC_ADC1_CLK_ENABLE(): i learned in a beginner course for programming an stm32 that u have to always manually enable the clk for peripherals u want to use!!! but i dont manually enable the clock for my ADC too and everything is working fine nevertheless. so i think this is done automatically by the Arduino core

when u want to read on A0 aka PA0_C u have to set up ADC1 channel0. just look at the pinout from the portenta and check the modes of PA0_C in CubeMX for the STM32H747XI

1 Like

Hi there^^

are u still active on that project/problem?
what is ur Code right now and ur problem in running it propper?

maybe if u have just replaced ur ADC_Init with my ADC_Init there could be a problem with the diffrent modes in which we are using the ADC, since i use it in continiousConv mode i just have to start (call HAL_ADC_Start(...)) once (in void setup() {}). But u are calling HAL_ADC_Start(...) in void loop() {} so u are starting it again and again althougt it is still running (my ADC_Init set it into continiousConv mode so it will just stop to generate new values if u tell it to stop (call HAL_ADC_Stop())). if u start the ADC again and agin in continiousConv mode without stopping before start again the stm gets confused and will hang (throw an error)

could this be the problem?

if thats the case either:

call HAL_ADC_Start(...) just once in void setup() {...}

OR

set hadc1.Init.ContinuousConvMode = DISABLE; in static void MX_ADC1_Init(void) {...}

OR

(dirty)
call HAL_ADC_Stop() after each conversion

Hi alexbau,

thank you for your help! However, still the result of HAL_ADC_GetValue is 0... Maybe you can find the error... I will post the code below. For the code to work you will need the dwt_delay package:

Do you have a working example of the ADC initialization and read which you could share? I really would like to make progress with the DMA example, however there is no point if my ADC doesnt work...

Thank you!

#include "stm32h7xx_hal_gpio.h"
#include "stm32h7xx_hal.h"
#include "stm32h7xx_hal_adc.h"

int     RxPin       = 0;            
int     ADC_raw     = 0;
const int TxPin     = 0; 
uint16_t raw        = 0;


ADC_HandleTypeDef hadc1;

void setup() {
  // put your setup code here, to run once:

  MX_GPIO_H15_Init();       // Initialize GPIO-H
  MX_adc1_Init();
  DWT_Init();
  
  
  Serial.begin(9600);
  //analogReadResolution(16);
  pinMode(RxPin, INPUT);
  pinMode(TxPin, OUTPUT);


HAL_ADC_Start(&hadc1);                                        //Start ADC 
  
}



void loop() {

     
// Test: 
    HAL_GPIO_WritePin(GPIOH, GPIO_PIN_15, GPIO_PIN_SET);      // SET Pin PH15 HIGH
    DWT_Delay(10);
    HAL_GPIO_WritePin(GPIOH, GPIO_PIN_15, GPIO_PIN_RESET);    // SET Pin PH15 LOW
    DWT_Delay(10);
    HAL_GPIO_WritePin(GPIOH, GPIO_PIN_15, GPIO_PIN_SET);      // SET Pin PH15 HIGH
    
    raw = 1;                                     
   
   // HAL_ADC_Start(&hadc1); 
   // if(HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY)== HAL_OK){     //wait until ADC conversion completes.... this is where the code hangs, see oscilloscope pic
      raw = HAL_ADC_GetValue(&hadc1);
    //  }        
    
    HAL_GPIO_WritePin(GPIOH, GPIO_PIN_15, GPIO_PIN_RESET); // SET Pin PH15 LOW
    
   // HAL_ADC_Stop(&hadc1); 
  Serial.print(raw & 0xFF);
  Serial.println((raw>>8) & 0xFF);
  


}


 static void MX_adc1_Init(void)
{

  ADC_MultiModeTypeDef multimode = {0};
  ADC_ChannelConfTypeDef sConfig = {0};


  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV256;
  hadc1.Init.Resolution = ADC_RESOLUTION_16B;
  hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;       
  hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;       // was:ADC_EOC_SEQ_CONV;
  hadc1.Init.LowPowerAutoWait = DISABLE;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.NbrOfConversion = 1;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.ConversionDataManagement = ADC_CONVERSIONDATA_DR;
  hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;
  //hadc1.Init.Overrun = ADC_OVR_DATA_PRESERVED;
  hadc1.Init.LeftBitShift = ADC_LEFTBITSHIFT_NONE;
  hadc1.Init.OversamplingMode = DISABLE;
  
  //multimode.Mode = ADC_MODE_INDEPENDENT;
  //if (HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode) != HAL_OK)
  //{
  //  Error_Handler();
 // }

  sConfig.Channel = ADC_CHANNEL_0;                  // Maybe this is the problem? Which channel should I choose for Pin A0 on the arduino portenta?
  sConfig.Rank = ADC_REGULAR_RANK_1;
  sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;
  sConfig.SingleDiff = ADC_SINGLE_ENDED;
  sConfig.OffsetNumber = ADC_OFFSET_NONE;
  sConfig.Offset = 0;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }


}



// -------------------------------- GPIO Pin "PH15" Initialization for STM32 ---------------------------------
void MX_GPIO_H15_Init()
{
  /* GPIO Ports Clock Enable */
 __HAL_RCC_GPIOH_CLK_ENABLE();

 GPIO_InitTypeDef GPIO_InitStruct; 

  /*Configure GPIO pin : PH15 */
 GPIO_InitStruct.Pin = GPIO_PIN_15;       // Pin15
 GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // digital Output, push-pull configuration
 GPIO_InitStruct.Speed = GPIO_SPEED_HIGH;
 HAL_GPIO_Init(GPIOH, &GPIO_InitStruct);  // GPIO-H
}
// ----------------------------------------------------------------------------------------------------------

void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */

  /* USER CODE END Error_Handler_Debug */
}


// --------------------------------- Timer Functions using DWT for STM32 ------------------------------------
void DWT_Init(void)
{
    if (!(CoreDebug->DEMCR & CoreDebug_DEMCR_TRCENA_Msk)) {
        CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
        DWT->CYCCNT = 0;
        DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
    }
}
// ----------------------------------------------------------------------------------------------------------


// ----------------------------------------------------------------------------------------------------------
void DWT_Delay(uint32_t us) // microseconds
{
    uint32_t startTick  = DWT->CYCCNT,
             targetTick = DWT->CYCCNT + us * (SystemCoreClock/1000000);     // (SystemCoreClock/1000000);
  // Must check if target tick is out of bounds and overflowed
    if (targetTick > startTick) {
        // Not overflowed
        while (DWT->CYCCNT < targetTick);
    } else {
        // Overflowed
        while (DWT->CYCCNT > startTick || DWT->CYCCNT < targetTick);
    }
    
}
// ----------------------------------------------------------------------------------------------------------


Hi^^

at the first look in ur code there are missing some necessary function calls:

  1. u are not initializing ur ADC at all u have to call:
if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    Error_Handler();
  }

after the configuration of ur ADC_HandleTypeDef hadc1 otherwise it would not take the setting as the ADC configuration! look at my static void MX_ADC1_Init(void) for the right position;)

  1. why did u commented the fellowing part?:
multimode.Mode = ADC_MODE_INDEPENDENT;
if (HAL_ADCEx_MultiModeConfigChannel(&hadc1, &multimode) != HAL_OK)
{
  Error_Handler();
}

since CubeMX is generating this part of code i guess its necessary for the right configuration of the ADC! otherwise there can occur some misbehaviors of the ADC

Hello everyone,

it seems that my collegue Moes_Pub and I have found a solution to operate the ADC in the Msps region.

The solution was:

  1. To update CubeMx. We found an update from November 2021 that was specifically for the STM32H7.

  2. To generate the code in STM32CubeMX and then copy all of the generated files (.c & .h) and folders into the folder of an Arduino project. Finally, we copied the main.c in an Arduino (main.ino) sketch and changed the loops to setup() and loop().

Doing so, however, the IDE finds that some functions are defined multiple times. The solution is to comment every function in the stm32h7xx_it.c file or delete it.

Important ADC settings:

hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV1;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;

ADC_CLOCK_SYNC_PCLK_DIV1 Did the trick. You can find this setting in CubeMX.

The example code:

The ADC samples in the loop() until a threshold is reached.

Then the program moves into a noInterrupts() area to block any sort of interruptions that will cause the process to hang and disturb the more or less equidistant ADC samples.
See: Fast GPIO-Toggle with portenta H7 using HAL

In the noInterrupts() area the ADC will collect 100000 measurements and store them in the data array. The GPIO pin PH15 is set and reset to measure the time required for one ADC read.

You can see two things from the oscilliscope screenshot:

  1. The ADC takes about 1 us (1Msps) for one ADC conversion and storage in the data array
  2. The duration is not equal for all conversions

Our solution at the moment: Include a timer TIM16 which is started at the beginning of each ADC read. Then the while loop waits until 2 us have passed. This way, we get equidistant samples with 500ksps. Using noInterrupts() we were able to sample a 250kHz rect() signal twice every period for 500ms and got perfect results without having a synchronization problem.

´

Playing with the timer prescaler etc. should allow equidistant sampling at more than 800ksps.

Here is the working example as .zip file.

Fast_ADC_arduino.zip (13.1 KB)

At the moment, we think that we dont reach the 3.6Msps because we cant change SystemClock_Config().

We get the same error as alexbau in How to set up the Portenta with 480MHz Clock (at least more than 4MHz)

Greetings,
An_Dreh

1 Like

@an_dreh & @Moes_Pub
This is great! I will try to develop this into a DMA based example.

Did you have any luck, using the MPU?

Ah great work :grinning: :+1:
But be carefull with high speed and high resolution ADC-readings. the accuracy is decreasing with faster and higher resolved measurments, as described in this PDF from ST
cd00211314-how-to-get-the-best-adc-accuracy-in-stm32-microcontrollers-stmicroelectronics.pdf (1,8 MB)