mirror of
https://github.com/synthetos/g2.git
synced 2026-02-05 10:39:53 +08:00
Cleanup SD card persistence somewhat
This commit is contained in:
@@ -38,6 +38,7 @@
|
|||||||
#include "MotateUniqueID.h"
|
#include "MotateUniqueID.h"
|
||||||
#include "MotatePower.h"
|
#include "MotatePower.h"
|
||||||
#include "board_spi.h"
|
#include "board_spi.h"
|
||||||
|
#include "sd_persistence.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* hardware_init() - lowest level hardware init
|
* hardware_init() - lowest level hardware init
|
||||||
@@ -48,10 +49,12 @@ SPIBus_used_t spiBus;
|
|||||||
Motate::SPIChipSelectPin<Motate::kSD_ChipSelectPinNumber> sdcs{};
|
Motate::SPIChipSelectPin<Motate::kSD_ChipSelectPinNumber> sdcs{};
|
||||||
SDCard_used_t sd_card{spiBus, sdcs};
|
SDCard_used_t sd_card{spiBus, sdcs};
|
||||||
|
|
||||||
|
|
||||||
void hardware_init()
|
void hardware_init()
|
||||||
{
|
{
|
||||||
spiBus.init();
|
spiBus.init();
|
||||||
sd_card.init();
|
sd_card.init();
|
||||||
|
setup_sd_persistence();
|
||||||
board_hardware_init();
|
board_hardware_init();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
403
g2core/device/sd_card/sd_persistence.cpp
Normal file
403
g2core/device/sd_card/sd_persistence.cpp
Normal file
@@ -0,0 +1,403 @@
|
|||||||
|
/*
|
||||||
|
* sd_persistence.cpp - persistence functions for SD cards
|
||||||
|
* This file is part of the g2core project
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 Matt Staniszewski
|
||||||
|
* Copyright (c) 2019 Robert Giseburt
|
||||||
|
*
|
||||||
|
* This file ("the software") is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License, version 2 as published by the
|
||||||
|
* Free Software Foundation. You should have received a copy of the GNU General Public
|
||||||
|
* License, version 2 along with the software. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* As a special exception, you may use this file as part of a software library without
|
||||||
|
* restriction. Specifically, if other files instantiate templates or use macros or
|
||||||
|
* inline functions from this file, or you compile this file and link it with other
|
||||||
|
* files to produce an executable, this file does not by itself cause the resulting
|
||||||
|
* executable to be covered by the GNU General Public License. This exception does not
|
||||||
|
* however invalidate any other reasons why the executable file might be covered by the
|
||||||
|
* GNU General Public License.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, BUT WITHOUT ANY
|
||||||
|
* WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
* SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "g2core.h"
|
||||||
|
#include "persistence.h"
|
||||||
|
#include "canonical_machine.h"
|
||||||
|
#include "report.h"
|
||||||
|
#include "util.h"
|
||||||
|
#include "board_spi.h"
|
||||||
|
#include "ff.h"
|
||||||
|
|
||||||
|
#include "sd_persistence.h" // needed for nvObj_t definition
|
||||||
|
#include "config.h" // needed for nvObj_t definition
|
||||||
|
|
||||||
|
#define NVM_VALUE_LEN 4 // NVM value length (float, fixed length)
|
||||||
|
#define NVM_BASE_ADDR 0x0000 // base address of usable NVM
|
||||||
|
|
||||||
|
#define IO_BUFFER_SIZE 512 // this should be evenly divisible by NVM_VALUE_LEN, and <=512 until multi-block reads are fixed (right now they are hanging...)
|
||||||
|
#define MIN_WRITE_INTERVAL 1000 // minimum interval between persistence file writes
|
||||||
|
#define MAX_WRITE_FAILURES 3
|
||||||
|
#define MAX_WRITE_CHANGES IO_BUFFER_SIZE // maximum number of write values that change - ms: TODO
|
||||||
|
|
||||||
|
/***********************************************************************************
|
||||||
|
**** STRUCTURE ALLOCATIONS ********************************************************
|
||||||
|
***********************************************************************************/
|
||||||
|
|
||||||
|
//**** persistence singleton ****
|
||||||
|
|
||||||
|
struct nvmSingleton_t {
|
||||||
|
float tmp_value;
|
||||||
|
FATFS fat_fs;
|
||||||
|
FIL file;
|
||||||
|
uint8_t file_index;
|
||||||
|
alignas(4) uint8_t io_buffer[IO_BUFFER_SIZE];
|
||||||
|
uint16_t changed_nvs;
|
||||||
|
uint32_t last_write_systick;
|
||||||
|
uint8_t write_failures;
|
||||||
|
} nvm;
|
||||||
|
|
||||||
|
/***********************************************************************************
|
||||||
|
**** GENERIC STATIC FUNCTIONS AND VARIABLES ***************************************
|
||||||
|
***********************************************************************************/
|
||||||
|
|
||||||
|
stat_t prepare_persistence_file();
|
||||||
|
stat_t write_persistent_values();
|
||||||
|
stat_t validate_persistence_file();
|
||||||
|
uint8_t active_file_index();
|
||||||
|
|
||||||
|
// Leaving this in for now in case bugs come up; we can remove it when we're confident
|
||||||
|
// it's stable
|
||||||
|
#if 0
|
||||||
|
# define DEBUG_PRINT(...) printf(__VA_ARGS__);
|
||||||
|
#else
|
||||||
|
# define DEBUG_PRINT(...) do {} while (0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Analogous to ritorno(a), but for the FatFS return codes and with optional debug output
|
||||||
|
#define fs_ritorno(a, msg) if((status_code=a) != FR_OK) \
|
||||||
|
{ DEBUG_PRINT("%s res: %i\n", msg, status_code); return(STAT_PERSISTENCE_ERROR); }
|
||||||
|
|
||||||
|
/*
|
||||||
|
We cycle between three different files, indexed by a suffix. Each time we need to write
|
||||||
|
new values, we copy data from the current file to a new file with NEXT_FILE_INDEX, then
|
||||||
|
delete the current file once the write is complete. This ensures that at least one recent
|
||||||
|
copy of the file will be preserved if power is lost in the middle of a write.
|
||||||
|
*/
|
||||||
|
#define PERSISTENCE_DIR "persist"
|
||||||
|
#define PERSISTENCE_FILENAME(num) PERSISTENCE_DIR"/persist"#num".bin"
|
||||||
|
#define PERSISTENCE_FILENAME_CNT 3
|
||||||
|
#define NEXT_FILE_INDEX (nvm.file_index+1) % PERSISTENCE_FILENAME_CNT
|
||||||
|
#define PREV_FILE_INDEX (nvm.file_index+PERSISTENCE_FILENAME_CNT-1) % PERSISTENCE_FILENAME_CNT
|
||||||
|
const char* filenames[PERSISTENCE_FILENAME_CNT] = {
|
||||||
|
PERSISTENCE_FILENAME(0), PERSISTENCE_FILENAME(1), PERSISTENCE_FILENAME(2) };
|
||||||
|
|
||||||
|
#define CRC_LEN 4
|
||||||
|
|
||||||
|
/***********************************************************************************
|
||||||
|
**** CODE *************************************************************************
|
||||||
|
***********************************************************************************/
|
||||||
|
|
||||||
|
class SD_Persistence : public Persistence {
|
||||||
|
public:
|
||||||
|
void init() override;
|
||||||
|
stat_t read(nvObj_t *nv) override;
|
||||||
|
stat_t write(nvObj_t *nv) override;
|
||||||
|
stat_t periodic() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
SD_Persistence sdp {};
|
||||||
|
|
||||||
|
void setup_sd_persistence() {
|
||||||
|
persistence = &sdp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SD_Persistence::init()
|
||||||
|
{
|
||||||
|
nvm.file_index = 0;
|
||||||
|
nvm.last_write_systick = Motate::SysTickTimer.getValue();
|
||||||
|
nvm.write_failures = 0;
|
||||||
|
nvm.changed_nvs = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* read_persistent_value() - return value (as float) by index
|
||||||
|
*
|
||||||
|
* It's the responsibility of the caller to make sure the index does not exceed range
|
||||||
|
*/
|
||||||
|
|
||||||
|
stat_t SD_Persistence::read(nvObj_t *nv)
|
||||||
|
{
|
||||||
|
ritorno(prepare_persistence_file());
|
||||||
|
DEBUG_PRINT("file opened for reading\n");
|
||||||
|
fs_ritorno(f_lseek(&nvm.file, nv->index * NVM_VALUE_LEN), "f_lseek during read");
|
||||||
|
UINT br;
|
||||||
|
fs_ritorno(f_read(&nvm.file, &nvm.io_buffer, NVM_VALUE_LEN, &br), "read value");
|
||||||
|
if (br != NVM_VALUE_LEN) {
|
||||||
|
return (STAT_PERSISTENCE_ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfgArray[nv->index].flags & TYPE_INTEGER) {
|
||||||
|
nv->valuetype = TYPE_INTEGER;
|
||||||
|
nv->value_int = *(int32_t *)nvm.io_buffer;
|
||||||
|
DEBUG_PRINT("value (i) copied from address %l in file: %l\n", nv->index * NVM_VALUE_LEN, nv->value_int);
|
||||||
|
} else if (cfgArray[nv->index].flags & TYPE_BOOLEAN) {
|
||||||
|
nv->valuetype = TYPE_BOOLEAN;
|
||||||
|
nv->value_int = *(int32_t *)nvm.io_buffer;
|
||||||
|
DEBUG_PRINT("value (b) copied from address %l in file: %l\n", nv->index * NVM_VALUE_LEN, nv->value_int);
|
||||||
|
} else {
|
||||||
|
nv->valuetype = TYPE_FLOAT;
|
||||||
|
nv->value_flt = *(float *)nvm.io_buffer;
|
||||||
|
DEBUG_PRINT("value (f) copied from address %l in file: %f\n", nv->index * NVM_VALUE_LEN, nv->value_flt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (STAT_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* write_persistent_value() - write to NVM by index, but only if the value has changed
|
||||||
|
*
|
||||||
|
* It's the responsibility of the caller to make sure the index does not exceed range
|
||||||
|
* Note: Removed NAN and INF checks on floats - not needed
|
||||||
|
*/
|
||||||
|
|
||||||
|
stat_t SD_Persistence::write(nvObj_t *nv)
|
||||||
|
{
|
||||||
|
nvm.changed_nvs++;
|
||||||
|
return (STAT_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* write_persistent_values_callback()
|
||||||
|
*
|
||||||
|
* On ARM, write cached values to a file. No-op on AVR.
|
||||||
|
*/
|
||||||
|
|
||||||
|
stat_t SD_Persistence::periodic()
|
||||||
|
{
|
||||||
|
// Check the disk status to ensure we catch card-detect pin changes.
|
||||||
|
// FIXME: it would be much better to do this with an interrupt!
|
||||||
|
f_polldisk();
|
||||||
|
if (nvm.changed_nvs > 0) {
|
||||||
|
if (Motate::SysTickTimer.getValue() - nvm.last_write_systick < MIN_WRITE_INTERVAL) {
|
||||||
|
return (STAT_NOOP);
|
||||||
|
}
|
||||||
|
// this check may not be necessary on ARM, but just in case...
|
||||||
|
if (cm->cycle_type != CYCLE_NONE) {
|
||||||
|
return(STAT_NOOP); // can't write when machine is moving
|
||||||
|
}
|
||||||
|
|
||||||
|
if(write_persistent_values() == STAT_OK) {
|
||||||
|
nvm.changed_nvs = 0;
|
||||||
|
nvm.write_failures = 0;
|
||||||
|
} else {
|
||||||
|
// if the write failed, make sure no half-written output file exists
|
||||||
|
f_unlink(filenames[NEXT_FILE_INDEX]);
|
||||||
|
if (++nvm.write_failures >= MAX_WRITE_FAILURES) {
|
||||||
|
nvm.changed_nvs = 0; // give up on these values
|
||||||
|
nvm.write_failures = 0; // but try again if we get more values later
|
||||||
|
return(rpt_exception(STAT_PERSISTENCE_ERROR, NULL));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
nvm.last_write_systick = Motate::SysTickTimer.getValue();
|
||||||
|
return STAT_OK;
|
||||||
|
}
|
||||||
|
return STAT_NOOP;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* active_file_index()
|
||||||
|
*
|
||||||
|
* Determines which of the existing files is most current and should be used for
|
||||||
|
* value reads. If no files exist, return 0. This assumes that no more than two
|
||||||
|
* files exist at any one time, which should always be the case under our updating
|
||||||
|
* scheme (described in a comment near the PERSISTENCE_FILENAME define).
|
||||||
|
*/
|
||||||
|
uint8_t active_file_index()
|
||||||
|
{
|
||||||
|
uint8_t i = 0;
|
||||||
|
for (; i < PERSISTENCE_FILENAME_CNT; ++i) {
|
||||||
|
if (f_stat(filenames[i], nullptr) == FR_OK) {
|
||||||
|
uint8_t next = (i+1)%PERSISTENCE_FILENAME_CNT;
|
||||||
|
if (next > i && f_stat(filenames[next], nullptr) == FR_OK) {
|
||||||
|
i = next;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* prepare_persistence_file()
|
||||||
|
*
|
||||||
|
* ARM only. Ensures that the file is open and has a valid CRC. This should be called
|
||||||
|
* prior to using the file in any other function.
|
||||||
|
*/
|
||||||
|
stat_t prepare_persistence_file()
|
||||||
|
{
|
||||||
|
// if the file is already open and valid, no further prep is necessary.
|
||||||
|
// NOTE: we don't close the file after every use because the higher latency
|
||||||
|
// would slow down consecutive reads. However, we still need to re-validate before
|
||||||
|
// every use to ensure that the card status hasn't changed.
|
||||||
|
if (f_is_open(&nvm.file) && validate(&nvm.file) == FR_OK) return STAT_OK;
|
||||||
|
|
||||||
|
// mount volume if necessary
|
||||||
|
if (!nvm.fat_fs.fs_type) {
|
||||||
|
fs_ritorno(f_mount(&nvm.fat_fs, "", 1), "mount"); /* Give a work area to the default drive */
|
||||||
|
}
|
||||||
|
f_mkdir(PERSISTENCE_DIR);
|
||||||
|
uint8_t index = active_file_index();
|
||||||
|
fs_ritorno(f_open(&nvm.file, filenames[index], FA_READ | FA_OPEN_EXISTING), "open input");
|
||||||
|
nvm.file_index = index;
|
||||||
|
|
||||||
|
// if CRC doesn't match, delete file and return error
|
||||||
|
if (validate_persistence_file() != STAT_OK) {
|
||||||
|
f_close(&nvm.file);
|
||||||
|
f_unlink(filenames[nvm.file_index]);
|
||||||
|
nvm.file_index = 0;
|
||||||
|
return STAT_PERSISTENCE_ERROR;
|
||||||
|
}
|
||||||
|
// OK to delete old file now (if it still exists), since we know the current one is good
|
||||||
|
f_unlink(filenames[PREV_FILE_INDEX]);
|
||||||
|
return STAT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* validate_persistence_file()
|
||||||
|
*
|
||||||
|
* ARM only. Helper function that checks the CRC and byte count of the
|
||||||
|
* persistence file. Assumes the file is already open.
|
||||||
|
*/
|
||||||
|
stat_t validate_persistence_file()
|
||||||
|
{
|
||||||
|
uint32_t crc = 0;
|
||||||
|
uint32_t filecrc = -1;
|
||||||
|
UINT br, br_sum = 0;
|
||||||
|
fs_ritorno(f_lseek(&nvm.file, 0), "crc check seek");
|
||||||
|
while (!f_eof(&nvm.file)) {
|
||||||
|
fs_ritorno(f_read(&nvm.file, &nvm.io_buffer, IO_BUFFER_SIZE, &br), "file read during CRC check");
|
||||||
|
|
||||||
|
if (f_eof(&nvm.file)) {
|
||||||
|
br -= std::min((UINT)CRC_LEN, br); // don't include old CRC in current CRC calculation
|
||||||
|
memcpy(&filecrc, nvm.io_buffer+br, CRC_LEN); // copy old CRC out of read buffer
|
||||||
|
}
|
||||||
|
// update calculated CRC
|
||||||
|
crc = crc32(crc, nvm.io_buffer, br);
|
||||||
|
br_sum += br;
|
||||||
|
}
|
||||||
|
|
||||||
|
// how did we do?
|
||||||
|
if (br_sum != nv_index_max() * NVM_VALUE_LEN) {
|
||||||
|
DEBUG_PRINT("bad byte count in file: %i\n", br_sum);
|
||||||
|
return STAT_PERSISTENCE_ERROR;
|
||||||
|
}
|
||||||
|
DEBUG_PRINT("crc: %lu from file, %lu calculated\n", filecrc, crc);
|
||||||
|
return crc == filecrc ? STAT_OK : STAT_PERSISTENCE_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* write_persistent_values()
|
||||||
|
*
|
||||||
|
* ARM only. Writes all the values from the write cache to the SD card. Since we can't
|
||||||
|
* rewrite individual pieces of data in the middle of an existing file, this requires
|
||||||
|
* rewriting all the data into a new file.
|
||||||
|
*/
|
||||||
|
stat_t write_persistent_values()
|
||||||
|
{
|
||||||
|
DEBUG_PRINT("writing new version\n");
|
||||||
|
|
||||||
|
FIL f_out;
|
||||||
|
UINT bw;
|
||||||
|
nvObj_t *nv = nv_reset_nv_list(); // sets *nv to the start of the body
|
||||||
|
cmDistanceMode saved_distance_mode;
|
||||||
|
|
||||||
|
// Save the current units mode
|
||||||
|
saved_distance_mode = (cmDistanceMode)cm_get_distance_mode(ACTIVE_MODEL);
|
||||||
|
|
||||||
|
// attempt to open file with previously persisted values
|
||||||
|
if (prepare_persistence_file() == STAT_OK) {
|
||||||
|
fs_ritorno(f_lseek(&nvm.file, 0), "f_lseek to input file start");
|
||||||
|
}
|
||||||
|
|
||||||
|
// open new file for writing updated values
|
||||||
|
fs_ritorno(f_open(&f_out, filenames[NEXT_FILE_INDEX], FA_WRITE | FA_OPEN_ALWAYS), "open output");
|
||||||
|
fs_ritorno(f_sync(&f_out), "sync output file");
|
||||||
|
DEBUG_PRINT("opened %s for writing\n", filenames[NEXT_FILE_INDEX]);
|
||||||
|
|
||||||
|
uint32_t crc = 0;
|
||||||
|
uint16_t step = IO_BUFFER_SIZE/NVM_VALUE_LEN;
|
||||||
|
|
||||||
|
for (index_t cnt = 0; cnt < nv_index_max(); cnt += step) {
|
||||||
|
// try to read old values from existing file
|
||||||
|
uint16_t io_byte_count = std::min((float)IO_BUFFER_SIZE, (float)((nv_index_max()-cnt) * NVM_VALUE_LEN));
|
||||||
|
UINT br = 0;
|
||||||
|
f_read(&nvm.file, &nvm.io_buffer, io_byte_count, &br);
|
||||||
|
DEBUG_PRINT("read %i bytes from old file\n", br);
|
||||||
|
|
||||||
|
// if we didn't get enough bytes from the old file, pad the buffer with defaults
|
||||||
|
// to keep the length correct
|
||||||
|
// FIXME: integrate this with default-setting code in config.cpp
|
||||||
|
for (; br<io_byte_count; br += NVM_VALUE_LEN) {
|
||||||
|
index_t index = cnt + br/NVM_VALUE_LEN;
|
||||||
|
memcpy(nvm.io_buffer+br, &cfgArray[index].def_value, NVM_VALUE_LEN);
|
||||||
|
}
|
||||||
|
DEBUG_PRINT("io_buffer populated with %i bytes total\n", br);
|
||||||
|
|
||||||
|
// update the values in the buffer from the changes indexes in the current range
|
||||||
|
for (nv->index = cnt; nv->index < (cnt + step); nv->index++) {
|
||||||
|
|
||||||
|
// Found an entry that needs to be written
|
||||||
|
if (nv->index == 0 || cfgArray[nv->index].flags & F_PERSIST) {
|
||||||
|
nv_get_nvObj(nv);
|
||||||
|
|
||||||
|
// Get the index for the current NVM value
|
||||||
|
index_t index = (nv->index - cnt) * NVM_VALUE_LEN;
|
||||||
|
|
||||||
|
// Write out based on the value type
|
||||||
|
if (nv->valuetype == TYPE_INTEGER || nv->valuetype == TYPE_BOOLEAN) {
|
||||||
|
memcpy(nvm.io_buffer + index, &nv->value_int, NVM_VALUE_LEN);
|
||||||
|
DEBUG_PRINT("item index: %l , write index: %l (cnt: %l), value: %i\n", nv->index, index, cnt, nv->value_int);
|
||||||
|
} else if (nv->valuetype == TYPE_FLOAT) {
|
||||||
|
memcpy(nvm.io_buffer + index, &nv->value_flt, NVM_VALUE_LEN);
|
||||||
|
DEBUG_PRINT("item index: %l , write index: %l (cnt: %l), value: %f\n", nv->index, index, cnt, nv->value_flt);
|
||||||
|
} else {
|
||||||
|
// next - ignore strings and other stuff which shouldn't be set to persist anyway
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write updated values to output file and sync
|
||||||
|
fs_ritorno(f_write(&f_out, &nvm.io_buffer, io_byte_count, &bw), "new file write");
|
||||||
|
if (bw != io_byte_count) return (STAT_PERSISTENCE_ERROR);
|
||||||
|
fs_ritorno(f_sync(&f_out), "out sync");
|
||||||
|
|
||||||
|
// update CRC
|
||||||
|
crc = crc32(crc, nvm.io_buffer, io_byte_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write CRC in final 4 bytes
|
||||||
|
fs_ritorno(f_write(&f_out, &crc, CRC_LEN, &bw), "write crc");
|
||||||
|
DEBUG_PRINT("wrote crc: %lu\n", crc);
|
||||||
|
|
||||||
|
// close both old and new files
|
||||||
|
fs_ritorno(f_close(&f_out), "close output");
|
||||||
|
if (f_is_open(&nvm.file)) {
|
||||||
|
fs_ritorno(f_close(&nvm.file), "close input");
|
||||||
|
// if we made it here, it's now safe to delete the older file
|
||||||
|
fs_ritorno(f_unlink(filenames[nvm.file_index]), "old file delete");
|
||||||
|
DEBUG_PRINT("deleted obsolete file %s\n", filenames[nvm.file_index]);
|
||||||
|
nvm.file_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore units mode
|
||||||
|
cm_set_distance_mode(saved_distance_mode);
|
||||||
|
|
||||||
|
return (STAT_OK);
|
||||||
|
}
|
||||||
34
g2core/device/sd_card/sd_persistence.h
Normal file
34
g2core/device/sd_card/sd_persistence.h
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* sd_persistence.h - persistence code for SD cards
|
||||||
|
* This file is part of the g2code project
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 Matt Staniszewski
|
||||||
|
* Copyright (c) 2019 Robert Giseburt
|
||||||
|
*
|
||||||
|
* This file ("the software") is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License, version 2 as published by the
|
||||||
|
* Free Software Foundation. You should have received a copy of the GNU General Public
|
||||||
|
* License, version 2 along with the software. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
* As a special exception, you may use this file as part of a software library without
|
||||||
|
* restriction. Specifically, if other files instantiate templates or use macros or
|
||||||
|
* inline functions from this file, or you compile this file and link it with other
|
||||||
|
* files to produce an executable, this file does not by itself cause the resulting
|
||||||
|
* executable to be covered by the GNU General Public License. This exception does not
|
||||||
|
* however invalidate any other reasons why the executable file might be covered by the
|
||||||
|
* GNU General Public License.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, BUT WITHOUT ANY
|
||||||
|
* WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
* SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||||
|
* OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef SD_PERSISTENCE_H_ONCE
|
||||||
|
#define SD_PERSISTENCE_H_ONCE
|
||||||
|
|
||||||
|
void setup_sd_persistence();
|
||||||
|
|
||||||
|
#endif // End of include guard: SD_PERSISTENCE_H_ONCE
|
||||||
@@ -26,77 +26,19 @@
|
|||||||
*/
|
*/
|
||||||
#include "g2core.h"
|
#include "g2core.h"
|
||||||
#include "persistence.h"
|
#include "persistence.h"
|
||||||
#include "canonical_machine.h"
|
|
||||||
#include "report.h"
|
|
||||||
#include "util.h"
|
|
||||||
#include "board_spi.h"
|
|
||||||
#include "ff.h"
|
|
||||||
|
|
||||||
/***********************************************************************************
|
|
||||||
**** STRUCTURE ALLOCATIONS ********************************************************
|
|
||||||
***********************************************************************************/
|
|
||||||
|
|
||||||
//**** persistence singleton ****
|
|
||||||
|
|
||||||
struct nvmSingleton_t {
|
|
||||||
float tmp_value;
|
|
||||||
FATFS fat_fs;
|
|
||||||
FIL file;
|
|
||||||
uint8_t file_index;
|
|
||||||
alignas(4) uint8_t io_buffer[IO_BUFFER_SIZE];
|
|
||||||
uint16_t changed_nvs;
|
|
||||||
uint32_t last_write_systick;
|
|
||||||
uint8_t write_failures;
|
|
||||||
} nvm;
|
|
||||||
|
|
||||||
/***********************************************************************************
|
/***********************************************************************************
|
||||||
**** GENERIC STATIC FUNCTIONS AND VARIABLES ***************************************
|
**** GENERIC STATIC FUNCTIONS AND VARIABLES ***************************************
|
||||||
***********************************************************************************/
|
***********************************************************************************/
|
||||||
|
|
||||||
stat_t prepare_persistence_file();
|
Persistence *persistence = nullptr;
|
||||||
stat_t write_persistent_values();
|
|
||||||
stat_t validate_persistence_file();
|
|
||||||
uint8_t active_file_index();
|
|
||||||
|
|
||||||
// Leaving this in for now in case bugs come up; we can remove it when we're confident
|
|
||||||
// it's stable
|
|
||||||
#if 0
|
|
||||||
# define DEBUG_PRINT(...) printf(__VA_ARGS__);
|
|
||||||
#else
|
|
||||||
# define DEBUG_PRINT(...) do {} while (0)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Analogous to ritorno(a), but for the FatFS return codes and with optional debug output
|
|
||||||
#define fs_ritorno(a, msg) if((status_code=a) != FR_OK) \
|
|
||||||
{ DEBUG_PRINT("%s res: %i\n", msg, status_code); return(STAT_PERSISTENCE_ERROR); }
|
|
||||||
|
|
||||||
/*
|
|
||||||
We cycle between three different files, indexed by a suffix. Each time we need to write
|
|
||||||
new values, we copy data from the current file to a new file with NEXT_FILE_INDEX, then
|
|
||||||
delete the current file once the write is complete. This ensures that at least one recent
|
|
||||||
copy of the file will be preserved if power is lost in the middle of a write.
|
|
||||||
*/
|
|
||||||
#define PERSISTENCE_DIR "persist"
|
|
||||||
#define PERSISTENCE_FILENAME(num) PERSISTENCE_DIR"/persist"#num".bin"
|
|
||||||
#define PERSISTENCE_FILENAME_CNT 3
|
|
||||||
#define NEXT_FILE_INDEX (nvm.file_index+1) % PERSISTENCE_FILENAME_CNT
|
|
||||||
#define PREV_FILE_INDEX (nvm.file_index+PERSISTENCE_FILENAME_CNT-1) % PERSISTENCE_FILENAME_CNT
|
|
||||||
const char* filenames[PERSISTENCE_FILENAME_CNT] = {
|
|
||||||
PERSISTENCE_FILENAME(0), PERSISTENCE_FILENAME(1), PERSISTENCE_FILENAME(2) };
|
|
||||||
|
|
||||||
#define CRC_LEN 4
|
|
||||||
|
|
||||||
/***********************************************************************************
|
|
||||||
**** CODE *************************************************************************
|
|
||||||
***********************************************************************************/
|
|
||||||
|
|
||||||
void persistence_init()
|
void persistence_init()
|
||||||
{
|
{
|
||||||
nvm.file_index = 0;
|
if (persistence == nullptr) {
|
||||||
nvm.last_write_systick = Motate::SysTickTimer.getValue();
|
return;
|
||||||
nvm.write_failures = 0;
|
}
|
||||||
nvm.changed_nvs = 0;
|
persistence->init();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -107,30 +49,11 @@ void persistence_init()
|
|||||||
|
|
||||||
stat_t read_persistent_value(nvObj_t *nv)
|
stat_t read_persistent_value(nvObj_t *nv)
|
||||||
{
|
{
|
||||||
ritorno(prepare_persistence_file());
|
if (persistence == nullptr) {
|
||||||
DEBUG_PRINT("file opened for reading\n");
|
return (STAT_OK); // it worked! ;-)
|
||||||
fs_ritorno(f_lseek(&nvm.file, nv->index * NVM_VALUE_LEN), "f_lseek during read");
|
|
||||||
UINT br;
|
|
||||||
fs_ritorno(f_read(&nvm.file, &nvm.io_buffer, NVM_VALUE_LEN, &br), "read value");
|
|
||||||
if (br != NVM_VALUE_LEN) {
|
|
||||||
return (STAT_PERSISTENCE_ERROR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cfgArray[nv->index].flags & TYPE_INTEGER) {
|
return persistence->read(nv);
|
||||||
nv->valuetype = TYPE_INTEGER;
|
|
||||||
nv->value_int = *(int32_t *)nvm.io_buffer;
|
|
||||||
DEBUG_PRINT("value (i) copied from address %l in file: %l\n", nv->index * NVM_VALUE_LEN, nv->value_int);
|
|
||||||
} else if (cfgArray[nv->index].flags & TYPE_BOOLEAN) {
|
|
||||||
nv->valuetype = TYPE_BOOLEAN;
|
|
||||||
nv->value_int = *(int32_t *)nvm.io_buffer;
|
|
||||||
DEBUG_PRINT("value (b) copied from address %l in file: %l\n", nv->index * NVM_VALUE_LEN, nv->value_int);
|
|
||||||
} else {
|
|
||||||
nv->valuetype = TYPE_FLOAT;
|
|
||||||
nv->value_flt = *(float *)nvm.io_buffer;
|
|
||||||
DEBUG_PRINT("value (f) copied from address %l in file: %f\n", nv->index * NVM_VALUE_LEN, nv->value_flt);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (STAT_OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -142,235 +65,23 @@ stat_t read_persistent_value(nvObj_t *nv)
|
|||||||
|
|
||||||
stat_t write_persistent_value(nvObj_t *nv)
|
stat_t write_persistent_value(nvObj_t *nv)
|
||||||
{
|
{
|
||||||
nvm.changed_nvs++;
|
if (persistence == nullptr) {
|
||||||
return (STAT_OK);
|
return (STAT_OK); // it worked! ;-)
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistence->write(nv);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* write_persistent_values_callback()
|
* write_persistent_values_callback()
|
||||||
*
|
|
||||||
* On ARM, write cached values to a file. No-op on AVR.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
stat_t write_persistent_values_callback()
|
stat_t write_persistent_values_callback()
|
||||||
{
|
{
|
||||||
// Check the disk status to ensure we catch card-detect pin changes.
|
if (persistence == nullptr) {
|
||||||
// FIXME: it would be much better to do this with an interrupt!
|
return (STAT_OK); // it worked! ;-)
|
||||||
f_polldisk();
|
}
|
||||||
if (nvm.changed_nvs > 0) {
|
|
||||||
if (Motate::SysTickTimer.getValue() - nvm.last_write_systick < MIN_WRITE_INTERVAL) {
|
return persistence->periodic();
|
||||||
return (STAT_NOOP);
|
|
||||||
}
|
|
||||||
// this check may not be necessary on ARM, but just in case...
|
|
||||||
if (cm->cycle_type != CYCLE_NONE) {
|
|
||||||
return(STAT_NOOP); // can't write when machine is moving
|
|
||||||
}
|
|
||||||
|
|
||||||
if(write_persistent_values() == STAT_OK) {
|
|
||||||
nvm.changed_nvs = 0;
|
|
||||||
nvm.write_failures = 0;
|
|
||||||
} else {
|
|
||||||
// if the write failed, make sure no half-written output file exists
|
|
||||||
f_unlink(filenames[NEXT_FILE_INDEX]);
|
|
||||||
if (++nvm.write_failures >= MAX_WRITE_FAILURES) {
|
|
||||||
nvm.changed_nvs = 0; // give up on these values
|
|
||||||
nvm.write_failures = 0; // but try again if we get more values later
|
|
||||||
return(rpt_exception(STAT_PERSISTENCE_ERROR, NULL));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nvm.last_write_systick = Motate::SysTickTimer.getValue();
|
|
||||||
return STAT_OK;
|
|
||||||
}
|
|
||||||
return STAT_NOOP;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* active_file_index()
|
|
||||||
*
|
|
||||||
* Determines which of the existing files is most current and should be used for
|
|
||||||
* value reads. If no files exist, return 0. This assumes that no more than two
|
|
||||||
* files exist at any one time, which should always be the case under our updating
|
|
||||||
* scheme (described in a comment near the PERSISTENCE_FILENAME define).
|
|
||||||
*/
|
|
||||||
uint8_t active_file_index()
|
|
||||||
{
|
|
||||||
uint8_t i = 0;
|
|
||||||
for (; i < PERSISTENCE_FILENAME_CNT; ++i) {
|
|
||||||
if (f_stat(filenames[i], nullptr) == FR_OK) {
|
|
||||||
uint8_t next = (i+1)%PERSISTENCE_FILENAME_CNT;
|
|
||||||
if (next > i && f_stat(filenames[next], nullptr) == FR_OK) {
|
|
||||||
i = next;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* prepare_persistence_file()
|
|
||||||
*
|
|
||||||
* ARM only. Ensures that the file is open and has a valid CRC. This should be called
|
|
||||||
* prior to using the file in any other function.
|
|
||||||
*/
|
|
||||||
stat_t prepare_persistence_file()
|
|
||||||
{
|
|
||||||
// if the file is already open and valid, no further prep is necessary.
|
|
||||||
// NOTE: we don't close the file after every use because the higher latency
|
|
||||||
// would slow down consecutive reads. However, we still need to re-validate before
|
|
||||||
// every use to ensure that the card status hasn't changed.
|
|
||||||
if (f_is_open(&nvm.file) && validate(&nvm.file) == FR_OK) return STAT_OK;
|
|
||||||
|
|
||||||
// mount volume if necessary
|
|
||||||
if (!nvm.fat_fs.fs_type) {
|
|
||||||
fs_ritorno(f_mount(&nvm.fat_fs, "", 1), "mount"); /* Give a work area to the default drive */
|
|
||||||
}
|
|
||||||
f_mkdir(PERSISTENCE_DIR);
|
|
||||||
uint8_t index = active_file_index();
|
|
||||||
fs_ritorno(f_open(&nvm.file, filenames[index], FA_READ | FA_OPEN_EXISTING), "open input");
|
|
||||||
nvm.file_index = index;
|
|
||||||
|
|
||||||
// if CRC doesn't match, delete file and return error
|
|
||||||
if (validate_persistence_file() != STAT_OK) {
|
|
||||||
f_close(&nvm.file);
|
|
||||||
f_unlink(filenames[nvm.file_index]);
|
|
||||||
nvm.file_index = 0;
|
|
||||||
return STAT_PERSISTENCE_ERROR;
|
|
||||||
}
|
|
||||||
// OK to delete old file now (if it still exists), since we know the current one is good
|
|
||||||
f_unlink(filenames[PREV_FILE_INDEX]);
|
|
||||||
return STAT_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* validate_persistence_file()
|
|
||||||
*
|
|
||||||
* ARM only. Helper function that checks the CRC and byte count of the
|
|
||||||
* persistence file. Assumes the file is already open.
|
|
||||||
*/
|
|
||||||
stat_t validate_persistence_file()
|
|
||||||
{
|
|
||||||
uint32_t crc = 0;
|
|
||||||
uint32_t filecrc = -1;
|
|
||||||
UINT br, br_sum = 0;
|
|
||||||
fs_ritorno(f_lseek(&nvm.file, 0), "crc check seek");
|
|
||||||
while (!f_eof(&nvm.file)) {
|
|
||||||
fs_ritorno(f_read(&nvm.file, &nvm.io_buffer, IO_BUFFER_SIZE, &br), "file read during CRC check");
|
|
||||||
|
|
||||||
if (f_eof(&nvm.file)) {
|
|
||||||
br -= std::min((UINT)CRC_LEN, br); // don't include old CRC in current CRC calculation
|
|
||||||
memcpy(&filecrc, nvm.io_buffer+br, CRC_LEN); // copy old CRC out of read buffer
|
|
||||||
}
|
|
||||||
// update calculated CRC
|
|
||||||
crc = crc32(crc, nvm.io_buffer, br);
|
|
||||||
br_sum += br;
|
|
||||||
}
|
|
||||||
|
|
||||||
// how did we do?
|
|
||||||
if (br_sum != nv_index_max() * NVM_VALUE_LEN) {
|
|
||||||
DEBUG_PRINT("bad byte count in file: %i\n", br_sum);
|
|
||||||
return STAT_PERSISTENCE_ERROR;
|
|
||||||
}
|
|
||||||
DEBUG_PRINT("crc: %lu from file, %lu calculated\n", filecrc, crc);
|
|
||||||
return crc == filecrc ? STAT_OK : STAT_PERSISTENCE_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* write_persistent_values()
|
|
||||||
*
|
|
||||||
* ARM only. Writes all the values from the write cache to the SD card. Since we can't
|
|
||||||
* rewrite individual pieces of data in the middle of an existing file, this requires
|
|
||||||
* rewriting all the data into a new file.
|
|
||||||
*/
|
|
||||||
stat_t write_persistent_values()
|
|
||||||
{
|
|
||||||
DEBUG_PRINT("writing new version\n");
|
|
||||||
|
|
||||||
FIL f_out;
|
|
||||||
UINT bw;
|
|
||||||
nvObj_t *nv = nv_reset_nv_list(); // sets *nv to the start of the body
|
|
||||||
cmDistanceMode saved_distance_mode;
|
|
||||||
|
|
||||||
// Save the current units mode
|
|
||||||
saved_distance_mode = (cmDistanceMode)cm_get_distance_mode(ACTIVE_MODEL);
|
|
||||||
|
|
||||||
// attempt to open file with previously persisted values
|
|
||||||
if (prepare_persistence_file() == STAT_OK) {
|
|
||||||
fs_ritorno(f_lseek(&nvm.file, 0), "f_lseek to input file start");
|
|
||||||
}
|
|
||||||
|
|
||||||
// open new file for writing updated values
|
|
||||||
fs_ritorno(f_open(&f_out, filenames[NEXT_FILE_INDEX], FA_WRITE | FA_OPEN_ALWAYS), "open output");
|
|
||||||
fs_ritorno(f_sync(&f_out), "sync output file");
|
|
||||||
DEBUG_PRINT("opened %s for writing\n", filenames[NEXT_FILE_INDEX]);
|
|
||||||
|
|
||||||
uint32_t crc = 0;
|
|
||||||
uint16_t step = IO_BUFFER_SIZE/NVM_VALUE_LEN;
|
|
||||||
|
|
||||||
for (index_t cnt = 0; cnt < nv_index_max(); cnt += step) {
|
|
||||||
// try to read old values from existing file
|
|
||||||
uint16_t io_byte_count = std::min((float)IO_BUFFER_SIZE, (float)((nv_index_max()-cnt) * NVM_VALUE_LEN));
|
|
||||||
UINT br = 0;
|
|
||||||
f_read(&nvm.file, &nvm.io_buffer, io_byte_count, &br);
|
|
||||||
DEBUG_PRINT("read %i bytes from old file\n", br);
|
|
||||||
|
|
||||||
// if we didn't get enough bytes from the old file, pad the buffer with defaults
|
|
||||||
// to keep the length correct
|
|
||||||
// FIXME: integrate this with default-setting code in config.cpp
|
|
||||||
for (; br<io_byte_count; br += NVM_VALUE_LEN) {
|
|
||||||
index_t index = cnt + br/NVM_VALUE_LEN;
|
|
||||||
memcpy(nvm.io_buffer+br, &cfgArray[index].def_value, NVM_VALUE_LEN);
|
|
||||||
}
|
|
||||||
DEBUG_PRINT("io_buffer populated with %i bytes total\n", br);
|
|
||||||
|
|
||||||
// update the values in the buffer from the changes indexes in the current range
|
|
||||||
for (nv->index = cnt; nv->index < (cnt + step); nv->index++) {
|
|
||||||
|
|
||||||
// Found an entry that needs to be written
|
|
||||||
if (nv->index == 0 || cfgArray[nv->index].flags & F_PERSIST) {
|
|
||||||
nv_get_nvObj(nv);
|
|
||||||
|
|
||||||
// Get the index for the current NVM value
|
|
||||||
index_t index = (nv->index - cnt) * NVM_VALUE_LEN;
|
|
||||||
|
|
||||||
// Write out based on the value type
|
|
||||||
if (nv->valuetype == TYPE_INTEGER || nv->valuetype == TYPE_BOOLEAN) {
|
|
||||||
memcpy(nvm.io_buffer + index, &nv->value_int, NVM_VALUE_LEN);
|
|
||||||
DEBUG_PRINT("item index: %l , write index: %l (cnt: %l), value: %i\n", nv->index, index, cnt, nv->value_int);
|
|
||||||
} else if (nv->valuetype == TYPE_FLOAT) {
|
|
||||||
memcpy(nvm.io_buffer + index, &nv->value_flt, NVM_VALUE_LEN);
|
|
||||||
DEBUG_PRINT("item index: %l , write index: %l (cnt: %l), value: %f\n", nv->index, index, cnt, nv->value_flt);
|
|
||||||
} else {
|
|
||||||
// next - ignore strings and other stuff which shouldn't be set to persist anyway
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write updated values to output file and sync
|
|
||||||
fs_ritorno(f_write(&f_out, &nvm.io_buffer, io_byte_count, &bw), "new file write");
|
|
||||||
if (bw != io_byte_count) return (STAT_PERSISTENCE_ERROR);
|
|
||||||
fs_ritorno(f_sync(&f_out), "out sync");
|
|
||||||
|
|
||||||
// update CRC
|
|
||||||
crc = crc32(crc, nvm.io_buffer, io_byte_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
// write CRC in final 4 bytes
|
|
||||||
fs_ritorno(f_write(&f_out, &crc, CRC_LEN, &bw), "write crc");
|
|
||||||
DEBUG_PRINT("wrote crc: %lu\n", crc);
|
|
||||||
|
|
||||||
// close both old and new files
|
|
||||||
fs_ritorno(f_close(&f_out), "close output");
|
|
||||||
if (f_is_open(&nvm.file)) {
|
|
||||||
fs_ritorno(f_close(&nvm.file), "close input");
|
|
||||||
// if we made it here, it's now safe to delete the older file
|
|
||||||
fs_ritorno(f_unlink(filenames[nvm.file_index]), "old file delete");
|
|
||||||
DEBUG_PRINT("deleted obsolete file %s\n", filenames[nvm.file_index]);
|
|
||||||
nvm.file_index = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore units mode
|
|
||||||
cm_set_distance_mode(saved_distance_mode);
|
|
||||||
|
|
||||||
return (STAT_OK);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,6 @@
|
|||||||
#define PERSISTENCE_H_ONCE
|
#define PERSISTENCE_H_ONCE
|
||||||
|
|
||||||
#include "config.h" // needed for nvObj_t definition
|
#include "config.h" // needed for nvObj_t definition
|
||||||
// #include "ff.h"
|
|
||||||
// #include "util.h" // FIXME: this won't compile if included after <map>
|
|
||||||
|
|
||||||
#define NVM_VALUE_LEN 4 // NVM value length (float, fixed length)
|
#define NVM_VALUE_LEN 4 // NVM value length (float, fixed length)
|
||||||
#define NVM_BASE_ADDR 0x0000 // base address of usable NVM
|
#define NVM_BASE_ADDR 0x0000 // base address of usable NVM
|
||||||
@@ -42,6 +40,16 @@
|
|||||||
|
|
||||||
//**** persistence function prototypes ****
|
//**** persistence function prototypes ****
|
||||||
|
|
||||||
|
class Persistence {
|
||||||
|
public:
|
||||||
|
virtual void init();
|
||||||
|
virtual stat_t read(nvObj_t *nv);
|
||||||
|
virtual stat_t write(nvObj_t *nv);
|
||||||
|
virtual stat_t periodic();
|
||||||
|
};
|
||||||
|
|
||||||
|
extern Persistence *persistence;
|
||||||
|
|
||||||
void persistence_init(void);
|
void persistence_init(void);
|
||||||
stat_t read_persistent_value(nvObj_t *nv);
|
stat_t read_persistent_value(nvObj_t *nv);
|
||||||
stat_t write_persistent_value(nvObj_t *nv);
|
stat_t write_persistent_value(nvObj_t *nv);
|
||||||
|
|||||||
Reference in New Issue
Block a user