diff --git a/core/app_main.c b/core/app_main.c
index a38fb5f..9f3ccd9 100644
--- a/core/app_main.c
+++ b/core/app_main.c
@@ -26,6 +26,7 @@
 #include "espressif/esp_common.h"
 #include "espressif/phy_info.h"
 #include "sdk_internal.h"
+#include "sysparam.h"
 
 /* This is not declared in any header file (but arguably should be) */
 
@@ -139,6 +140,8 @@ void IRAM sdk_user_start(void) {
     uint32_t cksum_len;
     uint32_t cksum_value;
     uint32_t ic_flash_addr;
+    uint32_t sysparam_addr;
+    sysparam_status_t status;
 
     SPI(0).USER0 |= SPI_USER0_CS_SETUP;
     sdk_SPIRead(0, buf32, 4);
@@ -204,6 +207,20 @@ void IRAM sdk_user_start(void) {
     }
     memcpy(&sdk_g_ic.s, buf32, sizeof(struct sdk_g_ic_saved_st));
 
+    // By default, put the sysparam region just below the config sectors at the
+    // top of the flash space
+    sysparam_addr = flash_size - (4 + DEFAULT_SYSPARAM_SECTORS) * sdk_flashchip.sector_size;
+    status = sysparam_init(sysparam_addr, flash_size);
+    if (status == SYSPARAM_NOTFOUND) {
+        status = sysparam_create_area(sysparam_addr, DEFAULT_SYSPARAM_SECTORS, false);
+        if (status == SYSPARAM_OK) {
+            status = sysparam_init(sysparam_addr, 0);
+        }
+    }
+    if (status != SYSPARAM_OK) {
+        printf("WARNING: Could not initialize sysparams (%d)!\n", status);
+    }
+
     user_start_phase2();
 }
 
diff --git a/core/include/esp/types.h b/core/include/esp/types.h
index cb816da..53c4cd4 100644
--- a/core/include/esp/types.h
+++ b/core/include/esp/types.h
@@ -3,6 +3,7 @@
 
 #include <stdint.h>
 #include <stdbool.h>
+#include <stddef.h>
 
 typedef volatile uint32_t *esp_reg_t;
 
