SpenceKonde / megaTinyCore

Arduino core for the tinyAVR 0/1/2-series - Ones's digit 2,4,5,7 (pincount, 8,14,20,24), tens digit 0, 1, or 2 (featureset), preceded by flash in kb. Library maintainers: porting help available!
Other
551 stars 143 forks source link

Can't get below 1.4mA in STANDBY with an ATTINY16 #888

Closed intakenick closed 1 year ago

intakenick commented 1 year ago

Discussed in https://github.com/SpenceKonde/megaTinyCore/discussions/887

Originally posted by **intakenick** January 3, 2023 I'm attempting to approach the low uA sleep power consumption that others here have been able to achieve but am unable. The only thing that could be pulling power on the board is a 100k resistor which is biasing a mosfet and connected to a GPIO (so that pin is pulled high and sinking some negligible current due to its high impedance path to ground). Interestingly, this is an old project that was started a long time ago on a very outdated version of this core. Back then, the sleep mode current was around 4uA. Code below: `#include #define RAIL 5 #define BUTTON 10 bool showtime=0; int main() { //Serial.begin(115200); set_sleep_mode(SLEEP_MODE_STANDBY); sleep_enable(); setRail(0); while(1){ //Serial.println("Awake"); delay(1000); sleep(); } } void wake(){ detachInterrupt(digitalPinToInterrupt(BUTTON)); pinMode(BUTTON, INPUT); showtime = 1; } void sleep(){ for(int i=0; i<18; i++){ if(i != BUTTON && i != RAIL){ pinMode(i, INPUT_PULLUP); //digitalWrite(i, LOW); } } ADC0.CTRLA = 0; //ADC0.CTRLA &= ~ADC_ENABLE_bm; //ADCPowerOptions(ADC_DISABLE); setRail(0); while(digitalRead(BUTTON)==LOW){} showtime = 0; //Serial.println("Sleep"); delay(50); attachInterrupt(digitalPinToInterrupt(BUTTON), wake, LOW); sleep_cpu(); } void setRail(bool enable){ if(enable){ pinMode(RAIL, OUTPUT); digitalWrite(RAIL, LOW); } else{ pinMode(RAIL, INPUT); } } And here's the power profile: ![image](https://user-images.githubusercontent.com/71517444/210485740-4f4d3221-52a8-4319-b738-dd0f471859f1.png) `
SpenceKonde commented 1 year ago

Thaaaats what? a 1624 you mean? looks like it since you're doing LOWLAT stuff?

Well, I don't have a current meter capable of going that low - (was actually looking into how to get or make a uA capable meter, because any sort of sleep library is blocked by the fact that I don't have the necessary instrumentation. I might end up making something with a tiny1624 (for the good ADC) which has power supplied to the positive side of the target by one power supply, power to the 1624 by another, and puts a sense resistor in the low side. Actually, it needs TWO sense resistors, one at around 1k (for when current is low and we can't get enough voltage drop to measure with 10), and one at like 10 ohms (so it will power the chip with under 100mV of drop at 10mA. Take differential measurements between target supply in free-run mode, and adjust gain to make sure we maximize the information we're getting from the readings, and when that's not enough to keep it in the desired range, turn the fet on or off. Target has like 115 uF of decoupling, and I can turn on the MOSFET before the voltage would have dropped so much that the chip reset, so I should be able to detect the voltage drop changing fast enough to switch the 10 ohm resistor on when it wakes up, and off the voltage drop gets too small to measure. I calculate I should be able to measure values from 10mA-100uA with 10 ohm on, and down to a uA or less with the 1k on. I'm not confident about it's absolute accuracy, but I don't see why it wouldn't work (I might need something more sophisticated on the hardware side, like a high side current amplifier like the MCP6802 (or was it 6208?) coupled with different sense resistors. 

But back on YOUR question - You are not configuring the chip to minimize power usage. Where is the initialization code that initializes the unused pins? You need to either turn on pullup, or set as output, and pin that is not used, and anything with an analog signal on it turn off the port input buffer (pinConfigure(pin,PIN_ISC_DISABLE); or by setting the bit in the port's PINnCTRL). You must not have any floating pins to get nice low power consumption. You always did on AVRs, but the modern AVRs (I think because of the vastly expanded pin interrupt/event system) are definitely fiddlier about it - and I think it also depends on stupid shit like EM background noise because the mechanism is pin input buffers on the floating pins, or analog pins with a voltage in between HIGH and LOW and changing, and em noise picked up by a pin depends on how much noise is in the environment,. I would suggest putting those pin manipulations in a function called from setup, so during normal dev you can work without having to remember to go back and add or delete a line every time you change what pins you use just by commenting out that call, then once it's otherwise working, and just hogging power, you doublecheck the initUnusedPins() or whatever you called it, uncomment the function, and then see what the power consumption is down to. I have heard numbers nearly that high thrown around as the current draw in some environments

intakenick commented 1 year ago

Sorry the chip is the ATTINY1616. Regarding setting the pins, I loop through them and set them all to INPUT_PULLUP in the sleep() function. I've also tried OUTPUT but it makes no difference.

Also, the tool I use to measure uA level current is called the Power Profiler Kit 2 from Nordic semi. Best $99 I've spent and the software is free. It has a 2A adjustable power supply in it so you can power your device from it, log current, and export as a csv if you need to do calculations

intakenick commented 1 year ago

I've tried what you suggested and saw no difference on the ATTINY1616: `#include <avr/sleep.h>

define RAIL 5

define BUTTON 10

bool showtime=0;

int main() { set_sleep_mode(SLEEP_MODE_STANDBY); sleep_enable(); initPins(); setRail(0);

while(1){ delay(1000); sleep();

} }

void wake(){ detachInterrupt(digitalPinToInterrupt(BUTTON)); pinMode(BUTTON, INPUT); showtime = 1; }

void sleep(){

ADC0.CTRLA = 0;

setRail(0); while(digitalRead(BUTTON)==LOW){} showtime = 0; delay(50); attachInterrupt(digitalPinToInterrupt(BUTTON), wake, LOW); sleep_cpu(); }

void setRail(bool enable){ if(enable){ pinMode(RAIL, OUTPUT); digitalWrite(RAIL, LOW);
} else{ pinMode(RAIL, INPUT); } }

void initPins(){ for(int i=0; i<18; i++){ if(i != BUTTON && i != RAIL){ pinMode(i, OUTPUT); pinConfigure(i,PIN_ISC_DISABLE); digitalWrite(i, LOW); } }

} ` The pinConfigure function also seems to have disabled the UPDI pin so I'll have to switch to another board to keep testing. Any other suggestions? I'm still pulling around 1.4mA in STANDBY mode

MX682X commented 1 year ago
  1. could you try SLEEP_MODE_POWER_DOWN - This disables all Stand-by peripherals aswell. Might give some additional information where to look next.
  2. Have you disconnected the debugger / UPDI pin? UPDI takes about 1mA, If I remember correctly (might be wrong, long time ago)
  3. Please use code formating. you can do that by selecting your code and < Ctrl+e > or the corresponding button above the textfield. or just copypaste ´´´ above and below the code fragment
SpenceKonde commented 1 year ago

Okay: no matter HOW early in the initialization process disable the input buffer on PA0, I've found a trivial way to unbrick: Two upload attempts in extremely rapid succession. (I did it with the onPreMain hook, which is triggered very very early).

You may also need to powercycle and start spamming upload attempts as soon as it is connected to power again.

And as I said in my response, you need one or more of those conditions to be true. Unless set as GPIO, PIN_PA0 always has the internal pullup wedged on, so there is no reason to do anything to it. You do not need to disable input buffers, set as output AND write low. Writing low after setting a pin that has not been otherwise configured since reset aside to set it output achieves nothing If you haven't changed the state of a pin other than making it output, it's outputting a LOW. If the input buffer is disabled, you do not need to do anything else.

Also, you are overriding main. That is not advised. We strongly recommend users use the normal setup/loop model, because otherwise, delay, millis, analogWrite, and a number of other Arduino API functions will not work. Additionally, the chip will not run at the clock speed you selected if you override main, because then the code that sets the clock speed doesn't get called. Though of course, since millis and delay both will never indicate passage of any time at all. you might not notice. Haven't you noticed that those delays() aren't working right?

Are you certain that what's happening isn't that it hits the first delay, and since millis has not been initialized, the delay never expires and you're simply looking at the power consumption of the chip at 3.33 or 2.66 MHz?

main calls a bunch of critically important stuff, and if main is overridden, we really can't guarantee that anything will work. digitalWrite on some pins will have unexpected sideffects, analogWrite will not produce PWM, analogRead() will hang forever, delay() will never end, millis() and micros() will never increase, and if you try to get around that wilh /util/delay.h, _delay_ms() won't delay for the right length of time because it relies on F_CPU matching the system clock speed. The code that makes the system clock speed match FCPU gets called from the default implementation of main. Overriding main is something you should very very rarely do, and only after you have read the source and understood main.cpp and the init* functions it calls. located in wiring.c.

Oh, and two other things.... It is recommended to use PIN_Pxn to refer to pins whenever possible, not numbers. There was debate for DxCore about whether to show pin numbers at all on the chart. If you use the PIN_Pxn notation, because the peripherals are always onthe same port pins within a part family (8-pin tinies being the only modern avr exception), you can move between parts much more easily. If I had a 14-pin part, and realized I needed more pins for GPIO, but had used numbers, I would need to painstaking go through and adjust the numbers to match what they were on the 20 pin version. But if I used PIN_Pxn, I could just connect everything up to the same port pins. There'd by 6 more pins (2 in PORTB, 4 on PORTC) and I could use those for the new connections I needed, but my existing code would "just work".

Whenever the pin being operated on is a constant, and especially if what it's being set to or the mode it's being set to is also constant, you get MUCH better performance and (unless a library is using the non-fast versions) significantly reduced flash use if you use pinModeFast(), digitalReadFast() and digitalWriteFast(). There's more information in the digital and Analog reference pages (linked to from the main readme)

(note that - if analogWrite or manual means are used to make PWM come out of a pin, neither pinModeFast nor digitalWriteFast will turn it off (* (turnOffPWM is actually the majority of the time taken by digitalWrite() (at least on 1-series tinyAVR too where digitalWrite() is used at least once on PC0 or PC1 as well as one other PWM capable pin, even if PWM is never generated), unless none of the pins that digitalWrite() is called on support PWM, in which case the optimizer can see that digitalPinToTimer() will always return NOT_ON_TIMER and ski[p the whole thing) The first time you use digitalWrite() it can add a couple hundred bytes to the sketch size. (details depend on part and pins are involved and can take 100 clocks or more iirc..

with constant pin and constant value, digitalWriteFast takes.... 2 bytes of flash and a single clock.

* Also, as stated in the core docs, digitalRead() doesn't turn off PWM like if does in the official cores. Doing that adds significant overhead, and a common sense assumption, namely that "reading" means "read" not "change what the pin is doing and then read"

Oh, what is the hardware you are using? Could there be something on the same board that's drawing current? A voltage regulator can have a quiescent current of a few mA.... things like that.

Please, no fritzing-circuits diagrams - a hand-drawn schematic photographed with a cellphone camera is far more effective at communicating what you've got connected

SpenceKonde commented 1 year ago

Oh, and attachInterrupt is a flash devouring and slow executing abomination, even after I put a great deal of effort into trying to make it suck less. The fact is that no matter what, any scheme whereby interrupts can be attached or detached, owing to the AVR-GCC abi, and the slow executing logic to figure out which pin triggered it, and call the appropriate handler. It's less slow and shitty now (I reimplemented the whole bloody mess in assembly, reducing the flash used significantly. Did it around the same time as and using similar techniques to what I did to Serial to reduce the flash footprint greatly the version before 2.6.x.

You get much much better performance manually making pin interrupts to wake, and use far less flash too - though unfortunately there's no example of that in PowerSave.md PowerSave.md is an old doc mostly written by others, because I keep putting off my planned powersave and sleep library (largely because I don't give a damn about power saving personally. Everything I do is plugged in to main adapter, pretty muich.

intakenick commented 1 year ago

Ok so I came back to this today and made the following changes:

  1. Went from int main() back to setup and loop (I'd made this change as per another discussion here)
  2. I removed the pinConfigure(i,PIN_ISC_DISABLE); line from initPins()
  3. I tried SLEEP_MODE_PWR_DOWN first and then switched back to STANDBY

Results:

Upon switching to PWR_DOWN (along with other two changes above) sleep mode current went down to .76uA (!). Switching back to STANDBY it increased slightly to 1.78uA (still perfect for my needs). Code posted below, hopefully formatted this time for anyone else who runs into this issue. Any ideas on what happened @SpenceKonde @MX682X ?

#include <avr/sleep.h>

#define RAIL     5
#define BUTTON   10

bool showtime=0;

void setup() {
  set_sleep_mode(SLEEP_MODE_STANDBY);
  //set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable(); 
  initPins();
  setRail(0);
}

void loop(){
    delay(1000);
    sleep();
}

void wake(){
  detachInterrupt(digitalPinToInterrupt(BUTTON)); 
  pinMode(BUTTON, INPUT);
  showtime = 1; 
}

void sleep(){

  ADC0.CTRLA = 0;

  setRail(0);
  while(digitalRead(BUTTON)==LOW){}
  showtime = 0; 
  delay(50);
  attachInterrupt(digitalPinToInterrupt(BUTTON), wake, LOW);
  sleep_cpu();
}

void setRail(bool enable){
  if(enable){
    pinMode(RAIL, OUTPUT);
    digitalWrite(RAIL, LOW);    
  }
  else{
    pinMode(RAIL, INPUT);
  }
}

void initPins(){
    for(int i=0; i<18; i++){
    if(i != BUTTON && i != RAIL){
      pinMode(i, OUTPUT);
      digitalWrite(i, LOW);
    }
  }

}
intakenick commented 1 year ago

Two other possible contributing factors:

  1. I measured current without the programmer connected. With it connected, it was fine until I touched the connector
  2. I removed all serial printing (in previous iterations). Adding that back in jumped me into the mA range again
SpenceKonde commented 1 year ago

I think that when you had main() instead of setup and loop, because init_clock was never called, it was running at 2.66 or 3.33 MHz (these parts always start up running from the 16 or 20 MHz oscillator with the 1/6 prescaler enabled. And you're running it at 5v right? That's just about the expected active mode current at 5V and around 3 MHz.

So I'm going to guess that you didn't have millis timekeeping disabled in the submenu - but you had overridden main, and your overridden main doesn't call init(). Since init() isn't called, init_timers, init_ADC, init_millis, and init_clock weren't called either, and your code didn't call them either. But since millis was not disabled from the submenu, the core was proceeding as if it had millis timekeeping. So when it got to delay(), it used the millis-enabled version of delay: it recorded the time with micros, and then went into a loop calling micros and subtracting the old micros from the new micros, waiting for a result larger than 1000, at which point it would increment the start time by 1000 and decrement the millis count.

With the millis timer not running at all, micros returned zero every call. So it hit the delay, and stopped, in a busy-wait loop for a condition that would never come. While running at 1/6th of the base clock speed (2.66 if set to 16/8/4/2/1, 3.33 if set to 20/10/5). You'd have also noticed that any serial prints came out as gibberish (because the baud rate was 1/6th what you'd asked for, because the chip was running at 1/6th the speed you asked for.

Had millis been disabled from the tools submenu, but everything else been the same, you'd have seen 6 second delay instead of the 1 second one you asked for (and serial would still have been borked.

See https://github.com/SpenceKonde/megaTinyCore/blob/master/megaavr/extras/Ref_Callbacks.md for a more thorough treatment of all the overridables functions, with notes about which ones are probably not a good idea to override, and why.

Now that you've got that sorted out, you're running into a different issue when you have the serial prints. I suspect the DRE interrupt is firing when the usart finishes sending it's current character, waking the system back up (per datasheet the transmitter always finishes it's current character before sleeping). But if there are more characters to send, the DRE interrupt will still be enabled; and that's firing and waking the chip up. Before you put the chip to sleep, call Serial.flush() if you've been doing any serial prints recently, that tells the sketch to busy-wait until the serial transmit buffer is empty - at which point you can go to sleep safely

SpenceKonde commented 1 year ago

Closing this, please reopen if contradictory information has become known to you.

SpenceKonde commented 1 year ago

Okay, first: I said that one or more of the following had to be true. Your function does three of those. Just do one of them yo.

pinMode(pin,OUTPUT); digitalWrite(pin,LOW); is redundant on a pin that has not been previously configured. pinMode() sets the PORTx.DIR register's bit n (where x is the port letter and n the number of the pin within the port), leaving PORTx.OUT unchanged. PORTx.OUT defaults to 0. So if you run that before configuring pins: