diff --git a/minimal/Makefile b/minimal/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..a59bb9f89ad334be5fe6536e2a1273a6643b8d02
--- /dev/null
+++ b/minimal/Makefile
@@ -0,0 +1,48 @@
+include ../py/mkenv.mk
+
+# qstr definitions (must come before including py.mk)
+QSTR_DEFS = qstrdefsport.h
+
+# include py core make definitions
+include ../py/py.mk
+
+CROSS_COMPILE = arm-none-eabi-
+
+INC =  -I.
+INC += -I..
+INC += -I$(BUILD)
+
+CFLAGS_CORTEX_M4 = -mthumb -mtune=cortex-m4 -mabi=aapcs-linux -mcpu=cortex-m4 -mfpu=fpv4-sp-d16 -mfloat-abi=hard -fsingle-precision-constant -Wdouble-promotion
+CFLAGS = $(INC) -Wall -Werror -ansi -std=gnu99 -nostdlib $(CFLAGS_CORTEX_M4) $(COPT)
+
+#Debugging/Optimization
+ifeq ($(DEBUG), 1)
+CFLAGS += -O0 -ggdb
+else
+CFLAGS += -Os -DNDEBUG
+endif
+
+LDFLAGS = -nostdlib -T stm32f405.ld -Map=$@.map --cref
+LIBS =
+
+SRC_C = \
+	main.c \
+#	printf.c \
+	string0.c \
+	malloc0.c \
+	gccollect.c \
+
+SRC_S = \
+#	startup_stm32f40xx.s \
+	gchelper.s \
+
+OBJ = $(PY_O) $(addprefix $(BUILD)/, $(SRC_C:.c=.o) $(SRC_S:.s=.o))
+
+all: $(BUILD)/firmware.elf
+
+$(BUILD)/firmware.elf: $(OBJ)
+	$(ECHO) "LINK $@"
+	$(Q)$(LD) $(LDFLAGS) -o $@ $(OBJ) $(LIBS)
+	$(Q)$(SIZE) $@
+
+include ../py/mkrules.mk
diff --git a/minimal/main.c b/minimal/main.c
new file mode 100644
index 0000000000000000000000000000000000000000..a1e94313c54dace9484a7cbe0eea4aecd6a39bcb
--- /dev/null
+++ b/minimal/main.c
@@ -0,0 +1,121 @@
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "py/nlr.h"
+#include "py/parsehelper.h"
+#include "py/compile.h"
+#include "py/runtime.h"
+#include "py/repl.h"
+#include "py/pfenv.h"
+
+void do_str(const char *src) {
+    mp_lexer_t *lex = mp_lexer_new_from_str_len(MP_QSTR__lt_stdin_gt_, src, strlen(src), 0);
+    if (lex == NULL) {
+        return;
+    }
+
+    mp_parse_error_kind_t parse_error_kind;
+    mp_parse_node_t pn = mp_parse(lex, MP_PARSE_SINGLE_INPUT, &parse_error_kind);
+
+    if (pn == MP_PARSE_NODE_NULL) {
+        // parse error
+        mp_parse_show_exception(lex, parse_error_kind);
+        mp_lexer_free(lex);
+        return;
+    }
+
+    // parse okay
+    qstr source_name = lex->source_name;
+    mp_lexer_free(lex);
+    mp_obj_t module_fun = mp_compile(pn, source_name, MP_EMIT_OPT_NONE, true);
+
+    if (mp_obj_is_exception_instance(module_fun)) {
+        // compile error
+        mp_obj_print_exception(printf_wrapper, NULL, module_fun);
+        return;
+    }
+
+    nlr_buf_t nlr;
+    if (nlr_push(&nlr) == 0) {
+        mp_call_function_0(module_fun);
+        nlr_pop();
+    } else {
+        // uncaught exception
+        mp_obj_print_exception(printf_wrapper, NULL, (mp_obj_t)nlr.ret_val);
+    }
+}
+
+int main(int argc, char **argv) {
+    mp_init();
+    do_str("print('hello world!', list(x+1 for x in range(10)), end='eol\n')");
+    mp_deinit();
+    return 0;
+}
+
+void gc_collect(void) {
+}
+
+mp_lexer_t *mp_lexer_new_from_file(const char *filename) {
+    return NULL;
+}
+
+mp_import_stat_t mp_import_stat(const char *path) {
+    return MP_IMPORT_STAT_NO_EXIST;
+}
+
+mp_obj_t mp_builtin_open(uint n_args, const mp_obj_t *args, mp_map_t *kwargs) {
+    return mp_const_none;
+}
+MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_open_obj, 1, mp_builtin_open);
+
+void nlr_jump_fail(void *val) {
+}
+
+void NORETURN __fatal_error(const char *msg) {
+    while (1);
+}
+
+#ifndef NDEBUG
+void MP_WEAK __assert_func(const char *file, int line, const char *func, const char *expr) {
+    printf("Assertion '%s' failed, at file %s:%d\n", expr, file, line);
+    __fatal_error("Assertion failed");
+}
+#endif
+
+/*
+int _lseek() {return 0;}
+int _read() {return 0;}
+int _write() {return 0;}
+int _close() {return 0;}
+void _exit(int x) {for(;;){}}
+int _sbrk() {return 0;}
+int _kill() {return 0;}
+int _getpid() {return 0;}
+int _fstat() {return 0;}
+int _isatty() {return 0;}
+*/
+
+void *malloc(size_t n) {return NULL;}
+void *calloc(size_t nmemb, size_t size) {return NULL;}
+void *realloc(void *ptr, size_t size) {return NULL;}
+void free(void *p) {}
+int printf(const char *m, ...) {return 0;}
+void *memcpy(void *dest, const void *src, size_t n) {return NULL;}
+int memcmp(const void *s1, const void *s2, size_t n) {return 0;}
+void *memmove(void *dest, const void *src, size_t n) {return NULL;}
+void *memset(void *s, int c, size_t n) {return NULL;}
+int strcmp(const char *s1, const char* s2) {return 0;}
+int strncmp(const char *s1, const char* s2, size_t n) {return 0;}
+size_t strlen(const char *s) {return 0;}
+char *strcat(char *dest, const char *src) {return NULL;}
+char *strchr(const char *dest, int c) {return NULL;}
+#include <stdarg.h>
+int vprintf(const char *format, va_list ap) {return 0;}
+int vsnprintf(char *str,  size_t  size,  const  char  *format, va_list ap) {return 0;}
+
+#undef putchar
+int putchar(int c) {return 0;}
+int puts(const char *s) {return 0;}
+
+void _start(void) {main(0, NULL);}
diff --git a/minimal/mpconfigport.h b/minimal/mpconfigport.h
new file mode 100644
index 0000000000000000000000000000000000000000..e9a755294f0f805eb27022e6e900316924b615c4
--- /dev/null
+++ b/minimal/mpconfigport.h
@@ -0,0 +1,59 @@
+#include <stdint.h>
+
+// options to control how Micro Python is built
+
+#define MICROPY_ALLOC_PATH_MAX      (512)
+#define MICROPY_EMIT_X64            (0)
+#define MICROPY_EMIT_THUMB          (0)
+#define MICROPY_EMIT_INLINE_THUMB   (0)
+#define MICROPY_COMP_MODULE_CONST   (0)
+#define MICROPY_COMP_CONST          (0)
+#define MICROPY_MEM_STATS           (0)
+#define MICROPY_DEBUG_PRINTERS      (0)
+#define MICROPY_ENABLE_GC           (0)
+#define MICROPY_HELPER_REPL         (0)
+#define MICROPY_HELPER_LEXER_UNIX   (0)
+#define MICROPY_ENABLE_SOURCE_LINE  (0)
+#define MICROPY_ENABLE_DOC_STRING   (0)
+#define MICROPY_ERROR_REPORTING     (MICROPY_ERROR_REPORTING_TERSE)
+#define MICROPY_PY_BUILTINS_BYTEARRAY (0)
+#define MICROPY_PY_BUILTINS_MEMORYVIEW (0)
+#define MICROPY_PY_BUILTINS_FROZENSET (0)
+#define MICROPY_PY_BUILTINS_SET     (0)
+#define MICROPY_PY_BUILTINS_SLICE   (0)
+#define MICROPY_PY_BUILTINS_PROPERTY (0)
+#define MICROPY_PY___FILE__         (0)
+#define MICROPY_PY_GC               (0)
+#define MICROPY_PY_ARRAY            (0)
+#define MICROPY_PY_COLLECTIONS      (0)
+#define MICROPY_PY_MATH             (0)
+#define MICROPY_PY_CMATH            (0)
+#define MICROPY_PY_IO               (0)
+#define MICROPY_PY_STRUCT           (0)
+#define MICROPY_PY_SYS              (0)
+#define MICROPY_CPYTHON_COMPAT      (0)
+#define MICROPY_LONGINT_IMPL        (MICROPY_LONGINT_IMPL_NONE)
+#define MICROPY_FLOAT_IMPL          (MICROPY_FLOAT_IMPL_NONE)
+
+// type definitions for the specific machine
+
+#define BYTES_PER_WORD (4)
+
+#define MICROPY_MAKE_POINTER_CALLABLE(p) ((void*)((mp_uint_t)(p) | 1))
+
+#define UINT_FMT "%lu"
+#define INT_FMT "%ld"
+
+typedef int32_t mp_int_t; // must be pointer size
+typedef uint32_t mp_uint_t; // must be pointer size
+typedef void *machine_ptr_t; // must be of pointer size
+typedef const void *machine_const_ptr_t; // must be of pointer size
+typedef long mp_off_t;
+
+// extra built in names to add to the global namespace
+extern const struct _mp_obj_fun_builtin_t mp_builtin_open_obj;
+#define MICROPY_PORT_BUILTINS \
+    { MP_OBJ_NEW_QSTR(MP_QSTR_open), (mp_obj_t)&mp_builtin_open_obj },
+
+// We need to provide a declaration/definition of alloca()
+#include <alloca.h>
diff --git a/minimal/qstrdefsport.h b/minimal/qstrdefsport.h
new file mode 100644
index 0000000000000000000000000000000000000000..3ba897069bf7340b61b969cac38bf4a007860097
--- /dev/null
+++ b/minimal/qstrdefsport.h
@@ -0,0 +1 @@
+// qstrs specific to this port
diff --git a/minimal/stm32f405.ld b/minimal/stm32f405.ld
new file mode 100644
index 0000000000000000000000000000000000000000..345a92d3c1b7f4056a5e19563259aab246a9dd4e
--- /dev/null
+++ b/minimal/stm32f405.ld
@@ -0,0 +1,117 @@
+/*
+    GNU linker script for STM32F405
+*/
+
+/* Specify the memory areas */
+MEMORY
+{
+    FLASH (rx)      : ORIGIN = 0x08000000, LENGTH = 0x100000 /* entire flash, 1 MiB */
+    FLASH_ISR (rx)  : ORIGIN = 0x08000000, LENGTH = 0x004000 /* sector 0, 16 KiB */
+    FLASH_TEXT (rx) : ORIGIN = 0x08020000, LENGTH = 0x080000 /* sectors 5,6,7,8, 4*128KiB = 512 KiB (could increase it more) */
+    CCMRAM (xrw)    : ORIGIN = 0x10000000, LENGTH = 0x010000 /* 64 KiB */
+    RAM (xrw)       : ORIGIN = 0x20000000, LENGTH = 0x020000 /* 128 KiB */
+}
+ 
+/* top end of the stack */
+_estack = ORIGIN(RAM) + LENGTH(RAM);
+
+/* RAM extents for the garbage collector */
+_ram_end = ORIGIN(RAM) + LENGTH(RAM);
+_heap_end = 0x2001c000; /* tunable */
+
+/* define output sections */
+SECTIONS
+{
+    /* The startup code goes first into FLASH */
+    .isr_vector :
+    {
+        . = ALIGN(4);
+        KEEP(*(.isr_vector)) /* Startup code */
+
+        . = ALIGN(4);
+    } >FLASH_ISR
+   
+    /* The program code and other data goes into FLASH */
+    .text :
+    {
+        . = ALIGN(4);
+        *(.text)           /* .text sections (code) */
+        *(.text*)          /* .text* sections (code) */
+        *(.rodata)         /* .rodata sections (constants, strings, etc.) */
+        *(.rodata*)        /* .rodata* sections (constants, strings, etc.) */
+    /*  *(.glue_7)   */    /* glue arm to thumb code */
+    /*  *(.glue_7t)  */    /* glue thumb to arm code */
+
+        . = ALIGN(4);
+        _etext = .;        /* define a global symbol at end of code */
+        _sidata = _etext;  /* This is used by the startup in order to initialize the .data secion */
+    } >FLASH_TEXT
+     
+    /*
+    .ARM.extab :
+    {
+        *(.ARM.extab* .gnu.linkonce.armextab.*)
+    } >FLASH
+
+    .ARM :
+    {
+        __exidx_start = .;
+        *(.ARM.exidx*)
+        __exidx_end = .;
+    } >FLASH
+    */
+        
+    /* This is the initialized data section
+    The program executes knowing that the data is in the RAM
+    but the loader puts the initial values in the FLASH (inidata).
+    It is one task of the startup to copy the initial values from FLASH to RAM. */
+    .data : AT ( _sidata )
+    {
+        . = ALIGN(4);
+        _sdata = .;        /* create a global symbol at data start; used by startup code in order to initialise the .data section in RAM */
+        _ram_start = .;    /* create a global symbol at ram start for garbage collector */
+        *(.data)           /* .data sections */
+        *(.data*)          /* .data* sections */
+
+        . = ALIGN(4);
+        _edata = .;        /* define a global symbol at data end; used by startup code in order to initialise the .data section in RAM */
+    } >RAM
+            
+    /* Uninitialized data section */
+    .bss :
+    {
+        . = ALIGN(4);
+        _sbss = .;         /* define a global symbol at bss start; used by startup code */
+        *(.bss)
+        *(.bss*)
+        *(COMMON)
+
+        . = ALIGN(4);
+        _ebss = .;         /* define a global symbol at bss end; used by startup code */
+    } >RAM
+
+    /* this is to define the start of the heap, and make sure we have a minimum size */
+    .heap :
+    {
+        . = ALIGN(4);
+        _heap_start = .;    /* define a global symbol at heap start */
+    } >RAM
+
+    /* this just checks there is enough RAM for the stack */
+    .stack :
+    {
+        . = ALIGN(4);
+    } >RAM
+
+    /* Remove information from the standard libraries */
+    /*
+    /DISCARD/ :
+    {
+        libc.a ( * )
+        libm.a ( * )
+        libgcc.a ( * )
+    }
+    */
+
+    .ARM.attributes 0 : { *(.ARM.attributes) }
+}