Porting MicroPython to MCU: A Guide Using MDK+CORTEX-M

1. Introduction

MicroPython is a compact and efficient implementation of Python 3, containing a subset of the optimized Python standard library that can run on microcontrollers and other resource-constrained platforms. MicroPython also supports several advanced features, such as interactive prompts, arbitrary precision integers, closures, list comprehensions, generators, exception handling, etc. Its implementation is very small, capable of running on platforms with only 256k of code space and 16k of memory, making it especially suitable for porting to MCUs.

The design of MicroPython aims to be as compatible as possible with standard Python, allowing easy transfer of code from desktop platforms to run on microcontrollers or embedded systems. Personally, I believe that porting MicroPython to one’s own platform for automated validation testing is a very good application. After setting up the MicroPython environment, colleagues in verification and testing can directly write Python scripts for validation tests without needing software engineers’ full cooperation, thus improving collaboration efficiency.

The official provides various adaptations for pyboard, which can directly compile corresponding firmware for use.

2. Obtaining the Source Code

Download the source code git clone https://github.com/micropython/micropython.git

Porting MicroPython to MCU: A Guide Using MDK+CORTEX-M

MicroPython has its own build method, which defaults to adapting to some platforms. A more general approach is to use its source code to port it to our own projects.

To understand the code framework, you can first build it on an officially supported platform to learn about the build process, which will help you know which source codes need to be compiled and what processes are involved.

You can directly use vs to open micropython\mpy-cross\mpy-cross.vcxproj to compile the compiler.

Then open micropython\ports\windows\micropython.vcxproj to compile the interpreter.

Understand its project architecture. Note that before building, you need to cancel the read-only attributes of all files.

2. Add Source Code

We will port a minimal implementation, temporarily not adding external modules, only using some built-in modules. The overall code framework is as follows

Porting MicroPython to MCU: A Guide Using MDK+CORTEX-M

Py Source Code

The py directory contains the core code of the MicroPython interpreter, which we will copy to our own project directory.

Copy all c files under it to our own project.

And add the py path to the header file include path.

The following files should be selected according to the platform; here I choose asmthumb.c for the ARM CORTEX-M series, and the others are not needed.

asmarm.c

asmrv32.c

asmthumb.c

asmx64.c

asmx86.c

asmxtensa.c

Shared Source Code

Temporarily only add the following content; others can be added as needed later.

Shared/runtime/pyexec.c pyexec.h

Shared/readline/readline.c readline.h

extmod Source CodeSource Code

Temporarily only add the following content; others can be added as needed later.

modplatform.h

virtpin.h

py_port Source Code

Then create a porting folder py_port. We reference micropython\ports\minimal for porting.

Copy the files from micropython\ports\minimal to our py_port path.

Add py_port to our project directory.

The files are as follows, where uart_core.c implements the serial input/output interface, mpconfigport.h is the configuration file, mphalport.h is some HAL layer interface implementation, py_main is the interpreter entry point, and qstrdefsport.h is for qstrs specific to this port.

The unistd.h is a substitute for a Unix-like platform.

Porting MicroPython to MCU: A Guide Using MDK+CORTEX-M

Py_main.c

Change py_port/main.c to py_port/py_main.c.

It is implemented based on stm32 or pc host, and we need to make some modifications according to our platform.

Remove the content of #if MICROPY_MIN_USE_STM32_MCU and #if MICROPY_MIN_USE_CORTEX_CPU because we are using our platform.

The main function name should be changed to py_main.

Then call py_main at the appropriate place in your platform.

Ensure it can compile first, and we will look at how to implement py_main later.

Uart_core.c

We need to implement the following interfaces, which we will look at later.

#include "py/mpconfig.h" /* * Core UART functions to implement for a port */ // Receive single characterint mp_hal_stdin_rx_chr(void) {    unsigned char c = 0;     return c;} // Send string of given lengthmp_uint_t mp_hal_stdout_tx_strn(const char *str, mp_uint_t len) {    mp_uint_t ret = len;     return ret;} void mp_hal_stdout_tx_strn_cooked(const char *str, size_t len){} void mp_hal_stdout_tx_str(const char *str){}

Unistd.h

Defines ssize_t and some other macros.

