diff --git a/ports/stm32/modpyb.c b/ports/stm32/modpyb.c
index 176fc846637b17f9a03a1a9f097d408ec3fe4289..81cbdcc19135e077b78029909ec0180946daae7c 100644
--- a/ports/stm32/modpyb.c
+++ b/ports/stm32/modpyb.c
@@ -27,7 +27,7 @@
 #include <stdint.h>
 #include <stdio.h>
 
-#include "py/obj.h"
+#include "py/runtime.h"
 #include "py/gc.h"
 #include "py/builtin.h"
 #include "py/mphal.h"
@@ -104,6 +104,28 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_1(pyb_elapsed_micros_obj, pyb_elapsed_micros);
 
 MP_DECLARE_CONST_FUN_OBJ_KW(pyb_main_obj); // defined in main.c
 
+// Get or set the UART object that the REPL is repeated on.
+// This is a legacy function, use of uos.dupterm is preferred.
+STATIC mp_obj_t pyb_repl_uart(size_t n_args, const mp_obj_t *args) {
+    if (n_args == 0) {
+        if (MP_STATE_PORT(pyb_stdio_uart) == NULL) {
+            return mp_const_none;
+        } else {
+            return MP_STATE_PORT(pyb_stdio_uart);
+        }
+    } else {
+        if (args[0] == mp_const_none) {
+            MP_STATE_PORT(pyb_stdio_uart) = NULL;
+        } else if (mp_obj_get_type(args[0]) == &pyb_uart_type) {
+            MP_STATE_PORT(pyb_stdio_uart) = args[0];
+        } else {
+            mp_raise_ValueError("need a UART object");
+        }
+        return mp_const_none;
+    }
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(pyb_repl_uart_obj, 0, 1, pyb_repl_uart);
+
 STATIC const mp_rom_map_elem_t pyb_module_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_pyb) },
 
@@ -126,7 +148,7 @@ STATIC const mp_rom_map_elem_t pyb_module_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR_stop), MP_ROM_PTR(&machine_sleep_obj) },
     { MP_ROM_QSTR(MP_QSTR_standby), MP_ROM_PTR(&machine_deepsleep_obj) },
     { MP_ROM_QSTR(MP_QSTR_main), MP_ROM_PTR(&pyb_main_obj) },
-    { MP_ROM_QSTR(MP_QSTR_repl_uart), MP_ROM_PTR(&mod_os_dupterm_obj) },
+    { MP_ROM_QSTR(MP_QSTR_repl_uart), MP_ROM_PTR(&pyb_repl_uart_obj) },
 
     { MP_ROM_QSTR(MP_QSTR_usb_mode), MP_ROM_PTR(&pyb_usb_mode_obj) },
     { MP_ROM_QSTR(MP_QSTR_hid_mouse), MP_ROM_PTR(&pyb_usb_hid_mouse_obj) },
diff --git a/ports/stm32/moduos.c b/ports/stm32/moduos.c
index f661b3b5e08fca40faaf112d1319404133f5d510..f6e1483d35516b2e7014a9b579867faee8fda4e1 100644
--- a/ports/stm32/moduos.c
+++ b/ports/stm32/moduos.c
@@ -33,6 +33,7 @@
 #include "lib/timeutils/timeutils.h"
 #include "lib/oofatfs/ff.h"
 #include "lib/oofatfs/diskio.h"
+#include "extmod/misc.h"
 #include "extmod/vfs.h"
 #include "extmod/vfs_fat.h"
 #include "genhdr/mpversion.h"
