diff --git a/epicardium/modules/buttons.c b/epicardium/modules/buttons.c
index 91f5ad02d9000e22cbf13b93c9d2170891856da6..14e9e613719e3e5279f811393afdfaf843025430 100644
--- a/epicardium/modules/buttons.c
+++ b/epicardium/modules/buttons.c
@@ -26,7 +26,7 @@ uint8_t epic_buttons_read(uint8_t mask)
 		 * Not using PB_Get() here as that performs one I2C transcation
 		 * per button.
 		 */
-		uint8_t pin_status = ~portexpander_get();
+		uint8_t pin_status = ~portexpander_in_get(0xFF);
 
 		hwlock_release(HWLOCK_I2C);
 
diff --git a/lib/card10/card10.c b/lib/card10/card10.c
index 371912d5909ce5f479f7761ea3fe5b5d87e159e0..306d9f4d7c0ac720145f991072ce61475d3bf528 100644
--- a/lib/card10/card10.c
+++ b/lib/card10/card10.c
@@ -215,6 +215,7 @@ void core1_stop(void)
 void card10_poll(void)
 {
 	pmic_poll();
+	portexpander_poll();
 }
 
 void card10_reset(void)
diff --git a/lib/card10/display.c b/lib/card10/display.c
index f5fd471579a41a0ea520f35594dd0553b089490d..f9c2f90d7e7e0fa0a0d25d86cf0a76844217bc98 100644
--- a/lib/card10/display.c
+++ b/lib/card10/display.c
@@ -22,7 +22,7 @@ void display_set_reset_pin(uint8_t state)
 	if (!portexpander_detected()) {
 		MAX77650_setDO(state ? true : false);
 	} else {
-		portexpander_set(4, state);
+		portexpander_out_put(PIN_4, state);
 	}
 }
 
