Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add Sleep-with-millis and power-saving library, understand & document power-saving modes #158

Open
SpenceKonde opened this issue Apr 2, 2020 · 30 comments
Assignees
Labels
DxCore Too This issue also impacts DxCore and probably others. enhancement New feature or request

Comments

@SpenceKonde
Copy link
Owner

This library will provide the following functionality:

  • Allow sleep where the RTC is used to keep time in standby sleep, and adjust millis() time upon wake, when using any timer except RTC as the timing source.
  • Provide a wrapper around sleep when RTC is used to keep time so that user does not have to manage re-sleeping when the ISR fires to update millis()

This will also coincide with testing to better understand sleep modes on the tinyAVR 0-series and 1-series parts.

@SpenceKonde
Copy link
Owner Author

Interesting observation in #264 -

Also SLEEP_MODE_PWR_DOWN does not work (with the fore-mentioned sketch)!

Should be investigated here...

@rolland-fx
Copy link

rolland-fx commented Dec 20, 2020

Interesting observation in #264 -

Also SLEEP_MODE_PWR_DOWN does not work (with the fore-mentioned sketch)!

Should be investigated here...

Using 2.1.5, SLEEP_MODE_PWR_DOWN seems to be actually SLEEP_MODE_STANDBY from a power point of view.
No sleep, Idle sleep and stanbdy sleep work as intendeed, but Power Down does not seems to achieve the targeted sleep.
In my tests, SLEEP_MODE_PWR_DOWN used to be +/- 6uA on my board. But is now +/- 150uA (at 3.3v), which is what i used to have with SLEEP_MODE_STANDBY .

EDIT :
looks like it's due to a parasitic capacitance on my board, not sure why it was only impacting SLEEP_MODE_PWR_DOWN.

@SpenceKonde SpenceKonde changed the title add megaTinySleep library add megaTinySleep library, understand & document power-saving modes Dec 23, 2020
@ArnieO
Copy link

ArnieO commented Feb 7, 2021

Thank you for your great work!
While we're all eagerly waiting for the megaTinySleep library, I am working on a board design using ATtiny1616, where I want the controller to periodically wake up from sleep, do some measurements etc - and go back to sleep.
But I also want to enable external wakeup if a relay input is triggered.
I have found out how to do those things separately, but have not gotten my head around how to get the ATtiny161 to do both. I think what is confusing me is the ISR that seems to need different input parameters for each case, and I have not seen a way to set up two ISRs in parallel.
Any hint on how this might be done?
I could of course add an external RTC chip, but with such a powerful microcontroller with a RTC sitting there ...
Any guidance would be highly appreciated!

@freemovers
Copy link
Collaborator

freemovers commented Feb 7, 2021 via email

@SpenceKonde SpenceKonde changed the title add megaTinySleep library, understand & document power-saving modes add Sleep-with-millis and power-saving library, understand & document power-saving modes Feb 8, 2021
@SpenceKonde
Copy link
Owner Author

@ArnieO - For now I think @freemovers is the guy you want to be asking about this; I have yet to have time to put one of these into sleep mode! I do think that this sounds like it just calls for a linear combination of your two pieces of code...?

@ArnieO
Copy link

ArnieO commented Feb 8, 2021

@freemovers : Thank you for your rapid response!
This will be my first application with an ATtiny microcontroller, and I prefer to work in Arduino IDE. Compared to interrupts on other microcontrollers I have some experience with, the ISR syntax for ATtiny confuses me. I have spent a few hours searching for overview information, but have only found bits and pieces. I have not read the datasheet in detail (it is 589 pages...) - and the sections relating to interrupts seem to require basic knowledge on how this is handled.

My confusion:
In Arduino-style coding several ISRs can be defined, each activated by a call to attachInterrupt().
The ISR examples I have seen for ATtiny use one ISR(<register?>) function call, and it looks like the ISR function must always have this name.

So it could well be that my issue is just lack of competence on the coding of this controller family:

  • Is it possible to define multiple ISR(xxx) functions in the same code? Your linked code indicates that the answer is YES - which is already a great indication. If you also have Arduino examples available that would surely be a bonus!
  • How is priority between those different ISRs handled?

