You can support me by reading this article on Medium

What Do I Mean by Low Power?

I was involved in an art project where we wanted to use a smallish (10 x 15cm) solar panel to power an e-paper display inside a gallery, controlled by an ESP32 micro controller.

This would be a pretty trivial job outdoors: the panel has 5W peak power and I could just put a small DC-DC converter on it to get stable 3.3V power from it during normal daylight.

Indoors, the game changes completely, due to two main issues: low illuminance and low solar panel efficiency at low illuminance.

Outdoors, the illuminance will be anywhere from 1000 to 100000lux during the day. On the other hand, normal indoor lighting would normally range from 100 to 500lux. This alone reduces the available power from the solar cell by a factor of 200 or more. 5W turns into 25mW.

But there is another problem: The solar panel efficiency drops at low light. At full illuminance, the total power conversion efficiency of a modern solar panel can exceed 20%, but the same panel will have a much lower efficiency at low light due to interior current leakage. The expected 25mW turns into 5 or 10mW at most with normal indoor lighting.

To put this in perspective: ESP32 will consume somewhere around 900 mW transferring data via WiFi and some 20 to 70 mW with radio off. See this helpful article.

So is there no way to power the ESP32 using our solar panel?

The Solution

Our project involves refreshing an e-paper display at certain intervals and e-paper consumes power only during the refresh (drawing). So the solution is to somehow store the miniscule amount of available energy until there is enough to draw the next picture, turn everything on, draw the picture, save the data for the next one, and power off.

The Hardware

On the hardware side, I achieved this using a solar harvester chip AEM10941 evaluation board that very efficiently charges a single super-capacitor. This super-capacitor in turn powers a low-power, low-voltage boost converter and this powers the ESP32.

The Power Harvesting System
The Power Harvesting System

The Software

But the hardware alone is not the solution yet: I needed a way to turn off everything so the harvester has the chance to charge the capacitor up, do the drawing only when the capacitor is full and then turn off everything again until there is enough power available.

ESP32 Deep Sleep

The solution is to implement the deep sleep on the ESP32. While in deep sleep, the ESP32 chip only consumes 10μA, but it powers off almost everything, leaving only a small portion of slow RTC RAM and a counter that periodically wakes it up.

So how does it all work?

  1. The system spends most of the time in deep sleep, where the combined power usage of the power harvesting system, converter and ESP32 never exceeds about 150μW.

  2. Every 5 or 10 seconds it wakes up for a fraction of a second to measure the capacitor voltage:

    2a. If the voltage is not high enough, it goes to deep sleep again.

    2b. If the voltage is high enough, it draws the picture, stores the sequence number into the RTC RAM and goes to deep sleep again.

  3. Rinse and repeat.

Pick the Right ESP32 Board!

There is a possible pitfall here: Some ESP32 development boards, like the basic ESP32 Dev Module, power additional circuitry (level converters, USB-to-RS232 chips etc.) from the 3.3V rail. This means they will always use at least 10mW of power, regardless of the deep-sleep mode, which is useless for our use case. I had really good experience with the Olimex ESP32-DevKit-LiPo boards in this respect, but there are other well built development boards out there.

There Are Always Unintended Consequences

Remember how the chip powers off almost everything when in deep sleep? All the regular RAM contents are lost too, only the sequence number in RTC RAM remains. So when I try to draw the next picture, the system doesn’t know a picture was already drawn previously and simply draws over it. The results are really ugly:

Video of the flawed drawing

I spent way too much time thinking how to fix this…

One option would be to use a full refresh, but that would entail the annoying blinking of the whole display. Then I thought about adapting the library and storing some of it’s variables in the RTC RAM. This seemed like a sure way to lose all my remaining hair, though.

After a lot of thinking, I finally thought of a nice hack. The system doesn’t know what was displayed previously? I should draw the exact same picture as the last time to “refresh it’s memory” and right afterwards draw the new one.

This worked beautifully:

Video of the corrected drawing

Conclusion

Super-capacitors are a wonderful way to power small devices that work at intervals when there is not much power available. They are preferable to batteries due to their much better cycling ability. I like things that you can put on a wall and expect them to work for years and years instead of becoming electronic waste soon.

And if you use deep sleep with ESP32, always keep in mind the lost RAM contents when the chip wakes up again.

The Code

#include <GxEPD2_BW.h>

#include "GxEPD2_display_selection_new_style.h"

#include "bitmaps/images.h"
#include <pgmspace.h>

#include <driver/adc.h>
#include <esp_sleep.h>

RTC_DATA_ATTR int pic_index = 0;
const int adcPin = 34; // GPIO34 (ADC1_CH6)
const int sleepTimeSeconds = 3; // Sleep time in seconds
const int array_size = sizeof(images)/sizeof(images[0]);
const int refresh_size = 30;

void setup()
{
  // Configure ADC
  adc1_config_width(ADC_WIDTH_BIT_12);
  adc1_config_channel_atten(ADC1_CHANNEL_6, ADC_ATTEN_DB_11);
  
  // Read the voltage on pin 34
  int adcValue = adc1_get_raw(ADC1_CHANNEL_6);
  float voltage = adcValue * (3.3 / 4095.0); // Convert ADC reading to voltage

  if (voltage>2.45) { // Only if voltage is high enough
    if (pic_index%refresh_size == 0 || pic_index%array_size == 0) {
      display.init(0, true, 2, true); // first update should be full refresh
    } else {
      display.init(0, false, 2, true); // following updates should be normal refresh + draw previous picture to avoid artifacts
      display.drawImage(images[(pic_index-1)%array_size], 0, 0, 400, 300, false, false, true);
    }

    display.drawImage(images[pic_index%array_size], 0, 0, 400, 300, false, false, true);
  
    display.hibernate(); //Turn off voltage converters inside the display to save power

    pic_index++;
  }

  // Configure the ESP32 to wake up after a fixed delay
  esp_sleep_enable_timer_wakeup(sleepTimeSeconds * 1000000);
  esp_deep_sleep_start();
}

void loop()
{
}

If you found this article useful, you can buy me a beer here.