diff --git a/lib/card10/pb.c b/lib/card10/pb.c
index 42525527a86aa117121638863c47b5f20b271a3f..727882ddd57a724d25180bea1ae835b2008932c6 100644
--- a/lib/card10/pb.c
+++ b/lib/card10/pb.c
@@ -116,7 +116,7 @@ int PB_Get(unsigned int pb)
 	case 3:
 	case 4:
 		if (portexpander_detected()) {
-			uint8_t port = portexpander_get();
+			uint8_t port = portexpander_in_get(0xFF);
 			return (port & (1 << expander_pins[pb - 1])) == 0;
 		} else {
 			return GPIO_InGet(&pb_pin[pb - 1]) == 0;
diff --git a/lib/card10/portexpander.c b/lib/card10/portexpander.c
index c6a8c302292828c5796d6b8f95a82fc1a9db8e26..b799e21529fe08ebd991636faf5b798487c5bd22 100644
--- a/lib/card10/portexpander.c
+++ b/lib/card10/portexpander.c
@@ -1,5 +1,10 @@
+/* PCAL6408A I2C port expander */
+
+/* **** Includes **** */
 #include "portexpander.h"
 
+#include "mxc_config.h"
+#include "mxc_assert.h"
 #include "i2c.h"
 
 #include <stdio.h>
@@ -7,10 +12,7 @@
 #include <string.h>
 #include <stdbool.h>
 
-// PCAL6408A I2C port expander
-
-static bool detected = false;
-static uint8_t output_state;
+/* **** Definitions **** */
 
 /* clang-format off */
 #define PE_ADDR 0x42
@@ -40,54 +42,147 @@ static uint8_t output_state;
 
 #define PE_INPUT_MASK ((uint8_t)0b01101000) // 3, 5, 6 = input
 
+/* **** Globals **** */
+
+static bool detected = false;
+
+static volatile bool interrupt_pending;
+
+static uint8_t type_state           = 0xFF;
+static uint8_t output_state         = 0xFF;
+static uint8_t pull_enable_state    = 0x00;
+static uint8_t pull_selection_state = 0xFF;
+static uint8_t int_mask_state       = 0xFF;
+
+static gpio_int_pol_t int_edge_config[8] = { 0 };
+
+static pe_callback callbacks[8] = { NULL };
+static void *cbparam[8]         = { NULL };
+
+const gpio_cfg_t pe_int_pin = { PORT_1, PIN_7, GPIO_FUNC_IN, GPIO_PAD_PULL_UP };
+
+static const portexpander_cfg_t pe_pin_config[] = {
+	{ PE_INPUT_MASK, GPIO_FUNC_IN, GPIO_PAD_PULL_UP },
+	{ ~PE_INPUT_MASK, GPIO_FUNC_OUT, GPIO_PAD_PULL_UP },
+};
+
+/* **** Functions **** */
+
 static int portexpander_write(uint8_t command, uint8_t data)
 {
 	uint8_t i2c_data[2] = { command, data };
 	return I2C_MasterWrite(MXC_I2C1_BUS0, PE_ADDR, i2c_data, 2, 0);
 }
 
+/* ************************************************************************** */
 static int portexpander_read(uint8_t command, uint8_t *data)
 {
 	I2C_MasterWrite(MXC_I2C1_BUS0, PE_ADDR, &command, 1, 1);
 	return I2C_MasterRead(MXC_I2C1_BUS0, PE_ADDR, data, 1, 0);
 }
 
-void portexpander_init(void)
+/* ************************************************************************** */
+int portexpander_init(void)
 {
 	int ret;
 
-	// Enable pull-ups for buttons (type defaults to pull-up)
-	ret = portexpander_write(PE_C_PULL_ENABLE, PE_INPUT_MASK);
-
+	// Set _all_ outputs to open-drain to support the high side p-channel transistors.
+	ret = portexpander_write(PE_C_OUTPUT_PORT_CONFIG, PE_OUT_OPEN_DRAIN);
 	if (ret != 2) {
 		printf("portexpander NOT detected\n");
 		detected = false;
-		return;
+		return E_NO_DEVICE;
 	}
 	detected = true;
 
-	// Set _all_ outputs to open-drain to support the high side p-channel transistors.
-	portexpander_write(PE_C_OUTPUT_PORT_CONFIG, PE_OUT_OPEN_DRAIN);
+	// Set outputs to high
+	portexpander_out_set(~PE_INPUT_MASK);
 
+	// Enable pull-ups for buttons
 	// Enable outputs for the transistors, the LED and the LCD reset
-	portexpander_write(PE_C_CONFIG, PE_INPUT_MASK);
+	for (int i = 0; i < sizeof(pe_pin_config) / sizeof(pe_pin_config[0]);
+	     i++) {
+		MXC_ASSERT(
+			portexpander_config(&pe_pin_config[i]) == E_NO_ERROR
+		);
+	}
+
+	// Latch inputs so we can figure out whether an interrupt was caused by a rising or falling edge
+	portexpander_write(PE_C_INPUT_LATCH, PE_INPUT_MASK);
+
+	// Configure interrupt GPIO
+	MXC_ASSERT(GPIO_Config(&pe_int_pin) == E_NO_ERROR);
+
+	// Configure and enable portexpander interrupt
+	GPIO_RegisterCallback(
+		&pe_int_pin, &portexpander_interrupt_callback, NULL
+	);
+	MXC_ASSERT(
+		GPIO_IntConfig(&pe_int_pin, GPIO_INT_EDGE, GPIO_INT_FALLING) ==
+		E_NO_ERROR);
+	GPIO_IntEnable(&pe_int_pin);
+	NVIC_EnableIRQ((IRQn_Type)MXC_GPIO_GET_IRQ(pe_int_pin.port));
+
+	return E_SUCCESS;
+}
+
+/* ************************************************************************** */
+int portexpander_config(const portexpander_cfg_t *cfg)
+{
+	// Set the GPIO type
+	switch (cfg->func) {
+	case GPIO_FUNC_IN:
+		type_state |= cfg->mask;
+		break;
+	case GPIO_FUNC_OUT:
+		type_state &= ~cfg->mask;
+		break;
+	default:
+		return E_BAD_PARAM;
+	}
 
-	// Set outputs to high (i.e. open-drain)
-	output_state = ~PE_INPUT_MASK;
-	portexpander_write(PE_C_OUTPUT_PORT, output_state);
+	if (portexpander_write(PE_C_CONFIG, type_state) != 2) {
+		return E_NO_DEVICE;
+	}
+
+	switch (cfg->pad) {
+	case GPIO_PAD_NONE:
+		pull_enable_state &= ~cfg->mask;
+		break;
+	case GPIO_PAD_PULL_UP:
+		pull_selection_state |= cfg->mask;
+		pull_enable_state |= cfg->mask;
+		break;
+	case GPIO_PAD_PULL_DOWN:
+		pull_selection_state &= ~cfg->mask;
+		pull_enable_state |= cfg->mask;
+		break;
+	default:
+		return E_BAD_PARAM;
+	}
+
+	portexpander_write(PE_C_PULL_ENABLE, pull_selection_state);
+	portexpander_write(PE_C_PULL_ENABLE, pull_enable_state);
+
+	return E_NO_ERROR;
 }
 
-uint8_t portexpander_get(void)
+/* ************************************************************************** */
+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) {
 		portexpander_read(PE_C_INPUT_PORT, &buf);
 	}
 
