diff --git a/Documentation/epicardium/sensor-streams.rst b/Documentation/epicardium/sensor-streams.rst
new file mode 100644
index 0000000000000000000000000000000000000000..a111402d74a13430bc21aeb16ab768ad2602d750
--- /dev/null
+++ b/Documentation/epicardium/sensor-streams.rst
@@ -0,0 +1,29 @@
+Sensor Streams
+==============
+Sensor drivers can make their data available to core 1 in a stream-like format.
+This allows batch-reading many samples and shoud reduce pressure on the
+Epicardium API this way.  Sensor streams are read on core 1 using
+:c:func:`epic_stream_read`.
+
+This page intends to document how to add this stream interface to a sensor driver.
+It also serves as a reference of existing streams.  For that, take a look at the
+definitions in the :c:type:`stream_descriptor` enum.
+
+Adding a new Stream
+-------------------
+The list of possible sensor streams must be known at compile time.  Each stream
+gets a unique ID in the :c:type:`stream_descriptor` enum.  Please do not assign
+IDs manually but instead let the enum assign sequencial IDs.  :c:macro:`SD_MAX`
+must always be the highest stream ID.  Additionally, please document what this
+stream is for using a doc-comment so it shows up on this page.
+
+When a sensor driver enables data collection, it should also register its
+respective stream.  This is done using a :c:type:`stream_info` object.  Pass
+this object to :c:func:`stream_register` to make your stream available.  Your
+driver must guarantee the :c:member:`stream_info.queue` handle to be valid until
+deregistration using :c:func:`stream_deregister`.
+
+Definitions
+-----------
+
+.. c:autodoc:: epicardium/modules/stream.h
diff --git a/Documentation/index.rst b/Documentation/index.rst
index 1cbba07da9e948adfe3a5bb67fce17b0698f26ce..d9a7339a5fcb8f241fad2a66395d91ed318123f9 100644
--- a/Documentation/index.rst
+++ b/Documentation/index.rst
@@ -35,6 +35,7 @@ Last but not least, if you want to start hacking the lower-level firmware, the
    debugger
    pycardium-guide
    memorymap
+   epicardium/sensor-streams
 
 .. toctree::
    :maxdepth: 1
@@ -43,5 +44,3 @@ Last but not least, if you want to start hacking the lower-level firmware, the
    epicardium/overview
    epicardium/api
    epicardium-guide
-
-
diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 7886cc5f0bb28fbc2ba5d818e43ef5f7879c0e50..4e52d85c51c38d897f38dbee03ed96cbbb7b0de1 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -1,6 +1,8 @@
 #ifndef _EPICARDIUM_H
 #define _EPICARDIUM_H
 #include <stdint.h>
+#include <stddef.h>
+#include <errno.h>
 
 #ifndef API
 #define API(id, def) def
@@ -12,6 +14,7 @@
 #define API_LEDS_SET           0x3
 #define API_VIBRA_SET          0x4
 #define API_VIBRA_VIBRATE      0x5
+#define API_STREAM_READ        0x6
 /* clang-format on */
 
 /**
@@ -55,6 +58,68 @@ API(API_UART_READ, char epic_uart_read_chr(void));
  */
 API(API_LEDS_SET, void epic_leds_set(int led, uint8_t r, uint8_t g, uint8_t b));
 
