How to disable WiFi from GPIO ISR for Brownout?

NotMyRealName
Posts: 41
Joined: Thu Feb 13, 2020 1:35 am

How to disable WiFi from GPIO ISR for Brownout?

Postby NotMyRealName » Wed Mar 01, 2023 10:11 pm

Hi Everyone,

I'm trying to write a brownout handler routine for my commercial project (using ESP32-WROVER IE) and I'm wondering if anyone can help me figure out how to do this. I am using internal SPIFAT, NVS, and SD FAT. I also have a second MCU on the board that is relying on the same supply being up for a similar routine.

I've written an intermediate layer on all my file IOs so that I can prevent all my code from accessing the files after the brownout is triggered. When brownout is triggered by the external comparator they should only finish the current operation.

The idea is that the circuit has enough bulk energy storage to finish any pending operations and exit cleanly. When everything goes as expected my GPIO brownout task drops clock speeds, closes files etc and I get >800ms of time (I need ~400ms). The problem is that flash operations on the internal SPI chip are slow and block all non IRAM code, including the brownout task. I've been through the menuconfig and setup the options to lower the latency/duration on the internal flash ops as much as possible already (yield during erase, and force sector erase etc.). The delay in processing the brownout uses too much power.


I want to do the following:

GPIO ISR handler in IRAM on Brownout input:
Immediately drop CPU speed and disable wifi. Don't care about clean deinit, just power consumption.

Very high priority brownout task:
Close open files etc and then go to sleep until hardware supervisor resets circuit.

I'm stuck on not understanding the low level details of how to reduce the power consumption of the wifi from that ISR in IRAM.
esp_wifi_stop() takes a long time to complete I think and also not sure if it is ISR or IRAM safe.

Should I be looking at lower layers of code? Should I be writing to registers to force things to disable and can I kill the WiFi task?

Any ideas or lessons from someone else doing the same thing would be appreciated.

Thanks.

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

Re: How to disable WiFi from GPIO ISR for Brownout?

Postby ESP_Sprite » Thu Mar 02, 2023 12:48 am

There's very few 'big' things you can safely do from an ISR, and I don't think stopping WiFi is one of them. As you presumably already have that high-prio task that gets unblocked from the ISR, it's better to put that call in there. I understand the issue with waiting for a flash write to finish, but I'm not sure if there's a proper solution to that.

NotMyRealName
Posts: 41
Joined: Thu Feb 13, 2020 1:35 am

Re: How to disable WiFi from GPIO ISR for Brownout?

Postby NotMyRealName » Fri Mar 03, 2023 12:14 am

Thanks for your input ESP_Sprite,

Still keen to find a way if anyone else knows of anything.

I'm going to update a little bit about what I have found here in case someone ends up following in my footsteps later on.

I'm using

Code: Select all

esp_wifi_set_mode(WIFI_MODE_NULL);
to turn off the wifi now in my high priority task. It seems quicker than esp_wifi_stop.

Also, with regard to CONFIG_SPI_FLASH_ERASE_YIELD_TICKS, it is in the docs but I missed the fact that this is a yield on a per-task basis. That means that if two tasks are erasing the flash they will interleave and can do a flash operation during the yield time of the other task. This is great for keeping up erase performance, but it does mean that if your yield time is less than your erase time you can end up totally locked up again and your two erase tasks can use all of the CPU cycles. I'm going to have to look at a mutex or something here to prevent simultaneous operations. Ideally it would be great to have a menuconfig option to make the yield global via a mutex so that you could be assured that a minimum number of ticks would be free every sector erase. If you had two tasks, all the normal priority mechanisms would still work as well to determine who had priority.

I've attached a snapshot of my test setup. I have the logic analyzer on a GPIO pin. A high priority task is pulsing the pin every millisecond. Two competing tasks with lower priority are both trying to erase flash at the same time. One is accessing NVS, the other SPIFAT. NOTE: I am reading between the lines here a little bit as I don't actually have an indication of which task is running.

EDIT: These options are from my sdkconfig file for the attached test:

