How those voltage levels for brownout of ESP32S3 are defined if there is no internal voltage references implemented?

djixon
Posts: 113
Joined: Sun Oct 01, 2023 7:48 pm

How those voltage levels for brownout of ESP32S3 are defined if there is no internal voltage references implemented?

Postby djixon » Sun Apr 07, 2024 1:48 pm

Are those levels defined as some percentage of power supplied voltage (which is usually 3.3V +- 0.3V) or they are deffined by some internal circuit in ESP32S3 which is not exposed to the rest of hardware but only to brown_out function?

Battery powered applications, can generate large voltage fluctuation on power supply (esspecially in fast switching of large consumers possibly connected to ESP chip, like relay(s), coils, amps etc.). So in scenarios where exaustion of battery must be prevented (not because of battery itself, because most of LiPo bateries already contain a small circuit which protect the battery goes to deep discharge by cutting battery output once the voltage drops below some critical level) because you have to protect time keeping mechanism handled by RTC, so you must not allow situation where large consumption of whole application will trigger protection implemented in battery which then will be followed by ESP reset, but rather, you want to recognize some specific voltage level before that happens, so application can put LX7 cores at sleep and reducing current consumption avoiding chip reset on brown out, allowing the remaining of battery capacitance to hold RTC and time keeping function.

So something like function GO_TO_SLEEP_ON_BROWN_OUT (if such thing exists). Technically it would be posible to try to simulate such a function using ULP (which consumes really small amount of current in operational mode) but problem is related to absence of internal absolute stable voltage reference. For example utilizing the AD converter on ULP and measurring power supply voltage is not suitable for that purpose because measuring value of AD converter is ALWAYS related to some percents of power supply voltage. So basically, if you configure a free input pin for AD converter on ULP, to measure power supply voltage, you will always measure value of 4095 (full saturation) becuase that internal AD converter uses the same power source for its reference of full saturation. The same thing, for example, if you make some voltage divider, let's say by using two identical resistors in between power supply and GND, and measure voltage at that point in between, you will always get 12 bit value of 2048, because you are actually measuring 50% of power supply voltage and not an absolute rference of 1.65V. So if power supply voltage is 3 V that binary 2048 will represent 1.5V however if power supply voltage is 3.7 V the same value represents 1.85V. So you see the problem. You can not measure a power supply voltage if it is used as a refference at the same time.

