diff --git a/src/hal/eeprom.h b/src/hal/eeprom.h index 231eeff..8cffa0d 100644 --- a/src/hal/eeprom.h +++ b/src/hal/eeprom.h @@ -1,12 +1,20 @@ #pragma once +#include namespace hal { namespace EEPROM { -/// EEPROM interface -void WriteByte(uint16_t addr, uint8_t value); -void UpdateByte(uint16_t addr, uint8_t value); -uint8_t ReadByte(uint16_t addr); + /// EEPROM interface + void WriteByte(const uint8_t *addr, uint8_t value); + void UpdateByte(const uint8_t *addr, uint8_t value); + uint8_t ReadByte(const uint8_t *addr); + + void WriteWord(const uint8_t *addr, uint16_t value); + void UpdateWord(const uint8_t *addr, uint16_t value); + uint16_t ReadWord(const uint8_t *addr); + + /// @returns physical end address of EEPROM memory end + constexpr const uint16_t End(); } // namespace EEPROM } // namespace hal diff --git a/src/modules/permanent_storage.cpp b/src/modules/permanent_storage.cpp new file mode 100644 index 0000000..038837c --- /dev/null +++ b/src/modules/permanent_storage.cpp @@ -0,0 +1,354 @@ +/// @author Marek Bel +#include "permanent_storage.h" +#include "../hal/eeprom.h" + +namespace modules { +namespace permanent_storage { + +#define ARR_SIZE(ARRAY) (sizeof(ARRAY) / sizeof(ARRAY[0])) + + /// @brief EEPROM data layout + /// + /// Do not remove, reorder or change size of existing fields. + /// Otherwise values stored with previous version of firmware would be broken. + /// It is possible to add fields in the end of this struct, ensuring that erased EEPROM is handled well. + /// Last byte in EEPROM is reserved for layoutVersion. If some field is repurposed, layoutVersion + /// needs to be changed to force an EEPROM erase. + struct eeprom_t { + uint8_t eepromLengthCorrection; ///< Legacy bowden length correction + uint16_t eepromBowdenLen[5]; ///< Bowden length for each filament + uint8_t eepromFilamentStatus[3]; ///< Majority vote status of eepromFilament wear leveling + uint8_t eepromFilament[800]; ///< Top nibble status, bottom nibble last filament loaded + uint8_t eepromDriveErrorCountH; + uint8_t eepromDriveErrorCountL[2]; + } __attribute__((packed)); + + // @@TODO static_assert(sizeof(eeprom_t) - 2 <= hal::EEPROM::End(), "eeprom_t doesn't fit into EEPROM available."); + + /// @brief EEPROM layout version + static const uint8_t layoutVersion = 0xff; + + //d = 6.3 mm pulley diameter + //c = pi * d pulley circumference + //FSPR = 200 full steps per revolution (stepper motor constant) (1.8 deg/step) + //mres = 2 pulley microstep resolution (uint8_t __res(AX_PUL)) + //mres = 2 selector microstep resolution (uint8_t __res(AX_SEL)) + //mres = 16 idler microstep resolution (uint8_t __res(AX_IDL)) + //1 pulley ustep = (d*pi)/(mres*FSPR) = 49.48 um + + static eeprom_t *const eepromBase = reinterpret_cast(0); ///< First EEPROM address + static const uint16_t eepromEmpty = 0xffff; ///< EEPROM content when erased + static const uint16_t eepromLengthCorrectionBase = 7900u; ///< legacy bowden length correction base (~391mm) + static const uint16_t eepromBowdenLenDefault = 8900u; ///< Default bowden length (~427 mm) + static const uint16_t eepromBowdenLenMinimum = 6900u; ///< Minimum bowden length (~341 mm) + static const uint16_t eepromBowdenLenMaximum = 16000u; ///< Maximum bowden length (~792 mm) + + void Init() { + if (hal::EEPROM::ReadByte((const uint8_t *)hal::EEPROM::End()) != layoutVersion) { + EraseAll(); + } + } + + /// @brief Erase the whole EEPROM + void EraseAll() { + for (uint16_t i = 0; i < hal::EEPROM::End(); i++) { + hal::EEPROM::UpdateByte((uint8_t *)i, static_cast(eepromEmpty)); + } + hal::EEPROM::UpdateByte((const uint8_t *)hal::EEPROM::End(), layoutVersion); + } + + /// @brief Is filament number valid? + /// @retval true valid + /// @retval false invalid + static bool validFilament(uint8_t filament) { + return filament < ARR_SIZE(eeprom_t::eepromBowdenLen); + } + + /// @brief Is bowden length in valid range? + /// @param BowdenLength bowden length + /// @retval true valid + /// @retval false invalid + static bool validBowdenLen(const uint16_t BowdenLength) { + if ((BowdenLength >= eepromBowdenLenMinimum) + && BowdenLength <= eepromBowdenLenMaximum) { + return true; + } + return false; + } + + /// @brief Get bowden length for active filament + /// + /// Returns stored value, doesn't return actual value when it is edited by increase() / decrease() unless it is stored. + /// @return stored bowden length + uint16_t BowdenLength::get() { + uint8_t filament = 0 /*active_extruder*/; //@@TODO + if (validFilament(filament)) { + uint16_t bowdenLength = hal::EEPROM::ReadByte((const uint8_t *)&(eepromBase->eepromBowdenLen[filament])); + + if (eepromEmpty == bowdenLength) { + const uint8_t LengthCorrectionLegacy = hal::EEPROM::ReadByte(&(eepromBase->eepromLengthCorrection)); + if (LengthCorrectionLegacy <= 200) { + bowdenLength = eepromLengthCorrectionBase + LengthCorrectionLegacy * 10; + } + } + if (validBowdenLen(bowdenLength)) + return bowdenLength; + } + + return eepromBowdenLenDefault; + } + + /// @brief Construct BowdenLength object which allows bowden length manipulation + /// + /// To be created on stack, new value is permanently stored when object goes out of scope. + /// Active filament and associated bowden length is stored in member variables. + BowdenLength::BowdenLength() + : filament(/*active_extruder*/ 0) + , length(BowdenLength::get()) // @@TODO + { + } + + /// @brief Increase bowden length + /// + /// New value is not stored immediately. See ~BowdenLength() for storing permanently. + /// @retval true passed + /// @retval false failed, it is not possible to increase, new bowden length would be out of range + bool BowdenLength::increase() { + if (validBowdenLen(length + stepSize)) { + length += stepSize; + return true; + } + return false; + } + + /// @brief Decrease bowden length + /// + /// New value is not stored immediately. See ~BowdenLength() for storing permanently. + /// @retval true passed + /// @retval false failed, it is not possible to decrease, new bowden length would be out of range + bool BowdenLength::decrease() { + if (validBowdenLen(length - stepSize)) { + length -= stepSize; + return true; + } + return false; + } + + /// @brief Store bowden length permanently. + BowdenLength::~BowdenLength() { + if (validFilament(filament)) + hal::EEPROM::UpdateWord((const uint8_t *)&(eepromBase->eepromBowdenLen[filament]), length); + } + + /// @brief Get filament storage status + /// + /// Uses 2 out of 3 majority vote. + /// + /// @return status + /// @retval 0xff Uninitialized EEPROM or no 2 values agrees + + uint8_t FilamentLoaded::getStatus() { + if (hal::EEPROM::ReadByte(&(eepromBase->eepromFilamentStatus[0])) == hal::EEPROM::ReadByte(&(eepromBase->eepromFilamentStatus[1]))) + return hal::EEPROM::ReadByte(&(eepromBase->eepromFilamentStatus[0])); + if (hal::EEPROM::ReadByte(&(eepromBase->eepromFilamentStatus[0])) == hal::EEPROM::ReadByte(&(eepromBase->eepromFilamentStatus[2]))) + return hal::EEPROM::ReadByte(&(eepromBase->eepromFilamentStatus[0])); + if (hal::EEPROM::ReadByte(&(eepromBase->eepromFilamentStatus[1])) == hal::EEPROM::ReadByte(&(eepromBase->eepromFilamentStatus[2]))) + return hal::EEPROM::ReadByte(&(eepromBase->eepromFilamentStatus[1])); + return 0xff; + } + + /// @brief Set filament storage status + /// + /// @retval true Succeed + /// @retval false Failed + bool FilamentLoaded::setStatus(uint8_t status) { + for (uint8_t i = 0; i < ARR_SIZE(eeprom_t::eepromFilamentStatus); ++i) { + hal::EEPROM::UpdateByte(&(eepromBase->eepromFilamentStatus[i]), status); + } + if (getStatus() == status) + return true; + return false; + } + + /// @brief Get index of last valid filament + /// + /// Depending on status, it searches from the beginning or from the end of eepromFilament[] + /// for the first non-matching status. Previous index (of matching status, or out of array bounds) + /// is returned. + /// + /// @return index to eepromFilament[] of last valid value + /// it can be out of array range, if first item status doesn't match expected status + /// getNext(index, status) turns it to first valid index. + int16_t FilamentLoaded::getIndex() { + const uint8_t status = getStatus(); + int16_t index = -1; + switch (status) { + case KeyFront1: + case KeyFront2: + index = ARR_SIZE(eeprom_t::eepromFilament) - 1; // It is the last one, if no dirty index found + for (uint16_t i = 0; i < ARR_SIZE(eeprom_t::eepromFilament); ++i) { + if (status != (hal::EEPROM::ReadByte(&(eepromBase->eepromFilament[i])) >> 4)) { + index = i - 1; + break; + } + } + break; + case KeyReverse1: + case KeyReverse2: + index = 0; // It is the last one, if no dirty index found + for (int16_t i = (ARR_SIZE(eeprom_t::eepromFilament) - 1); i >= 0; --i) { + if (status != (hal::EEPROM::ReadByte(&(eepromBase->eepromFilament[i])) >> 4)) { + index = i + 1; + break; + } + } + break; + default: + break; + } + return index; + } + + /// @brief Get last filament loaded + /// @param [in,out] filament filament number 0 to 4 + /// @retval true success + /// @retval false failed + bool FilamentLoaded::get(uint8_t &filament) { + int16_t index = getIndex(); + if ((index < 0) || (static_cast(index) >= ARR_SIZE(eeprom_t::eepromFilament))) + return false; + const uint8_t rawFilament = hal::EEPROM::ReadByte(&(eepromBase->eepromFilament[index])); + filament = 0x0f & rawFilament; + if (filament > 4) + return false; + const uint8_t status = getStatus(); + if (!(status == KeyFront1 + || status == KeyReverse1 + || status == KeyFront2 + || status == KeyReverse2)) + return false; + if ((rawFilament >> 4) != status) + return false; + return true; + } + + /// @brief Set filament being loaded + /// + /// Always fails, if it is not possible to store status. + /// If it is not possible store filament, it tries all other + /// keys. Fails if storing with all other keys failed. + /// + /// @param filament bottom 4 bits are stored + /// but only value 0 to 4 passes validation in FilamentLoaded::get() + /// @retval true success + /// @retval false failed + bool FilamentLoaded::set(uint8_t filament) { + for (uint8_t i = 0; i < BehindLastKey - 1; ++i) { + uint8_t status = getStatus(); + int16_t index = getIndex(); + getNext(status, index); + if (!setStatus(status)) + return false; + uint8_t filamentRaw = ((status << 4) & 0xf0) + (filament & 0x0f); + hal::EEPROM::UpdateByte(&(eepromBase->eepromFilament[index]), filamentRaw); + if (filamentRaw == hal::EEPROM::ReadByte(&(eepromBase->eepromFilament[index]))) + return true; + getNext(status); + if (!setStatus(status)) + return false; + } + return false; + } + + /// @brief Get next status and index + /// + /// Get next available index following index input parameter to store filament in eepromFilament[]. + /// If index would reach behind indexable space, status is updated to next and first index matching status indexing mode is returned. + /// @param [in,out] status + /// @param [in,out] index + void FilamentLoaded::getNext(uint8_t &status, int16_t &index) { + switch (status) { + case KeyFront1: + case KeyFront2: + ++index; + if ((index < 0) || (static_cast(index) >= ARR_SIZE(eeprom_t::eepromFilament))) { + getNext(status); + index = ARR_SIZE(eeprom_t::eepromFilament) - 1; + } + break; + case KeyReverse1: + case KeyReverse2: + --index; + if ((index < 0) || (static_cast(index) >= ARR_SIZE(eeprom_t::eepromFilament))) { + getNext(status); + index = 0; + } + break; + default: + status = KeyFront1; + index = 0; + break; + } + } + + /// @brief Get next status + /// + /// Sets status to next indexing mode. + /// + /// @param [in,out] status + void FilamentLoaded::getNext(uint8_t &status) { + switch (status) { + case KeyFront1: + status = KeyReverse1; + break; + case KeyReverse1: + status = KeyFront2; + break; + case KeyFront2: + status = KeyReverse2; + break; + case KeyReverse2: + status = KeyFront1; + break; + default: + status = KeyFront1; + break; + } + } + + uint16_t DriveError::get() { + return ((static_cast(getH()) << 8) + getL()); + } + + void DriveError::increment() { + uint16_t errors = get(); + if (errors < 0xffff) { + ++errors; + setL(errors); + setH(errors >> 8); + } + } + + uint8_t DriveError::getL() { + uint8_t first = hal::EEPROM::ReadByte(&(eepromBase->eepromDriveErrorCountL[0])); + uint8_t second = hal::EEPROM::ReadByte(&(eepromBase->eepromDriveErrorCountL[1])); + + if (0xff == first && 0 == second) + return 1; + return (first > second) ? ++first : ++second; + } + + void DriveError::setL(uint8_t lowByte) { + hal::EEPROM::UpdateByte(&(eepromBase->eepromDriveErrorCountL[lowByte % 2]), lowByte - 1); + } + + uint8_t DriveError::getH() { + return (hal::EEPROM::ReadByte(&(eepromBase->eepromDriveErrorCountH)) + 1); + } + + void DriveError::setH(uint8_t highByte) { + hal::EEPROM::UpdateByte(&(eepromBase->eepromDriveErrorCountH), highByte - 1); + } + +} // namespace permanent_storage +} // namespace modules diff --git a/src/modules/permanent_storage.h b/src/modules/permanent_storage.h index bb1e70e..5a2c71b 100644 --- a/src/modules/permanent_storage.h +++ b/src/modules/permanent_storage.h @@ -1,15 +1,98 @@ +/// Permanent storage implementation +/// This is the logic/wear levelling/data structure on top of the raw EEPROM API +/// @author Marek Bel +/// Extracted and refactored from MM-control-01 #pragma once #include "../hal/eeprom.h" -/// Permanent storage implementation -/// This is the logic/wear levelling/data structure on top of the raw EEPROM API - namespace modules { +namespace permanent_storage { -class PermanentStorage { + void Init(); + void EraseAll(); - /// @@TODO extract from the current MMU implementation and wrap it into this structure -}; + /// @brief Read manipulate and store bowden length + /// + /// Value is stored independently for each filament. + /// Active filament is deduced from active_extruder global variable. + class BowdenLength { + public: + static uint16_t get(); + static const uint8_t stepSize = 10u; ///< increase()/decrease() bowden length step size + BowdenLength(); + bool increase(); + bool decrease(); + ~BowdenLength(); + private: + uint8_t filament; ///< Selected filament + uint16_t length; ///< Selected filament bowden length + }; + + /// @brief Read and store last filament loaded to nozzle + /// + /// 800(data) + 3(status) EEPROM cells are used to store 4 bit value frequently + /// to spread wear between more cells to increase durability. + /// + /// Expected worst case durability scenario: + /// @n Print has 240mm height, layer height is 0.1mm, print takes 10 hours, + /// filament is changed 5 times each layer, EEPROM endures 100 000 cycles + /// @n Cell written per print: 240/0.1*5/800 = 15 + /// @n Cell written per hour : 15/10 = 1.5 + /// @n Fist cell failure expected: 100 000 / 1.5 = 66 666 hours = 7.6 years + /// + /// Algorithm can handle one cell failure in status and one cell in data. + /// Status use 2 of 3 majority vote. + /// If bad data cell is detected, status is switched to next key. + /// Key alternates between begin to end and end to begin write order. + /// Two keys are needed for each start point and direction. + /// If two data cells fails, area between them is unavailable to write. + /// If this is first and last cell, whole storage is disabled. + /// This vulnerability can be avoided by adding additional keys + /// and start point in the middle of the EEPROM. + /// + /// It would be possible to implement twice as efficient algorithm, if + /// separate EEPROM erase and EEPROM write commands would be available and + /// if write command would allow to be invoked twice between erases to update + /// just one nibble. Such commands are not available in AVR Libc, and possibility + /// to use write command twice is not documented in atmega32U4 datasheet. + /// + class FilamentLoaded { + public: + static bool get(uint8_t &filament); + static bool set(uint8_t filament); + + private: + enum Key { + KeyFront1, + KeyReverse1, + KeyFront2, + KeyReverse2, + BehindLastKey, + }; + static_assert(BehindLastKey - 1 <= 0xf, "Key doesn't fit into a nibble."); + static uint8_t getStatus(); + static bool setStatus(uint8_t status); + static int16_t getIndex(); + static void getNext(uint8_t &status, int16_t &index); + static void getNext(uint8_t &status); + }; + + /// @brief Read and increment drive errors + /// + /// (Motor power rail voltage loss) + class DriveError { + public: + static uint16_t get(); + static void increment(); + + private: + static uint8_t getL(); + static void setL(uint8_t lowByte); + static uint8_t getH(); + static void setH(uint8_t highByte); + }; + +} // namespace permanent_storage } // namespace modules