From fff5c2520edfc7b5f929ef872d9479b51e1c57e4 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sat, 10 Oct 2026 16:43:45 -0400 Subject: [PATCH 1/2] zephyr-cp: don't drop UART/USB CDC RX bytes when the queue is full serial_cb() drained the driver unconditionally and discarded bytes once the 128-byte msgq was full, so REPL lines of 128 bytes or more lost their tail and newline and the REPL waited forever. Pause RX with uart_irq_rx_disable() instead, leaving the bytes in the driver so USB CDC applies flow control, and resume it when read() or clear_rx_buffer() makes room. Co-Authored-By: Claude Opus 5.5 --- ports/zephyr-cp/common-hal/busio/UART.c | 46 +++++++++++++++++++------ 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/ports/zephyr-cp/common-hal/busio/UART.c b/ports/zephyr-cp/common-hal/busio/UART.c index 7de3106a27c..04437a55925 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.c +++ b/ports/zephyr-cp/common-hal/busio/UART.c @@ -27,6 +27,14 @@ #include LOG_MODULE_REGISTER(busio_uart); +// Turn RX back on after serial_cb() paused it for a full msgq. +static void resume_rx(busio_uart_obj_t *self) { + if (self->rx_paused) { + self->rx_paused = false; + uart_irq_rx_enable(self->uart_device); + } +} + /* * Read characters from UART until line end is detected. Afterwards push the * data to the message queue. @@ -42,20 +50,35 @@ static void serial_cb(const struct device *dev, void *user_data) { return; } - /* read until FIFO empty */ - while (uart_fifo_read(dev, &c, 1) == 1) { + /* read until FIFO empty or msgq full */ + while (true) { + if (k_msgq_num_free_get(&self->msgq) == 0) { + // Leave the remaining bytes in the driver instead of dropping + // them. USB CDC then stops accepting packets, so the host + // waits. read() calls resume_rx() after taking bytes out of + // the msgq. + self->rx_paused = true; + uart_irq_rx_disable(dev); + // read() may have freed msgq space just before rx_paused was + // set. Then read() saw rx_paused false and did not call + // resume_rx(), so check for space again. + if (k_msgq_num_free_get(&self->msgq) == 0) { + break; + } + resume_rx(self); + } + if (uart_fifo_read(dev, &c, 1) != 1) { + break; + } if (mp_interrupt_char == c) { common_hal_busio_uart_clear_rx_buffer(self); mp_sched_keyboard_interrupt(); port_wake_main_task_from_isr(); - } else if (!self->rx_paused) { - if (k_msgq_put(&self->msgq, &c, K_NO_WAIT) == 0) { - // Wake the main task so it can service the new RX data - // instead of sleeping out its full timeout. - port_wake_main_task_from_isr(); - } else { - self->rx_paused = true; - } + } else { + k_msgq_put(&self->msgq, &c, K_NO_WAIT); + // Wake the main task so it can service the new RX data + // instead of sleeping out its full timeout. + port_wake_main_task_from_isr(); } } } @@ -203,7 +226,7 @@ size_t common_hal_busio_uart_read(busio_uart_obj_t *self, uint8_t *data, size_t count++; } if (count > 0) { - self->rx_paused = false; + resume_rx(self); } return count; @@ -253,6 +276,7 @@ uint32_t common_hal_busio_uart_rx_characters_available(busio_uart_obj_t *self) { void common_hal_busio_uart_clear_rx_buffer(busio_uart_obj_t *self) { k_msgq_purge(&self->msgq); + resume_rx(self); } bool common_hal_busio_uart_ready_to_tx(busio_uart_obj_t *self) { From 7c9cc46a93d90fe8cb291710e9c27f82af400bf8 Mon Sep 17 00:00:00 2001 From: Dan Halbert Date: Sun, 11 Oct 2026 17:44:26 -0400 Subject: [PATCH 2/2] zephyr-cp: pause RX only on USB CDC, and enlarge its queue to 1 KB Pausing RX hid a Ctrl-C behind unread input. On UART the sender can't wait, so pausing only lost the Ctrl-C; drop bytes there as before. On USB CDC, a 1024-byte queue (as on TinyUSB nRF) holds 1 KB of unread input before a Ctrl-C can be delayed. Add test_ctrl_c_after_unread_input (native_sim, UART console). Co-authored-by: Mikey Sklar Co-Authored-By: Claude Opus 5.5 --- ports/zephyr-cp/common-hal/busio/UART.c | 8 ++++---- ports/zephyr-cp/common-hal/busio/UART.h | 3 +++ ports/zephyr-cp/common-hal/usb_cdc/Serial.c | 1 + ports/zephyr-cp/supervisor/usb.c | 8 ++++---- ports/zephyr-cp/tests/test_basics.py | 12 ++++++++++++ 5 files changed, 24 insertions(+), 8 deletions(-) diff --git a/ports/zephyr-cp/common-hal/busio/UART.c b/ports/zephyr-cp/common-hal/busio/UART.c index 04437a55925..97ab44bb8c1 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.c +++ b/ports/zephyr-cp/common-hal/busio/UART.c @@ -50,9 +50,9 @@ static void serial_cb(const struct device *dev, void *user_data) { return; } - /* read until FIFO empty or msgq full */ + /* read until FIFO empty, or until msgq full if flow_control */ while (true) { - if (k_msgq_num_free_get(&self->msgq) == 0) { + if (self->flow_control && k_msgq_num_free_get(&self->msgq) == 0) { // Leave the remaining bytes in the driver instead of dropping // them. USB CDC then stops accepting packets, so the host // waits. read() calls resume_rx() after taking bytes out of @@ -74,8 +74,7 @@ static void serial_cb(const struct device *dev, void *user_data) { common_hal_busio_uart_clear_rx_buffer(self); mp_sched_keyboard_interrupt(); port_wake_main_task_from_isr(); - } else { - k_msgq_put(&self->msgq, &c, K_NO_WAIT); + } else if (k_msgq_put(&self->msgq, &c, K_NO_WAIT) == 0) { // Wake the main task so it can service the new RX data // instead of sleeping out its full timeout. port_wake_main_task_from_isr(); @@ -104,6 +103,7 @@ mp_obj_t common_hal_busio_uart_construct_from_device(busio_uart_obj_t *self, con self->timeout = K_FOREVER; self->write_timeout = K_FOREVER; self->rx_paused = false; + self->flow_control = false; uart_irq_rx_enable(uart_device); return MP_OBJ_FROM_PTR(self); diff --git a/ports/zephyr-cp/common-hal/busio/UART.h b/ports/zephyr-cp/common-hal/busio/UART.h index 3e8614a8dd2..d98a0d2c929 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.h +++ b/ports/zephyr-cp/common-hal/busio/UART.h @@ -27,6 +27,9 @@ typedef struct { const mcu_pin_obj_t *cts; bool rx_paused; // set by irq if no space in rbuf + // Set when the sender waits for unread data (USB CDC), so RX can + // pause on a full msgq instead of dropping bytes. + bool flow_control; // True when the underlying Zephyr device was dynamically routed to the // pins above at construction time. Such objects own their receiver diff --git a/ports/zephyr-cp/common-hal/usb_cdc/Serial.c b/ports/zephyr-cp/common-hal/usb_cdc/Serial.c index 715955d6e0f..6384dff22b1 100644 --- a/ports/zephyr-cp/common-hal/usb_cdc/Serial.c +++ b/ports/zephyr-cp/common-hal/usb_cdc/Serial.c @@ -12,6 +12,7 @@ mp_obj_t common_hal_usb_cdc_serial_construct_from_device(usb_cdc_serial_obj_t *self, const struct device *uart_device, uint16_t receiver_buffer_size, byte *receiver_buffer) { common_hal_busio_uart_construct_from_device(self, uart_device, receiver_buffer_size, receiver_buffer); + self->flow_control = true; self->base.type = &usb_cdc_serial_type; return MP_OBJ_FROM_PTR(self); } diff --git a/ports/zephyr-cp/supervisor/usb.c b/ports/zephyr-cp/supervisor/usb.c index da22cbb0d35..f269a38ea59 100644 --- a/ports/zephyr-cp/supervisor/usb.c +++ b/ports/zephyr-cp/supervisor/usb.c @@ -270,9 +270,9 @@ void usb_init(void) { bool console = usb_cdc_console_enabled(); if (console) { - uint8_t *receiver_buffer = port_malloc(128, true); + uint8_t *receiver_buffer = port_malloc(1024, true); if (receiver_buffer != NULL) { - common_hal_usb_cdc_serial_construct_from_device(&usb_cdc_console_obj, DEVICE_DT_GET(DT_NODELABEL(cdc_acm_console)), 128, receiver_buffer); + common_hal_usb_cdc_serial_construct_from_device(&usb_cdc_console_obj, DEVICE_DT_GET(DT_NODELABEL(cdc_acm_console)), 1024, receiver_buffer); } else { console = false; } @@ -281,9 +281,9 @@ void usb_init(void) { bool data = usb_cdc_data_enabled(); if (data) { - uint8_t *receiver_buffer = port_malloc(128, true); + uint8_t *receiver_buffer = port_malloc(1024, true); if (receiver_buffer != NULL) { - common_hal_usb_cdc_serial_construct_from_device(&usb_cdc_data_obj, DEVICE_DT_GET(DT_NODELABEL(cdc_acm_data)), 128, receiver_buffer); + common_hal_usb_cdc_serial_construct_from_device(&usb_cdc_data_obj, DEVICE_DT_GET(DT_NODELABEL(cdc_acm_data)), 1024, receiver_buffer); } else { data = false; } diff --git a/ports/zephyr-cp/tests/test_basics.py b/ports/zephyr-cp/tests/test_basics.py index fc4dc18e58c..f86668d8368 100644 --- a/ports/zephyr-cp/tests/test_basics.py +++ b/ports/zephyr-cp/tests/test_basics.py @@ -124,6 +124,18 @@ def test_ctrl_c_interrupts_long_sleep(circuitpython): assert elapsed < 5, f"Ctrl+C took {elapsed:.1f}s to interrupt time.sleep(30)" +@pytest.mark.circuitpy_drive({"code.py": 'print("spinning")\nwhile True:\n pass\n'}) +@pytest.mark.duration(10) +def test_ctrl_c_after_unread_input(circuitpython): + """Ctrl+C must still interrupt when the console queue is full of unread input.""" + circuitpython.serial.wait_for("spinning") + circuitpython.serial.write("x" * 200) + time.sleep(0.5) + circuitpython.serial.write("\x03") + circuitpython.wait_until_done() + assert "KeyboardInterrupt" in circuitpython.serial.all_output + + RELOAD_CODE = """\ print("first run") import time