0
\$\begingroup\$

I am experiencing the following problem. When I setup an ADC with DMA for 5 channels, I get lower readings than the expected ones.

We have PCBs of the same batch in 3 countries, but only the ones tested in China are failing. At all times the ADC readings read lower than the expected values for all the ADC channels, but the PCBs at the other countries return the expected reading in all channels. All those PCBs are identical and they are driving small motors, have a keypad, have some LEDs UI, and a UART debugging port were we connect a USB to TTL UART cable.

Here is the ADC configuration with DMA (source code attached too):
CubeMX CubeMX schematic schematic schematic

We have measured the reference voltage and all the expected voltages on ADC input pins (some are fixed and known) and all looking good too. On the other hand, when I sample only one ADC channel in polling mode, I do not get this problem, the voltages read as expected.

For example: the readings of a healthy ADC reading at 10-bit resolution should read something like 914 raw value which will correspond to 3652mV for the battery voltage, as opposed to the lower reading which will be something like 837 raw value which corresponds to 3346mV for the battery voltage.

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    adc.c
  * @brief   This file provides code for the configuration
  *          of the ADC instances.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2023 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "adc.h"

/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;

/* ADC1 init function */
void MX_ADC1_Init(void)
{

  /* USER CODE BEGIN ADC1_Init 0 */

  /* USER CODE END ADC1_Init 0 */

  ADC_ChannelConfTypeDef sConfig = {0};

  /* USER CODE BEGIN ADC1_Init 1 */

  /* USER CODE END ADC1_Init 1 */

  /** Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion)
  */
  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV10;
  hadc1.Init.Resolution = ADC_RESOLUTION_10B;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
  hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
  hadc1.Init.LowPowerAutoWait = ENABLE;
  hadc1.Init.LowPowerAutoPowerOff = ENABLE;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.NbrOfConversion = 5;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.DMAContinuousRequests = ENABLE;
  hadc1.Init.Overrun = ADC_OVR_DATA_PRESERVED;
  hadc1.Init.SamplingTimeCommon1 = ADC_SAMPLETIME_160CYCLES_5;
  hadc1.Init.SamplingTimeCommon2 = ADC_SAMPLETIME_160CYCLES_5;
  hadc1.Init.OversamplingMode = DISABLE;
  hadc1.Init.TriggerFrequencyMode = ADC_TRIGGER_FREQ_LOW;
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_0;
  sConfig.Rank = ADC_REGULAR_RANK_1;
  sConfig.SamplingTime = ADC_SAMPLINGTIME_COMMON_1;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_1;
  sConfig.Rank = ADC_REGULAR_RANK_2;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_2;
  sConfig.Rank = ADC_REGULAR_RANK_3;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_3;
  sConfig.Rank = ADC_REGULAR_RANK_4;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  /** Configure Regular Channel
  */
  sConfig.Channel = ADC_CHANNEL_TEMPSENSOR;
  sConfig.Rank = ADC_REGULAR_RANK_5;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN ADC1_Init 2 */

  /* USER CODE END ADC1_Init 2 */

}

void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
{

  GPIO_InitTypeDef GPIO_InitStruct = {0};
  RCC_PeriphCLKInitTypeDef PeriphClkInit = {0};
  if(adcHandle->Instance==ADC1)
  {
  /* USER CODE BEGIN ADC1_MspInit 0 */

  /* USER CODE END ADC1_MspInit 0 */

  /** Initializes the peripherals clocks
  */
    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADC;
    PeriphClkInit.AdcClockSelection = RCC_ADCCLKSOURCE_SYSCLK;
    if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK)
    {
      Error_Handler();
    }

    /* ADC1 clock enable */
    __HAL_RCC_ADC_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**ADC1 GPIO Configuration
    PA0     ------> ADC1_IN0
    PA1     ------> ADC1_IN1
    PA2     ------> ADC1_IN2
    PA3     ------> ADC1_IN3
    */
    GPIO_InitStruct.Pin = V_BAT_IN_Pin|I_BAT_IN_Pin|VARIANT_ID_Pin|BOARD_ID_Pin;
    GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* ADC1 DMA Init */
    /* ADC1 Init */
    hdma_adc1.Instance = DMA1_Channel1;
    hdma_adc1.Init.Request = DMA_REQUEST_ADC1;
    hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
    hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_adc1.Init.Mode = DMA_CIRCULAR;
    hdma_adc1.Init.Priority = DMA_PRIORITY_LOW;
    if (HAL_DMA_Init(&hdma_adc1) != HAL_OK)
    {
      Error_Handler();
    }

    __HAL_LINKDMA(adcHandle,DMA_Handle,hdma_adc1);

    /* ADC1 interrupt Init */
    HAL_NVIC_SetPriority(ADC1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(ADC1_IRQn);
  /* USER CODE BEGIN ADC1_MspInit 1 */

  /* USER CODE END ADC1_MspInit 1 */
  }
}