For proper, measurement of power supply, an internally generated voltage reference is required with respect to ground. (many microcontroller manufacturers provide in their chips, stable internal refferences of: 1.024V, 2.048V (and 4.096V for chips running at 5V). From values, it is obvious why they choose exactlly those. Because they represent values of fully saturated 10, 11 and 12 bit AD convertors and are also useful in combination with DA converters and comparator circuits.

So my question, technically speaking is, is it possible to accuratelly measure power supply voltage, in battery powered applications utillizing ESP32S3 in sleep mode, without any additional hardware circuits (VCOs, external references ICs etc)? And how?
Last edited by djixon on Sun Apr 07, 2024 3:24 pm, edited 1 time in total.

ESP_Sprite
Posts: 9764
Joined: Thu Nov 26, 2015 4:08 am

Re: How those voltage levels for brownout of ESP32S3 are defined if there is no internal voltage references implemented?

Postby ESP_Sprite » Sun Apr 07, 2024 2:06 pm

I have no idea where you got the idea that the ADC measures relative to the voltage line. It doesn't; the ESP32 has an internal reference voltage of about 1.1V (exact voltage is stored in the eFuses) and the ADC measures relative to that, with an optional attenuation.

djixon
Posts: 113
Joined: Sun Oct 01, 2023 7:48 pm

Re: How those voltage levels for brownout of ESP32S3 are defined if there is no internal voltage references implemented?

Postby djixon » Sun Apr 07, 2024 2:17 pm

I meassured power supply voltage (using ULP AD converter on ESP32 since it is the only available AD converter when main cores are at sleep) with pin directly connected to power supply, generated on laboratory voltage source generator, which I could manually adjust from 2.800V to 3.600V and I always got satturated 4095 value. That should not happen if there is an intrernal reference.

My idea was to use ULP to recognize proper voltage level at which to turn LX cores to sleep, but also to recognize when to wake them up (if charger is applied and voltage returned back above some treshold). But all I got by measuring direct voltage (which is used for powering ESP chip) is always full saturation.

Is it possible that, reference you are talking about, is for AD converters existing at LX cores and doesn't work for ULP AD when the main cores are in deep sleep?

MicroController
Posts: 1734
Joined: Mon Oct 17, 2022 7:38 pm
Location: Europe, Germany

Re: How those voltage levels for brownout of ESP32S3 are defined if there is no internal voltage references implemented?

Postby MicroController » Mon Apr 08, 2024 1:20 am

djixon wrote:
Sun Apr 07, 2024 2:17 pm
from 2.800V to 3.600V and I always got satturated 4095 value. That should not happen if there is an intrernal reference.
What value do you expect to read when applying 3V to the ADC with a Vref of 1.1V?

ESP_Sprite
Posts: 9764
Joined: Thu Nov 26, 2015 4:08 am

Re: How those voltage levels for brownout of ESP32S3 are defined if there is no internal voltage references implemented?

Postby ESP_Sprite » Mon Apr 08, 2024 1:27 am

djixon wrote:
Sun Apr 07, 2024 2:17 pm
I meassured power supply voltage (using ULP AD converter on ESP32 since it is the only available AD converter when main cores are at sleep) with pin directly connected to power supply, generated on laboratory voltage source generator, which I could manually adjust from 2.800V to 3.600V and I always got satturated 4095 value. That should not happen if there is an intrernal reference.
Yeah, that's to be expected. As I stated before, the ESP32 has an internal 1.1V reference, so any voltage above that obviously saturates to 4095. You can turn on attenuation (not sure if you had), but even with the maximum attenuation of 12dB, your max voltage is 2450mV (ref).

djixon
Posts: 113
Joined: Sun Oct 01, 2023 7:48 pm

Re: How those voltage levels for brownout of ESP32S3 are defined if there is no internal voltage references implemented?

Postby djixon » Wed Apr 10, 2024 3:35 pm

Well, it is possibly related to AD converter on chip when it works under regular condition (normal working state). However, if main cores are in sleep state (almost everything is shut down except RTC module and ULP co-processor). It obviously uses separated AD converters powered by that RTC power branch. Take a look at an example you deliver with IDE, under ULP section. Demo project name is ulp_adc.
It is configured to do similar thing I want. With minor changes to that project, and two tresholds set up that: lower activates sleep of main cores when voltage on AD input is below 1.35V and upper at 1.75V to bring back main cores from sleep into normal working state. That example works perfect for fixed power source of 3.3V. When eSP is powered with those fixed 3.3V, and I use laboratory volatage generator and bring voltage to that AD pin, it works as it is stated in example. It brings to sleep main cores if input voltage on AD conv is below 1.35V (powering is fixed at 3.3V) and also return back from sleep main cores when voltage raise above 1.75V (powering is fixed at 3.3V).

Based on that I got an idea to measure powering line, with 50:50 resistor divider. But whatever voltage I bring to power line I always get the same value on AD converter. If I bring "raw" voltage I get full saturation, if I get 50:50 divided voltage I get something arround 2000 (decimal) and it depends on resistors, but if I change the powering voltage from 2.8V to 3.6V it is always the same value. So ULP's AD converter doesn't work the same as main AD converters. But main ADs are not available at sleep.

Please, try that example, try to measure middle point inbetween 2 resistors each 10K connected in serie in between power line and GND. And then when you mesure that value, try to decrease imput voltage to 3V or 2.8V or to increase it to 3.6V. You will always get the same measured value.

djixon
Posts: 113
Joined: Sun Oct 01, 2023 7:48 pm

Re: How those voltage levels for brownout of ESP32S3 are defined if there is no internal voltage references implemented?

Postby djixon » Wed Apr 10, 2024 11:13 pm

Yeah, that's to be expected. As I stated before, the ESP32 has an internal 1.1V reference, so any voltage above that obviously saturates to 4095. You can turn on attenuation (not sure if you had), but even with the maximum attenuation of 12dB, your max voltage is 2450mV (ref).
Damn, I found Shotky diode SB560 gone into shortcircuit on my board which made voltage from battery line back into stabilizer circuit which should not happen. I replaced it and now everything work like a charm.

Here is modified assembly code to do what I expected. So, just take ulp_adc example from IDF, and replace content in assembler file adc.S in ULP subfolder by this content:

Code: Select all

/* ULP assembly files are passed through C preprocessor first, so include directives
   and C macros may be used in these files
 */
#include "soc/rtc_cntl_reg.h"
#include "soc/soc_ulp.h"
#include "example_config.h"

	.set adc_channel, EXAMPLE_ADC_CHANNEL

	/* Configure the number of ADC samples to average on each measurement.
	   For convenience, make it a power of 2. */
	.set adc_oversampling_factor_log, 2
	.set adc_oversampling_factor, (1 << adc_oversampling_factor_log)

	/* list of variables in .bss section (zero-initialized data) */
	.bss

	/* Low threshold of ADC reading. Set by the main program. */
	.global low_thr
low_thr:
	.long 0

	/* High threshold of ADC reading. Set by the main program. */
	.global high_thr
high_thr:
	.long 0

	/* Counter of measurements done */
	.global sample_counter
sample_counter:
	.long 0

	.global last_result
last_result:
	.long 0

	 .global is_main_cpu_wake  
is_main_cpu_wake:
     .long 0

	/* Code goes into .text section */
	.text
	.global entry
entry:
	/* increment sample counter */
	move r3, sample_counter
	ld r2, r3, 0
	add r2, r2, 1
	st r2, r3, 0

	/* do measurements using ADC */
	/* r0 will be used as accumulator */
	move r0, 0
	/* initialize the loop counter */
	stage_rst
measure:
	/* measure and add value to accumulator */
	adc r1, 0, adc_channel + 1
	add r0, r0, r1
	/* increment loop counter and check exit condition */
	stage_inc 1
	jumps measure, adc_oversampling_factor, lt

	/* divide accumulator by adc_oversampling_factor.
	   Since it is chosen as a power of two, use right shift */
	rsh r0, r0, adc_oversampling_factor_log
	/* averaged value is now in r0; store it into last_result */
	move r3, last_result
	st r0, r3, 0
    
	// test if main CPU is already in wake state
	// and just jump to halt if it is
    move r3, is_main_cpu_wake
	ld r3, r3, 0
	sub r3, r3, 1
    jump loop_end, eq

	//if it is not waken then
	// compare with high_thr; wake up if value > high_thr 

	move r3, high_thr
	ld r3, r3, 0
	sub r3, r3, r0
	jump wake_up, ov

	/* still below upper treshold */
	/* just continue measurement and do nothing else */
	.global loop_end
loop_end:
    // stop ULP program and wait until its timer restarts program at entry point
     halt

	.global wake_up
wake_up:
	// Check if the system can be woken up 
	READ_RTC_FIELD(RTC_CNTL_LOW_POWER_ST_REG, RTC_CNTL_RDY_FOR_WAKEUP)
	and r0, r0, 1
	jump wake_up, eq

	// Wake up the SoC
	wake
// next line is under comment because we need ULP running all the time
// allowing re-entering into sleep if voltage again drops below lower treshold level
// we also involwed new variable is_main_cpu_wake which allows us to skip
// re-entering into wake process if main cores are already waken

//    WRITE_RTC_FIELD(RTC_CNTL_STATE0_REG, RTC_CNTL_ULP_CP_SLP_TIMER_EN, 0)

    // here we know main cores are waken up by ULP
	// so we can set up that state in is_maincpu_wake variable
	// stored in RTC memory

	move r3, is_main_cpu_wake
	move r0, 1
	st r0, r3, 0
    // stop ULP program and wait until its timer restart program at entry point
    halt
I put comments in all crucial parts. Also, activate maximal attenuation (value 3, approx 12dB). I also made resistor divider: 2 x 10K so when power voltage is 3.3V that mid point is at 1.65V and AD measures value 1860. When voltage is 3.7V it measures 2120 when power voltage is 2.8V mid point is at 1.4V it measures 1230. So I made lower limit at 1300 whic will trigger the sleep state and upper limit to 1860 to trigger wake from sleep. So modified example_config.h file looks like:

Code: Select all

#pragma once

/* Ints are used here to be able to include the file in assembly as well */
#define EXAMPLE_ADC_CHANNEL     6 // ADC_CHANNEL_6, GPIO34 on ESP32, GPIO7 on ESP32-S3
#define EXAMPLE_ADC_UNIT        0 // ADC_UNIT_1
#define EXAMPLE_ADC_ATTEN       3 // we want half of power voltage (got by two identical 10K resistors) is measurable
                                  // so we need strongest attenuation approx 12dB which should give values approx 1860 on AD converter 
#define EXAMPLE_ADC_WIDTH       0 // ADC_BITWIDTH_DEFAULT means 12 bits AD

/* Set low and high thresholds */
#define EXAMPLE_ADC_LOW_TRESHOLD    1300
#define EXAMPLE_ADC_HIGH_TRESHOLD   1860 


Here is modified ulp_adc_example_main.c

Code: Select all

#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include "esp_sleep.h"
#include "soc/rtc_cntl_reg.h"
#include "soc/sens_reg.h"
#include "driver/gpio.h"
#include "driver/rtc_io.h"
#include "ulp.h"
#include "ulp_main.h"
#include "esp_adc/adc_oneshot.h"
#include "ulp/example_config.h"
#include "ulp_adc.h"
#include "esp_timer.h"

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

extern const uint8_t ulp_main_bin_start[] asm("_binary_ulp_main_bin_start");
extern const uint8_t ulp_main_bin_end[]   asm("_binary_ulp_main_bin_end");

static void init_ulp_program(void);
static void start_ulp_program(void);

static void periodic_timer_callback(void* arg)
{
    // monitoring routine which will activate deep sleep state
    // in the case the measured value is below lower treshold limit
    uint16_t cur_val = ulp_last_result & 0xFFFF;
    printf("current value at ULP AD %"PRIu16"\n", cur_val);
    if(cur_val<ulp_low_thr){
        printf("Entering to deep sleep\n");
        ESP_ERROR_CHECK( esp_sleep_enable_ulp_wakeup() );
        #if !CONFIG_IDF_TARGET_ESP32
         /* RTC peripheral power domain needs to be kept on to keep SAR ADC related configs during sleep */
         esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
        #endif
        // here we reset varibla which keeps track are the main CORES in sleep or not
        // that variable is set to value 1 in the case ULP has waken the main cores
        // however if ESP is regulary powered or powered from real reset that variable
        // has to be set at 1 in proper part of main routine before ULP program is started
        ulp_is_main_cpu_wake = 0;
        esp_deep_sleep_start();
    }
}

static void timer_config_and_install(void)
{
    const esp_timer_create_args_t periodic_timer_args = {
            .callback = &periodic_timer_callback,
            .name = "periodic"
    };
    esp_timer_handle_t periodic_timer;
    ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
    ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, 1000000));
}