@SpenceKonde : I thought so too until I started looking at code examples. @freemovers will surely be able to provide guidance!

@SpenceKonde
Copy link
Owner Author

SpenceKonde commented Feb 8, 2021

attachInterrupt() is an ABOMINATION
It is one of the most vile pieces of code included with the Arduino IDE... and the competition in that space is very tough, let me tell you! All of the third party cores do have it. I would love to cut that cancer out of my cores, but too many people are used to it, and too many libraries depend on it.

Under the hood, all it does is store function pointers - then it's got a row of `ISR(PORTA_PORT_vect)) - well, they use a macro to generate the muiltiple near identical interrupts - but that's basically what it is- a bunch of ISR's that have a loop that runs 8 times, checking each bit of the int flags inturnm and if that is set and there is a function pointer corresponding to it, call that fnction.

You use ONE attachInterrupt. On ONE port - bam. Every single pin interrupt is now claimed by WInterrupt.c and you'll get a multiple definition error if you try to define your own ISR the normal way (with ISR(PERIPHERAL_INTNAME_vect) ...).
And the overhead of it having to be able to handle a separate function for each pin slows it down, too (and you generally want an ISR to run as close to instantly as possible and get back to whatever it interrupted.

Each chip has limited number of interrupts. Somewhere I think in extras there's a .md file with a list of all the vector names used on 0/1-series
Each vector can have one function assigned to it with ISR() though you can have it do more than one thing, depending on the "flags" For interrupts on a pin, you get one vector per "port" (ie, PORTA has one, PORTB has one, and so on). There are often sensible ways to group things "any of these pins should wake it and turn on backlight") to get some added efficiency - plus you leave the interrupts you're not using available for later use.

I've actually been thinking about whether there is a way that I could make it only generate the ISR for ports that get an interrupt function attached to them.... that would at least take it from apocalyptically bad to only moderately bad. The pin would have to be known at compiletime, but you generally aren't deciding what pin is connected to the interrupt source at runtime...

@SpenceKonde
Copy link
Owner Author

SpenceKonde commented Feb 8, 2021

Oh, and no priority, generally speaking, first come first served - interrupts can't interrupt other interrrupts except that one interrupts can be marked as high priority, if it is, that can interrupt. In a situation where there are multiple interrupts simultaneously wanting to fire(generally, interrupts were disabled globally, or it was in an interrupt, it is done, interrupts reenabled and all these pending interrupts want to fire - this goes in a fixed numerical priorirty, but you can set a bit to make it do round-robin (IMO if you're considering that, either, you are paranoid and don't need it , or your code is ending up in a horrible position because the interrupts run too slowly and fire too often and like, maybe that's the thing to be fixing?).

@ArnieO
Copy link

ArnieO commented Feb 8, 2021

attachInterrupt() is an ABOMINATION
It is one of the most vile pieces of code included with the Arduino IDE... and the competition in that space is very tough, let me tell you!

😆

Thank you for this great "ATtiny interrupts for dummies" writeup, @SpenceKonde - very helpful and very much appreciated!

And I promise not to attempt using attachInterrupt() in my ATtiny project. 😉

@freemovers
Copy link
Collaborator

freemovers commented Feb 8, 2021

Here is some basic code that uses the Periodic Interrupt Timer (PIT) and a PORT interrupt. The timer is triggered every 4 seconds to turn off the LED while the PORT interrupt is used to turn on the LED. PIT and PORT peripherals both work in Power Down sleep mode.

#include <avr/sleep.h>

void RTC_init(void)
{
  RTC.CLKSEL = RTC_CLKSEL_INT1K_gc;         // 1kHz Internal Crystal Oscillator (INT1K)
  while (RTC.STATUS > 0 || RTC.PITSTATUS);  // Wait for all register to be synchronized
  RTC.PITINTCTRL = RTC_PI_bm;               // Periodic Interrupt: enabled
  RTC.PITCTRLA = RTC_PERIOD_CYC4096_gc      // 1024 / 4096 = 1/4Hz, 4 sec
  | RTC_PITEN_bm;                           // Enable: enabled PIT
}

