diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index b576662965726dcf83caefcb3287e87c791da319..0c21eee1dc3f092de7fb782eb23bd1ae4b70761f 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -111,6 +111,10 @@ typedef _Bool bool;
 #define API_BME680_DEINIT          0xD1
 #define API_BME680_GET_DATA        0xD2
 
+#define API_BHI160_ENABLE          0xe0
+#define API_BHI160_DISABLE         0xe1
+#define API_BHI160_DISABLE_ALL     0xe2
+
 /* clang-format on */
 
 typedef uint32_t api_int_id_t;
@@ -151,9 +155,16 @@ API(API_INTERRUPT_DISABLE, int epic_interrupt_disable(api_int_id_t int_id));
 #define EPIC_INT_UART_RX                2
 /** RTC Alarm interrupt.  See :c:func:`epic_isr_rtc_alarm` */
 #define EPIC_INT_RTC_ALARM              3
+/** BHI */
+#define EPIC_INT_BHI160_ACCELEROMETER   4
+API_ISR(EPIC_INT_BHI160_ACCELEROMETER, epic_isr_bhi160_accelerometer);
+#define EPIC_INT_BHI160_ORIENTATION     5
+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);
 
 /* Number of defined interrupts. */
-#define EPIC_INT_NUM                    4
+#define EPIC_INT_NUM                    7
 /* clang-format on */
 
 /*
@@ -859,6 +870,173 @@ API(API_PERSONAL_STATE_IS_PERSISTENT, int epic_personal_state_is_persistent());
  */
 API(API_STREAM_READ, int epic_stream_read(int sd, void *buf, size_t count));
 
