summaryrefslogtreecommitdiffstatshomepage
path: root/docs/develop/porting.rst
blob: 08f3557f1835ddc9e5e9d83408d3b236f835312c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
.. _porting_to_a_board:

Porting MicroPython
===================

The MicroPython project contains several ports to different microcontroller families and
architectures. The project repository has a `ports <https://github.com/micropython/micropython/tree/master/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 <https://github.com/micropython/micropython/tree/master/ports/minimal>`_ 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 "shared/runtime/gchelper.h"
   #include "shared/runtime/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 \
       shared/readline/readline.c \
       shared/runtime/gchelper_generic.c \
       shared/runtime/pyexec.c \
       shared/runtime/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 <stdint.h>

   // 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 <alloca.h>

   // 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 <unistd.h>
   #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
    >>>