557 lines
20 KiB
C
557 lines
20 KiB
C
/* ----------------------------------------------------------------------------
|
|
* Copyright (c) 2013 Ben Blazak <benblazak.dev@gmail.com>
|
|
* Released under The MIT License (see "doc/licenses/MIT.md")
|
|
* Project located at <https://github.com/benblazak/ergodox-firmware>
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
/** description
|
|
* Implements the EEPROM interface defined in "../eeprom.h" for the ATMega32U4
|
|
*/
|
|
|
|
|
|
#include <stdbool.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <util/atomic.h>
|
|
#include <avr/io.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`
|
|
* - `ACTION_FILL`
|
|
*/
|
|
enum action {
|
|
ACTION_WRITE,
|
|
ACTION_COPY,
|
|
ACTION_FILL,
|
|
};
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// types ----------------------------------------------------------------------
|
|
|
|
/** types/write_t/description
|
|
* To hold values for a "write" to the EEPROM
|
|
*
|
|
* Struct members:
|
|
* - `action`: A field indicating what type of action to perform. Valid values
|
|
* defined by `enum action`
|
|
* - `to`: The address in EEPROM memory space to write to
|
|
* - union:
|
|
* - `data`: The data to write
|
|
* - `ACTION_WRITE`
|
|
* - `ACTION_FILL`
|
|
* - `length`: The number of bytes left to copy
|
|
* - `ACTION_COPY`
|
|
*
|
|
* Implementation notes:
|
|
* - Since the ATMega32U4 only has 1024 bytes of EEPROM (addressed from
|
|
* `0` to `1024-1` = 2^10-1), it's safe to restrict `to` to 10 bits.
|
|
*/
|
|
typedef struct {
|
|
uint8_t action : 6;
|
|
uint16_t to : 10;
|
|
union {
|
|
uint8_t data;
|
|
uint8_t length;
|
|
};
|
|
} write_t;
|
|
|
|
/** types/extra_t/description
|
|
* To hold the extra values needed for some of the actions
|
|
*
|
|
* Struct members:
|
|
* - union:
|
|
* - `from`: The address in the EEPROM memory space to copy from
|
|
* - `ACTION_COPY`
|
|
* - `length`: The number of bytes left to fill
|
|
* - `ACTION_FILL`
|
|
*/
|
|
typedef struct {
|
|
union {
|
|
uint16_t from;
|
|
uint8_t length;
|
|
};
|
|
} extra_t;
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// variables ------------------------------------------------------------------
|
|
|
|
/** variables/status/description
|
|
* Flags for keeping track of the status of these functions (since write
|
|
* operations will be buffered)
|
|
*
|
|
* Members:
|
|
* - `writing`: Indicates that writes are queued and/or being performed
|
|
* - If this is `true`, `write_queued()` is either running or scheduled to
|
|
* run, and should not be rescheduled by any function other than itself.
|
|
* - If this is `false`, no writes are queued or being performed, and
|
|
* `write_queued()` must be scheduled to run externally before writes
|
|
* will commence.
|
|
*/
|
|
struct {
|
|
bool writing : 1;
|
|
} status;
|
|
|
|
/** variables/(group) queues/description
|
|
* Members:
|
|
* - `to_write`: To hold the write queue, and related metadata
|
|
* - `to_extra`: To hold the extra data needed for copies and fills,
|
|
* along with related metadata
|
|
* - `to_extra` is an awkward name, but it does have the advantage that it
|
|
* is the same length as `to_write`. And `to_write_extra` felt a bit too
|
|
* long in some places...
|
|
*
|
|
* 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_extra.data`: A queue of extra information for each
|
|
* `action == ACTION_COPY` or `action == ACTION_FILL` 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 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;
|
|
extra_t * data;
|
|
} to_extra;
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// 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_extra(void) {
|
|
#define queue to_extra
|
|
#define queue_type_size (sizeof(extra_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_extra`: operates on the `to_extra` queue
|
|
*/
|
|
static void pop_to_write(void) {
|
|
to_write.unused_front++;
|
|
resize_to_write();
|
|
}
|
|
static void pop_to_extra(void) {
|
|
to_extra.unused_front++;
|
|
resize_to_extra();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// back end functions ---------------------------------------------------------
|
|
|
|
/** functions/write/description
|
|
* Write `data` to `address` in EEPROM memory space as soon as possible
|
|
*
|
|
* Arguments:
|
|
* - `to: The address of the location to write to
|
|
* - `data`: The data to write
|
|
*
|
|
* Notes:
|
|
* - This function is static (and `eeprom__write()`, the public function, is
|
|
* written below) because writes are really slow, and we're going to buffer
|
|
* them.
|
|
*
|
|
* Implementation notes:
|
|
* - This function (and most of the comments) were taken more or less straight
|
|
* from the data sheet, section 5.3
|
|
* - If another write is in progress when this function is called, this
|
|
* function will busy wait until the first write has been completed. This
|
|
* may take quite some time (up to 3.4 ms if a write has just been started),
|
|
* so we must be careful about when this function is called.
|
|
* - This function starts the write to the EEPROM, but returns long before it
|
|
* has been completed.
|
|
*
|
|
* Assumptions:
|
|
* - The address passed as `to` is valid.
|
|
* - Voltage will never fall below the specified minimum for the clock
|
|
* frequency being used.
|
|
* - A write to flash memory (PROGMEM) will never be in progress when this
|
|
* function is called.
|
|
*/
|
|
static void write(uint16_t to, uint8_t data) {
|
|
|
|
// - if a write is in progress, this will also wait until it's finished
|
|
uint8_t old_data = eeprom__read( (void *) to );
|
|
|
|
if (data == old_data) {
|
|
// do nothing
|
|
return;
|
|
} else if (data == 0xFF) {
|
|
// erase only (1.8 ms)
|
|
EECR &= ~(1<<EEPM1); // clear
|
|
EECR |= (1<<EEPM0); // set
|
|
} else if (old_data == 0xFF) {
|
|
// write only (1.8 ms)
|
|
EECR |= (1<<EEPM1); // set
|
|
EECR &= ~(1<<EEPM0); // clear
|
|
} else {
|
|
// erase and write in one (atomic) operation (3.4 ms)
|
|
EECR &= ~(1<<EEPM1); // clear
|
|
EECR &= ~(1<<EEPM0); // clear
|
|
}
|
|
|
|
EEAR = to; // set up address register
|
|
EEDR = data; // set up data register
|
|
|
|
// - interrupts must be disabled between these two operations, or else
|
|
// "EEPROM Master Programming Enable" may time out (since it's cleared by
|
|
// hardware 4 clock cycles after being written to `1` by software)
|
|
ATOMIC_BLOCK(ATOMIC_FORCEON) {
|
|
EECR |= (1<<EEMPE); // set "EEPROM Master Programming Enable" to `1`
|
|
EECR |= (1<<EEPE); // start EEPROM write (then halt, 2 clock cycles)
|
|
}
|
|
}
|
|
|
|
/** functions/write_queued/description
|
|
* Write (or copy, or fill) the next byte of data as dictated by our queue(s),
|
|
* and schedule the write of the next byte if necessary
|
|
*
|
|
* Assumptions:
|
|
* - The length of `to_extra` is always correct: i.e. there is exactly 1 entry
|
|
* in `to_extra` for every `action == ACTION_...` element in `to_write` that
|
|
* is supposed to have one.
|
|
*/
|
|
static void write_queued(void) {
|
|
#define next_write ( to_write.data[to_write.unused_front] )
|
|
#define next_extra ( to_extra.data[to_extra.unused_front] )
|
|
#define length(queue) ( queue.allocated \
|
|
- queue.unused_front \
|
|
- queue.unused_back )
|
|
|
|
// if there's nothing to write
|
|
// - 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) {
|
|
|
|
// write 1 byte
|
|
write( next_write.to, next_write.data );
|
|
// prepare for the next
|
|
pop_to_write();
|
|
|
|
} else if ( next_write.action == ACTION_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_extra`
|
|
// when this function is called
|
|
if (next_write.length == 0) {
|
|
pop_to_write();
|
|
pop_to_extra();
|
|
}
|
|
|
|
// copy 1 byte
|
|
write( next_write.to, eeprom__read( (void *) next_extra.from ) );
|
|
// prepare for the next
|
|
if (next_write.to < next_extra.from) {
|
|
++(next_write.to);
|
|
++(next_extra.from);
|
|
} else {
|
|
--(next_write.to);
|
|
--(next_extra.from);
|
|
}
|
|
--(next_write.length);
|
|
|
|
} else if ( next_write.action == ACTION_FILL ) {
|
|
|
|
// if we're done with the current fill
|
|
// - checking for this here requires an extra iteration between a fill
|
|
// being finished and the next operation being started; but it also
|
|
// allows us not to make assumptions about the state of `to_extra`
|
|
// when this function is called
|
|
if (next_extra.length == 0) {
|
|
pop_to_write();
|
|
pop_to_extra();
|
|
}
|
|
|
|
// fill 1 byte
|
|
write( next_write.to, next_write.data );
|
|
// prepare for the next
|
|
--(next_extra.length);
|
|
|
|
} else {
|
|
// if we get here, there was an invalid node: remove it
|
|
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 a nonzero integer (other than by truncation), the
|
|
// `+1` is not strictly necessary; but in that case, being so close to
|
|
// needing another cycle, we may as well wait anyway.
|
|
timer__schedule_cycles( (3.5/OPT__DEBOUNCE_TIME)+1, &write_queued );
|
|
|
|
#undef next_write
|
|
#undef next_extra
|
|
#undef length
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// front end functions --------------------------------------------------------
|
|
|
|
/** functions/eeprom__read/description
|
|
* Implementation notes:
|
|
* - This function (and most of the comments) were taken more or less straight
|
|
* from the data sheet, section 5.3
|
|
* - If a write is in progress when this function is called, this function will
|
|
* busy wait until the write has been completed. This may take quite some
|
|
* time (up to 3.4 ms if a write has just been started), so you should be
|
|
* careful about when this function is called.
|
|
*
|
|
* Assumptions:
|
|
* - The address passed as `address` is valid.
|
|
*/
|
|
uint8_t eeprom__read(void * from) {
|
|
while (EECR & (1<<EEPE)); // wait for previous write to complete
|
|
EEAR = (uint16_t) from; // set up address register
|
|
EECR |= (1<<EERE); // start EEPROM read (then halt, 4 clock cycles)
|
|
return EEDR; // return the value in the data register
|
|
}
|
|
|
|
uint8_t eeprom__write(void * address, uint8_t data) {
|
|
to_write.unused_back--;
|
|
if (resize_to_write()) {
|
|
to_write.unused_back++;
|
|
return 1; // resize failed
|
|
}
|
|
|
|
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].data = data;
|
|
|
|
if (!status.writing) {
|
|
timer__schedule_cycles( 0, &write_queued );
|
|
status.writing = true;
|
|
}
|
|
|
|
return 0; // success
|
|
}
|
|
|
|
uint8_t eeprom__fill(void * to, uint8_t data, uint8_t length) {
|
|
to_write.unused_back--;
|
|
to_extra.unused_back--;
|
|
if (resize_to_write() || resize_to_extra()) {
|
|
to_write.unused_back++; resize_to_write();
|
|
to_extra.unused_back++; resize_to_extra();
|
|
return 1; // resize failed
|
|
}
|
|
|
|
uint8_t index;
|
|
|
|
index = to_write.allocated - to_write.unused_back - 1;
|
|
to_write.data[index].action = ACTION_FILL;
|
|
to_write.data[index].to = (uint16_t) to;
|
|
to_write.data[index].data = data;
|
|
|
|
index = to_extra.allocated - to_extra.unused_back - 1;
|
|
to_extra.data[index].length = length;
|
|
|
|
if (!status.writing) {
|
|
timer__schedule_cycles( 0, &write_queued );
|
|
status.writing = true;
|
|
}
|
|
|
|
return 0; // success
|
|
}
|
|
|
|
uint8_t eeprom__copy(void * to, void * from, uint8_t length) {
|
|
if (to == from)
|
|
return 0; // nothing to do
|
|
|
|
to_write.unused_back--;
|
|
to_extra.unused_back--;
|
|
if (resize_to_write() || resize_to_extra()) {
|
|
to_write.unused_back++; resize_to_write();
|
|
to_extra.unused_back++; resize_to_extra();
|
|
return 1; // resize failed
|
|
}
|
|
|
|
uint8_t index;
|
|
|
|
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].length = length;
|
|
|
|
index = to_extra.allocated - to_extra.unused_back - 1;
|
|
to_extra.data[index].from = (uint16_t) from;
|
|
|
|
if (!status.writing) {
|
|
timer__schedule_cycles( 0, &write_queued );
|
|
status.writing = true;
|
|
}
|
|
|
|
return 0; // success
|
|
}
|
|
|
|
uint8_t eeprom__block_read(void * to, void * from, uint8_t length) {
|
|
for (; length; length--)
|
|
*(uint8_t *)(to+length-1) = eeprom__read( from+length-1 );
|
|
|
|
return 0; // success
|
|
}
|
|
|