diff --git a/ports/stm32/Makefile b/ports/stm32/Makefile
index 678ece9bd25aa7bf28f98c893e35b8ab25501c3f..ee4de61cc40adc3923948f95fa63de2b480fcb78 100644
--- a/ports/stm32/Makefile
+++ b/ports/stm32/Makefile
@@ -114,6 +114,7 @@ SRC_LIB = $(addprefix lib/,\
 	utils/pyexec.c \
 	utils/interrupt_char.c \
 	utils/sys_stdio_mphal.c \
+	utils/mpirq.c \
 	)
 
 ifeq ($(MICROPY_FLOAT_IMPL),double)
diff --git a/ports/stm32/machine_uart.c b/ports/stm32/machine_uart.c
index 1a21ff8f4b12bfb84258905c460db36764640efc..c45396046757644beb8c6b45e6dc92890effbee3 100644
--- a/ports/stm32/machine_uart.c
+++ b/ports/stm32/machine_uart.c
@@ -33,6 +33,7 @@
 #include "py/mperrno.h"
 #include "py/mphal.h"
 #include "lib/utils/interrupt_char.h"
+#include "lib/utils/mpirq.h"
 #include "uart.h"
 #include "irq.h"
 #include "pendsv.h"
@@ -72,6 +73,77 @@
 ///
 ///     uart.any()               # returns True if any characters waiting
 
+typedef struct _pyb_uart_irq_map_t {
+    uint16_t irq_en;
+    uint16_t flag;
+} pyb_uart_irq_map_t;
+
+STATIC const pyb_uart_irq_map_t mp_irq_map[] = {
+    { USART_CR1_IDLEIE, UART_FLAG_IDLE}, // RX idle
+    { USART_CR1_PEIE,   UART_FLAG_PE},   // parity error
+    { USART_CR1_TXEIE,  UART_FLAG_TXE},  // TX register empty
+    { USART_CR1_TCIE,   UART_FLAG_TC},   // TX complete
+    { USART_CR1_RXNEIE, UART_FLAG_RXNE}, // RX register not empty
+    #if 0
+    // For now only IRQs selected by CR1 are supported
+    #if defined(STM32F4)
+    { USART_CR2_LBDIE,  UART_FLAG_LBD},  // LIN break detection
+    #else
+    { USART_CR2_LBDIE,  UART_FLAG_LBDF}, // LIN break detection
+    #endif
+    { USART_CR3_CTSIE,  UART_FLAG_CTS},  // CTS
+    #endif
+};
+
+// OR-ed IRQ flags which should not be touched by the user
+STATIC const uint32_t mp_irq_reserved = UART_FLAG_RXNE;
+
+// OR-ed IRQ flags which are allowed to be used by the user
+STATIC const uint32_t mp_irq_allowed = UART_FLAG_IDLE;
+
+STATIC mp_obj_t pyb_uart_irq(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args);
+
+STATIC void pyb_uart_irq_config(pyb_uart_obj_t *self, bool enable) {
+    if (self->mp_irq_trigger) {
+        for (size_t entry = 0; entry < MP_ARRAY_SIZE(mp_irq_map); ++entry) {
+            if (mp_irq_map[entry].flag & mp_irq_reserved) {
+                continue;
+            }
+            if (mp_irq_map[entry].flag & self->mp_irq_trigger) {
+                if (enable) {
+                    self->uartx->CR1 |= mp_irq_map[entry].irq_en;
+                } else {
+                    self->uartx->CR1 &= ~mp_irq_map[entry].irq_en;
+                }
+            }
+        }
+    }
+}
+
+STATIC mp_uint_t pyb_uart_irq_trigger(mp_obj_t self_in, mp_uint_t new_trigger) {
+    pyb_uart_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    pyb_uart_irq_config(self, false);
+    self->mp_irq_trigger = new_trigger;
+    pyb_uart_irq_config(self, true);
+    return 0;
+}
+
+STATIC mp_uint_t pyb_uart_irq_info(mp_obj_t self_in, mp_uint_t info_type) {
+    pyb_uart_obj_t *self = MP_OBJ_TO_PTR(self_in);
+    if (info_type == MP_IRQ_INFO_FLAGS) {
+        return self->mp_irq_flags;
+    } else if (info_type == MP_IRQ_INFO_TRIGGERS) {
+        return self->mp_irq_trigger;
+    }
+    return 0;
+}
+
+STATIC const mp_irq_methods_t pyb_uart_irq_methods = {
+    .init = pyb_uart_irq,
+    .trigger = pyb_uart_irq_trigger,
+    .info = pyb_uart_irq_info,
+};
+
 STATIC void pyb_uart_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) {
     pyb_uart_obj_t *self = MP_OBJ_TO_PTR(self_in);
     if (!self->is_enabled) {
@@ -123,9 +195,13 @@ STATIC void pyb_uart_print(const mp_print_t *print, mp_obj_t self_in, mp_print_k
                 mp_print_str(print, "CTS");
             }
         }
-        mp_printf(print, ", timeout=%u, timeout_char=%u, rxbuf=%u)",
+        mp_printf(print, ", timeout=%u, timeout_char=%u, rxbuf=%u",
             self->timeout, self->timeout_char,
             self->read_buf_len == 0 ? 0 : self->read_buf_len - 1); // -1 to adjust for usable length of buffer
+        if (self->mp_irq_trigger != 0) {
+            mp_printf(print, "; irq=0x%x", self->mp_irq_trigger);
+        }
+        mp_print_str(print, ")");
     }
 }
 
