Arduino EEPROM wear leveling using a ring counter

Over the past few weeks I've been playing around with an Ardunio Uno, the plan is to build an IOT (Internet of things) controlled central heating thermostat that doesn't require any re-wiring of the current system.

As part of that system I have the need to store the current thermostat value in the EEPROM memory so that the Arduino can go to sleep and it will be able to read the value back after it wakes up a few minutes later.

Now, the problem is there could potentially be quite a lot of read/write operations on the EEPROM memory and over time there would be a chance that the memory would fail as EEPROM only has a specific lifespan before it starts becoming unreliable.

Excerpt from the Wikipedia - Wear Leveling article.

EEPROM and flash memory media have individually erasable segments, each of which can be put through a limited number of erase cycles before becoming unreliable. This is usually around 3,000/5,000 cycles but many flash devices have one block with a specially extended life of 100,000+ cycles that can be used by the Flash memory controller to track wear and movement of data across segments.

Assuming a value is only logged once every 5 minutes, that's still 12/hour, 288/day and 105,120/year, which puts us firmly in the danger zone.

If the location of the data can be changed every time is is being written then it is possible to extend the lifetime of the EEPROM quite considerably. This is where a ring counter comes in useful.

I'll let the Wikipedia Ring counter article explain...

A twisted ring counter, also called Johnson counter or Möbius counter (also Moebius), connects the complement of the output of the last shift register to the input of the first register and circulates a stream of ones followed by zeros around the ring. For example, in a 4-register counter, with initial register values of 0000, the repeating pattern is: 0000, 1000, 1100, 1110, 1111, 0111, 0011, 0001, 0000... .

All I have done in the code below is allow the register to be a variable length of up to 255, and to combine that with a with the same amount of data storage. This means that by finding the position at which the 1's turn to 0's which is where the last update occurred, it is possible to look up the same memory position in the data and find the last stored value. Once the register is full of 1's we reset the position back to the start and being writing 0's.

Using the short example from the Wikipedia and storing the alphabet along side it, the memory would look like this 0000:0000, 1000:A000, 1100:AB00, 1110:ABC0, 1111:ABCD, 0111:EBCD, 0011:EFCD and so on...

So, in summary setting the memSize to just 50 will give me 50x the memory life, meaning each byte will only get written 2102 times a year, thereby extending the life to 47.5 years, I think that should do the job!

Anyway, here's the code...

#include 

// the memory location at which the ring counter starts [0-255]
const uint8_t memStart = 0;

// the number of bytes to use for the counter [0-255]
// the storage will also use this number of bytes
const uint8_t memSize = 4;

// clear any previously used EEPROM
void ringClear() {
  const uint16_t memEnd = (memStart + (memSize * 2)) - 1;
  for (uint16_t i = memStart; i <= memEnd; i++) {
    EEPROM.write(i, 0);
  }
}

// prints the EEPROM memory to Serial
void ringDebug() {
  const uint16_t memEnd = (memStart + (memSize * 2)) - 1;
  for (uint16_t i = memStart; i <= memEnd; i++) {
    if (i == memStart) {
      Serial.println( "----- counter -----" );
    }
    if (i == (memStart + memSize)) {
      Serial.println( "----- storage -----" );
    }
    Serial.print("EEPROM[");
    Serial.print(i);
    Serial.print("] : ");
    Serial.println(EEPROM.read(i));
  }
}

void ringBegin() {
  // Init EEPROM, 4096B is the maximum, 4 bytes is the minimum
  EEPROM.begin((memSize * 2));
}

// looks for a change in the counter values and returns the offset of the last updated value
uint8_t ringOffset() {
  const uint16_t counterEnd = (memStart + memSize) - 1;
  const uint8_t initalVal = EEPROM.read(memStart);
  // if none of the values are different, it was the last one
  uint8_t offset = (memSize - 1);
  for (uint16_t i = memStart; i <= counterEnd; i++) {
    uint8_t thisVal = EEPROM.read(i);
    if (thisVal != initalVal) {
      offset = (i - memStart) - 1;
      break;
    } else if (i == counterEnd) {
      offset = (memSize - 1);
      break;
    }
  }
  return offset;
}

// read the last byte of data that was written to the EEPROM
uint8_t ringRead() {
  const uint8_t offset = ringOffset();
  const uint16_t storagePos = (memStart + memSize) + offset;
  return EEPROM.read(storagePos);
}

// write a new byte of data to the EEPROM
void ringWrite(uint8_t newValue) {
  uint8_t offset = ringOffset();
  offset = offset + 1;
  // if the last write was at the end of the register, start again
  if (offset == memSize) {
    offset = 0;
  }
  const uint16_t counterPos = memStart + offset;
  const uint16_t storagePos = (memStart + memSize) + offset;
  const uint8_t counterVal = EEPROM.read(memStart + offset);
  // write the new counter position
  if (counterVal == 1) {
    EEPROM.write(counterPos, 0);
  } else {
    EEPROM.write(counterPos, 1);
  }
  // and the new value
  EEPROM.write(storagePos, newValue);
}

// this setup() code is only for testing/demonstration
// there should be no need to use any of it in your finished code
void setup() {

  Serial.begin(9600);
  delay(10);

  ringClear();
  ringWrite(10);
  ringDebug();
  ringWrite(11);
  ringDebug();
  ringWrite(12);
  ringDebug();
  ringWrite(13);
  ringDebug();
  ringWrite(14);
  ringDebug();
  ringWrite(15);
  ringDebug();

}

void loop() { }
By Karl Payne

Comments

There are no comments, don't be shy, somebody has to be first.

Leave a comment