How to measure voltage of battery using ESP32 internal reference voltage?

When using the internal 1.1V reference voltage, I powered an ESP32 with 5V USB and measured a 1.5V source using one of the ADC inputs, this got me 4095 as the ADC measurement which was expected as 1.5>1.1. However, when powering the ESP32 with a 3.3V battery directly with the 3.3V port, I got an ADC value of ~1600 which is completely wrong.
I have tried with 2 boards, the MakerHawk ESP32 (https://www.amazon.co.uk/gp/product/B076P8GRWV/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1) and the MELIFE ESP32-DevKitC (https://www.amazon.co.uk/gp/product/B0811LGWY2/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)
This is the code I used and the ADC port was GPIO36.

#include "esp_adc_cal.h"
 
void setup() {
  
  Serial2.begin(9600, SERIAL_8N1, 2, 17); //2 = RX, 17 = TX
  adc1_config_width(ADC_WIDTH_12Bit);
  adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_0db); //set reference voltage to internal
  
}

void loop() {

  //Obtain ADC reading
  int adcValue = adc1_get_raw(ADC1_CHANNEL_0);
  
  //Send reading over UART to another ESP32 which is connected to laptop so can view ADC reading on serial monitor
  Serial2.write(adcValue);      //Bits 7:0 of integer 'adcValue'
  Serial2.write(adcValue>>8);   //Bits 15:8 of integer 'adcValue'

}

This is the header file used:

// Copyright 2015-2016 Espressif Systems (Shanghai) PTE LTD
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at

//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#ifndef __ESP_ADC_CAL_H__
#define __ESP_ADC_CAL_H__

#include <stdint.h>
#include "esp_err.h"
#include "driver/adc.h"

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief Type of calibration value used in characterization
 */
typedef enum {
    ESP_ADC_CAL_VAL_EFUSE_VREF = 0,         /**< Characterization based on reference voltage stored in eFuse*/
    ESP_ADC_CAL_VAL_EFUSE_TP = 1,           /**< Characterization based on Two Point values stored in eFuse*/
    ESP_ADC_CAL_VAL_DEFAULT_VREF = 2,       /**< Characterization based on default reference voltage*/
    ESP_ADC_CAL_VAL_MAX,
    ESP_ADC_CAL_VAL_NOT_SUPPORTED = ESP_ADC_CAL_VAL_MAX,
} esp_adc_cal_value_t;

/**
 * @brief Structure storing characteristics of an ADC
 *
 * @note Call esp_adc_cal_characterize() to initialize the structure
 */
typedef struct {
    adc_unit_t adc_num;                     /**< ADC number*/
    adc_atten_t atten;                      /**< ADC attenuation*/
    adc_bits_width_t bit_width;             /**< ADC bit width */
    uint32_t coeff_a;                       /**< Gradient of ADC-Voltage curve*/
    uint32_t coeff_b;                       /**< Offset of ADC-Voltage curve*/
    uint32_t vref;                          /**< Vref used by lookup table*/
    const uint32_t *low_curve;              /**< Pointer to low Vref curve of lookup table (NULL if unused)*/
    const uint32_t *high_curve;             /**< Pointer to high Vref curve of lookup table (NULL if unused)*/
} esp_adc_cal_characteristics_t;

/**
 * @brief Checks if ADC calibration values are burned into eFuse
 *
 * This function checks if ADC reference voltage or Two Point values have been
 * burned to the eFuse of the current ESP32
 *
 * @param   value_type  Type of calibration value (ESP_ADC_CAL_VAL_EFUSE_VREF or ESP_ADC_CAL_VAL_EFUSE_TP)
 * @note in ESP32S2, only ESP_ADC_CAL_VAL_EFUSE_TP is supported. Some old ESP32S2s do not support this, either.
 * In which case you have to calibrate it manually, possibly by performing your own two-point calibration on the chip.
 *
 * @return
 *      - ESP_OK: The calibration mode is supported in eFuse
 *      - ESP_ERR_NOT_SUPPORTED: Error, eFuse values are not burned
 *      - ESP_ERR_INVALID_ARG: Error, invalid argument (ESP_ADC_CAL_VAL_DEFAULT_VREF)
 */
esp_err_t esp_adc_cal_check_efuse(esp_adc_cal_value_t value_type);