-	return buf;
+	return buf & mask;
 }
 
+/* ************************************************************************** */
 bool portexpander_detected(void)
 {
 	return detected;
@@ -106,6 +201,24 @@ void portexpander_set(uint8_t pin, uint8_t value)
 	}
 }
 
+/* ************************************************************************** */
+void portexpander_out_set(uint8_t mask)
+{
+	if (detected) {
+		output_state |= mask;
+		portexpander_write(PE_C_OUTPUT_PORT, output_state);
+	}
+}
+
+/* ************************************************************************** */
+void portexpander_out_clr(uint8_t mask)
+{
+	if (detected) {
+		output_state &= ~mask;
+		portexpander_write(PE_C_OUTPUT_PORT, output_state);
+	}
+}
+
 void portexpander_prep(uint8_t pin, uint8_t value)
 {
 	if (pin < 8) {
@@ -117,6 +230,12 @@ void portexpander_prep(uint8_t pin, uint8_t value)
 	}
 }
 
+/* ************************************************************************** */
+uint8_t portexpander_out_get(uint8_t mask)
+{
+	return output_state & mask;
+}
+
 void portexpander_update(void)
 {
 	if (detected) {
@@ -124,11 +243,126 @@ void portexpander_update(void)
 	}
 }
 
-void portexpander_set_mask(uint8_t mask, uint8_t values)
+/* ************************************************************************** */
+void portexpander_out_put(uint8_t mask, uint8_t val)
+{
+	if (detected) {
+		output_state = (output_state & ~mask) | (val & mask);
+		portexpander_write(PE_C_OUTPUT_PORT, output_state);
+	}
+}
+
+/* ************************************************************************** */
+void portexpander_out_toggle(uint8_t mask)
 {
 	if (detected) {
-		output_state &= ~(mask & ~values);
-		output_state |= mask & values;
+		output_state ^= mask;
 		portexpander_write(PE_C_OUTPUT_PORT, output_state);
 	}
 }
