2026-02-10 2:18 AM
We have a design that has been working fine for some time now using STM32G071CBT6 in LQFP48 but a new batch is suddenly failing with ADC readings from VREFINT that are quite a way out of spec.
Previous versions have always been slightly low - reading maybe ~3.15v on a ~3.25v supply voltage but that's always been "close enough", but now the latest batch of devices are dipping below 3.0v on the same ~2.35v supply and triggering our under-voltage detection.
We are monitoring a few voltages using the ADC, including the supply voltage which is commoned with VDD/VDDA, VBAT and VREF pins - decoupling is per the power supply scheme shown in the datasheet DS12232.
VREFBUF is disabled.
Since we are not worried too much about absolute accuracy, we are not running the calibration routine on the ADC.
System clock is 16MHz and ADC sampling is 79.5 cycles, ADC resolution is 12 bit.
The ground pin is solidly grounded, supply voltage going into the pins is ~3.25v and I can't see any significant current being injected into pins anywhere.
Setup code generated by CubeIDE, USE_TIMEOUT is not defined:
/* ADC1 init function */
void MX_ADC1_Init(void)
{
/* USER CODE BEGIN ADC1_Init 0 */
/**ADC1 GPIO Configuration
PA0 ------> ADC1_IN0 --> ADC_12VA
PA1 ------> ADC1_IN1 --> ADC_12VB
PA2 ------> ADC1_IN2 --> ADC_V_IN
PA3 ------> ADC1_IN3 --> ADC_5V
*/
/* USER CODE END ADC1_Init 0 */
LL_ADC_InitTypeDef ADC_InitStruct = {0};
LL_ADC_REG_InitTypeDef ADC_REG_InitStruct = {0};
LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
/* Peripheral clock enable */
LL_APB2_GRP1_EnableClock(LL_APB2_GRP1_PERIPH_ADC);
LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
/**ADC1 GPIO Configuration
PA0 ------> ADC1_IN0
PA1 ------> ADC1_IN1
PA2 ------> ADC1_IN2
PA3 ------> ADC1_IN3
*/
GPIO_InitStruct.Pin = ADC_12VA_Pin;
GPIO_InitStruct.Mode = LL_GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(ADC_12VA_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = ADC_12VB_Pin;
GPIO_InitStruct.Mode = LL_GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(ADC_12VB_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = ADC_V_IN_Pin;
GPIO_InitStruct.Mode = LL_GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(ADC_V_IN_GPIO_Port, &GPIO_InitStruct);
GPIO_InitStruct.Pin = ADC_5V_Pin;
GPIO_InitStruct.Mode = LL_GPIO_MODE_ANALOG;
GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
LL_GPIO_Init(ADC_5V_GPIO_Port, &GPIO_InitStruct);
/* ADC1 DMA Init */
/* ADC1 Init */
LL_DMA_SetPeriphRequest(DMA1, LL_DMA_CHANNEL_5, LL_DMAMUX_REQ_ADC1);
LL_DMA_SetDataTransferDirection(DMA1, LL_DMA_CHANNEL_5, LL_DMA_DIRECTION_PERIPH_TO_MEMORY);
LL_DMA_SetChannelPriorityLevel(DMA1, LL_DMA_CHANNEL_5, LL_DMA_PRIORITY_LOW);
LL_DMA_SetMode(DMA1, LL_DMA_CHANNEL_5, LL_DMA_MODE_CIRCULAR);
LL_DMA_SetPeriphIncMode(DMA1, LL_DMA_CHANNEL_5, LL_DMA_PERIPH_NOINCREMENT);
LL_DMA_SetMemoryIncMode(DMA1, LL_DMA_CHANNEL_5, LL_DMA_MEMORY_INCREMENT);
LL_DMA_SetPeriphSize(DMA1, LL_DMA_CHANNEL_5, LL_DMA_PDATAALIGN_HALFWORD);
LL_DMA_SetMemorySize(DMA1, LL_DMA_CHANNEL_5, LL_DMA_MDATAALIGN_HALFWORD);
/* 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)
*/
#define ADC_CHANNEL_CONF_RDY_TIMEOUT_MS ( 1U)
#if (USE_TIMEOUT == 1)
uint32_t Timeout ; /* Variable used for Timeout management */
#endif /* USE_TIMEOUT */
ADC_InitStruct.Clock = LL_ADC_CLOCK_SYNC_PCLK_DIV2;
ADC_InitStruct.Resolution = LL_ADC_RESOLUTION_12B;
ADC_InitStruct.DataAlignment = LL_ADC_DATA_ALIGN_RIGHT;
ADC_InitStruct.LowPowerMode = LL_ADC_LP_AUTOWAIT;
LL_ADC_Init(ADC1, &ADC_InitStruct);
LL_ADC_REG_SetSequencerConfigurable(ADC1, LL_ADC_REG_SEQ_CONFIGURABLE);
/* Poll for ADC channel configuration ready */
#if (USE_TIMEOUT == 1)
Timeout = ADC_CHANNEL_CONF_RDY_TIMEOUT_MS;
#endif /* USE_TIMEOUT */
while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
{
#if (USE_TIMEOUT == 1)
/* Check Systick counter flag to decrement the time-out value */
if (LL_SYSTICK_IsActiveCounterFlag())
{
if(Timeout-- == 0)
{
Error_Handler();
}
}
#endif /* USE_TIMEOUT */
}
/* Clear flag ADC channel configuration ready */
LL_ADC_ClearFlag_CCRDY(ADC1);
ADC_REG_InitStruct.TriggerSource = LL_ADC_REG_TRIG_SOFTWARE;
ADC_REG_InitStruct.SequencerLength = LL_ADC_REG_SEQ_SCAN_ENABLE_6RANKS;
ADC_REG_InitStruct.SequencerDiscont = LL_ADC_REG_SEQ_DISCONT_DISABLE;
ADC_REG_InitStruct.ContinuousMode = LL_ADC_REG_CONV_SINGLE;
ADC_REG_InitStruct.DMATransfer = LL_ADC_REG_DMA_TRANSFER_LIMITED;
ADC_REG_InitStruct.Overrun = LL_ADC_REG_OVR_DATA_PRESERVED;
LL_ADC_REG_Init(ADC1, &ADC_REG_InitStruct);
LL_ADC_SetOverSamplingScope(ADC1, LL_ADC_OVS_DISABLE);
LL_ADC_SetTriggerFrequencyMode(ADC1, LL_ADC_CLOCK_FREQ_MODE_HIGH);
LL_ADC_SetCommonPathInternalCh(__LL_ADC_COMMON_INSTANCE(ADC1), LL_ADC_AWD_CH_VREFINT_REG|LL_ADC_AWD_CH_TEMPSENSOR_REG);
LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_COMMON_1, LL_ADC_SAMPLINGTIME_79CYCLES_5);
LL_ADC_SetSamplingTimeCommonChannels(ADC1, LL_ADC_SAMPLINGTIME_COMMON_2, LL_ADC_SAMPLINGTIME_1CYCLE_5);
LL_ADC_DisableIT_EOC(ADC1);
LL_ADC_DisableIT_EOS(ADC1);
/* Enable ADC internal voltage regulator */
LL_ADC_EnableInternalRegulator(ADC1);
/* Delay for ADC internal voltage regulator stabilization. */
/* Compute number of CPU cycles to wait for, from delay in us. */
/* Note: Variable divided by 2 to compensate partially */
/* CPU processing cycles (depends on compilation optimization). */
/* Note: If system core clock frequency is below 200kHz, wait time */
/* is only a few CPU processing cycles. */
uint32_t wait_loop_index;
wait_loop_index = ((LL_ADC_DELAY_INTERNAL_REGUL_STAB_US * (SystemCoreClock / (100000 * 2))) / 10);
while(wait_loop_index != 0)
{
wait_loop_index--;
}
/** Configure Regular Channel
*/
LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_1, LL_ADC_CHANNEL_VREFINT);
/* Poll for ADC channel configuration ready */
#if (USE_TIMEOUT == 1)
Timeout = ADC_CHANNEL_CONF_RDY_TIMEOUT_MS;
#endif /* USE_TIMEOUT */
while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
{
#if (USE_TIMEOUT == 1)
/* Check Systick counter flag to decrement the time-out value */
if (LL_SYSTICK_IsActiveCounterFlag())
{
if(Timeout-- == 0)
{
Error_Handler();
}
}
#endif /* USE_TIMEOUT */
}
/* Clear flag ADC channel configuration ready */
LL_ADC_ClearFlag_CCRDY(ADC1);
LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_VREFINT, LL_ADC_SAMPLINGTIME_COMMON_1);
/** Configure Regular Channel
*/
LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_2, LL_ADC_CHANNEL_TEMPSENSOR);
/* Poll for ADC channel configuration ready */
#if (USE_TIMEOUT == 1)
Timeout = ADC_CHANNEL_CONF_RDY_TIMEOUT_MS;
#endif /* USE_TIMEOUT */
while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
{
#if (USE_TIMEOUT == 1)
/* Check Systick counter flag to decrement the time-out value */
if (LL_SYSTICK_IsActiveCounterFlag())
{
if(Timeout-- == 0)
{
Error_Handler();
}
}
#endif /* USE_TIMEOUT */
}
/* Clear flag ADC channel configuration ready */
LL_ADC_ClearFlag_CCRDY(ADC1);
LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_TEMPSENSOR, LL_ADC_SAMPLINGTIME_COMMON_1);
/** Configure Regular Channel
*/
LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_3, LL_ADC_CHANNEL_0);
LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_0, LL_ADC_SAMPLINGTIME_COMMON_1);
/** Configure Regular Channel
*/
LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_4, LL_ADC_CHANNEL_1);
/* Poll for ADC channel configuration ready */
#if (USE_TIMEOUT == 1)
Timeout = ADC_CHANNEL_CONF_RDY_TIMEOUT_MS;
#endif /* USE_TIMEOUT */
while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
{
#if (USE_TIMEOUT == 1)
/* Check Systick counter flag to decrement the time-out value */
if (LL_SYSTICK_IsActiveCounterFlag())
{
if(Timeout-- == 0)
{
Error_Handler();
}
}
#endif /* USE_TIMEOUT */
}
/* Clear flag ADC channel configuration ready */
LL_ADC_ClearFlag_CCRDY(ADC1);
LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_1, LL_ADC_SAMPLINGTIME_COMMON_1);
/** Configure Regular Channel
*/
LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_5, LL_ADC_CHANNEL_2);
/* Poll for ADC channel configuration ready */
#if (USE_TIMEOUT == 1)
Timeout = ADC_CHANNEL_CONF_RDY_TIMEOUT_MS;
#endif /* USE_TIMEOUT */
while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
{
#if (USE_TIMEOUT == 1)
/* Check Systick counter flag to decrement the time-out value */
if (LL_SYSTICK_IsActiveCounterFlag())
{
if(Timeout-- == 0)
{
Error_Handler();
}
}
#endif /* USE_TIMEOUT */
}
/* Clear flag ADC channel configuration ready */
LL_ADC_ClearFlag_CCRDY(ADC1);
LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_2, LL_ADC_SAMPLINGTIME_COMMON_1);
/** Configure Regular Channel
*/
LL_ADC_REG_SetSequencerRanks(ADC1, LL_ADC_REG_RANK_6, LL_ADC_CHANNEL_3);
/* Poll for ADC channel configuration ready */
#if (USE_TIMEOUT == 1)
Timeout = ADC_CHANNEL_CONF_RDY_TIMEOUT_MS;
#endif /* USE_TIMEOUT */
while (LL_ADC_IsActiveFlag_CCRDY(ADC1) == 0)
{
#if (USE_TIMEOUT == 1)
/* Check Systick counter flag to decrement the time-out value */
if (LL_SYSTICK_IsActiveCounterFlag())
{
if(Timeout-- == 0)
{
Error_Handler();
}
}
#endif /* USE_TIMEOUT */
}
/* Clear flag ADC channel configuration ready */
LL_ADC_ClearFlag_CCRDY(ADC1);
LL_ADC_SetChannelSamplingTime(ADC1, LL_ADC_CHANNEL_3, LL_ADC_SAMPLINGTIME_COMMON_1);
/* USER CODE BEGIN ADC1_Init 2 */
adc_dma_setup();
/* USER CODE END ADC1_Init 2 */
}
And here's the code calculating VDD from VREFINT using the macro from stm32g0xx_ll_adc.h:
vdda = (uint16_t)__LL_ADC_CALC_VREFANALOG_VOLTAGE(gADCRawData, LL_ADC_RESOLUTION_12B);
On a "good" device the values look like:
VREFINT CAL value in flash is 1660
ADC readings are hovering around ~1585 at ~3.3VDD giving a calculated VDD of ~3.145v
On the "bad" device, the values are:
VREFINT CAL value in flash is 1662
ADC readings are hovering around ~1660 at ~3.3VDD giving a calculated VDD of ~3.000v
Since we originally set a low voltage detect threshold of <3.000v this is occasionally dipping below that, an error of 300mv seems very large even if the ADC is not calibrated.
Is this simply a case where we should run the ADC Calibration routine (LL_ADC_StartCalibration() etc...) and it will correct it or is something else going on here? The error seems a bit too large for it to be down to lack of calibration.
Solved! Go to Solution.
2026-02-10 8:21 AM
"Low accuracy" in ADC terms wouldn't usually be -10% / 300mV though, that's a barn door even for a basic MCU ADC.
Anyway - adding in the calibration code from \NUCLEO-G071RB\Examples_LL\ADC\ADC_MultiChannelSingleConversion\Src\main.c and throwing in an 8x averaging for good measure seems to have thrown out a cal factor of ~75 and bought the readings back to reality.
2026-02-10 3:06 AM
You should always run the ADC calibration before using it.
2026-02-10 3:22 AM
The HAL says it's optional to improve accuracy...
(#) Optionally, perform an automatic ADC calibration to improve the conversion accuracy
using function HAL_ADCEx_Calibration_Start().
stm32g0xx_hal_adc.c line 130
But I will try it and see if it improves matters.
2026-02-10 3:31 AM
2026-02-10 5:50 AM - edited 2026-02-10 5:51 AM
> The HAL says it's optional to improve accuracy...
Well, yes, and isn't low accuracy exactly the problem you have?
JW
2026-02-10 8:21 AM
"Low accuracy" in ADC terms wouldn't usually be -10% / 300mV though, that's a barn door even for a basic MCU ADC.
Anyway - adding in the calibration code from \NUCLEO-G071RB\Examples_LL\ADC\ADC_MultiChannelSingleConversion\Src\main.c and throwing in an 8x averaging for good measure seems to have thrown out a cal factor of ~75 and bought the readings back to reality.
2026-02-10 8:49 AM
Then let that be a lesson on the value of HAL documentation.
As @RobK1 said above, the RM is crystal clear in the requirement for calibration.
JW