Universal sleep solution for different boards

Hi,

I'm thinking about a a solution to put boards to sleep with one dedicated 'sleep solution'. I'm hoping for some input on what could be a good approach.

The problem is:
-- I work with different boards (Arduino, Teensy, Pi Pico, ESP, etc.)
-- Both in Arduino C++ and Micropython
-- Figuring out (deep) sleep functionality is time consuming and sometimes not really possible.

My thought is:
-- Programming a suitable board to be a universal sleep module
-- This board should wake up every xx minutes on its own (no external interrupts)
-- The board powers ON the main board through a transistor when it wakes
-- The board powers OFF the main board through a transistor when it sleeps

Yes, there are drawbacks. But these might be overcome by implementing some very simple communication between the boards:
-- A 'going to sleep warning' by setting a interrupt pin low. (sleep module > main board)
-- A 'do not sleep' command by setting a pin high, (main board > sleep module)

TLDR:
An autonomous 'intelligent' and programmable 'power switch'

At the moment I'm looking into a Digispark Attiny 85

Suggestions and thought are welcome.

Thanks for reading :slight_smile:

The ESP32 already has

in the way of the ULP (Ultra Low Power) processor. The ULP works real well in shutting down the main processor and built in peripherals. Th ULP has available a variety of programmable wakeup options.

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/system/ulp.html?highlight=ulp

It is possible to program the ULP under the Arduino IDE using the GNU legacy thingy.

Power down is a universal option and saves the most energy.

Take a look at the Adafruit TPL5110 and TPL5111 low power timers, designed for just that purpose.

Thanks for your reply Idahowalker,

But this is actually what I'm trying to avoid. Diving into sleep modes for different processors, boards, firmwares etc. It's just too time consuming.

Yes, maybe this is an awesome solution for an ESP32, but what if I'm using an W5100S-EVB_Pico or any other board?

Then there is a problem.

Ok, thanks, it does come pretty close and might be usable. I'd have to look into into it further, but...

Basically, the TPL will turn on periodically, adjustable by 
potentiometer or resistor, and turn on your project's power. 
It will then wait until a signal is received from the project 
to tell the TPL that it can safely turn off the power. 
If the TPL does not receive a signal by the set time-out, 
it will reset the device like a watchdog timer.

Usage is easy. First, set your desired delay by adjusting 
the on-board trim pot: nearly-all-the-way-to-the-left is once per 100ms 
and all-the-way-to-the-right is once every 2 hours.

It doesn't seem to be very programmable at the turn on interval. That has to be set with a potmeter.
I mean, the turn on interval is between 100 ms and (2h60m60s*1000ms) 7.200.000 ms. Setting it to 15 minutes would be virtually impossible ?

Any thoughts on the Attiny 85 and transistor idea maybe?

If you mean 15.000 +/- 0.001 minutes, then perhaps, yes.

They are cheap, so try it out before making poorly informed decisions.

So set it to "who cares" and use another means of keeping track of exact times to execute, like an RTC.

Device wakes up processor every so often. Processor finally sees that 30 minutes are gone by and does whatever. Rinse and repeat.

a7

I think the ATTiny85 circuit would work. It would be always-on, but could sleep when the main load power is switched off. But you would have to come up with a system to notify the 85 when to turn off the power, and tell it when it should turn power back on. So you would either freeze all of that, or provide for some way to communicate between the two processors.

Another option is to use a coin-cell-powered RTC like the DS3231, which has an open-drain alarm output which could be used to control the main switching mosfet. If the input power line is less than 5.5V, it could do that directly, but for higher voltages an intermediate N-channel mosfet or NPN would be needed.

When the alarm time is reached, the alarm output will go low, and stay low, which causes the main processor to power up and do its thing. When it's finished, it turns on the Vcc line to the RTC, then uses I2C to set the next alarm time, and then as its last official act, it clears the alarm flag in the RTC, which causes the alarm output to go back to tristate, which turns off the power.

Rather than find a DS3231 library that works on every system you might ever use, it might be better to just develop raw C++ code to handle the RTC directly. But you would would still have to figure out how to use I2C on each system.

The only drawback I can see for the TPL5110 is having to set the wakeup period using a pot, which would be difficult to do if you need precise timing. And there's no flexibility once you start running. Here's Ralph Bacon's video on the TPL5110:

