mirror of
https://github.com/odriverobotics/ODrive.git
synced 2026-08-19 02:43:27 +08:00
This bug prevented the user from saving the configuration more than twice without rebooting.
454 lines
16 KiB
C
454 lines
16 KiB
C
/*
|
|
* Flash-based Non-Volatile Memory (NVM)
|
|
*
|
|
* This file supports storing and loading persistent configuration based on
|
|
* the STM32 builtin flash memory.
|
|
*
|
|
* The STM32F405xx has 12 flash sectors of heterogeneous size. We use the last
|
|
* two sectors for configuration data. These pages have a size of 128kB each.
|
|
* Setting any bit in these sectors to 0 is always possible, but setting them
|
|
* to 1 requires erasing the whole sector.
|
|
*
|
|
* We consider each sector as an array of 64-bit fields except the first N bytes, which we
|
|
* instead use as an allocation block. The allocation block is a compact bit-field (2 bit per entry)
|
|
* that keeps track of the state of each field (erased, invalid, valid).
|
|
*
|
|
* One sector is always considered the valid (read) sector and the other one is the
|
|
* target for the next write access: they can be considered to be ping-pong or double buffred.
|
|
*
|
|
* When writing a block of data, instead of always erasing the whole writable sector the
|
|
* new data is appended in the erased area. This presumably increases flash life span.
|
|
* The writable sector is only erased if there is not enough space for the new data.
|
|
*
|
|
* On startup, if there is exactly one sector
|
|
* whose last non-erased value has the state "valid" that sector is considered
|
|
* the valid sector. In any other case the selection is undefined.
|
|
*
|
|
*
|
|
* To write a new block of data atomically we first mark all associated fields
|
|
* as "invalid" (in the allocation table) then write the data and then mark the
|
|
* fields as "valid" (in the direction of increasing address).
|
|
*/
|
|
|
|
#include "nvm.h"
|
|
|
|
#include <stm32f405xx.h>
|
|
#include <stm32f4xx_hal.h>
|
|
#include <string.h>
|
|
|
|
#if defined(STM32F405xx)
|
|
|
|
// refer to page 75 of datasheet:
|
|
// http://www.st.com/content/ccc/resource/technical/document/reference_manual/3d/6d/5a/66/b4/99/40/d4/DM00031020.pdf/files/DM00031020.pdf/jcr:content/translations/en.DM00031020.pdf
|
|
#define FLASH_SECTOR_10_BASE (const volatile uint8_t*)0x80C0000UL
|
|
#define FLASH_SECTOR_10_SIZE 0x20000UL
|
|
#define FLASH_SECTOR_11_BASE (const volatile uint8_t*)0x80E0000UL
|
|
#define FLASH_SECTOR_11_SIZE 0x20000UL
|
|
|
|
#define HAL_FLASH_ClearError() __HAL_FLASH_CLEAR_FLAG(FLASH_FLAG_EOP | FLASH_FLAG_OPERR | FLASH_FLAG_WRPERR | FLASH_FLAG_PGAERR | FLASH_FLAG_PGSERR | FLASH_FLAG_PGPERR)
|
|
#else
|
|
#error "unknown flash sector size"
|
|
#endif
|
|
|
|
typedef enum {
|
|
VALID = 0,
|
|
INVALID = 1,
|
|
ERASED = 3
|
|
} field_state_t;
|
|
|
|
typedef struct {
|
|
size_t index; //!< next field to be written to (can be equal to n_data)
|
|
const uint32_t sector_id; //!< HAL ID of this sector
|
|
const size_t n_data; //!< number of 64-bit fields in this sector
|
|
const size_t n_reserved; //!< number of 64-bit fields in this sector that are reserved for the allocation table
|
|
const volatile uint8_t* const alloc_table;
|
|
const volatile uint64_t* const data;
|
|
} sector_t;
|
|
|
|
sector_t sectors[] = { {
|
|
.sector_id = FLASH_SECTOR_10,
|
|
.n_data = FLASH_SECTOR_10_SIZE >> 3,
|
|
.n_reserved = (FLASH_SECTOR_10_SIZE >> 3) >> 5,
|
|
.alloc_table = FLASH_SECTOR_10_BASE,
|
|
.data = (uint64_t *)FLASH_SECTOR_10_BASE
|
|
}, {
|
|
.sector_id = FLASH_SECTOR_11,
|
|
.n_data = FLASH_SECTOR_11_SIZE >> 3,
|
|
.n_reserved = (FLASH_SECTOR_11_SIZE >> 3) >> 5,
|
|
.alloc_table = FLASH_SECTOR_11_BASE,
|
|
.data = (uint64_t *)FLASH_SECTOR_11_BASE
|
|
}};
|
|
|
|
uint8_t read_sector_; // 0 or 1 to indicate which sector to read from and which to write to
|
|
size_t n_staging_area_; // number of 64-bit values that were reserved using NVM_start_write
|
|
size_t n_valid_; // number of 64-bit fields that can be read
|
|
|
|
// @brief Erases a flash sector. This sets all bits in the sector to 1.
|
|
// The sector's current index is reset to the minimum value (n_reserved).
|
|
// @returns 0 on success or a non-zero error code otherwise
|
|
int erase(sector_t *sector) {
|
|
FLASH_EraseInitTypeDef erase_struct = {
|
|
.TypeErase = FLASH_TYPEERASE_SECTORS,
|
|
.Banks = 0, // only used for mass erase
|
|
.Sector = sector->sector_id,
|
|
.NbSectors = 1,
|
|
.VoltageRange = FLASH_VOLTAGE_RANGE_3
|
|
};
|
|
HAL_FLASH_Unlock();
|
|
HAL_FLASH_ClearError();
|
|
uint32_t sector_error;
|
|
if (HAL_FLASHEx_Erase(&erase_struct, §or_error) != HAL_OK)
|
|
goto fail;
|
|
sector->index = sector->n_reserved;
|
|
|
|
HAL_FLASH_Lock();
|
|
return 0;
|
|
fail:
|
|
HAL_FLASH_Lock();
|
|
//printf("erase failed: %u \r\n", HAL_FLASH_GetError());
|
|
return HAL_FLASH_GetError(); // non-zero
|
|
}
|
|
|
|
|
|
// @brief Writes states into the allocation table.
|
|
// The write operation goes in the direction of increasing indices.
|
|
// @param state: 11: erased, 10: writing, 00: valid data
|
|
// @returns 0 on success or a non-zero error code otherwise
|
|
int set_allocation_state(sector_t *sector, size_t index, size_t count, field_state_t state) {
|
|
if (index < sector->n_reserved)
|
|
return -1;
|
|
if (index + count >= sector->n_data)
|
|
return -1;
|
|
|
|
// expand state to state for 4 values
|
|
const uint8_t states = (state << 0) | (state << 2) | (state << 4) | (state << 6);
|
|
|
|
// handle unaligned start
|
|
uint8_t mask = ~(0xff << ((index & 0x3) << 1));
|
|
count += index & 0x3;
|
|
index -= index & 0x3;
|
|
|
|
HAL_FLASH_Unlock();
|
|
HAL_FLASH_ClearError();
|
|
|
|
// write states
|
|
for (; count >= 4; count -= 4, index += 4) {
|
|
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, (uintptr_t)§or->alloc_table[index >> 2], states | mask) != HAL_OK)
|
|
goto fail;
|
|
mask = 0;
|
|
}
|
|
|
|
// handle unaligned end
|
|
if (count) {
|
|
mask |= ~(0xff >> ((4 - count) << 1));
|
|
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE, (uintptr_t)§or->alloc_table[index >> 2], states | mask) != HAL_OK)
|
|
goto fail;
|
|
}
|
|
|
|
HAL_FLASH_Lock();
|
|
return 0;
|
|
fail:
|
|
HAL_FLASH_Lock();
|
|
return HAL_FLASH_GetError(); // non-zero
|
|
}
|
|
|
|
// @brief Reads the allocation table from behind to determine how many fields match the
|
|
// reference state.
|
|
// @param sector: The sector on which to perform the search
|
|
// @param max_index: The maximum index that should be considered
|
|
// @param ref_state: The reference state
|
|
// @param state: Set to the first encountered state that is unequal to ref_state.
|
|
// Set to ref_state if all encountered states are equal to ref_state.
|
|
// @returns The smallest index that points to a field with ref_state.
|
|
// This value is at least sector->n_reserved and at most max_index.
|
|
size_t scan_allocation_table(sector_t *sector, size_t max_index, field_state_t ref_state, field_state_t *state) {
|
|
const uint8_t ref_states = (ref_state << 0) | (ref_state << 2) | (ref_state << 4) | (ref_state << 6);
|
|
size_t index = (((max_index + 3) >> 2) << 2); // start at the max index but round up to a multiple of 4
|
|
size_t ignore = index - max_index;
|
|
uint8_t states = ref_states;
|
|
|
|
//printf("scan from %08x to %08x for %02x\r\n", index, sector->n_reserved, ref_states); osDelay(5);
|
|
|
|
// read 4 states at a time
|
|
for (; index >= (sector->n_reserved + 4); index -= 4) {
|
|
states = sector->alloc_table[(index - 1) >> 2];
|
|
if (ignore) { // ignore the upper 1, 2 or 3 states if max_index was unaligned
|
|
uint8_t ignore_mask = ~(0xff >> (ignore << 1));
|
|
states = (states & ~ignore_mask) | (ref_states & ignore_mask);
|
|
ignore = 0;
|
|
}
|
|
if (states != ref_states)
|
|
break;
|
|
}
|
|
|
|
// once we encounterd a byte with any state mismatch determine which of the 4 states it is
|
|
for (; ((states >> 6) == (ref_states & 0x3)) && (index > sector->n_reserved); index--) {
|
|
states <<= 2;
|
|
}
|
|
|
|
*state = states >> 6;
|
|
//printf("(it's %02x)\r\n", index); osDelay(5);
|
|
return index;
|
|
}
|
|
|
|
// Loads the head of the NVM data.
|
|
// If this function fails subsequent calls to NVM functions (other than NVM_init or NVM_erase)
|
|
// cause undefined behavior.
|
|
// @returns 0 on success or a non-zero error code otherwise
|
|
int NVM_init(void) {
|
|
field_state_t sector0_state, sector1_state;
|
|
sectors[0].index = scan_allocation_table(§ors[0], sectors[0].n_data,
|
|
ERASED, §or0_state);
|
|
sectors[1].index = scan_allocation_table(§ors[1], sectors[1].n_data,
|
|
ERASED, §or1_state);
|
|
//printf("sector states: %02x, %02x\r\n", sector0_state, sector1_state); osDelay(5);
|
|
|
|
// Select valid sector on a best effort basis
|
|
// (in unfortunate cases valid_sector might actually point
|
|
// to an invalid or erased sector)
|
|
read_sector_ = 0;
|
|
if (sector1_state == VALID)
|
|
read_sector_ = 1;
|
|
|
|
// count the number of valid fields
|
|
sector_t *read_sector = §ors[read_sector_];
|
|
uint8_t first_nonvalid_state;
|
|
size_t min_valid_index = scan_allocation_table(read_sector, read_sector->index,
|
|
VALID, &first_nonvalid_state);
|
|
n_valid_ = read_sector->index - min_valid_index;
|
|
|
|
n_staging_area_ = 0;
|
|
|
|
int status = 0;
|
|
/*// bring non-valid sectors into a known state
|
|
this is not absolutely required
|
|
if (sector0_state != VALID)
|
|
status |= erase(§ors[0]);
|
|
if (sector1_state != VALID)
|
|
status |= erase(§ors[1]);
|
|
*/
|
|
return status;
|
|
}
|
|
|
|
// @brief Erases all data in the NVM.
|
|
//
|
|
// If this function fails subsequent calls to NVM functions (other than NVM_init or NVM_erase)
|
|
// cause undefined behavior.
|
|
// Caution: this function may take a long time (like 1 second)
|
|
//
|
|
// @returns 0 on success or a non-zero error code otherwise
|
|
int NVM_erase(void) {
|
|
read_sector_ = 0;
|
|
sectors[0].index = sectors[0].n_reserved;
|
|
sectors[1].index = sectors[1].n_reserved;
|
|
|
|
int state = 0;
|
|
state |= erase(§ors[0]);
|
|
state |= erase(§ors[1]);
|
|
return state;
|
|
}
|
|
|
|
// @brief Returns the maximum number of bytes that can be read using NVM_read.
|
|
// This holds until NVM_commit is called.
|
|
size_t NVM_get_max_read_length(void) {
|
|
return n_valid_ << 3;
|
|
}
|
|
|
|
// @brief Returns the maximum length (in bytes) that can passed to NVM_start_write.
|
|
// This holds until NVM_commit is called.
|
|
size_t NVM_get_max_write_length(void) {
|
|
sector_t *target = §ors[1 - read_sector_];
|
|
return (target->n_data - target->n_reserved) << 3;
|
|
}
|
|
|
|
// @brief Reads from the latest committed block in the non-volatile memory.
|
|
// @param offset: offset in bytes (0 meaning the beginning of the valid area)
|
|
// @param data: buffer to write to
|
|
// @param length: length in bytes (if (offset + length) is out of range, the function fails)
|
|
// @returns 0 on success or a non-zero error code otherwise
|
|
int NVM_read(size_t offset, uint8_t *data, size_t length) {
|
|
if (offset + length > (n_valid_ << 3))
|
|
return -1;
|
|
sector_t *read_sector = §ors[read_sector_];
|
|
const uint8_t *src_ptr = ((const uint8_t *)&read_sector->data[read_sector->index - n_valid_]) + offset;
|
|
memcpy(data, src_ptr, length);
|
|
return 0;
|
|
}
|
|
|
|
// @brief Starts an atomic write operation.
|
|
//
|
|
// The most recent valid NVM data is not modified or invalidated until NVM_commit is called.
|
|
// The length must be at most equal to the size indicated by NVM_get_max_write_length().
|
|
//
|
|
// @param length: Length of the staging block that should be created
|
|
int NVM_start_write(size_t length) {
|
|
int status = 0;
|
|
sector_t *target = §ors[1 - read_sector_];
|
|
|
|
length = (length + 7) >> 3; // round to multiple of 64 bit
|
|
if (length > target->n_data - target->n_reserved)
|
|
return -1;
|
|
|
|
// make room for the new data
|
|
if (length > target->n_data - target->index)
|
|
if ((status = erase(target)))
|
|
return status;
|
|
|
|
// invalidate the fields we're about to write
|
|
status = set_allocation_state(target, target->index, length, INVALID);
|
|
if (status)
|
|
return status;
|
|
|
|
n_staging_area_ = length;
|
|
return 0;
|
|
}
|
|
|
|
// @brief Writes to the current data block that was opened with NVM_start_write.
|
|
//
|
|
// The operation fails if (offset + length) is larger than the length passed to NVM_start_write.
|
|
// The most recent valid NVM data is not modified or invalidated until NVM_commit is called.
|
|
// Warning: Writing different data to the same area multiple times during a single transaction
|
|
// will cause data corruption.
|
|
//
|
|
// @param offset: The offset in bytes, 0 being the beginning of the staging block.
|
|
// @param data: Pointer to the data that should be written
|
|
// @param length: Data length in bytes
|
|
int NVM_write(size_t offset, uint8_t *data, size_t length) {
|
|
if (offset + length > (n_staging_area_ << 3))
|
|
return -1;
|
|
sector_t *target = §ors[1 - read_sector_];
|
|
|
|
HAL_FLASH_Unlock();
|
|
HAL_FLASH_ClearError();
|
|
|
|
// handle unaligned start
|
|
for (; (offset & 0x3) && length; ++data, ++offset, --length)
|
|
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE,
|
|
((uintptr_t)&target->data[target->index]) + offset, *data) != HAL_OK)
|
|
goto fail;
|
|
|
|
// write 32-bit values (64-bit doesn't work)
|
|
for (; length >= 4; data += 4, offset += 4, length -=4)
|
|
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
|
|
((uintptr_t)&target->data[target->index]) + offset, *(uint32_t*)data) != HAL_OK)
|
|
goto fail;
|
|
|
|
// handle unaligned end
|
|
for (; length; ++data, ++offset, --length)
|
|
if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_BYTE,
|
|
((uintptr_t)&target->data[target->index]) + offset, *data) != HAL_OK)
|
|
goto fail;
|
|
|
|
HAL_FLASH_Lock();
|
|
return 0;
|
|
fail:
|
|
HAL_FLASH_Lock();
|
|
return HAL_FLASH_GetError(); // non-zero
|
|
}
|
|
|
|
// @brief Commits the new data to NVM atomically.
|
|
int NVM_commit(void) {
|
|
sector_t *read_sector = §ors[read_sector_];
|
|
sector_t *write_sector = §ors[1 - read_sector_];
|
|
|
|
// mark the newly-written fields as valid
|
|
int status = set_allocation_state(write_sector, write_sector->index, n_staging_area_, VALID);
|
|
if (status)
|
|
return status;
|
|
|
|
write_sector->index += n_staging_area_;
|
|
n_valid_ = n_staging_area_;
|
|
n_staging_area_ = 0;
|
|
read_sector_ = 1 - read_sector_;
|
|
|
|
// invalidate the other sector
|
|
if (read_sector->index < read_sector->n_data) {
|
|
status = set_allocation_state(read_sector, read_sector->index, 1, INVALID);
|
|
read_sector->index += 1;
|
|
} else {
|
|
status = erase(read_sector);
|
|
}
|
|
|
|
return status;
|
|
}
|
|
|
|
|
|
#include <cmsis_os.h>
|
|
/** @brief Call this at startup to test/demo the NVM driver
|
|
|
|
Expected output when starting with a fully erased NVM
|
|
|
|
[1st boot]
|
|
=== NVM TEST ===
|
|
NVM is empty
|
|
write 0x00, ..., 0x25 to NVM
|
|
new data committed to NVM
|
|
|
|
[2nd boot]
|
|
=== NVM TEST ===
|
|
NVM contains 40 valid bytes:
|
|
00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
|
|
10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f
|
|
20 21 22 23 24 25 ff ff
|
|
write 0xbd, ..., 0xe2 to NVM
|
|
new data committed to NVM
|
|
|
|
[3rd boot]
|
|
=== NVM TEST ===
|
|
NVM contains 40 valid bytes:
|
|
bd be bf c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 ca cb cc
|
|
cd ce cf d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da db dc
|
|
dd de df e0 e1 e2 ff ff
|
|
write 0xcb, ..., 0xf0 to NVM
|
|
new data committed to NVM
|
|
*/
|
|
void NVM_demo(void) {
|
|
const size_t len = 38;
|
|
uint8_t data[len];
|
|
int progress = 0;
|
|
uint8_t seed = 0;
|
|
|
|
osDelay(100);
|
|
printf("=== NVM TEST ===\r\n"); osDelay(5);
|
|
//NVM_erase();
|
|
if (progress++, NVM_init() != 0)
|
|
goto fail;
|
|
|
|
// load bytes from NVM and print them
|
|
size_t available = NVM_get_max_read_length();
|
|
if (available) {
|
|
printf("NVM contains %d valid bytes:\r\n", available); osDelay(5);
|
|
uint8_t buf[available];
|
|
if (progress++, NVM_read(0, buf, available) != 0)
|
|
goto fail;
|
|
for (size_t pos = 0; pos < available; ++pos) {
|
|
seed += buf[pos];
|
|
printf(" %02x", buf[pos]);
|
|
if ((((pos + 1) % 16) == 0) || ((pos + 1) == available))
|
|
printf("\r\n");
|
|
osDelay(2);
|
|
}
|
|
} else {
|
|
printf("NVM is empty\r\n"); osDelay(5);
|
|
}
|
|
|
|
// store new bytes in NVM (data based on seed)
|
|
printf("write 0x%02x, ..., 0x%02x to NVM\r\n", seed, seed + len - 1); osDelay(5);
|
|
for (size_t i = 0; i < len; i++)
|
|
data[i] = seed++;
|
|
if (progress++, NVM_start_write(len) != 0)
|
|
goto fail;
|
|
if (progress++, NVM_write(0, data, len / 2))
|
|
goto fail;
|
|
if (progress++, NVM_write(len / 2, &data[len / 2], len - (len / 2)))
|
|
goto fail;
|
|
if (progress++, NVM_commit())
|
|
goto fail;
|
|
printf("new data committed to NVM\r\n"); osDelay(5);
|
|
|
|
return;
|
|
|
|
fail:
|
|
printf("NVM test failed at %d!\r\n", progress);
|
|
}
|