diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 13358966d499dcd430f3ff6dcca4df9165e48311..096c48f3ad412d89b18109b68fca77b323f35063 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -143,6 +143,9 @@ typedef _Bool bool;
 
 #define API_WS2812_WRITE           0x0120
 
+#define API_CONFIG_GET_STRING      0x130
+#define API_CONFIG_GET_INTEGER     0x131
+#define API_CONFIG_GET_BOOLEAN     0x132
 /* clang-format on */
 
 typedef uint32_t api_int_id_t;
@@ -1924,5 +1927,53 @@ API(API_USB_CDCACM, int epic_usb_cdcacm(void));
  */
 API(API_WS2812_WRITE, void epic_ws2812_write(uint8_t pin, uint8_t *pixels, uint32_t n_bytes));
 
+
+/**
+ * Configuration
+ * ======
+ */
+
+/**
+ * Read an integer from the configuration file
+ *
+ * :param char* key: Name of the option to read
+ * :param int* value: Place to read the value into
+ * :return: `0` on success or a negative value if an error occured. Possible
+ *    errors:
+ *
+ *    - ``-ENOENT``: Value can not be read
+ */
+API(API_CONFIG_GET_INTEGER, int epic_config_get_integer(const char *key, int *value));
+
+/**
+ * Read a boolean from the configuration file
+ *
+ * :param char* key: Name of the option to read
+ * :param bool* value: Place to read the value into
+ * :return: `0` on success or a negative value if an error occured. Possible
+ *    errors:
+ *
+ *    - ``-ENOENT``: Value can not be read
+ */
+API(API_CONFIG_GET_BOOLEAN, int epic_config_get_boolean(const char *key, bool *value));
+
+/**
+ * Read a string from the configuration file.
+ *
+ * If the buffer supplied is too small for the config option,
+ * no error is reported and the first `buf_len - 1` characters
+ * are returned (0 terminated).
+ *
+ * :param char* key: Name of the option to read
+ * :param char* buf: Place to read the string into
+ * :param size_t buf_len: Size of the provided buffer
+ * :return: `0` on success or a negative value if an error occured. Possible
+ *    errors:
+ *
+ *    - ``-ENOENT``: Value can not be read
+ */
+API(API_CONFIG_GET_STRING, int epic_config_get_string(const char *key, char *buf, size_t buf_len));
+
+
 #endif /* _EPICARDIUM_H */
 
diff --git a/epicardium/modules/config.c b/epicardium/modules/config.c
index cb0094c22fcd5c826cbde040ebb195939a9e39e6..6bd22d22bc8212e2db15d5bdc568446c9794029e 100644
--- a/epicardium/modules/config.c
+++ b/epicardium/modules/config.c
@@ -1,6 +1,7 @@
 #include "modules/log.h"
 #include "modules/config.h"
 #include "modules/filesystem.h"
+#include "epicardium.h"
 
 #include <assert.h>
 #include <stdbool.h>
@@ -8,172 +9,128 @@
 #include <string.h>
 #include <stdlib.h>
 #include <unistd.h>
+#include <stddef.h>
 
-#define CONFIG_MAX_LINE_LENGTH 80
-
-enum OptionType {
-	OptionType_Boolean,
-	OptionType_Int,
-	OptionType_Float,
-	OptionType_String,
-};
-
-struct config_option {
-	const char *name;
-	enum OptionType type;
-	union {
-		bool boolean;
-		long integer;
-		double floating_point;
-		char *string;
-	} value;
-};
-
-static struct config_option s_options[_EpicOptionCount] = {
-/* clang-format off */
-	#define INIT_Boolean(v)  { .boolean        = (v) }
-	#define INIT_Int(v)      { .integer        = (v) }
-	#define INIT_Float(v)    { .floating_point = (v) }
-	#define INIT_String(v)   { .string         = (v) }
-	#define INIT_(tp, v)     INIT_ ## tp (v)
-	#define INIT(tp, v)      INIT_ (tp, v)
-
-	#define CARD10_SETTING(identifier, spelling, tp, default_value)         \
-		[Option ## identifier] = { .name  = (spelling),                 \
-		                           .type  = OptionType_ ## tp,          \
-		                           .value = INIT(tp, (default_value)) },
-
-	#include "modules/config.def"
-	/* clang-format on */
-};
-
-static struct config_option *findOption(const char *key)
+#define MAX_LINE_LENGTH 80
+#define KEYS_PER_BLOCK 16
+#define KEY_LENGTH 16
+#define NOT_INT_MAGIC 0x80000000
+
+// one key-value pair representing a line in the config
+typedef struct {
+	char key[KEY_LENGTH];
+
+	// the value in the config file, if it's an integer.
+	// for strings it's set to NOT_INT_MAGIC
+	int value;
+
+	// the byte offset in the config file to read the value string
+	size_t value_offset;
+} config_slot;
+
+// a block of 16 config slots
+// if more are needed, this becomes a linked list
+typedef struct {
+	config_slot slots[KEYS_PER_BLOCK];
+	void *next;
+} config_block;
+
+static config_block *config_data = NULL;
+
+// returns the config slot for a key name
+static config_slot *find_config_slot(const char *key)
 {
-	for (int i = 0; i < _EpicOptionCount; ++i) {
-		if (!strcmp(key, s_options[i].name)) {
-			return &s_options[i];
+	config_block *current = config_data;
+
+	while (current) {
+		for (int i = 0; i < KEYS_PER_BLOCK; i++) {
+			config_slot *k = &current->slots[i];
+
+			if (strcmp(k->key, key) == 0) {
+				// found what we're looking for
+				return k;
+
+			} else if (*k->key == '\0') {
+				// found the first empty key
+				return NULL;
+			}
 		}
+		current = current->next;
 	}
+
 	return NULL;
 }
 
-static bool set_bool(struct config_option *opt, const char *value)
+// returns the next available config slot, or allocates a new block if needed
+static config_slot *allocate_config_slot()
 {
-	bool val;
-	if (!strcmp(value, "1")) {
-		val = true;
-	} else if (!strcmp(value, "true")) {
-		val = true;
-	} else if (!strcmp(value, "0")) {
-		val = false;
-	} else if (!strcmp(value, "false")) {
-		val = false;
-	} else {
-		return false;
+	config_block *current;
+
+	if (config_data == NULL) {
+		config_data = malloc(sizeof(config_block));
+		assert(config_data != NULL);
+		memset(config_data, 0, sizeof(config_block));
 	}
-	opt->value.boolean = val;
-	LOG_DEBUG(
-		"card10.cfg",
-		"setting '%s' to %s",
-		opt->name,
-		val ? "true" : "false"
-	);
-	return true;
-}
 
-static bool set_int(struct config_option *opt, const char *value)
-{
-	char *endptr;
-	size_t len = strlen(value);
-	int v      = strtol(value, &endptr, 0);
-	if (endptr != (value + len)) {
-		return false;
+	current = config_data;
+
+	while (true) {
+		for (int i = 0; i < KEYS_PER_BLOCK; i++) {
+			config_slot *k = &current->slots[i];
+			if (*k->key == '\0') {
+				return k;
+			}
+		}
+
+		// this block is full and there's no next allocated block
+		if (current->next == NULL) {
+			current->next = malloc(sizeof(config_block));
+			assert(current->next != NULL);
+			memset(current->next, 0, sizeof(config_block));
+		}
+		current = current->next;
 	}
-	opt->value.integer = v;
-	LOG_DEBUG("card10.cfg", "setting '%s' to %d (0x%08x)", opt->name, v, v);
-	return true;
 }
 
-static bool set_float(struct config_option *opt, const char *value)
+// parses an int out of 'value' or returns NOT_INT_MAGIC
+static int try_parse_int(const char *value)
 {
 	char *endptr;
 	size_t len = strlen(value);
-	double v   = strtod(value, &endptr);
-	if (endptr != (value + len)) {
-		return false;
-	}
-	opt->value.floating_point = v;
-	LOG_DEBUG("card10.cfg", "setting '%s' to %f", opt->name, v);
-	return true;
-}
+	int v      = strtol(value, &endptr, 0);
 
-const char *elide(const char *str)
-{
-	static char ret[21];
-	size_t len = strlen(str);
-	if (len <= 20) {
-		return str;
+	if (endptr != (value + len)) {
+		return NOT_INT_MAGIC;
 	}
-	strncpy(ret, str, 17);
-	ret[17] = '.';
-	ret[18] = '.';
-	ret[19] = '.';
-	ret[20] = '\0';
-	return ret;
-}
 
-static bool set_string(struct config_option *opt, const char *value)
-{
-	//this leaks, but the lifetime of these ends when epicardium exits, so...
-	char *leaks       = strdup(value);
-	opt->value.string = leaks;
-	LOG_DEBUG("card10.cfg", "setting '%s' to %s", opt->name, elide(leaks));
-	return true;
+	return v;
 }
 
-static void configure(const char *key, const char *value, int lineNumber)
-{
-	struct config_option *opt = findOption(key);
-	if (!opt) {
-		//invalid key
+// loads a key/value pair into a new config slot
+static void add_config_pair(
+	const char *key, const char *value, int line_number, size_t value_offset
+) {
+	if (strlen(key) > KEY_LENGTH - 1) {
 		LOG_WARN(
 			"card10.cfg",
-			"line %d: ignoring unknown option '%s'",
-			lineNumber,
-			key
+			"line:%d: too long - aborting",
+			line_number
 		);
 		return;
 	}
-	bool ok = false;
-	switch (opt->type) {
-	case OptionType_Boolean:
-		ok = set_bool(opt, value);
-		break;
-	case OptionType_Int:
-		ok = set_int(opt, value);
-		break;
-	case OptionType_Float:
-		ok = set_float(opt, value);
-		break;
-	case OptionType_String:
-		ok = set_string(opt, value);
-		break;
-	default:
-		assert(0 && "unreachable");
-	}
-	if (!ok) {
-		LOG_WARN(
-			"card10.cfg",
-			"line %d: ignoring invalid value '%s' for option '%s'",
-			lineNumber,
-			value,
-			key
-		);
-	}
+
+	config_slot *slot = allocate_config_slot();
+	strncpy(slot->key, key, KEY_LENGTH);
+	slot->value        = try_parse_int(value);
+	slot->value_offset = value_offset;
 }
 
-static void doline(char *line, char *eol, int lineNumber)
+// parses one line of the config file
+static void
+parse_line(char *line, char *eol, int line_number, size_t line_offset)
 {
+	char *line_start = line;
+
 	//skip leading whitespace
 	while (*line && isspace((int)*line))
 		++line;
@@ -189,9 +146,8 @@ static void doline(char *line, char *eol, int lineNumber)
 		if (*key) {
 			LOG_WARN(
 				"card10.cfg",
-				"line %d (%s): syntax error",
-				lineNumber,
-				elide(line)
+				"line %d: syntax error",
+				line_number
 			);
 		}
 		return;
@@ -203,7 +159,7 @@ static void doline(char *line, char *eol, int lineNumber)
 		--e_key;
 	e_key[1] = '\0';
 	if (*key == '\0') {
-		LOG_WARN("card10.cfg", "line %d: empty key", lineNumber);
+		LOG_WARN("card10.cfg", "line %d: empty key", line_number);
 		return;
 	}
 
@@ -220,43 +176,30 @@ static void doline(char *line, char *eol, int lineNumber)
 		LOG_WARN(
 			"card10.cfg",
 			"line %d: empty value for option '%s'",
-			lineNumber,
+			line_number,
 			key
 		);
 		return;
 	}
 
-	configure(key, value, lineNumber);
-}
-
-bool config_get_boolean(enum EpicConfigOption option)
-{
-	struct config_option *opt = &s_options[option];
-	assert(opt->type == OptionType_Boolean);
-	return opt->value.boolean;
-}
+	size_t value_offset = value - line_start + line_offset;
 
-long config_get_integer(enum EpicConfigOption option)
-{
-	struct config_option *opt = &s_options[option];
-	assert(opt->type == OptionType_Int);
-	return opt->value.integer;
-}
-
-double config_get_float(enum EpicConfigOption option)
-{
-	struct config_option *opt = &s_options[option];
-	assert(opt->type == OptionType_Float);
-	return opt->value.floating_point;
+	add_config_pair(key, value, line_number, value_offset);
 }
 
-const char *config_get_string(enum EpicConfigOption option)
+// convert windows line endings to unix line endings.
+// we don't care about the extra empty lines
+static void convert_crlf_to_lflf(char *buf, int n)
 {
-	struct config_option *opt = &s_options[option];
-	assert(opt->type == OptionType_String);
-	return opt->value.string;
+	while (n--) {
+		if (*buf == '\r') {
+			*buf = '\n';
+		}
+		buf++;
+	}
 }
 
+// parses the entire config file
 void load_config(void)
 {
 	LOG_DEBUG("card10.cfg", "loading...");
@@ -270,12 +213,14 @@ void load_config(void)
 		);
 		return;
 	}
-	char buf[CONFIG_MAX_LINE_LENGTH + 1];
-	int lineNumber = 0;
+	char buf[MAX_LINE_LENGTH + 1];
+	int line_number    = 0;
+	size_t file_offset = 0;
 	int nread;
 	do {
-		nread = epic_file_read(fd, buf, CONFIG_MAX_LINE_LENGTH);
-		if (nread < CONFIG_MAX_LINE_LENGTH) {
+		nread = epic_file_read(fd, buf, MAX_LINE_LENGTH);
+		convert_crlf_to_lflf(buf, nread);
+		if (nread < MAX_LINE_LENGTH) {
 			//add fake EOL to ensure termination
 			buf[nread++] = '\n';
 		}
@@ -285,64 +230,184 @@ void load_config(void)
 		char *eol    = NULL;
 		int last_eol = 0;
 		while (line) {
-			//line points one character past the las (if any) '\n' hence '- 1'
+			//line points one character past the last (if any) '\n' hence '- 1'
 			last_eol = line - buf - 1;
 			eol      = strchr(line, '\n');
-			++lineNumber;
+			++line_number;
 			if (eol) {
 				*eol = '\0';
-				doline(line, eol, lineNumber);
+				parse_line(line, eol, line_number, file_offset);
+				file_offset += eol - line + 1;
 				line = eol + 1;
-			} else {
-				if (line == buf) {
-					//line did not fit into buf
-					LOG_WARN(
-						"card10.cfg",
-						"line:%d: too long - aborting",
-						lineNumber
-					);
-					return;
-				} else {
-					int seek_back = last_eol - nread;
-					LOG_DEBUG(
-						"card10.cfg",
-						"nread, last_eol, seek_back: %d,%d,%d",
-						nread,
-						last_eol,
-						seek_back
-					);
-					assert(seek_back <= 0);
-					if (seek_back) {
-						int rc = epic_file_seek(
-							fd,
-							seek_back,
-							SEEK_CUR
-						);
-						if (rc < 0) {
-							LOG_ERR("card10.cfg",
-								"seek failed, aborting");
-							return;
-						}
-						char newline;
-						rc = epic_file_read(
-							fd, &newline, 1
-						);
-						if (rc < 0 || newline != '\n') {
-							LOG_ERR("card10.cfg",
-								"seek failed, aborting");
-							LOG_DEBUG(
-								"card10.cfg",
-								"seek failed at read-back of newline: rc: %d read: %d",
-								rc,
-								(int)newline
-							);
-							return;
-						}
-					}
-					break;
-				}
+				continue;
+			}
+			if (line == buf) {
+				//line did not fit into buf
+				LOG_WARN(
+					"card10.cfg",
+					"line:%d: too long - aborting",
+					line_number
+				);
+				return;
+			}
+			int seek_back = last_eol - nread;
+
+			LOG_DEBUG(
+				"card10.cfg",
+				"nread, last_eol, seek_back: %d,%d,%d",
+				nread,
+				last_eol,
+				seek_back
+			);
+			assert(seek_back <= 0);
+			if (!seek_back) {
+				break;
 			}
+
+			int rc = epic_file_seek(fd, seek_back, SEEK_CUR);
+			if (rc < 0) {
+				LOG_ERR("card10.cfg", "seek failed, aborting");
+				return;
+			}
+			char newline;
+			rc = epic_file_read(fd, &newline, 1);
+			if (rc < 0 || (newline != '\n' && newline != '\r')) {
+				LOG_ERR("card10.cfg", "seek failed, aborting");
+				LOG_DEBUG(
+					"card10.cfg",
+					"seek failed at read-back of newline: rc: %d read: %d",
+					rc,
+					(int)newline
+				);
+				return;
+			}
+			break;
 		}
-	} while (nread == sizeof(buf));
+	} while (nread == MAX_LINE_LENGTH);
 	epic_file_close(fd);
 }
+
+// opens the config file, seeks to seek_offset and reads buf_len bytes
+// used for reading strings without storing them in memory
+// since we don't need to optimize for that use case as much
+static size_t read_config_offset(size_t seek_offset, char *buf, size_t buf_len)
+{
+	int fd = epic_file_open("card10.cfg", "r");
+	if (fd < 0) {
+		LOG_DEBUG(
+			"card10.cfg",
+			"opening config failed: %s (%d)",
+			strerror(-fd),
+			fd
+		);
+		return 0;
+	}
+
+	int rc = epic_file_seek(fd, seek_offset, SEEK_SET);
+	if (rc < 0) {
+		LOG_ERR("card10.cfg", "seek failed, aborting");
+		return 0;
+	}
+
+	// one byte less to accommodate the 0 termination
+	int nread = epic_file_read(fd, buf, buf_len - 1);
+
+	buf[nread] = '\0';
+
+	epic_file_close(fd);
+
+	return nread;
+}
+
+// returns error if not found or invalid
+int epic_config_get_integer(const char *key, int *value)
+{
+	config_slot *slot = find_config_slot(key);
+	if (slot && slot->value != NOT_INT_MAGIC) {
+		*value = slot->value;
+		return 0;
+	}
+	return -ENOENT;
+}
+
+// returns default_value if not found or invalid
+int config_get_integer_with_default(const char *key, int default_value)
+{
+	int value;
+	int ret = epic_config_get_integer(key, &value);
+	if (ret) {
+		return default_value;
+	} else {
+		return value;
+	}
+}
+
+// returns error if not found
+int epic_config_get_string(const char *key, char *buf, size_t buf_len)
+{
+	config_slot *slot = find_config_slot(key);
+	if (!(slot && slot->value_offset)) {
+		return -ENOENT;
+	}
+
+	size_t nread = read_config_offset(slot->value_offset, buf, buf_len);
+	if (nread == 0) {
+		return -ENOENT;
+	}
+
+	char *eol = strchr(buf, '\n');
+	if (eol) {
+		*eol = '\0';
+	}
+
+	return 0;
+}
+
+// returns dflt if not found, otherwise same pointer as buf
+char *config_get_string_with_default(
+	const char *key, char *buf, size_t buf_len, char *dflt
+) {
+	int ret = epic_config_get_string(key, buf, buf_len);
+	if (ret) {
+		return dflt;
+	} else {
+		return buf;
+	}
+}
+
+// returns error if not found or invalid
+int epic_config_get_boolean(const char *key, bool *value)
+{
+	int int_value;
+	int ret = epic_config_get_integer(key, &int_value);
+
+	if (ret == 0) {
+		*value = !!int_value;
+		return 0;
+	}
+
+	char buf[MAX_LINE_LENGTH];
+	epic_config_get_string(key, buf, MAX_LINE_LENGTH);
+
+	if (!strcmp(buf, "true")) {
+		*value = true;
+		return 0;
+	} else if (!strcmp(buf, "false")) {
+		*value = false;
+		return 0;
+	}
+
+	return -ERANGE;
+}
+
+// returns default_value if not found or invalid
+bool config_get_boolean_with_default(const char *key, bool default_value)
+{
+	bool value;
+	int ret = epic_config_get_boolean(key, &value);
+	if (ret) {
+		return default_value;
+	} else {
+		return value;
+	}
+}
diff --git a/epicardium/modules/config.def b/epicardium/modules/config.def
deleted file mode 100644
index 455aaedd8d61f821c1d08ee99bb45c3a61d983ba..0000000000000000000000000000000000000000
--- a/epicardium/modules/config.def
+++ /dev/null
@@ -1,11 +0,0 @@
-#ifndef CARD10_SETTING
-#  define CARD10_SETTING(identifier, spelling, type, default_value)
-#endif
-
-CARD10_SETTING(ExecuteElf, "execute_elf", Boolean, false)
-//CARD10_SETTING(Nick, "nick", String, "an0n")
-//CARD10_SETTING(Timeout, "timeout", Integer, 123)
-//CARD10_SETTING(Dampening, "dampening", Float, 420)
-
-
-#undef CARD10_SETTING
diff --git a/epicardium/modules/config.h b/epicardium/modules/config.h
index b72b08a5205bf9344bc3e3d805423dc826f30ccc..c95d504cfb07f94d829d316eb15d84b20979c0cf 100644
--- a/epicardium/modules/config.h
+++ b/epicardium/modules/config.h
@@ -2,20 +2,16 @@
 #define EPICARDIUM_MODULES_CONFIG_H_INCLUDED
 
 #include <stdbool.h>
-
-enum EpicConfigOption {
-    #define CARD10_SETTING(identifier, spelling, type, default_value) Option ## identifier,
-	#include "modules/config.def"
-    _EpicOptionCount
-};
+#include <stddef.h>
 
 //initialize configuration values and load card10.cfg
 void load_config(void);
 
-bool config_get_boolean(enum EpicConfigOption option);
-long config_get_integer(enum EpicConfigOption option);
-double config_get_float(enum EpicConfigOption option);
-const char* config_get_string(enum EpicConfigOption option);
+// returns default_value if not found or invalid
+bool config_get_boolean_with_default(const char *key, bool default_value);
+int config_get_integer_with_default(const char *key, int default_value);
 
+// returns dflt if not found, otherwise same pointer as buf
+char *config_get_string_with_default(const char *key, char *buf, size_t buf_len, char *dflt);
 
 #endif//EPICARDIUM_MODULES_CONFIG_H_INCLUDED
diff --git a/epicardium/modules/lifecycle.c b/epicardium/modules/lifecycle.c
index 25689bd3db224789e5874deedd501fc1bcf8bfea..2e72880fb892fc38db427c36494b4044d40d1401 100644
--- a/epicardium/modules/lifecycle.c
+++ b/epicardium/modules/lifecycle.c
@@ -362,7 +362,7 @@ void vLifecycleTask(void *pvParameters)
 
 	hardware_init();
 
-	execute_elfs = config_get_boolean(OptionExecuteElf);
+	execute_elfs = config_get_boolean_with_default("execute_elf", false);
 
 	/* When triggered, reset core 1 to menu */
 	while (1) {