// Wake up routines
ISR(RTC_PIT_vect)
{
  RTC.PITINTFLAGS = RTC_PI_bm;        // Clear flag
  digitalWrite(2, LOW);
}

ISR(PORTA_PORT_vect)
{
  PORTA.INTFLAGS = PIN2_bm;       // clear interrupt flag  
  digitalWrite(2, HIGH);
}

void setup() {
  RTC_init();
  pinMode(2, OUTPUT);
  PORTA.PIN2CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc;  // digital pin #5 on 412
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);   // Set sleep mode to POWER DOWN mode
  sleep_enable();
  interrupts();
}

void loop() {
  // put your main code here, to run repeatedly:
  sleep_cpu();
}

@ArnieO
Copy link

ArnieO commented Feb 9, 2021

Thanks a lot @freemovers, this is very helpful!
Your code works - and I also managed to move the interrupt to an other pin after having read up a bit on the basics of port manipulation.

@ArnieO
Copy link

ArnieO commented Feb 22, 2021

@freemovers

Your example here above use RTC_PIT_vect for timed sleep. So the RTC must be enabled in Arduino IDE submenu.
The consequence seems to be that micros() is not available -- which I need in my design for controlling a neopixel.

Do you see a way out; can timed sleep be done with an other counter?

@freemovers
Copy link
Collaborator

I have not used the RTC for millis() before (the option in the Arduino Menu), but I just use the interrupt above. You can still use the default timer for millis() and micros(). The RTC is enabled in the example above as follows (not from the Arduino menu):

  RTC.PITCTRLA = RTC_PERIOD_CYC4096_gc      // 1024 / 4096 = 1/4Hz, 4 sec
  | RTC_PITEN_bm;                           // Enable: enabled PIT

Keep in mind that you cannot use the power down sleep mode since that will turn off the timer (TCA or TCD) that controls the millis() and micros(). These timers will only work in Idle Sleep mode:

set_sleep_mode(SLEEP_MODE_IDLE);

I'm not sure how often you change the neopixel, but you could update the neopixel while the MCU is running, and put it back in sleep mode after the neopixel is updated.

@SpenceKonde
Copy link
Owner Author

SpenceKonde commented Feb 22, 2021 via email

@ArnieO
Copy link

ArnieO commented Feb 22, 2021

Thank you both for pointing me in the right direction.

And indeed - I got it working now!

@freemovers
Copy link
Collaborator

@SpenceKonde, most peripherals can run in standby mode, but looks like the TCA and TCD are limited to idle sleep:
image

@SpenceKonde
Copy link
Owner Author

Ooo, I see what you meant.

Right.
But why the hell do you need micros for neopixels?! o_o

@ArnieO
Copy link

ArnieO commented Feb 22, 2021

But why the hell do you need micros for neopixels?! o_o
I have not found yet a neopixel library that compiles without having micros() available, including this one: https://github.com/SpenceKonde/megaTinyCore/blob/master/megaavr/extras/tinyNeoPixel.md 😉
Error message if I disable millis()/micros():
...\2.2.6\libraries\tinyNeoPixel_Static/tinyNeoPixel_Static.h:297: undefined reference to micros'`
It compiles after having enabled millis()/micros().

An other point: It looks like millis() stops running while in SLEEP_MODE_STANDBY.
Just an observation, not an issue for my current application.
My current settings are:
image

EDIT
I think @freemovers answered this above. Default timer is TCD, which does not run in Standby mode.

EDIT 2
Nope, I tried with other clock sources (TCA, TCB0, TCB1, TCD) in the submenu millis()/micros().
In all cases, millis() seems to stop during Standby sleep.

@freemovers
Copy link
Collaborator

Only the TCB you should be able to run in Standby, but you have to enable that option:
image
But you don't really need micros() running all the time for neopixels to work. Here is an example of the simple sketch from the TinyNeoPixel Static example. Timers are running when it updates the output to the neopixels, but instead of using a delay, the MCU simple goes to sleep:

#include <tinyNeoPixel_Static.h>
#include <avr/sleep.h>

#define PIN            15
#define NUMPIXELS      30