/**
 * @brief Characterize an ADC at a particular attenuation
 *
 * This function will characterize the ADC at a particular attenuation and generate
 * the ADC-Voltage curve in the form of [y = coeff_a * x + coeff_b].
 * Characterization can be based on Two Point values, eFuse Vref, or default Vref
 * and the calibration values will be prioritized in that order.
 *
 * @note
 * For ESP32, Two Point values and eFuse Vref calibration can be enabled/disabled using menuconfig.
 * For ESP32s2, only Two Point values calibration and only ADC_WIDTH_BIT_13 is supported. The parameter default_vref is unused.
 *
 *
 * @param[in]   adc_num         ADC to characterize (ADC_UNIT_1 or ADC_UNIT_2)
 * @param[in]   atten           Attenuation to characterize
 * @param[in]   bit_width       Bit width configuration of ADC
 * @param[in]   default_vref    Default ADC reference voltage in mV (Only in ESP32, used if eFuse values is not available)
 * @param[out]  chars           Pointer to empty structure used to store ADC characteristics
 *
 * @return
 *      - ESP_ADC_CAL_VAL_EFUSE_VREF: eFuse Vref used for characterization
 *      - ESP_ADC_CAL_VAL_EFUSE_TP: Two Point value used for characterization (only in Linear Mode)
 *      - ESP_ADC_CAL_VAL_DEFAULT_VREF: Default Vref used for characterization
 */
esp_adc_cal_value_t esp_adc_cal_characterize(adc_unit_t adc_num,
                                             adc_atten_t atten,
                                             adc_bits_width_t bit_width,
                                             uint32_t default_vref,
                                             esp_adc_cal_characteristics_t *chars);

/**
 * @brief   Convert an ADC reading to voltage in mV
 *
 * This function converts an ADC reading to a voltage in mV based on the ADC's
 * characteristics.
 *
 * @note    Characteristics structure must be initialized before this function
 *          is called (call esp_adc_cal_characterize())
 *
 * @param[in]   adc_reading     ADC reading
 * @param[in]   chars           Pointer to initialized structure containing ADC characteristics
 *
 * @return      Voltage in mV
 */
uint32_t esp_adc_cal_raw_to_voltage(uint32_t adc_reading, const esp_adc_cal_characteristics_t *chars);

/**
 * @brief   Reads an ADC and converts the reading to a voltage in mV
 *
 * This function reads an ADC then converts the raw reading to a voltage in mV
 * based on the characteristics provided. The ADC that is read is also
 * determined by the characteristics.
 *
 * @note    The Characteristics structure must be initialized before this
 *          function is called (call esp_adc_cal_characterize())
 *
 * @param[in]   channel     ADC Channel to read
 * @param[in]   chars       Pointer to initialized ADC characteristics structure
 * @param[out]  voltage     Pointer to store converted voltage
 *
 * @return
 *      - ESP_OK: ADC read and converted to mV
 *      - ESP_ERR_TIMEOUT: Error, timed out attempting to read ADC
 *      - ESP_ERR_INVALID_ARG: Error due to invalid arguments
 */
esp_err_t esp_adc_cal_get_voltage(adc_channel_t channel, const esp_adc_cal_characteristics_t *chars, uint32_t *voltage);

#ifdef __cplusplus
}
#endif

#endif /* __ESP_ADC_CAL_H__ */

Please do not past pictures of code. It is a waste of time as it cannot be copied for examination and testing

The easier you make it to read and copy your code the more likely it is that you will get help

Please follow the advice given in the link below when posting code , use code tags and post the code here

If you get errors when compiling please copy them from the IDE using the "Copy error messages" button and paste the clipboard here in code tags

My apologies, I am new to the forum and so do not have experience when asking questions. The code is below. Thanks.

#include "esp_adc_cal.h"
 
void setup() {
  
  adc1_config_width(ADC_WIDTH_12Bit);
  adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_0db); //set reference voltage to internal
  
}

void loop() {

  int adcValue = adc1_get_raw(ADC1_CHANNEL_0);

}

We need to see ALL the code Andrew - please post the rest (ie your header file, functions etc.)

Also please tell us if you are using an ESP32 alone, or on a development board & if so which one - as they ALL have subtle differences as explained in this link

