diff --git a/Documentation/index.rst b/Documentation/index.rst
index 77f03db287cfdc576d6af03ad153cdf03e8cae8c..561fb9bddf931dc6492f05bbbc94654fc4c8c8d0 100644
--- a/Documentation/index.rst
+++ b/Documentation/index.rst
@@ -22,6 +22,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the
 
    pycardium/overview
    pycardium/stdlib
+   pycardium/bhi160
    pycardium/bme680
    pycardium/buttons
    pycardium/color
@@ -32,10 +33,10 @@ Last but not least, if you want to start hacking the lower-level firmware, the
    pycardium/os
    pycardium/personal_state
    pycardium/power
+   pycardium/pride
    pycardium/simple_menu
    pycardium/utime
    pycardium/vibra
-   pycardium/pride
 
 .. toctree::
    :maxdepth: 1
diff --git a/Documentation/pycardium/bhi160.rst b/Documentation/pycardium/bhi160.rst
new file mode 100644
index 0000000000000000000000000000000000000000..6905f779c29d0d92b5a73a0c0020bdeace2c1df1
--- /dev/null
+++ b/Documentation/pycardium/bhi160.rst
@@ -0,0 +1,102 @@
+.. py:module:: bhi160
+
+``bhi160`` - Sensor Fusion
+==========================
+.. versionadded:: 1.4
+
+Supports the BHI160 sensor on the card10 for accelerometer, gyroscope...
+
+**Example**:
+
+.. code-block:: python
+
+   import bhi160
+   import utime
+   
+   bhi = bhi160.BHI160Orientation()
+
+    while True:
+        samples = bhi.read()
+        print(samples)
+        utime.sleep(0.5)
+
+
+.. class:: bhi160.BHI160Orientation(sample_rate,dynamic_range,callback,sample_buffer_len)
+
+    Orientation of the BHI160
+
+    Parameters:
+        sample_rate: int, optional
+            Sample rate (default is 4)
+        dynamic_range: int, optional
+            Dynamic range (default is 2)
+        callback: callable, optional
+            Call this callback when enough data is collected (default is None)
+
+            .. todo:: The callback functionality is untested, so do not be confused if it does not work.
+        sample_buffer_len: int, optional
+            Length of sample buffer (default is 200)
+
+   .. py:method:: read():
+
+   Read sensor values
+
+   :returns: Collected sensor values as list
+
+   .. py:method:: close():
+
+   Close the connection to the sensor
+
+   
+.. class:: bhi160.BHI160Accelerometer
+
+    Accelerometer of the BHI160
+
+    Parameters:
+        sample_rate: int, optional
+            Sample rate (default is 4)
+        dynamic_range: int, optional
+            Dynamic range (default is 2)
+        callback: callable, optional
+            Call this callback when enough data is collected (default is None)
+
+            .. todo:: The callback functionality is untested, so do not be confused if it does not work.
+        sample_buffer_len: int, optional
+            Length of sample buffer (default is 200)
+
+   .. py:method:: read():
+
+   Read sensor values
+
+   :returns: Collected sensor values as list
+
+   .. py:method:: close():
+
+   Close the connection to the sensor
+
+.. class:: bhi160.BHI160Gyroscope
+
+    Gyroscope of the BHI160
+
+    Parameters:
+        sample_rate: int, optional
+            Sample rate (default is 4)
+        dynamic_range: int, optional
+            Dynamic range (default is 2)
+        callback: callable, optional
+            Call this callback when enough data is collected (default is None)
+
+            .. todo:: The callback functionality is untested, so do not be confused if it does not work.
+        sample_buffer_len: int, optional
+            Length of sample buffer (default is 200)
+
+   .. py:method:: read():
+
+   Read sensor values
+
+   :returns: Collected sensor values as list
+
+   .. py:method:: close():
+
+   Close the connection to the sensor
+
diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 54201a181a5dc9b7b85ec4c2a3ea4f36402a11ab..f701dc4f17d652913e02536582bec11fdca54fee 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -124,9 +124,14 @@ typedef _Bool bool;
 #define API_BHI160_DISABLE         0xe1
 #define API_BHI160_DISABLE_ALL     0xe2
 
-#define API_MAX86150_INIT		0xf0
-#define API_MAX86150_GET_DATA		0xf1
-#define API_MAX86150_SET_LED_AMPLITUDE	0xf2
+#define API_MAX30001_ENABLE        0xf0
+#define API_MAX30001_DISABLE       0xf1
+
+#define API_MAX86150_INIT		0x0100
+#define API_MAX86150_GET_DATA		0x0101
+#define API_MAX86150_SET_LED_AMPLITUDE	0x0102
+
+>>>>>>> 4f32a5ceaf749a0f8bc8b2b6ec706e118dd368fd
 
 /* clang-format on */
 
@@ -175,9 +180,11 @@ API_ISR(EPIC_INT_BHI160_ACCELEROMETER, epic_isr_bhi160_accelerometer);
 API_ISR(EPIC_INT_BHI160_ORIENTATION, epic_isr_bhi160_orientation);
 #define EPIC_INT_BHI160_GYROSCOPE       6
 API_ISR(EPIC_INT_BHI160_GYROSCOPE, epic_isr_bhi160_gyroscope);