Code: Select all

CONFIG_SPI_FLASH_BYPASS_BLOCK_ERASE=y
CONFIG_SPI_FLASH_YIELD_DURING_ERASE=y
CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS=5
CONFIG_SPI_FLASH_ERASE_YIELD_TICKS=100
CONFIG_SPI_FLASH_WRITE_CHUNK_SIZE=512
Attachments
Flash erase yield behaviour.png
Flash erase yield behaviour.png (18.66 KiB) Viewed 2341 times

NotMyRealName
Posts: 41
Joined: Thu Feb 13, 2020 1:35 am

Re: How to disable WiFi from GPIO ISR for Brownout?

Postby NotMyRealName » Mon Mar 06, 2023 10:43 pm

So after going through the SPI driver I have come to the conclusion that the scenario above (multiple tasks pending on flash operations) wasn't really considered when it was written.

I've come up with my own derivative implementation of the yield function. (Original Espressif version here: https://github.com/espressif/esp-idf/bl ... func_app.c)

I'll attach some snippets here with my changes for reference, but I'm somewhat out of my comfort zone so evaluate before using for your own purposes. I've used critical sections to protect the state tracking variables for the yield timer. I'm not sure if that is the correct solution. So far it seems to help, and I don't get multiple erase operations without a yield period even with multiple tasks. If anyone else comes up with something better, or can spot an issue, please let me know! :)

Here are my changes:

I have added a spinlock and an acquired state tracking variable to the spi arguments:

Code: Select all

typedef struct {
    app_func_arg_t common_arg; //shared args, must be the first item
    bool no_protect;    //to decide whether to check protected region (for the main chip) or not.
    uint32_t acquired_since_us;    // Time since last explicit yield()
    uint32_t released_since_us;    // Time since last end() (implicit yield)
    uint32_t acquired;             // Track the acquired/released state
} spi1_app_func_arg_t;
static portMUX_TYPE DRAM_ATTR yield_spinlock = portMUX_INITIALIZER_UNLOCKED;        // Spin lock to make accessing spi1 args threadsafe.
I have changed the Yield function to re-asses if another yield is required after the yield finishes:

Code: Select all

static IRAM_ATTR esp_err_t spi1_flash_os_yield(void *arg, uint32_t* out_status)
{
    spi1_app_func_arg_t * ctx = (spi1_app_func_arg_t *)arg;
    if (likely(xTaskGetSchedulerState() == taskSCHEDULER_RUNNING)) {
#ifdef CONFIG_SPI_FLASH_ERASE_YIELD_TICKS
        do{
            int32_t delay;
            uint32_t time = esp_system_get_time();
            // Spin lock may not be required as reading U32 should be atomic.
            // Will get issues if not aligned correctly etc. I think
            portENTER_CRITICAL_ISR(&yield_spinlock);            
            delay = (time - ctx->released_since_us);
            portEXIT_CRITICAL_ISR(&yield_spinlock);
            
            // Work out the remaining delay time.
            delay = (CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS) - (delay/1000);
            // Limit it 
            if(delay > CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS){
                delay = CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS;
            }
            else if (delay <= 0){
                delay = 1;
            }
            vTaskDelay(delay * portTICK_PERIOD_MS);
        }while(on_spi1_check_yield(ctx));       // Check again to make sure another yield is not required.
#else
        vTaskDelay(1);
#endif
    }
    on_spi1_yielded(ctx);
    return ESP_OK;
}
And changed the yield check function to track the acquired/released state

Code: Select all