diff --git a/core/include/sysparam.h b/core/include/sysparam.h
new file mode 100644
index 0000000..9260e73
--- /dev/null
+++ b/core/include/sysparam.h
@@ -0,0 +1,413 @@
+#ifndef _SYSPARAM_H_
+#define _SYSPARAM_H_
+
+#include <esp/types.h>
+
+#ifndef DEFAULT_SYSPARAM_SECTORS
+#define DEFAULT_SYSPARAM_SECTORS 4
+#endif
+
+/** @file sysparam.h
+ *
+ *  Read/write "system parameters" to persistent flash.
+ *
+ *  System parameters are stored as key/value pairs.  Keys are string values
+ *  between 1 and 255 characters long.  Values can be any data up to 255 bytes
+ *  in length (but are most commonly also text strings).  Up to 126 key/value
+ *  pairs can be stored at a time.
+ *
+ *  Keys and values are stored in flash using a progressive list structure
+ *  which allows space-efficient storage and minimizes flash erase cycles,
+ *  improving write speed and increasing the lifespan of the flash memory.
+ */
+
+/** Status codes returned by all sysparam functions
+ *
+ *  Error codes (`SYSPARAM_ERR_*`) all have values less than zero, and can be
+ *  returned by any function.  Values greater than zero are non-error status
+ *  codes which may be returned by some functions to indicate various results.
+ */
+typedef enum {
+    SYSPARAM_OK           = 0,  ///< Success
+    SYSPARAM_NOTFOUND     = 1,  ///< Entry not found matching criteria
+    SYSPARAM_PARSEFAILED  = 2,  ///< Unable to parse retrieved value
+    SYSPARAM_ERR_NOINIT   = -1, ///< sysparam_init() must be called first
+    SYSPARAM_ERR_BADVALUE = -2, ///< One or more arguments were invalid
+    SYSPARAM_ERR_FULL     = -3, ///< No space left in sysparam area (or too many keys in use)
+    SYSPARAM_ERR_IO       = -4, ///< I/O error reading/writing flash
+    SYSPARAM_ERR_CORRUPT  = -5, ///< Sysparam region has bad/corrupted data
+    SYSPARAM_ERR_NOMEM    = -6, ///< Unable to allocate memory
+} sysparam_status_t;
+
+/** Structure used by sysparam_iter_next() to keep track of its current state
+ * and return its results.  This should be initialized by calling
+ * sysparam_iter_start() and cleaned up afterward by calling
+ * sysparam_iter_end().
+ */
+typedef struct {
+    char *key;
+    uint8_t *value;
+    size_t key_len;
+    size_t value_len;
+    bool binary;
+    size_t bufsize;
+    struct sysparam_context *ctx;
+} sysparam_iter_t;
+
+/** Initialize sysparam and set up the current area of flash to use.
+ *
+ *  This must be called (and return successfully) before any other sysparam
+ *  routines (except sysparam_create_area()) are called.
+ *
+ *  This should normally be taken care of automatically on boot by the OS
+ *  startup routines.  It may be necessary to call it specially, however, if
+ *  the normal initialization failed, or after calling sysparam_create_area()
+ *  to reformat the current area.
+ *
+ *  This routine will start at `base_addr` and scan all sectors up to
+ *  `top_addr` looking for a valid sysparam area.  If `top_addr` is zero (or
+ *  equal to `base_addr`, then only the sector at `base_addr` will be checked.
+ *
+ *  @param[in] base_addr  The flash address to start looking for the start of
+ *                        the (already present) sysparam area
+ *  @param[in] top_addr   The flash address to stop looking for the sysparam
+ *                        area
+ *
+ *  @retval ::SYSPARAM_OK           Initialization successful.
+ *  @retval ::SYSPARAM_NOTFOUND     The specified address does not appear to
+ *                                  contain a sysparam area.  It may be
+ *                                  necessary to call sysparam_create_area() to
+ *                                  create one first.
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_init(uint32_t base_addr, uint32_t top_addr);
+
+/** Create a new sysparam area in flash at the specified address.
+ *
+ *  By default, this routine will scan the specified area to make sure it
+ *  appears to be empty (i.e. all 0xFF bytes) before setting it up as a new
+ *  sysparam area.  If there appears to be other data already present, it will
+ *  not overwrite it.  Setting `force` to `true` will cause it to clobber any
+ *  existing data instead.
+ *
+ *  @param[in] base_addr   The flash address at which it should start
+ *                         (must be a multiple of the sector size)
+ *  @param[in] num_sectors The number of flash sectors to use for the sysparam
+ *                         area.  This should be an even number >= 2.  Note
+ *                         that the actual amount of useable parameter space
+ *                         will be roughly half this amount.
+ *  @param[in] force       Proceed even if the space does not appear to be empty
+ *
+ *  @retval ::SYSPARAM_OK           Area (re)created successfully.
+ *  @retval ::SYSPARAM_NOTFOUND     `force` was not specified, and the area at
+ *                                  `base_addr` appears to have other data.  No
+ *                                  action taken.
+ *  @retval ::SYSPARAM_ERR_BADVALUE The `num_sectors` value was not even (or
+ *                                  was zero)
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ *
+ *  Note: This routine can create a sysparam area in another location than the
+ *  one currently being used, but does not change which area is currently used
+ *  (you will need to call sysparam_init() again if you want to do that).  If
+ *  you reformat the area currently being used, you will also need to call
+ *  sysparam_init() again afterward before you will be able to continue using
+ *  it.
+ */ 
+sysparam_status_t sysparam_create_area(uint32_t base_addr, uint16_t num_sectors, bool force);
+
+/** Get the start address and size of the currently active sysparam area
+ *
+ *  Fills in `base_addr` and `num_sectors` with the location and size of the
+ *  currently active sysparam area.  The returned values correspond to the
+ *  arguments passed to the sysparam_create_area() call when the area was
+ *  originally created.
+ *
+ *  @param[out] base_addr   The flash address at which the sysparam area starts
+ *  @param[out] num_sectors The number of flash sectors used by the sysparam
+ *                          area
+ *
+ *  @retval ::SYSPARAM_OK           Completed successfully
+ *  @retval ::SYSPARAM_ERR_NOINIT   No current sysparam area is active
+ */
+sysparam_status_t sysparam_get_info(uint32_t *base_addr, uint32_t *num_sectors);
+
+/** Get the value associated with a key
+ *
+ *  This is the core "get value" function.  It will retrieve the value for the
+ *  specified key in a freshly malloc()'d buffer and return it.  Raw values can
+ *  contain any data (including zero bytes), so the `actual_length` parameter
+ *  should be used to determine the length of the data in the buffer.
+ *
+ *  It is up to the caller to free() the returned buffer when done using it.
+ *
+ *  Note: If the status result is anything other than ::SYSPARAM_OK, the value
+ *  in `destptr` is not changed.  This means it is possible to set a default
+ *  value before calling this function which will be left as-is if a sysparam
+ *  value could not be successfully read.
+ *
+ *  @param[in]  key            Key name (zero-terminated string)
+ *  @param[out] destptr        Pointer to a location to hold the address of the
+ *                             returned data buffer
+ *  @param[out] actual_length  Pointer to a location to hold the length of the
+ *                             returned data buffer (may be NULL)
+ *  @param[out] is_binary      Pointer to a bool to hold whether the returned
+ *                             value is "binary" or not (may be NULL)
+ *
+ *  @retval ::SYSPARAM_OK           Value successfully retrieved.
+ *  @retval ::SYSPARAM_NOTFOUND     Key/value not found.  No buffer returned.
+ *  @retval ::SYSPARAM_ERR_NOINIT   sysparam_init() must be called first
+ *  @retval ::SYSPARAM_ERR_NOMEM    Unable to allocate memory
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_get_data(const char *key, uint8_t **destptr, size_t *actual_length, bool *is_binary);
+
+/** Get the value associate with a key (static buffers only)
+ *
+ *  This performs the same function as sysparam_get_data() but without
+ *  performing any memory allocations.  It can thus be used before the heap has
+ *  been configured or in other cases where using the heap would be a problem
+ *  (i.e. in an OOM handler, etc).  It requires that the caller pass in a
+ *  suitably sized buffer for the value to be read (if the supplied buffer is
+ *  not large enough, the returned value will be truncated and the full
+ *  required length will be returned in `actual_length`).
+ *
+ *  NOTE: In addition to being large enough for the value, the supplied buffer
+ *  must also be at least as large as the length of the key being requested.
+ *  If it is not, an error will be returned.
+ *
+ *  @param[in]  key            Key name (zero-terminated string)
+ *  @param[in]  buffer         Pointer to a buffer to hold the returned value
+ *  @param[in]  buffer_size    Length of the supplied buffer in bytes
+ *  @param[out] actual_length  pointer to a location to hold the actual length
+ *                             of the data which was associated with the key
+ *                             (may be NULL).
+ *  @param[out] is_binary      Pointer to a bool to hold whether the returned
+ *                             value is "binary" or not (may be NULL)
+ *
+ *  @retval ::SYSPARAM_OK           Value successfully retrieved
+ *  @retval ::SYSPARAM_NOTFOUND     Key/value not found
+ *  @retval ::SYSPARAM_ERR_NOINIT   sysparam_init() must be called first
+ *  @retval ::SYSPARAM_ERR_NOMEM    The supplied buffer is too small
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_get_data_static(const char *key, uint8_t *buffer, size_t buffer_size, size_t *actual_length, bool *is_binary);
+
+/** Get the string value associated with a key
+ * 
+ *  This routine can be used if you know that the value in a key will (or at
+ *  least should) be a string.  It will return a zero-terminated char buffer
+ *  containing the value retrieved.
+ *
+ *  It is up to the caller to free() the returned buffer when done using it.
+ *
+ *  Note: If the status result is anything other than ::SYSPARAM_OK, the value
+ *  in `destptr` is not changed.  This means it is possible to set a default
+ *  value before calling this function which will be left as-is if a sysparam
+ *  value could not be successfully read.
+ *
+ *  @param[in]  key      Key name (zero-terminated string)
+ *  @param[out] destptr  Pointer to a location to hold the address of the
+ *                       returned data buffer
+ *
+ *  @retval ::SYSPARAM_OK           Value successfully retrieved.
+ *  @retval ::SYSPARAM_NOTFOUND     Key/value not found.
+ *  @retval ::SYSPARAM_PARSEFAILED  The retrieved value was a binary value
+ *  @retval ::SYSPARAM_ERR_NOINIT   sysparam_init() must be called first
+ *  @retval ::SYSPARAM_ERR_NOMEM    Unable to allocate memory
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_get_string(const char *key, char **destptr);
+
+/** Get the int32_t value associated with a key
+ * 
+ *  This routine can be used if you know that the value in a key will (or at
+ *  least should) be an integer value.  It will parse the stored data as a
+ *  number (in standard decimal or "0x" hex notation) and return the result.
+ *
+ *  Note: If the status result is anything other than ::SYSPARAM_OK, the value
+ *  in `result` is not changed.  This means it is possible to set a default
+ *  value before calling this function which will be left as-is if a sysparam
+ *  value could not be successfully read.
+ *
+ *  @param[in]  key     Key name (zero-terminated string)
+ *  @param[out] result  Pointer to a location to hold returned integer value
+ *
+ *  @retval ::SYSPARAM_OK           Value successfully retrieved.
+ *  @retval ::SYSPARAM_NOTFOUND     Key/value not found.
+ *  @retval ::SYSPARAM_PARSEFAILED  The retrieved value could not be parsed as
+ *                                  an integer.
+ *  @retval ::SYSPARAM_ERR_NOINIT   sysparam_init() must be called first
+ *  @retval ::SYSPARAM_ERR_NOMEM    Unable to allocate memory
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_get_int(const char *key, int32_t *result);
+
+/** Get the boolean value associated with a key
+ * 
+ *  This routine can be used if you know that the value in a key will (or at
+ *  least should) be a boolean setting.  It will read the specified value as a
+ *  text string and attempt to parse it as a boolean value.
+ *
+ *  It will recognize the following (case-insensitive) strings:
+ *    * True: "yes", "y", "true", "t", "1"
+ *    * False: "no", "n", "false", "f", "0"
+ *
+ *  Note: If the status result is anything other than ::SYSPARAM_OK, the value
+ *  in `result` is not changed.  This means it is possible to set a default
+ *  value before calling this function which will be left as-is if a sysparam
+ *  value could not be successfully read.
+ *
+ *  @param[in]  key     Key name (zero-terminated string)
+ *  @param[out] result  Pointer to a location to hold returned boolean value
+ *
+ *  @retval ::SYSPARAM_OK           Value successfully retrieved.
+ *  @retval ::SYSPARAM_NOTFOUND     Key/value not found.
+ *  @retval ::SYSPARAM_PARSEFAILED  The retrieved value could not be parsed as a
+ *                                  boolean setting.
+ *  @retval ::SYSPARAM_ERR_NOINIT   sysparam_init() must be called first
+ *  @retval ::SYSPARAM_ERR_NOMEM    Unable to allocate memory
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_get_bool(const char *key, bool *result);
+
+/** Set the value associated with a key
+ *
+ *  The supplied value can be any data, up to 255 bytes in length.  If `value`
+ *  is NULL or `value_len` is 0, this is treated as a request to delete any
+ *  current entry matching `key`.
+ *
+ *  If `binary` is true, the data will be considered binary (unprintable) data,
+ *  and this will be annotated in the saved entry.  This does not affect the
+ *  saving or loading process in any way, but may be used by some applications
+ *  to (for example) print binary data differently than text entries when
+ *  printing parameter values.
+ *
+ *  @param[in] key        Key name (zero-terminated string)
+ *  @param[in] value      Pointer to a buffer containing the value data
+ *  @param[in] value_len  Length of the data in the buffer
+ *  @param[in] binary     Whether the data should be considered "binary"
+ *                        (unprintable) data
+ *
+ *  @retval ::SYSPARAM_OK           Value successfully set.
+ *  @retval ::SYSPARAM_ERR_NOINIT   sysparam_init() must be called first
+ *  @retval ::SYSPARAM_ERR_BADVALUE Either an empty key was provided or
+ *                                  value_len is too large
+ *  @retval ::SYSPARAM_ERR_FULL     No space left in sysparam area
+ *                                  (or too many keys in use)
+ *  @retval ::SYSPARAM_ERR_NOMEM    Unable to allocate memory
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_set_data(const char *key, const uint8_t *value, size_t value_len, bool binary);
+
+/** Set a key's value from a string
+ *
+ *  Performs the same function as sysparam_set_data(), but accepts a
+ *  zero-terminated string value instead.
+ *
+ *  @param[in] key        Key name (zero-terminated string)
+ *  @param[in] value      Value to set (zero-terminated string)
+ *
+ *  @retval ::SYSPARAM_OK           Value successfully set.
+ *  @retval ::SYSPARAM_ERR_BADVALUE Either an empty key was provided or the
+ *                                  length of `value` is too large
+ *  @retval ::SYSPARAM_ERR_FULL     No space left in sysparam area
+ *                                  (or too many keys in use)
+ *  @retval ::SYSPARAM_ERR_NOMEM    Unable to allocate memory
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_set_string(const char *key, const char *value);
+
+/** Set a key's value as a number
+ *
+ *  Converts an int32_t value to a decimal number and writes it to the
+ *  specified key.  This does the inverse of the sysparam_get_int()
+ *  function.
+ *
+ *  @param[in] key        Key name (zero-terminated string)
+ *  @param[in] value      Value to set
+ *
+ *  @retval ::SYSPARAM_OK           Value successfully set.
+ *  @retval ::SYSPARAM_ERR_BADVALUE An empty key was provided.
+ *  @retval ::SYSPARAM_ERR_FULL     No space left in sysparam area
+ *                                  (or too many keys in use)
+ *  @retval ::SYSPARAM_ERR_NOMEM    Unable to allocate memory
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_set_int(const char *key, int32_t value);
+
+/** Set a key's value as a boolean (yes/no) string
+ *
+ *  Converts a bool value to a corresponding text string and writes it to the
+ *  specified key.  This does the inverse of the sysparam_get_bool()
+ *  function.
+ *
+ *  Note that if the key already contains a value which parses to the same
+ *  boolean (true/false) value, it is left unchanged.
+ *
+ *  @param[in] key        Key name (zero-terminated string)
+ *  @param[in] value      Value to set
+ *
+ *  @retval ::SYSPARAM_OK           Value successfully set.
+ *  @retval ::SYSPARAM_ERR_BADVALUE An empty key was provided.
+ *  @retval ::SYSPARAM_ERR_FULL     No space left in sysparam area
+ *                                  (or too many keys in use)
+ *  @retval ::SYSPARAM_ERR_NOMEM    Unable to allocate memory
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_set_bool(const char *key, bool value);
+
+/** Begin iterating through all key/value pairs
+ *
+ *  This function initializes a sysparam_iter_t structure to prepare it for
+ *  iterating through the list of key/value pairs using sysparam_iter_next().
+ *  This does not fetch any items (the first successive call to
+ *  sysparam_iter_next() will return the first key/value in the list).
+ *
+ *  NOTE: When done, you must call sysparam_iter_end() to free the resources
+ *  associated with `iter`, or you will leak memory.
+ *
+ *  @param[in] iter  A pointer to a sysparam_iter_t structure to initialize
+ *
+ *  @retval ::SYSPARAM_OK           Initialization successful
+ *  @retval ::SYSPARAM_ERR_NOMEM    Unable to allocate memory
+ */
+sysparam_status_t sysparam_iter_start(sysparam_iter_t *iter);
+
+/** Fetch the next key/value pair
+ *
+ *  This will retrieve the next key and value from the sysparam area, placing
+ *  them in `iter->key`, and `iter->value` (and updating `iter->key_len` and
+ *  `iter->value_len`).
+ *
+ *  NOTE: `iter->key` and `iter->value` are static buffers local to the `iter`
+ *  structure, and will be overwritten with the next call to
+ *  sysparam_iter_next() using the same `iter`.  They should *not* be free()d
+ *  after use.
+ *
+ *  @param[in] iter  The iterator structure to update
+ *
+ *  @retval ::SYSPARAM_OK           Next key/value retrieved
+ *  @retval ::SYSPARAM_ERR_NOMEM    Unable to allocate memory
+ *  @retval ::SYSPARAM_ERR_CORRUPT  Sysparam region has bad/corrupted data
+ *  @retval ::SYSPARAM_ERR_IO       I/O error reading/writing flash
+ */
+sysparam_status_t sysparam_iter_next(sysparam_iter_t *iter);
+
+/** Finish iterating through keys/values
+ *
+ *  Cleans up and releases resources allocated by sysparam_iter_start() /
+ *  sysparam_iter_next().
+ */
+void sysparam_iter_end(sysparam_iter_t *iter);
+
+#endif /* _SYSPARAM_H_ */
diff --git a/core/sysparam.c b/core/sysparam.c
new file mode 100644
index 0000000..cb08ac3
--- /dev/null
+++ b/core/sysparam.c
@@ -0,0 +1,1050 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <sysparam.h>
+#include <espressif/spi_flash.h>
+#include <common_macros.h>
+
+//TODO: make this properly threadsafe
+//TODO: reduce stack usage
+
+/* The "magic" value that indicates the start of a sysparam region in flash.
+ */
+#define SYSPARAM_MAGIC 0x70524f45 // "EORp" in little-endian
+
+/* The size of the initial buffer created by sysparam_iter_start, etc, to hold
+ * returned key-value pairs.  Setting this too small may result in a lot of
+ * unnecessary reallocs.  Setting it too large will waste memory when iterating
+ * through entries.
+ */
+#define DEFAULT_ITER_BUF_SIZE 64
+
+/* The size of the buffer (in words) used by `sysparam_create_area` when
+ * scanning a potential area to make sure it's currently empty.  Note that this
+ * space is taken from the stack, so it should not be too large.
+ */
+#define SCAN_BUFFER_SIZE 8 // words
+
+/* The size of the temporary buffer used for reading back and verifying data
+ * written to flash.  Making this larger will make the write-and-verify
+ * operation slightly faster, but will use more heap during writes
+ */
+#define VERIFY_BUF_SIZE 64
+
+/* Size of region/entry headers.  These should not normally need tweaking (and
+ * will probably require some code changes if they are tweaked).
+ */
+#define REGION_HEADER_SIZE 8 // NOTE: Must be multiple of 4
+#define ENTRY_HEADER_SIZE 4  // NOTE: Must be multiple of 4
+
+/* These are limited by the format to 0xffff, but could be set lower if desired
+ */
+#define MAX_KEY_LEN   0xffff
+#define MAX_VALUE_LEN 0xffff
+
+/* Maximum value that can be used for a key_id.  This is limited by the format
+ * to 0xffe (0xfff indicates end/unwritten space)
+ */
+#define MAX_KEY_ID 0x0ffe
+
+#define REGION_FLAG_SECOND  0x8000 // First (0) or second (1) region
+#define REGION_FLAG_ACTIVE  0x4000 // Stale (0) or active (1) region
+#define REGION_MASK_SIZE    0x0fff // Region size in sectors
+
+#define ENTRY_FLAG_ALIVE    0x8000 // Deleted (0) or active (1)
+#define ENTRY_FLAG_INVALID  0x4000 // Valid (0) or invalid (1) entry
+#define ENTRY_FLAG_VALUE    0x2000 // Key (0) or value (1)
+#define ENTRY_FLAG_BINARY   0x1000 // Text (0) or binary (1) data
+
+#define ENTRY_MASK_ID  0xfff
+
+#define ENTRY_ID_END   0xfff
+#define ENTRY_ID_ANY  0x1000
+
+#ifndef SYSPARAM_DEBUG
+#define SYSPARAM_DEBUG 0
+#endif
+
+/******************************* Useful Macros *******************************/
+
+#define ROUND_TO_WORD_BOUNDARY(x) (((x) + 3) & 0xfffffffc)
+#define ENTRY_SIZE(payload_len) (ENTRY_HEADER_SIZE + ROUND_TO_WORD_BOUNDARY(payload_len))
+
+#define max(x, y) ((x) > (y) ? (x) : (y))
+#define min(x, y) ((x) < (y) ? (x) : (y))
+
+#define debug(level, format, ...) if (SYSPARAM_DEBUG >= (level)) { printf("%s" format "\n", "sysparam: ", ## __VA_ARGS__); }
+
+#define CHECK_FLASH_OP(x) do { int __x = (x); if ((__x) != SPI_FLASH_RESULT_OK) { debug(1, "FLASH ERR: %d", __x); return SYSPARAM_ERR_IO; } } while (0);
+
+/********************* Internal datatypes and structures *********************/
+
+struct region_header {
+    uint32_t magic;
+    uint16_t flags_size;
+    uint16_t reserved;
+} __attribute__ ((packed));
+
+struct entry_header {
+    uint16_t idflags;
+    uint16_t len;
+} __attribute__ ((packed));
+
+struct sysparam_context {
+    uint32_t addr;
+    struct entry_header entry;
+    int unused_keys;
+    size_t compactable;
+    uint16_t max_key_id;
+};
+
+/*************************** Global variables/data ***************************/
+
+static struct {
+    uint32_t cur_base;
+    uint32_t alt_base;
+    uint32_t end_addr;
+    size_t region_size;
+    bool force_compact;
+} _sysparam_info;
+
+/***************************** Internal routines *****************************/
+
+static inline IRAM sysparam_status_t _do_write(uint32_t addr, const void *data, size_t data_size) {
+    CHECK_FLASH_OP(sdk_spi_flash_write(addr, data, data_size));
+    return SYSPARAM_OK;
+}
+
+static inline IRAM sysparam_status_t _do_verify(uint32_t addr, const void *data, void *buffer, size_t len) {
+    CHECK_FLASH_OP(sdk_spi_flash_read(addr, buffer, len));
+    if (memcmp(data, buffer, len)) {
+        return SYSPARAM_ERR_IO;
+    }
+    return SYSPARAM_OK;
+}
+
+/*FIXME: Eventually, this should probably be implemented down at the SPI flash library layer, where it can just compare bytes/words straight from the SPI hardware buffer instead of allocating a whole separate temp buffer, reading chunks into that, and then doing a memcmp.. */
+static IRAM sysparam_status_t _write_and_verify(uint32_t addr, const void *data, size_t data_size) {
+    int i;
+    size_t count;
+    sysparam_status_t status = SYSPARAM_OK;
+    uint8_t *verify_buf = malloc(VERIFY_BUF_SIZE);
+
+    if (!verify_buf) return SYSPARAM_ERR_NOMEM;
+    do {
+        status = _do_write(addr, data, data_size);
+        if (status != SYSPARAM_OK) break;
+        for (i = 0; i < data_size; i += VERIFY_BUF_SIZE) {
+            count = min(data_size - i, VERIFY_BUF_SIZE);
+            status = _do_verify(addr + i, data + i, verify_buf, count);
+            if (status != SYSPARAM_OK) {
+                debug(1, "Flash write (@ 0x%08x) verify failed!", addr);
+                break;
+            }
+        }
+    } while (false);
+    free(verify_buf);
+    return status;
+}
+
+/** Erase the sectors of a region */
+static sysparam_status_t _format_region(uint32_t addr, uint16_t num_sectors) {
+    uint16_t sector = addr / sdk_flashchip.sector_size;
+    int i;
+
+    for (i = 0; i < num_sectors; i++) {
+        CHECK_FLASH_OP(sdk_spi_flash_erase_sector(sector + i));
+    }
+    return SYSPARAM_OK;
+}
+
+/** Write the magic data at the beginning of a region */
+static inline sysparam_status_t _write_region_header(uint32_t addr, uint32_t other, bool active) {
+    struct region_header header;
+    sysparam_status_t status;
+    int16_t num_sectors;
+
+    header.magic = SYSPARAM_MAGIC;
+    if (addr < other) {
+        num_sectors = (other - addr) / sdk_flashchip.sector_size;
+        header.flags_size = num_sectors & REGION_MASK_SIZE;
+    } else {
+        num_sectors = (addr - other) / sdk_flashchip.sector_size;
+        header.flags_size = num_sectors & REGION_MASK_SIZE;
+        header.flags_size |= REGION_FLAG_SECOND;
+    }
+    if (active) {
+        header.flags_size |= REGION_FLAG_ACTIVE;
+    }
+    header.reserved = 0;
+
+    debug(3, "write region header (0x%04x) @ 0x%08x", header.flags_size, addr);
+    status = _write_and_verify(addr, &header, REGION_HEADER_SIZE);
+    if (status != SYSPARAM_OK) {
+        // Uh oh.. Something failed, so we don't know whether what we wrote is
+        // actually in the flash or not.  Try to zero it out to be sure and
+        // return an error.
+        debug(3, "zero region header @ 0x%08x", addr);
+        memset(&header, 0, REGION_HEADER_SIZE);
+        _write_and_verify(addr, &header, REGION_HEADER_SIZE);
+        return SYSPARAM_ERR_IO;
+    }
+    return SYSPARAM_OK;
+}
+
+/** Initialize a context structure at the beginning of the active region */
+static void _init_context(struct sysparam_context *ctx) {
+    memset(ctx, 0, sizeof(*ctx));
+    ctx->addr = _sysparam_info.cur_base;
+}
+
+/** Initialize a context structure at the end of the active region */
+static sysparam_status_t init_write_context(struct sysparam_context *ctx) {
+    memset(ctx, 0, sizeof(*ctx));
+    ctx->addr = _sysparam_info.end_addr;
+    debug(3, "read entry header @ 0x%08x", ctx->addr);
+    CHECK_FLASH_OP(sdk_spi_flash_read(ctx->addr, &ctx->entry, ENTRY_HEADER_SIZE));
+    return SYSPARAM_OK;
+}
+
+/** Search through the region for an entry matching the specified id
+ *
+ *  @param match_id  The id to match, or 0 to match any key, or 0xfff to scan
+ *                   to the end.
+ */
+static sysparam_status_t _find_entry(struct sysparam_context *ctx, uint16_t match_id, bool find_value) {
+    uint16_t id;
+
+    while (true) {
+        if (ctx->addr == _sysparam_info.cur_base) {
+            ctx->addr += REGION_HEADER_SIZE;
+        } else {
+            uint32_t next_addr = ctx->addr + ENTRY_SIZE(ctx->entry.len);
+            if (next_addr > _sysparam_info.cur_base + _sysparam_info.region_size) {
+                // This entry has an obviously impossible length, so we need to
+                // stop reading here.
+                // We can report this as the end of the valid entries, but then
+                // any future writes (to the end) will write over
+                // previously-written data and result in garbage.  The best
+                // workaround is to make sure that the next write operation
+                // will always start with a compaction, which will leave off
+                // the invalid data at the end and fix the issue going forward.
+                debug(1, "Encountered entry with invalid length (0x%04x) @ 0x%08x (region end is 0x%08x).  Truncating entries.", ctx->entry.len, ctx->addr, _sysparam_info.end_addr);
+                _sysparam_info.force_compact = true;
+                break;
+            }
+            ctx->addr = next_addr;
+            if (ctx->addr == _sysparam_info.cur_base + _sysparam_info.region_size) {
+                // This is the last entry in the available space, but it
+                // exactly fits.  Stop reading here.
+                break;
+            }
+        }
+
+        debug(3, "read entry header @ 0x%08x", ctx->addr);
+        CHECK_FLASH_OP(sdk_spi_flash_read(ctx->addr, &ctx->entry, ENTRY_HEADER_SIZE));
+        debug(3, "  idflags = 0x%04x", ctx->entry.idflags);
+        if (ctx->entry.idflags == 0xffff) {
+            // 0xffff is never a valid id field, so this means we've hit the
+            // end and are looking at unwritten flash space from here on.
+            break;
+        }
+
+        id = ctx->entry.idflags & ENTRY_MASK_ID;
+        if ((ctx->entry.idflags & (ENTRY_FLAG_ALIVE | ENTRY_FLAG_INVALID)) == ENTRY_FLAG_ALIVE) {
+            debug(3, "  entry is alive and valid");
+            if (!(ctx->entry.idflags & ENTRY_FLAG_VALUE)) {
+                debug(3, "  entry is a key");
+                ctx->max_key_id = id;
+                ctx->unused_keys++;
+                if (!find_value) {
+                    if ((id == match_id) || (match_id == ENTRY_ID_ANY)) {
+                        return SYSPARAM_OK;
+                    }
+                }
+            } else {
+                debug(3, "  entry is a value");
+                ctx->unused_keys--;
+                if (find_value) {
+                    if ((id == match_id) || (match_id == ENTRY_ID_ANY)) {
+                        return SYSPARAM_OK;
+                    }
+                }
+            }
+            debug(3, "  (not a match)");
+        } else {
+            debug(3, "  entry is deleted or invalid");
+            ctx->compactable += ENTRY_SIZE(ctx->entry.len);
+        }
+    }
+    if (match_id == ENTRY_ID_END) {
+        return SYSPARAM_OK;
+    }
+    ctx->entry.len = 0;
+    ctx->entry.idflags = 0;
+    return SYSPARAM_NOTFOUND;
+}
+
+/** Read the payload from the current entry pointed to by `ctx` */
+static inline sysparam_status_t _read_payload(struct sysparam_context *ctx, uint8_t *buffer, size_t buffer_size) {
+    debug(3, "read payload (%d) @ 0x%08x", min(buffer_size, ctx->entry.len), ctx->addr);
+    CHECK_FLASH_OP(sdk_spi_flash_read(ctx->addr + ENTRY_HEADER_SIZE, buffer, min(buffer_size, ctx->entry.len)));
+    return SYSPARAM_OK;
+}
+
+/** Find the entry corresponding to the specified key name */
+static sysparam_status_t _find_key(struct sysparam_context *ctx, const char *key, uint16_t key_len, uint8_t *buffer) {
+    sysparam_status_t status;
+
+    debug(3, "find key: %s", key ? key : "(null)");
+    while (true) {
+        // Find the next key entry
+        status = _find_entry(ctx, ENTRY_ID_ANY, false);
+        if (status != SYSPARAM_OK) return status;
+        debug(3, "found a key entry @ 0x%08x", ctx->addr);
+        if (!key) {
+            // We're looking for the next (any) key, so we're done.
+            break;
+        }
+        if (ctx->entry.len == key_len) {
+            status = _read_payload(ctx, buffer, key_len);
+            if (status < 0) return status;
+            if (!memcmp(key, buffer, key_len)) {
+                // We have a match
+                break;
+            }
+            debug(3, "entry payload does not match");
+        } else {
+            debug(3, "key length (%d) does not match (%d)", ctx->entry.len, key_len);
+        }
+    }
+    debug(3, "key match @ 0x%08x (idflags = 0x%04x)", ctx->addr, ctx->entry.idflags);
+
+    return SYSPARAM_OK;
+}
+
+/** Find the value entry matching the id field from a particular key */
+static inline sysparam_status_t _find_value(struct sysparam_context *ctx, uint16_t id_field) {
+    debug(3, "find value: 0x%04x", id_field);
+    return _find_entry(ctx, id_field & ENTRY_MASK_ID, true);
+}
+
+/** Write an entry at the specified address */
+static inline sysparam_status_t _write_entry(uint32_t addr, uint16_t id, const uint8_t *payload, uint16_t len) {
+    struct entry_header entry;
+    sysparam_status_t status;
+
+    debug(2, "Writing entry 0x%02x @ 0x%08x", id, addr);
+    entry.idflags = id | ENTRY_FLAG_ALIVE | ENTRY_FLAG_INVALID;
+    entry.len = len;
+    debug(3, "write initial entry header @ 0x%08x", addr);
+    status = _write_and_verify(addr, &entry, ENTRY_HEADER_SIZE);
+    if (status == SYSPARAM_ERR_IO) {
+        // Uh-oh.. Either the flash call failed in some way or we didn't get
+        // back what we wrote.  This could be a problem because depending on
+        // how it went wrong it could screw up all reads/writes from this point
+        // forward.  Try to salvage the on-flash structure by overwriting the
+        // failed header with all zeros, which (if successful) will be
+        // interpreted on later reads as a deleted empty-payload entry (and it
+        // will just skip to the next spot).
+        memset(&entry, 0, ENTRY_HEADER_SIZE);
+        debug(3, "zeroing entry header @ 0x%08x", addr);
+        status = _write_and_verify(addr, &entry, ENTRY_HEADER_SIZE);
+        if (status != SYSPARAM_OK) return status;
+
+        // Make sure future writes skip past this zeroed bit
+        if (_sysparam_info.end_addr == addr) {
+            _sysparam_info.end_addr += ENTRY_HEADER_SIZE;
+        }
+        // We could just skip to the next space and try again, but
+        // unfortunately now we can't be sure there's enough space remaining to
+        // fit the entry, so we just have to fail this operation.  Hopefully,
+        // at least, future requests will still succeed, though.
+        status = SYSPARAM_ERR_IO;
+    }
+    if (status != SYSPARAM_OK) return status;
+
+    // If we've gotten this far, we've committed to writing the full entry.
+    if (_sysparam_info.end_addr == addr) {
+        _sysparam_info.end_addr += ENTRY_SIZE(len);
+    }
+    debug(3, "write payload (%d) @ 0x%08x", len, addr + ENTRY_HEADER_SIZE);
+    status = _write_and_verify(addr + ENTRY_HEADER_SIZE, payload, len);
+    if (status != SYSPARAM_OK) return status;
+
+    debug(3, "set entry valid @ 0x%08x", addr);
+    entry.idflags &= ~ENTRY_FLAG_INVALID;
+    status = _write_and_verify(addr, &entry, ENTRY_HEADER_SIZE);
+
+    return status;
+}
+
+/** Mark an entry as "deleted" so it won't be considered in future reads */
+static inline sysparam_status_t _delete_entry(uint32_t addr) {
+    struct entry_header entry;
+
+    debug(2, "Deleting entry @ 0x%08x", addr);
+    debug(3, "read entry header @ 0x%08x", addr);
+    CHECK_FLASH_OP(sdk_spi_flash_read(addr, &entry, ENTRY_HEADER_SIZE));
+    // Set the ID to zero to mark it as "deleted"
+    entry.idflags &= ~ENTRY_FLAG_ALIVE;
+    debug(3, "write entry header @ 0x%08x", addr);
+    CHECK_FLASH_OP(sdk_spi_flash_write(addr, &entry, ENTRY_HEADER_SIZE));
+
+    return SYSPARAM_OK;
+}
+
+/** Compact the current region, removing all deleted/unused entries, and write
+ *  the result to the alternate region, then make the new alternate region the
+ *  active one.
+ *
+ *  @param key_id  A pointer to the "current" key ID.
+ *
+ *  NOTE: The value corresponding to the passed key ID will not be written to
+ *  the output (because it is assumed it will be overwritten as the next step
+ *  in `sysparam_set_data` anyway).  When compacting, this routine will
+ *  automatically update *key_id to contain the ID of this key in the new
+ *  compacted result as well.
+ */
+static sysparam_status_t _compact_params(struct sysparam_context *ctx, int *key_id) {
+    uint32_t new_base = _sysparam_info.alt_base;
+    sysparam_status_t status;
+    uint32_t addr = new_base + REGION_HEADER_SIZE;
+    uint16_t current_key_id = 0;
+    sysparam_iter_t iter;
+    uint16_t binary_flag;
+    uint16_t num_sectors = _sysparam_info.region_size / sdk_flashchip.sector_size;
+
+    debug(1, "compacting region (current size %d, expect to recover %d%s bytes)...", _sysparam_info.end_addr - _sysparam_info.cur_base, ctx->compactable, (ctx->unused_keys > 0) ? "+ (unused keys present)" : "");
+    status = _format_region(new_base, num_sectors);
+    if (status < 0) return status;
+    status = sysparam_iter_start(&iter);
+    if (status < 0) return status;
+
+    while (true) {
+        status = sysparam_iter_next(&iter);
+        if (status != SYSPARAM_OK) break;
+
+        current_key_id++;
+
+        // Write the key to the new region
+        debug(2, "writing %d key @ 0x%08x", current_key_id, addr);
+        status = _write_entry(addr, current_key_id, (uint8_t *)iter.key, iter.key_len);
+        if (status < 0) break;
+        addr += ENTRY_SIZE(iter.key_len);
+
+        if ((iter.ctx->entry.idflags & ENTRY_MASK_ID) == *key_id) {
+            // Update key_id to have the correct id for the compacted result
+            *key_id = current_key_id;
+            // Don't copy the old value, since we'll just be deleting it
+            // and writing a new one as soon as we return.
+            continue;
+        }
+
+        // Copy the value to the new region
+        debug(2, "writing %d value @ 0x%08x", current_key_id, addr);
+        binary_flag = iter.binary ? ENTRY_FLAG_BINARY : 0;
+        status = _write_entry(addr, current_key_id | ENTRY_FLAG_VALUE | binary_flag, iter.value, iter.value_len);
+        if (status < 0) break;
+        addr += ENTRY_SIZE(iter.value_len);
+    }
+    sysparam_iter_end(&iter);
+
+    // If we broke out with an error, return the error instead of continuing.
+    if (status < 0) {
+        debug(1, "error encountered during compacting (%d)", status);
+        return status;
+    }
+
+    // Switch to officially using the new region.
+    status = _write_region_header(new_base, _sysparam_info.cur_base, true);
+    if (status < 0) return status;
+    status = _write_region_header(_sysparam_info.cur_base, new_base, false);
+    if (status < 0) return status;
+
+    _sysparam_info.alt_base = _sysparam_info.cur_base;
+    _sysparam_info.cur_base = new_base;
+    _sysparam_info.end_addr = addr;
+    _sysparam_info.force_compact = false;
+
+    // Fix up ctx so it doesn't point to invalid stuff
+    memset(ctx, 0, sizeof(*ctx));
+    ctx->addr = addr;
+    ctx->max_key_id = current_key_id;
+
+    debug(1, "done compacting (current size %d)", _sysparam_info.end_addr - _sysparam_info.cur_base);
+
+    return SYSPARAM_OK;
+}
+
+/***************************** Public Functions ******************************/
+
+sysparam_status_t sysparam_init(uint32_t base_addr, uint32_t top_addr) {
+    sysparam_status_t status;
+    uint32_t addr0, addr1;
+    struct region_header header0, header1;
+    struct sysparam_context ctx;
+    uint16_t num_sectors;
+
+    // Make sure we're starting at the beginning of the sector
+    base_addr -= (base_addr % sdk_flashchip.sector_size);
+
+    if (!top_addr || top_addr == base_addr) {
+        // Only scan the specified sector, nowhere else.
+        top_addr = base_addr + sdk_flashchip.sector_size;
+    }
+    for (addr0 = base_addr; addr0 < top_addr; addr0 += sdk_flashchip.sector_size) {
+        CHECK_FLASH_OP(sdk_spi_flash_read(addr0, &header0, REGION_HEADER_SIZE));
+        if (header0.magic == SYSPARAM_MAGIC) {
+            // Found a starting point...
+            break;
+        }
+    }
+    if (addr0 >= top_addr) {
+        return SYSPARAM_NOTFOUND;
+    }
+
+    // We've found a valid header at addr0.  Now find the other half of the sysparam area.
+    num_sectors = header0.flags_size & REGION_MASK_SIZE;
+
+    if (header0.flags_size & REGION_FLAG_SECOND) {
+        addr1 = addr0 - num_sectors * sdk_flashchip.sector_size;
+    } else {
+        addr1 = addr0 + num_sectors * sdk_flashchip.sector_size;
+    }
+    CHECK_FLASH_OP(sdk_spi_flash_read(addr1, &header1, REGION_HEADER_SIZE));
+
+    if (header1.magic == SYSPARAM_MAGIC) {
+        // Yay! Found the other one.  Sanity-check it..
+        if ((header0.flags_size & REGION_FLAG_SECOND) == (header1.flags_size & REGION_FLAG_SECOND)) {
+            // Hmm.. they both say they're the same region.  That can't be right...
+            debug(1, "Found region headers @ 0x%08x and 0x%08x, but both claim to be the same region.", addr0, addr1);
+            return SYSPARAM_ERR_CORRUPT;
+        }
+    } else {
+        // Didn't find a valid header at the alternate location (which probably means something clobbered it or something went wrong at a critical point when rewriting it.  Is the one we did find the active or stale one?
+        if (header0.flags_size & REGION_FLAG_ACTIVE) {
+            // Found the active one.  We can work with this.  Try to recreate the missing stale region...
+            debug(2, "Found active region header @ 0x%08x but no stale region @ 0x%08x. Trying to recreate stale region.", addr0, addr1);
+            status = _format_region(addr1, num_sectors);
+            if (status != SYSPARAM_OK) return status;
+            status = _write_region_header(addr1, addr0, false);
+            if (status != SYSPARAM_OK) return status;
+        } else {
+            // Found the stale one.  We have no idea how old it is, so we shouldn't use it without some sort of confirmation/recovery.  We'll have to bail for now.
+            debug(1, "Found stale-region header @ 0x%08x, but no active region.", addr0);
+            return SYSPARAM_ERR_CORRUPT;
+        }
+    }
+    // At this point we have confirmed valid regions at addr0 and addr1.
+
+    _sysparam_info.region_size = num_sectors * sdk_flashchip.sector_size;
+    if (header0.flags_size & REGION_FLAG_ACTIVE) {
+        _sysparam_info.cur_base = addr0;
+        _sysparam_info.alt_base = addr1;
+        debug(3, "Active region @ 0x%08x (0x%04x).  Stale region @ 0x%08x (0x%04x).", addr0, header0.flags_size, addr1, header1.flags_size);
+
+    } else {
+        _sysparam_info.cur_base = addr1;
+        _sysparam_info.alt_base = addr0;
+        debug(3, "Active region @ 0x%08x (0x%04x).  Stale region @ 0x%08x (0x%04x).", addr1, header1.flags_size, addr0, header0.flags_size);
+    }
+
+    // Find the actual end
+    _sysparam_info.end_addr = _sysparam_info.cur_base + _sysparam_info.region_size;
+    _sysparam_info.force_compact = false;
+    _init_context(&ctx);
+    status = _find_entry(&ctx, ENTRY_ID_END, false);
+    if (status < 0) {
+        _sysparam_info.cur_base = 0;
+        _sysparam_info.alt_base = 0;
+        _sysparam_info.end_addr = 0;
+        return status;
+    }
+    if (status == SYSPARAM_OK) {
+        _sysparam_info.end_addr = ctx.addr;
+    }
+
+    return SYSPARAM_OK;
+}
+
+sysparam_status_t sysparam_create_area(uint32_t base_addr, uint16_t num_sectors, bool force) {
+    size_t region_size;
+    sysparam_status_t status;
+    uint32_t buffer[SCAN_BUFFER_SIZE];
+    uint32_t addr;
+    int i;
+
+    // Convert "number of sectors for area" into "number of sectors per region"
+    if (num_sectors < 1 || (num_sectors & 1)) {
+        return SYSPARAM_ERR_BADVALUE;
+    }
+    num_sectors >>= 1;
+    region_size = num_sectors * sdk_flashchip.sector_size;
+
+    if (!force) {
+        // First, scan through the area and make sure it's actually empty and
+        // we're not going to be clobbering something else important.
+        for (addr = base_addr; addr < base_addr + region_size * 2; addr += SCAN_BUFFER_SIZE) {
+            debug(3, "read %d words @ 0x%08x", SCAN_BUFFER_SIZE, addr);
+            CHECK_FLASH_OP(sdk_spi_flash_read(addr, buffer, SCAN_BUFFER_SIZE * 4));
+            for (i = 0; i < SCAN_BUFFER_SIZE; i++) {
+                if (buffer[i] != 0xffffffff) {
+                    // Uh oh, not empty.
+                    return SYSPARAM_NOTFOUND;
+                }
+            }
+        }
+    }
+
+    if (_sysparam_info.cur_base == base_addr || _sysparam_info.alt_base == base_addr) {
+        // We're reformating the same region we're already using.
+        // De-initialize everything to force the caller to do a clean
+        // `sysparam_init()` afterwards.
+        memset(&_sysparam_info, 0, sizeof(_sysparam_info));
+    }
+    status = _format_region(base_addr, num_sectors);
+    if (status < 0) return status;
+    status = _format_region(base_addr + region_size, num_sectors);
+    if (status < 0) return status;
+    status = _write_region_header(base_addr, base_addr + region_size, true);
+    if (status < 0) return status;
+    status = _write_region_header(base_addr + region_size, base_addr, false);
+    if (status < 0) return status;
+
+    return SYSPARAM_OK;
+}
+
+sysparam_status_t sysparam_get_info(uint32_t *base_addr, uint32_t *num_sectors) {
+    if (!_sysparam_info.cur_base) return SYSPARAM_ERR_NOINIT;
+
+    *base_addr = min(_sysparam_info.cur_base, _sysparam_info.alt_base);
+    *num_sectors = (_sysparam_info.region_size / sdk_flashchip.sector_size) * 2;
+    return SYSPARAM_OK;
+}
+
+sysparam_status_t sysparam_get_data(const char *key, uint8_t **destptr, size_t *actual_length, bool *is_binary) {
+    struct sysparam_context ctx;
+    sysparam_status_t status;
+    size_t key_len = strlen(key);
+    uint8_t *buffer;
+    uint8_t *newbuf;
+   
+    if (!_sysparam_info.cur_base) return SYSPARAM_ERR_NOINIT;
+
+    buffer = malloc(key_len + 2);
+    if (!buffer) return SYSPARAM_ERR_NOMEM;
+    do {
+        _init_context(&ctx);
+        status = _find_key(&ctx, key, key_len, buffer);
+        if (status != SYSPARAM_OK) break;
+
+        // Find the associated value
+        status = _find_value(&ctx, ctx.entry.idflags);
+        if (status != SYSPARAM_OK) break;
+
+        newbuf = realloc(buffer, ctx.entry.len + 1);
+        if (!newbuf) {
+            status = SYSPARAM_ERR_NOMEM;
+            break;
+        }
+        buffer = newbuf;
+        status = _read_payload(&ctx, buffer, ctx.entry.len);
+        if (status != SYSPARAM_OK) break;
+
+        // Zero-terminate the result, just in case (doesn't hurt anything for
+        // non-string data, and can avoid nasty mistakes if the caller wants to
+        // interpret the result as a string).
+        buffer[ctx.entry.len] = 0;
+
+        *destptr = buffer;
+        if (actual_length) *actual_length = ctx.entry.len;
+        if (is_binary) *is_binary = (bool)(ctx.entry.idflags & ENTRY_FLAG_BINARY);
+        return SYSPARAM_OK;
+    } while (false);
+
+    free(buffer);
+    if (actual_length) *actual_length = 0;
+    return status;
+}
+
+sysparam_status_t sysparam_get_data_static(const char *key, uint8_t *buffer, size_t buffer_size, size_t *actual_length, bool *is_binary) {
+    struct sysparam_context ctx;
+    sysparam_status_t status = SYSPARAM_OK;
+    size_t key_len = strlen(key);
+
+    if (!_sysparam_info.cur_base) return SYSPARAM_ERR_NOINIT;
+
+    // Supplied buffer must be at least as large as the key, or 2 bytes,
+    // whichever is larger.
+    if (buffer_size < max(key_len, 2)) return SYSPARAM_ERR_NOMEM;
+
+    if (actual_length) *actual_length = 0;
+
+    _init_context(&ctx);
+    status = _find_key(&ctx, key, key_len, buffer);
+    if (status != SYSPARAM_OK) return status;
+    status = _find_value(&ctx, ctx.entry.idflags);
+    if (status != SYSPARAM_OK) return status;
+    status = _read_payload(&ctx, buffer, buffer_size);
+    if (status != SYSPARAM_OK) return status;
+
+    if (actual_length) *actual_length = ctx.entry.len;
+    if (is_binary) *is_binary = (bool)(ctx.entry.idflags & ENTRY_FLAG_BINARY);
+    return SYSPARAM_OK;
+}
+
+sysparam_status_t sysparam_get_string(const char *key, char **destptr) {
+    bool is_binary;
+    sysparam_status_t status;
+    uint8_t *buf;
+
+    status = sysparam_get_data(key, &buf, NULL, &is_binary);
+    if (status != SYSPARAM_OK) return status;
+    if (is_binary) {
+        // Value was saved as binary data, which means we shouldn't try to
+        // interpret it as a string.
+        free(buf);
+        return SYSPARAM_PARSEFAILED;
+    }
+    // `sysparam_get_data` will zero-terminate the result as a matter of course,
+    // so no need to do that here.
+    *destptr = (char *)buf;
+    return SYSPARAM_OK;
+}
+
+sysparam_status_t sysparam_get_int(const char *key, int32_t *result) {
+    char *buffer;
+    char *endptr;
+    int32_t value;
+    sysparam_status_t status;
+
+    status = sysparam_get_string(key, &buffer);
+    if (status != SYSPARAM_OK) return status;
+    value = strtol(buffer, &endptr, 0);
+    if (*endptr) {
+        // There was extra crap at the end of the string.
+        free(buffer);
+        return SYSPARAM_PARSEFAILED;
+    }
+
+    *result = value;
+    free(buffer);
+    return SYSPARAM_OK;
+}
+
+sysparam_status_t sysparam_get_bool(const char *key, bool *result) {
+    char *buffer;
+    sysparam_status_t status;
+
+    status = sysparam_get_string(key, &buffer);
+    if (status != SYSPARAM_OK) return status;
+    do {
+        if (!strcasecmp(buffer, "y")    ||
+            !strcasecmp(buffer, "yes")  ||
+            !strcasecmp(buffer, "t")    ||
+            !strcasecmp(buffer, "true") ||
+            !strcmp(buffer, "1")) {
+                *result = true;
+                break;
+        }
+        if (!strcasecmp(buffer, "n")     ||
+            !strcasecmp(buffer, "no")    ||
+            !strcasecmp(buffer, "f")     ||
+            !strcasecmp(buffer, "false") ||
+            !strcmp(buffer, "0")) {
+                *result = false;
+                break;
+        }
+        status = SYSPARAM_PARSEFAILED;
+    } while (0);
+
+    free(buffer);
+    return status;
+}
+
+sysparam_status_t sysparam_set_data(const char *key, const uint8_t *value, size_t value_len, bool is_binary) {
+    struct sysparam_context ctx;
+    struct sysparam_context write_ctx;
+    sysparam_status_t status = SYSPARAM_OK;
+    uint16_t key_len = strlen(key);
+    uint8_t *buffer;
+    uint8_t *newbuf;
+    size_t free_space;
+    size_t needed_space;
+    bool free_value = false;
+    int key_id = -1;
+    uint32_t old_value_addr = 0;
+    uint16_t binary_flag;
+   
+    if (!_sysparam_info.cur_base) return SYSPARAM_ERR_NOINIT;
+    if (!key_len) return SYSPARAM_ERR_BADVALUE;
+    if (key_len > MAX_KEY_LEN) return SYSPARAM_ERR_BADVALUE;
+    if (value_len > MAX_VALUE_LEN) return SYSPARAM_ERR_BADVALUE;
+
+    if (!value) value_len = 0;
+
+    debug(1, "updating value for '%s' (%d bytes)", key, value_len);
+    if (value_len && ((intptr_t)value & 0x3)) {
+        // The passed value isn't word-aligned.  This will be a problem later
+        // when calling `sdk_spi_flash_write`, so make a word-aligned copy.
+        buffer = malloc(value_len);
+        if (!buffer) return SYSPARAM_ERR_NOMEM;
+        memcpy(buffer, value, value_len);
+        value = buffer;
+        free_value = true;
+    }
+    // Create a working buffer for `_find_key` to use.
+    buffer = malloc(key_len);
+    if (!buffer) {
+        if (free_value) free((void *)value);
+        return SYSPARAM_ERR_NOMEM;
+    }
+
+    do {
+        _init_context(&ctx);
+        status = _find_key(&ctx, key, key_len, buffer);
+        if (status == SYSPARAM_OK) {
+            // Key already exists, see if there's a current value.
+            key_id = ctx.entry.idflags & ENTRY_MASK_ID;
+            status = _find_value(&ctx, key_id);
+            if (status == SYSPARAM_OK) {
+                old_value_addr = ctx.addr;
+            }
+        }
+        if (status < 0) break;
+
+        binary_flag = is_binary ? ENTRY_FLAG_BINARY : 0;
+
+        if (value_len) {
+            if (old_value_addr) {
+                if ((ctx.entry.idflags & ENTRY_FLAG_BINARY) == binary_flag && ctx.entry.len == value_len) {
+                    // Are we trying to write the same value that's already there?
+                    if (value_len > key_len) {
+                        newbuf = realloc(buffer, value_len);
+                        if (!newbuf) {
+                            status = SYSPARAM_ERR_NOMEM;
+                            break;
+                        }
+                        buffer = newbuf;
+                    }
+                    status = _read_payload(&ctx, buffer, value_len);
+                    if (status < 0) break;
+                    if (!memcmp(buffer, value, value_len)) {
+                        // Yup, it's a match! No need to do anything further,
+                        // just leave the current value as-is.
+                        status = SYSPARAM_OK;
+                        break;
+                    }
+                }
+
+                // Since we will be deleting the old value (if any) make sure
+                // that the compactable count includes the space taken up by
+                // that entry too (even though it's not actually deleted yet)
+                ctx.compactable += ENTRY_SIZE(ctx.entry.len);
+            }
+
+            // Append new value to the end, but first make sure we have enough
+            // space.
+            free_space = _sysparam_info.cur_base + _sysparam_info.region_size - _sysparam_info.end_addr;
+            needed_space = ENTRY_SIZE(value_len);
+            if (key_id < 0) {
+                // We did not find a previous key entry matching this key.  We
+                // will need to add a key entry as well.
+                key_len = strlen(key);
+                needed_space += ENTRY_SIZE(key_len);
+            }
+            if (needed_space > free_space) {
+                // Can we compact things?
+                // First, scan all remaining entries up to the end so we can
+                // get a reasonably accurate "compactable" reading.
+                _find_entry(&ctx, ENTRY_ID_END, false);
+                if (needed_space <= free_space + ctx.compactable) {
+                    // We should be able to get enough space by compacting.
+                    status = _compact_params(&ctx, &key_id);
+                    if (status < 0) break;
+                    old_value_addr = 0;
+                } else if (ctx.unused_keys > 0) {
+                    // Compacting will gain more space than expected, because
+                    // there are some keys that can be omitted too, but we
+                    // don't know exactly how much that will gain, so all we
+                    // can do is give it a try and see if it gives us enough.
+                    status = _compact_params(&ctx, &key_id);
+                    if (status < 0) break;
+                    old_value_addr = 0;
+                }
+                free_space = _sysparam_info.cur_base + _sysparam_info.region_size - _sysparam_info.end_addr;
+            }
+            if (needed_space > free_space) {
+                // Nothing we can do here.. We're full.
+                // (at least full enough that compacting won't help us store
+                // this value)
+                debug(1, "region full (need %d of %d remaining)", needed_space, free_space);
+                status = SYSPARAM_ERR_FULL;
+                break;
+            }
+
+            if (key_id < 0) {
+                // We need to write a key entry for a new key.
+                // If we didn't find the key, then we already know _find_entry
+                // has gone through the entire contents, and thus
+                // ctx.max_key_id has the largest key_id found in the whole
+                // region.
+                if (ctx.max_key_id >= MAX_KEY_ID) {
+                    if (ctx.unused_keys > 0) {
+                        status = _compact_params(&ctx, &key_id);
+                        if (status < 0) break;
+                        old_value_addr = 0;
+                    } else {
+                        debug(1, "out of ids!");
+                        status = SYSPARAM_ERR_FULL;
+                        break;
+                    }
+                }
+            }
+
+            if (_sysparam_info.force_compact) {
+                // We didn't need to compact above, but due to previously
+                // detected inconsistencies, we should compact anyway before
+                // writing anything new, so do that.
+                status = _compact_params(&ctx, &key_id);
+                if (status < 0) break;
+            }
+
+            init_write_context(&write_ctx);
+
+            if (key_id < 0) {
+                // Write a new key entry
+                key_id = ctx.max_key_id + 1;
+                status = _write_entry(write_ctx.addr, key_id, (uint8_t *)key, key_len);
+                if (status < 0) break;
+                write_ctx.addr += ENTRY_SIZE(key_len);
+            }
+
+            // Write new value
+            status = _write_entry(write_ctx.addr, key_id | ENTRY_FLAG_VALUE | binary_flag, value, value_len);
+            if (status < 0) break;
+            write_ctx.addr += ENTRY_SIZE(value_len);
+            _sysparam_info.end_addr = write_ctx.addr;
+        }
+
+        // Delete old value (if present) by clearing its "alive" flag
+        if (old_value_addr) {
+            status = _delete_entry(old_value_addr);
+            if (status < 0) break;
+        }
+
+        debug(1, "New addr is 0x%08x (%d bytes remaining)", _sysparam_info.end_addr, _sysparam_info.cur_base + _sysparam_info.region_size - _sysparam_info.end_addr);
+    } while (false);
+
+    if (free_value) free((void *)value);
+    free(buffer);
+    return status;
+}
+
+sysparam_status_t sysparam_set_string(const char *key, const char *value) {
+    return sysparam_set_data(key, (const uint8_t *)value, strlen(value), false);
+}
+
+sysparam_status_t sysparam_set_int(const char *key, int32_t value) {
+    uint8_t buffer[12];
+    int len;
+    
+    len = snprintf((char *)buffer, 12, "%d", value);
+    return sysparam_set_data(key, buffer, len, false);
+}
+
+sysparam_status_t sysparam_set_bool(const char *key, bool value) {
+    uint8_t buf[4] = {0xff, 0xff, 0xff, 0xff};
+    bool old_value;
+
+    // Don't write anything if the current setting already evaluates to the
+    // same thing.
+    if (sysparam_get_bool(key, &old_value) == SYSPARAM_OK) {
+        if (old_value == value) return SYSPARAM_OK;
+    }
+
+    buf[0] = value ? 'y' : 'n';
+    return sysparam_set_data(key, buf, 1, false);
+}
+
+sysparam_status_t sysparam_iter_start(sysparam_iter_t *iter) {
+    if (!_sysparam_info.cur_base) return SYSPARAM_ERR_NOINIT;
+
+    iter->bufsize = DEFAULT_ITER_BUF_SIZE;
+    iter->key = malloc(iter->bufsize);
+    if (!iter->key) {
+        iter->bufsize = 0;
+        return SYSPARAM_ERR_NOMEM;
+    }
+    iter->key_len = 0;
+    iter->value_len = 0;
+    iter->ctx = malloc(sizeof(struct sysparam_context));
+    if (!iter->ctx) {
+        free(iter->key);
+        iter->bufsize = 0;
+        return SYSPARAM_ERR_NOMEM;
+    }
+    _init_context(iter->ctx);
+
+    return SYSPARAM_OK;
+}
+
+sysparam_status_t sysparam_iter_next(sysparam_iter_t *iter) {
+    uint8_t buffer[2];
+    sysparam_status_t status;
+    size_t required_len;
+    struct sysparam_context *ctx = iter->ctx;
+    struct sysparam_context value_ctx;
+    size_t key_space;
+    char *newbuf;
+
+    while (true) {
+        status = _find_key(ctx, NULL, 0, buffer);
+        if (status != SYSPARAM_OK) return status;
+        memcpy(&value_ctx, ctx, sizeof(value_ctx));
+
+        status = _find_value(&value_ctx, ctx->entry.idflags);
+        if (status < 0) return status;
+        if (status == SYSPARAM_NOTFOUND) continue;
+
+        key_space = ROUND_TO_WORD_BOUNDARY(ctx->entry.len + 1);
+        required_len = key_space + value_ctx.entry.len + 1;
+        if (required_len > iter->bufsize) {
+            newbuf = realloc(iter->key, required_len);
+            if (!newbuf) {
+                return SYSPARAM_ERR_NOMEM;
+            }
+            iter->key = newbuf;
+            iter->bufsize = required_len;
+        }
+
+        status = _read_payload(ctx, (uint8_t *)iter->key, iter->bufsize);
+        if (status < 0) return status;
+        // Null-terminate the key
+        iter->key[ctx->entry.len] = 0;
+        iter->key_len = ctx->entry.len;
+
+        iter->value = (uint8_t *)(iter->key + key_space);
+        status = _read_payload(&value_ctx, iter->value, iter->bufsize - key_space);
+        if (status < 0) return status;
+        // Null-terminate the value (just in case)
+        iter->value[value_ctx.entry.len] = 0;
+        iter->value_len = value_ctx.entry.len;
+        if (value_ctx.entry.idflags & ENTRY_FLAG_BINARY) {
+            iter->binary = true;
+            debug(2, "iter_next: (0x%08x) '%s' = (0x%08x) <binary-data> (%d)", ctx->addr, iter->key, value_ctx.addr, iter->value_len);
+        } else {
+            iter->binary = false;
+            debug(2, "iter_next: (0x%08x) '%s' = (0x%08x) '%s' (%d)", ctx->addr, iter->key, value_ctx.addr, iter->value, iter->value_len);
+        }
+
+        return SYSPARAM_OK;
+    }
+}
+
+void sysparam_iter_end(sysparam_iter_t *iter) {
+    if (iter->key) free(iter->key);
+    if (iter->ctx) free(iter->ctx);
+}
+
diff --git a/examples/sysparam_editor/Makefile b/examples/sysparam_editor/Makefile
new file mode 100644
index 0000000..a774b68
--- /dev/null
+++ b/examples/sysparam_editor/Makefile
@@ -0,0 +1,14 @@
+PROGRAM=sysparam_editor
+
+# Setting this to 1..3 will add extra debugging output to stdout
+EXTRA_CFLAGS=-DSYSPARAM_DEBUG=0
+
+include ../../common.mk
+
+# `make dump-flash` can be used to view the current contents of the sysparam
+# regions in flash.
+dump-flash:
+	esptool.py read_flash 0x1f8000 8192 r1.bin
+	hexdump -C r1.bin
+	esptool.py read_flash 0x1fa000 8192 r2.bin
+	hexdump -C r2.bin
diff --git a/examples/sysparam_editor/sysparam_editor.c b/examples/sysparam_editor/sysparam_editor.c
new file mode 100644
index 0000000..22f6190
--- /dev/null
+++ b/examples/sysparam_editor/sysparam_editor.c
@@ -0,0 +1,233 @@
+#include "FreeRTOS.h"
+#include "task.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sysparam.h>
+
+#include <espressif/spi_flash.h>
+
+#define CMD_BUF_SIZE 5000
+
+const int status_base = -6;
+const char *status_messages[] = {
+    "SYSPARAM_ERR_NOMEM",
+    "SYSPARAM_ERR_CORRUPT",
+    "SYSPARAM_ERR_IO",
+    "SYSPARAM_ERR_FULL",
+    "SYSPARAM_ERR_BADVALUE",
+    "SYSPARAM_ERR_NOINIT",
+    "SYSPARAM_OK",
+    "SYSPARAM_NOTFOUND",
+    "SYSPARAM_PARSEFAILED",
+};
+
+void usage(void) {
+    printf(
+        "Available commands:\n"
+        "  <key>?          -- Query the value of <key>\n"
+        "  <key>=<value>   -- Set <key> to text <value>\n"
+        "  <key>:<hexdata> -- Set <key> to binary value represented as hex\n"
+        "  dump            -- Show all currently set keys/values\n"
+        "  reformat        -- Reinitialize (clear) the sysparam area\n"
+        "  help            -- Show this help screen\n"
+        );
+}
+
+size_t tty_readline(char *buffer, size_t buf_size, bool echo) {
+    size_t i = 0;
+    int c;
+
+    while (true) {
+        c = getchar();
+        if (c == '\r') {
+            if (echo) putchar('\n');
+            break;
+        } else if (c == '\b' || c == 0x7f) {
+            if (i) {
+                if (echo) printf("\b \b");
+                i--;
+            }
+        } else if (c < 0x20) {
+            /* Ignore other control characters */
+        } else if (i >= buf_size - 1) {
+            if (echo) putchar('\a');
+        } else {
+            buffer[i++] = c;
+            if (echo) putchar(c);
+        }
+    }
+
+    buffer[i] = 0;
+    return i;
+}
+
+void print_text_value(char *key, char *value) {
+    printf("  '%s' = '%s'\n", key, value);
+}
+
+void print_binary_value(char *key, uint8_t *value, size_t len) {
+    size_t i;
+
+    printf("  %s:", key);
+    for (i = 0; i < len; i++) {
+        if (!(i & 0x0f)) {
+            printf("\n   ");
+        }
+        printf(" %02x", value[i]);
+    }
+    printf("\n");
+}
+
+sysparam_status_t dump_params(void) {
+    sysparam_status_t status;
+    sysparam_iter_t iter;
+
+    status = sysparam_iter_start(&iter);
+    if (status < 0) return status;
+    while (true) {
+        status = sysparam_iter_next(&iter);
+        if (status != SYSPARAM_OK) break;
+        if (!iter.binary) {
+            print_text_value(iter.key, (char *)iter.value);
+        } else {
+            print_binary_value(iter.key, iter.value, iter.value_len);
+        }
+    }
+    sysparam_iter_end(&iter);
+
+    if (status == SYSPARAM_NOTFOUND) {
+        // This is the normal status when we've reached the end of all entries.
+        return SYSPARAM_OK;
+    } else {
+        // Something apparently went wrong
+        return status;
+    }
+}
+
+uint8_t *parse_hexdata(char *string, size_t *result_length) {
+    size_t string_len = strlen(string);
+    uint8_t *buf = malloc(string_len / 2);
+    uint8_t c;
+    int i, j;
+    bool digit = false;
+
+    j = 0;
+    for (i = 0; string[i]; i++) {
+        c = string[i];
+        if (c >= 0x30 && c <= 0x39) {
+            c &= 0x0f;
+        } else if (c >= 0x41 && c <= 0x46) {
+            c -= 0x37;
+        } else if (c >= 0x61 && c <= 0x66) {
+            c -= 0x57;
+        } else if (c == ' ') {
+            continue;
+        } else {
+            free(buf);
+            return NULL;
+        }
+        if (!digit) {
+            buf[j] = c << 4;
+        } else {
+            buf[j++] |= c;
+        }
+        digit = !digit;
+    }
+    if (digit) {
+        free(buf);
+        return NULL;
+    }
+    *result_length = j;
+    return buf;
+}
+
+void sysparam_editor_task(void *pvParameters) {
+    char *cmd_buffer = malloc(CMD_BUF_SIZE);
+    sysparam_status_t status;
+    char *value;
+    uint8_t *bin_value;
+    size_t len;
+    uint8_t *data;
+    uint32_t base_addr, num_sectors;
+
+    if (!cmd_buffer) {
+        printf("ERROR: Cannot allocate command buffer!\n");
+        return;
+    }
+
+    printf("\nWelcome to the system parameter editor!  Enter 'help' for more information.\n\n");
+
+    status = sysparam_get_info(&base_addr, &num_sectors);
+    if (status == SYSPARAM_OK) {
+        printf("[current sysparam region is at 0x%08x (%d sectors)]\n", base_addr, num_sectors);
+    } else {
+        printf("[NOTE: No current sysparam region (initialization problem during boot?)]\n");
+        // Default to the same place/size as the normal system initialization
+        // stuff, so if the user uses this utility to reformat it, it will put
+        // it somewhere the system will find it later
+        num_sectors = DEFAULT_SYSPARAM_SECTORS;
+        base_addr = sdk_flashchip.chip_size - (4 + num_sectors) * sdk_flashchip.sector_size;
+    }
+    while (true) {
+        printf("==> ");
+        len = tty_readline(cmd_buffer, CMD_BUF_SIZE, true);
+        status = 0;
+        if (!len) continue;
+        if (cmd_buffer[len - 1] == '?') {
+            cmd_buffer[len - 1] = 0;
+            printf("Querying '%s'...\n", cmd_buffer);
+            status = sysparam_get_string(cmd_buffer, &value);
+            if (status == SYSPARAM_OK) {
+                print_text_value(cmd_buffer, value);
+                free(value);
+            } else if (status == SYSPARAM_PARSEFAILED) {
+                // This means it's actually a binary value
+                status = sysparam_get_data(cmd_buffer, &bin_value, &len, NULL);
+                if (status == SYSPARAM_OK) {
+                    print_binary_value(cmd_buffer, bin_value, len);
+                    free(value);
+                }
+            }
+        } else if ((value = strchr(cmd_buffer, '='))) {
+            *value++ = 0;
+            printf("Setting '%s' to '%s'...\n", cmd_buffer, value);
+            status = sysparam_set_string(cmd_buffer, value);
+        } else if ((value = strchr(cmd_buffer, ':'))) {
+            *value++ = 0;
+            data = parse_hexdata(value, &len);
+            if (value) {
+                printf("Setting '%s' to binary data...\n", cmd_buffer);
+                status = sysparam_set_data(cmd_buffer, data, len, true);
+                free(data);
+            } else {
+                printf("Error: Unable to parse hex data\n");
+            }
+        } else if (!strcmp(cmd_buffer, "dump")) {
+            printf("Dumping all params:\n");
+            status = dump_params();
+        } else if (!strcmp(cmd_buffer, "reformat")) {
+            printf("Re-initializing region...\n");
+            status = sysparam_create_area(base_addr, num_sectors, true);
+            if (status == SYSPARAM_OK) {
+                // We need to re-init after wiping out the region we've been
+                // using.
+                status = sysparam_init(base_addr, 0);
+            }
+        } else if (!strcmp(cmd_buffer, "help")) {
+            usage();
+        } else {
+            printf("Unrecognized command.\n\n");
+            usage();
+        }
+
+        if (status != SYSPARAM_OK) {
+            printf("! Operation returned status: %d (%s)\n", status, status_messages[status - status_base]);
+        }
+    }
+}
+
+void user_init(void)
+{
+    xTaskCreate(sysparam_editor_task, (signed char *)"sysparam_editor_task", 512, NULL, 2, NULL);
+}