+/**
+ * BHI160 Sensor Fusion
+ * ====================
+ * card10 has a BHI160 onboard which is used as an IMU.  BHI160 exposes a few
+ * different sensors which can be accessed using Epicardium API.
+ *
+ * **Example**:
+ *
+ * .. code-block:: cpp
+ *
+ *    #include "epicardium.h"
+ *
+ *    // Configure a sensor & enable it
+ *    struct bhi160_sensor_config cfg = {0};
+ *    cfg.sample_buffer_len = 40;
+ *    cfg.sample_rate = 4;   // Hz
+ *    cfg.dynamic_range = 2; // g
+ *
+ *    int sd = epic_bhi160_enable_sensor(BHI160_ACCELEROMETER, &cfg);
+ *
+ *    // Read sensor data
+ *    while (1) {
+ *            struct bhi160_data_vector buf[10];
+ *
+ *            int n = epic_stream_read(sd, buf, sizeof(buf));
+ *
+ *            for (int i = 0; i < n; i++) {
+ *                    printf("X: %6d Y: %6d Z: %6d\n",
+ *                           buf[i].x,
+ *                           buf[i].y,
+ *                           buf[i].z);
+ *            }
+ *    }
+ *
+ *    // Disable the sensor
+ *    epic_bhi160_disable_sensor(BHI160_ACCELEROMETER);
+ */
+
+/**
+ * BHI160 Sensor Types
+ * -------------------
+ */
+
+/**
+ * BHI160 virtual sensor type.
+ */
+enum bhi160_sensor_type {
+	/**
+	 * Accelerometer
+	 *
+	 * - Data type: :c:type:`bhi160_data_vector`
+	 * - Dynamic range: g's (1x Earth Gravity, ~9.81m*s^-2)
+	 */
+	BHI160_ACCELEROMETER               = 0,
+	/** Magnetometer (**Unimplemented**) */
+	BHI160_MAGNETOMETER                = 1,
+	/** Orientation */
+	BHI160_ORIENTATION                 = 2,
+	/** Gyroscope */
+	BHI160_GYROSCOPE                   = 3,
+	/** Gravity (**Unimplemented**) */
+	BHI160_GRAVITY                     = 4,
+	/** Linear acceleration (**Unimplemented**) */
+	BHI160_LINEAR_ACCELERATION         = 5,
+	/** Rotation vector (**Unimplemented**) */
+	BHI160_ROTATION_VECTOR             = 6,
+	/** Uncalibrated magnetometer (**Unimplemented**) */
+	BHI160_UNCALIBRATED_MAGNETOMETER   = 7,
+	/** Game rotation vector (whatever that is supposed to be) */
+	BHI160_GAME_ROTATION_VECTOR        = 8,
+	/** Uncalibrated gyroscrope (**Unimplemented**) */
+	BHI160_UNCALIBRATED_GYROSCOPE      = 9,
+	/** Geomagnetic rotation vector (**Unimplemented**) */
+	BHI160_GEOMAGNETIC_ROTATION_VECTOR = 10,
+};
+
+enum bhi160_data_type {
+	BHI160_DATA_TYPE_VECTOR
+};
+
+/**
+ * BHI160 Sensor Data Types
+ * ------------------------
+ */
+
+/**
+ * Vector Data.  The scaling of these values is dependent on the chosen dynamic
+ * range.  See the individual sensor's documentation for details.
+ */
+struct bhi160_data_vector {
+	enum bhi160_data_type data_type;
+
+	/** X */
+	int16_t x;
+	/** Y */
+	int16_t y;
+	/** Z */
+	int16_t z;
+	/** Status */
+	uint8_t status;
+};
+
+/**
+ * BHI160 API
+ * ----------
+ */
+
+/**
+ * Configuration for a BHI160 sensor.
+ *
+ * This struct is used when enabling a sensor using
+ * :c:func:`epic_bhi160_enable_sensor`.
+ */
+struct bhi160_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.  Maximum data rate is limited
+	 * to 200 Hz for all sensors though some might be limited at a lower
+	 * rate.
+	 */
+	uint16_t sample_rate;
+	/**
+	 * Dynamic range.  Interpretation of this value depends on
+	 * the sensor type.  Please refer to the specific sensor in
+	 * :c:type:`bhi160_sensor_type` for details.
+	 */
+	uint16_t dynamic_range;
+	/** Always zero. Reserved for future parameters. */
+	uint8_t _padding[8];
+};
+
+/**
+ * Enable a BHI160 virtual sensor.  Calling this funciton will instruct the
+ * BHI160 to collect data for this specific virtual sensor.  You can then
+ * retrieve the samples using :c:func:`epic_stream_read`.
+ *
+ * :param bhi160_sensor_type sensor_type: Which sensor to enable.
+ * :param bhi160_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 BHI160 driver is currently busy with other tasks and
+ *      could not be acquired for enabling a sensor.
+ */
+API(API_BHI160_ENABLE, int epic_bhi160_enable_sensor(
+	enum bhi160_sensor_type sensor_type,
+	struct bhi160_sensor_config *config
+));
+
+/**
+ * Disable a BHI160 sensor.
+ *
+ * :param bhi160_sensor_type sensor_type: Which sensor to disable.
+ */
+API(API_BHI160_DISABLE, int epic_bhi160_disable_sensor(
+	enum bhi160_sensor_type sensor_type
+));
+
+/**
+ * Disable all BHI160 sensors.
+ */
+API(API_BHI160_DISABLE_ALL, void epic_bhi160_disable_all_sensors());
+
 /**
  * Vibration Motor
  * ===============
diff --git a/epicardium/main.c b/epicardium/main.c
index 5e38e0bffe74c3dca93818bc40a50f5e1407fb09..8c260528f12a535501a22bbde882b8d4f5d0fa88 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -50,6 +50,18 @@ int main(void)
 		abort();
 	}
 
+	/* BHI160 */
+	if (xTaskCreate(
+		    vBhi160Task,
+		    (const char *)"BHI160 Driver",
+		    configMINIMAL_STACK_SIZE * 2,
+		    NULL,
+		    tskIDLE_PRIORITY + 1,
+		    NULL) != pdPASS) {
+		LOG_CRIT("startup", "Failed to create %s task!", "BHI160");
+		abort();
+	}
+
 	/* API */
 	if (xTaskCreate(
 		    vApiDispatcher,
diff --git a/epicardium/meson.build b/epicardium/meson.build
index 220f0bae63c7f84e60aee6002760c8271c7587de..ea28d3664563816c5fa8a4294e9fb3ba9308622c 100644
--- a/epicardium/meson.build
+++ b/epicardium/meson.build
@@ -80,7 +80,7 @@ elf = executable(
   l0der_sources,
   ble_sources,
   version_hdr,
-  dependencies: [libcard10, max32665_startup_core0, maxusb, libff13, ble],
+  dependencies: [libcard10, max32665_startup_core0, maxusb, libff13, ble, bhy1],
   link_with: [api_dispatcher_lib, freertos],
   link_whole: [max32665_startup_core0_lib, board_card10_lib, newlib_heap_lib],
   include_directories: [freertos_includes],
diff --git a/epicardium/modules/bhi.c b/epicardium/modules/bhi.c
new file mode 100644
index 0000000000000000000000000000000000000000..7d3689502e07c3de554744b1f10684d9d0af256b
--- /dev/null
+++ b/epicardium/modules/bhi.c
@@ -0,0 +1,494 @@
+#include <stdio.h>
+#include <string.h>
+
+#include "gpio.h"
+#include "bhy_uc_driver.h"
+#include "bhy.h"
+#include "pmic.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(BHI160_MUTEX_WAIT_MS)
+
+/* BHI160 Firmware Blob.  Contents are defined in libcard10. */
+extern uint8_t bhy1_fw[];
+
+/* Interrupt Pin */
+static const gpio_cfg_t bhi160_interrupt_pin = {
+	PORT_0, PIN_13, GPIO_FUNC_IN, GPIO_PAD_PULL_UP
+};
+
+/* Axis remapping matrices */
+static int8_t bhi160_mapping_matrix[3 * 3] = { 0, -1, 0, 1, 0, 0, 0, 0, 1 };
+static int8_t bmm150_mapping_matrix[3 * 3] = { -1, 0, 0, 0, 1, 0, 0, 0, -1 };
+
+/*
+ * From the official docs:
+ *
+ *    The sic matrix should be calculated for customer platform by logging
+ *    uncalibrated magnetometer data.  The sic matrix here is only an example
+ *    array (identity matrix). Customer should generate their own matrix.  This
+ *    affects magnetometer fusion performance.
+ *
+ * TODO: Get data for card10
+ */
+/* clang-format off */
+static float bhi160_sic_array[3 * 3] = { 1.0, 0.0, 0.0,
+                                         0.0, 1.0, 0.0,
+                                         0.0, 0.0, 1.0 };
+/* clang-format on */
+
+/* BHI160 Fifo */
+static uint8_t bhi160_fifo[BHI160_FIFO_SIZE];
+static size_t start_index = 0;
+
+/* BHI160 Task ID */
+static TaskHandle_t bhi160_task_id = NULL;
+
+/* BHI160 Mutex */
+static StaticSemaphore_t bhi160_mutex_data;
+static SemaphoreHandle_t bhi160_mutex = NULL;
+
+/* Streams */
+static struct stream_info bhi160_streams[10];
+
+/* Active */
+static bool bhi160_sensor_active[10] = { 0 };
+
+/* -- Utilities -------------------------------------------------------- {{{ */
+/*
+ * Retrieve the data size for a sensor.  This value is needed for the creation
+ * of the sensor's sample queue.
+ */
+static size_t bhi160_lookup_data_size(enum bhi160_sensor_type type)
+{
+	switch (type) {
+	case BHI160_ACCELEROMETER:
+	case BHI160_MAGNETOMETER:
+	case BHI160_ORIENTATION:
+	case BHI160_GYROSCOPE:
+		return sizeof(struct bhi160_data_vector);
+	default:
+		return 0;
+	}
+}
+
+/*
+ * Map a sensor type to the virtual sensor ID used by BHy1.
+ */
+static bhy_virtual_sensor_t bhi160_lookup_vs_id(enum bhi160_sensor_type type)
+{
+	switch (type) {
+	case BHI160_ACCELEROMETER:
+		return VS_ID_ACCELEROMETER;
+	case BHI160_ORIENTATION:
+		return VS_ID_ORIENTATION;
+	case BHI160_GYROSCOPE:
+		return VS_ID_GYROSCOPE;
+	default:
+		return -1;
+	}
+}
+
+/*
+ * Map a sensor type to its stream descriptor.
+ */
+static int bhi160_lookup_sd(enum bhi160_sensor_type type)
+{
+	switch (type) {
+	case BHI160_ACCELEROMETER:
+		return SD_BHI160_ACCELEROMETER;
+	case BHI160_ORIENTATION:
+		return SD_BHI160_ORIENTATION;
+	case BHI160_GYROSCOPE:
+		return SD_BHI160_GYROSCOPE;
+	default:
+		return -1;
+	}
+}
+/* }}} */
+
+/* -- API -------------------------------------------------------------- {{{ */
+int epic_bhi160_enable_sensor(
+	enum bhi160_sensor_type sensor_type,
+	struct bhi160_sensor_config *config
+) {
+	int result = 0;
+
+	bhy_virtual_sensor_t vs_id = bhi160_lookup_vs_id(sensor_type);
+	if (vs_id < 0) {
+		return -ENODEV;
+	}
+
+	result = hwlock_acquire(HWLOCK_I2C, pdMS_TO_TICKS(100));
+	if (result < 0) {
+		return result;
+	}
+
+	if (xSemaphoreTake(bhi160_mutex, LOCK_WAIT) != pdTRUE) {
+		result = -EBUSY;
+		goto out_free_i2c;
+	}
+
+	struct stream_info *stream = &bhi160_streams[sensor_type];
+	stream->item_size          = bhi160_lookup_data_size(sensor_type);
+	/* TODO: Sanity check length */
+	stream->queue =
+		xQueueCreate(config->sample_buffer_len, stream->item_size);
+	if (stream->queue == NULL) {
+		result = -ENOMEM;
+		goto out_free_both;
+	}
+
+	result = stream_register(bhi160_lookup_sd(sensor_type), stream);
+	if (result < 0) {
+		goto out_free_both;
+	}
+
+	result = bhy_enable_virtual_sensor(
+		vs_id,
+		VS_WAKEUP,
+		config->sample_rate,
+		0,
+		VS_FLUSH_NONE,
+		0,
+		config->dynamic_range /* dynamic range is sensor dependent */
+	);
+	if (result != BHY_SUCCESS) {
+		goto out_free_both;
+	}
+
+	bhi160_sensor_active[sensor_type] = true;
+	result                            = bhi160_lookup_sd(sensor_type);
+
+out_free_both:
+	xSemaphoreGive(bhi160_mutex);
+out_free_i2c:
+	hwlock_release(HWLOCK_I2C);
+	return result;
+}
+
+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) {
+		return -ENODEV;
+	}
+
+	result = hwlock_acquire(HWLOCK_I2C, pdMS_TO_TICKS(100));
+	if (result < 0) {
+		return result;
+	}
+
+	if (xSemaphoreTake(bhi160_mutex, LOCK_WAIT) != pdTRUE) {
+		result = -EBUSY;
+		goto out_free_i2c;
+	}
+
+	struct stream_info *stream = &bhi160_streams[sensor_type];
+	result = stream_deregister(bhi160_lookup_sd(sensor_type), stream);
+	if (result < 0) {
+		goto out_free_both;
+	}
+
+	vQueueDelete(stream->queue);
+	stream->queue = NULL;
+	result        = bhy_disable_virtual_sensor(vs_id, VS_WAKEUP);
+	if (result < 0) {
+		goto out_free_both;
+	}
+
+	bhi160_sensor_active[sensor_type] = false;
+
+	result = 0;
+out_free_both:
+	xSemaphoreGive(bhi160_mutex);
+out_free_i2c:
+	hwlock_release(HWLOCK_I2C);
+	return result;
+}
+
+void epic_bhi160_disable_all_sensors()
+{
+	for (int i = 0; i < sizeof(bhi160_sensor_active); i++) {
+		if (bhi160_sensor_active[i]) {
+			epic_bhi160_disable_sensor(i);
+		}
+	}
+}
+
+/* }}} */
+
+/* -- Driver ----------------------------------------------------------- {{{ */
+/*
+ * Handle a single packet from the FIFO.  For most sensors this means pushing
+ * the sample into its sample queue.
+ */
+static void
+bhi160_handle_packet(bhy_data_type_t data_type, bhy_data_generic_t *sensor_data)
+{
+	uint8_t sensor_id = sensor_data->data_vector.sensor_id;
+	struct bhi160_data_vector data_vector;
+	/*
+	 * Timestamp of the next samples, counting at 32 kHz.
+	 * Currently unused.
+	 */
+	static uint32_t timestamp           = 0;
+	enum bhi160_sensor_type sensor_type = 0;
+	int epic_int                        = 0;
+	bool wakeup                         = false;
+
+	switch (sensor_id) {
+	case VS_ID_TIMESTAMP_MSW_WAKEUP:
+		wakeup = true;
+		/* fall through */
+	case VS_ID_TIMESTAMP_MSW:
+		MXC_ASSERT(data_type == BHY_DATA_TYPE_SCALAR_U16);
+		timestamp = sensor_data->data_scalar_u16.data << 16;
+		break;
+	case VS_ID_TIMESTAMP_LSW_WAKEUP:
+		wakeup = true;
+		/* fall through */
+	case VS_ID_TIMESTAMP_LSW:
+		MXC_ASSERT(data_type == BHY_DATA_TYPE_SCALAR_U16);
+		timestamp = (timestamp & 0xFFFF0000) |
+			    sensor_data->data_scalar_u16.data;
+		break;
+	case VS_ID_ACCELEROMETER_WAKEUP:
+	case VS_ID_ORIENTATION_WAKEUP:
+	case VS_ID_GYROSCOPE_WAKEUP:
+		wakeup = true;
+		/* fall through */
+	case VS_ID_ACCELEROMETER:
+	case VS_ID_ORIENTATION:
+	case VS_ID_GYROSCOPE:
+		switch (sensor_id) {
+		case VS_ID_ACCELEROMETER_WAKEUP:
+		case VS_ID_ACCELEROMETER:
+			sensor_type = BHI160_ACCELEROMETER;
+			epic_int    = EPIC_INT_BHI160_ACCELEROMETER;
+			break;
+		case VS_ID_ORIENTATION_WAKEUP:
+		case VS_ID_ORIENTATION:
+			sensor_type = BHI160_ORIENTATION;
+			epic_int    = EPIC_INT_BHI160_ORIENTATION;
+			break;
+		case VS_ID_GYROSCOPE_WAKEUP:
+		case VS_ID_GYROSCOPE:
+			sensor_type = BHI160_GYROSCOPE;
+			epic_int    = EPIC_INT_BHI160_GYROSCOPE;
+			break;
+		}
+
+		MXC_ASSERT(data_type == BHY_DATA_TYPE_VECTOR);
+		if (bhi160_streams[sensor_type].queue == NULL) {
+			break;
+		}
+		data_vector.data_type = BHI160_DATA_TYPE_VECTOR;
+		data_vector.x         = sensor_data->data_vector.x;
+		data_vector.y         = sensor_data->data_vector.y;
+		data_vector.z         = sensor_data->data_vector.z;
+		data_vector.status    = sensor_data->data_vector.status;
+		xQueueSend(
+			bhi160_streams[sensor_type].queue,
+			&data_vector,
+			BHI160_MUTEX_WAIT_MS
+		);
+		if (wakeup) {
+			api_interrupt_trigger(epic_int);
+		}
+		break;
+	default:
+		break;
+	}
+}
+
+/*
+ * Fetch all data available from BHI160's FIFO buffer and handle all packets
+ * contained in it.
+ */
+static int bhi160_fetch_fifo(void)
+{
+	/*
+	 * Warning:  The code from the BHy1 docs has some issues.  This
+	 * implementation looks similar, but has a few important differences.
+	 * You'll probably be best of leaving it as it is ...
+	 */
+
+	int result = 0;
+	/* Number of bytes left in BHI160's FIFO buffer */
+	uint16_t bytes_left_in_fifo = 1;
+
+	result = hwlock_acquire(HWLOCK_I2C, pdMS_TO_TICKS(100));
+	if (result < 0) {
+		return result;
+	}
+
+	if (xSemaphoreTake(bhi160_mutex, LOCK_WAIT) != pdTRUE) {
+		result = -EBUSY;
+		goto out_free_i2c;
+	}
+
+	while (bytes_left_in_fifo) {
+		/* Fill local FIFO buffer with as many bytes as possible */
+		uint16_t bytes_read;
+		bhy_read_fifo(
+			&bhi160_fifo[start_index],
+			BHI160_FIFO_SIZE - start_index,
+			&bytes_read,
+			&bytes_left_in_fifo
+		);
+
+		/* Add the bytes left from the last transfer on top */
+		bytes_read += start_index;
+
+		/* Handle all full packets received in this transfer */
+		uint8_t *fifo_ptr   = bhi160_fifo;
+		uint16_t bytes_left = bytes_read;
+		while (bytes_left > 0) {
+			bhy_data_generic_t sensor_data;
+			bhy_data_type_t data_type;
+			result = bhy_parse_next_fifo_packet(
+				&fifo_ptr,
+				&bytes_left,
+				&sensor_data,
+				&data_type
+			);
+
+			if (result == BHY_SUCCESS) {
+				bhi160_handle_packet(data_type, &sensor_data);
+			} else {
+				break;
+			}
+		}
+
+		/* Shift the remaining bytes to the beginning */
+		for (int i = 0; i < bytes_left; i++) {
+			bhi160_fifo[i] =
+				bhi160_fifo[bytes_read - bytes_left + i];
+		}
+		start_index = bytes_left;
+	}
+
+	xSemaphoreGive(bhi160_mutex);
+out_free_i2c:
+	hwlock_release(HWLOCK_I2C);
+	return result;
+}
+
+/*
+ * Callback for the BHI160 interrupt pin.  This callback is called from the
+ * SDK's GPIO interrupt driver, in interrupt context.
+ */
+static void bhi160_interrupt_callback(void *_)
+{
+	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
+
+	if (bhi160_task_id != NULL) {
+		vTaskNotifyGiveFromISR(
+			bhi160_task_id, &xHigherPriorityTaskWoken
+		);
+		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
+	}
+}
+/* }}} */
+
+void vBhi160Task(void *pvParameters)
+{
+	int ret;
+
+	bhi160_task_id = xTaskGetCurrentTaskHandle();
+	bhi160_mutex   = xSemaphoreCreateMutexStatic(&bhi160_mutex_data);
+
+	int lockret = hwlock_acquire(HWLOCK_I2C, pdMS_TO_TICKS(100));
+	if (lockret < 0) {
+		LOG_CRIT("bhi160", "Failed to acquire I2C lock!");
+		vTaskDelay(portMAX_DELAY);
+	}
+
+	/* Take Mutex during initialization, just in case */
+	if (xSemaphoreTake(bhi160_mutex, 0) != pdTRUE) {
+		LOG_CRIT("bhi160", "Failed to acquire BHI160 mutex!");
+		vTaskDelay(portMAX_DELAY);
+	}
+
+	memset(bhi160_streams, 0x00, sizeof(bhi160_streams));
+
+	/* Install interrupt callback */
+	GPIO_Config(&bhi160_interrupt_pin);
+	GPIO_RegisterCallback(
+		&bhi160_interrupt_pin, bhi160_interrupt_callback, NULL
+	);
+	GPIO_IntConfig(&bhi160_interrupt_pin, GPIO_INT_EDGE, GPIO_INT_RISING);
+	GPIO_IntEnable(&bhi160_interrupt_pin);
+	NVIC_SetPriority(
+		(IRQn_Type)MXC_GPIO_GET_IRQ(bhi160_interrupt_pin.port), 2
+	);
+	NVIC_EnableIRQ((IRQn_Type)MXC_GPIO_GET_IRQ(bhi160_interrupt_pin.port));
+
+	/* Upload firmware */
+	ret = bhy_driver_init(bhy1_fw);
+	if (ret) {
+		LOG_CRIT("bhi160", "BHy1 init failed!");
+		vTaskDelay(portMAX_DELAY);
+	}
+
+	/* Wait for first interrupt */
+	hwlock_release(HWLOCK_I2C);
+	ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(100));
+	lockret = hwlock_acquire(HWLOCK_I2C, pdMS_TO_TICKS(100));
+	if (lockret < 0) {
+		LOG_CRIT("bhi160", "Failed to acquire I2C lock!");
+		vTaskDelay(portMAX_DELAY);
+	}
+
+	/* Remap axes to match card10 layout */
+	bhy_mapping_matrix_set(
+		PHYSICAL_SENSOR_INDEX_ACC, bhi160_mapping_matrix
+	);
+	bhy_mapping_matrix_set(
+		PHYSICAL_SENSOR_INDEX_MAG, bmm150_mapping_matrix
+	);
+	bhy_mapping_matrix_set(
+		PHYSICAL_SENSOR_INDEX_GYRO, bhi160_mapping_matrix
+	);
+
+	/* Set "SIC" matrix.  TODO: Find out what this is about */
+	bhy_set_sic_matrix(bhi160_sic_array);
+
+	xSemaphoreGive(bhi160_mutex);
+	hwlock_release(HWLOCK_I2C);
+
+	/* ----------------------------------------- */
+
+	while (1) {
+		int ret = bhi160_fetch_fifo();
+		if (ret == -EBUSY) {
+			LOG_WARN("bhi160", "Could not acquire mutex for FIFO?");
+			continue;
+		} else if (ret < 0) {
+			LOG_ERR("bhi160", "Unknown error: %d", -ret);
+		}
+
+		/*
+		 * Wait for interrupt.  After two seconds, fetch FIFO anyway in
+		 * case there are any diagnostics or errors.
+		 *
+		 * 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/hardware.c b/epicardium/modules/hardware.c
index 1173977a70f8a3a1acb4475ef6e42005eb282fee..3b2c0696e127e9681344404ca0dba19d95c5913c 100644
--- a/epicardium/modules/hardware.c
+++ b/epicardium/modules/hardware.c
@@ -259,6 +259,11 @@ int hardware_reset(void)
 	 */
 	display_init_slim();
 
+	/*
+	 * BHI160
+	 */
+	epic_bhi160_disable_all_sensors();
+
 	/*
 	 * BME680 Sensor
 	 */
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index 73975adceee81bb83894ecdd15784112ce1f3221..736b3efec72ee9462833e574c8b1c7a5ae32ea01 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -1,4 +1,5 @@
 module_sources = files(
+  'bhi.c',
   'bme680.c',
   'buttons.c',
   'dispatcher.c',
diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h
index 8760e691567f4e89e989cfe48d43d4c0d05cb77b..d6d045f2e8cc4e45a59e7a9a10495fa0373f9333 100644
--- a/epicardium/modules/modules.h
+++ b/epicardium/modules/modules.h
@@ -84,4 +84,10 @@ int hwlock_release(enum hwlock_periph p);
 /* Forces an unlock of the display. Only to be used in Epicardium */
 void disp_forcelock();
 
+/* ---------- BHI160 ------------------------------------------------------- */
+#define BHI160_FIFO_SIZE             128
+#define BHI160_MUTEX_WAIT_MS          50
+void vBhi160Task(void *pvParameters);
+
+
 #endif /* MODULES_H */
diff --git a/epicardium/modules/stream.h b/epicardium/modules/stream.h
index 9d137a20e6fdf5b84e9e568b5e06a294069d1606..41064bd5d716bc340721bd7b36267d7a648ec45f 100644
--- a/epicardium/modules/stream.h
+++ b/epicardium/modules/stream.h
@@ -25,6 +25,10 @@ typedef unsigned int size_t;
  *    Please keep IDs in sequential order.
  */
 enum stream_descriptor {
+	/** BHI160 */
+	SD_BHI160_ACCELEROMETER,
+	SD_BHI160_ORIENTATION,
+	SD_BHI160_GYROSCOPE,
 	/** Highest descriptor must always be ``SD_MAX``. */
 	SD_MAX,
 };
diff --git a/preload/apps/bhi160/__init__.py b/preload/apps/bhi160/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..3632008c0375fe10d40c24096c56600be7f6c4dc
--- /dev/null
+++ b/preload/apps/bhi160/__init__.py
@@ -0,0 +1,46 @@
+import bhi160
+import display
+import utime
+import buttons
+
+disp = display.open()
+sensor = 0
+
+sensors = [
+    {"sensor": bhi160.BHI160Orientation(), "name": "Orientation"},
+    {"sensor": bhi160.BHI160Accelerometer(), "name": "Accelerometer"},
+    {"sensor": bhi160.BHI160Gyroscope(), "name": "Gyroscope"},
+]
+
+while True:
+    # Read and print sample
+    samples = sensors[sensor]["sensor"].read()
+    if len(samples) > 0:
+        disp.clear()
+        sample = samples[0]
+
+        color = [255, 0, 0]
+        if sample.status == 1:
+            color = [255, 128, 0]
+        elif sample.status == 2:
+            color = [255, 255, 0]
+        elif sample.status == 3:
+            color = [0, 200, 0]
+
+        disp.print(sensors[sensor]["name"], posy=0)
+        disp.print("X: %f" % sample.x, posy=20, fg=color)
+        disp.print("Y: %f" % sample.y, posy=40, fg=color)
+        disp.print("Z: %f" % sample.z, posy=60, fg=color)
+
+        disp.update()
+
+    # Read button
+    v = buttons.read(buttons.BOTTOM_RIGHT)
+    if v == 0:
+        button_pressed = False
+
+    if not button_pressed and v & buttons.BOTTOM_RIGHT != 0:
+        button_pressed = True
+        sensor = (sensor + 1) % len(sensors)
+
+    utime.sleep(0.1)
diff --git a/preload/apps/bhi160/metadata.json b/preload/apps/bhi160/metadata.json
new file mode 100644
index 0000000000000000000000000000000000000000..e8a0a37a4779bf3916449420f7724172b4eee021
--- /dev/null
+++ b/preload/apps/bhi160/metadata.json
@@ -0,0 +1 @@
+{"author": "card10badge team", "name": "BHI160", "description": "Read BHI160 sensor data", "category": "Hardware", "revision": 1}
diff --git a/pycardium/meson.build b/pycardium/meson.build
index f0a3798a6ab5ef5538673a98f6de9748e705f3b7..2c43f62f35d0dfed88fab3dfbd6bfcecdcd0a70d 100644
--- a/pycardium/meson.build
+++ b/pycardium/meson.build
@@ -1,6 +1,7 @@
 name = 'pycardium'
 
 modsrc = files(
+  'modules/bhi160-sys.c',
   'modules/buttons.c',
   'modules/fat_file.c',
   'modules/fat_reader_import.c',
diff --git a/pycardium/modules/bhi160-sys.c b/pycardium/modules/bhi160-sys.c
new file mode 100644
index 0000000000000000000000000000000000000000..db0a5c9a65eb42f9b8ee392ffa5650909877bfb0
--- /dev/null
+++ b/pycardium/modules/bhi160-sys.c
@@ -0,0 +1,88 @@
+#include "py/obj.h"
+#include "py/runtime.h"
+#include "py/builtin.h"
+#include "epicardium.h"
+#include "api/common.h"
+#include "mphalport.h"
+
+extern const mp_obj_type_t mp_type_bhi160_sample;
+
+STATIC mp_obj_t mp_bhi160_enable_sensor(size_t n_args, const mp_obj_t *args)
+{
+	int sensor_type = mp_obj_get_int(args[0]);
+
+	struct bhi160_sensor_config cfg = { 0 };
+	cfg.sample_buffer_len           = mp_obj_get_int(args[1]);
+	cfg.sample_rate                 = mp_obj_get_int(args[2]);
+	cfg.dynamic_range               = mp_obj_get_int(args[3]);
+
+	int stream_id = epic_bhi160_enable_sensor(sensor_type, &cfg);
+
+	return MP_OBJ_NEW_SMALL_INT(stream_id);
+}
+
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(
+	mp_bhi160_enable_sensor_obj, 4, 4, mp_bhi160_enable_sensor
+);
+
+STATIC mp_obj_t mp_bhi160_read_sensor(mp_obj_t stream_id_in)
+{
+	struct bhi160_data_vector buf[100];
+	int stream_id = mp_obj_get_int(stream_id_in);
+
+	int n = epic_stream_read(stream_id, buf, sizeof(buf));
+
+	mp_obj_list_t *list = mp_obj_new_list(0, NULL);
+	for (int i = 0; i < n; i++) {
+		if (buf[i].data_type != BHI160_DATA_TYPE_VECTOR) {
+			// other data types are currently not supported
+			mp_raise_OSError(EINVAL);
+		}
+		mp_obj_t tuple[4];
+		tuple[0] = mp_obj_new_int(buf[i].x);
+		tuple[1] = mp_obj_new_int(buf[i].y);
+		tuple[2] = mp_obj_new_int(buf[i].z);
+		tuple[3] = mp_obj_new_int(buf[i].status);
+		mp_obj_list_append(list, mp_obj_new_tuple(4, tuple));
+	}
+
+	return MP_OBJ_FROM_PTR(list);
+}
+
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(
+	mp_bhi160_read_sensor_obj, mp_bhi160_read_sensor
+);
+
+STATIC mp_obj_t mp_bhi160_disable_sensor(mp_obj_t sensor_type_in)
+{
+	int sensor_type = mp_obj_get_int(sensor_type_in);
+
+	int ret = epic_bhi160_disable_sensor(sensor_type);
+
+	return MP_OBJ_NEW_SMALL_INT(ret);
+}
+
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(
+	mp_bhi160_disable_sensor_obj, mp_bhi160_disable_sensor
+);
+
+STATIC const mp_rom_map_elem_t bhi160_module_globals_table[] = {
+	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_sys_bhi160) },
+	{ MP_ROM_QSTR(MP_QSTR_enable_sensor),
+	  MP_ROM_PTR(&mp_bhi160_enable_sensor_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_read_sensor),
+	  MP_ROM_PTR(&mp_bhi160_read_sensor_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_disable_sensor),
+	  MP_ROM_PTR(&mp_bhi160_disable_sensor_obj) },
+};
+STATIC MP_DEFINE_CONST_DICT(bhi160_module_globals, bhi160_module_globals_table);
+
+// Define module object.
+const mp_obj_module_t bhi160_module = {
+	.base    = { &mp_type_module },
+	.globals = (mp_obj_dict_t *)&bhi160_module_globals,
+};
+
+/* clang-format off */
+// Register the module to make it available in Python
+MP_REGISTER_MODULE(MP_QSTR_sys_bhi160, bhi160_module, MODULE_BHI160_ENABLED);
diff --git a/pycardium/modules/interrupt.c b/pycardium/modules/interrupt.c
index 10770a19648a9bd9af182dfd58e2f0fddbf32def..927b936b906a8a7a8a952d12bf9940ab76ce8eff 100644
--- a/pycardium/modules/interrupt.c
+++ b/pycardium/modules/interrupt.c
@@ -85,6 +85,12 @@ static const mp_rom_map_elem_t interrupt_module_globals_table[] = {
 	/* Interrupt Numbers */
 	{ MP_ROM_QSTR(MP_QSTR_RTC_ALARM),
 	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_RTC_ALARM) },
+	{ MP_ROM_QSTR(MP_QSTR_BHI160_ACCELEROMETER),
+	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_BHI160_ACCELEROMETER) },
+	{ MP_ROM_QSTR(MP_QSTR_BHI160_ORIENTATION),
+	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_BHI160_ORIENTATION) },
+	{ MP_ROM_QSTR(MP_QSTR_BHI160_GYROSCOPE),
+	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_BHI160_GYROSCOPE) },
 };
 static MP_DEFINE_CONST_DICT(
 	interrupt_module_globals, interrupt_module_globals_table
diff --git a/pycardium/modules/py/bhi160.py b/pycardium/modules/py/bhi160.py
new file mode 100644
index 0000000000000000000000000000000000000000..86ad6f01f10b4f929d3390d41dda8283badb3676
--- /dev/null
+++ b/pycardium/modules/py/bhi160.py
@@ -0,0 +1,121 @@
+import sys_bhi160
+import interrupt
+import ucollections
+
+DataVector = ucollections.namedtuple("DataVector", ["x", "y", "z", "status"])
+
+
+class BHI160:
+    def enable_sensor(self):
+        interrupt.disable_callback(self.interrupt_id)
+        interrupt.set_callback(self.interrupt_id, self._interrupt)
+        self.stream_id = sys_bhi160.enable_sensor(
+            self.sensor_id, self.sample_buffer_len, self.sample_rate, self.dynamic_range
+        )
+
+        if self.stream_id < 0:
+            raise ValueError("Enable sensor returned %i", self.stream_id)
+
+        self.active = True
+
+        if self._callback:
+            interrupt.enable_callback(self.interrupt_id)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, _et, _ev, _t):
+        self.close()
+
+    def close(self):
+        if self.active:
+            self.active = False
+            ret = sys_bhi160.disable_sensor(self.sensor_id)
+
+            if ret < 0:
+                raise ValueError("Disable sensor returned %i", ret)
+
+            interrupt.disable_callback(self.interrupt_id)
+            interrupt.set_callback(self.interrupt_id, None)
+
+    def read(self):
+        result = []
+        if self.active:
+            for sample in sys_bhi160.read_sensor(self.stream_id):
+                result.append(self.convert(sample))
+        return result
+
+    def _interrupt(self, _):
+        if self.active:
+            data = self.read()
+            print(data)
+            if self._callback:
+                self._callback(data)
+
+    def convert_data_vector(self, sample):
+        return DataVector(
+            self.convert_single(sample[0]),
+            self.convert_single(sample[1]),
+            self.convert_single(sample[2]),
+            sample[3],
+        )
+
+
+class BHI160Accelerometer(BHI160):
+    def __init__(
+        self, sample_rate=4, dynamic_range=2, callback=None, sample_buffer_len=200
+    ):
+        self.sample_rate = sample_rate
+        self.dynamic_range = dynamic_range
+        self.callback = callback
+        self.sample_buffer_len = sample_buffer_len
+        self.sensor_id = 0
+        self.interrupt_id = interrupt.BHI160_ACCELEROMETER
+        self._callback = callback
+        self.enable_sensor()
+
+    def convert_single(self, value):
+        return 2 * value / 32768.0
+
+    def convert(self, sample):
+        return self.convert_data_vector(sample)
+
+
+class BHI160Gyroscope(BHI160):
+    def __init__(
+        self, sample_rate=4, dynamic_range=2, callback=None, sample_buffer_len=200
+    ):
+        self.sample_rate = sample_rate
+        self.dynamic_range = dynamic_range
+        self.callback = callback
+        self.sample_buffer_len = sample_buffer_len
+        self.sensor_id = 3
+        self.interrupt_id = interrupt.BHI160_GYROSCOPE
+        self._callback = callback
+        self.enable_sensor()
+
+    def convert_single(self, value):
+        return 360 * value / 32768.0
+
+    def convert(self, sample):
+        return self.convert_data_vector(sample)
+
+
+class BHI160Orientation(BHI160):
+    def __init__(
+        self, sample_rate=4, dynamic_range=2, callback=None, sample_buffer_len=200
+    ):
+        self.sample_rate = sample_rate
+        self.dynamic_range = dynamic_range
+        self.callback = callback
+        self.sample_buffer_len = sample_buffer_len
+        self.sensor_id = 2
+        self.interrupt_id = interrupt.BHI160_ORIENTATION
+        self._callback = callback
+        self.enable_sensor()
+
+    def convert_single(self, value):
+        return 360 * value / 32768.0
+
+    def convert(self, sample):
+        return self.convert_data_vector(sample)
diff --git a/pycardium/modules/py/meson.build b/pycardium/modules/py/meson.build
index 59dd24d1fa95143f80c3f728129d6a3c5c170a0d..5548fff7e9fb15c17fec2f6c1c8d9add23a6541d 100644
--- a/pycardium/modules/py/meson.build
+++ b/pycardium/modules/py/meson.build
@@ -1,4 +1,5 @@
 python_modules = files(
+  'bhi160.py',
   'color.py',
   'htmlcolor.py',
   'display.py',
diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h
index 60c5c56c9cbc385bb87fb8fe33f2f4881fd41947..82c1d12577ce06917fa8a94408f2961b2fbb5905 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -57,9 +57,20 @@ Q(vibrate)
 Q(set_callback)
 Q(enable_callback)
 Q(disable_callback)
-Q(BHI160)
+Q(BHI160_ACCELEROMETER)
+Q(BHI160_ORIENTATION)
+Q(BHI160_GYROSCOPE)
 Q(RTC_ALARM)
 
+/* bhi160 */
+Q(sys_bhi160)
+Q(enable_sensor)
+Q(disable_sensor)
+Q(read_sensor)
+Q(x)
+Q(y)
+Q(z)
+
 /* display */
 Q(sys_display)
 Q(display)
diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h
index af27e146f99e46735e5f33bd013383dd47233fa7..b84867980a9858cdd6f9ec56398645ab4a9af8cd 100644
--- a/pycardium/mpconfigport.h
+++ b/pycardium/mpconfigport.h
@@ -45,6 +45,7 @@ int mp_hal_trng_read_int(void);
 #define MICROPY_PY_UERRNO                   (1)
 
 /* Modules */
+#define MODULE_BHI160_ENABLED               (1)
 #define MODULE_BME680_ENABLED               (1)
 #define MODULE_BUTTONS_ENABLED              (1)
 #define MODULE_DISPLAY_ENABLED              (1)