void app_main(void)
{
    esp_sleep_wakeup_cause_t cause = esp_sleep_get_wakeup_cause();
    if (cause != ESP_SLEEP_WAKEUP_ULP) {
       // at this point, chip is powered by regular reset or real power up
       // so we need to initialize ulp program
       // also at this point RTC RAM is empty 
        printf("Not ULP wakeup\n");
        init_ulp_program();
        ESP_ERROR_CHECK( esp_sleep_enable_ulp_wakeup() );
        #if !CONFIG_IDF_TARGET_ESP32
         /* RTC peripheral power domain needs to be kept on to keep SAR ADC related configs during sleep */
         esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
        #endif
        ulp_is_main_cpu_wake = 1;
        start_ulp_program();
    } else {
       // at this point, we know the powering up is actually wake up from deep sleep
       // so do nothing about ULP program
       // it works all the time and variable stored in RTC memory are here all the time

        printf("Wakeup from deep sleep\n");
        printf("ULP did %"PRIu32" measurements since last reset\n", ulp_sample_counter & UINT16_MAX);
        printf("Thresholds:  low=%"PRIu32"  high=%"PRIu32"\n", ulp_low_thr, ulp_high_thr);
        ulp_last_result &= UINT16_MAX;
        printf("Value=%"PRIu32" was %s threshold\n", ulp_last_result,ulp_last_result < ulp_low_thr ? "below" : "above");
    }

    // here we initialize and start periodic timer
    // which will monitor results from ULP AD converter
   
    timer_config_and_install();

    while(1){
      // here is your main loop
    } 
}