Sorry, I have updated the post to hopefully include all of the relevant information as well as the actual code rather than a screenshot. Thanks.

The code you have posted is useless without the include file. Can you attach it please?

Hi, sorry again, I have attached the header file. I believe the specific function used is defined in this library (esp-idf/adc_common.c at master · espressif/esp-idf · GitHub). I was following the information available at Analog to Digital Converter - ESP32 - — ESP-IDF Programming Guide latest documentation to get the correct functionality.

Hi Andrew: IMHO you are making life very hard for yourself (and us :wink: )
its very easy to read the analog voltage with an ESP32 without all that additional code.

Here is an example I have tested with a pot connected between 0V and 3.3V, slider to GPIO34
(adapted from ESP32 Analog Input with Arduino IDE | Random Nerd Tutorials)

/*
 * NB ADC2 can not be used when using WiFi
 * Potentiometer is connected to GPIO 34 (Analog ADC1_CH6) 
*/

const int potPin = 34;
int potValue = 0;
float volts;

void setup() {
  Serial.begin(115200);
  delay(1000);
}

void loop() {
  // Reading potentiometer value
  potValue = analogRead(potPin);
  volts = potValue * 3.3/4096;
   Serial.printf("reading is %d and voltage is %5.3f \n",potValue, volts);
  delay(500);
}

My conversion will give you a "ball park" figure for voltage but you should be aware the ESP32 ADC is VERY inaccurate - as you will see on the RN page here

Hi, thanks for your help. I have adapted some of your code to test my configuration but the problem still exists where when I have a battery powering the ESP32, connected directly to the 3.3V pin, the ADC reading is constantly fluctuating from 0 to 4095 whereas with the USB connected and powering the device, the ADC reading is constant. I tested it with a voltage of roughy 2.5V with this code:

void setup() {
  
  //Using UART to send the ADC reading to another ESP32 which is connected to laptop and will display value on serial monitor
  Serial2.begin(9600, SERIAL_8N1, 2, 17); //2 = RX, 17 = TX
  analogSetCycles(255);
  analogSetAttenuation(ADC_0db);
  
}

void loop() {

  //Obtain ADC reading
  int adcVal = analogRead(34);
  
  //Send reading over UART to another ESP32 which is connected to laptop so can view ADC reading on serial monitor
  Serial2.write(adcValue);      //Bits 7:0 of integer 'adcValue'
  Serial2.write(adcValue>>8);   //Bits 15:8 of integer 'adcValue'

}

To clarify, my end result is to power the ESP32 using just a battery and to every now and again take a reading of the battery to see roughly how much percentage is remaining. I am using a CR123A lithium battery which should produce enough current as I have already tested the ESP32s WiFi functionality whilst being powered by the battery, this uses roughly 110mA.
Do you know why using the battery to power the ESP32 causes the ADC reading to fluctuate across the entire 12-bit range? As you said, the ADC reading will not be very accurate but a rough estimate of the voltage remaining should be good enough for my purposes. Thanks.

sets no attenuation. ADC can measure up to approximately 800 mV (1V input = ADC reading of 1088).

I'd suggest you leave the default settings - just comment these out

Run it from USB and test it with my code above unmodified. Then you can add in the
Send reading over UART

Would probably work better with adcVal in the serial writes though.

Did you forget the ~500mA and ESP draws during short transmit peaks.
That could drop battery voltage to ~2.6volt if you believe these graphs.
Did you put a scope on the 3.3volt terminal?
Try a >= 1000uF tantalum buffer cap across the 3.3volt/GND of the ESP chip/module.
Leo..

Edit: I'm still not sure if the ESP32 has a ratiometric A/D (with PGA) or an absolute A/D like the ESP8266. Who can enlighten me.

Hi Leo; The ESP32 ADC is also absolute (also inaccurate and non-linear)

Per design the ADC reference voltage is 1100 mV, however the true reference voltage can range from 1000 mV to 1200 mV amongst different ESP32-S2s.
https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-reference/peripherals/adc.html

I had to look up PGA - pin grid array, etc but eventually found "Programmable Gain Amplifier" which made more sense.

While adding a cap would make sense I have a feeling that a battery is itself a pretty effective capacitor.

OP is using a CR123A camera battery, which is about half the size of an AA battery.
A cap could help with the short high-current peaks an ESP needs.
Leo..