diff --git a/lib/utils/pyexec.c b/lib/utils/pyexec.c
index 2b86af3bba..86321ac129 100644
--- a/lib/utils/pyexec.c
+++ b/lib/utils/pyexec.c
@@ -56,6 +56,7 @@ STATIC bool repl_display_debugging_info = 0;
 #define EXEC_FLAG_SOURCE_IS_RAW_CODE (8)
 #define EXEC_FLAG_SOURCE_IS_VSTR (16)
 #define EXEC_FLAG_SOURCE_IS_FILENAME (32)
+#define EXEC_FLAG_SOURCE_IS_READER (64)
 
 // parses, compiles and executes the code in the lexer
 // frees the lexer before returning
@@ -91,6 +92,8 @@ STATIC int parse_compile_execute(const void *source, mp_parse_input_kind_t input
             if (exec_flags & EXEC_FLAG_SOURCE_IS_VSTR) {
                 const vstr_t *vstr = source;
                 lex = mp_lexer_new_from_str_len(MP_QSTR__lt_stdin_gt_, vstr->buf, vstr->len, 0);
+            } else if (exec_flags & EXEC_FLAG_SOURCE_IS_READER) {
+                lex = mp_lexer_new(MP_QSTR__lt_stdin_gt_, *(mp_reader_t *)source);
             } else if (exec_flags & EXEC_FLAG_SOURCE_IS_FILENAME) {
                 lex = mp_lexer_new_from_file(source);
             } else {
@@ -122,6 +125,12 @@ STATIC int parse_compile_execute(const void *source, mp_parse_input_kind_t input
         // uncaught exception
         mp_hal_set_interrupt_char(-1); // disable interrupt
         mp_handle_pending(false); // clear any pending exceptions (and run any callbacks)
+
+        if (exec_flags & EXEC_FLAG_SOURCE_IS_READER) {
+            const mp_reader_t *reader = source;
+            reader->close(reader->data);
+        }
+
         // print EOF after normal output
         if (exec_flags & EXEC_FLAG_PRINT_EOF) {
             mp_hal_stdout_tx_strn("\x04", 1);
@@ -170,6 +179,99 @@ STATIC int parse_compile_execute(const void *source, mp_parse_input_kind_t input
 }
 
 #if MICROPY_ENABLE_COMPILER
+
+// This can be configured by a port (and even configured to a function to be
+// computed dynamically) to indicate the maximum number of bytes that can be
+// held in the stdin buffer.
+#ifndef MICROPY_REPL_STDIN_BUFFER_MAX
+#define MICROPY_REPL_STDIN_BUFFER_MAX (256)
+#endif
+
+typedef struct _mp_reader_stdin_t {
+    bool eof;
+    uint16_t window_max;
+    uint16_t window_remain;
+} mp_reader_stdin_t;
+
+STATIC mp_uint_t mp_reader_stdin_readbyte(void *data) {
+    mp_reader_stdin_t *reader = (mp_reader_stdin_t *)data;
+
+    if (reader->eof) {
+        return MP_READER_EOF;
+    }
+
+    int c = mp_hal_stdin_rx_chr();
+
+    if (c == CHAR_CTRL_C || c == CHAR_CTRL_D) {
+        reader->eof = true;
+        mp_hal_stdout_tx_strn("\x04", 1); // indicate end to host
+        if (c == CHAR_CTRL_C) {
+            #if MICROPY_KBD_EXCEPTION
+            MP_STATE_VM(mp_kbd_exception).traceback_data = NULL;
+            nlr_raise(MP_OBJ_FROM_PTR(&MP_STATE_VM(mp_kbd_exception)));
+            #else
+            mp_raise_type(&mp_type_KeyboardInterrupt);
+            #endif
+        } else {
+            return MP_READER_EOF;
+        }
+    }
+
+    if (--reader->window_remain == 0) {
+        mp_hal_stdout_tx_strn("\x01", 1); // indicate window available to host
+        reader->window_remain = reader->window_max;
+    }
+
+    return c;
+}
+
+STATIC void mp_reader_stdin_close(void *data) {
+    mp_reader_stdin_t *reader = (mp_reader_stdin_t *)data;
+    if (!reader->eof) {
+        reader->eof = true;
+        mp_hal_stdout_tx_strn("\x04", 1); // indicate end to host
+        for (;;) {
+            int c = mp_hal_stdin_rx_chr();
+            if (c == CHAR_CTRL_C || c == CHAR_CTRL_D) {
+                break;
+            }
+        }
+    }
+}
+
+STATIC void mp_reader_new_stdin(mp_reader_t *reader, mp_reader_stdin_t *reader_stdin, uint16_t buf_max) {
+    // Make flow-control window half the buffer size, and indicate to the host that 2x windows are
+    // free (sending the window size implicitly indicates that a window is free, and then the 0x01
+    // indicates that another window is free).
+    size_t window = buf_max / 2;
+    char reply[3] = { window & 0xff, window >> 8, 0x01 };
+    mp_hal_stdout_tx_strn(reply, sizeof(reply));
+
+    reader_stdin->eof = false;
+    reader_stdin->window_max = window;
+    reader_stdin->window_remain = window;
+    reader->data = reader_stdin;
+    reader->readbyte = mp_reader_stdin_readbyte;
+    reader->close = mp_reader_stdin_close;
+}
+
+STATIC int do_reader_stdin(int c) {
+    if (c != 'A') {
+        // Unsupported command.
+        mp_hal_stdout_tx_strn("R\x00", 2);
+        return 0;
+    }
+
+    // Indicate reception of command.
+    mp_hal_stdout_tx_strn("R\x01", 2);
+
+    mp_reader_t reader;
+    mp_reader_stdin_t reader_stdin;
+    mp_reader_new_stdin(&reader, &reader_stdin, MICROPY_REPL_STDIN_BUFFER_MAX);
+    int exec_flags = EXEC_FLAG_PRINT_EOF | EXEC_FLAG_SOURCE_IS_READER;
+    return parse_compile_execute(&reader, MP_PARSE_FILE_INPUT, exec_flags);
+}
+
 #if MICROPY_REPL_EVENT_DRIVEN
 
 typedef struct _repl_t {
@@ -203,6 +305,13 @@ void pyexec_event_repl_init(void) {
 STATIC int pyexec_raw_repl_process_char(int c) {
     if (c == CHAR_CTRL_A) {
         // reset raw REPL
+        if (vstr_len(MP_STATE_VM(repl_line)) == 2 && vstr_str(MP_STATE_VM(repl_line))[0] == CHAR_CTRL_E) {
+            int ret = do_reader_stdin(vstr_str(MP_STATE_VM(repl_line))[1]);
+            if (ret & PYEXEC_FORCED_EXIT) {
+                return ret;
+            }
+            goto reset;
+        }
         mp_hal_stdout_tx_str("raw REPL; CTRL-B to exit\r\n");
         goto reset;
     } else if (c == CHAR_CTRL_B) {
@@ -388,6 +497,15 @@ raw_repl_reset:
             int c = mp_hal_stdin_rx_chr();
             if (c == CHAR_CTRL_A) {
                 // reset raw REPL
+                if (vstr_len(&line) == 2 && vstr_str(&line)[0] == CHAR_CTRL_E) {
+                    int ret = do_reader_stdin(vstr_str(&line)[1]);
+                    if (ret & PYEXEC_FORCED_EXIT) {
+                        return ret;
+                    }
+                    vstr_reset(&line);
+                    mp_hal_stdout_tx_str(">");
+                    continue;
+                }
                 goto raw_repl_reset;
             } else if (c == CHAR_CTRL_B) {
                 // change to friendly REPL