static void init_ulp_program(void)
{
    esp_err_t err = ulp_load_binary(0, ulp_main_bin_start,
            (ulp_main_bin_end - ulp_main_bin_start) / sizeof(uint32_t));
    ESP_ERROR_CHECK(err);

    ulp_adc_cfg_t cfg = {
        .adc_n    = EXAMPLE_ADC_UNIT,
        .channel  = EXAMPLE_ADC_CHANNEL,
        .width    = EXAMPLE_ADC_WIDTH,
        .atten    = EXAMPLE_ADC_ATTEN,
        .ulp_mode = ADC_ULP_MODE_FSM,
    };

    ESP_ERROR_CHECK(ulp_adc_init(&cfg));

    ulp_low_thr = EXAMPLE_ADC_LOW_TRESHOLD;
    ulp_high_thr = EXAMPLE_ADC_HIGH_TRESHOLD;

    /* Set ULP wake up period to 1000000us = 1s */
    ulp_set_wakeup_period(0, 1000000);

#if CONFIG_IDF_TARGET_ESP32
    /* Disconnect GPIO12 and GPIO15 to remove current drain through
     * pullup/pulldown resistors on modules which have these (e.g. ESP32-WROVER)
     * GPIO12 may be pulled high to select flash voltage.
     */
    rtc_gpio_isolate(GPIO_NUM_12);
    rtc_gpio_isolate(GPIO_NUM_15);
#endif // CONFIG_IDF_TARGET_ESP32

    esp_deep_sleep_disable_rom_logging(); // suppress boot messages
}

