diff --git a/docs/reference/cmodules.rst b/docs/reference/cmodules.rst
new file mode 100644
index 0000000000000000000000000000000000000000..f361af4a3821e71c3090a576a72f70240d0a76c1
--- /dev/null
+++ b/docs/reference/cmodules.rst
@@ -0,0 +1,86 @@
+Extending MicroPython with C
+============================
+
+Some specialized code would be unacceptably slow or needs to access hardware in
+a way that cannot be done from MicroPython. Therefore, it supports a way of
+extending the language with custom modules written in C. But before you consider
+writing a module in C, please take a look at :ref:`speed_python`.
+
+`Unlike CPython <https://docs.python.org/3/extending/building.html>`_, these
+modules are (currently) embedded directly in the program image instead of being
+dynamically loaded. This requires a `custom build of MicroPython
+<https://github.com/micropython/micropython/wiki/Getting-Started>`_.
+
+
+Writing a module
+----------------
+
+A module is a directory with the following files:
+
+  * ``micropython.mk``, which contains the Makefile fragment for this module.
+  * All C files you would like included.
+
+Put the required build commands in ``micropython.mk``. For a simple module, you
+will only have to add the file paths to ``SRC_MOD``, which will include these C
+files in the build:
+
+.. highlight:: make
+.. code::
+
+    # Add all C files to SRC_MOD.
+    SRC_MOD += $(USER_C_MODULES)/example/example.c
+
+This is a very bare bones module named ``example`` that provides
+``example.double(x)``. Note that the name of the module must be equal to the
+directory name and is also used in the name of the ``mp_obj_module_t`` object at
+the bottom.
+
+.. highlight:: c
+.. code::
+
+    // Include required definitions first.
+    #include "py/obj.h"
+    #include "py/runtime.h"
+
+    // This is the function you will call using example.double(n).
+    STATIC mp_obj_t example_double(mp_obj_t x_obj) {
+        // Check input value and convert it to a C type.
+        if (!MP_OBJ_IS_SMALL_INT(x_obj)) {
+            mp_raise_ValueError("x is not a small int");
+        }
+        int x = mp_obj_int_get_truncated(x_obj);
+
+        // Calculate the double, and convert back to MicroPython object.
+        return mp_obj_new_int(x + x);
+    }
+    STATIC MP_DEFINE_CONST_FUN_OBJ_1(example_double_obj, example_double);
+
+    // Define all properties of the example module, which currently are the name (a
+    // string) and a function.
+    // All identifiers and strings are written as MP_QSTR_xxx and will be
+    // optimized to word-sized integers by the build system (interned strings).
+    STATIC const mp_rom_map_elem_t example_module_globals_table[] = {
+        { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_example) },
+        { MP_ROM_QSTR(MP_QSTR_double), MP_ROM_PTR(&example_double_obj) },
+    };
+    STATIC MP_DEFINE_CONST_DICT(example_module_globals, example_module_globals_table);
+
+    // Define module object.
+    const mp_obj_module_t example_user_cmodule = {
+        .base = { &mp_type_module },
+        .globals = (mp_obj_dict_t*)&example_module_globals,
+    };
+
+
+Using a module
+--------------
+
+To build such a module, compile MicroPython (see `getting started
+<https://github.com/micropython/micropython/wiki/Getting-Started>`_) with an
+extra ``make`` flag named ``USER_C_MODULES`` set to the directory containing
+all modules you want included (not to the module itself!). For example:
+
+.. highlight:: shell
+.. code::
+
+    $ make USER_C_MODULES=path-to-modules-folder all
diff --git a/docs/reference/index.rst b/docs/reference/index.rst
index d0c7f69de9561e36ba7ebc513ac0453f92890b07..e2e08a7f72d9031cb7b876540448be98a3296669 100644
--- a/docs/reference/index.rst
+++ b/docs/reference/index.rst
@@ -26,3 +26,11 @@ implementation and the best practices to use them.
    constrained.rst
    packages.rst
    asm_thumb2_index.rst
+   cmodules.rst
+
+.. only:: port_pyboard
+
+   .. toctree::
+      :maxdepth: 1
+
+      asm_thumb2_index.rst
diff --git a/docs/reference/speed_python.rst b/docs/reference/speed_python.rst
index 4db60ec14d68f3312f212aab4c034cabefade5e9..c5aa80c6e18413a3b34876e237babca1edf776f9 100644
--- a/docs/reference/speed_python.rst
+++ b/docs/reference/speed_python.rst
@@ -1,3 +1,5 @@
+.. _speed_python:
+
 Maximising MicroPython Speed
 ============================
 
diff --git a/py/mkenv.mk b/py/mkenv.mk
index 87e92ec6f9d6b054fe4cc86148717230f335e96b..5f4b11b6be59d4e970fce38607ac5eb4afbfd00b 100644
--- a/py/mkenv.mk
+++ b/py/mkenv.mk
@@ -61,6 +61,7 @@ endif
 MAKE_FROZEN = $(PYTHON) $(TOP)/tools/make-frozen.py
 MPY_CROSS = $(TOP)/mpy-cross/mpy-cross
 MPY_TOOL = $(PYTHON) $(TOP)/tools/mpy-tool.py
+GEN_CMODULES = $(PYTHON) $(TOP)/tools/gen-cmodules.py
 
 all:
 .PHONY: all
