diff --git a/epicardium/main.c b/epicardium/main.c
index 56639ea56ead01fc06138e0f2f712518193c475f..417f8929e7648a03343ba74692d3809664606030 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -1,6 +1,7 @@
 #include "modules/modules.h"
 #include "modules/log.h"
 #include "modules/filesystem.h"
+#include "modules/config.h"
 #include "card10-version.h"
 
 #include "FreeRTOS.h"
diff --git a/epicardium/modules/config.c b/epicardium/modules/config.c
new file mode 100644
index 0000000000000000000000000000000000000000..13ce3abaf12947237bf9963ad78af6e437e2c525
--- /dev/null
+++ b/epicardium/modules/config.c
@@ -0,0 +1,315 @@
+#include "modules/log.h"
+#include "modules/config.h"
+#include "modules/filesystem.h"
+
+#include <assert.h>
+#include <stdbool.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdlib.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] = {
+	[OptionExecuteElf] = { .name  = "execute_elf",
+			       .type  = OptionType_Boolean,
+			       .value = { .boolean = false } },
+};
+
+static struct config_option *findOption(const char *key)
+{
+	for (int i = 0; i < _EpicOptionCount; ++i) {
+		if (!strcmp(key, s_options[i].name)) {
+			return &s_options[i];
+		}
+	}
+	return NULL;
+}
+
+static bool set_bool(struct config_option *opt, const char *value)
+{
+	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;
+	}
+	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 base   = 10;
+	if (len > 2 && value[0] == '0' && value[1] == 'x') {
+		base = 16;
+#ifdef CONFIG_ENABLE_OCTAL_NUMBERS
+	} else if (len > 1 && value[0] == '0') {
+		base = 8;
+#endif
+	}
+	int v = strtol(value, &endptr, base);
+	if (endptr != (value + len)) {
+		return false;
+	}
+	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)
+{
+	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;
+}
+
+const char *elide(const char *str)
+{
+	static char ret[21];
+	size_t len = strlen(str);
+	if (len <= 20) {
+		return str;
+	}
+	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...
+	size_t len  = strlen(value);
+	char *leaks = (char *)malloc(len);
+	strncpy(leaks, value, len);
+	opt->value.string = leaks;
+	LOG_DEBUG("card10.cfg", "setting '%s' to %s", opt->name, elide(leaks));
+	return true;
+}
+
+static void configure(const char *key, const char *value, int lineNumber)
+{
+	struct config_option *opt = findOption(key);
+	if (!opt) {
+		//invalid key
+		LOG_WARN(
+			"card10.cfg",
+			"line %d: ignoring unknown option '%s'",
+			lineNumber,
+			key
+		);
+		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
+		);
+	}
+}
+
+static void doline(char *line, char *eol, int lineNumber)
+{
+	//skip leading whitespace
+	while (*line && isspace(*line))
+		++line;
+
+	char *key = line;
+	if (*key == '#') {
+		//skip comments
+		return;
+	}
+
+	char *eq = strchr(line, '=');
+	if (!eq) {
+		if (*key) {
+			LOG_WARN(
+				"card10.cfg",
+				"line %d (%s): syntax error",
+				lineNumber,
+				elide(line)
+			);
+		}
+		return;
+	}
+
+	char *e_key = eq - 1;
+	//skip trailing whitespace in key
+	while (e_key > key && isspace(*e_key))
+		--e_key;
+	e_key[1] = '\0';
+	if (*key == '\0') {
+		LOG_WARN("card10.cfg", "line %d: empty key", lineNumber);
+		return;
+	}
+
+	char *value = eq + 1;
+	//skip leading whitespace
+	while (*value && isspace(*value))
+		++value;
+
+	char *e_val = eol - 1;
+	//skip trailing whitespace
+	while (e_val > value && isspace(*e_val))
+		--e_val;
+	if (*value == '\0') {
+		LOG_WARN(
+			"card10.cfg",
+			"line %d: empty value for option '%s'",
+			lineNumber,
+			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;
+}
+
+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;
+}
+
+const char *config_get_string(enum EpicConfigOption option)
+{
+	struct config_option *opt = &s_options[option];
+	assert(opt->type == OptionType_String);
+	return opt->value.string;
+}
+
+void load_config(void)
+{
+	LOG_DEBUG("card10.cfg", "loading...");
+	int fd = epic_file_open("card10.cfg", "r");
+	if (fd < 0) {
+		LOG_DEBUG(
+			"card10.cfg",
+			"loading failed: %s (%d)",
+			strerror(-fd),
+			fd
+		);
+		return;
+	}
+	char buf[CONFIG_MAX_LINE_LENGTH];
+	int nread = epic_file_read(fd, buf, sizeof(buf));
+	if (nread < sizeof(buf)) {
+		//add fake EOL
+		buf[nread] = '\n';
+	}
+	int lineNumber = 0;
+	while (nread) {
+		char *line = buf;
+		char *eol  = NULL;
+		while (line) {
+			eol = strchr(line, '\n');
+			if (!eol) {
+				break;
+			}
+			++lineNumber;
+			*eol = '\0';
+			doline(line, eol, lineNumber);
+			line = eol + 1;
+			if (line - buf >= sizeof(buf)) {
+				//eol was right at the end of buf,
+				//break this loop and prevent memmove:
+				line = NULL;
+			}
+		}
+		if (line) {
+			int head = line - buf;
+			if (head == 0) {
+				//line did not fit into buf
+				LOG_WARN(
+					"card10.cfg",
+					"line:%d: too long",
+					lineNumber
+				);
+				break;
+			}
+			int tail = (int)sizeof(buf) - head;
+			memmove(buf, line, tail);
+			nread = epic_file_read(fd, buf + tail, head);
+			if (nread < head) {
+				//add fake-eol in case of partial read
+				buf[tail + nread] = '\n';
+			}
+		}
+	}
+}
diff --git a/epicardium/modules/config.h b/epicardium/modules/config.h
new file mode 100644
index 0000000000000000000000000000000000000000..cfde8b163bfc761ebd9aac9111e7be2b6de7a5ea
--- /dev/null
+++ b/epicardium/modules/config.h
@@ -0,0 +1,20 @@
+#ifndef EPICARDIUM_MODULES_CONFIG_H_INCLUDED
+#define EPICARDIUM_MODULES_CONFIG_H_INCLUDED
+
+#include <stdbool.h>
+
+enum EpicConfigOption {
+    OptionExecuteElf,
+    _EpicOptionCount
+};
+
+//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);
+
+
+#endif//EPICARDIUM_MODULES_CONFIG_H_INCLUDED
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index 693ca9a8f94a432ff64d770f91d9e9005c88d42a..628249a305098911b6dfe3c861b8759d6d5a5a62 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -21,5 +21,6 @@ module_sources = files(
   'trng.c',
   'vibra.c',
   'watchdog.c',
-  'usb.c'
+  'usb.c',
+  'config.c',
 )