.. _porting_to_a_board: Porting MicroPython =================== The MicroPython project contains several ports to different microcontroller families and architectures. The project repository has a `ports `_ directory containing a subdirectory for each supported port. A port will typically contain definitions for multiple "boards", each of which is a specific piece of hardware that that port can run on, e.g. a development kit or device. The `minimal port `_ is available as a simplified reference implementation of a MicroPython port. It can run on both the host system and an STM32F4xx MCU. In general, starting a port requires: - Setting up the toolchain (configuring Makefiles, etc). - Implementing boot configuration and CPU initialization. - Initialising basic drivers required for development and debugging (e.g. GPIO, UART). - Performing the board-specific configurations. - Implementing the port-specific modules. Minimal MicroPython firmware ---------------------------- The best way to start porting MicroPython to a new board is by integrating a minimal MicroPython interpreter. For this walkthrough, create a subdirectory for the new port in the ``ports`` directory: .. code-block:: bash $ cd ports $ mkdir example_port The basic MicroPython firmware is implemented in the main port file, e.g ``main.c``: .. code-block:: c #include "py/compile.h" #include "py/gc.h" #include "py/mperrno.h" #include "py/stackctrl.h" #include "lib/utils/gchelper.h" #include "lib/utils/pyexec.h" // Allocate memory for the MicroPython GC heap. static char heap[4096]; int main(int argc, char **argv) { // Initialise the MicroPython runtime. mp_stack_ctrl_init(); gc_init(heap, heap + sizeof(heap)); mp_init(); mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_path), 0); mp_obj_list_init(MP_OBJ_TO_PTR(mp_sys_argv), 0); // Start a normal REPL; will exit when ctrl-D is entered on a blank line. pyexec_friendly_repl(); // Deinitialise the runtime. gc_sweep_all(); mp_deinit(); return 0; } // Handle uncaught exceptions (should never be reached in a correct C implementation). void nlr_jump_fail(void *val) { for (;;) { } } // Do a garbage collection cycle. void gc_collect(void) { gc_collect_start(); gc_helper_collect_regs_and_stack(); gc_collect_end(); } // There is no filesystem so stat'ing returns nothing. mp_import_stat_t mp_import_stat(const char *path) { return MP_IMPORT_STAT_NO_EXIST; } // There is no filesystem so opening a file raises an exception. mp_lexer_t *mp_lexer_new_from_file(const char *filename) { mp_raise_OSError(MP_ENOENT); } We also need a Makefile at this point for the port: .. code-block:: Makefile # Include the core environment definitions; this will set $(TOP). include ../../py/mkenv.mk # Include py core make definitions. include $(TOP)/py/py.mk # Set CFLAGS and libraries. CFLAGS = -I. -I$(BUILD) -I$(TOP) LIBS = -lm # Define the required source files. SRC_C = \ main.c \ mphalport.c \ lib/mp-readline/readline.c \ lib/utils/gchelper_generic.c \ lib/utils/pyexec.c \ lib/utils/stdout_helpers.c \ # Define the required object files. OBJ = $(PY_CORE_O) $(addprefix $(BUILD)/, $(SRC_C:.c=.o)) # Define the top-level target, the main firmware. all: $(BUILD)/firmware.elf # Define how to build the firmware. $(BUILD)/firmware.elf: $(OBJ) $(ECHO) "LINK $@" $(Q)$(CC) $(LDFLAGS) -o $@ $^ $(LIBS) $(Q)$(SIZE) $@ # Include remaining core make rules. include $(TOP)/py/mkrules.mk Remember to use proper tabs to indent the Makefile. MicroPython Configurations -------------------------- After integrating the minimal code above, the next step is to create the MicroPython configuration files for the port. The compile-time configurations are specified in ``mpconfigport.h`` and additional hardware-abstraction functions, such as time keeping, in ``mphalport.h``. The following is an example of an ``mpconfigport.h`` file: .. code-block:: c #include // Python internal features. #define MICROPY_ENABLE_GC (1) #define MICROPY_HELPER_REPL (1) #define MICROPY_ERROR_REPORTING (MICROPY_ERROR_REPORTING_TERSE) #define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_FLOAT) // Fine control over Python builtins, classes, modules, etc. #define MICROPY_PY_ASYNC_AWAIT (0) #define MICROPY_PY_BUILTINS_SET (0) #define MICROPY_PY_ATTRTUPLE (0) #define MICROPY_PY_COLLECTIONS (0) #define MICROPY_PY_MATH (0) #define MICROPY_PY_IO (0) #define MICROPY_PY_STRUCT (0) // Type definitions for the specific machine. typedef intptr_t mp_int_t; // must be pointer size typedef uintptr_t mp_uint_t; // must be pointer size typedef long mp_off_t; // We need to provide a declaration/definition of alloca(). #include // Define the port's name and hardware. #define MICROPY_HW_BOARD_NAME "example-board" #define MICROPY_HW_MCU_NAME "unknown-cpu" #define MP_STATE_PORT MP_STATE_VM #define MICROPY_PORT_ROOT_POINTERS \ const char *readline_hist[8]; This configuration file contains machine-specific configurations including aspects like if different MicroPython features should be enabled e.g. ``#define MICROPY_ENABLE_GC (1)``. Making this Setting ``(0)`` disables the feature. Other configurations include type definitions, root pointers, board name, microcontroller name etc. Similarly, an minimal example ``mphalport.h`` file looks like this: .. code-block:: c static inline void mp_hal_set_interrupt_char(char c) {} Support for standard input/output --------------------------------- MicroPython requires at least a way to output characters, and to have a REPL it also requires a way to input characters. Functions for this can be implemented in the file ``mphalport.c``, for example: .. code-block:: c #include #include "py/mpconfig.h" // Receive single character, blocking until one is available. int mp_hal_stdin_rx_chr(void) { unsigned char c = 0; int r = read(STDIN_FILENO, &c, 1); (void)r; return c; } // Send the string of given length. void mp_hal_stdout_tx_strn(const char *str, mp_uint_t len) { int r = write(STDOUT_FILENO, str, len); (void)r; } These input and output functions have to be modified depending on the specific board API. This example uses the standard input/output stream. Building and running -------------------- At this stage the directory of the new port should contain:: ports/example_port/ ├── main.c ├── Makefile ├── mpconfigport.h ├── mphalport.c └── mphalport.h The port can now be built by running ``make`` (or otherwise, depending on your system). If you are using the default compiler settings in the Makefile given above then this will create an executable called ``build/firmware.elf`` which can be executed directly. To get a functional REPL you may need to first configure the terminal to raw mode: .. code-block:: bash $ stty raw opost -echo $ ./build/firmware.elf That should give a MicroPython REPL. You can then run commands like: .. code-block:: bash MicroPython v1.13 on 2021-01-01; example-board with unknown-cpu >>> import usys >>> usys.implementation ('micropython', (1, 13, 0)) >>> Use Ctrl-D to exit, and then run ``reset`` to reset the terminal. Adding a module to the port --------------------------- To add a custom module like ``myport``, first add the module definition in a file ``modmyport.c``: .. code-block:: c #include "py/runtime.h" STATIC mp_obj_t myport_info(void) { mp_printf(&mp_plat_print, "info about my port\n"); return mp_const_none; } STATIC MP_DEFINE_CONST_FUN_OBJ_0(myport_info_obj, myport_info); STATIC const mp_rom_map_elem_t myport_module_globals_table[] = { { MP_OBJ_NEW_QSTR(MP_QSTR___name__), MP_OBJ_NEW_QSTR(MP_QSTR_myport) }, { MP_ROM_QSTR(MP_QSTR_info), MP_ROM_PTR(&myport_info_obj) }, }; STATIC MP_DEFINE_CONST_DICT(myport_module_globals, myport_module_globals_table); const mp_obj_module_t myport_module = { .base = { &mp_type_module }, .globals = (mp_obj_dict_t *)&myport_module_globals, }; MP_REGISTER_MODULE(MP_QSTR_myport, myport_module, 1); Note: the "1" as the third argument in ``MP_REGISTER_MODULE`` enables this new module unconditionally. To allow it to be conditionally enabled, replace the "1" by ``MICROPY_PY_MYPORT`` and then add ``#define MICROPY_PY_MYPORT (1)`` in ``mpconfigport.h`` accordingly. You will also need to edit the Makefile to add ``modmyport.c`` to the ``SRC_C`` list, and a new line adding the same file to ``SRC_QSTR`` (so qstrs are searched for in this new file), like this: .. code-block:: Makefile SRC_C = \ main.c \ modmyport.c \ mphalport.c \ ... SRC_QSTR += modport.c If all went correctly then, after rebuilding, you should be able to import the new module: .. code-block:: bash >>> import myport >>> myport.info() info about my port >>>