diff --git a/epicardium/epicardium.h b/epicardium/epicardium.h
index 1e6c48cef47b349311450b3d666f3e16d34c8871..96fbbbeab7d5b1201587ea3a6904089ab795a8a9 100644
--- a/epicardium/epicardium.h
+++ b/epicardium/epicardium.h
@@ -56,6 +56,7 @@ typedef _Bool bool;
 #define API_DISP_FRAMEBUFFER       0x29
 #define API_DISP_BACKLIGHT         0x2a
 #define API_DISP_PRINT_ADV         0x2b
+#define API_DISP_BLIT              0x2d
 
 /* API_BATTERY_VOLTAGE              0x30 */
 #define API_BATTERY_CURRENT        0x31
@@ -1670,6 +1671,25 @@ API(API_DISP_PIXEL, int epic_disp_pixel(
 	int16_t x, int16_t y, uint16_t color
 ));
 
+/**
+ * Blit an image buffer to display
+ *
+ * :param x: x position
+ * :param y: y position
+ * :param w: image width
+ * :param h: image height
+ * :param img: image data (rgb565)
+ * :param alpha: 8 bit alpha channel. Currently unused.
+ */
+API(API_DISP_BLIT, int epic_disp_blit(
+	int16_t x,
+	int16_t y,
+	int16_t w,
+	int16_t h,
+	uint16_t *img,
+	uint8_t *alpha
+));
+
 /**
  * Draws a line on the display
  *
diff --git a/epicardium/modules/display.c b/epicardium/modules/display.c
index c3a84812fcb3f105a8e1ccf4c710c64854d63269..d7607a2027735db1813c86fc49c8a1718756d0b3 100644
--- a/epicardium/modules/display.c
+++ b/epicardium/modules/display.c
@@ -87,6 +87,68 @@ int epic_disp_pixel(int16_t x, int16_t y, uint16_t color)
 	}
 }
 
+int epic_disp_blit(
+	int16_t pos_x,
+	int16_t pos_y,
+	int16_t width,
+	int16_t height,
+	uint16_t *img,
+	uint8_t *alpha
+) {
+	/* TODO: alpha is not supported yet */
+	int cl = check_lock();
+	if (cl < 0) {
+		return cl;
+	} else {
+		int16_t offset_x = (pos_x < 0) ? -pos_x : 0;
+		int16_t count_x  = width - offset_x;
+		int16_t offset_y = (pos_y < 0) ? -pos_y : 0;
+		int16_t count_y  = height - offset_y;
+
+		if (pos_x + width >= 160) {
+			count_x -= (pos_x + width) % 160;
+		}
+		if (pos_y + height >= 80) {
+			count_y -= (pos_y + height) % 80;
+		}
+
+		if (offset_x == 0 && offset_y == 0 && count_x == width &&
+		    count_y == height) {
+			/* Simply copy full image, no cropping or alpha blending */
+			gfx_copy_region(
+				&display_screen,
+				pos_x,
+				pos_y,
+				width,
+				height,
+				GFX_RAW,
+				width * height * 2,
+				img
+			);
+		} else {
+			/* Copy cropped image line by line */
+			for (int16_t curr_y = offset_y;
+			     curr_y < offset_y + count_y;
+			     curr_y++) {
+				uint16_t *curr_img =
+					img + (curr_y * width + offset_x);
+				gfx_copy_region(
+					&display_screen,
+					pos_x + offset_x,
+					pos_y + curr_y,
+					count_x,
+					1,
+					GFX_RAW,
+					count_x * 2,
+					curr_img
+				);
+			}
+		}
+
+		return 0;
+	}
+}
+
 int epic_disp_line(
 	int16_t xstart,
 	int16_t ystart,
diff --git a/pycardium/modules/py/display.py b/pycardium/modules/py/display.py
index a6ef82778a9b7638d900f89818435ffcffcd1b8e..1c94d47b35844db5272447265c94389c2eac918b 100644
--- a/pycardium/modules/py/display.py
+++ b/pycardium/modules/py/display.py
@@ -120,6 +120,83 @@ class Display:
         sys_display.pixel(x, y, col)
         return self
 
+    def blit(self, x, y, w, h, img, rgb565=False, alpha=None):
+        """
+        Draws an image on the display.
+
+        :param x: X coordinate
+        :param y: Y coordinate
+        :param w: Image width
+        :param h: Image height
+        :param img: Buffer with pixel data. Default format is RGB with 8 bits per channel.
+        :param alpha: Alpha mask for `img`
+        :param rgb565: Set to `True` if the data supplied is in rgb565 format instead of 8 bit RGB.
+
+        .. note::
+           Alpha mask support is not yet implemented.
+
+        .. versionadded:: 1.17
+
+        **Example with RGB data:**
+
+        .. code-block:: python
+
+           import display
+           import color
+
+           # Draw a blue 32x20 pixel rectangle:
+           # Each pixel is 3 bytes big. Order is red, green, blue
+           img = bytes(color.BLUE) * 32 * 20
+           with display.open() as d:
+               d.clear()
+               d.blit(10, 10, 32, 20, img)
+               d.update()
+
+
+        **Example with rgb565 data:**
+
+        .. code-block:: python
+
+           import array
+           import display
+
+           # Draw a green 32x20 pixel rectangle:
+           # 0x07E0 has all bits for green set to 1. Order is RRRR RGGG GGGB BBBB
+           img = array.array('H', [0x07E0 for x in range(32 * 20)])
+           with display.open() as d:
+               d.clear()
+               d.blit(10, 10, 32, 20, img, rgb565=True)
+               d.update()
+
+
+        **Example with a MicroPython FrameBuffer:**
+
+        .. code-block:: python
+
+           import framebuf
+           import display
+
+           # Create a 160x80 pixel frame buffer and write "Hello World" on the display
+           f = framebuf.FrameBuffer(bytearray(160 * 80 * 2), 160, 80, framebuf.RGB565)
+           with display.open() as d:
+               f.text("Hello World", 0, 0, 0xF800) # red
+               d.blit(0, 0, 160, 80, f, rgb565=True)
+               d.update()
+
+
+        """
+
+        # TODO: alpha is not yet supported by epicardium
+        if alpha is not None:
+            raise ValueError("alpha not yet supported")
+
+        if alpha is None:
+            sys_display.blit(x, y, w, h, img, rgb565)
+        else:
+            sys_display.blit(x, y, w, h, img, rgb565, alpha)
+
+        return self
+
     def backlight(self, brightness):
         """
         Set display backlight brightness
diff --git a/pycardium/modules/qstrdefs.h b/pycardium/modules/qstrdefs.h
index 721509d776b4d180418f530c64a2f14de6180f17..58f4f80d59d9b8ab97b9687a0dcf462f701b81a6 100644
--- a/pycardium/modules/qstrdefs.h
+++ b/pycardium/modules/qstrdefs.h
@@ -98,6 +98,7 @@ Q(display)
 Q(print)
 Q(print_adv)
 Q(pixel)
+Q(blit)
 Q(backlight)
 Q(line)
 Q(rect)
diff --git a/pycardium/modules/sys_display.c b/pycardium/modules/sys_display.c
index 40ec6125bfef12132ab9cbe3ce6273b807131d37..eedeb705e12531d3ddf46a153abafbe8adebca0a 100644
--- a/pycardium/modules/sys_display.c
+++ b/pycardium/modules/sys_display.c
@@ -94,6 +94,76 @@ static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(
 	display_pixel_obj, 3, 3, mp_display_pixel
 );
 
+/* blit image to display */
+static mp_obj_t mp_display_blit(size_t n_args, const mp_obj_t *args)
+{
+	/* Required arguments: posx, posy (on display),
+	                       width, height (of image),
+						   buffer (rgb data of image) */
+	int pos_x   = mp_obj_get_int(args[0]);
+	int pos_y   = mp_obj_get_int(args[1]);
+	int width   = mp_obj_get_int(args[2]);
+	int height  = mp_obj_get_int(args[3]);
+	bool rgb565 = mp_obj_is_true(args[5]);
+	mp_buffer_info_t img;
+
+	int res = 0;
+
+	/* Load buffer and ensure it contains enough data */
+	if (!mp_get_buffer(args[4], &img, MP_BUFFER_READ)) {
+		mp_raise_TypeError("'img' does not support buffer protocol.");
+	}
+
+	int bpp = rgb565 ? 2 : 3;
+	if ((int)img.len < width * height * bpp) {
+		mp_raise_ValueError("'img' is too small.");
+	}
+
+	uint16_t *buf = NULL;
+	if (rgb565) {
+		buf = (uint16_t *)img.buf;
+	} else {
+		/* Will raise an exception if out of memory: */
+		buf = m_malloc(width * height * bpp);
+		for (int i = 0; i < width * height; i++) {
+			buf[i] = rgb888_to_rgb565(((uint8_t *)img.buf) + 3 * i);
+		}
+	}
+
+	if (n_args > 6) {
+		mp_buffer_info_t alpha;
+
+		/* Load alpha buffer and check size */
+		if (!mp_get_buffer(args[6], &alpha, MP_BUFFER_READ)) {
+			mp_raise_TypeError(
+				"'alpha' does not support buffer protocol."
+			);
+		}
+		if ((int)alpha.len < width * height) {
+			mp_raise_ValueError("'alpha' is too small.");
+		}
+
+		res = epic_disp_blit(
+			pos_x, pos_y, width, height, buf, (uint8_t *)alpha.buf
+		);
+	} else {
+		res = epic_disp_blit(pos_x, pos_y, width, height, buf, NULL);
+	}
+
+	if (buf) {
+		m_free(buf);
+	}
+
+	if (res < 0) {
+		mp_raise_OSError(-res);
+	}
+
+	return mp_const_none;
+}
+static MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(
+	display_blit_obj, 5, 6, mp_display_blit
+);
+
 /* set display backlight brightness */
 static mp_obj_t mp_display_backlight(size_t n_args, const mp_obj_t *args)
 {
@@ -233,6 +303,7 @@ static const mp_rom_map_elem_t display_module_globals_table[] = {
 	{ MP_ROM_QSTR(MP_QSTR_print), MP_ROM_PTR(&display_print_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_print_adv), MP_ROM_PTR(&display_print_adv_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_pixel), MP_ROM_PTR(&display_pixel_obj) },
+	{ MP_ROM_QSTR(MP_QSTR_blit), MP_ROM_PTR(&display_blit_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_backlight), MP_ROM_PTR(&display_backlight_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_line), MP_ROM_PTR(&display_line_obj) },
 	{ MP_ROM_QSTR(MP_QSTR_rect), MP_ROM_PTR(&display_rect_obj) },