diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 2428cb583fe0924fb33a1046bb1693e2349a5ad3..17adf63a0a4b22033cd5ca5e8900a5a784daf0fe 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -196,9 +196,11 @@ API(API_INTERRUPT_DISABLE, int epic_interrupt_disable(api_int_id_t int_id));
 #define EPIC_INT_BHI160_GYROSCOPE       6
 /** MAX30001 ECG.  See :c:func:`epic_isr_max30001_ecg`. */
 #define EPIC_INT_MAX30001_ECG           7
+/** Button interrupt.  See :c:func:`epic_isr_button`. */
+#define EPIC_INT_BUTTON                 8
 
 /* Number of defined interrupts. */
-#define EPIC_INT_NUM                    8
+#define EPIC_INT_NUM                    9
 /* clang-format on */
 
 /*
@@ -445,6 +447,12 @@ enum epic_button {
  */
 API(API_BUTTONS_READ, uint8_t epic_buttons_read(uint8_t mask));
 
+struct epic_isr_button_param {
+        unsigned int pb;
+        bool falling;
+};
+API_ISR(EPIC_INT_BUTTON, epic_isr_button);
+
 /**
  * Wristband GPIO
  * ==============
diff --git a/epicardium/main.c b/epicardium/main.c
index a0c5fad5758c141428512b86ffc287b229ef113a..dffbb9b46320ba54d8ae4d6f14665bd11541588b 100644
--- a/epicardium/main.c
+++ b/epicardium/main.c
@@ -150,6 +150,18 @@ int main(void)
 		abort();
 	}
 
+	/* Buttons */
+	if (xTaskCreate(
+		    vButtonTask,
+		    (const char *)"Button",
+		    configMINIMAL_STACK_SIZE,
+		    NULL,
+		    tskIDLE_PRIORITY + 1,
+		    NULL) != pdPASS) {
+		LOG_CRIT("startup", "Failed to create %s task!", "Button");
+		abort();
+	}
+
 	/*
 	 * Initialize serial driver data structures.
 	 */
diff --git a/epicardium/modules/buttons.c b/epicardium/modules/buttons.c
index 14e9e613719e3e5279f811393afdfaf843025430..eeb1eb8ff6d0fa4d7a4a30edcda46d3e6ea32349 100644
--- a/epicardium/modules/buttons.c
+++ b/epicardium/modules/buttons.c
@@ -1,11 +1,22 @@
 #include "epicardium.h"
 #include "modules/modules.h"
 #include "modules/log.h"
+#include "api/interrupt-sender.h"
+#include "api/dispatcher.h"
 
 #include "portexpander.h"
+#include "pb.h"
 #include "MAX77650-Arduino-Library.h"
 
 #include <stdint.h>
