diff --git a/lib/micropython/gen-frozen.sh b/lib/micropython/gen-frozen.sh
new file mode 100755
index 0000000000000000000000000000000000000000..3bf9c4ea984c90c6032c61a20c3c05e07398629b
--- /dev/null
+++ b/lib/micropython/gen-frozen.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+set -e
+
+PYTHON="$1"
+SOURCE_DIR="$2"
+OUTPUT="$3"
+QSTR_HEADER="$4"
+
+shift 4
+
+# We need the defs header, not the generated
+QSTR_HEADER="$(dirname "$QSTR_HEADER")/qstrdefs.preprocessed.h"
+
+"$PYTHON" "$SOURCE_DIR"/micropython/tools/mpy-tool.py \
+    --freeze \
+    --qstr-header "$QSTR_HEADER" \
+    -mlongint-impl longlong \
+    "$@" >"$OUTPUT"
diff --git a/lib/micropython/gen-mpy-cross.sh b/lib/micropython/gen-mpy-cross.sh
new file mode 100755
index 0000000000000000000000000000000000000000..56f10a2346db8865d27adfc78c94389664767835
--- /dev/null
+++ b/lib/micropython/gen-mpy-cross.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+set -e
+
+SOURCE_DIR="$1"
+OUTPUT="$(realpath "$2")"
+
+cd "$SOURCE_DIR"/micropython/mpy-cross
+make -j "$(nproc)" >/dev/null
+cp mpy-cross "$OUTPUT"
diff --git a/lib/micropython/meson.build b/lib/micropython/meson.build
index c6a16cca2dd45642e4a01852a6bce9066a01b7f0..5922396ac046604e5d8cad3b1c261cd4c0ec9755 100644
--- a/lib/micropython/meson.build
+++ b/lib/micropython/meson.build
@@ -17,6 +17,36 @@ micropython_gen_qstr = [
   meson.current_source_dir(),
 ]
 
