ATTiny85 Issue: EEPROM Memory wipes post Sleep Power Down Mode

Hey everyone,

Stumped on an issue involving EEPROM / Sleep routines in an ATTiny85 project and thought I'd turn to the community for help!

I'm developing a project centred around an ATTiny 85 (Using the excellent ATTinyCore); the basic overview is that a circuit detects a light flash using an LDR, increments a count, displays this count on an OLED screen and, if no flash is detected, saves the count to an EEPROM address and sleeps. During this sleep routine, a light flash or button press awakes the ATTiny85, which should then recall the EEPROM saved count and carry on counting.

I still need to perfect the code - in it's current state, the IF conditional isn't quite right as the circuit can't detect a light flash if it's already taken the "else if" branch (I'm looking into re-writing this to a While statement/something suitable).

As it stands, I've written the code so that an Interrupt is attached to a Reset button, allowing the user to reset the count whilst the device is awake at anytime.

When the sleep() function is called, I've then "transferred" interrupt rights, turning off the Reset button as an interrupt and only allowing a light flash / Wake button press to awake the device.

I've found that when the device awakes, the EEPROM count is lost and returned to 0 - unsure if this is due to how I've handled the interrupts or the EEPROM save process - any advice / thoughts on the code would be greatly appreciated (I've cobbled together this script using a host of awesome resources, such as Big Dan the Blogging Man and excellent posts from this forum ATtiny85 Sleep Help but can't figure this one out ... many thanks in advance for any guidance

/*  
 *  
 *  Sleep function 'borrowed' from Big Dan the Blogging Man https://bigdanzblog.wordpress.com
 *  Advice taken from: https://forum.arduino.cc/t/attiny-85-sleep-help/1032655/16 
 */

#include <Tiny4kOLED.h>
#include <EEPROM.h> 
#include <avr/io.h>
#include <avr/sleep.h>
#include <avr/interrupt.h>

#define PCINT_VECTOR PCINT0_vect 

int Count;      //set as integer when passing info to EEPROM using get/put to not be limited to 255 count
int eeAddress = 1; //EEPROM address to start reading from 

int LDR = 1;      // Assigned Op-Amp comparator output from LDR to PB1 
int WAKE = 3;     // Assigned Wake Button Input to PB3
int RST = 4;      // Assigned Reset Button Input to PB4 


//sleep function - this takes control of interrupt routine when called, disabling Reset button until awake 
void sleep() {
    
    GIMSK |= _BV(PCIE);                     // Enable Pin Change Interrupts
    PCMSK |= _BV(PCINT1);                   // Light input/PB1 can awake device ...
    PCMSK |= _BV(PCINT3);                   // ... as can pressing Wake/PB3
    PCMSK &= ~_BV(PCINT4);                  // Turn off Reset/PB4 as interrupt pin to not disturb sleep routine until specifically wake pressed
    ADCSRA &= ~_BV(ADEN);                   // ADC off
    
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);                          
    sleep_mode();                           // sleep mode macro includes sleep_enable() + sleep_cpu() + sleep_disable() all in one

    // ATtiny85 now asleep until wake button pressed
  
    cli();                                  // Disable interrupts
    PCMSK &= ~_BV(PCINT1);                  // Turn off PB1 as interrupt pin
    PCMSK &= ~_BV(PCINT3);                  // Turn off Wake/PB3 as interrupt pin
    PCMSK |= _BV(PCINT4);                   // Turn Reset/PB4 interrupt back on 
    ADCSRA |= _BV(ADEN);                    // ADC on
    sei();                                  // Enable interrupts - delay needs it to function

    } 


void setup() {

  pinMode(LDR, INPUT_PULLUP);   //sets PB1 as input for A0 from Op-Amp
  pinMode(WAKE, INPUT_PULLUP);  //sets PB3 as input from Wake button
  pinMode(RST, INPUT_PULLUP);   //sets PB4 as input from RST button

  // Following code enables Reset button interrupt routine 
  cli();                        // Disable interrupts during setup
  PCMSK |= (1 << PCINT4);       // Enable interrupt handler (ISR) for our chosen interrupt pin (PB4)
  GIMSK |= (1 << PCIE);         // Enable PCINT interrupt in the general interrupt mask
  sei();                        //enable interrupts 

  // initialise OLED
  oled.begin(128, 32, sizeof(tiny4koled_init_128x32br), tiny4koled_init_128x32br);
  oled.clear();
  oled.setFontX2Smooth(FONT6X8P);
  oled.setCursor(0, 0);
  oled.on();

  //set-up EEPROM location & variable
  delay(100); 
  EEPROM.get(eeAddress, Count); //get data from selected EEPROM address 
  delay(100); 

}

void loop() {
    
  oled.clear();
  oled.setCursor(1, 1);
  oled.println(Count);
  delay(500); 
  

    if (digitalRead(LDR)==HIGH){   
      
      ++ Count;
      delay(2000);
      }
      
      else if(digitalRead(LDR)==LOW){
      delay(10000); //wait for 10 seconds, if no input device will sleep  
      EEPROM.put(eeAddress, Count); 
      delay(1000);
      oled.clear();
      oled.setCursor(1, 1);
      oled.print("...Sleeping...");
      delay(1000); 
      oled.off();
      
      sleep(); 
      //When wake button pressed/LDR high, code carries on from here
      
      Count = EEPROM.get(eeAddress, Count); //get data from selected EEPROM address 
      delay(500); 
      oled.clear();
      oled.setCursor(1, 1);
      oled.on();
      oled.print("!!! Awake !!!");
      delay(1000); 
      }
   
}

// This interrupt handler is called when Reset button pressed 
ISR(PCINT_VECTOR)
{
  Count = 0; 
  oled.clear();
  oled.setCursor(1, 1);
  oled.println(Count);
  EEPROM.put(eeAddress, 0);
  delay(500);
}
  

@DrAzzy may be able to shed some light.

1 Like

The following assumes that the behavior occurs after a reset or powercycle, not just after uploading fresh code;

What is count now, when you print it? If nothing is being written to the EEPROM at all, we would expect to read 0xFFFF, -1 for an unsigned integer.

If you're reading 0xFFFF (-1), then you should be suspicious that you're not writing it at all, or it is experiencing a hard reset upon writing.

If it's instead reading 0, that implies that somehow you wrote 0 to it, or the device is experiencing a hard reset upon writing, or that you aren't correctly reading it at power on.

You can verify that it's not resetting upon an attempted write (which renders the results of the write unpredictable) by simply having the chip do something distinctive in setup (eg, turn a led on, delay for a second, turn it off, and don't turn the LED on for any other reason, so any time the light goes on, you know the board has reset). In this context, I would expect that it would be almost always caused by failure to include a 0.1uF ceramic capacitor for bypass/decoupling between Vdd and Gnd; No digital integrated circuit should be expected to work without a decoupling cap unless it is documented explicitly that one is not required. Many of the classic AVRs (though not all) highlight the importance of this in the datasheet, thought the tiny85 does not, though it also doesn't say that you don't need one. And I happen to know from experience that it cannot be used without any decoupling. I think this discrepancy can be explained on the grounds that they assume basic electrical design practices on the part of designers using their parts, including that the power rail has some enough decoupling on the board that Vdd is reasonably stable. Without decoupling capacitors, because a digital circuit can change almost instantly from a sleep mode using under a microamp, to a full function mode drawing thousands of times that, the supply voltage "seen" by the chip will sharply drop when this occurs - current though a wire cannot change instantly, due to the parasitic inductance that any wire or other conductor has, so the voltage across the power rails next to the chip falls when the current sharply increases, as it does in normal operation of a digital IC. This is solved by putting a small ceramic capacitor between each power/ground pair of pins, as close to the chip as possible (long leads on the ceramic cap defeat the point of a decoupling cap - the distance you get total, between the pins of the chip is a few inches (in some - admittedly not-identical - conditions that were documented in a rather nice whitepaper I read, they describe the measurements of the effectiveness of a capacitor at filtering noise near the freqencies that we'd expect to see as they changed the distance to the cap, and by like 2 or 3 inches (2-3 inches each way, to be clear). the graph with the cap looked the same as without it, while that was very much not the case for a closer capacitor).

There are a few things that give me pause, though I don't use EEPROM.get/put much myself.
I see you using EEPROM.get() in two different ways

EEPROM.get(eeAddress, Count); //get data from selected EEPROM address 
// and...
Count = EEPROM.get(eeAddress, Count); //get data from selected EEPROM address 
// Certainly, from a good practices perspective, this should not be done 
// you shouldn't have such stylistic inconsistencies...
// though I can't see why either of them wouldn't work. 
    if (digitalRead(LDR)==HIGH){   
      //body A
      } 
      else if(digitalRead(LDR)==LOW){
      //body B

These lines are technically hazardous; you expect that either bodyA or body B would run. But that is not always what will happen. With that code you read from the pin. If it's high, you do body A, otherwise, you read the pin again, and if it's low on this second read, do body B. I think you just wanted an else, so that you'd always get one or the other, never neither... Also, your code styling...

    // USE PROPER INDENTATION! Every level of curly braces that you're inside of should be 
    // represented with 1 tab (2 spaces) of indentation. This convention (equal indentation for equal
    // number of nested blocks, and either all tabs or all spaces for indentation, is universal.
    // There is also a fairly strong majority opinion in favor of spaces rather than tabs). 
    // The other stylistic suggestions are things that some people might not agree on... 
    if (digitalRead(LDR) == HIGH) { // space between an operator and the operrands
      // Whether to put a space after * and & when they appear as the unary (reference and 
      //dereference) operators is debatable, and there isn't a clear consensus, so I don't take a
      // postion on those for others' code, as if you do the other operators, you're free from the
      // unreadability quagmire I'm trying to steer folks away from; just do it consistently. 
      //body A
    } else { // space before the opening curly brace greatly improves readability. 
      // I also put the else or else if on the same line as the previous closing brace; this is a tradeoff
      // between the readability benefits of spreading the lines out, and those of fitting more of the
      // file onto the screen at a time. 
      //body B
    }
// And on that note, nobody ever does
++ Count;
// They do
Count++; //again, there isn't a consensus on whether to put a space between the ++ operator
// and the variable name; but I rarely see it, and don't do it.
// In general, postincrement and postdecrement are very common. Preincrement is rarely seen,
// and when it is, there's a reason. predecrement is not exotic to see in pointers, though I wouldn't
// say it's common either. 
// Regardless of this, when you have a choice of two C constructs that will do the same thing for
// your application, and one of them doesn't have some sneaky advantage, don't do the wierd one.
// That makes people (including yourself months from now) reading it see the uncommon way
// and assume that there *was* some sneaky advantage.

Finally, I also would note that your system is absolutely hammering address 0. You only get 100k rewrites per EEPROM cell. Rewriting the EEPROM every 10 seconds you'd have only 11 days or so before your EEPROM was no longer within spec and may not work. My understanding is that when the EEPROM was partly "worn out", the parts of the EEPROM that had been accessed excessively would either always read as 1 or always read 0 - I think the latter. A runaway sketch that just sat there writing to a fixed address as fast as it could (3.4ms per rewrite) like this one, could exhaust the spec'ed eeprom life in just 6 minutes; That's not a strangely long time to have known non-working code on a board during development; so you can't upload any old code that may write to the EEPROM without taking some care that it couldn't sit there rewriting an address until it dies.

void loop() {
  static byte t = 0;
  EEPROM.write(0, t++); //every pass through loop will write a number to address 0.
}

If the problem does not manifest when the chip is reset in some way, but does manifest when new code is uploaded, and you are uploading the code via an ISP programmer (as we recommend for all t85s that are not digispark-clones*). (If that behavior was seen when uploading through a bootloader, that's a bootloader other than what the core ships with), the problem is failure to "burn bootloader" with the desired options selected from the tools -> submenus; the applicable options are marked (Burn bootloader required to apply) or something similar to that (as much of that as I was comfortable with the length of the menu names; I forget whether the applicable options included any with names that were hard to make short on ATTC; that was what shaped the length of the parenthetical note on modern AVRs supported by mTC and DxC)

* - Digispark clones (like any micronucleus bootloader for an otherwise supported part) are supported, as you likely know, but use a different "board definition"; many people post asking for support with a digispark-alike without mentioning that they're using such a part - particularly for the t85, as digisparks are so common that newbies don't always realize that that is not the normal configuration that people are assuming while answering. I'm pretty sure you're not using a digispark, but this is something that bears mention: 2 of the pins on the USB connector of those parts are tied to the USB lines, so they can't be used as inputs when the device is connected to a computer or even to some USB phone chargers (due to a "passive power supply" scheme they use to tell the connected device how much current it can provide by connecting the data lines to voltage dividers, and the voltages on the pins were read by early phones to enable the faster-charging modes, used before QC and USB-PD, which use the digital USB protocol proper)

** - I firmly oppose your convention of having a pin that is not the hardware reset pin being named "RST" or "Reset", simply on the grounds that a name should refer to a specific object, concept, etc as unambiguously as possible. Atmel->Microchip has already claimed "Reset" and "RST", thus an application specific pin shouldn't be referred to in the same way. Especially because the hardware reset pin can be made into an I/O pin *** (this also prevents ISP programming), Making the reset pin into an I/O pin is common among hobbyists on exactly one part: The tiny85, because that's how the original digispark boards shipped; the official ones are long gone, and the clones generally don't disable reset (this is an improvement IMO, since it's hard to enable reset if it's disabled, but easy to disable if it's enabled. Reset pins made into I/O also suck at the output part; they're around 20-30x the strength of the pullup, but a normal output pin is 20-30x the strength of the reset-as-output****) digispark boards. It is virtually unheardof among hobbyists on anything else (okay, not unheardof, everyone's heard of it, but only in the context of accidentally writing garbage to the fuses such that reset was disabled and they couldn't reprogram the parts), since the tiny85, yes, is very pin-constrained, and since they had the ability to drive home some homogeneity through their high profile kickstarter campaign, and of course since they use a bootloader, they successfully shipped the official digisparks with reset disabled, and ran the bootloader only after a POR, hence the "replug within 8 seconds". I have never encountered a digispark clone, including ATtiny167 Digispark Pro clones and the MHET Tiny88 board where they had disabled reset. And this paragraph illustrates my point, as I was preparing a screed on reset as I/O before realizing that's not what you were doing.

*** - an interesting bit of trivia: On a classic AVR, with reset enabled, the RST pin will always read LOW. With reset disabled, of course, this isn't true; they reflect the state of the pin. Thus, one can more expediently verify that reset is or isn't disabled at startup by turning on the pullup of RST. waiting a bit, and then reading the pin - if it's LOW, reset must be enabled. Thinking about it now, there may be an even easier method for app code to detect the reset disabled-ness or lack thereof - if they have the bits in DDx/PORTx forced low, then that whole waiting game could be skipped by simply immediately testing whether that bit in PORTB stayed set. I don't know if the registers work like that; I know they do on modern AVRs.

**** - these numbers are quite rough - there isn't a single number that reflects the strength of an I/O pin driver or pullup - "strength" is a function of the voltage drop, plus operating conditions like vdd and temperature; it's a curve on a graph. As it happens, the specs that Microchip/Atmel (I don't think they've updated the typical characteristic graphs since the buyout, so the blame lies with Atmel) supplies seem to be designed to make comparisons between reset-as-i/o, normal i/o, and pullup strength as hard as possible. Reset as I/O is given at -40/0/85C. the other two at -40/25/85C. The reset and normal I/O pins are shown on graphs with current on X and voltage on Y, the pullup strength is given as uA on Y and voltage on X (though it changes considerably less as voltage changes than output strength)

1 Like

Hi @DrAzzy,

Thank you very much for your thorough response, the advice is much appreciated and I found your comments on decoupling capacitors/AVR reset pin trivia very interesting, thank you for taking the time to share this! Your comments are noted ref Syntax / Indentation - I have cleaned up the latest code version, so thank you for these tips.

I have already made sure to integrate a 0.1uF capicitor across VCC/GND in all my ATTiny85 designs, having read your github sometime ago - so you have already helped remove this potential issue for me!

After reading through your suggestions, and isolating whether it was an issue with the EEPROM writing process/hard resetting/EEPROM integrity due to multiple writes, I found by tweaking the code / burning the bootloader to a new ATTiny85 IC that none of these suspected problems were the culprit. The EEPROM was retaining values correctly throughout power cycles.

Instead, I realised that I had contained my Reset Button function inside the ISR(PCINT_VECTOR) - so that whilst this function was called as expected during normal code rountine, I believe it was also being called during the sleep mode routine and effectively wiping my EEPROM value each time sleep mode was initiated.

I have removed the Reset function routine from the ISR(PCINT_VECTOR) Interrupt Handler, and the code is functioning as expected.

Which leads me to the next issue ... how to best implement an interrupt for the reset button to interrupt the code during non-sleep mode routine? My knowledge on ISRs is limited - Am I correct in understanding that the ATTiny85 only has one interrupt handler? Could I use the same interrupt handler but for different interrupts? Or is there a better, simpler way of going about this in your opinion that doesn't require interrupts?

Many Thanks

Well to implement a reset button, you would just connect a button between the Reset pin and ground if you wanted a clean reset. It sounds like this is not what you want though,

You get..... 2 types of pin interrupts, it;'s a lowpincount classic so it's going to have an INT0. That appears to be on PB2.

There is also the PCINT.

An INTn -type interrupt can detect rising or falling edges only as long as the clock is running (ie, the chip is not sleeping). INTn type interrupts can also be set to trigger upon "change" or "low level", which are detected even during sleep. It only works on one pin, usually one shared with a lot of important other features (On the tiny85, every pin is shared with a lot of other important features, but I recall most of the INTn pins were usually on inconvenient pins. To the extent that there is interrupt priority on these parts, the INTn interrupts come first (but the extent of it is that if interrupts are ever enabled with multiple interrupt conditions enabled and triggered, this one will run first.

This is what is used by the work of evil known as attachInterrupt().

A warning about the INTn interrupts - actually, to any "Low level" interrupt - A low level interrupt is triggered if the pin is LOW. Not when it goes LOW. Hence if the pin is still LOW after the ISR finishes, if your ISR didn't turn off the interrupt, it's going to execute a single instruction of normal code before running the ISR again as long as the pin is low.

This is generally not the desired behavior so it is common to turn off the interrupt in the ISR for cases like this. Though often it's considered acceptable (think about common consumer electronics, and remember what happens when you push the button - does it take effect when you push it or when you release it? In the wild it is extremely common for low end consumer electronics to hang while a button is depressed, and then react to the press when the button is released...

The other method of interrupts is to use PCINTs, available on all pins. These are async (can wake from sleep), but react to any pin change on enabled pins (controlled by PCMSK). It also doesn't tell you which pin triggered it; if you ever have multiple pins enabled for PCINTs, you need a way to tell which was pressed. Typically all the lines are pulled up, pressing a button grounds it, and the interrupt just looks for LOWs, which indicate that a button is currently pressed or was very recently pressed or released and is bouncing, as most physical switches will do a few times.

Note that any library which uses PCINTs will glom onto every vector for them it finds, preventing user code from using it. This impacts SoftwareSerial.h (a library I consider an abomination) - you can';t use PCINTs if you're using SoftwareSerial. Even if you're on a part with more than 1 port, the library in order to support using any pin has no choice but to take over every damned PCINT vector. (That's why ATTC includes a second implementation of a software serial that uses a fixed pin and a much more rarely used interrupt vector - otherwise software serial blows up interrupts, and that's just no fun.

But yeah, you got lots of options here depending on your application requirements; you either don't have both buttons enabled as PCINTs or you check for the buttons in the PCINT handler (via a direct port read mechanism to avoid missing presses on account of digitalRead being slow as hell) or you can even use the separate interrupt mechanism....

1 Like

Thank you DrAzzy, appreciate your feedback a lot!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.