diff --git a/epicardium/api/common.h b/epicardium/api/common.h
index c884f37c48aaa7c6b26b64d5961f99df7a07c6f2..ff0c9f3abf4da6822ca89c890c3a6bb55fab7ce6 100644
--- a/epicardium/api/common.h
+++ b/epicardium/api/common.h
@@ -8,7 +8,8 @@
  * Semaphore used for API synchronization.
  * TODO: Replace this with a LDREX/STREX based implementation
  */
-#define _API_SEMAPHORE    0
+#define _API_SEMAPHORE        0
+#define _CONTROL_SEMAPHORE    1
 
 /* Type of API IDs */
 typedef uint32_t api_id_t;
@@ -19,6 +20,13 @@ typedef uint32_t api_id_t;
 
 /* Layout of the shared memory for API calls */
 struct api_call_mem {
+	/*
+	 * Reset stub.  The reset stub is a small function provided by
+	 * epicardium that should be called by a payload when receiving the
+	 * reset interrupt.
+	 */
+	void (*reset_stub)();
+
 	/*
 	 * Flag for synchronization of API calls.  When this flag
 	 * is set, the caller has issued a call and is waiting for
diff --git a/epicardium/api/control.c b/epicardium/api/control.c
new file mode 100644
index 0000000000000000000000000000000000000000..daa1ff52ea81cb897e8c22efc1c024d6cc4d96ec
--- /dev/null
+++ b/epicardium/api/control.c
@@ -0,0 +1,233 @@
+#include "epicardium.h"
+#include "api/dispatcher.h"
+#include "api/interrupt-sender.h"
+#include "modules/log.h"
+
+#include "card10.h"
+
+#include "max32665.h"
+#include "sema.h"
+#include "tmr.h"
+
+static void __core1_init(void);
+
+struct core1_info {
+	/* Location of core1's interrupt vector table */
+	volatile uintptr_t ivt_addr;
+	/* Whether core 1 is ready for a new IVT */
+	volatile bool ready;
+};
+
+/*
+ * Information passing structure for controlling core 1.
+ */
+static volatile struct core1_info core1_info = {
+	.ivt_addr = 0x00,
+	.ready    = false,
+};
+
+/*
+ * Minimal IVT needed for initial startup.  This IVT only contains the initial
+ * stack pointer and reset-handler and is used to startup core 1.  Afterwards,
+ * the payload's IVT is loaded into VTOR and used from then on.
+ */
+static uintptr_t core1_initial_ivt[] = {
+	/* Initial Stack Pointer */
+	0x20080000,
+	/* Reset Handler */
+	(uintptr_t)__core1_reset,
+};
+
+/*
+ * Reset Handler
+ *
+ * Calls __core1_init() to reset & prepare the core for loading a new payload.
+ */
+__attribute__((naked)) void __core1_reset(void)
+{
+	__asm volatile("mov	sp, %0\n\t"
+		       : /* No Outputs */
+		       : "r"(core1_initial_ivt[0]));
+	__core1_init();
+}
+
+/*
+ * Init core 1.  This function will reset the core and wait for a new IVT
+ * address from Epicardium.  Once this address is received, it will start
+ * execution with the supplied reset handler.
+ */
+void __core1_init(void)
+{
+	/*
+	 * Clear any pending API interrupts.
+	 */
+	TMR_IntClear(MXC_TMR5);
+
+	/*
+	 * Reset Interrupts
+	 *
+	 * To ensure proper operation of the new payload, disable all interrupts
+	 * and clear all pending ones.
+	 */
+	for (int i = 0; i < MXC_IRQ_EXT_COUNT; i++) {
+		NVIC_DisableIRQ(i);
+		NVIC_ClearPendingIRQ(i);
+		NVIC_SetPriority(i, 0);
+	}
+
+	/*
+	 * Check whether we catched the core during an interrupt.  If this is
+	 * the case, try returning from the exception handler first and call
+	 * __core1_reset() again in thread context.
+	 */
+	if ((SCB->ICSR & SCB_ICSR_VECTACTIVE_Msk) != 0) {
+		/*
+		 * Construct an exception frame so the CPU will jump back to our
+		 * __core1_reset() function once we exit from the exception
+		 * handler.
+		 *
+		 * To exit the exception, a special "EXC_RETURN" value is loaded
+		 * into the link register and then branched to.
+		 */
+		__asm volatile(
+			"ldr	r0, =0x41000000\n\t"
+			"ldr	r1, =0\n\t"
+			"push	{ r0 }\n\t" /* xPSR */
+			"push	{ %0 }\n\t" /* PC */
+			"push	{ %0 }\n\t" /* LR */
+			"push	{ r1 }\n\t" /* R12 */
+			"push	{ r1 }\n\t" /* R3 */
+			"push	{ r1 }\n\t" /* R2 */
+			"push	{ r1 }\n\t" /* R1 */
+			"push	{ r1 }\n\t" /* R0 */
+
+			"ldr	lr, =0xFFFFFFF9\n\t"
+			"bx	lr\n\t"
+			: /* No Outputs */
+			: "r"((uintptr_t)__core1_reset)
+			: "pc", "lr");
+
+		/* unreachable */
+		while (1)
+			;
+	}
+
+	/* Wait for the IVT address */
+	while (1) {
+		while (SEMA_GetSema(_CONTROL_SEMAPHORE) == E_BUSY) {
+		}
+
+		__DMB();
+		__ISB();
+
+		/*
+		 * The IVT address is reset to 0 by Epicardium before execution
+		 * gets here.  Once a new address has been set, core 1 can use
+		 * the new IVT.
+		 */
+		if (core1_info.ivt_addr != 0x00) {
+			break;
+		}
+
+		/* Signal that we are ready for an IVT address */
+		core1_info.ready = true;
+
+		SEMA_FreeSema(_CONTROL_SEMAPHORE);
+
+		__WFE();
+	}
+
+	uintptr_t *ivt      = (uintptr_t *)core1_info.ivt_addr;
+	core1_info.ivt_addr = 0x00;
+
+	SEMA_FreeSema(_CONTROL_SEMAPHORE);
+
+	/*
+	 * Reset the call-flag before entering the payload so API calls behave
+	 * properly.  This is necessary because epic_exec() will set the flag
+	 * to "returning" on exit.
+	 */
+	API_CALL_MEM->call_flag = _API_FLAG_IDLE;
+
+	/*
+	 * Set the IVT
+	 */
+	SCB->VTOR = (uintptr_t)ivt;
+
+	/*
+	 * Clear any pending API interrupts.
+	 */
+	TMR_IntClear(MXC_TMR5);
+	NVIC_ClearPendingIRQ(TMR5_IRQn);
+
+	/*
+	 * Jump to payload's reset handler
+	 */
+	__asm volatile(
+		"ldr r0, %0\n\t"
+		"blx r0\n\r"
+		: /* No Outputs */
+		: "m"(*(ivt + 1))
+		: "r0");
+}
+
+void core1_boot(void)
+{
+	/*
+	 * Boot using the initial IVT.  This will place core 1 into a loop,
+	 * waiting for a payload.
+	 */
+	core1_start(&core1_initial_ivt);
+}
+
+void core1_reset(void)
+{
+	/* Signal core 1 that we intend to load a new payload. */
+	api_interrupt_trigger(EPIC_INT_RESET);
+
+	/* Wait for the core to accept */
+	while (1) {
+		while (SEMA_GetSema(_CONTROL_SEMAPHORE) == E_BUSY) {
+		}
+
+		/*
+		 * core 1 will set the ready flag once it is spinning in the
+		 * above loop, waiting for a new IVT.
+		 */
+		if (core1_info.ready) {
+			break;
+		}
+
+		SEMA_FreeSema(_CONTROL_SEMAPHORE);
+
+		for (int i = 0; i < 10000; i++) {
+		}
+	}
+
+	/*
+	 * TODO: If the other core does not respond within a certain grace
+	 * period, we need to force it into our desired state by overwriting
+	 * all of its memory.  Yes, I don't like this method either ...
+	 */
+}
+
+void core1_load(void *ivt, char *args)
+{
+	/* If the core is currently in an API call, reset it. */
+	API_CALL_MEM->call_flag = _API_FLAG_IDLE;
+	API_CALL_MEM->id        = 0;
+	API_CALL_MEM->int_id    = (-1);
+
+	api_prepare_args(args);
+
+	core1_info.ivt_addr = (uintptr_t)ivt;
+	core1_info.ready    = false;
+
+	__DMB();
+	__ISB();
+
+	SEMA_FreeSema(_CONTROL_SEMAPHORE);
+
+	__SEV();
+	__WFE();
+}
diff --git a/epicardium/api/dispatcher.c b/epicardium/api/dispatcher.c
index 730579758e22bd66bd84520a4006bb0acb8ebe17..4ffac4220a9aa0ed473a5e7cf911cb092a7135d0 100644
--- a/epicardium/api/dispatcher.c
+++ b/epicardium/api/dispatcher.c
@@ -15,8 +15,12 @@ int api_dispatcher_init()
 {
 	int ret;
 
-	ret                     = SEMA_Init(NULL);
-	API_CALL_MEM->call_flag = _API_FLAG_IDLE;
+	ret = SEMA_Init(NULL);
+	SEMA_FreeSema(_API_SEMAPHORE);
+	API_CALL_MEM->reset_stub = __core1_reset;
+	API_CALL_MEM->call_flag  = _API_FLAG_IDLE;
+	API_CALL_MEM->id         = 0;
+	API_CALL_MEM->int_id     = (-1);
 
 	/*
 	 * Enable TX events for both cores.
diff --git a/epicardium/api/dispatcher.h b/epicardium/api/dispatcher.h
index 1294592d79eedf1bec49cc68525c81e306248c55..2299f54df34f5ac0d13e187de1bb712ae2af1b34 100644
--- a/epicardium/api/dispatcher.h
+++ b/epicardium/api/dispatcher.h
@@ -28,3 +28,19 @@ api_id_t api_dispatcher_exec();
  * The data is a NULL-terminated string.
  */
 void api_prepare_args(char *args);
+
+/*********************************************************************
+ *                         core 1 control                            *
+ *********************************************************************/
+
+/* Startup core1 into a state where it is ready to receive a payload. */
+void core1_boot(void);
+
+/* Reset core 1 into a state where it can accept a new payload */
+void core1_reset(void);
+
+/* Load a payload into core 1 */
+void core1_load(void *ivt, char *args);
+
+/* core 1 reset stub.  See epicardium/api/control.c for details. */
+void __core1_reset(void);
diff --git a/epicardium/api/interrupt-receiver.c b/epicardium/api/interrupt-receiver.c
index 7684c6bb1e370fe0c48c71f475d3794c7174bf89..f9856423fd1a51211752dcd810b608fb549f9fcd 100644
--- a/epicardium/api/interrupt-receiver.c
+++ b/epicardium/api/interrupt-receiver.c
@@ -12,3 +12,10 @@ void TMR5_IRQHandler(void)
 	__dispatch_isr(API_CALL_MEM->int_id);
 	API_CALL_MEM->int_id = (-1);
 }
+
+/* Reset Handler */
+void __epic_isr_reset(void)
+{
+	API_CALL_MEM->int_id = (-1);
+	API_CALL_MEM->reset_stub();
+}
diff --git a/epicardium/main.c b/epicardium/main.c
index dc3dfd25955fab8ea4682724e9f6af25ae539a0d..84abf8d9ae37d936ef2535204575e986cec7b274 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -148,6 +148,8 @@ int main(void)
 	LOG_INFO("startup", "starting light sensor ...");
 	epic_light_sensor_run();
 
+	core1_boot();
+
 	/*
 	 * See if there's a l0dable.elf to run. If not, run pycardium.
 	 * This is temporary until epicardium gets a l0dable API from pycardium.
@@ -163,11 +165,11 @@ int main(void)
 			LOG_INFO(
 				"startup", "Starting %s on core1 ...", l0dable
 			);
-			core1_start(info.isr_vector);
+			core1_load(info.isr_vector, "");
 		}
 	} else {
-		LOG_INFO("startup", "Starting pycardium on core1 ...");
-		core1_start((void *)0x10080000);
+		LOG_INFO("startup", "Starting pycardium on core 1 ...");
+		core1_load((void *)0x10080000, "main.py");
 	}
 
 	LOG_INFO("startup", "Starting FreeRTOS ...");
diff --git a/epicardium/meson.build b/epicardium/meson.build
index c9b128f7289f791bc4c8bbd2040d7b8d10e1d710..220f0bae63c7f84e60aee6002760c8271c7587de 100644
--- a/epicardium/meson.build
+++ b/epicardium/meson.build
@@ -37,8 +37,9 @@ api_dispatcher_lib = static_library(
   'api-dispatcher',
   'api/dispatcher.c',
   'api/interrupt-sender.c',
+  'api/control.c',
   api[1], # Dispatcher
-  dependencies: periphdriver,
+  dependencies: [libcard10, periphdriver],
 )
 
 ##########################################################################