void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
{

  if(adcHandle->Instance==ADC1)
  {
  /* USER CODE BEGIN ADC1_MspDeInit 0 */

  /* USER CODE END ADC1_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_ADC_CLK_DISABLE();

    /**ADC1 GPIO Configuration
    PA0     ------> ADC1_IN0
    PA1     ------> ADC1_IN1
    PA2     ------> ADC1_IN2
    PA3     ------> ADC1_IN3
    */
    HAL_GPIO_DeInit(GPIOA, V_BAT_IN_Pin|I_BAT_IN_Pin|VARIANT_ID_Pin|BOARD_ID_Pin);

    /* ADC1 DMA DeInit */
    HAL_DMA_DeInit(adcHandle->DMA_Handle);

    /* ADC1 interrupt Deinit */
    HAL_NVIC_DisableIRQ(ADC1_IRQn);
  /* USER CODE BEGIN ADC1_MspDeInit 1 */

  /* USER CODE END ADC1_MspDeInit 1 */
  }
}

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */





/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    dma.c
  * @brief   This file provides code for the configuration
  *          of all the requested memory to memory DMA transfers.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2024 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */

/* Includes ------------------------------------------------------------------*/
#include "dma.h"

/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/*----------------------------------------------------------------------------*/
/* Configure DMA                                                              */
/*----------------------------------------------------------------------------*/

/* USER CODE BEGIN 1 */

/* USER CODE END 1 */

/**
  * Enable DMA controller clock
  */
