switched lib/eeprom from linked lists to dynamic arrays

partial-rewrite
Ben Blazak 2013-07-23 11:14:22 -07:00
parent 9796e78efb
commit c6944a095e
2 changed files with 264 additions and 69 deletions

View File

@ -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.
*/

View File

@ -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;
}