void RTC_init(void)
{
  RTC.CLKSEL = RTC_CLKSEL_INT1K_gc;         // 1kHz Internal Crystal Oscillator (INT1K)
  while (RTC.STATUS > 0 || RTC.PITSTATUS);  // Wait for all register to be synchronized
  RTC.PITINTCTRL = RTC_PI_bm;               // Periodic Interrupt: enabled
  RTC.PITCTRLA = RTC_PERIOD_CYC512_gc      // 1024 / 512 = 2Hz, 500 msec
  | RTC_PITEN_bm;                           // Enable: enabled PIT
}

// Wake up routines
ISR(RTC_PIT_vect)
{
  RTC.PITINTFLAGS = RTC_PI_bm;        // Clear flag
}

byte pixels[NUMPIXELS * 3];

tinyNeoPixel leds = tinyNeoPixel(NUMPIXELS, PIN, NEO_GRB, pixels);

// int delayval = 500; // delay for half a second

void setup() {
  RTC_init();
  pinMode(PIN, OUTPUT);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);   // Set sleep mode to POWER DOWN mode
  sleep_enable();
  interrupts();
}

void loop() {

  for (int i = 0; i < NUMPIXELS; i++) {
    leds.setPixelColor(i, leds.Color(0, 150, 0)); // Moderately bright green color.
    leds.show(); // This sends the updated pixel color to the hardware.
    // delay(delayval); // Delay for a period of time (in milliseconds).
    sleep_cpu();
  }
  for (int i = 0; i < (NUMPIXELS * 3); i++) {
    pixels[i] = 150; //set byte i of array (this is channel (i%3) of led (i/3) (respectively, i%4 and i/4 for RGBW leds)
    leds.show(); //show
    // delay(delayval); //delay for a period of time
    sleep_cpu();
    pixels[i] = 0; //turn off the above pixel
  }
}

@SpenceKonde
Copy link
Owner Author

Oh, I see, to ensure that it doesn;t get called so frequently the pixels never latch....
I will correct the dependence on micros in next release, and instead issue a #warning in that situation that they MUST ensure at least 50 us (in practice, apparently only 20 is needed!) between calls to show() - with the tighter constraint imposed by real-world neopixels, (datasheet says 50, but in reality? they latch after 20!), I think it's unlikely to be tripped over, except for very tight loops with very few pixels. I have seen neopixels used to reduce the pincount requirement for an RGB led when flash was abundant and pins scarce...

@SpenceKonde
Copy link
Owner Author

I had even checked for it in the DISABLE_MILLIS case! But not RTC millis case. Might have predated RTC millis

@ArnieO
Copy link

ArnieO commented Feb 23, 2021

TinyNeoPixel dependency on micros()
Great @SpenceKonde , thank you for looking into that!
Neopixel is a great solution for implementing a multi-colour signaling LED using only one GPIO (the case in my current project). A library implementation (with some minor limitations) that is not dependent on micros() will be a nice improvement.

millis() not running during SLEEP_MODE_STANDBY

Only the TCB you should be able to run in Standby, but you have to enable that option:
image

Indeed, it seems like TCB should be used if needing to have a counter running during Standby.
TDC (which seems to be the default on 1-series) can not be kept running:
image

EDIT
OK, so I added this line to setup():
TCB1.CTRLA |= TCB_RUNSTDBY_bm;
And recompiled with
image
Alas, the counter still does not seem to run during Standby, at least it is not seen by millis().
Any ideas?

@SpenceKonde
Copy link
Owner Author

SpenceKonde commented Feb 23, 2021 via email

@ArnieO
Copy link

ArnieO commented Feb 23, 2021

Why do you want an oscillator running, unless it's being used by something?

For the moment I am only trying to use it for verifying the sleep duration, while learning how to master these devices. So not a blocking point for my ongoing project.

What do you mean by "it will no longer work if the code is recompiled with
a different clock source setting"? which setting are you referring to?

If the port manipulation I attempted in my previous post had worked, it would no longer have worked if I rebuilt the code while in the Arduino IDE submenu selecting another clock source, for instance TCB0, because the port manipulation is done on TCB1 in the code.

@SpenceKonde
Copy link
Owner Author