void MX_DMA_Init(void)
{

  /* DMA controller clock enable */
  __HAL_RCC_DMA1_CLK_ENABLE();

  /* DMA interrupt init */
  /* DMA1_Channel1_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA1_Channel1_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA1_Channel1_IRQn);

}

/* USER CODE BEGIN 2 */

/* USER CODE END 2 */





/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    BxADC.c
  * @brief   Analogue interface with HAL and data conversions
  *
  ******************************************************************************
  * @attention
  *
  * Copyright Statement:
  * Copyright (c) Bboxx Ltd 2023
  * The copyright in this document, which contains information of a proprietary and confidential nature,
  * is vested in Bboxx Limited. The content of this document may not be used for purposes other
  * than that for which it has been supplied and may not be reproduced, either wholly or in part,
  * nor may it be used by, or its contents divulged to, any other person who so ever without written permission
  * of Bboxx Limited.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
//----------------------------------------------------------------------------------------------------------------------
// Includes
//----------------------------------------------------------------------------------------------------------------------

#include <stdbool.h>
#include "adc.h"
#include "stm32g0xx_hal_adc.h"
#include "main.h"
#include "Bboxx.h"
#include "BxMotor.h"
#include "BxADC.h"
#include "BxMessage.h"
#include "BxSerial.h"


//----------------------------------------------------------------------------------------------------------------------
// Defines
//----------------------------------------------------------------------------------------------------------------------
typedef enum {
    V_BAT,
    I_BAT,
    VARIANT_ID,
    BOARD_ID,
    TEMPERATURE,
    NUM_OF_ADC_CH
} ADC_CHS;

//----------------------------------------------------------------------------------------------------------------------
// Variables
//----------------------------------------------------------------------------------------------------------------------
static uint16_t ADCreadings[NUM_OF_ADC_CH];
static uint16_t ADCoffset;
static uint16_t ADCref = ADCREF;
static uint16_t ADCpsuConv = ((ADCREF*(ADCPSUR1+ADCPSUR2))/(ADCMAX*ADCPSUR1)) * RESULTMAG;
static uint16_t ADCiConv = (ADCREF/(ADCIGAIN*ADCISENSER));
extern ADC_HandleTypeDef hadc1;

//----------------------------------------------------------------------------------------------------------------------
// Functions
//----------------------------------------------------------------------------------------------------------------------

void ADC_Initialise(void)
/**
  * @brief  Initialises all ADC
  * @param  None
  * @retval None
  */
{
    HAL_GPIO_WritePin(En3v3A_GPIO_Port, En3v3A_Pin, GPIO_PIN_RESET);  // Turn On current amp power
    HAL_GPIO_WritePin(En3v3B_GPIO_Port, En3v3B_Pin, GPIO_PIN_RESET);  // Turn On IDs (+Motor) power

    HAL_ADC_Start_DMA(&hadc1, (uint16_t*)ADCreadings, sizeof(ADCreadings)/sizeof(ADCreadings[V_BAT]));  // start ADC conversion

    return;
}

void ADC_Calibrate(void)
/**
  * @brief  Calibrates ADC
  * @param  None
  * @retval None
  */
{
    HAL_Delay(2000);
    static bool firstTime = true;
    // Run once
    while (firstTime) {
        // Calibrate to a known voltage such as the battery
        uint16_t volt = ADC_Get_Voltage();
        if (volt > (uint16_t)3750u) {
            firstTime = false; // Stop if battery voltage is reached
            ADCoffset = ADCreadings[I_BAT]; // Save the run mode current offset
            Set_String_Tx_Buffer("calibration done ");
            Message_16_Bit_Number(volt);
            Set_String_Tx_Buffer(" mV and ");
            Message_16_Bit_Number(ADCref);
            Set_String_Tx_Buffer(" mV");
            Set_String_Tx_Buffer(NEWLINE);
        } else {
            ADCref++; // Increase gain via adding error to the reference voltage
        }
    }
}

uint16_t ADC_Remove_Offset(uint16_t RawValue)
/**
  * @brief  Removes the offset from the ADC circuit
  * @param  None
  * @retval None
  */
{
    if(RawValue < ADCoffset)
        RawValue =0;
    else
        RawValue -= ADCoffset;
    return RawValue;
}

uint16_t ADC_Convert_Voltage(uint16_t RawValue)
/**
  * @brief  Converts the Raw ADC data into mM
  * @param  None
  * @retval None
  */
{
    uint32_t Result =0;
    RawValue =ADC_Remove_Offset(RawValue);
    ADCpsuConv = ((ADCref*(ADCPSUR1+ADCPSUR2))/(ADCMAX*ADCPSUR1)) * RESULTMAG;
    Result =(ADCpsuConv * RawValue); // scale result
    Result =(Result>>ADCVARDIVIDE);
    return (uint16_t) Result;
}

uint16_t ADC_Convert_Current(uint16_t RawValue)
/**
  * @brief  Converts the Raw ADC data into mA
  * @param  None
  * @retval None
  */
{
    uint32_t Result =0;
    RawValue =ADC_Remove_Offset(RawValue);
    ADCiConv = (ADCref/(ADCIGAIN*ADCISENSER));
    Result = (ADCiConv * RawValue);
    Result =(Result>>ADCVARDIVIDE);
    return (uint16_t) Result;
}

uint16_t ADC_Convert_ID(uint16_t RawValue)
/**
  * @brief  Converts the Raw ADC data into An ID voltage 10*units
  * @param  None
  * @retval None
  */
{
    uint16_t Result16 =0;                           // For Faster calculation
    uint32_t Result32 =0;                           // For higher resolution
    RawValue =ADC_Remove_Offset(RawValue);          // Needs 32bit calculation
    Result32 =(ADCref * RawValue);                  // scale result
    Result16 =(uint16_t)(Result32>>ADCVARDIVIDE);   // mV
    Result16 += 50;                                 // add 50mV for rounding up
    Result16 = Result16/100;                        // Scale for 10*units
    return (uint8_t) Result16;
}

uint16_t ADC_Get_Voltage(void)
/**
  * @brief  returns the value of the PSU voltage (mV)
  * @param  None
  * @retval None
  */
{
    return ADC_Convert_Voltage(ADCreadings[V_BAT]);
}

uint16_t ADC_Get_Current(void)
/**
  * @brief  returns the current current value (mA)
  * @param  None
  * @retval None
  */
{
    return ADC_Convert_Current(ADCreadings[I_BAT]);
}

uint8_t ADC_Get_Variant(void)
/**
  * @brief  returns the Variant as defined by R68//R70 in 10*units
  * @param  None
  * @retval None
  */
{
    return ADC_Convert_ID(ADCreadings[VARIANT_ID]);
}

uint8_t ADC_Get_BoardID(void)
/**
  * @brief  returns the ID as defined by R67//R71 in 10*units
  * @param  None
  * @retval None
  */
{
    return ADC_Convert_ID(ADCreadings[BOARD_ID]);
}

uint16_t ADC_Get_Temperature(void)
/**
  * @brief  returns the Temp in 100*C
  * @param  None
  * @retval None
  */
{
    return ADCreadings[TEMPERATURE];
}

void ADC_Print_Raw(void)
/**
  * @brief  Prints the Raw ADC data in serial terminal
  * @param  None
  * @retval None
  */
{
    Set_String_Tx_Buffer("ADCref== ");
    Message_16_Bit_Number(ADCref);
    Set_String_Tx_Buffer(NEWLINE);
    Set_String_Tx_Buffer("ADCpsuConv== ");
    Message_16_Bit_Number(ADCpsuConv);
    Set_String_Tx_Buffer(NEWLINE);
    Set_String_Tx_Buffer("ADCiConv== ");
    Message_16_Bit_Number(ADCiConv);
    Set_String_Tx_Buffer(NEWLINE);
    Set_String_Tx_Buffer("ADCoffset== ");
    Message_16_Bit_Number(ADCoffset);
    Set_String_Tx_Buffer(NEWLINE);  

    Set_String_Tx_Buffer("ADCvoltageRAW== ");
    Message_16_Bit_Number(ADCreadings[V_BAT]);

    Set_String_Tx_Buffer("\t\tADCvoltage== ");
    Message_16_Bit_Number_Formatted(ADC_Get_Voltage(), BareNumber);
    Set_String_Tx_Buffer("mV");
    Set_String_Tx_Buffer(NEWLINE);

    Set_String_Tx_Buffer("ADCcurrentRAW== ");
    Message_16_Bit_Number(ADCreadings[I_BAT]);

    Set_String_Tx_Buffer("\t\tADCcurrentRAW== ");
    Message_16_Bit_Number_Formatted(ADC_Get_Current(), BareNumber);
    Set_String_Tx_Buffer("mA");
    Set_String_Tx_Buffer(NEWLINE);

    Set_String_Tx_Buffer("ADCvariantRAW== ");
    Message_16_Bit_Number(ADCreadings[VARIANT_ID]);

    Set_String_Tx_Buffer("\t\tADCvariant== ");
    Message_16_Bit_Number_Formatted(ADC_Get_Variant(), BareNumber);
    Set_String_Tx_Buffer(NEWLINE);

    Set_String_Tx_Buffer("ADCboardIDRAW== ");
    Message_16_Bit_Number(ADCreadings[BOARD_ID]);

    Set_String_Tx_Buffer("\t\tADCboardID== ");
    Message_16_Bit_Number_Formatted(ADC_Get_BoardID(), BareNumber);
    Set_String_Tx_Buffer(NEWLINE);

    Set_String_Tx_Buffer("Temperature== ");
    Message_16_Bit_Number_Formatted(ADC_Get_Temperature(), BareNumber);
    Set_String_Tx_Buffer(NEWLINE);

    Set_String_Tx_Buffer("En3v3A== ");
    Message_8_Bit_Number(HAL_GPIO_ReadPin(En3v3A_GPIO_Port, En3v3A_Pin));
    Set_String_Tx_Buffer(NEWLINE);

    Set_String_Tx_Buffer("En3v3B== ");
    Message_8_Bit_Number(HAL_GPIO_ReadPin(En3v3B_GPIO_Port, En3v3B_Pin));
    Set_String_Tx_Buffer(NEWLINE);
}


void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    //ADC_Print_Raw();
}

