diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index cf7a7f350e9f50a06e5692deff129a065a2f87d6..470a41853895ef3fbaec66ebea29cb2a8e68490c 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -35,6 +35,9 @@ typedef unsigned int size_t;
 #define API_STREAM_READ        0x6
 #define API_INTERRUPT_ENABLE   0x7
 #define API_INTERRUPT_DISABLE  0x8
+#define API_LIGHT_SENSOR_RUN   0x9
+#define API_LIGHT_SENSOR_GET   0xa
+#define API_LIGHT_SENSOR_STOP  0xb
 
 #define API_DISP_OPEN          0x10
 #define API_DISP_CLOSE         0x11
@@ -398,4 +401,42 @@ API(API_DISP_CIRC,
 	    uint16_t pixelsize)
     );
 
+/**
+ * Start continuous readout of the light sensor. Will read light level
+ * at preconfigured interval and make it available via `epic_light_sensor_get()`.
+ *
+ * If the continuous readout was already running, this function will silently pass.
+ *
+ *
+ * :return: `0` if the start was successful or a negative error value
+ *      if an error occured. Possible errors:
+ *
+ *      - ``-EBUSY``: The timer could not be scheduled.
+ */
+API(API_LIGHT_SENSOR_RUN, int epic_light_sensor_run());
+
+/**
+ * Get the last light level measured by the continuous readout.
+ *
+ * :param uint16_t* value: where the last light level should be written.
+ * :return: `0` if the readout was successful or a negative error
+ *      value. Possible errors:
+ *
+ *      - ``-ENODATA``: Continuous readout not currently running.
+ */
+API(API_LIGHT_SENSOR_GET, int epic_light_sensor_get(uint16_t* value));
+
+
+/**
+ * Stop continuous readout of the light sensor.
+ *
+ * If the continuous readout wasn't running, this function will silently pass.
+ *
+ * :return: `0` if the stop was sucessful or a negative error value
+ *      if an error occured. Possible errors:
+ *
+ *      - ``-EBUSY``: The timer stop could not be scheduled.
+ */
+API(API_LIGHT_SENSOR_STOP, int epic_light_sensor_stop());
+
 #endif /* _EPICARDIUM_H */
diff --git a/epicardium/modules/light_sensor.c b/epicardium/modules/light_sensor.c
new file mode 100644
index 0000000000000000000000000000000000000000..ea5c4ccc3bf012dbc7e730bd2818ffdec57aeb45
--- /dev/null
+++ b/epicardium/modules/light_sensor.c
@@ -0,0 +1,76 @@
+#include "FreeRTOS.h"
+#include "timers.h"
+#include "led.h"
+#include "mxc_config.h"
+#include "adc.h"
+#include "gpio.h"
+#include <errno.h>
+
+#define READ_FREQ pdMS_TO_TICKS(100)
+
+static uint16_t last_value;
+static TimerHandle_t poll_timer;
+static StaticTimer_t poll_timer_buffer;
+
+int epic_light_sensor_init()
+{
+	const sys_cfg_adc_t sys_adc_cfg =
+		NULL; /* No system specific configuration needed. */
+	if (ADC_Init(0x9, &sys_adc_cfg) != E_NO_ERROR) {
+		return -EINVAL;
+	}
+	GPIO_Config(&gpio_cfg_adc7);
+	return 0;
+}
+
+void readAdcCallback()
+{
+	ADC_StartConvert(ADC_CH_7, 0, 0);
+	ADC_GetData(&last_value);
+}
+
+int epic_light_sensor_run()
+{
+	epic_light_sensor_init();
+
+	if (!poll_timer) {
+		poll_timer = xTimerCreateStatic(
+			"light_sensor_adc",
+			READ_FREQ,
+			pdTRUE,
+			NULL,
+			readAdcCallback,
+			&poll_timer_buffer
+		);
+		// since &poll_timer_buffer is not NULL, xTimerCreateStatic should allways succeed, so
+		// we don't need to check for poll_timer being NULL.
+	}
+	if (xTimerIsTimerActive(poll_timer) == pdFALSE) {
+		if (xTimerStart(poll_timer, 0) != pdPASS) {
+			return -EBUSY;
+		}
+	}
+	return 0;
+}
+
+int epic_light_sensor_stop()
+{
+	if (!poll_timer || xTimerIsTimerActive(poll_timer) == pdFALSE) {
+		// timer wasn't running (or never started), just silently pass
+		return 0;
+	}
+
+	if (xTimerStop(poll_timer, 0) != pdPASS) {
+		return -EBUSY;
+	}
+	return 0;
+}
+
+int epic_light_sensor_get(uint16_t *value)
+{
+	if (!poll_timer || !xTimerIsTimerActive(poll_timer)) {
+		return -ENODATA;
+	}
+	*value = last_value;
+	return 0;
+}
diff --git a/epicardium/modules/meson.build b/epicardium/modules/meson.build
index d552623e237ef83efa144b5dbf7690f61e922deb..68c1b9d909170d69157fe844fe2100f119b00423 100644
--- a/epicardium/modules/meson.build
+++ b/epicardium/modules/meson.build
@@ -7,4 +7,5 @@ module_sources = files(
   'serial.c',
   'stream.c',
   'vibra.c',
+  'light_sensor.c',
 )