+/**
+ * Sensor Data Streams
+ * ===================
+ * A few of card10's sensors can do continuous measurements.  To allow
+ * performant access to their data, the following function is made for generic
+ * access to streams.
+ */
+
+/**
+ * Read sensor data into a buffer.  ``epic_stream_read()`` will read as many
+ * sensor samples into the provided buffer as possible and return the number of
+ * samples written.  If no samples are available, ``epic_stream_read()`` will
+ * return ``0`` immediately.
+ *
+ * ``epic_stream_read()`` expects the provided buffer to have a size which is a
+ * multiple of the sample size for the given stream.  For the sample-format and
+ * size, please consult the sensors documentation.
+ *
+ * Before reading the internal sensor sample queue, ``epic_stream_read()`` will
+ * call a sensor specific *poll* function to allow the sensor driver to fetch
+ * new samples from its hardware.  This should, however, never take a long
+ * amount of time.
+ *
+ * :param int sd: Sensor Descriptor.  You get sensor descriptors as return
+ *    values when activating the respective sensors.
+ * :param void* buf: Buffer where sensor data should be read into.
+ * :param size_t count: How many bytes to read at max.  Note that fewer bytes
+ *    might be read.  In most cases, this should be ``sizeof(buf)``.
+ * :return: Number of data packets read (**not** number of bytes) or a negative
+ *    error value.  Possible errors:
+ *
+ *    - ``-ENODEV``: Sensor is not currently available.
+ *    - ``-EBADF``: The given sensor descriptor is unknown.
+ *    - ``-EINVAL``:  ``count`` is not a multiple of the sensor's sample size.
+ *    - ``-EBUSY``: The descriptor table lock could not be acquired.
+ *
+ * **Example**:
+ *
+ * .. code-block:: cpp
+ *
+ *    #include "epicardium.h"
+ *
+ *    struct foo_measurement sensor_data[16];
+ *    int foo_sd, n;
+ *
+ *    foo_sd = epic_foo_sensor_enable(9001);
+ *
+ *    while (1) {
+ *            n = epic_stream_read(
+ *                    foo_sd,
+ *                    &sensor_data,
+ *                    sizeof(sensor_data)
+ *            );
+ *
+ *            // Print out the measured sensor samples
+ *            for (int i = 0; i < n; i++) {
+ *                    printf("Measured: %?\n", sensor_data[i]);
+ *            }
+ *    }
+ */
+API(API_STREAM_READ, int epic_stream_read(int sd, void *buf, size_t count));
+
 /**
  * Misc
  * ====
diff --git a/epicardium/main.c b/epicardium/main.c
index 0117add3c1290474fba9f8cb6bb88c18339057a0..51d365f2c0a754b1432dfe5ae14ddf95de620aee 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -11,6 +11,7 @@
 #include "api/dispatcher.h"
 #include "modules/modules.h"
 #include "modules/log.h"
+#include "modules/stream.h"
 
 #include <Heart.h>
 #include "GUI_Paint.h"
@@ -53,6 +54,7 @@ int main(void)
 	}
 
 	fatfs_init();
+	stream_init();
 
 	LOG_INFO("startup", "Initializing tasks ...");
 
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index dfc27d00574c49ec1bdbeffc99321f3bb4f1758e..a038fb21d7f5ef7c650a10851503150a27c0b1eb 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -4,5 +4,6 @@ module_sources = files(
   'log.c',
   'pmic.c',
   'serial.c',
+  'stream.c',
   'vibra.c',
 )
diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h
index 8fe73d59755c6146bba1d364938a6305af9e1baa..0db3d11bf84ca07f8a65f499fa1497a55940968c 100644
--- a/epicardium/modules/modules.h
+++ b/epicardium/modules/modules.h
@@ -15,4 +15,3 @@ void vSerialTask(void *pvParameters);
 void vPmicTask(void *pvParameters);
 
 #endif /* MODULES_H */
