diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8cb17e52579e40700e76c4b04e9fcde11a5de46e..2db2c375d0d039502887d0115f238897448bd455 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,34 @@ All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 
 ## [Unreleased]
+### Added
+- ECG plotter tool (for desktop machines) which can plot ECG logs taken with card10.
+- The `input()` Python function.
+- Enabled the MicroPython `framebuf` module for a Pycardium-only framebuffer
+  implementation.
+- Added the `utime.ticks_us()` and `utime.ticks_ms()` functions for very
+  accurate timing of MicroPython code.
+- Added an option to use the right buttons for scrolling and the left one for
+  selecting.  This will be made configurable in a future release.
+
+### Changed
+- Changed timezone to CET.  A future release will make the timezone entirely
+  configurable.
+- Made a few library functions callable without any parameters so they are
+  easier to use.
+
+### Fixed
+- Fixed the Pycardium delay implementation in preparation for features like
+  button-interrupts.  Should also be more accurate now.
+- Fixed the filter which is used by the ECG app.
+- Fixed the display staying off while printing the sleep-messages.
+- Improved the USB-Storage mode in the menu app.
+- Fixed GPIO module not properly configuring a pin if both IN and ADC are given.
+- Added missing documentation for `os.mkdir()` and `os.rename()`.
+
+### Removed
+- Removed unnecessary out-of-bounds checks in display module.  Drawing outside
+  the display is now perfectly fine and the pixels will silently be ignored.
 
 
 ## [v1.12] - 2019-10-19 - [Leek]