#ifndef MICROPY_INCLUDED_UNISTD_H#define MICROPY_INCLUDED_UNISTD_H typedef int ssize_t;#define F_OK 0#define W_OK 2#define R_OK 4 #define STDIN_FILENO  0#define STDOUT_FILENO 1#define STDERR_FILENO 2 #define SEEK_CUR 1#define SEEK_END 2#define SEEK_SET 0 #endif 

mphalport.h

HAL layer interface

static inline mp_uint_t mp_hal_ticks_ms(void) {    return 0;}static inline void mp_hal_set_interrupt_char(char c) {}

qstrdefsport.h

// qstrs specific to this port// *FORMAT-OFF*

mpconfigport.h

Configuration file, which is very important. Pay attention to whether the MICROPY_PY_XXX macros are enabled for certain modules.

#include <stdint.h> // options to control how MicroPython is built // Use the minimal starting configuration (disables all optional features).#define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_MINIMUM) // You can disable the built-in MicroPython compiler by setting the following// config option to 0.  If you do this then you won't get a REPL prompt, but you// will still be able to execute pre-compiled scripts, compiled with mpy-cross.#define MICROPY_ENABLE_COMPILER     (1) //#define MICROPY_QSTR_EXTRA_POOL           mp_qstr_frozen_const_pool#define MICROPY_HELPER_REPL               (1)#define MICROPY_MODULE_FROZEN_MPY         (0)#define MICROPY_ENABLE_EXTERNAL_IMPORT    (0) #define MICROPY_PY_IO                     (0)#define MICROPY_PY_IO_IOBASE              (1)#define MICROPY_PY_ERRNO                  (1)#define MICROPY_PY_MATH                   (1)#define MICROPY_PY_CMATH                  (1)#define MICROPY_PY_BUILTINS_FLOAT         (1)#define MICROPY_PY_BUILTINS_COMPLEX       (1) #define MICROPY_PY_GC                     (1)#define MICROPY_ENABLE_GC                 (1)#define MICROPY_PY_SYS                    (1)#define MICROPY_PY_MICROPYTHON            (1)#define MICROPY_USE_INTERNAL_PRINTF       (0)#define MICROPY_PY_ARRAY                  (1)#define MICROPY_PY_BUILTINS_BYTEARRAY     (1)#define MICROPY_PY_BUILTINS_MEMORYVIEW    (1)#define MICROPY_FLOAT_IMPL                (MICROPY_FLOAT_IMPL_DOUBLE)#define MICROPY_ALLOC_PATH_MAX            (256) // Use the minimum headroom in the chunk allocator for parse nodes.#define MICROPY_ALLOC_PARSE_CHUNK_INIT    (16) // type definitions for the specific machine typedef intptr_t mp_int_t; // must be pointer sizetypedef uintptr_t mp_uint_t; // must be pointer sizetypedef long mp_off_t; // We need to provide a declaration/definition of alloca()#include <alloca.h> #define MICROPY_HW_BOARD_NAME "minimal"#define MICROPY_HW_MCU_NAME "unknown-cpu" #define MICROPY_HEAP_SIZE      (2048) // heap size 2 kilobytes #define MP_STATE_PORT MP_STATE_VM#define MICROPY_USE_INTERNAL_ERRNO 1

4. Porting

Standard Library Dependencies

The standard libraries required are as follows:

#include <stdlib.h>

#include <assert.h>

#include <stdio.h>

#include <string.h>

#include <stdint.h>

#include <stddef.h>

#include <stdbool.h>

#include <stdarg.h>

#include <math.h>

#include <limits.h>

#include <unistd.h> which we implement ourselves in py_port.

If there is no #include <errno.h>, then configure MICROPY_USE_INTERNAL_ERRNO to be 1.

#include <alloca.h> This is supported by the compiler.

Generating genhdr

We can build from other already built sources or use Windows samples to build first, then copy the corresponding header files.

moduledefs.h defines some module information. Since we are not using the official build process, it may not be automatically updated, so we can manually modify and reduce certain modules. Later we will manually call the corresponding scripts in MDK’s project configuration at the appropriate time.

mpversion.hqstrdefs.generated.hroot_pointers.h

The generation process refers to ports\windows\msvc\genhdr.targets.

which contains commands like:

<Exec Command=”type $(QstrDefsCollected) >> $(DestDir)qstrdefs.preprocessed.h”/>

<Exec Command=”$(PyPython) $(PySrcDir)makeqstrdata.py $(DestDir)qstrdefs.preprocessed.h > $(TmpFile)”/>

where the specified file is qstrdefscollected.h.

<QstrDefsCollected>$(DestDir)qstrdefscollected.h</QstrDefsCollected>

This file is generated by the following command:

<Exec Command=”$(PyPython) $(PySrcDir)makeqstrdefs.py split qstr $(DestDir)qstr.i.last $(DestDir)qstr _”/>

<Exec Command=”$(PyPython) $(PySrcDir)makeqstrdefs.py cat qstr _ $(DestDir)qstr $(QstrDefsCollected)”/>

Corresponding to the commands below:

makeqstrdata.py is located in the py directory. Open the command line here:

First execute:

python makeqstrdefs.py split qstr qstr.i.last qstr _

python makeqstrdefs.py cat qstr _ qstr qstrdefscollected.h

Then execute:

type qstrdefscollected.h >> qstrdefs.preprocessed.h

Then execute:

python makeqstrdata.py qstrdefs.preprocessed.h >qstrdefs.generated.h.tmp

Adapting Serial Ports

In py_port/uart_core.c, remove:

#include <unistd.h>

and add our own serial interface:

#include “uart.h”

Remove:

#if MICROPY_MIN_USE_STM32_MCU

#endif

#if MICROPY_MIN_USE_STDOUT

#elif MICROPY_MIN_USE_STM32_MCU

#endif

the content, which implements blocking read of one character mp_hal_stdin_rx_chr and blocking send string interface mp_hal_stdout_tx_strn, mp_hal_stdout_tx_strn_cooked, mp_hal_stdout_tx_str. The serial implementation can refer to the article, which adopts FIFO.

https://mp.weixin.qq.com/s/vzjWu2LxpVGZw-msCooh8Q?token=1312261758&lang=zh_CN Super Simplified Series 16: Serial Driver Based on IO Simulation + FIFO:

Uart.c is as follows:

#include <stdio.h>#include <stdbool.h>#include "uart.h"#include "fifo.h"#include <stdio.h> #include "fr30xx.h" #define CriticalAlloc()#define EnterCritical()                              __disable_irq()#define ExitCritical()                              __enable_irq() static uint8_t s_uart_rx_buffer[64]; static fifo_st s_uart_fifo_dev={ .in = 0, .len = 0, .out = 0, .buffer = s_uart_rx_buffer, .buffer_len = sizeof(s_uart_rx_buffer),}; volatile bool g_data_transmit_flag = false;uint8_t rx_buffer[1];  void uart_rx_cb(uint8_t* buff, uint32_t len){ fifo_in(&amp;s_uart_fifo_dev, buff, len);}   uint32_t uart_send(uint8_t* buffer, uint32_t len){ g_data_transmit_flag = false; for(uint32_t i=0;i&lt;len;i++) { putchar(buffer[i]); } return len;} uint32_t uart_read(uint8_t* buffer, uint32_t len){ uint32_t rlen; CriticalAlloc(); EnterCritical(); rlen = fifo_out(&amp;s_uart_fifo_dev, buffer, len); ExitCritical(); return rlen;}
uart.h is as follows:
#ifndef UART_H#define UART_H #ifdef __cplusplusextern "C" {#endif  #include <stdint.h> uint32_t uart_send(uint8_t* buffer, uint32_t len);uint32_t uart_read(uint8_t* buffer, uint32_t len);void uart_rx_cb(uint8_t* buff, uint32_t len); #ifdef __cplusplus}#endif #endif
fifo.c is as follows:
#include <string.h>#include "fifo.h" #define FIFO_PARAM_CHECK 0 /** * in????? 0~(buffer_len-1)? * out????? 0~(buffer_len-1)? * in == out?????,?????,????len?????????? * ???in??,?????out??? * ????out??,?????in??? * in??out??[out,in)???????? * in??out??[out,buffer_len)?[0,in)???????? *********************************************************** *     0                                 buffer_len-1 buffer_len *     (1)?? in?out??0 *     |                                             | *     in(0) *     out(0) *     len = 0 *     (2)??n???? in??n?out??0 ??in??out??? *     |                                             | *     out(0)————————————&gt;in(n)                      |    *     len = n *     (3)??m????(m&lt;n) in??n?out??m ??in??out??? *     |                                             | *             out(m)————&gt;in(n) *     len = n-m *     (4)??????,?????,??in??out??? *     |                                             | *             out(m)————————————————————————————————&gt; *     ——&gt;in(k) *     len = k + buffer_len-m */uint32_t fifo_in(fifo_st* dev, uint8_t* buffer, uint32_t len){  uint32_t space = 0;  /* ?????????? */  /* ???? */  #if FIFO_PARAM_CHECK  if((dev == 0) || (buffer == 0) || (len == 0))  {    return 0;  }  if(dev-&gt;buffer == 0)  {    return 0;  }  #endif   /* ??len??????buffer?? */  if(len &gt; dev-&gt;buffer_len)  {    len = dev-&gt;buffer_len;  }   /* ????????    * ??dev-&gt;len?????dev-&gt;buffer_len   */  if(dev-&gt;buffer_len &gt;= dev-&gt;len)  {    space = dev-&gt;buffer_len - dev-&gt;len;   }  else  {    /* ???????, ?????? */    dev-&gt;len = 0;    space = dev-&gt;buffer_len;   }   /* ???????, ??len???????????????? */  len = (len &gt;= space) ? space : len;    if(len == 0)  {    return 0; /* ??????????,???? */  }   /* ??len??????????,?????? */  space = dev-&gt;buffer_len - dev-&gt;in; /* ??????in???????????? */  if(space &gt;= len)  {    /* ??????in??????????? */    memcpy(dev-&gt;buffer+dev-&gt;in,buffer,len);  }  else  {    /* ??????in???????,????????? */    memcpy(dev-&gt;buffer+dev-&gt;in,buffer,space);    /* ???tail??  */    memcpy(dev-&gt;buffer,buffer+space,len-space);  /* ???????? */  }   /* ????????????? */  dev-&gt;in += len;  if(dev-&gt;in &gt;= dev-&gt;buffer_len)  {    dev-&gt;in -= dev-&gt;buffer_len;  /* ????? ?? dev-&gt;in %= dev-&gt;buffer-&gt;len */  }  dev-&gt;len += len;  /* dev-&gt;len??dev-&gt;buffer-&gt;len,??%= dev-&gt;buffer-&gt;len */  return len;} uint32_t fifo_out(fifo_st* dev, uint8_t* buffer, uint32_t len){  uint32_t space = 0;   /* ???? */  #if FIFO_PARAM_CHECK  if((dev == 0) || (buffer == 0) || (len == 0))  {    return 0;  }  if(dev-&gt;buffer == 0)  {    return 0;  }  #endif    /* ??????? */  if(dev-&gt;len == 0)  {    return 0;  }   /* ?????????????????? */  len = (dev-&gt;len) &gt; len ? len : dev-&gt;len;   /* ??len??????????,?????? */  space = dev-&gt;buffer_len - dev-&gt;out; /* ??????out???????????? */  if(space &gt;= len)  {    /* ??????out??????????? */    memcpy(buffer,dev-&gt;buffer+dev-&gt;out,len);  }  else  {    /* ??????out???????,????????? */    memcpy(buffer,dev-&gt;buffer+dev-&gt;out,space);    /* ???tail??  */    memcpy(buffer+space,dev-&gt;buffer,len-space);   /* ???????? */  }   /* ????????????? */  dev-&gt;out += len;  if(dev-&gt;out &gt;= dev-&gt;buffer_len)  {    dev-&gt;out -= dev-&gt;buffer_len;  /* ????? ?? dev-&gt;out %= dev-&gt;buffer-&gt;len */  }  dev-&gt;len -= len;   /* ??dev-&gt;len ?????len,???? */  return len;} uint32_t fifo_getlen(fifo_st* dev){  #if FIFO_PARAM_CHECK  if(dev == 0)  {    return 0;  }  #endif  return dev-&gt;len;} void fifo_clean(fifo_st* dev){  #if FIFO_PARAM_CHECK  if(dev == 0)  {    return 0;  }  #endif  dev-&gt;len = 0;  dev-&gt;in = 0;  dev-&gt;out = 0;}
fifo.h is as follows:
#ifndef FIFO_H#define FIFO_H #ifdef __cplusplusextern "C" {#endif  #include &lt;stdint.h&gt; /** * 	ruct fifo_st * FIFO?????. */typedef struct {  uint32_t in;          /**&lt; ????        */   uint32_t out;         /**&lt; ????        */   uint32_t len;         /**&lt; ??????    */   uint32_t buffer_len;  /**&lt; ????        */   uint8_t* buffer;      /**&lt; ??,????   */ } fifo_st;  /** * n fifo_in * ?fifo???? * 	ypedef[in] dev ef fifo_st * 	ypedef[in] buffer ?????? * 	ypedef[in] len ?????? * etval ?????????? */uint32_t fifo_in(fifo_st* dev, uint8_t* buffer, uint32_t len); /** * n fifo_out * ?fifo???? * 	ypedef[in] dev ef fifo_st * 	ypedef[in] buffer ?????? * 	ypedef[in] len ????????? * etval ?????????? */uint32_t fifo_out(fifo_st* dev, uint8_t* buffer, uint32_t len);  uint32_t fifo_getlen(fifo_st* dev); void fifo_clean(fifo_st* dev);#ifdef __cplusplus}#endif #endif

The final uart_core.c implementation is as follows:

#include <string.h>#include "py/mpconfig.h" /* * Core UART functions to implement for a port */ // Receive single characterint mp_hal_stdin_rx_chr(void) {    unsigned char c = 0; while(0 == uart_read(&amp;c,1)) { }; return c;} // Send string of given lengthmp_uint_t mp_hal_stdout_tx_strn(const char *str, mp_uint_t len) {    mp_uint_t ret = len; uart_send((uint8_t*)str,len); return ret;} void mp_hal_stdout_tx_strn_cooked(const char *str, size_t len){ uart_send((uint8_t*)str,len);} void mp_hal_stdout_tx_str(const char *str){ uart_send((uint8_t*)str,strlen(str));}

Py_main Entry

In py_main.c:

Set MICROPY_ENABLE_GC to 0.

Set MICROPY_ENABLE_COMPILER to 1.

Set MICROPY_REPL_EVENT_DRIVEN to 0.

The execution process is as follows:

mp_init();->pyexec_friendly_repl();

int py_main(int argc, char **argv) {    int stack_dummy;    stack_top = (char *)&amp;stack_dummy;     #if MICROPY_ENABLE_GC    gc_init(heap, heap + sizeof(heap));    #endif    mp_init();    #if MICROPY_ENABLE_COMPILER    #if MICROPY_REPL_EVENT_DRIVEN    pyexec_event_repl_init();    for (;;) {        int c = mp_hal_stdin_rx_chr();        if (pyexec_event_repl_process_char(c)) {            break;        }    }    #else    pyexec_friendly_repl();    #endif    // do_str("print('hello world!', list(x+1 for x in range(10)), end='eol\n')", MP_PARSE_SINGLE_INPUT);    // do_str("for i in range(10):\r\n  print(i)", MP_PARSE_FILE_INPUT);    #else    pyexec_frozen_module("frozentest.py", false);    #endif    mp_deinit();    return 0;} 

Create a thread to run the py_main function.

5. Testing

After running, we see the following output:

Porting MicroPython to MCU: A Guide Using MDK+CORTEX-M

Perform a simple calculation:

Porting MicroPython to MCU: A Guide Using MDK+CORTEX-M

Using alloca.h to allocate resources from the stack, so be careful to configure the stack size larger.

6. Summary

Some online materials can at most be considered as building MicroPython, but not porting. Here, I share the complete process of porting MicroPython source code to our own project from scratch. This can help understand what files are necessary for a minimal MicroPython port and its basic architecture. Later, we will introduce the addition of external modules, which is the most substantial work of porting, and to make it practical, it is essential to port various platform-related resource modules.

Leave a Comment