diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 67dcf2760056e4ac2e6715b522865b403947d654..3ed02dabb5e1aad6f8c64867e740ab5afd3b370f 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -81,6 +81,8 @@ typedef _Bool bool;
 #define API_RTC_SCHEDULE_ALARM     0x51
 #define API_RTC_SET_MILLISECONDS   0x52
 #define API_RTC_GET_MILLISECONDS   0x53
+#define API_RTC_GET_MONOTONIC_SECONDS      0x54
+#define API_RTC_GET_MONOTONIC_MILLISECONDS 0x55
 
 #define API_LEDS_SET               0x60
 #define API_LEDS_SET_HSV           0x61
@@ -1733,6 +1735,24 @@ API(API_FILE_MKDIR, int epic_file_mkdir(const char *dirname));
  * ===
  */
 
+/**
+ * Get the monotonic time in seconds.
+ *
+ * :return: monotonic time in seconds
+ */
+API(API_RTC_GET_MONOTONIC_SECONDS,
+	uint32_t epic_rtc_get_monotonic_seconds(void)
+);
+
+/**
+ * Get the monotonic time in ms.
+ *
+ * :return: monotonic time in milliseconds
+ */
+API(API_RTC_GET_MONOTONIC_MILLISECONDS,
+	uint64_t epic_rtc_get_monotonic_milliseconds(void)
+);
+
 /**
  * Read the current RTC value.
  *
diff --git a/epicardium/modules/rtc.c b/epicardium/modules/rtc.c
index de1ef70d26653a8b1f2441b8bf6a62a3d0124ef7..b563462dbc242194e7f931721d82fcd21d0b344e 100644
--- a/epicardium/modules/rtc.c
+++ b/epicardium/modules/rtc.c
@@ -9,6 +9,18 @@
 
 #include <stdint.h>
 
+uint64_t monotonic_offset = 0;
+
+uint32_t epic_rtc_get_monotonic_seconds(void)
+{
+	return epic_rtc_get_seconds() + monotonic_offset / 1000ULL;
+}
+
+uint64_t epic_rtc_get_monotonic_milliseconds(void)
+{
+	return epic_rtc_get_milliseconds() + monotonic_offset;
+}
+
 uint32_t epic_rtc_get_seconds(void)
 {
 	uint32_t sec, subsec;
@@ -32,12 +44,31 @@ uint64_t epic_rtc_get_milliseconds(void)
 	while (RTC_GetTime(&sec, &subsec) == E_BUSY) {
 		vTaskDelay(pdMS_TO_TICKS(4));
 	}
-	return subsec * 1000ULL / 4096 + sec * 1000ULL;
+
+	// Without the bias of 999 (0.24 milliseconds), this decoding function is
+	// numerically unstable:
+	//
+	// Encoding 5 milliseconds into 20 subsecs (using the encoding function in
+	// epic_rtc_set_milliseconds) and decoding it without the bias of 999 yields
+	// 4 milliseconds.
+	//
+	// The following invariants should hold when encoding / decoding from and to
+	// milliseconds / subseconds:
+	//
+	// - 0 <= encode(ms) < 4096 for 0 <= ms < 1000
+	// - decode(encode(ms)) == ms for 0 <= ms < 1000
+	// - 0 <= decode(subsec) < 1000 for 0 <= subsec < 4096
+	//
+	// These invariants were proven experimentally.
+	return (subsec * 1000ULL + 999ULL) / 4096 + sec * 1000ULL;
 }
 
 void epic_rtc_set_milliseconds(uint64_t milliseconds)
 {
 	uint32_t sec, subsec;
+	uint64_t old_milliseconds, diff;
+
+	old_milliseconds = epic_rtc_get_milliseconds();
 
 	sec    = milliseconds / 1000;
 	subsec = (milliseconds % 1000);
@@ -48,6 +79,9 @@ void epic_rtc_set_milliseconds(uint64_t milliseconds)
 		;
 	while (RTC_EnableRTCE(MXC_RTC) == E_BUSY)
 		;
+
+	diff = old_milliseconds - milliseconds;
+	monotonic_offset += diff;
 }
 
 void RTC_IRQHandler(void)
diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h
index 2e24b0504929c48546845cc9d7942f5f0a6e7536..0430d4d7c2df04658765d8cfa99b475ff84c6d87 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -48,6 +48,8 @@ Q(ticks_add)
 Q(ticks_diff)
 Q(localtime)
 Q(mktime)
+Q(monotonic)
+Q(monotonic_ms)
 Q(time)
 Q(time_ms)
 Q(set_time)
diff --git a/pycardium/modules/utime.c b/pycardium/modules/utime.c
index 4b06a9cd5419ffb77f433d3a4fed2a3a464df63b..9f71eef51e0dd22bec48f9c2091851c8c509cb27 100644
--- a/pycardium/modules/utime.c
+++ b/pycardium/modules/utime.c
@@ -51,6 +51,22 @@ static mp_obj_t time_time_ms(void)
 }
 MP_DEFINE_CONST_FUN_OBJ_0(time_time_ms_obj, time_time_ms);
 
+static mp_obj_t time_monotonic(void)
+{
+	mp_int_t seconds;
+	seconds = epic_rtc_get_monotonic_seconds();
+	return mp_obj_new_int(seconds);
+}
+MP_DEFINE_CONST_FUN_OBJ_0(time_monotonic_obj, time_monotonic);
+
+static mp_obj_t time_monotonic_ms(void)
+{
+	uint64_t milliseconds;
+	milliseconds = epic_rtc_get_monotonic_milliseconds();
+	return mp_obj_new_int_from_ull(milliseconds);
+}
+MP_DEFINE_CONST_FUN_OBJ_0(time_monotonic_ms_obj, time_monotonic_ms);
+
 static mp_obj_t time_localtime(size_t n_args, const mp_obj_t *args)
 {
 	mp_int_t seconds;
@@ -130,6 +146,9 @@ static const mp_rom_map_elem_t time_module_globals_table[] = {
 	{ MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_utime) },
 	{ MP_ROM_QSTR(MP_QSTR_time), MP_ROM_PTR(&time_time_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_time_ms), MP_ROM_PTR(&time_time_ms_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_monotonic), MP_ROM_PTR(&time_monotonic_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_monotonic_ms),
+	  MP_ROM_PTR(&time_monotonic_ms_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_set_time), MP_ROM_PTR(&time_set_time_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_set_unix_time),
 	  MP_ROM_PTR(&time_set_unix_time_obj) },