static void start_ulp_program(void)
{
    /* Reset sample counter */
    ulp_sample_counter = 0;

    // program in ULP is modified such way
    // that once started it works in infinity loop all the time
    // regrdles the MAIN CORES are in deep sleep or not.
    // Since ULP consumes exrtremaly low current idea is too keep
    // it works all the time until real reset or real powering up happens
    esp_err_t err = ulp_run(&ulp_entry - RTC_SLOW_MEM);
    ESP_ERROR_CHECK(err);
}
For proper compilation you have to add esp_timer component into CMakeLists.txt in main folder so it looks like:

Code: Select all

idf_component_register(SRCS "ulp_adc_example_main.c"
                    INCLUDE_DIRS ""
                    REQUIRES soc nvs_flash ulp driver esp_adc esp_timer)
#
# ULP support additions to component CMakeLists.txt.
#
# 1. The ULP app name must be unique (if multiple components use ULP).
set(ulp_app_name ulp_${COMPONENT_NAME})
#
# 2. Specify all assembly source files.
#    Files should be placed into a separate directory (in this case, ulp/),
#    which should not be added to COMPONENT_SRCS.
set(ulp_s_sources "ulp/adc.S")
#
# 3. List all the component source files which include automatically
#    generated ULP export file, ${ulp_app_name}.h:
set(ulp_exp_dep_srcs "ulp_adc_example_main.c")
#
# 4. Call function to build ULP binary and embed in project using the argument
#    values above.
ulp_embed_binary(${ulp_app_name} "${ulp_s_sources}" "${ulp_exp_dep_srcs}")
In sdkconfig turn off all watchdog timers. (Search for WDT and disable all) and also disble hardware brown out reset function
Cheers
@ESP_Sprite Thank you a lot. I would not be able to find that dead diode without your insisting on that reference voltage.
Last edited by djixon on Thu Apr 11, 2024 6:06 am, edited 1 time in total.

ESP_Sprite
Posts: 9764
Joined: Thu Nov 26, 2015 4:08 am

Re: How those voltage levels for brownout of ESP32S3 are defined if there is no internal voltage references implemented?

Postby ESP_Sprite » Thu Apr 11, 2024 12:03 am

djixon wrote:
Wed Apr 10, 2024 11:13 pm
Damn, I found Shotky diode SB560 gone into shortcircuit on my board which made voltage from battery line back into stabilizer circuit which should not happen. I replaced it and now everything work like a charm.
That was unexpected, glad you found it, and thanks for posting well-commented, working code!

djixon
Posts: 113
Joined: Sun Oct 01, 2023 7:48 pm

Re: How those voltage levels for brownout of ESP32S3 are defined if there is no internal voltage references implemented?

Postby djixon » Thu Apr 11, 2024 5:55 am

Thank you buddy. I didn't have time to search in ESP documentation, is there a more elegant way for the ULP to test if main cores are at sleep or in regular working state?
As you can see in that code, I involved a new variable to handle that information and made its initialization to 1 when regular power cycle (or boot after reset) occur and also set to 1 by ULP after wake call. In timer cb that variable is reset to 0 just before calling the sleep function. But, I guess, there is more elegant way for ULP, probably by testing some bits in some register accesible to ULP, to "see" if main cores are running or in deep sleep state. In that case variable is_main_cores_wake is not required at all, and in assembly code at position where it tests if main cores are already waken such new bit testing could be performed instead.

Who is online

Users browsing this forum: No registered users and 146 guests