https://www.youtube.com/watch?v=Qms_iEL7Uqg

And here's a DS3231 circuit for controlling power:

Thanks for this info ShermanP

Yes, that is actually what I meant when I said, 'virtuallly impossible'. Sorry, should have been more specific there. I do want that specific timing. I'll keep it in mind but in this case it's a no-go.

The DS3231 is interesting, not only precise but it could also be set to wake up on whole hours for example. There are libraries for both Arduino and Micropython so that might work.

Ok, so pros and cons so far.

Attiny
Pros:
-- Might be a bit easier to implement
-- No libraries needed on main board
Cons:
-- Sleep/wake time has to be set at programming. Simple prevention of sleep can be achieved by setting a pin high from main board to sleep module
-- Sleep cylces are max 8 seconds(?) so multiple sleep cycles would be needed for longer sleep times

DS3231
Pros:
-- Can trigger at certain (real) times based on RTC
-- Can have long sleep periods
Cons:
-- Needs more 'complex' communications with the main board which might make it less universal

At this moment I'm leaning towards the DS3231 option.

And one more question about he Attiny. The sleep cycles can be max 8 seconds right?
If you do multiple sleep cycles, i read it wakes up for a few microseconds. So the main board would get a very short power pulse? Would that be a problem (in the long run) for microcontroller boards?

Thanks.

Why would it? Doesn't the Attiny get to decide whether to,power up the rest of the circuitry? Not just because it is awake, but because it is awake, decides and takes action to make it so.

a7

Ok, yes, I see your point.

I'm totally new to sleep routines and to me it's not clear yet to what extent pin states/variables etc. are retained when putting a uC to sleep.

I think I'm going to order both the DS3231 and a Digispark Attiny85 and start experimenting.

They all are. This is very well documented in the data sheets. But you seem to want to ignore processor differences and find a "universal solution", correct?

Sorry I'm not sure how to interpret your answer.

I don't want to ignore anything, I'm trying to learn and get some feedback.

I don't know exactly what the Attiny does/retains in sleep mode.
I'll order and go some learning by doing for now.

Thanks for your reply.

If you order the DS3231 on the standard module, which is the ZS-042 module, there will be a few modifications you'll need to make.

It retains pin states and the contents of all registers and RAM. As you can very easily determine, simply by consulting the data sheet.

However, my point was, for a "universal solution" to low power consumption, these details should not matter.

An ATmega328P might be better than an ATTiny for this, depending on exactly how much you want to shave power. -P devices are known as PicoPower, and have some extra power saving features that a normal ATtiny85 won't have. The most relevant one here would be an extra sleep mode, Power Save. This lets you run Timer2 in Asynchronous mode driven by a 32kHz clock crystal. There are a couple advantages to this:

Accuracy: An ATtiny would have to use the Watchdog timer to time it's waking from deep sleep. This is an internal RC oscillator and its frequency only has a tolerance of +/- 10%, abysmal. Clocking off a watch crystal will give you much better accuracy.

Power consumption: An ATtiny in Power Down with the Watchdog timer running will consume 4-6 uA. A Mega328P in Power Save mode with watch crystal will draw about 1 uA.

So it seems seem some modification to the DS3231 are needed(?):
-- Disabeling the battery charging when having both a battery and external power supply.
-- Disabling a LED indicator
-- And for even lower power: DS3231 RTC on ZS-042 module - if low power is important

I think that at this point an Attiny or maybe even better an ATMega328P on an Arduino Pro Mini is what I'm going to explore. The RTC based wake up would be nice but it's not necessary. I think the stand alone uC will be more universal (no i2c between the boards).

Also, I'm not aiming for ultra low power, anything below 5 mA would be just fine. To put that into perspective, if I run my Teensy webserver/datalogger it's at about 120 mA

I'll order a Pro Mini and an Attiny and explore the options.
Thanks for all the feedback so far!

If I have a decent result I'll post back here.

[EDIT]
I found a work-around that does what I need, see next reply
[/EDIT]

It's been a while but I have a result. It's not perfect though.

I'm using an Arduino Pro Mini and the Narcoleptic library. I removed the power regulator and power LED from the Mini.

By using a cycle and awake time I can power my "external system" up and down at fairly precise intervals. This drastically reduces the average power consumption. See images below for results.

So it seems to be working just fine like this. However as you can see in my code below I'm setting a cycle start time:
cycle_starttime = millis() + Narcoleptic.millis();
The Arduino millis increment during awake time and narcoleptic millis are calculated by the library based on the sleep time. So when adding these toghether the result is close to millis in real time.

The thing is that (first) 'millis() + Narcoleptic.millis()' wil be greater then 4294967295 at some point and a rollover will occur. At some point millis() and Narcoleptic.millis() will also rollover.

I have a strong feeling this is going to seriously mess things up.

To avoid rollovers altogether I tried to invoke a watchdog timeout by:

  if(millis() + Narcoleptic.millis() > 100000)        // 24*60*60*1000 = 86 400 000 for 24 hours
  { 
    //wdt_disable();                   // Disabling and delaying 
    //delay(3000);                     // doesn't work either
    wdt_enable(WDTO_1S);
    while(1){};
  }

So my questions at this point:
-- Can I even use the watchdog timer, might there be a conflict with the Narcoleptic library?
-- If I can use the WDT, how?
-- If I can't use the WDT, what could be another approach in this case?

Thanks for reading!

Code:

#include <avr/wdt.h>
#include <Narcoleptic.h>

#define led 13                          // Onboard LED
#define external_system_power 2         // Pin connected to a transistor that switches external system on/off
#define external_system_signal 3        // As long as the external system keeps this pin high the module will not go into sleep mode

//===============================
// CYCLE AND WAKE TIME
const int cycle_time_min = 1;           // Cycle time in minutes, system will be awake at every start of new cycle
const int awake_time_sec = 20;          // Time in seconds that the system will be awake at the start of a new cycle

// Cycle time correction:
// There is a +4.8333% error on the cycle time, so if 100 sec is calculated, the cycle takes 104.8 seconds.
// So the correction factor is 100 / 104.8333 = 0,95389537484749597694625658068572
// The error seems to depend on temperature, +4.8333% @ 22°C and probably supply voltage(?)
// The lower the temperature, the smaller the error. Not significant enough to program for

const unsigned long cycle_time_ms = 1000UL * 60 * cycle_time_min * 0.95389537;        // Cycle time in milliseconds, addition UL needed to calculate as unsigned long
const unsigned long awake_time_ms = 1000UL * awake_time_sec;                          // Awake time in milliseconds, no correction needed: awake is time relatively short
unsigned long cycle_starttime;                                                        // Marks the start time of a new cycle in milliseconds


void setup()
{
  Narcoleptic.disableTimer1();
  Narcoleptic.disableTimer2();                  
  Narcoleptic.disableWire();
  Narcoleptic.disableSPI();
                       
  ADCSRA &= ~(1<<ADEN);                         // Disable ADC
  ACSR = (1<<ACD);                              // Disable the analog comparator
  
  // ATmega48/88/168/328
  DIDR0 = B00111111;                            // Disable digital input buffers on all ADC0-ADC5 pins
  DIDR1 = (1<<AIN1D)|(1<<AIN0D);                // Disable digital input buffer on AIN1/0

  pinMode(external_system_power, OUTPUT);       // Switches external power via transistor
  digitalWrite(external_system_power, 0);
  pinMode(external_system_signal, INPUT);       // Reads stay awake signal from external system

  pinMode(led, OUTPUT);
  digitalWrite(led,LOW);  
}

void loop()
{  
  cycle_starttime = millis() + Narcoleptic.millis();                                  // Mark the starttime of the cycle

  // Wake ========================================================================
  
  digitalWrite(led, 1);                                                               // Indicate power ON
  digitalWrite(external_system_power, 1);                                             // Switch ON external system
  
  while(   ((millis() + Narcoleptic.millis()) - cycle_starttime) < awake_time_ms      // Stay awake for the awake time
           || digitalRead(external_system_signal))                                    // OR Stay awake as long as external_system_signal is high
  {
    // Stay awake
  }

  digitalWrite(led, 0);                                                               // Indicate power OFF
  digitalWrite(external_system_power, 0);                                             // Switch OFF external system


  // Sleep =======================================================================
  
  while( ((millis() + Narcoleptic.millis()) - cycle_starttime) < cycle_time_ms )      // Sleep until the end of the cycle time
  {
    Narcoleptic.delay(1000);
  }
  
} // End loop

Ok, I found a work-around / solution.

I'll post some more info later ok.