+
+/* ************************************************************************** */
+void portexpander_int_config(uint8_t mask, gpio_int_pol_t edge)
+{
+	if (detected) {
+		for (uint8_t pin = 0; pin < 8; ++pin) {
+			if (mask & (1 << pin)) {
+				int_edge_config[pin] = edge;
+			}
+		}
+	}
+}
+
+/* ************************************************************************** */
+void portexpander_int_enable(uint8_t mask)
+{
+	if (detected) {
+		int_mask_state &= ~mask;
+		portexpander_write(PE_C_INT_MASK, int_mask_state);
+	}
+}
+
+/* ************************************************************************** */
+void portexpander_int_disable(uint8_t mask)
+{
+	if (detected) {
+		int_mask_state |= mask;
+		portexpander_write(PE_C_INT_MASK, int_mask_state);
+	}
+}
+
+/* ************************************************************************** */
+uint8_t portexpander_int_status()
+{
+	uint8_t buf = 0;
+	if (detected) {
+		portexpander_read(PE_C_INT_STATUS, &buf);
+	}
+
+	return buf;
+}
+
+/* ************************************************************************** */
+void portexpander_int_clr(uint8_t mask)
+{
+	if (detected) {
+		uint8_t tmp_mask = int_mask_state | mask;
+
+		// Setting an interrupt mask clears the corresponding interrupt
+		portexpander_write(PE_C_INT_MASK, tmp_mask);
+		portexpander_write(PE_C_INT_MASK, int_mask_state);
+	}
+}
+
+/* ************************************************************************** */
+int portexpander_register_callback(
+	uint8_t mask, pe_callback callback, void *cbdata
+) {
+	if (!detected) {
+		return E_NO_DEVICE;
+	}
+
+	for (uint8_t pin = 0; pin < 8; ++pin) {
+		if (mask & (1 << pin)) {
+			callbacks[pin] = callback;
+			cbparam[pin]   = cbdata;
+		}
+	}
+
+	return E_NO_ERROR;
+}
+
+/* ************************************************************************** */
+__attribute__((weak)) void portexpander_interrupt_callback(void *_)
+{
+	GPIO_IntDisable(&pe_int_pin);
+	GPIO_IntClr(&pe_int_pin);
+	interrupt_pending = true;
+}
+
+/* ************************************************************************** */
+void portexpander_poll()
+{
+	if (detected && interrupt_pending) {
+		interrupt_pending = false;
+
+		uint8_t caused_by = portexpander_int_status();
+		// Port read resets interrupts
+		uint8_t port_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](cbparam[pin]);
+				}
+			}
+		}
+	}
+}
diff --git a/lib/card10/portexpander.h b/lib/card10/portexpander.h
index 86614bdd09305ae725fd7d2145c058e184602ed2..727f8dfe5eed2aa7fa992597e4209987b0abede9 100644
--- a/lib/card10/portexpander.h
+++ b/lib/card10/portexpander.h
@@ -1,15 +1,48 @@
 #ifndef PORTEXPANDER_H
 #define PORTEXPANDER_H
 
+#include "mxc_config.h"
+
 #include <stdint.h>
 #include <stdbool.h>
 
-void portexpander_init(void);
-uint8_t portexpander_get(void);
 void portexpander_set(uint8_t pin, uint8_t value);
-void portexpander_set_mask(uint8_t mask, uint8_t values);
 void portexpander_prep(uint8_t pin, uint8_t value);
 void portexpander_update(void);
+
+/**
+ * Structure type for configuring the portexpander.
+ */
+typedef struct {
+        uint8_t mask;           /**< Pin mask (multiple pins may be set) */
+        gpio_func_t func;       /**< Function type */
+        gpio_pad_t pad;         /**< Pad type */
+} portexpander_cfg_t;
+
+
+typedef void (*pe_callback)(void *cbdata);
+
+int portexpander_init(void);
 bool portexpander_detected(void);
 
+int portexpander_config(const portexpander_cfg_t *cfg);
+
+uint8_t portexpander_in_get(uint8_t mask);
+
+void portexpander_out_set(uint8_t mask);
+void portexpander_out_clr(uint8_t mask);
+void portexpander_out_put(uint8_t mask, uint8_t val);
+void portexpander_out_toggle(uint8_t mask);
+uint8_t portexpander_out_get(uint8_t mask);
+
+void portexpander_int_config(uint8_t mask, gpio_int_pol_t edge);
+void portexpander_int_enable(uint8_t mask);
+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_poll();
+
+void portexpander_interrupt_callback(void *_);
+
 #endif