diff --git a/Documentation/bluetooth/nimble.rst b/Documentation/bluetooth/nimble.rst
new file mode 100644
index 0000000000000000000000000000000000000000..23d466f17bc0cf4d67616954c0029ef77bed705a
--- /dev/null
+++ b/Documentation/bluetooth/nimble.rst
@@ -0,0 +1,115 @@
+NimBLE
+======
+
+On the card10 the ARM Cordio-B50 stack is used, which is in a very early experimental state and has some incompatibilities with some smartphones.
+Therefore some alternative stacks are evaluated, which meight be used as a replacement in the long term.
+
+
+Here a stack called NimBLE is presented, which claims to be feature complete. Originally it has been developed for Mynewt, an open source embedded operating system by Apache (https://mynewt.apache.org/).
+
+
+There is a working port for the ESP32 espressif ESP-IDF framework.
+Like Epicardium, ESP-IDF is based on FreeRTOS. Therefore it can be used for evaluation purposes.
+
+Getting NimBLE run on the ESP32
+-------------------------------
+
+Install required packages:
+
+Ubuntu:
+
+.. code-block:: shell-session
+
+  sudo apt install git virtualenv python2.7 cmake
+
+Arch:
+
+.. code-block:: shell-session
+
+  sudo pacman -S git python2 python2-virtualenv cmake
+
+Download and extract xtensa ESP32 compiler:
+
+.. code-block:: shell-session
+
+  wget https://dl.espressif.com/dl/xtensa-esp32-elf-gcc8_2_0-esp32-2018r1-linux-amd64.tar.xz
+  tar -xf xtensa-esp32-elf-gcc8_2_0-esp32-2018r1-linux-amd64.tar.xz
+
+
+Clone esp-idf:
+
+.. code-block:: shell-session
+
+    git clone https://github.com/espressif/esp-idf.git
+
+Add xtensa and ESP-IDF path to $PATH:
+
+bash shell:
+
+.. code-block:: shell-session
+
+  export IDF_PATH=$PWD/esp-idf
+  export PATH=${PATH}:$PWD/xtensa-esp32-elf/bin:$PWD/esp-idf/tools
+
+fish shell:
+
+.. code-block:: shell-session
+
+  set -gx IDF_PATH $PWD/esp-idf
+  set -gx PATH $PWD/xtensa-esp32-elf/bin/ $PWD/esp-idf/tools $PATH
+
+Create a python2.7 virtualenv:
+
+.. code-block:: shell-session
+
+  cd esp-idf
+  virtualenv -p /usr/bin/python2.7 venv
+
+Enter the virtualenv:
+
+bash shell:
+
+.. code-block:: shell-session
+
+  . venv/bin/activate
+
+fish shell:
+
+.. code-block:: shell-session
+
+  . venv/bin/activate.fish
+
+Init git submodules and install all required Python packages:
+
+.. code-block:: shell-session
+
+  git submodule update --init --recursive
+  pip install -r requirements.txt
+
+
+Now you are ready to build!
+
+The following steps assume that your ESP32 is connected via USB and
+is accessible via /dev/ttyUSB0. This meight be different on your system.
+
+There are a few NimbLE examples which can be used for playing around:
+
+Build a BLE server example (host mode):
+---------------------------------------
+.. code-block:: shell-session
+
+  cd examples/bluetooth/nimble/bleprph
+  idf.py -p /dev/ttyUSB0 flash monitor
+
+This will build and flash the example to the ESP32 and instantly listens on /dev/ttyUSB0 serial port.
+After the flashing process the ESP32 will anounce itself as **nimble-bleprph** device via BLE.
+
+Build a BLE client example (central mode):
+------------------------------------------
+.. code-block:: shell-session
+
+  cd examples/bluetooth/nimble/blecent
+  idf.py -p /dev/ttyUSB0 flash monitor
+
+This will build and flash the example to the ESP32 and instantly listens on /dev/ttyUSB0 serial port.
+After the flashing process the ESP32 creates a GATT client and performs passive scan, it then connects to peripheral device if the device advertises connectability and the device advertises support for the Alert Notification service (0x1811) as primary service UUID.
diff --git a/Documentation/epicardium/mutex.rst b/Documentation/epicardium/mutex.rst
new file mode 100644
index 0000000000000000000000000000000000000000..928730b089b3caba9a3d33f0e6f4a762834031ea
--- /dev/null
+++ b/Documentation/epicardium/mutex.rst
@@ -0,0 +1,136 @@
+Mutex
+=====
+Ontop of FreeRTOS, we have our own mutex implementation.  **Never use the
+FreeRTOS mutexes directly!  Always use this abstraction layer instead**.  This
+mutex implementation tries to make reasoning about program flow and locking
+behavior easier.  And most importantly tries to help with debugging possible
+dead-locks.
+
+Design
+------
+There are a few guiding design principles:
+
+- Mutexes can only be used from tasks, **never** from interrupts!
+- Timers can use mutexes, but only with :c:func:`mutex_trylock`, **never** with
+  :c:func:`mutex_lock` (Because they are not allowed to block).
+- Locking can *never* fail (if it does, we consider this a fatal error ⇒ panic).
+- No recursive locking.
+- An unlock can only occur from the task which previously acquired the mutex.
+- An unlock is only allowed if the mutex was previously acquired.
+
+For a more elaborate explanation of the rationale behind these rules take a
+look at the :ref:`mutex-design-reasons`.
+
+Definitions
+-----------
+.. c:autodoc:: epicardium/modules/mutex.h
+
+.. _mutex-design-reasons:
+
+Reasons for this Design
+-----------------------
+
+Locking can *never* fail
+^^^^^^^^^^^^^^^^^^^^^^^^
+This might seem like a bold claim at first but in the end, it is just a matter
+of definition and shifting responsibilities.  Instead of requiring all code to
+be robust against a locking attempt failing, we require all code to properly
+lock and unlock their mutexes and thus never producing a situation where
+locking would fail.
+
+Because all code using any of the mutexes is contained in the Epicardium
+code-base, we can - *hopefully* - audit it properly behaving ahead of time and
+thus don't need to add code to ensure correctness at runtime.  This makes
+downstream code easier to read and easier to reason about.
+
+History of this project has shown that most code does not properly deal with
+locking failures anyway: There was code simply skipping the mutexed action on
+failure, code blocking a module entirely until reboot, and worst of all: Code
+exposing the locking failure to 'user-space' (Pycardium) instead of retrying.
+This has lead to spurious errors where technically there would not need to be
+any.
+
+Only from tasks
+^^^^^^^^^^^^^^^
+Locking a mutex from an ISR, a FreeRTOS software timer or any other context
+which does not allow blocking is complicated to do right.  The biggest
+difficulty is that a task might be holding the mutex during execution of such a
+context and there is no way to wait for it to release the mutex.  This requires
+careful design of the program flow to choose an alternative option in such a
+case.  A common approach is to 'outsource' the relevant parts of the code into
+an 'IRQ worker' which is essentially just a task waiting for the IRQ to wake it
+up and then attempts to lock the mutex.
+
+If you absolutely do need it (and for legacy reasons), software timers *can*
+lock a mutex using :c:func:`mutex_trylock` (which never blocks).  I strongly
+recommend **not** doing that, though.  As shown above, you will have to deal
+with the case of the mutex being held by another task and it is very well
+possible that your timer will get starved of the mutex because the scheduler
+has no knowledge of its intentions.  In most cases, it is a better idea to use
+a task and attempt locking using :c:func:`mutex_lock`.
+
+.. todo::
+
+   We might introduce a generic IRQ worker queue system at some point.
+
+No recursive locking
+^^^^^^^^^^^^^^^^^^^^
+Recursive locking refers to the ability to 'reacquire' a mutex already held by
+the current task, deeper down in the call-chain.  Only the outermost unlock
+will actually release the mutex.  This feature is sometimes implemented to
+allow more elegant abstractions where downstream code does not need to know
+about the mutexes upstream code uses and can still also create a larger region
+where the same mutex is held.
+
+But exactly by hiding the locking done by a function, these abstractions make
+it hard to trace locking chains and in some cases even make it impossible to
+create provably correct behavior.  As an alternative, I would suggest using
+different mutexes for the different levels of abstraction.  This also helps
+keeping each mutex separated and 'local' to its purpose.
+
+Only unlock from the acquiring task
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Because of the above mentioned mutex locking semantics, there should never be a
+need to force-unlock a forgein mutex.  Even in cases of failures, all code
+should still properly release all mutexes it holds.  One notable exceptions is
+``panic()``\s which will abort all ongoing operations anyway.
+
+Only unlock once after acquisition
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Justified with an argument of robustness, sometimes the :c:func:`mutex_unlock`
+call is written in a way that allows unlocking an already unlocked mutex.  But
+robustness of downstream code will not really be improved by the upstream API
+dealing with arguably invalid usage.  For example, this could encourage
+practices like unlocking everything again at the end of a function "just to be
+sure".
+
+Instead, code should be written in a way where the lock/unlock pair is
+immediately recognizable as belonging together and is thus easily auditable to
+have correct locking behavior.  A common pattern to help with readability in
+this regard is the *Single Function Exit* which looks like this:
+
+.. code-block:: cpp
+
+   int function()
+   {
+           int ret;
+           mutex_lock(&some_mutex);
+
+           ret = foo();
+           if (ret) {
+                   /* Return with an error code */
+                   ret = -ENODEV;
+                   goto out_unlock;
+           }
+
+           ret = bar();
+           if (ret) {
+                   /* Return the return value from foo */
+                   goto out_unlock;
+           }
+
+           ret = 0;
+   out_unlock:
+           mutex_unlock(&some_mutex);
+           return ret;
+   }
diff --git a/Documentation/index.rst b/Documentation/index.rst
index 44ecfcd04f8fb330aea3ae070643895bc70d9511..6693f3c2637eb5f53ed908623723231bfd8a6559 100644
--- a/Documentation/index.rst
+++ b/Documentation/index.rst
@@ -52,6 +52,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the
    debugger
    pycardium-guide
    memorymap
+   epicardium/mutex
    epicardium/sensor-streams
 
 .. toctree::
@@ -68,6 +69,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the
 
    bluetooth/file-transfer
    bluetooth/card10
+   bluetooth/nimble
 
 Indices and tables
 ==================
diff --git a/Documentation/pycardium/os.rst b/Documentation/pycardium/os.rst
index 2447ccfe88494b385e8ec9544b08eb5146a65b48..d2fd7cf224b316050b8f84df838213c0c6116df9 100644
--- a/Documentation/pycardium/os.rst
+++ b/Documentation/pycardium/os.rst
@@ -8,12 +8,6 @@ functions found in CPythons ``os`` module.
 CPython-Like
 ------------
 
-.. py:function:: unlink(path)
-
-   Unlink (remove) a file.
-
-   :param str path: The file to remove.
-
 .. py:function:: listdir(dir)
 
    List contents of a directory.
@@ -22,6 +16,28 @@ CPython-Like
    :returns: A list of entities (files or subdirectories) in the directory
       ``dir``.
 
+.. py:function:: mkdir(path)
+
+   Create a directory named *path*.
+
+   :param str path: Path to the directory to create.  Only the last component
+      of this path will be created.
+
+.. py:function:: rename(src, dst)
+
+   Rename the file or directory *src* to *dst*. If *dst* exists, the operation
+   will fail.
+
+   :param str src: Path to source file to rename.
+   :param str dst: Destination path to rename to.  Must not exist before
+      calling :py:func:`os.rename`.
+
+.. py:function:: unlink(path)
+
+   Unlink (remove) a file.
+
+   :param str path: The file to remove.
+
 .. py:function:: urandom(n)
 
    Return ``n`` random bytes.
diff --git a/Documentation/pycardium/stdlib.rst b/Documentation/pycardium/stdlib.rst
index 161f8204ec40df71314b988bb19cf2d50b5873be..1d1b7af492d00fde5d79a4601e233c277bfd45a2 100644
--- a/Documentation/pycardium/stdlib.rst
+++ b/Documentation/pycardium/stdlib.rst
@@ -3,6 +3,14 @@ MicroPython Standard Library
 Pycardium contains some modules from the MicroPython standard library.  These
 are:
 
+.. py:module:: framebuf
+
+``framebuf``
+------------
+Refer to the official `MicroPython docs for framebuf`_.
+
+.. _MicroPython docs for framebuf: https://docs.micropython.org/en/latest/library/framebuf.html
+
 .. py:module:: ubinascii
 
 ``ubinascii``
@@ -219,7 +227,7 @@ Struct module.
 
       UUID version accordiung to RFC 4122
 
-.. py:function:: uuid4():
+.. py:function:: uuid4()
 
    Generate a new UUID version 4 (random UUID).
 
diff --git a/Documentation/pycardium/utime.rst b/Documentation/pycardium/utime.rst
index 30af11ef7dcd9e9ae9da6dcd877621a4dc7d3d89..b28d7b11bd6fe6d56770048b2711df928ced0f0e 100644
--- a/Documentation/pycardium/utime.rst
+++ b/Documentation/pycardium/utime.rst
@@ -45,6 +45,20 @@ alarm.
 
    .. versionadded:: 1.11
 
+.. py:function:: ticks_ms()
+
+   Return processor ticks (converted to milliseconds) since Pycardium startup.
+
+   This function should be the preferred method for timing and profiling
+   because it does not need an API call and thus is very fast.
+
+.. py:function:: ticks_us()
+
+   Return processor ticks (converted to microseconds) since Pycardium startup.
+
+   This function should be the preferred method for timing and profiling
+   because it does not need an API call and thus is very fast.
+
 .. py:function:: unix_time()
 
    Return the current unix time as seconds since the epoch.
diff --git a/epicardium/FreeRTOSConfig.h b/epicardium/FreeRTOSConfig.h
index b731f6a9abdee7556f073936d86aba649e72a8d1..22c6dfb0f050c38c2d33b7f1d64b1e7940d8ea97 100644
--- a/epicardium/FreeRTOSConfig.h
+++ b/epicardium/FreeRTOSConfig.h
@@ -52,6 +52,8 @@
 #define INCLUDE_vTaskDelay          1
 #define INCLUDE_uxTaskGetStackHighWaterMark 1
 #define INCLUDE_xTimerPendFunctionCall 1
+#define INCLUDE_xSemaphoreGetMutexHolder 1
+
 /* Allow static allocation of data structures */
 #define configSUPPORT_STATIC_ALLOCATION 1
 
diff --git a/epicardium/fs/filesystem_fat.c b/epicardium/fs/filesystem_fat.c
index ccaec62157174cdfab14c957d157941b063acc64..c1468923f32c97d287319d00d8ea05949f807fa9 100644
--- a/epicardium/fs/filesystem_fat.c
+++ b/epicardium/fs/filesystem_fat.c
@@ -25,15 +25,12 @@
 #include "modules/log.h"
 #include "modules/modules.h"
 #include "api/common.h"
+#include "modules/mutex.h"
 
 #define SSLOG_DEBUG(...) LOG_DEBUG("fatfs", __VA_ARGS__)
 #define SSLOG_INFO(...) LOG_INFO("fatfs", __VA_ARGS__)
 #define SSLOG_ERR(...) LOG_ERR("fatfs", __VA_ARGS__)
 
-#ifndef EPIC_FAT_STATIC_SEMAPHORE
-#define EPIC_FAT_STATIC_SEMAPHORE 0
-#endif
-
 /* clang-format off */
 #define EPIC_FAT_MAX_OPENED           (1 << (EPIC_FAT_FD_INDEX_BITS))
 #define EPIC_FAT_FD_GENERATION_BITS   (31 - (EPIC_FAT_FD_INDEX_BITS))
@@ -67,8 +64,6 @@ struct EpicFileSystem {
 static const int s_libffToErrno[20];
 
 static const char *f_get_rc_string(FRESULT rc);
-static bool globalLockAccquire();
-static void globalLockRelease();
 static void efs_close_all(EpicFileSystem *fs, int coreMask);
 
 /**
@@ -97,11 +92,7 @@ static void efs_init_stat(struct epic_stat *stat, FILINFO *finfo);
 
 static EpicFileSystem s_globalFileSystem;
 
-#if (EPIC_FAT_STATIC_SEMAPHORE == 1)
-static StaticSemaphore_t s_globalLockBuffer;
-#endif
-
-static SemaphoreHandle_t s_globalLock = NULL;
+static struct mutex fatfs_lock = { 0 };
 
 static void cb_attachTimer(void *a, uint32_t b)
 {
@@ -118,11 +109,7 @@ void fatfs_init()
 	assert(!s_initCalled);
 	s_initCalled = true;
 
-#if (EPIC_FAT_STATIC_SEMAPHORE == 1)
-	s_globalLock = xSemaphoreCreateMutexStatic(&s_globalLockBuffer);
-#else
-	s_globalLock = xSemaphoreCreateMutex();
-#endif
+	mutex_create(&fatfs_lock);
 
 	s_globalFileSystem.generationCount = 1;
 	fatfs_attach();
@@ -142,26 +129,24 @@ int fatfs_attach()
 {
 	FRESULT ff_res;
 	int rc = 0;
-	if (globalLockAccquire()) {
-		EpicFileSystem *fs = &s_globalFileSystem;
-		if (!fs->attached) {
-			ff_res = f_mount(&fs->FatFs, "/", 0);
-			if (ff_res == FR_OK) {
-				fs->attached = true;
-				SSLOG_INFO("attached\n");
-			} else {
-				SSLOG_ERR(
-					"f_mount error %s\n",
-					f_get_rc_string(ff_res)
-				);
-				rc = -s_libffToErrno[ff_res];
-			}
-		}
 
-		globalLockRelease();
-	} else {
-		SSLOG_ERR("Failed to lock\n");
+	mutex_lock(&fatfs_lock);
+
+	EpicFileSystem *fs = &s_globalFileSystem;
+	if (!fs->attached) {
+		ff_res = f_mount(&fs->FatFs, "/", 0);
+		if (ff_res == FR_OK) {
+			fs->attached = true;
+			SSLOG_INFO("attached\n");
+		} else {
+			SSLOG_ERR(
+				"f_mount error %s\n", f_get_rc_string(ff_res)
+			);
+			rc = -s_libffToErrno[ff_res];
+		}
 	}
+
+	mutex_unlock(&fatfs_lock);
 	return rc;
 }
 
@@ -227,24 +212,12 @@ static const char *f_get_rc_string(FRESULT rc)
 	return p;
 }
 
-static bool globalLockAccquire()
-{
-	return (int)(xSemaphoreTake(s_globalLock, FF_FS_TIMEOUT) == pdTRUE);
-}
-
-static void globalLockRelease()
-{
-	xSemaphoreGive(s_globalLock);
-}
-
 int efs_lock_global(EpicFileSystem **fs)
 {
 	*fs = NULL;
-	if (!globalLockAccquire()) {
-		return -EBUSY;
-	}
+	mutex_lock(&fatfs_lock);
 	if (!s_globalFileSystem.attached) {
-		globalLockRelease();
+		mutex_unlock(&fatfs_lock);
 		return -ENODEV;
 	}
 	*fs = &s_globalFileSystem;
@@ -259,7 +232,7 @@ int efs_lock_global(EpicFileSystem **fs)
 void efs_unlock_global(EpicFileSystem *fs)
 {
 	(void)fs;
-	globalLockRelease();
+	mutex_unlock(&fatfs_lock);
 }
 
 static bool efs_get_opened(
diff --git a/epicardium/modules/bhi.c b/epicardium/modules/bhi.c
index b88e1dc217d087e93d286a488a81ccef3cac619c..b04d2fd5c50ad62ec0294f366958f2af932cb0a8 100644
--- a/epicardium/modules/bhi.c
+++ b/epicardium/modules/bhi.c
@@ -131,7 +131,7 @@ int epic_bhi160_enable_sensor(
 	int result = 0;
 
 	bhy_virtual_sensor_t vs_id = bhi160_lookup_vs_id(sensor_type);
-	if (vs_id < 0) {
+	if (vs_id == (bhy_virtual_sensor_t)-1) {
 		return -ENODEV;
 	}
 
@@ -188,7 +188,7 @@ int epic_bhi160_disable_sensor(enum bhi160_sensor_type sensor_type)
 	int result = 0;
 
 	bhy_virtual_sensor_t vs_id = bhi160_lookup_vs_id(sensor_type);
-	if (vs_id < 0) {
+	if (vs_id == (bhy_virtual_sensor_t)-1) {
 		return -ENODEV;
 	}
 
diff --git a/epicardium/modules/dispatcher.c b/epicardium/modules/dispatcher.c
index 03f8534cc0a1c02dccc61f8c03e486e2ab472ff8..355bc0471730b23d592b3187f0dd5a6a04ec9483 100644
--- a/epicardium/modules/dispatcher.c
+++ b/epicardium/modules/dispatcher.c
@@ -1,21 +1,20 @@
 #include "modules/log.h"
+#include "modules/mutex.h"
 
 #include "api/dispatcher.h"
 
 #include "FreeRTOS.h"
 #include "task.h"
-#include "semphr.h"
 
 #define TIMEOUT pdMS_TO_TICKS(2000)
 
 TaskHandle_t dispatcher_task_id;
 
-static StaticSemaphore_t api_mutex_data;
-SemaphoreHandle_t api_mutex = NULL;
+struct mutex api_mutex = { 0 };
 
 void dispatcher_mutex_init(void)
 {
-	api_mutex = xSemaphoreCreateMutexStatic(&api_mutex_data);
+	mutex_create(&api_mutex);
 }
 
 /*
@@ -27,12 +26,9 @@ void vApiDispatcher(void *pvParameters)
 	LOG_DEBUG("dispatcher", "Ready.");
 	while (1) {
 		if (api_dispatcher_poll()) {
-			if (xSemaphoreTake(api_mutex, TIMEOUT) != pdTRUE) {
-				LOG_ERR("dispatcher", "API mutex blocked");
-				continue;
-			}
+			mutex_lock(&api_mutex);
 			api_dispatcher_exec();
-			xSemaphoreGive(api_mutex);
+			mutex_unlock(&api_mutex);
 		}
 		ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
 	}
diff --git a/epicardium/modules/gpio.c b/epicardium/modules/gpio.c
index a923812f5f49d90604bdce150a29d01821dc9ff6..4af356ee843b1268dec103a301b7bf9a766a3115 100644
--- a/epicardium/modules/gpio.c
+++ b/epicardium/modules/gpio.c
@@ -48,7 +48,16 @@ int epic_gpio_set_pin_mode(uint8_t pin, uint8_t mode)
 
 	gpio_cfg_t *cfg = &gpio_configs[pin];
 
-	if (mode & EPIC_GPIO_MODE_IN) {
+	if (mode & EPIC_GPIO_MODE_ADC) {
+		if (s_adc_channels[pin] == -1) {
+			LOG_WARN("gpio", "ADC not available on pin %d", pin);
+			return -EINVAL;
+		}
+		cfg->func = GPIO_FUNC_ALT1;
+		if (mode & EPIC_GPIO_MODE_OUT) {
+			return -EINVAL;
+		}
+	} else if (mode & EPIC_GPIO_MODE_IN) {
 		cfg->func = GPIO_FUNC_IN;
 		if (mode & EPIC_GPIO_MODE_OUT) {
 			return -EINVAL;
@@ -58,15 +67,6 @@ int epic_gpio_set_pin_mode(uint8_t pin, uint8_t mode)
 		if (mode & EPIC_GPIO_MODE_IN) {
 			return -EINVAL;
 		}
-	} else if (mode & EPIC_GPIO_MODE_ADC) {
-		if (s_adc_channels[pin] == -1) {
-			LOG_WARN("gpio", "ADC not available on pin %d", pin);
-			return -EINVAL;
-		}
-		cfg->func = GPIO_FUNC_ALT1;
-		if (mode & EPIC_GPIO_MODE_OUT) {
-			return -EINVAL;
-		}
 	} else {
 		return -EINVAL;
 	}
diff --git a/epicardium/modules/lifecycle.c b/epicardium/modules/lifecycle.c
index a1f8627aa53d648bc67be12455052d6a19352910..25689bd3db224789e5874deedd501fc1bcf8bfea 100644
--- a/epicardium/modules/lifecycle.c
+++ b/epicardium/modules/lifecycle.c
@@ -2,6 +2,7 @@
 #include "modules/log.h"
 #include "modules/modules.h"
 #include "modules/config.h"
+#include "modules/mutex.h"
 #include "api/dispatcher.h"
 #include "api/interrupt-sender.h"
 #include "l0der/l0der.h"
@@ -10,7 +11,6 @@
 
 #include "FreeRTOS.h"
 #include "task.h"
-#include "semphr.h"
 
 #include <string.h>
 #include <stdbool.h>
@@ -26,8 +26,7 @@
 #define PYINTERPRETER ""
 
 static TaskHandle_t lifecycle_task = NULL;
-static StaticSemaphore_t core1_mutex_data;
-static SemaphoreHandle_t core1_mutex = NULL;
+static struct mutex core1_mutex    = { 0 };
 
 enum payload_type {
 	PL_INVALID       = 0,
@@ -99,10 +98,7 @@ static int do_load(struct load_info *info)
 		LOG_INFO("lifecycle", "Loading \"%s\" ...", info->name);
 	}
 
-	if (xSemaphoreTake(api_mutex, BLOCK_WAIT) != pdTRUE) {
-		LOG_ERR("lifecycle", "API blocked");
-		return -EBUSY;
-	}
+	mutex_lock(&api_mutex);
 
 	if (info->do_reset) {
 		LOG_DEBUG("lifecycle", "Triggering core 1 reset.");
@@ -120,7 +116,7 @@ static int do_load(struct load_info *info)
 	 */
 	res = hardware_reset();
 	if (res < 0) {
-		return res;
+		goto out_free_api;
 	}
 
 	switch (info->type) {
@@ -134,8 +130,8 @@ static int do_load(struct load_info *info)
 			res = l0der_load_path(info->name, &l0dable);
 			if (res != 0) {
 				LOG_ERR("lifecycle", "l0der failed: %d\n", res);
-				xSemaphoreGive(api_mutex);
-				return -ENOEXEC;
+				res = -ENOEXEC;
+				goto out_free_api;
 			}
 			core1_load(l0dable.isr_vector, "");
 		} else {
@@ -149,12 +145,14 @@ static int do_load(struct load_info *info)
 		LOG_ERR("lifecyle",
 			"Attempted to load invalid payload (%s)",
 			info->name);
-		xSemaphoreGive(api_mutex);
-		return -EINVAL;
+		res = -EINVAL;
+		goto out_free_api;
 	}
 
-	xSemaphoreGive(api_mutex);
-	return 0;
+	res = 0;
+out_free_api:
+	mutex_unlock(&api_mutex);
+	return res;
 }
 
 /*
@@ -254,11 +252,7 @@ static void load_menu(bool reset)
 {
 	LOG_DEBUG("lifecycle", "Into the menu");
 
-	if (xSemaphoreTake(core1_mutex, BLOCK_WAIT) != pdTRUE) {
-		LOG_ERR("lifecycle",
-			"Can't load because mutex is blocked (menu).");
-		return;
-	}
+	mutex_lock(&core1_mutex);
 
 	int ret = load_async("menu.py", reset);
 	if (ret < 0) {
@@ -278,7 +272,7 @@ static void load_menu(bool reset)
 		}
 	}
 
-	xSemaphoreGive(core1_mutex);
+	mutex_unlock(&core1_mutex);
 }
 /* Helpers }}} */
 
@@ -298,14 +292,9 @@ void epic_system_reset(void)
  */
 int epic_exec(char *name)
 {
-	if (xSemaphoreTake(core1_mutex, BLOCK_WAIT) != pdTRUE) {
-		LOG_ERR("lifecycle",
-			"Can't load because mutex is blocked (epi exec).");
-		return -EBUSY;
-	}
-
+	mutex_lock(&core1_mutex);
 	int ret = load_sync(name, true);
-	xSemaphoreGive(core1_mutex);
+	mutex_unlock(&core1_mutex);
 	return ret;
 }
 
@@ -318,13 +307,9 @@ int epic_exec(char *name)
  */
 int __epic_exec(char *name)
 {
-	if (xSemaphoreTake(core1_mutex, BLOCK_WAIT) != pdTRUE) {
-		LOG_ERR("lifecycle",
-			"Can't load because mutex is blocked (1 exec).");
-		return -EBUSY;
-	}
+	mutex_lock(&core1_mutex);
 	int ret = load_async(name, false);
-	xSemaphoreGive(core1_mutex);
+	mutex_unlock(&core1_mutex);
 	return ret;
 }
 
@@ -361,17 +346,14 @@ void return_to_menu(void)
 void vLifecycleTask(void *pvParameters)
 {
 	lifecycle_task = xTaskGetCurrentTaskHandle();
-	core1_mutex    = xSemaphoreCreateMutexStatic(&core1_mutex_data);
-
-	if (xSemaphoreTake(core1_mutex, 0) != pdTRUE) {
-		panic("lifecycle: Failed to acquire mutex after creation.");
-	}
+	mutex_create(&core1_mutex);
+	mutex_lock(&core1_mutex);
 
 	LOG_DEBUG("lifecycle", "Booting core 1 ...");
 	core1_boot();
 	vTaskDelay(pdMS_TO_TICKS(10));
 
-	xSemaphoreGive(core1_mutex);
+	mutex_unlock(&core1_mutex);
 
 	/* If `main.py` exists, start it.  Otherwise, start `menu.py`. */
 	if (epic_exec("main.py") < 0) {
@@ -386,11 +368,7 @@ void vLifecycleTask(void *pvParameters)
 	while (1) {
 		ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
 
-		if (xSemaphoreTake(core1_mutex, BLOCK_WAIT) != pdTRUE) {
-			LOG_ERR("lifecycle",
-				"Can't load because mutex is blocked (task).");
-			continue;
-		}
+		mutex_lock(&core1_mutex);
 
 		if (write_menu) {
 			write_menu = false;
@@ -406,6 +384,6 @@ void vLifecycleTask(void *pvParameters)
 
 		do_load((struct load_info *)&async_load);
 
-		xSemaphoreGive(core1_mutex);
+		mutex_unlock(&core1_mutex);
 	}
 }
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index 022397ca26e3866ef9a72472a7621e4cd6dbfec2..8ead5e72c4cc80e3fe0601d713784028cc16645f 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -14,6 +14,7 @@ module_sources = files(
   'light_sensor.c',
   'log.c',
   'max30001.c',
+  'mutex.c',
   'panic.c',
   'personal_state.c',
   'pmic.c',
diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h
index bf59566b3e7ef168977ccf7606d55f445961da86..745ecec230be10e8a97ebd0b181b72eaa8e2fc28 100644
--- a/epicardium/modules/modules.h
+++ b/epicardium/modules/modules.h
@@ -2,8 +2,8 @@
 #define MODULES_H
 
 #include "FreeRTOS.h"
-#include "semphr.h"
 #include "gpio.h"
+#include "modules/mutex.h"
 
 #include <stdint.h>
 #include <stdbool.h>
@@ -15,7 +15,7 @@ void panic(const char *format, ...)
 /* ---------- Dispatcher --------------------------------------------------- */
 void vApiDispatcher(void *pvParameters);
 void dispatcher_mutex_init(void);
-extern SemaphoreHandle_t api_mutex;
+extern struct mutex api_mutex;
 extern TaskHandle_t dispatcher_task_id;
 
 /* ---------- Hardware Init & Reset ---------------------------------------- */
diff --git a/epicardium/modules/mutex.c b/epicardium/modules/mutex.c
new file mode 100644
index 0000000000000000000000000000000000000000..9d58eec080977d93f69f275350a5101a15d4a64b
--- /dev/null
+++ b/epicardium/modules/mutex.c
@@ -0,0 +1,55 @@
+#include "modules/mutex.h"
+
+#include <assert.h>
+
+void _mutex_create(struct mutex *m, const char *name)
+{
+	/* Assert that the mutex has not been initialized already */
+	assert(m->name == NULL);
+
+	/*
+	 * The name is just the parameter stringified which is almost always a
+	 * pointer.  If it is, skip over the '&' because it adds no value as
+	 * part of the name.
+	 */
+	if (name[0] == '&') {
+		m->name = &name[1];
+	} else {
+		m->name = name;
+	}
+
+	m->_rtos_mutex = xSemaphoreCreateMutexStatic(&m->_rtos_mutex_data);
+}
+
+void mutex_lock(struct mutex *m)
+{
+	int ret = xSemaphoreTake(m->_rtos_mutex, portMAX_DELAY);
+
+	/* Ensure locking was actually successful */
+	assert(ret == pdTRUE);
+}
+
+bool mutex_trylock(struct mutex *m)
+{
+	int ret = xSemaphoreTake(m->_rtos_mutex, 0);
+	return ret == pdTRUE;
+}
+
+void mutex_unlock(struct mutex *m)
+{
+	/* Ensure that only the owner can unlock a mutex */
+	assert(mutex_get_owner(m) == xTaskGetCurrentTaskHandle());
+
+	int ret = xSemaphoreGive(m->_rtos_mutex);
+
+	/*
+	 * Ensure that unlocking was successful; that is, the mutex must have
+	 * been acquired previously (no multiple unlocks).
+	 */
+	assert(ret == pdTRUE);
+}
+
+TaskHandle_t mutex_get_owner(struct mutex *m)
+{
+	return xSemaphoreGetMutexHolder(m->_rtos_mutex);
+}
diff --git a/epicardium/modules/mutex.h b/epicardium/modules/mutex.h
new file mode 100644
index 0000000000000000000000000000000000000000..1c3055996ac2142bee634d9fd9f4c09b3233d69d
--- /dev/null
+++ b/epicardium/modules/mutex.h
@@ -0,0 +1,82 @@
+#ifndef _MUTEX_H
+#define _MUTEX_H
+
+#ifndef __SPHINX_DOC
+/* Some headers are not recognized by hawkmoth for some odd reason */
+#include <stddef.h>
+#include <stdbool.h>
+
+#include "FreeRTOS.h"
+#include "semphr.h"
+#else
+typedef unsigned int size_t;
+typedef _Bool bool;
+
+typedef void *TaskHandle_t;
+typedef void *SemaphoreHandle_t;
+typedef int StaticSemaphore_t;
+#endif /* __SPHINX_DOC */
+
+
+/**
+ * Mutex data type.
+ */
+struct mutex {
+	/* Name of this mutex, kept for debugging purposes.  */
+	const char *name;
+
+	/* FreeRTOS mutex data structures. */
+	SemaphoreHandle_t _rtos_mutex;
+	StaticSemaphore_t _rtos_mutex_data;
+};
+
+/**
+ * Create a new mutex.
+ *
+ * Call this function as early as possible, in an obvious location so it is easy
+ * to find.  Mutexes should be defined statically so they stay alive for the
+ * entire run-time of the firmware.
+ */
+#define mutex_create(mutex) _mutex_create(mutex, #mutex)
+void _mutex_create(struct mutex *m, const char *name);
+
+/**
+ * Lock a mutex.
+ *
+ * If the mutex is held by another task, :c:func:`mutex_lock` will block the
+ * current task until the mutex is unlocked.
+ *
+ * .. warning::
+ *
+ *    This function is **not** safe to use in a timer!
+ */
+void mutex_lock(struct mutex *m);
+
+/**
+ * Try locking a mutex.
+ *
+ * If the mutex is currently locked by another task, :c:func:`mutex_trylock`
+ * will return ``false`` immediately.  If the attmept to lock was successful, it
+ * will return ``true``.
+ *
+ * This funciton is safe for use in timers.
+ */
+bool mutex_trylock(struct mutex *m);
+
+/**
+ * Unlock a mutex.
+ *
+ * You **must** call this function from the same task which originally locked
+ * the mutex.
+ */
+void mutex_unlock(struct mutex *m);
+
+/**
+ * Get the current owner of the mutex.
+ *
+ * Returns the task-handle of the task currently holding the mutex.  If the
+ * mutex is unlocked, ``NULL`` is returned.
+ */
+TaskHandle_t mutex_get_owner(struct mutex *m);
+
+#endif /* _MUTEX_H */
diff --git a/epicardium/modules/pmic.c b/epicardium/modules/pmic.c
index 6c95fa46f193520724f04653ce985ef603a55604..748b1d154e6161ffb4f55516d982d14e49ac311a 100644
--- a/epicardium/modules/pmic.c
+++ b/epicardium/modules/pmic.c
@@ -162,6 +162,9 @@ __attribute__((noreturn)) static void pmic_die(float u_batt)
 	/* Grab the screen */
 	disp_forcelock();
 
+	/* Turn it on in case it was off */
+	epic_disp_backlight(100);
+
 	/* Draw an error screen */
 	epic_disp_clear(0x0000);
 
@@ -336,6 +339,10 @@ void vPmicTask(void *pvParameters)
 
 				if (duration > 1000) {
 					disp_forcelock();
+
+					/* Turn it on in case it was off */
+					epic_disp_backlight(100);
+
 					epic_disp_clear(0x0000);
 
 					char buf[20];
diff --git a/epicardium/modules/stream.c b/epicardium/modules/stream.c
index 7453da118ea364db3f510538a617d34762a30dbd..07f55f3ac3d970ac10bb219328c42c0062fb0719 100644
--- a/epicardium/modules/stream.c
+++ b/epicardium/modules/stream.c
@@ -1,80 +1,71 @@
 #include <string.h>
 
 #include "FreeRTOS.h"
-#include "semphr.h"
+#include "queue.h"
 
 #include "epicardium.h"
 #include "modules/log.h"
 #include "modules/stream.h"
+#include "modules/mutex.h"
 
 /* Internal buffer of registered streams */
 static struct stream_info *stream_table[SD_MAX];
 
 /* Lock for modifying the stream info table */
-static StaticSemaphore_t stream_table_lock_data;
-static SemaphoreHandle_t stream_table_lock;
+static struct mutex stream_table_lock;
 
 int stream_init()
 {
 	memset(stream_table, 0x00, sizeof(stream_table));
-	stream_table_lock =
-		xSemaphoreCreateMutexStatic(&stream_table_lock_data);
+	mutex_create(&stream_table_lock);
 	return 0;
 }
 
 int stream_register(int sd, struct stream_info *stream)
 {
 	int ret = 0;
-	if (xSemaphoreTake(stream_table_lock, STREAM_MUTEX_WAIT) != pdTRUE) {
-		LOG_WARN("stream", "Lock contention error");
-		ret = -EBUSY;
-		goto out;
-	}
+
+	mutex_lock(&stream_table_lock);
 
 	if (sd < 0 || sd >= SD_MAX) {
 		ret = -EINVAL;
-		goto out_release;
+		goto out;
 	}
 
 	if (stream_table[sd] != NULL) {
 		/* Stream already registered */
 		ret = -EACCES;
-		goto out_release;
+		goto out;
 	}
 
 	stream_table[sd] = stream;
 
-out_release:
-	xSemaphoreGive(stream_table_lock);
 out:
+	mutex_unlock(&stream_table_lock);
 	return ret;
 }
 
 int stream_deregister(int sd, struct stream_info *stream)
 {
 	int ret = 0;
-	if (xSemaphoreTake(stream_table_lock, STREAM_MUTEX_WAIT) != pdTRUE) {
-		LOG_WARN("stream", "Lock contention error");
-		ret = -EBUSY;
-		goto out;
-	}
+
+	mutex_lock(&stream_table_lock);
 
 	if (sd < 0 || sd >= SD_MAX) {
 		ret = -EINVAL;
-		goto out_release;
+		goto out;
 	}
 
 	if (stream_table[sd] != stream) {
 		/* Stream registered by someone else */
 		ret = -EACCES;
-		goto out_release;
+		goto out;
 	}
 
 	stream_table[sd] = NULL;
 
-out_release:
-	xSemaphoreGive(stream_table_lock);
 out:
+	mutex_unlock(&stream_table_lock);
 	return ret;
 }
 
@@ -86,35 +77,31 @@ int epic_stream_read(int sd, void *buf, size_t count)
 	 * simulaneously.  I don't know what the most efficient implementation
 	 * of this would look like.
 	 */
-	if (xSemaphoreTake(stream_table_lock, STREAM_MUTEX_WAIT) != pdTRUE) {
-		LOG_WARN("stream", "Lock contention error");
-		ret = -EBUSY;
-		goto out;
-	}
+	mutex_lock(&stream_table_lock);
 
 	if (sd < 0 || sd >= SD_MAX) {
 		ret = -EBADF;
-		goto out_release;
+		goto out;
 	}
 
 	struct stream_info *stream = stream_table[sd];
 	if (stream == NULL) {
 		ret = -ENODEV;
-		goto out_release;
+		goto out;
 	}
 
 	/* Poll the stream, if a poll_stream function exists */
 	if (stream->poll_stream != NULL) {
-		int ret = stream->poll_stream();
+		ret = stream->poll_stream();
 		if (ret < 0) {
-			goto out_release;
+			goto out;
 		}
 	}
 
 	/* Check buffer size is a multiple of the data packet size */
 	if (count % stream->item_size != 0) {
 		ret = -EINVAL;
-		goto out_release;
+		goto out;
 	}
 
 	size_t i;
@@ -126,8 +113,7 @@ int epic_stream_read(int sd, void *buf, size_t count)
 
 	ret = i / stream->item_size;
 
-out_release:
-	xSemaphoreGive(stream_table_lock);
 out:
+	mutex_unlock(&stream_table_lock);
 	return ret;
 }
diff --git a/preload/apps/bhi160/metadata.json b/preload/apps/bhi160/metadata.json
index e8a0a37a4779bf3916449420f7724172b4eee021..807bf71206b9ef35d764a4fe6a9d8e564fe8f9e3 100644
--- a/preload/apps/bhi160/metadata.json
+++ b/preload/apps/bhi160/metadata.json
@@ -1 +1 @@
-{"author": "card10badge team", "name": "BHI160", "description": "Read BHI160 sensor data", "category": "Hardware", "revision": 1}
+{"author": "card10 contributors", "name": "BHI160", "description": "Read BHI160 sensor data", "category": "Hardware", "revision": -1, "source":"preload"}
diff --git a/preload/apps/ble/metadata.json b/preload/apps/ble/metadata.json
index 4726c23629fac128a30e18462cef67f518bbd9ed..e8538282997818ec7fb575fa380a2ca5cc36308c 100644
--- a/preload/apps/ble/metadata.json
+++ b/preload/apps/ble/metadata.json
@@ -1 +1 @@
-{"author": "card10badge team", "name": "Bluetooth", "description": "Toggle Bluetooth Low Energy Indicator", "category": "Hardware", "revision": 1}
+{"author": "card10 contributors", "name": "Bluetooth", "description": "Toggle Bluetooth Low Energy Indicator", "category": "Hardware", "revision": -1, "source":"preload"}
diff --git a/preload/apps/ecg/__init__.py b/preload/apps/ecg/__init__.py
index 8a501a21b667ade943fca83ca2e6dd32186ad983..da30babcc85a0c11dd170900b24ce9ae543a46d9 100644
--- a/preload/apps/ecg/__init__.py
+++ b/preload/apps/ecg/__init__.py
@@ -51,19 +51,25 @@ last_sample_count = 1
 leds.dim_top(1)
 COLORS = [((23 + (15 * i)) % 360, 1.0, 1.0) for i in range(11)]
 
-
 # variables for high-pass filter
+# note: corresponds to 1st order hpf with -3dB at ~18.7Hz
+# general formula: f(-3dB)=-(sample_rate/tau)*ln(1-betadash)
 moving_average = 0
 alpha = 2
 beta = 3
+betadash = beta / (alpha + beta)
 
 
 def update_history(datasets):
     global history, moving_average, alpha, beta, last_sample_count
     last_sample_count = len(datasets)
     for val in datasets:
-        history.append(val - moving_average)
-        moving_average = (alpha * moving_average + beta * val) / (alpha + beta)
+        if current_mode == MODE_FINGER:
+            history.append(val - moving_average)
+            moving_average += betadash * (val - moving_average)
+            # identical to: moving_average = (alpha * moving_average + beta * val) / (alpha + beta)
+        else:
+            history.append(val)
 
     # trim old elements
     history = history[-HISTORY_MAX:]
diff --git a/preload/apps/ecg/metadata.json b/preload/apps/ecg/metadata.json
index 8c553a32e558232272b9cc0710156f5f83d968ac..8a62057119eb0a261cfe7f6757dcbaf22b88e4db 100644
--- a/preload/apps/ecg/metadata.json
+++ b/preload/apps/ecg/metadata.json
@@ -1 +1 @@
-{"name":"ECG","description":"A simple ecg monitor which displays the heart rate and allows you to switch between usb and finger reader.","category":"hardware","author":"griffon","revision":1}
+{"name":"ECG","description":"A simple ecg monitor which displays the heart rate and allows you to switch between usb and finger reader.","category":"hardware","author":"card10 contributors","revision":-1,"source":"preload"}
diff --git a/preload/apps/personal_state/metadata.json b/preload/apps/personal_state/metadata.json
index 55ba63ef12071f8cf66784bb63886013525bf2ea..b4e2901acee14411aa8dcfb2af9b2d9a920b4b28 100644
--- a/preload/apps/personal_state/metadata.json
+++ b/preload/apps/personal_state/metadata.json
@@ -1 +1 @@
-{"author": "card10badge team", "name": "Personal State", "description": "Change the personal state LED", "category": "Hardware", "revision": 1}
+{"author": "card10 contributors", "name": "Personal State", "description": "Change the personal state LED", "category": "Hardware", "revision": -1, "source":"preload"}
diff --git a/preload/menu.py b/preload/menu.py
index 2a3794a0dab1a3778725c52e20224aa59aaff376..1fe2b532eb1e2c2572ac8aeb03511cc7b3224c88 100644
--- a/preload/menu.py
+++ b/preload/menu.py
@@ -67,16 +67,21 @@ def usb_mode(disp):
     disp.print("open", posx=52, posy=40, fg=color.CAMPGREEN_DARK)
     disp.update()
 
+    utime.sleep_ms(200)
+
     # Wait for select button to be released
-    while buttons.read(0xFF) == buttons.TOP_RIGHT:
+    while buttons.read() == buttons.TOP_RIGHT:
         pass
 
     # Wait for any button to be pressed and disable USB storage again
-    while buttons.read(0xFF) == 0:
+    while buttons.read() == 0:
         pass
 
     os.usbconfig(os.USB_SERIAL)
 
+    # Exit after USB-Mode to reload menu
+    os.exit(0)
+
 
 class MainMenu(simple_menu.Menu):
     timeout = 30.0
diff --git a/pycardium/modules/buttons.c b/pycardium/modules/buttons.c
index ab8dabd7d28692f7e586dc64d717c4c3cfb6aa31..ace61e722efdb977d12951a4eb96031dbb9dd005 100644
--- a/pycardium/modules/buttons.c
+++ b/pycardium/modules/buttons.c
@@ -5,13 +5,24 @@
 
 #include "epicardium.h"
 
-static mp_obj_t mp_buttons_read(mp_obj_t mask_in)
+static mp_obj_t mp_buttons_read(size_t n_args, const mp_obj_t *args)
 {
-	uint8_t mask          = mp_obj_get_int(mask_in);
+	uint8_t mask;
+	if (n_args == 1) {
+		mp_int_t mask_int = mp_obj_get_int(args[0]);
+		if (mask_int > 255) {
+			mp_raise_ValueError("mask must be less than 256");
+		}
+		mask = (uint8_t)mask_int;
+	} else {
+		mask = 0xff;
+	}
 	uint8_t button_states = epic_buttons_read(mask);
 	return MP_OBJ_NEW_SMALL_INT(button_states);
 }
-static MP_DEFINE_CONST_FUN_OBJ_1(buttons_read_obj, mp_buttons_read);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(
+	buttons_read_obj, 0, 1, mp_buttons_read
+);
 
 static const mp_rom_map_elem_t buttons_module_globals_table[] = {
 	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_buttons) },
diff --git a/pycardium/modules/os.c b/pycardium/modules/os.c
index 384a71e24385e638133a75f9de3d253f7ad2ec68..cc4b067996e5c505cfa0a71f3ae9e478fc99ddfb 100644
--- a/pycardium/modules/os.c
+++ b/pycardium/modules/os.c
@@ -91,10 +91,16 @@ static mp_obj_t mp_os_reset(void)
 }
 static MP_DEFINE_CONST_FUN_OBJ_0(reset_obj, mp_os_reset);
 
-static mp_obj_t mp_os_listdir(mp_obj_t py_path)
+static mp_obj_t mp_os_listdir(size_t n_args, const mp_obj_t *args)
 {
-	const char *path = mp_obj_str_get_str(py_path);
-	int fd           = epic_file_opendir(path);
+	const char *path;
+	if (n_args == 1) {
+		path = mp_obj_str_get_str(args[0]);
+	} else {
+		path = "";
+	}
+
+	int fd = epic_file_opendir(path);
 
 	if (fd < 0) {
 		mp_raise_OSError(-fd);
@@ -118,7 +124,7 @@ static mp_obj_t mp_os_listdir(mp_obj_t py_path)
 	epic_file_close(fd);
 	return MP_OBJ_FROM_PTR(list);
 }
-static MP_DEFINE_CONST_FUN_OBJ_1(listdir_obj, mp_os_listdir);
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(listdir_obj, 0, 1, mp_os_listdir);
 
 static mp_obj_t mp_os_unlink(mp_obj_t py_path)
 {
diff --git a/pycardium/modules/py/simple_menu.py b/pycardium/modules/py/simple_menu.py
index 18279db442ee22e1ce882a98478229126c82ed8a..7a193c686c69d84fa8b84d230c16bec559157b33 100644
--- a/pycardium/modules/py/simple_menu.py
+++ b/pycardium/modules/py/simple_menu.py
@@ -120,6 +120,12 @@ class Menu:
     .. versionadded:: 1.9
     """
 
+    right_buttons_scroll = False
+    """
+    Use top right and bottom right buttons to move in the list
+    instead of bottom left and bottom right buttons.
+    """
+
     def on_scroll(self, item, index):
         """
         Hook when the selector scrolls to a new item.
@@ -173,6 +179,12 @@ class Menu:
         self.idx = 0
         self.select_time = utime.time_ms()
         self.disp = display.open()
+        self.button_scroll_up = (
+            buttons.TOP_RIGHT if self.right_buttons_scroll else buttons.BOTTOM_LEFT
+        )
+        self.button_select = (
+            buttons.BOTTOM_LEFT if self.right_buttons_scroll else buttons.TOP_RIGHT
+        )
 
     def entry2name(self, value):
         """
@@ -290,7 +302,7 @@ class Menu:
                     except Exception as e:
                         print("Exception during menu.on_scroll():")
                         sys.print_exception(e)
-                elif ev == buttons.BOTTOM_LEFT:
+                elif ev == self.button_scroll_up:
                     self.select_time = utime.time_ms()
                     self.draw_menu(8)
                     self.idx = (self.idx + len(self.entries) - 1) % len(self.entries)
@@ -301,7 +313,7 @@ class Menu:
                     except Exception as e:
                         print("Exception during menu.on_scroll():")
                         sys.print_exception(e)
-                elif ev == buttons.TOP_RIGHT:
+                elif ev == self.button_select:
                     try:
                         self.on_select(self.entries[self.idx], self.idx)
                         self.select_time = utime.time_ms()
diff --git a/pycardium/modules/sys_display.c b/pycardium/modules/sys_display.c
index d038495445aec31d4b2c5cab285d0d5738f73d22..387101a1594efeb7ee63e7373c6666b2b6a5a896 100644
--- a/pycardium/modules/sys_display.c
+++ b/pycardium/modules/sys_display.c
@@ -84,15 +84,6 @@ static mp_obj_t mp_display_pixel(size_t n_args, const mp_obj_t *args)
 	int16_t y    = mp_obj_get_int(args[1]);
 	uint16_t col = get_color(args[2]);
 
-	//TODO: Move sanity checks to epicardium
-	if (x > 160 || x < 0) {
-		mp_raise_ValueError("X-Coords have to be 0 < x < 160");
-	}
-
-	if (y > 80 || y < 0) {
-		mp_raise_ValueError("Y-Coords have to be 0 < y < 80");
-	}
-
 	int res = epic_disp_pixel(x, y, col);
 	if (res < 0) {
 		mp_raise_OSError(-res);
@@ -129,15 +120,6 @@ static mp_obj_t mp_display_line(size_t n_args, const mp_obj_t *args)
 	uint16_t ls  = mp_obj_get_int(args[5]);
 	uint16_t ps  = mp_obj_get_int(args[6]);
 
-	//TODO: Move sanity checks to epicardium
-	if (xs > 160 || xs < 0 || xe > 160 || xe < 0) {
-		mp_raise_ValueError("X-Coords have to be 0 < x < 160");
-	}
-
-	if (ys > 80 || ys < 0 || ye > 80 || ye < 0) {
-		mp_raise_ValueError("Y-Coords have to be 0 < x < 80");
-	}
-
 	if (ls > 1 || ls < 0) {
 		mp_raise_ValueError("Line style has to be 0 or 1");
 	}
@@ -163,15 +145,6 @@ static mp_obj_t mp_display_rect(size_t n_args, const mp_obj_t *args)
 	uint16_t fs  = mp_obj_get_int(args[5]);
 	uint16_t ps  = mp_obj_get_int(args[6]);
 
-	//TODO: Move sanity checks to epicardium
-	if (xs > 160 || xs < 0 || xe > 160 || xe < 0) {
-		mp_raise_ValueError("X-Coords have to be 0 < x < 160");
-	}
-
-	if (ys > 80 || ys < 0 || ye > 80 || ye < 0) {
-		mp_raise_ValueError("Y-Coords have to be 0 < x < 80");
-	}
-
 	if (fs > 1 || fs < 0) {
 		mp_raise_ValueError("Fill style has to be 0 or 1");
 	}
diff --git a/pycardium/modules/sys_leds.c b/pycardium/modules/sys_leds.c
index a5f5747970e1060f545ab45702695a6e1985f247..16efdcc9e69b2dc2f4c24b2b7630832faeea8b19 100644
--- a/pycardium/modules/sys_leds.c
+++ b/pycardium/modules/sys_leds.c
@@ -202,8 +202,8 @@ static MP_DEFINE_CONST_FUN_OBJ_2(leds_set_rocket_obj, mp_leds_set_rocket);
 
 static mp_obj_t mp_leds_get_rocket(mp_obj_t led_in)
 {
-	int led     = mp_obj_get_int(led_in);
-	uint8_t ret = epic_leds_get_rocket(led);
+	int led = mp_obj_get_int(led_in);
+	int ret = epic_leds_get_rocket(led);
 	if (ret == -EINVAL) {
 		mp_raise_ValueError(
 			"invalid value: maybe the led does not exists"
diff --git a/pycardium/modules/utime.c b/pycardium/modules/utime.c
index 874d0d7a7fdbbc571f4066f71f4c74508fee0d95..d29708205e24a1b6f2c16e8b64b6d340feedb205 100644
--- a/pycardium/modules/utime.c
+++ b/pycardium/modules/utime.c
@@ -16,7 +16,9 @@
 
 /* MicroPython has its epoch at 2000-01-01. Our RTC is in UTC */
 #define EPOCH_OFFSET 946684800UL
-#define TZONE_OFFSET 7200UL
+
+/* Fixed time zone: CET */
+#define TZONE_OFFSET 3600UL
 
 static mp_obj_t time_set_time(mp_obj_t secs)
 {
@@ -200,6 +202,8 @@ static const mp_rom_map_elem_t time_module_globals_table[] = {
 	{ MP_ROM_QSTR(MP_QSTR_sleep), MP_ROM_PTR(&mp_utime_sleep_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_sleep_ms), MP_ROM_PTR(&mp_utime_sleep_ms_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_sleep_us), MP_ROM_PTR(&mp_utime_sleep_us_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_ticks_ms), MP_ROM_PTR(&mp_utime_ticks_ms_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_ticks_us), MP_ROM_PTR(&mp_utime_ticks_us_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_alarm), MP_ROM_PTR(&time_alarm_obj) },
 #if 0
 	/* TODO: Implement those */
diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h
index 767a69fbd78465b10c0ec5ea54608c8a0270670b..242c6a52f4e659879e634568fe39143b76397c79 100644
--- a/pycardium/mpconfigport.h
+++ b/pycardium/mpconfigport.h
@@ -31,6 +31,7 @@ int mp_hal_trng_read_int(void);
 #define MICROPY_PY_ALL_SPECIAL_METHODS      (1)
 #define MICROPY_PY_BUILTINS_HELP            (1)
 #define MICROPY_PY_BUILTINS_HELP_MODULES    (1)
+#define MICROPY_PY_BUILTINS_INPUT           (1)
 #define MICROPY_PY_UBINASCII                (1)
 #define MICROPY_PY_UHEAPQ                   (1)
 #define MICROPY_PY_UJSON                    (1)
@@ -44,6 +45,7 @@ int mp_hal_trng_read_int(void);
 #define MICROPY_PY_UTIME_MP_HAL             (1)
 #define MICROPY_PY_IO_FILEIO                (1)
 #define MICROPY_PY_UERRNO                   (1)
+#define MICROPY_PY_FRAMEBUF                 (1)
 
 /* Modules */
 #define MODULE_BHI160_ENABLED               (1)
diff --git a/pycardium/mphalport.c b/pycardium/mphalport.c
index 60cd9df0cdf1e08a01ffd3df84772d9c6276e7ac..5ae9cee49f60b7c133d3bdd97bc8c3a293d6b29c 100644
--- a/pycardium/mphalport.c
+++ b/pycardium/mphalport.c
@@ -31,6 +31,11 @@ void pycardium_hal_init(void)
 	 * a character becomes available.
 	 */
 	epic_interrupt_enable(EPIC_INT_UART_RX);
+
+	/*
+	 * Configure SysTick timer for 1ms period.
+	 */
+	SysTick_Config(SystemCoreClock / 1000);
 }
 
 /******************************************************************************
@@ -107,7 +112,7 @@ void epic_isr_ctrl_c(void)
 
 void mp_hal_set_interrupt_char(char c)
 {
-	if (c != -1) {
+	if (c != '\xFF') {
 		mp_obj_exception_clear_traceback(
 			MP_OBJ_FROM_PTR(&MP_STATE_VM(mp_kbd_exception))
 		);
@@ -120,23 +125,158 @@ void mp_hal_set_interrupt_char(char c)
 	}
 }
 
+/******************************************************************************
+ * SysTick timer at 1000 Hz
+ */
+
+static volatile uint64_t systick_count = 0;
+
+void SysTick_Handler(void)
+{
+	systick_count += 1;
+}
+
+/*
+ * Get an absolute "timestamp" in microseconds.
+ */
+static uint64_t systick_get_us()
+{
+	uint32_t irqsaved = __get_PRIMASK();
+	__set_PRIMASK(0);
+
+	uint64_t counts_per_us = SystemCoreClock / 1000000;
+	uint64_t us            = systick_count * 1000 +
+		      (SysTick->LOAD - SysTick->VAL) / counts_per_us;
+
+	__set_PRIMASK(irqsaved);
+
+	return us;
+}
+
+static void systick_delay_precise(uint32_t us)
+{
+	/*
+	 * Calculate how long the busy-spin needs to be.  As the very first
+	 * instruction, read the current timer value to ensure as little skew as
+	 * possible.
+	 *
+	 * Subtract 0.3us (constant_offset) to account for the duration of the
+	 * calculations.
+	 */
+	uint32_t count_to_overflow = SysTick->VAL;
+	uint32_t count_reload      = SysTick->LOAD;
+	uint32_t clocks_per_us     = SystemCoreClock / 1000000;
+	uint32_t constant_offset   = clocks_per_us * 3 / 10;
+	uint32_t delay_count       = us * clocks_per_us - constant_offset;
+
+	/*
+	 * Calculate the final count for both paths.  Marked as volatile so the
+	 * compiler can't move this into the branches and screw up the timing.
+	 */
+	volatile uint32_t count_final_direct = count_to_overflow - delay_count;
+	volatile uint32_t count_final_underflow =
+		count_reload - (delay_count - count_to_overflow);
+
+	if (delay_count > count_to_overflow) {
+		/*
+		 * Wait for the SysTick to underflow and then count down
+		 * to the final value.
+		 */
+		while (SysTick->VAL <= count_to_overflow ||
+		       SysTick->VAL > count_final_underflow) {
+			__NOP();
+		}
+	} else {
+		/*
+		 * Wait for the SysTick to count down to the final value.
+		 */
+		while (SysTick->VAL > count_final_direct) {
+			__NOP();
+		}
+	}
+}
+
+static void systick_delay_sleep(uint32_t us)
+{
+	uint64_t final_time = systick_get_us() + (uint64_t)us - 2;
+
+	while (1) {
+		uint64_t now = systick_get_us();
+
+		if (now >= final_time) {
+			break;
+		}
+
+		/*
+		 * Sleep with WFI if more than 1ms of delay is remaining.  The
+		 * SysTick interrupt is guaranteed to happen within any timespan
+		 * of 1ms.
+		 *
+		 * Use a critical section encompassing both the check and the
+		 * WFI to prevent a race-condition where the interrupt happens
+		 * just in between the check and WFI.
+		 */
+		uint32_t irqsaved = __get_PRIMASK();
+		__set_PRIMASK(0);
+		if ((now + 1000) < final_time) {
+			__WFI();
+		}
+		__set_PRIMASK(irqsaved);
+
+		/*
+		 * Handle pending MicroPython 'interrupts'.  This call could
+		 * potentially not return here when a handler raises an
+		 * exception.  Those will propagate outwards and thus make the
+		 * delay return early.
+		 *
+		 * One example of this happeing is the KeyboardInterrupt
+		 * (CTRL+C) which will abort the running code and exit to REPL.
+		 */
+		mp_handle_pending();
+	}
+}
+
+static void systick_delay(uint32_t us)
+{
+	if (us == 0)
+		return;
+
+	/*
+	 * For very short delays, use the systick_delay_precise() function which
+	 * delays with a microsecond accuracy.  For anything >1ms, use
+	 * systick_delay_sleep() which puts the CPU to sleep when nothing is
+	 * happening and also checks for MicroPython interrupts every now and
+	 * then.
+	 */
+	if (us < 1000) {
+		systick_delay_precise(us);
+	} else {
+		systick_delay_sleep(us);
+	}
+}
+
 /******************************************************************************
  * Time & Delay
  */
 
 void mp_hal_delay_ms(mp_uint_t ms)
 {
-	mxc_delay(ms * 1000);
+	systick_delay(ms * 1000);
 }
 
 void mp_hal_delay_us(mp_uint_t us)
 {
-	mxc_delay(us);
+	systick_delay(us);
 }
 
 mp_uint_t mp_hal_ticks_ms(void)
 {
-	return 0;
+	return (mp_uint_t)(systick_get_us() / 1000);
+}
+
+mp_uint_t mp_hal_ticks_us(void)
+{
+	return (mp_uint_t)systick_get_us();
 }
 
 /******************************************************************************
diff --git a/tools/ecg-plot.py b/tools/ecg-plot.py
new file mode 100644
index 0000000000000000000000000000000000000000..119bdde96113660619bce4c1978ac58b2bda5ed6
--- /dev/null
+++ b/tools/ecg-plot.py
@@ -0,0 +1,43 @@
+# vim: set ts=4 sw=4 tw=0 et pm=:
+import numpy
+import sys
+import matplotlib.pyplot as plt
+
+
+def read(file_name):
+    signal = numpy.fromfile(file_name, dtype=numpy.int16)
+    return signal
+
+
+signal = read(sys.argv[1])
+factor = -1
+count = 5
+offset = 0
+
+signal = signal[offset * 128 :]
+
+count = min(min(len(signal) / 1280 + 1, 10), count)
+
+font = {"family": "serif", "color": "darkred", "weight": "normal", "size": 16}
+
+title = False
+
+for i in range(count):
+    plt.subplot(count, 1, i + 1)
+    sub_signal = signal[i * 1280 : (i + 1) * 1280] * factor
+
+    # pad with 0 as needed.
+    # TODO: find a better solution to visialize this
+    sub_signal = numpy.pad(sub_signal, (0, 1280 - len(sub_signal)), "constant")
+
+    time_scale = (
+        numpy.array(range(i * 1280, i * 1280 + len(sub_signal))) / 128.0 + offset
+    )
+
+    plt.plot(time_scale, sub_signal, "-")
+    if not title:
+        plt.title("File: %s" % sys.argv[1].split("/")[-1], fontdict=font)
+        title = True
+
+plt.xlabel("time (s)", fontdict=font)
+plt.show()
diff --git a/tools/imageconvert.py b/tools/imageconvert.py
deleted file mode 100755
index 72ea36139b31c3115307968c73ce5cccfa0fb483..0000000000000000000000000000000000000000
--- a/tools/imageconvert.py
+++ /dev/null
@@ -1,30 +0,0 @@
-#!/usr/bin/env python3
-
-from PIL import Image
-import sys
-
-if len(sys.argv) < 2:
-    print("Usage: %s <image file>" % sys.argv[0])
-    sys.exit(1)
-
-im = Image.open(sys.argv[1])
-
-out_name = '.'.join(sys.argv[1].split('.')[:-1])
-
-print(out_name)
-out = open(out_name + ".h", 'w')
-
-out.write("const unsigned char %s[] = {\n" % out_name.split('/')[-1])
-
-for x in range(im.size[1]):
-    for y in range(im.size[0]):
-        px = im.getpixel((y, x))
-
-        px16 = ((px[0] >> 3) << 11) | ((px[1] >> 2) << 5) | (px[2] >> 3)
-
-        px16h = (px16 & 0xFF00) >> 8
-        px16l = px16 & 0xFF
-
-        out.write("0x%02x, 0x%02x,\n" % (px16l, px16h))
-
-out.write("};\n")
diff --git a/tools/version-image.py b/tools/version-image.py
new file mode 100755
index 0000000000000000000000000000000000000000..0a20327c25d5e669e05ed90b5255e888a0ac61e5
--- /dev/null
+++ b/tools/version-image.py
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+import argparse
+import os
+from PIL import Image
+
+
+def main() -> None:
+    parser = argparse.ArgumentParser(
+        description="""\
+Update the epicardium version-splash.  NOTE: You need to adjust
+epicardium/main.c if you want to actually see your version splash!!
+"""
+    )
+
+    parser.add_argument("image", help="Path to version image")
+
+    args = parser.parse_args()
+
+    im = Image.open(args.image)
+    assert im.size[0] == 160, "Image must be 160 pixels wide"
+    assert im.size[1] == 80, "Image must be 80 pixels high)"
+
+    # find version-splash.h
+    vsplash = os.path.join(os.path.dirname(__file__), "../epicardium/version-splash.h")
+
+    with open(vsplash, "w") as f:
+        tmp = """\
+#pragma once
+#include <stdint.h>
+
+/* clang-format off */
+const uint8_t version_splash[] = {
+"""
+        f.write(tmp)
+
+        for x in range(im.size[1]):
+            for y in range(im.size[0]):
+                px = im.getpixel((y, x))
+
+                px565 = ((px[0] >> 3) << 11) | ((px[1] >> 2) << 5) | (px[2] >> 3)
+                byte_high = (px565 & 0xFF00) >> 8
+                byte_low = px565 & 0xFF
+
+                if y % 4 == 0:
+                    f.write("\t")
+
+                f.write("0x{:02x}, 0x{:02x},".format(byte_low, byte_high))
+
+                if y % 4 == 3:
+                    f.write("\n")
+                else:
+                    f.write(" ")
+
+        f.write("};\n")
+
+
+if __name__ == "__main__":
+    main()