@@ -414,6 +490,43 @@ STATIC mp_obj_t pyb_uart_sendbreak(mp_obj_t self_in) {
 }
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(pyb_uart_sendbreak_obj, pyb_uart_sendbreak);
 
+// irq(handler, trigger, hard)
+STATIC mp_obj_t pyb_uart_irq(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) {
+    mp_arg_val_t args[MP_IRQ_ARG_INIT_NUM_ARGS];
+    mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_IRQ_ARG_INIT_NUM_ARGS, mp_irq_init_args, args);
+    pyb_uart_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]);
+
+    if (self->mp_irq_obj == NULL) {
+        self->mp_irq_trigger = 0;
+        self->mp_irq_obj = mp_irq_new(&pyb_uart_irq_methods, MP_OBJ_FROM_PTR(self));
+    }
+
+    if (n_args > 1 || kw_args->used != 0) {
+        // Check the handler
+        mp_obj_t handler = args[MP_IRQ_ARG_INIT_handler].u_obj;
+        if (handler != mp_const_none && !mp_obj_is_callable(handler)) {
+            mp_raise_ValueError("handler must be None or callable");
+        }
+
+        // Check the trigger
+        mp_uint_t trigger = args[MP_IRQ_ARG_INIT_trigger].u_int;
+        mp_uint_t not_supported = trigger & ~mp_irq_allowed;
+        if (trigger != 0 && not_supported) {
+            nlr_raise(mp_obj_new_exception_msg_varg(&mp_type_ValueError, "trigger 0x%08x unsupported", not_supported));
+        }
+
+        // Reconfigure user IRQs
+        pyb_uart_irq_config(self, false);
+        self->mp_irq_obj->handler = handler;
+        self->mp_irq_obj->ishard = args[MP_IRQ_ARG_INIT_hard].u_bool;
+        self->mp_irq_trigger = trigger;
+        pyb_uart_irq_config(self, true);
+    }
+
+    return MP_OBJ_FROM_PTR(self->mp_irq_obj);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_KW(pyb_uart_irq_obj, 1, pyb_uart_irq);
+
 STATIC const mp_rom_map_elem_t pyb_uart_locals_dict_table[] = {
     // instance methods
 
@@ -429,6 +542,7 @@ STATIC const mp_rom_map_elem_t pyb_uart_locals_dict_table[] = {
     { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&mp_stream_readinto_obj) },
     /// \method write(buf)
     { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mp_stream_write_obj) },