//----------------------------------------------------------------------------------------------------------------------
//---------------------------------------------------< end of file >----------------------------------------------------
//----------------------------------------------------------------------------------------------------------------------

\$\endgroup\$
3
  • 2
    \$\begingroup\$ "Same batch" - are they literally the same batch (all assembled at the same facility with the same batches of parts) and being tested with the same firmware? That really suggests the problem is with the testing hardware or procedure. \$\endgroup\$ Commented Apr 1 at 10:24
  • \$\begingroup\$ Since the issue seems to be localized to PCBs tested in China, it's better to double-check the hardware components on those boards. Ensure there are no discrepancies or defects compared to the boards tested elsewhere. \$\endgroup\$
    – liaifat85
    Commented Apr 1 at 16:11
  • \$\begingroup\$ Yes they are the same batch. Today we experienced the problem in a device in another country, hence is probably not a hardware dependent issue in the end. \$\endgroup\$
    – K S
    Commented Apr 5 at 8:47

2 Answers 2

1
\$\begingroup\$

You seem to be missing a call to HAL_ADCEx_Calibration_Start, see https://community.st.com/t5/stm32-mcus-products/what-could-be-the-cause-of-a-constant-voltage-offset-on-the/td-p/145346 and http://www.efton.sk/STM32/gotcha/g149.html. In any case, have a look at the other ADC gotchas listed on this page http://www.efton.sk/STM32/gotcha/

\$\endgroup\$
1
  • \$\begingroup\$ I have used the ST HAL calibration function at a later stage. It is not as straight forward because the way it is implemented in the generated library has bugs. It will require some delays between starting and stopping the ADC to work. But this is not solving the problem either. The ADC problem we got has to do with the wrong gain rather than the wrong offset. \$\endgroup\$
    – K S
    Commented Apr 5 at 8:46
0
\$\begingroup\$

Resolved at the ST forum by using the ST HAL calibration function for offset and the Vrefin channel for gain: https://community.st.com/t5/stm32-mcus-products/stm32-adc-dma-low-raw-voltage-readings/td-p/655958

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.