+# mpy-cross
+mpy_cross_bin = custom_target(
+  'mpy-cross',
+  output: 'mpy-cross',
+  build_by_default: true,
+  command: [files('gen-mpy-cross.sh'), meson.current_source_dir(), '@OUTPUT@'],
+)
+
+# seriously meson, this is retarded
+mpy_cross_wrapper = executable(
+  'mpy-cross-wrapper',
+  'mpy-cross-wrapper.c',
+  link_depends: mpy_cross_bin,
+  native: true,
+  c_args: ['-DMPY_CROSS_PATH="' + meson.current_build_dir() + '"'],
+)
+
+mpy_cross = generator(
+  mpy_cross_wrapper,
+  output: '@BASENAME@.mpy',
+  arguments: ['-o', '@OUTPUT@', '-s', '@PLAINNAME@', '-march=armv7m', '@INPUT@'],
+)
+
+micropython_gen_frozen = [
+  files('gen-frozen.sh'),
+  python3,
+  meson.current_source_dir(),
+]
+
+
 # Sources
 micropython_includes = include_directories(
   './micropython/',
diff --git a/lib/micropython/mpy-cross-wrapper.c b/lib/micropython/mpy-cross-wrapper.c
new file mode 100644
index 0000000000000000000000000000000000000000..6e333c2331aed1331970365b3d59edb16c217c82
--- /dev/null
+++ b/lib/micropython/mpy-cross-wrapper.c
@@ -0,0 +1,14 @@
+#include <stdio.h>
+#include <unistd.h>
+
+#ifndef MPY_CROSS_PATH
+#error "You need to define MPY_CROSS_PATH to compile this wrapper."
+#endif
+
+int main(int argc, char**argv)
+{
+	char path[] = MPY_CROSS_PATH "/mpy-cross";
+	argv[0] = "mpy-cross";
+	execv(path, argv);
+	fprintf(stderr, "Failed to run '%s'!\n", path);
+}
diff --git a/pycardium/meson.build b/pycardium/meson.build
index 0c839aaaa8b9fc783feb3ba37dea424632dd144c..833a9e0e0b8bf2878d4ae697de68c5186e1ce48c 100644
--- a/pycardium/meson.build
+++ b/pycardium/meson.build
@@ -36,6 +36,20 @@ qstr_h = custom_target(
 
 mp_headers = [version_h, modules_h, qstr_h]
 
+#################################
+#    Python Frozen Modules      #
+#################################
+
+subdir('modules/py')
+
+frozen_source = custom_target(
+  'frozen.c',
+  output: 'frozen.c',
+  input: [qstr_h, frozen_modules],
+  build_by_default: true,
+  command: [micropython_gen_frozen, '@OUTPUT@', '@INPUT@'],
+)
+
 ###################
 # MicroPython Lib #
 ###################
@@ -52,6 +66,7 @@ elf = executable(
   name + '.elf',
   'main.c',
   'mphalport.c',
+  frozen_source,
   modsrc,
   mp_headers,
   include_directories: micropython_includes,
diff --git a/pycardium/modules/py/foo.py b/pycardium/modules/py/foo.py
new file mode 100644
index 0000000000000000000000000000000000000000..e95c53d13b2a77c60eaadbc05a6b7ac951b85269
--- /dev/null
+++ b/pycardium/modules/py/foo.py
@@ -0,0 +1,8 @@
+# Foo Module
+
+
+def bar():
+    print("Hello from foo!")
+
+
+X = 3.14
diff --git a/pycardium/modules/py/meson.build b/pycardium/modules/py/meson.build
new file mode 100644
index 0000000000000000000000000000000000000000..29795f41a158ad5c43b4bfbd20f108763ac924a5
--- /dev/null
+++ b/pycardium/modules/py/meson.build
@@ -0,0 +1,5 @@
+python_modules = files(
+  'foo.py',
+)
+
+frozen_modules = mpy_cross.process(python_modules)
diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h
index c4e6a6cb0d86208d840d78e74b0fd1de40a3ae72..c4199e63e7e3f9f8a374b44e425d83b4ed84da74 100644
--- a/pycardium/mpconfigport.h
+++ b/pycardium/mpconfigport.h
@@ -4,16 +4,15 @@
 
 /* MicroPython Config Options */
 
-/*
- * Right now, we do not support importing external modules
- * though this might change in the future.
- */
-#define MICROPY_ENABLE_EXTERNAL_IMPORT      (0)
-
 /* We raise asynchronously from an interrupt handler */
 #define MICROPY_ASYNC_KBD_INTR              (1)
 #define MICROPY_KBD_EXCEPTION               (1)
 
+/* Enable precompiled frozen modules */
+#define MICROPY_MODULE_FROZEN_MPY           (1)
+#define MICROPY_MODULE_FROZEN               (1)
+#define MICROPY_QSTR_EXTRA_POOL             mp_qstr_frozen_const_pool
+
 #define MICROPY_ENABLE_DOC_STRING           (1)
 #define MICROPY_ENABLE_GC                   (1)
 #define MICROPY_FLOAT_IMPL                  (MICROPY_FLOAT_IMPL_FLOAT)
diff --git a/pycardium/mphalport.c b/pycardium/mphalport.c
index 9852fd85751feec7eadac27e59530039420f8b8a..3c5aa4458569ece114101e28993ec8b6b603a542 100644
--- a/pycardium/mphalport.c
+++ b/pycardium/mphalport.c
@@ -108,6 +108,11 @@ mp_lexer_t* mp_lexer_new_from_file(const char* filename)
 	mp_raise_OSError(MP_ENOENT);
 }
 
+mp_import_stat_t mp_import_stat(const char* path)
+{
+	return MP_IMPORT_STAT_NO_EXIST;
+}
+
 mp_obj_t mp_builtin_open(size_t n_args, const mp_obj_t* args, mp_map_t* kwargs)
 {
 	/* TODO: Once fs is implemented, get this working as well */