+#define EPIC_INT_MAX30001_ECG           7
+API_ISR(EPIC_INT_MAX30001_ECG, epic_isr_max30001_ecg);
 
 /* Number of defined interrupts. */
-#define EPIC_INT_NUM                    7
+#define EPIC_INT_NUM                    8
 /* clang-format on */
 
 /*
@@ -820,9 +827,9 @@ enum personal_state {
     STATE_NO_CONTACT = 1,
     /** ``2``, "chaos" - Adventure time - blue led, short blink, long blink. */
     STATE_CHAOS = 2,
-    /** ``3``, "communication" - want to learn something or have a nice conversation - green led, long blinks. */
+    /** ``3``, "communication" - want to learn something or have a nice conversation - yellow led, long blinks. */
     STATE_COMMUNICATION = 3,
-    /** ``4``, "camp" - I am focussed on self-, camp-, or community maintenance - yellow led, fade on and off. */
+    /** ``4``, "camp" - I am focussed on self-, camp-, or community maintenance - green led, fade on and off. */
     STATE_CAMP = 4,
     /** STATE_MAX gives latest value and count of possible STATEs**/
     STATE_MAX = 5,
@@ -1632,4 +1639,68 @@ API_ISR(EPIC_INT_RTC_ALARM, epic_isr_rtc_alarm);
  */
 API(API_TRNG_READ, int epic_trng_read(uint8_t *dest, size_t size));
 
+/**
+ * MAX30001 API
+ * ----------
+ */
+
+/**
+ * Configuration for a MAX30001 sensor.
+ *
+ * This struct is used when enabling the sensor using
+ * :c:func:`epic_max30001_enable_sensor`.
+ */
+struct max30001_sensor_config {
+	/**
+	 * Number of samples Epicardium should keep for this sensor.  Do not set
+	 * this number too high as the sample buffer will eat RAM.
+	 */
+	size_t sample_buffer_len;
+	/**
+	 * Sample rate for the sensor in Hz.
+	 */
+	uint16_t sample_rate;
+
+	/**
+	 * Set to true if the second lead comes from USB-C
+	 */
+	bool usb;
+
+	/**
+	* Set to true if the interal lead bias of the MAX30001 is to be used.
+	*/
+	bool bias;
+
+	/** Always zero. Reserved for future parameters. */
+	uint8_t _padding[8];
+};
+
+/**
+ * Enable a MAX30001 ecg sensor.  Calling this funciton will instruct the
+ * MAX30001 to collect data for this sensor.  You can then
+ * retrieve the samples using :c:func:`epic_stream_read`.
+ *
+ * :param max30001_sensor_config* config: Configuration for this sensor.
+ * :returns: A sensor descriptor which can be used with
+ *    :c:func:`epic_stream_read` or a negative error value:
+ *
+ *    - ``-EBUSY``:  The MAX30001 driver is currently busy with other tasks and
+ *      could not be acquired for enabling a sensor.
+ *
+ * .. versionadded:: 1.6
+ */
+API(API_MAX30001_ENABLE, int epic_max30001_enable_sensor(
+	struct max30001_sensor_config *config
+));
+
+/**
+ * Disable MAX30001
+ *
+ * .. versionadded:: 1.6
+ */
+API(API_MAX30001_DISABLE, int epic_max30001_disable_sensor(
+void
+));
+
+
 #endif /* _EPICARDIUM_H */
diff --git a/epicardium/main.c b/epicardium/main.c
index 8c260528f12a535501a22bbde882b8d4f5d0fa88..c7f58a05c889fc0c47eafb97d1f2995a8afce881 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -62,6 +62,17 @@ int main(void)
 		abort();
 	}
 