@@ -105,28 +106,6 @@ STATIC mp_obj_t os_urandom(mp_obj_t num) {
 STATIC MP_DEFINE_CONST_FUN_OBJ_1(os_urandom_obj, os_urandom);
 #endif
 
-// Get or set the UART object that the REPL is repeated on.
-// TODO should accept any object with read/write methods.
-STATIC mp_obj_t os_dupterm(size_t n_args, const mp_obj_t *args) {
-    if (n_args == 0) {
-        if (MP_STATE_PORT(pyb_stdio_uart) == NULL) {
-            return mp_const_none;
-        } else {
-            return MP_STATE_PORT(pyb_stdio_uart);
-        }
-    } else {
-        if (args[0] == mp_const_none) {
-            MP_STATE_PORT(pyb_stdio_uart) = NULL;
-        } else if (mp_obj_get_type(args[0]) == &pyb_uart_type) {
-            MP_STATE_PORT(pyb_stdio_uart) = args[0];
-        } else {
-            mp_raise_ValueError("need a UART object");
-        }
-        return mp_const_none;
-    }
-}
-MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(mod_os_dupterm_obj, 0, 1, os_dupterm);
-
 STATIC const mp_rom_map_elem_t os_module_globals_table[] = {
     { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_uos) },
 
@@ -154,7 +133,7 @@ STATIC const mp_rom_map_elem_t os_module_globals_table[] = {
 #endif
 
     // these are MicroPython extensions
-    { MP_ROM_QSTR(MP_QSTR_dupterm), MP_ROM_PTR(&mod_os_dupterm_obj) },
+    { MP_ROM_QSTR(MP_QSTR_dupterm), MP_ROM_PTR(&mp_uos_dupterm_obj) },
     { MP_ROM_QSTR(MP_QSTR_mount), MP_ROM_PTR(&mp_vfs_mount_obj) },
     { MP_ROM_QSTR(MP_QSTR_umount), MP_ROM_PTR(&mp_vfs_umount_obj) },
     { MP_ROM_QSTR(MP_QSTR_VfsFat), MP_ROM_PTR(&mp_fat_vfs_type) },
diff --git a/ports/stm32/mpconfigport.h b/ports/stm32/mpconfigport.h
index 2d59a1df87fbd84e98d1902c21884f438a69bf21..51d44256161abe0c85562354f39e6ca9da35cc97 100644
--- a/ports/stm32/mpconfigport.h
+++ b/ports/stm32/mpconfigport.h
@@ -126,6 +126,7 @@
 #define MICROPY_PY_USELECT          (1)
 #define MICROPY_PY_UTIMEQ           (1)
 #define MICROPY_PY_UTIME_MP_HAL     (1)
+#define MICROPY_PY_OS_DUPTERM       (1)
 #define MICROPY_PY_MACHINE          (1)
 #define MICROPY_PY_MACHINE_PULSE    (1)
 #define MICROPY_PY_MACHINE_PIN_MAKE_NEW mp_pin_make_new
diff --git a/ports/stm32/mphalport.c b/ports/stm32/mphalport.c
index 3bea6e2d91e2fe5af8e111dac3c6e26a5049419f..e9c4f28f79d0284b1e9c23a7d83d3ee32f3fb978 100644
--- a/ports/stm32/mphalport.c
+++ b/ports/stm32/mphalport.c
@@ -3,6 +3,7 @@
 #include "py/runtime.h"
 #include "py/mperrno.h"
 #include "py/mphal.h"
+#include "extmod/misc.h"
 #include "usb.h"
 #include "uart.h"
 
@@ -38,6 +39,10 @@ int mp_hal_stdin_rx_chr(void) {
         } else if (MP_STATE_PORT(pyb_stdio_uart) != NULL && uart_rx_any(MP_STATE_PORT(pyb_stdio_uart))) {
             return uart_rx_char(MP_STATE_PORT(pyb_stdio_uart));
         }
+        int dupterm_c = mp_uos_dupterm_rx_chr();
+        if (dupterm_c >= 0) {
+            return dupterm_c;
+        }
         MICROPY_EVENT_POLL_HOOK
     }
 }
@@ -56,6 +61,7 @@ void mp_hal_stdout_tx_strn(const char *str, size_t len) {
     if (usb_vcp_is_enabled()) {
         usb_vcp_send_strn(str, len);
     }
+    mp_uos_dupterm_tx_strn(str, len);
 }
 
 // Efficiently convert "\n" to "\r\n"