-
diff --git a/epicardium/modules/stream.c b/epicardium/modules/stream.c
new file mode 100644
index 0000000000000000000000000000000000000000..360e0357b79944c683a9017a936e613d9c3e306a
--- /dev/null
+++ b/epicardium/modules/stream.c
@@ -0,0 +1,112 @@
+#include <string.h>
+
+#include "FreeRTOS.h"
+#include "semphr.h"
+
+#include "epicardium.h"
+#include "modules/log.h"
+#include "modules/stream.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;
+
+int stream_init()
+{
+	memset(stream_table, 0x00, sizeof(stream_table));
+	stream_table_lock =
+		xSemaphoreCreateMutexStatic(&stream_table_lock_data);
+	return 0;
+}
+
+int stream_register(int sd, struct stream_info *stream)
+{
+	if (xSemaphoreTake(stream_table_lock, STREAM_MUTEX_WAIT) != pdTRUE) {
+		LOG_WARN("stream", "Lock contention error");
+		return -EBUSY;
+	}
+
+	if (sd < 0 || sd >= SD_MAX) {
+		return -EINVAL;
+	}
+
+	if (stream_table[sd] != NULL) {
+		/* Stream already registered */
+		return -EACCES;
+	}
+
+	stream_table[sd] = stream;
+
+	xSemaphoreGive(stream_table_lock);
+	return 0;
+}
+
+int stream_deregister(int sd, struct stream_info *stream)
+{
+	if (xSemaphoreTake(stream_table_lock, STREAM_MUTEX_WAIT) != pdTRUE) {
+		LOG_WARN("stream", "Lock contention error");
+		return -EBUSY;
+	}
+
+	if (sd < 0 || sd >= SD_MAX) {
+		return -EINVAL;
+	}
+
+	if (stream_table[sd] != stream) {
+		/* Stream registered by someone else */
+		return -EACCES;
+	}
+
+	stream_table[sd] = NULL;
+
+	xSemaphoreGive(stream_table_lock);
+	return 0;
+}
+
+int epic_stream_read(int sd, void *buf, size_t count)
+{
+	/*
+	 * TODO: In theory, multiple reads on different streams can happen
+	 * 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");
+		return -EBUSY;
+	}
+
+	if (sd < 0 || sd >= SD_MAX) {
+		return -EBADF;
+	}
+
+	struct stream_info *stream = stream_table[sd];
+	if (stream == NULL) {
+		return -ENODEV;
+	}
+
+	/* Poll the stream, if a poll_stream function exists */
+	if (stream->poll_stream != NULL) {
+		int ret = stream->poll_stream();
+		if (ret < 0) {
+			return ret;
+		}
+	}
+
+	/* Check buffer size is a multiple of the data packet size */
+	if (count % stream->item_size != 0) {
+		return -EINVAL;
+	}
+
+	size_t i;
+	for (i = 0; i < count; i += stream->item_size) {
+		if (!xQueueReceive(stream->queue, buf + i, 0)) {
+			break;
+		}
+	}
+
+	xSemaphoreGive(stream_table_lock);
+	return i / stream->item_size;
+}
diff --git a/epicardium/modules/stream.h b/epicardium/modules/stream.h
new file mode 100644
index 0000000000000000000000000000000000000000..32c9e6e219bc7b7998babcd25236d2417d61b8c5
--- /dev/null
+++ b/epicardium/modules/stream.h
@@ -0,0 +1,87 @@
+#ifndef STREAM_H
+#define STREAM_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "FreeRTOS.h"
+#include "queue.h"
+
+/* Time to wait for the descriptor table lock to become available */
+#define STREAM_MUTEX_WAIT pdMS_TO_TICKS(100)
+
+/**
+ * **Stream Descriptors**:
+ *
+ *    This enum defines all known stream descriptors.  Internally, the stream
+ *    module allocates an array slot for each ID defined here.  For that to
+ *    work, :c:macro:`SD_MAX` must be greater than the highest defined ID.
+ *    Please keep IDs in sequential order.
+ */
+enum stream_descriptor {
+	/** Highest descriptor must always be ``SD_MAX``. */
+	SD_MAX,
+};
+
+/**
+ * Stream Information Object.
+ *
+ * This struct contains the information necessary for :c:func:`epic_stream_read`
+ * to read from a sensor's stream.  This consists of:
+ */
+struct stream_info {
+	/**
+	 * A FreeRTOS queue handle.
+	 *
+	 * Management of this queue is the sensor drivers responsibility.
+	 */
+	QueueHandle_t queue;
+	/** The size of one data packet (= queue element). */
+	size_t item_size;
+	/**
+	 * An optional function to call before performing the read.  Set to
+	 * ``NULL`` if unused.
+	 *
+	 * ``poll_stream()`` is intended for sensors who passively collect data.
+	 * A sensor driver might for example retrieve the latest samples in this
+	 * function instead of actively polling in a task loop.
+	 *
+	 * The function registered here should never block for a longer time.
+	 */
+	int (*poll_stream)();
+};
+
+/**
+ * Register a stream so it can be read from Epicardium API.
+ *
+ * :param int sd: Stream Descriptor.  Must be from the :c:type:`stream_descriptor` enum.
+ * :param stream_info stream: Stream info.
+ * :returns: ``0`` on success or a negative value on error.  Possible errors:
+ *
+ *    - ``-EINVAL``: Out of range sensor descriptor.
+ *    - ``-EACCES``: Stream already registered.
+ *    - ``-EBUSY``: The descriptor lock could not be acquired.
+ */
+int stream_register(int sd, struct stream_info *stream);
+
+/**
+ * Deregister a stream.
+ *
+ * :param int sd:  Stream Descriptor.
+ * :param stream_info stream: The stream which should be registered for the
+ *    stream ``sd``.  If a different stream is registered, this function
+ *    will return an error.
+ * :returns: ``0`` on success or a negative value on error.  Possible errors:
+ *
+ *    - ``-EINVAL``: Out of range sensor descriptor.
+ *    - ``-EACCES``: Stream ``stream`` was not registered for ``sd``.
+ *    - ``-EBUSY``: The descriptor lock could not be acquired.
+ */
+int stream_deregister(int sd, struct stream_info *stream);
+
+/*
+ * Initialize stream interface.  Called by main().
+ */
+int stream_init();
+
+#endif /* STREAM_H */