+	/* MAX30001 */
+	if (xTaskCreate(
+		    vMAX30001Task,
+		    (const char *)"MAX30001 Driver",
+		    configMINIMAL_STACK_SIZE * 2,
+		    NULL,
+		    tskIDLE_PRIORITY + 1,
+		    NULL) != pdPASS) {
+		LOG_CRIT("startup", "Failed to create %s task!", "MAX30001");
+		abort();
+	}
 	/* API */
 	if (xTaskCreate(
 		    vApiDispatcher,
diff --git a/epicardium/modules/MAX30003.h b/epicardium/modules/MAX30003.h
new file mode 100644
index 0000000000000000000000000000000000000000..24c5d97f3c33ab14441f87a7221a0609d640205e
--- /dev/null
+++ b/epicardium/modules/MAX30003.h
@@ -0,0 +1,278 @@
+/* clang-format off */
+/*******************************************************************************
+ * Copyright (C) 2017 Maxim Integrated Products, Inc., All Rights Reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included
+ * in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+ * IN NO EVENT SHALL MAXIM INTEGRATED BE LIABLE FOR ANY CLAIM, DAMAGES
+ * OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * Except as contained in this notice, the name of Maxim Integrated
+ * Products, Inc. shall not be used except as stated in the Maxim Integrated
+ * Products, Inc. Branding Policy.
+ *
+ * The mere transfer of this software does not imply any licenses
+ * of trade secrets, proprietary technology, copyrights, patents,
+ * trademarks, maskwork rights, or any other form of intellectual
+ * property whatsoever. Maxim Integrated Products, Inc. retains all
+ * ownership rights.
+ *******************************************************************************
+ */
+
+
+#ifndef _MAX30003_H_
+#define _MAX30003_H_
+
+
+    ///MAX30003 Registers
+    enum Registers_e
+    {
+        NO_OP          = 0x00,
+        STATUS         = 0x01,
+        EN_INT         = 0x02,
+        EN_INT2        = 0x03,
+        MNGR_INT       = 0x04,
+        MNGR_DYN       = 0x05,
+        SW_RST         = 0x08,
+        SYNCH          = 0x09,
+        FIFO_RST       = 0x0A,
+        INFO           = 0x0F,
+        CNFG_GEN       = 0x10,
+        CNFG_ALL       = 0x12,
+        CNFG_EMUX      = 0x14,
+        CNFG_ECG       = 0x15,
+        CNFG_RTOR1     = 0x1D,
+        CNFG_RTOR2     = 0x1E,
+        ECG_FIFO_BURST = 0x20,
+        ECG_FIFO       = 0x21,
+        RTOR           = 0x25,
+        NO_OP2         = 0x7F
+    };
+
+    ///Status register bits
+    union Status_u
+    {
+        ///Access all bits
+        uint32_t all;
+
+        ///Access individual bits
+        struct
+        {
+            uint32_t loff_nl    : 1;
+            uint32_t loff_nh    : 1;
+            uint32_t loff_pl    : 1;
+            uint32_t loff_ph    : 1;
+            uint32_t reserved1  : 4;
+            uint32_t pllint     : 1;
+            uint32_t samp       : 1;
+            uint32_t rrint      : 1;
+            uint32_t lonint     : 1;
+            uint32_t reserved2  : 8;
+            uint32_t dcloffint : 1;
+            uint32_t fstint     : 1;
+            uint32_t eovf       : 1;
+            uint32_t eint       : 1;
+            uint32_t reserved3  : 8;
+        }bits;
+    };
+
+    ///Enable Interrupt registers bits
+    union EnableInterrupts_u
+    {
+        ///Access all bits
+        uint32_t all;
+
+        ///Access individual bits
+        struct 
+        {
+            uint32_t intb_type    : 2;
+            uint32_t reserved1    : 6;
+            uint32_t en_pllint    : 1;
+            uint32_t en_samp      : 1;
+            uint32_t en_rrint     : 1;
+            uint32_t en_loint     : 1;
+            uint32_t reserved2    : 8;
+            uint32_t en_dcloffint : 1;
+            uint32_t en_fstint    : 1;
+            uint32_t en_eovf      : 1;
+            uint32_t en_eint      : 1;
+            uint32_t reserved3    : 8;
+        }bits;
+    };
+
+    ///Manage Interrupt register bits
+    union ManageInterrupts_u
+    {
+        ///Access all bits
+        uint32_t all;
+
+        ///Access individual bits
+        struct 
+        {
+            uint32_t samp_it   : 4;
+            uint32_t clr_samp  : 1;
+            uint32_t reserved1 : 1;
+            uint32_t clr_rrint : 2;
+            uint32_t clr_fast  : 1;
+            uint32_t reserved2 : 12;
+            uint32_t efit      : 5;
+            uint32_t reserved3 : 8;
+        }bits;
+    };
+
+    ///Manage Dynamic Modes register bits
+    union ManageDynamicModes_u
+    {
+        ///Access all bits
+        uint32_t all;
+
+        ///Access individual bits
+        struct 
+        {
+            uint32_t reserved1 : 16;
+            uint32_t fast_th   : 6;
+            uint32_t fast      : 2;
+            uint32_t reserved2 : 8;
+        }bits;
+    };
+
+    ///General Configuration bits
+    union GeneralConfiguration_u
+    {
+        ///Access all bits
+        uint32_t all;
+
+        ///Access individual bits
+        struct 
+        {
+            uint32_t rbiasn     : 1;
+            uint32_t rbiasp     : 1;
+            uint32_t rbiasv     : 2;
+            uint32_t en_rbias   : 2;
+            uint32_t vth        : 2;
+            uint32_t imag       : 3;
+            uint32_t ipol       : 1;
+            uint32_t en_dcloff  : 2;
+            uint32_t reserved1  : 5;
+            uint32_t en_ecg     : 1;
+            uint32_t fmstr      : 2;
+            uint32_t en_ulp_lon : 2;
+            uint32_t reserved2  : 8;
+        }bits;
+    };
+
+    ///Cal Configuration bits
+    union CalConfiguration_u
+    {
+        ///Access all bits
+        uint32_t all;
+
+        ///Access individual bits
+        struct 
+        {
+            uint32_t thigh     : 11;
+            uint32_t fifty     : 1;
+            uint32_t fcal      : 3;
+            uint32_t reserved1 : 5;
+            uint32_t vmag      : 1;
+            uint32_t vmode     : 1;
+            uint32_t en_vcal   : 1;
+            uint32_t reserved2 : 9;
+
+        }bits;
+    };
+
+    ///Mux Configuration bits
+    union MuxConfiguration_u
+    {
+        ///Access all bits
+        uint32_t all;
+
+        ///Access individual bits
+        struct 
+        {
+            uint32_t reserved1 : 16;
+            uint32_t caln_sel  : 2;
+            uint32_t calp_sel  : 2;
+            uint32_t openn     : 1;
+            uint32_t openp     : 1;
+            uint32_t reserved2 : 1;
+            uint32_t pol       : 1;
+            uint32_t reserved3 : 8;
+        }bits;
+    };
+
+    ///ECG Configuration bits
+    union ECGConfiguration_u
+    {
+        ///Access all bits
+        uint32_t all;
+
+        ///Access individual bits
+        struct 
+        {
+            uint32_t reserved1 : 12;
+            uint32_t dlpf      : 2;
+            uint32_t dhpf      : 1;
+            uint32_t reserved2 : 1;
+            uint32_t gain      : 2;
+            uint32_t reserved3 : 4;
+            uint32_t rate      : 2;
+            uint32_t reserved4 : 8;
+        }bits;
+    };
+
+    ///RtoR1 Configuration bits
+    union RtoR1Configuration_u
+    {
+        ///Access all bits
+        uint32_t all;
+
+        ///Access individual bits
+        struct 
+        {
+            uint32_t reserved1 : 8;
+            uint32_t ptsf      : 4;
+            uint32_t pavg      : 2;
+            uint32_t reserved2 : 1;
+            uint32_t en_rtor   : 1;
+            uint32_t rgain     : 4;
+            uint32_t wndw      : 4;
+            uint32_t reserved3 : 8;
+        }bits;
+    };
+
+    ///RtoR2 Configuration bits
+    union RtoR2Configuration_u
+    {
+        ///Access all bits
+        uint32_t all;
+
+        ///Access individual bits
+        struct 
+        {
+            uint32_t reserved1 : 8;
+            uint32_t rhsf      : 3;
+            uint32_t reserved2 : 1;
+            uint32_t ravg      : 2;
+            uint32_t reserved3 : 2;
+            uint32_t hoff      : 6;
+            uint32_t reserved4 : 10;
+        }bits;
+    };
+
+#endif /* _MAX30003_H_ */
+/* clang-format on */
diff --git a/epicardium/modules/hardware.c b/epicardium/modules/hardware.c
index 3b2c0696e127e9681344404ca0dba19d95c5913c..6148513eb03014b19ff01873701ae3aedefb582f 100644
--- a/epicardium/modules/hardware.c
+++ b/epicardium/modules/hardware.c
@@ -269,5 +269,7 @@ int hardware_reset(void)
 	 */
 	epic_bme680_deinit();
 
+	epic_max30001_disable_sensor();
+
 	return 0;
 }
diff --git a/epicardium/modules/max30001.c b/epicardium/modules/max30001.c
new file mode 100644
index 0000000000000000000000000000000000000000..5cc3ef3654e19b6ea298ab7ba8e019676dfc4332
--- /dev/null
+++ b/epicardium/modules/max30001.c
@@ -0,0 +1,434 @@
+#include <stdio.h>
+#include <string.h>
+
+#include "gpio.h"
+#include "pmic.h"
+#include "spi.h"
+
+#include "MAX30003.h"
+
+#include "FreeRTOS.h"
+#include "task.h"
+#include "semphr.h"
+#include "queue.h"
+
+#include "api/interrupt-sender.h"
+#include "epicardium.h"
+#include "modules/log.h"
+#include "modules/modules.h"
+#include "modules/stream.h"
+
+/* Ticks to wait when trying to acquire lock */
+#define LOCK_WAIT pdMS_TO_TICKS(MAX30001_MUTEX_WAIT_MS)
+
+/* Interrupt Pin */
+static const gpio_cfg_t max30001_interrupt_pin = {
+	PORT_1, PIN_12, GPIO_FUNC_IN, GPIO_PAD_PULL_UP
+};
+
+static const gpio_cfg_t analog_switch = {
+	PORT_0, PIN_31, GPIO_FUNC_OUT, GPIO_PAD_NONE
+};
+
+/* clang-format on */
+
+/* MAX30001 Task ID */
+static TaskHandle_t max30001_task_id = NULL;
+
+/* MAX30001 Mutex */
+static StaticSemaphore_t max30001_mutex_data;
+static SemaphoreHandle_t max30001_mutex = NULL;
+
+/* Stream */
+static struct stream_info max30001_stream;
+;
+
+/* Active */
+static bool max30001_sensor_active = false;
+
+static int ecg_enable(int sample_rate, bool enable_internal_pull);
+static int ecg_disable(void);
+
+/* -- API -------------------------------------------------------------- {{{ */
+int epic_max30001_enable_sensor(struct max30001_sensor_config *config)
+{
+	int result = 0;
+
+	result = hwlock_acquire(HWLOCK_SPI_ECG, pdMS_TO_TICKS(100));
+	if (result < 0) {
+		return result;
+	}
+
+	if (xSemaphoreTake(max30001_mutex, LOCK_WAIT) != pdTRUE) {
+		result = -EBUSY;
+		goto out_free_spi;
+	}
+
+	struct stream_info *stream = &max30001_stream;
+	;
+	stream->item_size = sizeof(uint16_t);
+	stream->queue =
+		xQueueCreate(config->sample_buffer_len, stream->item_size);
+	if (stream->queue == NULL) {
+		result = -ENOMEM;
+		goto out_free_both;
+	}
+
+	result = stream_register(SD_MAX30001_ECG, stream);
+	if (result < 0) {
+		vQueueDelete(stream->queue);
+		goto out_free_both;
+	}
+
+	result = ecg_enable(config->sample_rate, config->bias);
+
+	if (result < 0) {
+		vQueueDelete(stream->queue);
+		goto out_free_both;
+	}
+
+	if (config->usb) {
+		GPIO_OutSet(&analog_switch); // USB
+	} else {
+		GPIO_OutClr(&analog_switch); // Wrist
+	}
+
+	max30001_sensor_active = true;
+	result                 = SD_MAX30001_ECG;
+
+out_free_both:
+	xSemaphoreGive(max30001_mutex);
+out_free_spi:
+	hwlock_release(HWLOCK_SPI_ECG);
+	return result;
+}
+
+int epic_max30001_disable_sensor(void)
+{
+	int result = 0;
+
+	result = hwlock_acquire(HWLOCK_SPI_ECG, pdMS_TO_TICKS(100));
+	if (result < 0) {
+		return result;
+	}
+
+	if (xSemaphoreTake(max30001_mutex, LOCK_WAIT) != pdTRUE) {
+		result = -EBUSY;
+		goto out_free_spi;
+	}
+
+	struct stream_info *stream = &max30001_stream;
+	result                     = stream_deregister(SD_MAX30001_ECG, stream);
+	if (result < 0) {
+		goto out_free_both;
+	}
+
+	vQueueDelete(stream->queue);
+	stream->queue = NULL;
+	result        = ecg_disable();
+	if (result < 0) {
+		goto out_free_both;
+	}
+
+	max30001_sensor_active = false;
+
+	result = 0;
+out_free_both:
+	xSemaphoreGive(max30001_mutex);
+out_free_spi:
+	hwlock_release(HWLOCK_SPI_ECG);
+	return result;
+}
+
+/* }}} */
+
+/* -- Driver ----------------------------------------------------------- {{{ */
+/*
+ * Handle a single packet from the FIFO.  For most sensors this means pushing
+ * the sample into its sample queue.
+ */
+static void max30001_handle_samples(int16_t *sensor_data, int16_t n)
+{
+	if (max30001_stream.queue == NULL) {
+		return;
+	}
+
+	while (n--) {
+		uint16_t data = -*sensor_data++;
+		if (xQueueSend(
+			    max30001_stream.queue,
+			    &data,
+			    MAX30001_MUTEX_WAIT_MS) != pdTRUE) {
+			LOG_WARN(
+				"max30001",
+				"queue full"); // TODO; handle queue full
+		}
+	}
+	api_interrupt_trigger(EPIC_INT_MAX30001_ECG);
+}
+
+/***** Functions *****/
+static uint32_t ecg_read_reg(uint8_t reg)
+{
+	spi_req_t req;
+	uint8_t tx_data[] = { (reg << 1) | 1, 0, 0, 0 };
+	uint8_t rx_data[] = { 0, 0, 0, 0 };
+	req.tx_data       = tx_data;
+	req.rx_data       = rx_data;
+	req.len           = 4;
+	req.bits          = 8;
+	req.width         = SPI17Y_WIDTH_1;
+	req.ssel          = 0;
+	req.deass         = 1;
+	req.ssel_pol      = SPI17Y_POL_LOW;
+	req.tx_num        = 0;
+	req.rx_num        = 0;
+
+	SPI_MasterTrans(SPI0, &req);
+
+	return (rx_data[1] << 16) | (rx_data[2] << 8) | rx_data[3];
+}
+
+static void ecg_write_reg(uint8_t reg, uint32_t data)
+{
+	//printf("write %02x %06lx\n", reg, data);
+	spi_req_t req;
+	uint8_t tx_data[] = {
+		(reg << 1) | 0, data >> 16, (data >> 8) & 0xFF, data & 0xFF
+	};
+	uint8_t rx_data[] = { 0, 0, 0, 0 };
+	req.tx_data       = tx_data;
+	req.rx_data       = rx_data;
+	req.len           = 4;
+	req.bits          = 8;
+	req.width         = SPI17Y_WIDTH_1;
+	req.ssel          = 0;
+	req.deass         = 1;
+	req.ssel_pol      = SPI17Y_POL_LOW;
+	req.tx_num        = 0;
+	req.rx_num        = 0;
+
+	SPI_MasterTrans(SPI0, &req);
+}
+
+static int ecg_enable(int sample_rate, bool enable_internal_pull)
+{
+	// Reset ECG to clear registers
+	ecg_write_reg(SW_RST, 0);
+
+	// General config register setting
+	union GeneralConfiguration_u CNFG_GEN_r;
+	CNFG_GEN_r.bits.en_ecg = 1; // Enable ECG channel
+	if (enable_internal_pull) {
+		CNFG_GEN_r.bits.rbiasn =
+			1; // Enable resistive bias on negative input
+		CNFG_GEN_r.bits.rbiasp =
+			1; // Enable resistive bias on positive input
+		CNFG_GEN_r.bits.en_rbias = 1; // Enable resistive bias
+	} else {
+		CNFG_GEN_r.bits.rbiasn =
+			0; // Enable resistive bias on negative input
+		CNFG_GEN_r.bits.rbiasp =
+			0; // Enable resistive bias on positive input
+		CNFG_GEN_r.bits.en_rbias = 0; // Enable resistive bias
+	}
+
+	CNFG_GEN_r.bits.imag      = 2; // Current magnitude = 10nA
+	CNFG_GEN_r.bits.en_dcloff = 1; // Enable DC lead-off detection
+	ecg_write_reg(CNFG_GEN, CNFG_GEN_r.all);
+
+	// ECG Config register setting
+	union ECGConfiguration_u CNFG_ECG_r;
+	CNFG_ECG_r.bits.dlpf = 1; // Digital LPF cutoff = 40Hz
+	CNFG_ECG_r.bits.dhpf = 1; // Digital HPF cutoff = 0.5Hz
+	//CNFG_ECG_r.bits.gain = 3;       // ECG gain = 160V/V
+	CNFG_ECG_r.bits.gain = 0;
+	if (sample_rate == 128) {
+		CNFG_ECG_r.bits.rate = 2; // Sample rate = 128 sps
+	} else if (sample_rate == 256) {
+		CNFG_ECG_r.bits.rate = 1; // Sample rate = 256 sps
+	} else {
+		return -EINVAL;
+	}
+
+	ecg_write_reg(CNFG_ECG, CNFG_ECG_r.all);
+
+	//R-to-R configuration
+	union RtoR1Configuration_u CNFG_RTOR_r;
+	CNFG_RTOR_r.bits.en_rtor = 1; // Enable R-to-R detection
+	ecg_write_reg(CNFG_RTOR1, CNFG_RTOR_r.all);
+
+	//Manage interrupts register setting
+	union ManageInterrupts_u MNG_INT_r;
+	MNG_INT_r.bits.efit      = 0b00011; // Assert EINT w/ 4 unread samples
+	MNG_INT_r.bits.clr_rrint = 0b01; // Clear R-to-R on RTOR reg. read back
+	ecg_write_reg(MNGR_INT, MNG_INT_r.all);
+
+	//Enable interrupts register setting
+	union EnableInterrupts_u EN_INT_r;
+	EN_INT_r.all            = 0;
+	EN_INT_r.bits.en_eint   = 1; // Enable EINT interrupt
+	EN_INT_r.bits.en_rrint  = 0; // Disable R-to-R interrupt
+	EN_INT_r.bits.intb_type = 3; // Open-drain NMOS with internal pullup
+	ecg_write_reg(EN_INT, EN_INT_r.all);
+
+	//Dyanmic modes config
+	union ManageDynamicModes_u MNG_DYN_r;
+	MNG_DYN_r.bits.fast = 0; // Fast recovery mode disabled
+	ecg_write_reg(MNGR_DYN, MNG_DYN_r.all);
+
+	// MUX Config
+	union MuxConfiguration_u CNFG_MUX_r;
+	CNFG_MUX_r.bits.openn = 0; // Connect ECGN to AFE channel
+	CNFG_MUX_r.bits.openp = 0; // Connect ECGP to AFE channel
+	ecg_write_reg(CNFG_EMUX, CNFG_MUX_r.all);
+
+	ecg_write_reg(SYNCH, 0);
+
+	return 0;
+}
+
+static int ecg_disable(void)
+{
+	// TODO
+	return 0;
+}
+/*
+ * Fetch all data available from FIFO buffer and handle all data
+ * contained in it.
+ */
+static int max30001_fetch_fifo(void)
+{
+	int result = 0;
+
+	result = hwlock_acquire(HWLOCK_SPI_ECG, pdMS_TO_TICKS(100));
+	if (result < 0) {
+		return result;
+	}
+
+	if (xSemaphoreTake(max30001_mutex, LOCK_WAIT) != pdTRUE) {
+		result = -EBUSY;
+		goto out_free_spi;
+	}
+
+	uint32_t ecgFIFO, readECGSamples, ETAG[32], status;
+	int16_t ecgSample[32];
+	const int EINT_STATUS_MASK       = 1 << 23;
+	const int FIFO_OVF_MASK          = 0x7;
+	const int FIFO_VALID_SAMPLE_MASK = 0x0;
+	const int FIFO_FAST_SAMPLE_MASK  = 0x1;
+	const int ETAG_BITS_MASK         = 0x7;
+
+	status = ecg_read_reg(STATUS); // Read the STATUS register
+
+	// Check if EINT interrupt asserted
+	if ((status & EINT_STATUS_MASK) == EINT_STATUS_MASK) {
+		readECGSamples = 0; // Reset sample counter
+
+		do {
+			ecgFIFO = ecg_read_reg(ECG_FIFO); // Read FIFO
+			ecgSample[readECGSamples] =
+				ecgFIFO >> 8; // Isolate voltage data
+			ETAG[readECGSamples] =
+				(ecgFIFO >> 3) & ETAG_BITS_MASK; // Isolate ETAG
+			readECGSamples++; // Increment sample counter
+
+			// Check that sample is not last sample in FIFO
+		} while (ETAG[readECGSamples - 1] == FIFO_VALID_SAMPLE_MASK ||
+			 ETAG[readECGSamples - 1] == FIFO_FAST_SAMPLE_MASK);
+
+		// Check if FIFO has overflowed
+		if (ETAG[readECGSamples - 1] == FIFO_OVF_MASK) {
+			ecg_write_reg(FIFO_RST, 0); // Reset FIFO
+			LOG_WARN(
+				"max30001",
+				"fifo overflow"); // TODO; handle fifo full
+		}
+		max30001_handle_samples(ecgSample, readECGSamples);
+	}
+
+	xSemaphoreGive(max30001_mutex);
+out_free_spi:
+	hwlock_release(HWLOCK_SPI_ECG);
+	return result;
+}
+
+/*
+ * Callback for the MAX30001 interrupt pin.  This callback is called from the
+ * SDK's GPIO interrupt driver, in interrupt context.
+ */
+static void max300001_interrupt_callback(void *_)
+{
+	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
+
+	if (max30001_task_id != NULL) {
+		vTaskNotifyGiveFromISR(
+			max30001_task_id, &xHigherPriorityTaskWoken
+		);
+		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
+	}
+}
+/* }}} */
+
+void vMAX30001Task(void *pvParameters)
+{
+	max30001_task_id = xTaskGetCurrentTaskHandle();
+	max30001_mutex   = xSemaphoreCreateMutexStatic(&max30001_mutex_data);
+
+	int lockret = hwlock_acquire(HWLOCK_SPI_ECG, pdMS_TO_TICKS(100));
+	if (lockret < 0) {
+		LOG_CRIT("max30001", "Failed to acquire SPI lock!");
+		vTaskDelay(portMAX_DELAY);
+	}
+
+	/* Take Mutex during initialization, just in case */
+	if (xSemaphoreTake(max30001_mutex, 0) != pdTRUE) {
+		LOG_CRIT("max30001", "Failed to acquire MAX30001 mutex!");
+		vTaskDelay(portMAX_DELAY);
+	}
+
+	/* Install interrupt callback */
+	GPIO_Config(&max30001_interrupt_pin);
+	GPIO_RegisterCallback(
+		&max30001_interrupt_pin, max300001_interrupt_callback, NULL
+	);
+	GPIO_IntConfig(
+		&max30001_interrupt_pin, GPIO_INT_EDGE, GPIO_INT_FALLING
+	);
+	GPIO_IntEnable(&max30001_interrupt_pin);
+	NVIC_SetPriority(
+		(IRQn_Type)MXC_GPIO_GET_IRQ(max30001_interrupt_pin.port), 2
+	);
+	NVIC_EnableIRQ(
+		(IRQn_Type)MXC_GPIO_GET_IRQ(max30001_interrupt_pin.port)
+	);
+
+	GPIO_Config(&analog_switch);
+	GPIO_OutClr(&analog_switch); // Wrist
+
+	xSemaphoreGive(max30001_mutex);
+	hwlock_release(HWLOCK_SPI_ECG);
+
+	/* ----------------------------------------- */
+
+	while (1) {
+		if (max30001_sensor_active) {
+			int ret = max30001_fetch_fifo();
+			if (ret == -EBUSY) {
+				LOG_WARN(
+					"max30001", "Could not acquire mutex?"
+				);
+				continue;
+			} else if (ret < 0) {
+				LOG_ERR("max30001", "Unknown error: %d", -ret);
+			}
+		}
+		/*
+		 * Wait for interrupt.  After two seconds, fetch FIFO anyway
+		 *
+		 * In the future, reads using epic_stream_read() might also
+		 * trigger a FIFO fetch, from outside this task.
+		 */
+		ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(2000));
+	}
+}
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index 736b3efec72ee9462833e574c8b1c7a5ae32ea01..1aa7494ea0eff9003b48f0aa41a63257b1ab3833 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -12,6 +12,7 @@ module_sources = files(
   'lifecycle.c',
   'light_sensor.c',
   'log.c',
+  'max30001.c',
   'personal_state.c',
   'pmic.c',
   'rtc.c',
diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h
index d6d045f2e8cc4e45a59e7a9a10495fa0373f9333..144b18fb765a78580d66a8e8b59a9593d8127a77 100644
--- a/epicardium/modules/modules.h
+++ b/epicardium/modules/modules.h
@@ -73,7 +73,8 @@ void hwlock_init(void);
 enum hwlock_periph {
 	HWLOCK_I2C = 0,
 	HWLOCK_ADC,
-    HWLOCK_LED,
+	HWLOCK_LED,
+	HWLOCK_SPI_ECG,
 	_HWLOCK_MAX,
 };
 
@@ -89,5 +90,7 @@ void disp_forcelock();
 #define BHI160_MUTEX_WAIT_MS          50
 void vBhi160Task(void *pvParameters);
 
+#define MAX30001_MUTEX_WAIT_MS          50
+void vMAX30001Task(void *pvParameters);
 
 #endif /* MODULES_H */
diff --git a/epicardium/modules/stream.c b/epicardium/modules/stream.c
index 360e0357b79944c683a9017a936e613d9c3e306a..7453da118ea364db3f510538a617d34762a30dbd 100644
--- a/epicardium/modules/stream.c
+++ b/epicardium/modules/stream.c
@@ -24,50 +24,63 @@ int stream_init()
 
 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");
-		return -EBUSY;
+		ret = -EBUSY;
+		goto out;
 	}
 
 	if (sd < 0 || sd >= SD_MAX) {
-		return -EINVAL;
+		ret = -EINVAL;
+		goto out_release;
 	}
 
 	if (stream_table[sd] != NULL) {
 		/* Stream already registered */
-		return -EACCES;
+		ret = -EACCES;
+		goto out_release;
 	}
 
 	stream_table[sd] = stream;
 
