Optimize EEPROM Writes Across Pages

Because I’m an embedded developer, I often work on projects where I need to store some data on an extern EEPROM or Flash chip. The internal memory of these chips is usually divided up into fixed sized pages. It’s often the case that you’re not allowed to write more than a page at a time. This makes things complicated if you want to do a write that spans multiple pages. It’s even more tricky if you want to support wrap-around, which turns out to be very handy in certain situations.

Wrap-Around is Possible

Optimize EEPROM/Flash writes across multiple pages, with multiple regions and handles wrap-around. I recently came up with an algorithm that neatly handles this. I thought I’d share my solution since, while it took a while to think up, the result is quite simple.

#define EEPROM_PAGE_SIZE (32)

struct eeprom_region {
  // start address of the region
  size_t start;
  // size of the region
  size_t size;

void eeprom_region_write(
  // the region you want to write into.
  struct eeprom_region const dest_region,
  // the offset within that region 
  //   this is allowed to be much bigger than the actual
  //   region size. If you write past the end of the
  //   region you'll loop back to the beginning.
  size_t dest_offset,
  // the source buffer you want to write to eeprom
  uint8_t const * p_src,
  // the length of the source buffer
  size_t length)
  while(length > 0)
    // calculate the actual starting offset in the region
    size_t region_offset           = dest_offset % dest_region.size;
    // calculate the real eeprom starting address
    size_t addr                    = region_offset + dest_region.start;
    // calculate the offset in the page containing our starting address,
    size_t page_offset             = addr % EEPROM_PAGE_SIZE;
    // calculate the largest write we can do in this region without wrapping
    size_t max_region_write_length = dest_region.size - region_offset;
    // calculate the largest write we can do in this page without wrapping
    size_t max_page_write_length   = EEPROM_PAGE_SIZE - page_offset; 
    // out of: length, max_page_write_length, and max_region_write_length, 
    //  pick the smallest. This is our write length for the next "chunk"
    size_t write_length            = MIN(MIN(max_page_write_length, length),

    // write bytes to eeprom
    eeprom_page_write(addr, p_src, write_length);

    // subtract what we've written from the length
    length -= write_length;
    // increment our source pointer
    p_src += write_length;
    // increment our destination offset.
    dest_offset += write_length;
    // and repeat until there's nothing left to write

// And for reading:
//   this assumes you can read continuously across page
///  boundaries (which is usually allowed) 
//   it only has to account for region boundaries
void eeprom_region_read(uint8_t * p_dest,
                        struct eeprom_region const src_region,
                        size_t src_offset,
                        size_t length)
  // at most this loops twice
  while(length > 0)
    size_t region_offset           = src_offset % src_region.size;
    size_t max_region_read_length  = src_region.size - region_offset;
    size_t addr                    = region_offset + src_region.start;
    size_t read_length             = MIN(max_region_read_length, length);

    // read bytes from eeprom
    eeprom_sequential_read(p_dest, addr, read_length);

    length -= read_length;
    p_dest += read_length;
    src_offset += read_length;

Wrap-Around is Handy

The wrap around turns out to be extremely handy, especially for logs. It essentially allows you to have an effectively infinite memory space, with the caveat that only that last region.size bytes are saved. As a result, you can just keep appending to your log forever, and then when you want to read it, you just read out the last region.size bytes.