+    { MP_ROM_QSTR(MP_QSTR_irq), MP_ROM_PTR(&pyb_uart_irq_obj) },
 
     { MP_ROM_QSTR(MP_QSTR_writechar), MP_ROM_PTR(&pyb_uart_writechar_obj) },
     { MP_ROM_QSTR(MP_QSTR_readchar), MP_ROM_PTR(&pyb_uart_readchar_obj) },
@@ -437,6 +551,9 @@ STATIC const mp_rom_map_elem_t pyb_uart_locals_dict_table[] = {
     // class constants
     { MP_ROM_QSTR(MP_QSTR_RTS), MP_ROM_INT(UART_HWCONTROL_RTS) },
     { MP_ROM_QSTR(MP_QSTR_CTS), MP_ROM_INT(UART_HWCONTROL_CTS) },
+
+    // IRQ flags
+    { MP_ROM_QSTR(MP_QSTR_IRQ_RXIDLE), MP_ROM_INT(UART_FLAG_IDLE) },
 };
 
 STATIC MP_DEFINE_CONST_DICT(pyb_uart_locals_dict, pyb_uart_locals_dict_table);
diff --git a/ports/stm32/uart.c b/ports/stm32/uart.c
index ff1e860041d2514cf74dfe548afaab8637d0ea40..74e601f3b8c01409e38677cb4bc6c76211b27b6e 100644
--- a/ports/stm32/uart.c
+++ b/ports/stm32/uart.c
@@ -33,6 +33,7 @@
 #include "py/mperrno.h"
 #include "py/mphal.h"
 #include "lib/utils/interrupt_char.h"
+#include "lib/utils/mpirq.h"
 #include "uart.h"
 #include "irq.h"
 #include "pendsv.h"
@@ -327,6 +328,9 @@ bool uart_init(pyb_uart_obj_t *uart_obj,
         uart_obj->char_width = CHAR_WIDTH_8BIT;
     }
 
+    uart_obj->mp_irq_trigger = 0;
+    uart_obj->mp_irq_obj = NULL;
+
     return true;
 }
 
@@ -697,4 +701,24 @@ void uart_irq_handler(mp_uint_t uart_id) {
             }
         }
     }
+
+    // Set user IRQ flags
+    self->mp_irq_flags = 0;
+    #if defined(STM32F4)
+    if (self->uartx->SR & USART_SR_IDLE) {
+        (void)self->uartx->SR;
+        (void)self->uartx->DR;
+        self->mp_irq_flags |= UART_FLAG_IDLE;
+    }
+    #else
+    if (self->uartx->ISR & USART_ISR_IDLE) {
+        self->uartx->ICR = USART_ICR_IDLECF;
+        self->mp_irq_flags |= UART_FLAG_IDLE;
+    }
+    #endif
+
+    // Check the flags to see if the user handler should be called
+    if (self->mp_irq_trigger & self->mp_irq_flags) {
+        mp_irq_handler(self->mp_irq_obj);
+    }
 }
diff --git a/ports/stm32/uart.h b/ports/stm32/uart.h
index 285277515a6ec48a5e82919f3bde529292a547cb..e21b4dd9c186510cde36d22106b0ec59c5482671 100644
--- a/ports/stm32/uart.h
+++ b/ports/stm32/uart.h
@@ -26,6 +26,8 @@
 #ifndef MICROPY_INCLUDED_STM32_UART_H
 #define MICROPY_INCLUDED_STM32_UART_H
 
+struct _mp_irq_obj_t;
+
 typedef enum {
     PYB_UART_NONE = 0,
     PYB_UART_1 = 1,
@@ -57,6 +59,9 @@ typedef struct _pyb_uart_obj_t {
     volatile uint16_t read_buf_head;    // indexes first empty slot
     uint16_t read_buf_tail;             // indexes first full slot (not full if equals head)
     byte *read_buf;                     // byte or uint16_t, depending on char size
+    uint16_t mp_irq_trigger;            // user IRQ trigger mask
+    uint16_t mp_irq_flags;              // user IRQ active IRQ flags
+    struct _mp_irq_obj_t *mp_irq_obj;   // user IRQ object
 } pyb_uart_obj_t;
 
 extern const mp_obj_type_t pyb_uart_type;