+out_release:
 	xSemaphoreGive(stream_table_lock);
-	return 0;
+out:
+	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");
-		return -EBUSY;
+		ret = -EBUSY;
+		goto out;
 	}
 
 	if (sd < 0 || sd >= SD_MAX) {
-		return -EINVAL;
+		ret = -EINVAL;
+		goto out_release;
 	}
 
 	if (stream_table[sd] != stream) {
 		/* Stream registered by someone else */
-		return -EACCES;
+		ret = -EACCES;
+		goto out_release;
 	}
 
 	stream_table[sd] = NULL;
 
+out_release:
 	xSemaphoreGive(stream_table_lock);
-	return 0;
+out:
+	return ret;
 }
 
 int epic_stream_read(int sd, void *buf, size_t count)
 {
+	int ret = 0;
 	/*
 	 * TODO: In theory, multiple reads on different streams can happen
 	 * simulaneously.  I don't know what the most efficient implementation
@@ -75,29 +88,33 @@ int epic_stream_read(int sd, void *buf, size_t count)
 	 */
 	if (xSemaphoreTake(stream_table_lock, STREAM_MUTEX_WAIT) != pdTRUE) {
 		LOG_WARN("stream", "Lock contention error");
-		return -EBUSY;
+		ret = -EBUSY;
+		goto out;
 	}
 
 	if (sd < 0 || sd >= SD_MAX) {
-		return -EBADF;
+		ret = -EBADF;
+		goto out_release;
 	}
 
 	struct stream_info *stream = stream_table[sd];
 	if (stream == NULL) {
-		return -ENODEV;
+		ret = -ENODEV;
+		goto out_release;
 	}
 
 	/* Poll the stream, if a poll_stream function exists */
 	if (stream->poll_stream != NULL) {
 		int ret = stream->poll_stream();
 		if (ret < 0) {
-			return ret;
+			goto out_release;
 		}
 	}
 
 	/* Check buffer size is a multiple of the data packet size */
 	if (count % stream->item_size != 0) {
-		return -EINVAL;
+		ret = -EINVAL;
+		goto out_release;
 	}
 
 	size_t i;
@@ -107,6 +124,10 @@ int epic_stream_read(int sd, void *buf, size_t count)
 		}
 	}
 
+	ret = i / stream->item_size;
+
+out_release:
 	xSemaphoreGive(stream_table_lock);
-	return i / stream->item_size;
+out:
+	return ret;
 }
diff --git a/epicardium/modules/stream.h b/epicardium/modules/stream.h
index 41064bd5d716bc340721bd7b36267d7a648ec45f..1b4cba074035c6b8d6e276f9b01b5c0fecbe19f3 100644
--- a/epicardium/modules/stream.h
+++ b/epicardium/modules/stream.h
@@ -29,6 +29,7 @@ enum stream_descriptor {
 	SD_BHI160_ACCELEROMETER,
 	SD_BHI160_ORIENTATION,
 	SD_BHI160_GYROSCOPE,
+	SD_MAX30001_ECG,
 	/** Highest descriptor must always be ``SD_MAX``. */
 	SD_MAX,
 };