switched lib/eeprom from linked lists to dynamic arrays
parent
9796e78efb
commit
c6944a095e
|
@ -137,5 +137,14 @@ uint8_t eeprom__copy (uint8_t * to, uint8_t * from, uint8_t length);
|
|||
* - The direction in which the block is copied (beginning with `to` and
|
||||
* incrementing, or beginning with `to + length - 1` and decrementing) is
|
||||
* undefined.
|
||||
* - Ideally, one would probably want to start with `to` and increment if
|
||||
* one was copying from a higher address to a lower one, and with `to +
|
||||
* length - 1` and decrement if one was copying from a lower address to a
|
||||
* higher one. This way, copying overlapping blocks would work as one
|
||||
* would expect (i.e. data would be moved up or down by `length` bytes).
|
||||
* But it seems like this might become more complicated than I'd like it
|
||||
* to be, and it also (if it turns out to matter) would be inordinately
|
||||
* difficult to make safe against data corruption in the event of power
|
||||
* loss.
|
||||
*/
|
||||
|
||||
|
|
|
@ -14,11 +14,37 @@
|
|||
#include <stdlib.h>
|
||||
#include <util/atomic.h>
|
||||
#include <avr/io.h>
|
||||
#include "../../../firmware/lib/data-types/list.h"
|
||||
#include "../../../firmware/lib/timer.h"
|
||||
#include "../eeprom.h"
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// macros ---------------------------------------------------------------------
|
||||
|
||||
/** macros/MIN_UNUSED/description
|
||||
* The minimum number of elements to have unused after a resize (in any queue)
|
||||
*/
|
||||
#define MIN_UNUSED 0
|
||||
|
||||
/** macros/MAX_UNUSED/description
|
||||
* The maximum number of elements to have unused after a resize (in any queue)
|
||||
*/
|
||||
#define MAX_UNUSED 4
|
||||
|
||||
/** macros/(enum) action/description
|
||||
* Valid values for `write_t.action`, determining the type of action to perform
|
||||
* for an entry in `to_write`
|
||||
*
|
||||
* Members:
|
||||
* - `ACTION_WRITE`
|
||||
* - `ACTION_COPY`
|
||||
*/
|
||||
enum action {
|
||||
ACTION_WRITE,
|
||||
ACTION_COPY,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// types ----------------------------------------------------------------------
|
||||
|
||||
/** types/write_t/description
|
||||
* To hold values for a "write" to the EEPROM
|
||||
|
@ -36,9 +62,8 @@
|
|||
* `to` to 10 bits.
|
||||
*/
|
||||
typedef struct {
|
||||
list__node_t _super;
|
||||
uint8_t action : 6;
|
||||
uint16_t to : 10; // since we only have 1024 bytes
|
||||
uint16_t to : 10;
|
||||
uint8_t value;
|
||||
} write_t;
|
||||
|
||||
|
@ -49,24 +74,11 @@ typedef struct {
|
|||
* - `from`: The address in the EEPROM memory space to copy from
|
||||
*/
|
||||
typedef struct {
|
||||
list__node_t _super;
|
||||
uint16_t from;
|
||||
} copy_t;
|
||||
|
||||
/** macros/(enum) action/description
|
||||
* Valid values for `write_t.action`, determining the type of action to perform
|
||||
* for an entry in `to_write`
|
||||
*
|
||||
* Members:
|
||||
* - `ACTION_WRITE`
|
||||
* - `ACTION_COPY`
|
||||
*/
|
||||
enum action {
|
||||
ACTION_WRITE,
|
||||
ACTION_COPY,
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// variables ------------------------------------------------------------------
|
||||
|
||||
/** variables/status/description
|
||||
* Flags for keeping track of the status of these functions (since write
|
||||
|
@ -84,18 +96,181 @@ struct {
|
|||
bool writing : 1;
|
||||
} status;
|
||||
|
||||
/** variables/to_write/description
|
||||
* A list of writes (and copies) to perform
|
||||
/** variables/(group) queues/description
|
||||
* Members:
|
||||
* - `to_write`: To hold the write queue, and related metadata
|
||||
* - `to_copy`: To hold the extra data needed for copies, along with related
|
||||
* metadata
|
||||
*
|
||||
* Struct members:
|
||||
* - `allocated`: The number of positions allocated
|
||||
* - `unused_front`: The number of unused positions at the beginning of the
|
||||
* queue
|
||||
* - `unused_back`: The number of unused positions at the end of the queue
|
||||
* - `to_write.data`: A queue of writes (and copies) to perform
|
||||
* - `to_copy.data`: A queue of extra information for each `action ==
|
||||
* ACTION_COPY` element in `to_write`
|
||||
*
|
||||
* Implementation notes:
|
||||
* - `unused_front` and `unused_back` each have a range of between -8 and 7,
|
||||
* inclusive. This means that, with the current scheme, `MAX_UNUSED` must
|
||||
* not be greater than 7. Note that negative values for either only make
|
||||
* sense temporarily, when adding an element to the queue.
|
||||
*/
|
||||
static list__list_t to_write;
|
||||
|
||||
/** variables/to_copy/description
|
||||
* A list of extra information for each `action == ACTION_COPY` element in
|
||||
* `to_write`
|
||||
*/
|
||||
static list__list_t to_copy;
|
||||
static struct {
|
||||
uint8_t allocated;
|
||||
int8_t unused_front : 4;
|
||||
int8_t unused_back : 4;
|
||||
write_t * data;
|
||||
} to_write;
|
||||
static struct {
|
||||
uint8_t allocated;
|
||||
int8_t unused_front : 4;
|
||||
int8_t unused_back : 4;
|
||||
copy_t * data;
|
||||
} to_copy;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// variable manipulator functions ---------------------------------------------
|
||||
|
||||
/** functions/(group) resize/description
|
||||
* Resize the appropriate queue, so that the number of unused elements is
|
||||
* between `MIN_UNUSED` and `MAX_UNUSED`, inclusive
|
||||
*
|
||||
* Returns:
|
||||
* - success: `0`
|
||||
* - failure: [other]
|
||||
*
|
||||
* Notes:
|
||||
* - These functions are separate (though identical in source, before macro
|
||||
* expansion) partly because this allows us to deal with elements of the
|
||||
* queue as discrete blocks of memory. It might not be hard to make the
|
||||
* function generic, but then we would loose the ability to depend on type
|
||||
* information, and have to operate on each `queue.data` solely as an array
|
||||
* of bytes. It's a close call, but I feel like the conceptual clarity we
|
||||
* gain with this method outweighs the clarity we would gain by having only a
|
||||
* single function, with no copy+pasted code.
|
||||
*
|
||||
* Implementation notes:
|
||||
* - We use a signed type for `unused` in case `unused_front + unused_back`
|
||||
* turns out to be negative, as may happen when adding a new element to the
|
||||
* queue.
|
||||
* - The only time `realloc()` should fail is if we are trying to grow the
|
||||
* queue. See [the documentation]
|
||||
* (http://www.nongnu.org/avr-libc/user-manual/malloc.html)
|
||||
* for `malloc()` in avr-libc.
|
||||
*/
|
||||
static uint8_t resize_to_write(void) {
|
||||
#define queue to_write
|
||||
#define queue_type_size (sizeof(write_t))
|
||||
|
||||
int8_t unused = queue.unused_front + queue.unused_back;
|
||||
|
||||
if (MIN_UNUSED <= queue.unused_back && unused <= MAX_UNUSED)
|
||||
return 0; // no need to grow, shrink, or shift the queue
|
||||
|
||||
// shift down (if necessary), and update metadata
|
||||
// - start with the first used element, and copy it down to index 0
|
||||
// - copy the second used element down to index 1, and so on until we copy
|
||||
// the last used element
|
||||
// - if there are no empty elements at the front of the queue, this will do
|
||||
// nothing
|
||||
if (queue.unused_front != 0) {
|
||||
for (uint8_t i = queue.unused_front; i < queue.allocated - unused; i++)
|
||||
queue.data[i-queue.unused_front] = queue.data[i];
|
||||
|
||||
queue.unused_front = 0;
|
||||
queue.unused_back = unused;
|
||||
|
||||
if (MIN_UNUSED <= unused && unused <= MAX_UNUSED)
|
||||
return 0; // no need to grow or shrink the queue
|
||||
}
|
||||
|
||||
uint8_t new_allocated;
|
||||
if (UINT8_MAX >= queue.allocated - unused + (MIN_UNUSED+MAX_UNUSED)/2)
|
||||
new_allocated = queue.allocated - unused + (MIN_UNUSED+MAX_UNUSED)/2;
|
||||
else if (UINT8_MAX >= queue.allocated - unused + MIN_UNUSED)
|
||||
new_allocated = UINT8_MAX;
|
||||
else
|
||||
return 1; // unable to count the required number of elements
|
||||
|
||||
void * new_data = realloc( queue.data, queue_type_size * new_allocated );
|
||||
if (!new_data)
|
||||
return 1; // error: `realloc()` failed (unable to grow queue)
|
||||
|
||||
queue.unused_back += new_allocated - queue.allocated;
|
||||
queue.allocated = new_allocated;
|
||||
queue.data = new_data;
|
||||
return 0; // success: queue reallocated
|
||||
|
||||
#undef queue
|
||||
#undef queue_type_size
|
||||
}
|
||||
static uint8_t resize_to_copy(void) {
|
||||
#define queue to_copy
|
||||
#define queue_type_size (sizeof(copy_t))
|
||||
|
||||
int8_t unused = queue.unused_front + queue.unused_back;
|
||||
|
||||
if (MIN_UNUSED <= queue.unused_back && unused <= MAX_UNUSED)
|
||||
return 0; // no need to grow, shrink, or shift the queue
|
||||
|
||||
// shift down (if necessary), and update metadata
|
||||
// - start with the first used element, and copy it down to index 0
|
||||
// - copy the second used element down to index 1, and so on until we copy
|
||||
// the last used element
|
||||
// - if there are no empty elements at the front of the queue, this will do
|
||||
// nothing
|
||||
if (queue.unused_front != 0) {
|
||||
for (uint8_t i = queue.unused_front; i < queue.allocated - unused; i++)
|
||||
queue.data[i-queue.unused_front] = queue.data[i];
|
||||
|
||||
queue.unused_front = 0;
|
||||
queue.unused_back = unused;
|
||||
|
||||
if (MIN_UNUSED <= unused && unused <= MAX_UNUSED)
|
||||
return 0; // no need to grow or shrink the queue
|
||||
}
|
||||
|
||||
uint8_t new_allocated;
|
||||
if (UINT8_MAX >= queue.allocated - unused + (MIN_UNUSED+MAX_UNUSED)/2)
|
||||
new_allocated = queue.allocated - unused + (MIN_UNUSED+MAX_UNUSED)/2;
|
||||
else if (UINT8_MAX >= queue.allocated - unused + MIN_UNUSED)
|
||||
new_allocated = UINT8_MAX;
|
||||
else
|
||||
return 1; // unable to count the required number of elements
|
||||
|
||||
void * new_data = realloc( queue.data, queue_type_size * new_allocated );
|
||||
if (!new_data)
|
||||
return 1; // error: `realloc()` failed (unable to grow queue)
|
||||
|
||||
queue.unused_back += new_allocated - queue.allocated;
|
||||
queue.allocated = new_allocated;
|
||||
queue.data = new_data;
|
||||
return 0; // success: queue reallocated
|
||||
|
||||
#undef queue
|
||||
#undef queue_type_size
|
||||
}
|
||||
|
||||
/** functions/(group) pop/description
|
||||
* Remove the first element from the appropriate queue
|
||||
*
|
||||
* Members:
|
||||
* - `pop_to_write`: operates on the `to_write` queue
|
||||
* - `pop_to_copy`: operates on the `to_copy` queue
|
||||
*/
|
||||
static void pop_to_write(void) {
|
||||
to_write.unused_front++;
|
||||
resize_to_write();
|
||||
}
|
||||
static void pop_to_copy(void) {
|
||||
to_copy.unused_front++;
|
||||
resize_to_copy();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// back end functions ---------------------------------------------------------
|
||||
|
||||
/** functions/write/description
|
||||
* Write `data` to `address` in EEPROM memory space as soon as possible
|
||||
|
@ -165,64 +340,70 @@ static void write(uint16_t to, uint8_t data) {
|
|||
* schedule the write of the next byte if necessary
|
||||
*/
|
||||
static void write_queued(void) {
|
||||
#define next_write ((write_t *) to_write.head)
|
||||
#define next_copy ((copy_t *) to_copy.head)
|
||||
#define next_write ( to_write.data[to_write.unused_front] )
|
||||
#define next_copy ( to_copy.data[to_copy.unused_front] )
|
||||
#define length(queue) ( queue.allocated \
|
||||
- queue.unused_front \
|
||||
- queue.unused_back )
|
||||
|
||||
// if there's nothing to write
|
||||
// - checking for this here will cause this function to be called an extra
|
||||
// time before it stops rescheduling itself; but it also allows us not to
|
||||
// make assumptions about the state of `to_write` when this function is
|
||||
// called
|
||||
if (to_write.length == 0) {
|
||||
// - checking for this here (instead of after writing) will cause this
|
||||
// function to be called an extra time before it stops rescheduling
|
||||
// itself; but it also allows us not to make assumptions about the state
|
||||
// of `to_write` when this function is called
|
||||
if ( length(to_write) == 0 ) {
|
||||
status.writing = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (next_write->action == ACTION_WRITE) {
|
||||
if (next_write.action == ACTION_WRITE) {
|
||||
|
||||
// write 1 byte
|
||||
write( next_write->to, next_write->value );
|
||||
write( next_write.to, next_write.value );
|
||||
// prepare for the next
|
||||
free( list__pop_index(&to_write, 0) );
|
||||
pop_to_write();
|
||||
|
||||
} else if (next_write->action == ACTION_COPY && to_copy.length) {
|
||||
} else if ( next_write.action == ACTION_COPY && length(to_copy) ) {
|
||||
|
||||
// if we're done with the current copy
|
||||
// - checking for this here requires an extra iteration between a copy
|
||||
// being finished and the next operation being started; but it also
|
||||
// allows us not to make assumptions about the state of `to_copy`
|
||||
// when this function is called
|
||||
if (next_write->value == 0) {
|
||||
free( list__pop_index(&to_write, 0) );
|
||||
free( list__pop_index(&to_copy, 0) );
|
||||
if (next_write.value == 0) {
|
||||
pop_to_write();
|
||||
pop_to_copy();
|
||||
}
|
||||
|
||||
// copy 1 byte
|
||||
write( next_write->to, eeprom__read( (uint8_t *) next_copy->from ) );
|
||||
write( next_write.to, eeprom__read( (uint8_t *) next_copy.from ) );
|
||||
// prepare for the next
|
||||
++(next_write->to);
|
||||
++(next_copy->from);
|
||||
--(next_write->value);
|
||||
++(next_write.to);
|
||||
++(next_copy.from);
|
||||
--(next_write.value);
|
||||
|
||||
} else {
|
||||
// if we get here, there was an invalid node: remove it
|
||||
free( list__pop_index(&to_write, 0) );
|
||||
pop_to_write();
|
||||
}
|
||||
|
||||
// - `(3.5/OPT__DEBOUNCE_TIME)+1` gives us the number of cycles we need to
|
||||
// wait until the next write, where `3.5` is the maximum number of
|
||||
// milliseconds an EEPROM write can take, and `OPT__DEBOUNCE_TIME` is the
|
||||
// minimum number of milliseconds a scan can take. Note that if the
|
||||
// division produces an integer, the `+1` is strictly unnecessary, since
|
||||
// no truncation will be performed; but if we're that close to needing to
|
||||
// wait an extra cycle we may as well wait anyway, just to be safe.
|
||||
// division produces an integer (other than by truncation), the `+1` is
|
||||
// strictly unnecessary, since no truncation will be performed; but if
|
||||
// we're that close to needing to wait an extra cycle we may as well wait
|
||||
// anyway, just to be safe.
|
||||
timer__schedule_cycles( (3.5/OPT__DEBOUNCE_TIME)+1, &write_queued );
|
||||
|
||||
#undef next_write
|
||||
#undef next_copy
|
||||
#undef length
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// front end functions --------------------------------------------------------
|
||||
|
||||
/** functions/eeprom__read/description
|
||||
* Implementation notes:
|
||||
|
@ -243,17 +424,20 @@ uint8_t eeprom__read(uint8_t * from) {
|
|||
return EEDR; // return the value in the data register
|
||||
}
|
||||
|
||||
// note: this should be the only function adding elements to `to_write`
|
||||
uint8_t eeprom__write(uint8_t * address, uint8_t data) {
|
||||
write_t * write = malloc(sizeof(write_t));
|
||||
if (!write) return 1; // error
|
||||
to_write.unused_back--;
|
||||
if (resize_to_write()) {
|
||||
to_write.unused_back++;
|
||||
return 1; // resize failed
|
||||
}
|
||||
|
||||
write->action = ACTION_WRITE;
|
||||
write->to = (uint16_t) address;
|
||||
write->value = data;
|
||||
uint8_t index = to_write.allocated - to_write.unused_back - 1;
|
||||
to_write.data[index].action = ACTION_WRITE;
|
||||
to_write.data[index].to = (uint16_t) address;
|
||||
to_write.data[index].value = data;
|
||||
|
||||
list__insert(&to_write, -1, write);
|
||||
|
||||
if (! status.writing) {
|
||||
if (!status.writing) {
|
||||
timer__schedule_cycles( 0, &write_queued );
|
||||
status.writing = true;
|
||||
}
|
||||
|
@ -261,25 +445,27 @@ uint8_t eeprom__write(uint8_t * address, uint8_t data) {
|
|||
return 0; // success
|
||||
}
|
||||
|
||||
// note: this should be the only function adding elements to `to_copy`
|
||||
uint8_t eeprom__copy(uint8_t * to, uint8_t * from, uint8_t length) {
|
||||
write_t * write = malloc(sizeof(write_t));
|
||||
copy_t * copy = malloc(sizeof(copy_t));
|
||||
if (!(write && copy)) {
|
||||
free(write);
|
||||
free(copy);
|
||||
return 1; // error
|
||||
to_write.unused_back--;
|
||||
to_copy.unused_back--;
|
||||
if (resize_to_write() || resize_to_copy()) {
|
||||
to_write.unused_back++; resize_to_write();
|
||||
to_copy.unused_back++; resize_to_copy();
|
||||
return 1; // resize failed
|
||||
}
|
||||
|
||||
write->action = ACTION_COPY;
|
||||
write->to = (uint16_t) to;
|
||||
write->value = length;
|
||||
uint8_t index;
|
||||
|
||||
copy->from = (uint16_t) from;
|
||||
index = to_write.allocated - to_write.unused_back - 1;
|
||||
to_write.data[index].action = ACTION_COPY;
|
||||
to_write.data[index].to = (uint16_t) to;
|
||||
to_write.data[index].value = length;
|
||||
|
||||
list__insert(&to_write, -1, write);
|
||||
list__insert(&to_copy, -1, copy);
|
||||
index = to_copy.allocated - to_copy.unused_back - 1;
|
||||
to_copy.data[index].from = (uint16_t) from;
|
||||
|
||||
if (! status.writing) {
|
||||
if (!status.writing) {
|
||||
timer__schedule_cycles( 0, &write_queued );
|
||||
status.writing = true;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue