diff --git a/examples/micropython/Makefile b/examples/micropython/Makefile new file mode 100644 index 000000000000..1bd38786ead7 --- /dev/null +++ b/examples/micropython/Makefile @@ -0,0 +1,30 @@ +# name of your application +APPLICATION = micropython + +# If no BOARD is found in the environment, use this default: +BOARD ?= native + +# This has to be the absolute path to the RIOT base directory: +RIOTBASE ?= $(CURDIR)/../.. + +# select MicroPython package +USEPKG += micropython + +# include boot.py as header +BLOBS += boot.py + +# configure MicroPython's heap size +MP_RIOT_HEAPSIZE ?= 8192U + +# MicroPython needs a larger stack +CFLAGS += '-DTHREAD_STACKSIZE_MAIN=THREAD_STACKSIZE_DEFAULT*4' + +# use miniterm (instead of the default pyterm) in order to support control +# characters (CTRL-D ...) +RIOT_TERMINAL ?= miniterm + +# enable modmachine support for peripherals if available +FEATURES_OPTIONAL += periph_adc +FEATURES_OPTIONAL += periph_spi + +include $(RIOTBASE)/Makefile.include diff --git a/examples/micropython/Makefile.ci b/examples/micropython/Makefile.ci new file mode 100644 index 000000000000..8f89fbf3203c --- /dev/null +++ b/examples/micropython/Makefile.ci @@ -0,0 +1,25 @@ +BOARD_INSUFFICIENT_MEMORY := \ + blackpill \ + bluepill \ + calliope-mini \ + i-nucleo-lrwan1 \ + microbit \ + nrf51dongle \ + nucleo-f030r8 \ + nucleo-f031k6 \ + nucleo-f042k6 \ + nucleo-f070rb \ + nucleo-f072rb \ + nucleo-f302r8 \ + nucleo-f303k8 \ + nucleo-f334r8 \ + nucleo-l031k6 \ + nucleo-l053r8 \ + opencm904 \ + saml10-xpro \ + saml11-xpro \ + stm32f0discovery \ + spark-core \ + stm32f030f4-demo \ + stm32l0538-disco \ + # diff --git a/examples/micropython/README.md b/examples/micropython/README.md new file mode 100644 index 000000000000..56f5e68cfac1 --- /dev/null +++ b/examples/micropython/README.md @@ -0,0 +1,6 @@ +# Overview + +WARNING: RIOT's MicroPython port is currently quite incomplete! + +This application provides an example on how to use MicroPython with RIOT. +Please see the documentation of pkg/micropython. diff --git a/examples/micropython/boot.py b/examples/micropython/boot.py new file mode 100644 index 000000000000..3f3c50a4c83f --- /dev/null +++ b/examples/micropython/boot.py @@ -0,0 +1 @@ +print("boot.py: MicroPython says hello!") diff --git a/examples/micropython/main.c b/examples/micropython/main.c new file mode 100644 index 000000000000..be5b7c00d235 --- /dev/null +++ b/examples/micropython/main.c @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2019 Kaspar Schleiser + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup examples + * @{ + * + * @file + * @brief micropython example application + * + * @author Kaspar Schleiser + * + * @} + */ + +#include + +#include "thread.h" + +#include "micropython.h" +#include "py/stackctrl.h" +#include "lib/utils/pyexec.h" + +#include "blob/boot.py.h" + +static char mp_heap[MP_RIOT_HEAPSIZE]; + +int main(void) +{ + int coldboot = 1; + + /* let MicroPython know the top of this thread's stack */ + uint32_t stack_dummy; + mp_stack_set_top((char*)&stack_dummy); + + /* Make MicroPython's stack limit somewhat smaller than actual stack limit */ + mp_stack_set_limit(THREAD_STACKSIZE_MAIN - MP_STACK_SAFEAREA); + + while (1) { + /* configure MicroPython's heap */ + mp_riot_init(mp_heap, sizeof(mp_heap)); + + /* execute boot.py + * + * MicroPython's test suite gets confused by extra output, so only do + * this the first time after the node boots up, not on following soft + * reboots. + */ + if (coldboot) { + puts("-- Executing boot.py"); + mp_do_str((const char *)boot_py, boot_py_len); + puts("-- boot.py exited. Starting REPL.."); + coldboot = 0; + } + + /* loop over REPL input */ + while (1) { + if (pyexec_mode_kind == PYEXEC_MODE_RAW_REPL) { + if (pyexec_raw_repl() != 0) { + break; + } + } else { + if (pyexec_friendly_repl() != 0) { + break; + } + } + } + puts("soft reboot"); + } + + return 0; +} diff --git a/examples/micropython/tests/01-run.py b/examples/micropython/tests/01-run.py new file mode 100755 index 000000000000..067029d74a7b --- /dev/null +++ b/examples/micropython/tests/01-run.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +import sys +from testrunner import run + + +def testfunc(child): + def get_time(): + child.sendline('utime.time()') + child.readline() + res = int(child.readline().rstrip()) + child.expect_exact('>>>') + return res + + child.expect_exact('boot.py: MicroPython says hello!') + child.expect_exact('>>>') + + child.sendline('print("echo this! " * 4)') + child.expect_exact('echo this! echo this! echo this! echo this!') + child.expect_exact('>>>') + + # test riot.thread_getpid() + child.sendline('import riot') + child.sendline('print(riot.thread_getpid())') + child.expect_exact('2') + child.expect_exact('>>>') + + # + # test xtimer integration + # + + child.sendline('import utime') + child.expect_exact('>>>') + + # testing timing over serial using the REPL is very inaccurate, thus + # we allow a *large* overshoot (100 by default). + def test_sleep(t, slack=100): + before = get_time() + child.sendline('utime.sleep_ms(%s)' % t) + child.expect_exact('>>>') + duration = get_time() - before + print("test_sleep(%s, %s): slept %sms" % (t, slack, duration)) + assert duration > t and duration < (t + slack) + return duration + + # get overhead from sleeping 0ms, add 10 percent + slack = int(test_sleep(0, 1000) * 1.1) + + test_sleep(50, slack) + test_sleep(250, slack) + test_sleep(500, slack) + + # test setting timers + child.sendline('import xtimer') + child.expect_exact('>>>') + child.sendline('a = 0') + child.expect_exact('>>>') + + child.sendline('def inc_a(): global a; a+=1') + child.expect_exact('...') + child.sendline('') + child.expect_exact('>>>') + + child.sendline('t = xtimer.xtimer(inc_a)') + child.expect_exact('>>>') + + before = get_time() + + child.sendline('t.set(500000)') + child.expect_exact('>>>') + + child.sendline('while a==0: pass') + child.expect_exact('...') + child.sendline('') + child.expect_exact('>>>') + + duration = get_time() - before + assert duration > 500 + + print("[TEST PASSED]") + + +if __name__ == "__main__": + sys.exit(run(testfunc)) diff --git a/pkg/micropython/Makefile b/pkg/micropython/Makefile new file mode 100644 index 000000000000..f29342605e14 --- /dev/null +++ b/pkg/micropython/Makefile @@ -0,0 +1,14 @@ +PKG_NAME=micropython +PKG_URL=https://github.com/kaspar030/micropython +PKG_VERSION=5c45688d431a4d0f626d86478ad490cfb6d8ac30 +PKG_LICENSE=MIT + +CFLAGS += -Wno-implicit-fallthrough -Wno-unused-parameter -Wno-error + +.PHONY: all + +all: + @mkdir -p $(PKG_BUILDDIR)/tmp + BUILD=$(PKG_BUILDDIR) "$(MAKE)" -C $(PKG_BUILDDIR)/ports/riot + +include $(RIOTBASE)/pkg/pkg.mk diff --git a/pkg/micropython/Makefile.dep b/pkg/micropython/Makefile.dep new file mode 100644 index 000000000000..af55a869531d --- /dev/null +++ b/pkg/micropython/Makefile.dep @@ -0,0 +1,11 @@ +USEMODULE += xtimer +USEMODULE += stdin + +# MicroPython doesn't compile for <32bit platforms +FEATURES_BLACKLIST += arch_8bit arch_16bit + +# This port currently requires ISR_STACKSIZE and thread_isr_stack_start +FEATURES_BLACKLIST += arch_arm7 arch_esp32 arch_esp8266 arch_riscv + +# The port currently doesn't compile for mips +FEATURES_BLACKLIST += arch_mips32r2 diff --git a/pkg/micropython/Makefile.include b/pkg/micropython/Makefile.include new file mode 100644 index 000000000000..b19a7006b775 --- /dev/null +++ b/pkg/micropython/Makefile.include @@ -0,0 +1,12 @@ +# configuration +MP_RIOT_HEAPSIZE ?= 16384U + +CFLAGS += -DMP_RIOT_HEAPSIZE=$(MP_RIOT_HEAPSIZE) + +# include paths +INCLUDES += -I$(RIOTBASE)/pkg/micropython/include +INCLUDES += -I$(PKGDIRBASE)/micropython +INCLUDES += -I$(PKGDIRBASE)/micropython/ports/riot + +# The port currently doesn't build with llvm +TOOLCHAINS_BLACKLIST += llvm diff --git a/pkg/micropython/doc.txt b/pkg/micropython/doc.txt new file mode 100644 index 000000000000..4daafcf9de1f --- /dev/null +++ b/pkg/micropython/doc.txt @@ -0,0 +1,87 @@ +/** + * @defgroup pkg_micropython MicroPython RIOT port + * @ingroup pkg + * @brief MicroPython - Python for microcontrollers + * + * # MicroPython RIOT package + * + * "MicroPython is a lean and efficient implementation of the Python 3 + * programming language that includes a small subset of the Python standard + * library and is optimised to run on microcontrollers and in constrained + * environments." + * + * @see https://micropython.org + * + * ## Status + * + * MicroPython on RIOT has to be considered experimental. While the basic + * interpreter works fairly well on native and Cortex-M, it has not seen much + * testing. + * + * ## Configuration options + * + * Use the following environment variables in the application Makefile + * or from the command line to configure MicroPython: + * + * MP_RIOT_HEAPSIZE: heap size for MicroPython, in bytes. Defaults to 16KiB. + * + * Example on the command line: + * ``` + * MP_RIOT_HEAPSIZE=2048 make -C examples/micropython + * ``` + * + * ## Implementation details + * + * The RIOT port of MicroPython currently resides in a fork at + * https://github.com/kaspar030/micropython (in branch add_riot_port). It is + * based on Micropython's "ports/minimal" with some extra modules enabled. + * It re-uses the gc_collect code from ports/unix, which has special support + * for i386 and Cortex-M. On other platforms, it uses setjmp() to collect + * registers. + * + * ## MicroPython's test suite + * + * It is possible to run MicroPython's test suite for testing this port. + * + * Steps: + * + * 1. make -Cexamples/micropython flash + * 2. cd examples/micropython/bin/pkg/${BOARD}/micropython + * 3. git apply ports/riot/slow_uart_writes.patch + * 4. cd tests + * 5. ./run-tests --target pyboard --device ${PORT} + * + * ## MicroPython modules + * + * Currently, the port enables only a subset of the available MycroPython + * modules. See "ports/riot/mpconfigport.h" for details. + * + * For now, the utime module has RIOT specific code and should work as expected. + * + * ## RIOT specific modules + * + * Currently, these are implemented: + * + * ### thread_getpid() + * + * >>> import riot + * >>> print(riot.thread_getpid()) + + * ### xtimer + * + * >>> import xtimer + * >>> + * >>> a = 0 + * >>> def inc_a(): + * >>> global a + * >>> a += 1 + * >>> + * >>> t = xtimer.xtimer(inc_a) + * >>> t.set(100000) + * >>> print(a) + * + * ## How to use + * + * See examples/micropython for example code. + * + */ diff --git a/pkg/micropython/include/micropython.h b/pkg/micropython/include/micropython.h new file mode 100644 index 000000000000..9c4839166cf4 --- /dev/null +++ b/pkg/micropython/include/micropython.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2019 Kaspar Schleiser + * 2019 Inria + * 2019 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup pkg_micropython + * + * @{ + * + * @file + * @brief MicroPython RIOT specific API + * + * @author Kaspar Schleiser + */ + +#ifndef MICROPYTHON_H +#define MICROPYTHON_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef MP_RIOT_HEAPSIZE +/* use a reasonable default heap size */ +#define MP_RIOT_HEAPSIZE (16U*1024) +#endif + +#ifndef MP_STACK_SAFEAREA +#define MP_STACK_SAFEAREA (128U) +#endif + +/** + * @brief Initialize RIOT MicroPython port + * + * @param[in] heap ptr to heap MicroPython should use + * @param[in] heap_size size of heap + */ +void mp_riot_init(char* heap, size_t heap_size); + +/** + * @brief Execute a string as MicroPython code + * + * The string will be executed on the global MicroPython instance. + * + * @param[in] src pointer to Python code + * @param[in] len length of src + */ +void mp_do_str(const char *src, int len); + +#endif /* MICROPYTHON_H */ +/** @} */