diff --git a/py/mkrules.mk b/py/mkrules.mk
index 30ac520aa1cd390d0a0bb9f2c1e905cda86dc342..65d86834ef906782b676a766f775d6a4f2b6f460 100644
--- a/py/mkrules.mk
+++ b/py/mkrules.mk
@@ -105,7 +105,7 @@ endif
 ifneq ($(FROZEN_MPY_DIR),)
 # to build the MicroPython cross compiler
 $(TOP)/mpy-cross/mpy-cross: $(TOP)/py/*.[ch] $(TOP)/mpy-cross/*.[ch] $(TOP)/ports/windows/fmode.c
-	$(Q)$(MAKE) -C $(TOP)/mpy-cross
+	$(Q)$(MAKE) -C $(TOP)/mpy-cross USER_C_MODULES=
 
 # make a list of all the .py files that need compiling and freezing
 FROZEN_MPY_PY_FILES := $(shell find -L $(FROZEN_MPY_DIR) -type f -name '*.py' | $(SED) -e 's=^$(FROZEN_MPY_DIR)/==')
@@ -123,6 +123,13 @@ $(BUILD)/frozen_mpy.c: $(FROZEN_MPY_MPY_FILES) $(BUILD)/genhdr/qstrdefs.generate
 	$(Q)$(MPY_TOOL) -f -q $(BUILD)/genhdr/qstrdefs.preprocessed.h $(FROZEN_MPY_MPY_FILES) > $@
 endif
 
+# to build a list of modules for py/objmodule.c.
+ifneq ($(USER_C_MODULES),)
+$(BUILD)/genhdr/cmodules.h: | $(HEADER_BUILD)/mpversion.h
+	@$(ECHO) "GEN $@"
+	$(Q)$(GEN_CMODULES) $(USER_C_MODULES) > $@
+endif
+
 ifneq ($(PROG),)
 # Build a standalone executable (unix does this)
 
diff --git a/py/objmodule.c b/py/objmodule.c
index 9191c73ec3138142cbd60026225d71e5bbbd5106..04d210260d3b1a96bcba93df22e80e193a5a7bc1 100644
--- a/py/objmodule.c
+++ b/py/objmodule.c
@@ -31,8 +31,6 @@
 #include "py/runtime.h"
 #include "py/builtin.h"
 
-#include "genhdr/moduledefs.h"
-
 STATIC void module_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     (void)kind;
     mp_obj_module_t *self = MP_OBJ_TO_PTR(self_in);
diff --git a/py/py.mk b/py/py.mk
index 85e9072d7c5523617bcf07fbe719a04dd21797ff..ad7d122048e99ac120de7f0f58af448643f49098 100644
--- a/py/py.mk
+++ b/py/py.mk
@@ -129,6 +129,13 @@ $(BUILD)/$(BTREE_DIR)/%.o: CFLAGS += -Wno-old-style-definition -Wno-sign-compare
 $(BUILD)/extmod/modbtree.o: CFLAGS += $(BTREE_DEFS)
 endif
 
+# External modules written in C.
+ifneq ($(USER_C_MODULES),)
+CFLAGS_MOD += -DMICROPY_CMODULES_INCLUDE_H='"genhdr/cmodules.h"'
+include $(USER_C_MODULES)/*/micropython.mk
+SRC_QSTR += $(BUILD)/genhdr/cmodules.h
+endif
+
 # py object files
 PY_CORE_O_BASENAME = $(addprefix py/,\
 	mpstate.o \
@@ -300,7 +307,7 @@ endif
 
 # Sources that may contain qstrings
 SRC_QSTR_IGNORE = py/nlr%
-SRC_QSTR = $(SRC_MOD) $(filter-out $(SRC_QSTR_IGNORE),$(PY_CORE_O_BASENAME:.o=.c)) $(PY_EXTMOD_O_BASENAME:.o=.c)
+SRC_QSTR += $(SRC_MOD) $(filter-out $(SRC_QSTR_IGNORE),$(PY_CORE_O_BASENAME:.o=.c)) $(PY_EXTMOD_O_BASENAME:.o=.c)
 
 # Anything that depends on FORCE will be considered out-of-date
 FORCE:
diff --git a/tools/gen-cmodules.py b/tools/gen-cmodules.py
new file mode 100755
index 0000000000000000000000000000000000000000..524e3c03d3335c297a99bec1806f7dc2392bef97
--- /dev/null
+++ b/tools/gen-cmodules.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python
+
+# Generate genhdr/cmodules.h for inclusion in py/objmodule.c.
+
+from __future__ import print_function
+
+import sys
+import os
+from glob import glob
+
+def update_modules(path):
+    modules = []
+    for module in sorted(os.listdir(path)):
+        if not os.path.isfile('%s/%s/micropython.mk' % (path, module)):
+            continue # not a module
+        modules.append(module)
+
+    # Print header file for all external modules.
+    print('// Automatically generated by genmodules.py.\n')
+    for module in modules:
+        print('extern const struct _mp_obj_module_t %s_user_cmodule;' % module)
+    print('\n#define MICROPY_EXTRA_BUILTIN_MODULES \\')
+    for module in modules:
+        print('    { MP_ROM_QSTR(MP_QSTR_%s), MP_ROM_PTR(&%s_user_cmodule) }, \\' % (module, module))
+    print()
+
+if __name__ == '__main__':
+    update_modules(sys.argv[1])