diff --git a/pycardium/meson.build b/pycardium/meson.build
index ad48e91ccff44b82d88dfafbae695de210764947..895be888ee81bdd886b990d12f7c6d63321a6c2e 100644
--- a/pycardium/meson.build
+++ b/pycardium/meson.build
@@ -6,6 +6,7 @@ modsrc = files(
   'modules/sys_display.c',
   'modules/utime.c',
   'modules/vibra.c',
+  'modules/light_sensor.c'
 )
 
 #################################
diff --git a/pycardium/modules/light_sensor.c b/pycardium/modules/light_sensor.c
new file mode 100644
index 0000000000000000000000000000000000000000..39f612a91a87946f426b7b1292121b0d0518a9dd
--- /dev/null
+++ b/pycardium/modules/light_sensor.c
@@ -0,0 +1,60 @@
+#include "py/obj.h"
+#include "py/runtime.h"
+#include "py/builtin.h"
+#include "epicardium.h"
+
+STATIC mp_obj_t mp_light_sensor_start()
+{
+	int status = epic_light_sensor_run();
+	if (status == -EBUSY) {
+		mp_raise_msg(
+			&mp_type_RuntimeError, "timer could not be scheduled"
+		);
+	}
+	return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(light_sensor_start_obj, mp_light_sensor_start);
+
+STATIC mp_obj_t mp_light_sensor_get_reading()
+{
+	uint16_t last;
+	int status = epic_light_sensor_get(&last);
+	if (status == -ENODATA) {
+		mp_raise_ValueError("sensor not running");
+		return mp_const_none;
+	}
+	return mp_obj_new_int_from_uint(last);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(
+	light_sensor_get_obj, mp_light_sensor_get_reading
+);
+
+STATIC mp_obj_t mp_light_sensor_stop()
+{
+	int status = epic_light_sensor_stop();
+	if (status == -EBUSY) {
+		mp_raise_msg(
+			&mp_type_RuntimeError, "timer could not be stopped"
+		);
+	}
+	return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(light_sensor_stop_obj, mp_light_sensor_stop);
+
+STATIC const mp_rom_map_elem_t light_sensor_module_globals_table[] = {
+	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_light_sensor) },
+	{ MP_ROM_QSTR(MP_QSTR_start), MP_ROM_PTR(&light_sensor_start_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&light_sensor_stop_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_get_reading), MP_ROM_PTR(&light_sensor_get_obj) }
+};
+STATIC MP_DEFINE_CONST_DICT(
+	light_sensor_module_globals, light_sensor_module_globals_table
+);
+
+const mp_obj_module_t light_sensor_module = {
+	.base    = { &mp_type_module },
+	.globals = (mp_obj_dict_t *)&light_sensor_module_globals,
+};
+
+/* clang-format off */
+MP_REGISTER_MODULE(MP_QSTR_light_sensor, light_sensor_module, MODULE_LIGHT_SENSOR_ENABLED);
diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h
index 707a27af2db328ff0d6c29699e7726d755065dbd..3dda54d667557f66d39978788fbde813f8e39390 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -40,3 +40,9 @@ Q(line)
 Q(rect)
 Q(circ)
 Q(clear)
+
+/* ambient */
+Q(light_sensor)
+Q(start)
+Q(get_reading)
+Q(stop)
diff --git a/pycardium/mpconfigport.h b/pycardium/mpconfigport.h
index b348861acb31ca0b6697f266e4edeb133c7adf10..6bfb2c6717fcddb0bfcd585fb13c401b5df86bb9 100644
--- a/pycardium/mpconfigport.h
+++ b/pycardium/mpconfigport.h
@@ -42,6 +42,7 @@
 #define MODULE_VIBRA_ENABLED                (1)
 #define MODULE_INTERRUPT_ENABLED            (1)
 #define MODULE_DISPLAY_ENABLED              (1)
+#define MODULE_LIGHT_SENSOR_ENABLED         (1)
 
 /*
  * This port is intended to be 32-bit, but unfortunately, int32_t for