Okay, now I see the context. Yes, you most certainly do have to configure the same timer/counter. Keeping the oscillator running would be beside the point though - if you had the oscillator running, but you hadn't set the currently selected timer to run in standby, doing that wouldn't make the the timer (which wasn't set to run in standby) run in standby.

Huh, I'm surprised that that didn't do something! What I would have expected is that it when you did that, it would sleep until the next millis overflow, and then the TCB overflow interrupt would fire and you would no longer be asleep....

You can't reasonably use the high speed timers we normally use for microsecond resolution timing while also sleeping - the overflows come often enough that you're waking up constantly. you can get like... 6.4 ms between interrupts with 20 MHz system clock while sleeping. And they're power hogs! I would be inclined to set up a second one to time it with - as in. have it drop a pin (with digitalWriteFast or VPORT write - those are both single cycle, vs what, 50 clocks for digitalWrite? It may even be more than that. digitalWrite is godawful slow for what it's doing - it's all to permit people to specify pins by Arduino pin numbers at runtime where the pins are mapped arbitrarily onto actual ports/bits). On the other AVR you could use pulseIn() if you're lazy and don't need extreme accuracy, or use a TCB for input capture if one of those isn't true. If you used an AVR DB-series part, or AVR DA-series or tinyAVR 2-series, you could put a pair of TCBs into CASCADE mode to do 32-bit input capture, and measure something 3 minutes long with a granularity of... 24ths of a microsecond? I'd probably clock the Dx at 16 MHz or 32 MHz (they work fine at 32 at room temp; I suspect they wouldn't at 85C) usingthe TCA prescaler prescaling /16 as it's clock source, that way I'd get 1 us granularity in a measurement of an event... oh... 71 minutes long (2^32 * (PRESCALE / F_CPU) / 1000 us/ms / 1000 ms/s / 60min/s = 71 minutes and change. Sometimes 16-bit input capture just doesn't feel like enough. But 32-bit always feels like way more than necessary.

terminology: "register" manipulation, not "port" manipulation ("port manipulation" specifically refers to doing that to the PORT registers, which control the I/O pins, generally to get faster digitalRead/etc, occasionally to save flash (I did that once because the hundred-some-odd bytes of flash you get when you get rid of the last instances of each of the three digital I/O functions, and the last instance of any of them (for the tables they use), well, we had code that needed to go there...). Here, on DxCore
and I think MegaCoreX as well, digitalReadFast, digitalWriteFast and now openDrainFast make direct port manipulation much less necessary now. digitalWriteFast(PIN_PA0,HIGH); compiles to the same instruction as VPORTA.OUT |= 1<<0; namely sbi 1,0;

@ArnieO
Copy link

ArnieO commented Feb 24, 2021

Huh, I'm surprised that that didn't do something! What I would have expected is that it when you did that, it would sleep until the next millis overflow, and then the TCB overflow interrupt would fire and you would no longer be asleep....

Apparently I have misunderstood something, as I don't understand why TCB1.CTRLA |= TCB_RUNSTDBY_bm; should wake it from sleep?

Below is my (pseudo) code. I have three ISRs, each sets a flag that is acted upon in loop().
It works as expected:

  • Wakes from sleep each 4 sec and makes a green blink
  • Red or blue blink whenever I actuate one of the interrupt pins

But timer value written to OLED display is 13 or 14 ms also with the TCB1 register manipulation at the end of setup(). I expected that line to cause TCB1 to run during the sleep period, so that 4014 ms was measured.
(And thank you for correcting my terminology!)

void setup()
{
    /* other stuff */
    
    // Port interrupt setup
    RTC_init(4);    // Number of seconds per sleep period
    PORTB.PIN2CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc; //PB2
    PORTC.PIN1CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc; //PC1
    interrupts();                                            // Enable interrupts; equivalent to sei();

    sleep_enable();
    set_sleep_mode(SLEEP_MODE_STANDBY);
    TCB1.CTRLA |= TCB_RUNSTDBY_bm;    //Should cause TCB1 to run during SLEEP_MODE_STANDBY so that millis() keeps running.
}

void loop()
{
    unsigned long timer = millis();
    power_all_disable();
    sleep_cpu(); //Sleeps CPU for the number of seconds set by argument to RTC_init(n) in setup()
    
    if (interruptTimer)
    {
        interruptTimer = false;
        /* Green blink */
        /* Write to OLED display: millis() - timer */
    }
    
    if (interruptMagnet)
    {
        interruptMagnet = false;
        /* Red blink */
    }
    if (interruptRelay)
    {
        interruptRelay = false;
        /* Blue blink */
    }
    
}

@SpenceKonde
Copy link
Owner Author

Deferred to 2.4.0. Sorry, blame microchip for releasing new parts.

@SpenceKonde SpenceKonde modified the milestones: 2.3.0, 2.4.0 Apr 3, 2021
@SpenceKonde SpenceKonde modified the milestones: 2.4.0, 2.5.0 Jul 19, 2021
@SpenceKonde
Copy link
Owner Author

Sorry again, 2.5.0. Need to get work on other cores done and 2.4.0 has taken almost a month longer than expected :-(

@dattasaurabh82
Copy link
Contributor

dattasaurabh82 commented Aug 23, 2021

I have to use Serial and millis() and micros() for an application.
BOD Mode is Disabled

what I'd be doing is:

#include <avr/sleep.h>

void setup(){
    Serial.begin(115200);
    // Initialise a button pin for wake up interrupt. 
    // Then initialise all unused pin to INPUT_PULLUP using Direct port manipulation methods. 

    //--- Sleep mode enablers ---//
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_enable();
}

void loop(){
    // when system is awake
    // read if there is something in serial. 
    // do something with that data
    // -- These can be, in future, be more optimised ...  
   
    Serial.flush();                    // flush everything before going to sleep
    delay(1);
    sleep_cpu();
}

I have many other stuff in there. But basically:

  1. If Serial is being used by the application, then the current consumption, even in sleep mode is very high (in milli Amps).
    ppk-20210823T143246

  2. If Serial methods are removed the current consumption reduces drastically. (in fraction of micro Amps)
    ppk-20210823T143406

Any pointers on that?
I have to use Serial and it would need to run on a small Low Capacity LiPo ... 🤕

Guesses: You're winding up with a bunch of stuff in the buffer and then flush is taking larger than you realize.
A serial interrupt is waking the device after you go to sleep (someting like a transmit compltet)

@SpenceKonde SpenceKonde added the DxCore Too This issue also impacts DxCore and probably others. label Jan 27, 2022
@SpenceKonde SpenceKonde self-assigned this Mar 22, 2022
@m9aertner
Copy link

m9aertner commented Dec 25, 2023

Thank you @SpenceKonde for all your great work and sharing of your know-how. To @freemovers, great sketch above which worked right away for me, too. Thanks for sharing.

I am using Core 2.6.10 with a Nordic PPK2 at 3V0 powering an ATtiny402-SSN at 1MHz internal, no BOD, no WDT, with RTC on, with just 2x 100nF capacitors and an actice-low LED on PA2, trigger on PA6. Alas, to really come down to 600nA during sleep, I had to follow best practice advice and add input disables (edit: sorry, saw this link only after posting, maybe the image below can be seen as a visualization now...):

  PORTA.PIN0CTRL = PORT_ISC_INPUT_DISABLE_gc;
  ...
  PORTA.PIN7CTRL = PORT_ISC_INPUT_DISABLE_gc;
  • SLEEP_MODE_IDLE: 600µA
  • SLEEP_MODE_STANDBY: 600nA
  • SLEEP_MODE_PWR_DOWN: 600nA (0.6µA, see screenshot below)
    • RTC is still running here, I guess, otherwise we should be at 100nA with "all peripherals are stopped"
  • No sleep / active loop: 770µA

I find these measurements meet what's stated in the data sheet (table 33.4) and more than good enough for my intended app which polls and de-bounces some reed switch input and sends data only upon Serial request. And when Serial is on, which will be most of the time, there's plenty of power from the requesting line. A small battery will be backup only.

Screenshot 2023-12-25 170533

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DxCore Too This issue also impacts DxCore and probably others. enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants