diff --git a/I2C_eeprom.cpp b/I2C_eeprom.cpp index 7adc793..97a5427 100644 --- a/I2C_eeprom.cpp +++ b/I2C_eeprom.cpp @@ -57,11 +57,21 @@ I2C_eeprom::I2C_eeprom(const uint8_t deviceAddress, const unsigned int deviceSiz this->_isAddressSizeTwoWords = false; this->_pageSize = 16; } - else + else if(deviceSize <= 8192 * 8) { this->_isAddressSizeTwoWords = true; this->_pageSize = 32; } + else if(deviceSize <= 65536 * 8) + { + this->_isAddressSizeTwoWords = true; + this->_pageSize = 64; + } + else + { + this->_isAddressSizeTwoWords = true; + this->_pageSize = 128; + } } #if defined (ESP8266) || defined(ESP32) diff --git a/I2C_eeprom_cyclic_store.h b/I2C_eeprom_cyclic_store.h new file mode 100644 index 0000000..7e0c457 --- /dev/null +++ b/I2C_eeprom_cyclic_store.h @@ -0,0 +1,262 @@ +#pragma once +// +// FILE: I2C_eeprom_cyclic_access.h +// AUTHOR: Tomas Hübner +// VERSION: 1.0.0 +// PURPOSE: Supplemental utility class for I2C_EEPROM library +// + +#include + +/** + * @brief This is a utility class for using an eeprom to store a simple + * data structure. + * + * The purpose of the utility is to extend the life of an eeprom memory by + * rotating the writes over different pages to reduce the number of writes + * to any individual page. + * It does this by partitioning the memory into slots (of continuous pages) + * big enough to contain the data structure. When data is written to a slot + * it is given a header with a + * version number. + * On initialization the slots are scanned for the one with the highest + * version number which is then used for subsequent reads. + * Whenever data is written the version number is incremented and the next + * slot in sequence is used (or the first slot if going past the end). + * + * Note that the data is stored in binary form which means it should not be + * expected that the eeprom can be moved between architectures. Data stored + * in an eeprom will also become invalid if the data structure is changed + * either manually or due to changed optimization settings. + * + * If the data structure has changed or if the eeprom contains other data it + * must first be formatted with a call to format(). + * + * Finally, since the version number is a long word, this class will + * *derail and fail* to add new versions past 4294967295 writes. + * + * @tparam T the type of the data structure to store, should only contain + * **value** members and no constructor/destructor nor other + * methods/functions - e g a pure DTO. + */ +template +class I2C_eeprom_cyclic_store +{ +public: + /** + * @brief Initializes the instance + * + * This call searches the eeprom for the latest written version and + * sets the current slot accordingly. + * + * @param eeprom The instance of I2C_eeprom to use. + * @param pageSize The number of bytes in each write page. + * @param totalPages Specifies the total number of pages to use. + * Specifying a number that is less than the available pages will + * exclude the remaining pages from being used. + * @return True if initialization succeeds, false otherwise. + */ + bool begin(I2C_eeprom &eeprom, uint8_t pageSize, uint16_t totalPages) + { + _eeprom = &eeprom; + _pageSize = pageSize; + _totalPages = totalPages; + auto bufferSize = sizeof(_currentVersion) + sizeof(T); + _bufferPages = bufferSize / _pageSize + (bufferSize % _pageSize ? 1 : 0); + + return (_bufferPages < _totalPages) && initialize(); + }; + + /** + * @brief Formats the eeprom + * + * This must be done if the eeprom already contains data or if the + * structure of the data stored changes. + * + * Formatting is done by writing the max version number to each slot, + * thus it performs a write cycle to the first write page of each slot. + */ + void format() + { + // Reset the EEPROM by writing a ~0 into all pages + for (uint16_t slot = 0; slot < _totalPages; slot++) + { + _eeprom->writeBlock(slot * _pageSize, (uint8_t *)"\xff\xff\xff\xff", 4); + } + + _isEmpty = true; + _currentSlot = 0; + _isInitialized = true; + } + + /** + * @brief Read data from the eeprom into a buffer. + * + * The data is read from the current slot of the eeprom. + * + * @param buffer A reference to the buffer to read data into. + * @return True if data was read successfully, false otherwise. + */ + bool read(T &buffer) const { return read(&buffer); } + + /** + * @brief Read data from the eeprom into a buffer. + * + * The data is read from the current slot of the eeprom. + * + * @param buffer A pointet to the buffer to read data into. + * @return True if data was read successfully, false otherwise. + */ + bool read(T *buffer) const + { + if (!_isInitialized) + return false; + + if (_isEmpty) + return false; + + return _eeprom->readBlock((_currentSlot * _bufferPages * _pageSize) + sizeof(_currentVersion), (uint8_t *)buffer, sizeof(T)) == sizeof(T); + } + + /** + * @brief Write a buffer to the next slot in the eeprom. + * + * This updates the current slot of this instance. + * + * @param buffer A reference to the buffer to write data from. + * @return True if data was written successfully, false otherwise. + */ + bool write(T &buffer) { return write(&buffer); } + + /** + * Write data of object to the next slot in the eeprom. This updates + * the current slot of the instance. + * + * @param buffer A pointer to the buffer to write data from. + * @return True if data was written successfully, false otherwise. + */ + bool write(T *buffer) + { + if (!_isInitialized) + { + return false; + } + + if (_isEmpty) + { + _currentSlot = 0; + _currentVersion = 0; + } + else + { + _currentSlot++; + _currentVersion++; + + // Wrap around to start if going past end of alotted region + uint16_t maxSlots = _totalPages / _bufferPages; + if (_currentSlot >= maxSlots) + _currentSlot = 0; + } + + auto buffer_length = sizeof(_currentVersion) + sizeof(T); + uint8_t tmp[buffer_length]; + + memcpy(tmp, &_currentVersion, sizeof(_currentVersion)); + memcpy(tmp + sizeof(_currentVersion), buffer, sizeof(T)); + + auto success = _eeprom->writeBlock(_currentSlot * _bufferPages * _pageSize, tmp, buffer_length) == 0; + + if (success) + _isEmpty = false; + + return success; + } + + /** + * @brief Returns metrics for the eeprom usage. + * + * Dividing the returned values of \p writeCounter with \p slots yields the average number of + * writes to the individual write pages of the eeprom. This can be used to estimate the remaining + * number of possible writes. + * + * @param[out] slots The number of slots used to write the data buffer. + * @param[out] writeCounter The total number of write to the eeprom since the last format (or first use). + * @return True if the instance is initialized, false otherwise. + */ + bool getMetrics(uint16_t &slots, uint32_t &writeCounter) + { + if(!_isInitialized) + return false; + + slots = _totalPages / _bufferPages; + writeCounter = _isEmpty ? 0 : _currentVersion+1; + + return true; + } + +private: + uint8_t _pageSize; + uint16_t _bufferPages; + uint16_t _totalPages; + uint16_t _currentSlot; + uint32_t _currentVersion; + bool _isInitialized = false; + bool _isEmpty = false; + I2C_eeprom *_eeprom; + + bool initialize() + { + uint16_t startSlot, probeSlot, endSlot; + uint32_t current, probe; + auto slotSize = _pageSize * _bufferPages; + + startSlot = 0; + endSlot = (_totalPages / _bufferPages) - 1; // Index of last slot + probeSlot = startSlot + ((endSlot - startSlot) / 2); // Midway between start and end + + if(_eeprom->readBlock(0, (uint8_t *)¤t, sizeof(current)) != sizeof(current)) + { + return false; + } + + if (current == ~0UL) + { + // Memory is blank + _isEmpty = true; + _currentSlot = 0; + _currentVersion = 0; + _isInitialized = true; + return true; + } + + while (startSlot != probeSlot) + { + if(_eeprom->readBlock(probeSlot * slotSize, (uint8_t *)&probe, sizeof(current)) != sizeof(current)) + { + return false; + } + + if (probe == ~0UL || probe <= current) + { + // 1. Nothing has been written to the memory at Probe + // 2. The slots have the same timestamp, this shouldn't happen, treat as if Probe slot hasn't been written + // 3. Probe is older that Start, change End to slot before Probe + endSlot = probeSlot-1; + } + else + { + // 1. Probe is later than Start, change Start to Probe + startSlot = probeSlot; + current = probe; + } + probeSlot = startSlot + ((endSlot - startSlot + 1) / 2); + } + + _currentSlot = startSlot; + _currentVersion = current; + _isEmpty = false; + _isInitialized = true; + + return true; + } +}; diff --git a/examples/I2C_eeprom_cyclic_store/I2C_eeprom_cyclic_store.ino b/examples/I2C_eeprom_cyclic_store/I2C_eeprom_cyclic_store.ino new file mode 100644 index 0000000..a1d7b38 --- /dev/null +++ b/examples/I2C_eeprom_cyclic_store/I2C_eeprom_cyclic_store.ino @@ -0,0 +1,54 @@ +// +// FILE: I2C_eeprom_cyclic_store.ino +// AUTHOR: Tomas Hübner +// VERSION: 1.0.0 +// PURPOSE: Simple example of how to use cyclic storage. +// + +#include +#include + +#define MEMORY_SIZE 0x2000 // Total capacity of the EEPROM +#define PAGE_SIZE 64 // Size of write page of device, use datasheet to find! + +struct SampleData { +public: + uint32_t counter; + // Must use fixed length string, avoid using the String class. + char message[32]; +}; + +SampleData data; + +I2C_eeprom ee(0x50, MEMORY_SIZE); +I2C_eeprom_cyclic_store cs; + +void setup() +{ + Serial.begin(115200); + while(!Serial); + + ee.begin(); + + cs.begin(ee, PAGE_SIZE, MEMORY_SIZE/PAGE_SIZE); + + if(!cs.read(data)) + { + // The eeprom is uninitialized + data.counter = 1; + sprintf(data.message, "Initialized to %x", data.counter); + cs.write(data); + } + +} + +void loop() +{ + Serial.println(data.message); + + data.counter++; + sprintf(data.message, "Written %x times", data.counter); + cs.write(data); + + delay(10000); +} diff --git a/keywords.txt b/keywords.txt index 8b4bc9e..3c05d24 100644 --- a/keywords.txt +++ b/keywords.txt @@ -2,8 +2,12 @@ # Datatypes (KEYWORD1) I2C_eeprom KEYWORD1 +I2C_eeprom_cyclic_store KEYWORD1 # Methods and Functions (KEYWORD2) +# Common +begin KEYWORD2 +# I2C_eeprom readByte KEYWORD2 writeByte KEYWORD2 setBlock KEYWORD2 @@ -11,5 +15,10 @@ readBlock KEYWORD2 writeBlock KEYWORD2 determineSize KEYWORD2 updateByte KEYWORD2 +# I2C_eeprom_cyclic_store +format KEYWORD2 +read KEYWORD2 +write KEYWORD2 +getMetrics KEYWORD2 # Constants (LITERAL1)