+#include <string.h>
+
+#define LOCK_WAIT pdMS_TO_TICKS(1000)
+
+static TaskHandle_t button_task_id = NULL;
+
+enum { BUTTON_NOTIFY_IRQ = 1,
+};
 
 static const uint8_t pin_mask[] = {
 	[BUTTON_LEFT_BOTTOM]  = 1 << 5,
@@ -13,6 +24,13 @@ static const uint8_t pin_mask[] = {
 	[BUTTON_RIGHT_TOP]    = 1 << 6,
 };
 
+static const uint8_t pb_epic_mapping[] = {
+	BUTTON_LEFT_BOTTOM,
+	BUTTON_LEFT_TOP,
+	BUTTON_RIGHT_BOTTOM,
+	BUTTON_RIGHT_TOP,
+};
+
 uint8_t epic_buttons_read(uint8_t mask)
 {
 	uint8_t ret = 0;
@@ -43,3 +61,64 @@ uint8_t epic_buttons_read(uint8_t mask)
 
 	return ret;
 }
+
+void portexpander_interrupt_callback(void *_)
+{
+	portexpander_ack_interrupt();
+
+	BaseType_t xHigherPriorityTaskWoken = pdFALSE;
+	if (button_task_id != NULL) {
+		xTaskNotifyFromISR(
+			button_task_id,
+			BUTTON_NOTIFY_IRQ,
+			eSetBits,
+			&xHigherPriorityTaskWoken
+		);
+		portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
+	}
+}
+
+void portexpander_poll_interrupts()
+{
+	while (hwlock_acquire(HWLOCK_I2C, LOCK_WAIT) < 0) {
+		LOG_WARN("portexpander", "Failed to acquire I2C. Retrying ...");
+		xTaskNotify(button_task_id, BUTTON_NOTIFY_IRQ, eSetBits);
+		return;
+	}
+
+	uint8_t pending = 0;
+	uint8_t levels  = 0;
+
+	portexpander_get_pending_interrupts(&pending, &levels);
+	hwlock_release(HWLOCK_I2C);
+
+	portexpander_handle_pending_interrupts(&pending, &levels);
+}
+
+void button_callback_handler(unsigned int pb, bool falling)
+{
+	// TODO: Proper parameter passing
+	struct epic_isr_button_param p = { pb_epic_mapping[pb - 1], falling };
+	memcpy(API_CALL_MEM->buffer + 0x20, &p, sizeof(p));
+	api_interrupt_trigger(EPIC_INT_BUTTON);
+}
+
+void vButtonTask(void *pvParameters)
+{
+	button_task_id = xTaskGetCurrentTaskHandle();
+	LOG_INFO("buttons", "Creating button task");
+
+	portexpander_int_clr(0xFF);
+
+	PB_RegisterCallback(1, button_callback_handler);
+	PB_RegisterCallback(3, button_callback_handler);
+	PB_RegisterCallback(4, button_callback_handler);
+
+	while (1) {
+		uint32_t reason = ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
+
+		if (reason & BUTTON_NOTIFY_IRQ) {
+			portexpander_poll_interrupts();
+		}
+	}
+}
diff --git a/epicardium/modules/modules.h b/epicardium/modules/modules.h
index 330f4f67e9c056df601f12a560029047c1872b59..fa83d5e3ad120253faedd25808ed069f366b44b1 100644
--- a/epicardium/modules/modules.h
+++ b/epicardium/modules/modules.h
@@ -111,4 +111,7 @@ void max30001_mutex_init(void);
 #define MAX30001_MUTEX_WAIT_MS          50
 extern gpio_cfg_t gpio_configs[];
 
+/* ---------- BUTTONS ------------------------------------------------------ */
+void vButtonTask(void *pvParameters);
+
 #endif /* MODULES_H */
diff --git a/lib/card10/portexpander.c b/lib/card10/portexpander.c
index 52eb88975a660306f540567b55abe56dc53e6151..d9ab4b0d7b430cbad2d673959f20783290e2cd16 100644
--- a/lib/card10/portexpander.c
+++ b/lib/card10/portexpander.c
@@ -170,9 +170,6 @@ int portexpander_config(const portexpander_cfg_t *cfg)
 /* ************************************************************************** */
 uint8_t portexpander_in_get(uint8_t mask)
 {
-	// Reading the input port clears interrupts, so we need to check them here to avoid losing information
-	portexpander_poll();
-
 	uint8_t buf = 0xFF;
 
 	if (detected) {
@@ -303,35 +300,52 @@ int portexpander_register_callback(
 
 /* ************************************************************************** */
 __attribute__((weak)) void portexpander_interrupt_callback(void *_)
+{
+	portexpander_ack_interrupt();
+}
+
+void portexpander_ack_interrupt()
 {
 	GPIO_IntDisable(&pe_int_pin);
 	GPIO_IntClr(&pe_int_pin);
 	interrupt_pending = true;
 }
 
-/* ************************************************************************** */
-void portexpander_poll()
+void portexpander_get_pending_interrupts(uint8_t *pending, uint8_t *levels)
 {
-	if (detected && interrupt_pending) {
+	if (detected) {
 		interrupt_pending = false;
-
-		uint8_t caused_by = portexpander_int_status();
+		*pending          = portexpander_int_status();
 		// Port read resets interrupts
-		uint8_t port_levels = portexpander_in_get(0xFF);
+		*levels = portexpander_in_get(0xFF);
 
 		GPIO_IntEnable(&pe_int_pin);
+	}
+}
 
-		for (uint8_t pin = 0; pin < 8; ++pin) {
-			if ((caused_by & (1 << pin)) && callbacks[pin]) {
-				gpio_int_pol_t edge_type =
-					(port_levels & (1 << pin) ?
-						 GPIO_INT_RISING :
-						 GPIO_INT_FALLING);
-				if ((int_edge_config[pin] == GPIO_INT_BOTH) ||
-				    (edge_type == int_edge_config[pin])) {
-					callbacks[pin](edge_type, cbparam[pin]);
-				}
+void portexpander_handle_pending_interrupts(uint8_t *pending, uint8_t *levels)
+{
+	for (uint8_t pin = 0; pin < 8; ++pin) {
+		if ((*pending & (1 << pin)) && callbacks[pin]) {
+			gpio_int_pol_t edge_type =
+				(*levels & (1 << pin) ? GPIO_INT_RISING :
+							GPIO_INT_FALLING);
+			if ((int_edge_config[pin] == GPIO_INT_BOTH) ||
+			    (edge_type == int_edge_config[pin])) {
+				callbacks[pin](edge_type, cbparam[pin]);
 			}
 		}
 	}
 }
+
+/* ************************************************************************** */
+void portexpander_poll()
+{
+	if (detected && interrupt_pending) {
+		uint8_t pending;
+		uint8_t levels;
+
+		portexpander_get_pending_interrupts(&pending, &levels);
+		portexpander_handle_pending_interrupts(&pending, &levels);
+	}
+}
diff --git a/lib/card10/portexpander.h b/lib/card10/portexpander.h
index b260d935b127bb913d26a003867412bb1ac51140..7216a9be0cb7b829c348e88d516d1b187c221d76 100644
--- a/lib/card10/portexpander.h
+++ b/lib/card10/portexpander.h
@@ -37,8 +37,11 @@ void portexpander_int_disable(uint8_t mask);
 uint8_t portexpander_int_status();
 void portexpander_int_clr(uint8_t mask);
 int portexpander_register_callback(uint8_t mask, pe_callback callback, void *cbdata);
+void portexpander_get_pending_interrupts(uint8_t *pending, uint8_t *levels);
+void portexpander_handle_pending_interrupts(uint8_t *pending, uint8_t *levels);
 void portexpander_poll();
 
 void portexpander_interrupt_callback(void *_);
+void portexpander_ack_interrupt();
 
 #endif
diff --git a/pycardium/modules/buttons.c b/pycardium/modules/buttons.c
index ab8dabd7d28692f7e586dc64d717c4c3cfb6aa31..174579e2e137493db9f74a203f0e35b30185db23 100644
--- a/pycardium/modules/buttons.c
+++ b/pycardium/modules/buttons.c
@@ -2,8 +2,50 @@
 #include "py/objlist.h"
 #include "py/runtime.h"
 #include <stdio.h>
+#include <string.h>
 
 #include "epicardium.h"
+#include "interrupt.h"
+
+#include "api/caller.h"
+
+// This should really be defined somewhere in a central location to make it reusable (+ use the better linux kernel version)
+#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
+
+/* clang-format off */
+static const uint8_t button_callback_mapping[] = {
+        BUTTON_LEFT_BOTTOM,
+        BUTTON_RIGHT_BOTTOM,
+        BUTTON_RIGHT_TOP,
+};
+/* clang-format on */
+
+static mp_obj_t button_callbacks[] = {
+	[0 ... ARRAY_SIZE(button_callback_mapping) - 1] = mp_const_none
+};
+
+void epic_isr_button()
+{
+	// TODO: Proper parameter passing
+	struct epic_isr_button_param p;
+	memcpy(&p, API_CALL_MEM->buffer + 0x20, sizeof(p));
+
+	mp_obj_t callback = mp_const_none;
+
+	for (uint8_t i = 0; i < ARRAY_SIZE(button_callback_mapping); ++i) {
+		if (p.pb == button_callback_mapping[i]) {
+			callback = button_callbacks[i];
+			break;
+		}
+	}
+
+	if (callback != mp_const_none) {
+		// This may drop some events if the queue is full
+		mp_sched_schedule(
+			callback, (p.falling ? mp_const_true : mp_const_false)
+		);
+	}
+}
 
 static mp_obj_t mp_buttons_read(mp_obj_t mask_in)
 {
@@ -13,9 +55,51 @@ static mp_obj_t mp_buttons_read(mp_obj_t mask_in)
 }
 static MP_DEFINE_CONST_FUN_OBJ_1(buttons_read_obj, mp_buttons_read);
 
+static mp_obj_t mp_buttons_set_callback(mp_obj_t mask_in, mp_obj_t callback)
+{
+	uint8_t mask             = mp_obj_get_int(mask_in);
+	uint8_t param_check_mask = mask;
+	bool enable_interrupt    = false;
+
+	for (uint8_t i = 0; i < ARRAY_SIZE(button_callback_mapping); ++i) {
+		param_check_mask &= ~button_callback_mapping[i];
+	}
+
+	if (param_check_mask) {
+		mp_raise_ValueError("Callbacks not supported for given button");
+	}
+
+	for (uint8_t i = 0; i < ARRAY_SIZE(button_callback_mapping); ++i) {
+		if (button_callback_mapping[i] & mask) {
+			button_callbacks[i] = callback;
+		}
+	}
+
+	for (uint8_t i = 0; i < ARRAY_SIZE(button_callbacks); ++i) {
+		if (button_callbacks[i] != mp_const_none) {
+			enable_interrupt = true;
+		}
+	}
+
+	mp_obj_t irq_id = MP_OBJ_NEW_SMALL_INT(EPIC_INT_BUTTON);
+
+	if (enable_interrupt) {
+		mp_interrupt_enable_callback(irq_id);
+	} else {
+		mp_interrupt_disable_callback(irq_id);
+	}
+
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_2(
+	buttons_set_callback_obj, mp_buttons_set_callback
+);
+
 static const mp_rom_map_elem_t buttons_module_globals_table[] = {
 	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_buttons) },
 	{ MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&buttons_read_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_set_callback),
+	  MP_ROM_PTR(&buttons_set_callback_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_BOTTOM_LEFT),
 	  MP_OBJ_NEW_SMALL_INT(BUTTON_LEFT_BOTTOM) },
 	{ MP_ROM_QSTR(MP_QSTR_BOTTOM_RIGHT),
diff --git a/pycardium/modules/interrupt.c b/pycardium/modules/interrupt.c
index 838ef2b3a397887221041804a0477a5d1cfdbce5..f35e6e4a3c07d9c3fb05aa247b137d9268419e50 100644
--- a/pycardium/modules/interrupt.c
+++ b/pycardium/modules/interrupt.c
@@ -7,17 +7,12 @@
 #include "py/obj.h"
 #include "py/runtime.h"
 
-// TODO: these should be intialized as mp_const_none
-mp_obj_t callbacks[EPIC_INT_NUM] = {
-	0,
-};
+mp_obj_t callbacks[EPIC_INT_NUM] = { [0 ... EPIC_INT_NUM - 1] = mp_const_none };
 
 void epic_isr_default_handler(api_int_id_t id)
 {
-	// TODO: check if id is out of rante
-	// TOOD: check against mp_const_none
 	if (id < EPIC_INT_NUM) {
-		if (callbacks[id]) {
+		if (callbacks[id] && (callbacks[id] != mp_const_none)) {
 			mp_sched_schedule(
 				callbacks[id], MP_OBJ_NEW_SMALL_INT(id)
 			);
@@ -93,6 +88,7 @@ static const mp_rom_map_elem_t interrupt_module_globals_table[] = {
 	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_BHI160_GYROSCOPE) },
 	{ MP_ROM_QSTR(MP_QSTR_MAX30001_ECG),
 	  MP_OBJ_NEW_SMALL_INT(EPIC_INT_MAX30001_ECG) },
+	{ MP_ROM_QSTR(MP_QSTR_BUTTON), MP_OBJ_NEW_SMALL_INT(EPIC_INT_BUTTON) },
 
 };
 static MP_DEFINE_CONST_DICT(
diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h
index 0ff0a8caac12fff03bcdb18b781a091a5784e98e..0b192d7fe8a60573ac33aa5471d7feb1721781d3 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -29,6 +29,7 @@ Q(TOP_RIGHT)
 /* buttons */
 Q(buttons)
 Q(read)
+Q(set_callback)
 Q(BOTTOM_LEFT)
 Q(TOP_LEFT)
 Q(BOTTOM_RIGHT)
@@ -57,6 +58,7 @@ Q(set_unix_time)
 Q(vibra)
 Q(vibrate)
 
+/* interrupt */
 Q(set_callback)
 Q(enable_callback)
 Q(disable_callback)
@@ -64,6 +66,7 @@ Q(BHI160_ACCELEROMETER)
 Q(BHI160_ORIENTATION)
 Q(BHI160_GYROSCOPE)
 Q(RTC_ALARM)
+Q(BUTTON)
 
 /* bhi160 */
 Q(sys_bhi160)