static inline IRAM_ATTR bool on_spi1_check_yield(spi1_app_func_arg_t* ctx)
{
#ifdef CONFIG_SPI_FLASH_YIELD_DURING_ERASE
    uint32_t time = esp_system_get_time();
    bool ret = true;

    portENTER_CRITICAL_ISR(&yield_spinlock);
    if(ctx->acquired){
        // SPI has been acquired by a task already for flash write/erase ops.       
        if ((time - ctx->acquired_since_us) >= (CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS * 1000)) {
            // Bus has been acquired for too long and it is time to release the bus.
            ctx->acquired = 0;                              
            ctx->released_since_us = time;
        }
    }
    else if ((time - ctx->released_since_us) >= (CONFIG_SPI_FLASH_ERASE_YIELD_TICKS * portTICK_PERIOD_MS * 1000)) {
        // SPI is available and enough time has passed to allow us to perform the next write/erase operation.
        ctx->acquired = 1;                  // Set acquired flag
        ctx->acquired_since_us = time;
        ret = false;                        // Don't need to yield. Bus will be acquired by caller.
    }
    portEXIT_CRITICAL_ISR(&yield_spinlock);
    return ret;
#else
    return false;
#endif

}
static inline IRAM_ATTR void on_spi1_released(spi1_app_func_arg_t* ctx)
{
#ifdef CONFIG_SPI_FLASH_YIELD_DURING_ERASE
    uint32_t time = esp_system_get_time();
    portENTER_CRITICAL_ISR(&yield_spinlock);
    if(ctx->acquired){
        // If the operation was a long operation but only called the yield function once It may still need a yield at the next operation.		
        if ((time - ctx->acquired_since_us) >= (CONFIG_SPI_FLASH_ERASE_YIELD_DURATION_MS * 1000)) {
            // Bus is released now. The next task needs to know when this one finished.   
            // NOTE: Resetting this every time it is released will make all writes/erases,
            //	  even short ones, trigger a full yield.             
            ctx->released_since_us = time;
        }
        ctx->acquired = 0;
    }
    portEXIT_CRITICAL_ISR(&yield_spinlock);
#endif
}

static inline IRAM_ATTR void on_spi1_acquired(spi1_app_func_arg_t* ctx)
{
    // Ideally, when the time after `on_spi1_released()` before this function is called is larger
    // than CONFIG_SPI_FLASH_ERASE_YIELD_TICKS, the acquired time should be reset. We assume the
    // time after `on_spi1_check_yield()` before this function is so short that we can do the reset
    // in that function instead.
}

static inline IRAM_ATTR void on_spi1_yielded(spi1_app_func_arg_t* ctx)
{
    // Not needed anymore. on_spi1_check_yield() is now called from the spi1_flash_os_yield() function above.
}

NotMyRealName
Posts: 41
Joined: Thu Feb 13, 2020 1:35 am

Re: How to disable WiFi from GPIO ISR for Brownout?

Postby NotMyRealName » Mon Mar 13, 2023 4:11 am

I've done a lot of work on this in the last week and I've come to the conclusion that I can't rely on any of the normal methods for disabling WiFi even from a high priority task rather than the ISR. Unless I am missing something obvious...

The reason for this is they all seem to pend on synchronization objects of some kind or another and will block the task for a while in some situations. Sometimes it works, and sometimes I get everything corrupted.

For one test case I was seeing anywhere from 330ms and 700ms of brownout time. I was trying to use every combination I could think of using the following functions but they all seem to block at some point.

Code: Select all

    esp_wifi_disconnect();
    esp_wifi_set_mode(WIFI_MODE_NULL);
    esp_wifi_stop();
   esp_wifi_deinit();
Against my better judgement, I'm now using this. It is super consistent and I get a nice 590ms +/- maybe 20ms.

Code: Select all

esp_wifi_power_domain_off();
However, this has to be included from the private header (esp_private/wifi.h). That same header carries the following disclaimer:

Code: Select all

/*
 * All the APIs declared here are internal only APIs, it can only be used by
 * espressif internal modules, such as SSC, LWIP, TCPIP adapter etc, espressif
 * customers are not recommended to use them.
 *
 * If someone really want to use specified APIs declared in here, please contact
 * espressif AE/developer to make sure you know the limitations or risk of
 * the API, otherwise you may get unexpected behavior!!!
 *
 */
Is this a bad idea? I can't even find source code for this. I assume it's part of the precompiled WiFi blob? Does anyone know who could confirm my usage scenario of this?

Who is online

Users browsing this forum: MicroController and 72 guests