From 5f49abac1846aae24b63e483efbc804c5108beb2 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 11 Oct 2026 19:44:10 -0700 Subject: [PATCH 1/4] busio.UART: let Ctrl-C stop a write blocked by flow control On raspberrypi and espressif, write() loops until every byte is in the TX FIFO. When CTS is held deasserted the FIFO never drains, so the loop never ends and Ctrl-C is ignored. Check mp_hal_is_interrupted() in the wait loops, the same way read() already does, and return the number of bytes written so far. Fixes #10621. Co-Authored-By: Claude Opus 5.5 (1M context) --- ports/espressif/common-hal/busio/UART.c | 8 ++++++-- ports/raspberrypi/common-hal/busio/UART.c | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ports/espressif/common-hal/busio/UART.c b/ports/espressif/common-hal/busio/UART.c index 01c8321843a..2046bc93f16 100644 --- a/ports/espressif/common-hal/busio/UART.c +++ b/ports/espressif/common-hal/busio/UART.c @@ -374,12 +374,16 @@ size_t common_hal_busio_uart_write(busio_uart_obj_t *self, const uint8_t *data, left_to_write -= count; data += count; RUN_BACKGROUND_TASKS; + // Allow user to break out of a write blocked by flow control. + if (mp_hal_is_interrupted()) { + break; + } } - while (uart_wait_tx_done(self->uart_num, 0) == ESP_ERR_TIMEOUT) { + while (uart_wait_tx_done(self->uart_num, 0) == ESP_ERR_TIMEOUT && !mp_hal_is_interrupted()) { RUN_BACKGROUND_TASKS; } - return len; + return len - left_to_write; } uint32_t common_hal_busio_uart_get_baudrate(busio_uart_obj_t *self) { diff --git a/ports/raspberrypi/common-hal/busio/UART.c b/ports/raspberrypi/common-hal/busio/UART.c index 074c78c2f4a..b3b89370607 100644 --- a/ports/raspberrypi/common-hal/busio/UART.c +++ b/ports/raspberrypi/common-hal/busio/UART.c @@ -231,12 +231,16 @@ size_t common_hal_busio_uart_write(busio_uart_obj_t *self, const uint8_t *data, left_to_write--; } RUN_BACKGROUND_TASKS; + // Allow user to break out of a write blocked by flow control. + if (mp_hal_is_interrupted()) { + break; + } } if (self->rs485_dir_pin != NO_PIN) { uart_tx_wait_blocking(self->uart); gpio_put(self->rs485_dir_pin, self->rs485_invert); } - return len; + return len - left_to_write; } // Read characters. From 74ebad267da939585a9367809b34d26408dde26b Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Sun, 11 Oct 2026 20:29:52 -0700 Subject: [PATCH 2/4] zephyr-cp: let Ctrl-C stop a busio.UART write blocked by flow control write() sent each byte with uart_poll_out(), which waits inside the driver until the byte is out. When CTS is held off it never returns, so Ctrl-C was ignored. Feed the driver from the TX interrupt in serial_cb() instead. write() only waits for that to finish, stops early on Ctrl-C, and returns the bytes handed to the driver. The console and usb_cdc keep using uart_poll_out(). With the host port closed, the CDC ACM driver drops their output instead of blocking, which matches the other ports, and console output is never cut short by a pending Ctrl-C or reload. Related to #10621. Co-Authored-By: Claude Opus 5.5 (1M context) --- ports/zephyr-cp/common-hal/busio/UART.c | 49 ++++++++++++++++++--- ports/zephyr-cp/common-hal/busio/UART.h | 8 ++++ ports/zephyr-cp/common-hal/usb_cdc/Serial.c | 3 +- ports/zephyr-cp/supervisor/serial.c | 4 +- 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/ports/zephyr-cp/common-hal/busio/UART.c b/ports/zephyr-cp/common-hal/busio/UART.c index 1c97555955b..7fed2923ed0 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.c +++ b/ports/zephyr-cp/common-hal/busio/UART.c @@ -36,8 +36,8 @@ static void resume_rx(busio_uart_obj_t *self) { } /* - * Read characters from UART until line end is detected. Afterwards push the - * data to the message queue. + * Feed pending TX bytes to the driver, then move received characters into + * the message queue. */ static void serial_cb(const struct device *dev, void *user_data) { busio_uart_obj_t *self = (busio_uart_obj_t *)user_data; @@ -46,6 +46,18 @@ static void serial_cb(const struct device *dev, void *user_data) { uart_irq_update(dev); + if (self->tx_len > 0 && uart_irq_tx_ready(dev)) { + int sent = uart_fifo_fill(dev, self->tx_data, self->tx_len); + if (sent > 0) { + self->tx_data += sent; + self->tx_len -= sent; + } + if (self->tx_len == 0) { + uart_irq_tx_disable(dev); + k_sem_give(&self->tx_done); + } + } + if (!uart_irq_rx_ready(dev)) { return; } @@ -102,6 +114,8 @@ 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->tx_len = 0; + k_sem_init(&self->tx_done, 0, 1); self->rx_paused = false; self->flow_control = false; self->sigint_enabled = sigint_enabled; @@ -233,13 +247,36 @@ size_t common_hal_busio_uart_read(busio_uart_obj_t *self, uint8_t *data, size_t return count; } -// Write characters. +// Write characters. serial_cb() feeds the driver from the TX interrupt, so +// this thread only waits and can stop on Ctrl-C. size_t common_hal_busio_uart_write(busio_uart_obj_t *self, const uint8_t *data, size_t len, int *errcode) { - for (int i = 0; i < len; i++) { - uart_poll_out(self->uart_device, data[i]); + if (len == 0) { + return 0; } + k_sem_reset(&self->tx_done); + self->tx_data = data; + self->tx_len = len; + uart_irq_tx_enable(self->uart_device); + while (k_sem_take(&self->tx_done, K_MSEC(10)) != 0) { + RUN_BACKGROUND_TASKS; + if (mp_hal_is_interrupted()) { + break; + } + } + uart_irq_tx_disable(self->uart_device); + + unsigned int key = irq_lock(); + size_t not_sent = self->tx_len; + self->tx_len = 0; + irq_unlock(key); + + return len - not_sent; +} - return len; +void common_hal_busio_uart_write_poll(busio_uart_obj_t *self, const uint8_t *data, size_t len) { + for (size_t i = 0; i < len; i++) { + uart_poll_out(self->uart_device, data[i]); + } } uint32_t common_hal_busio_uart_get_baudrate(busio_uart_obj_t *self) { diff --git a/ports/zephyr-cp/common-hal/busio/UART.h b/ports/zephyr-cp/common-hal/busio/UART.h index 3be26d2feec..0eb38689c98 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.h +++ b/ports/zephyr-cp/common-hal/busio/UART.h @@ -20,6 +20,11 @@ typedef struct { k_timeout_t timeout; k_timeout_t write_timeout; + // Bytes not yet handed to the driver by serial_cb(), and a semaphore it + // gives when they are all sent. + const uint8_t *tx_data; + size_t tx_len; + struct k_sem tx_done; byte *receiver_buffer; const mcu_pin_obj_t *tx; const mcu_pin_obj_t *rx; @@ -45,6 +50,9 @@ mp_obj_t common_hal_busio_uart_construct_from_device(busio_uart_obj_t *self, con // Internal helper for clearing buffer void common_hal_busio_uart_clear_rx_buffer(busio_uart_obj_t *self); +// Write all of data with uart_poll_out(), ignoring Ctrl-C. +void common_hal_busio_uart_write_poll(busio_uart_obj_t *self, const uint8_t *data, size_t len); + // Zephyr-port-specific write-timeout accessors, used by usb_cdc/Serial.c. mp_float_t common_hal_busio_uart_get_write_timeout(busio_uart_obj_t *self); void common_hal_busio_uart_set_write_timeout(busio_uart_obj_t *self, mp_float_t write_timeout); diff --git a/ports/zephyr-cp/common-hal/usb_cdc/Serial.c b/ports/zephyr-cp/common-hal/usb_cdc/Serial.c index b93e188b0a0..2096fcd2520 100644 --- a/ports/zephyr-cp/common-hal/usb_cdc/Serial.c +++ b/ports/zephyr-cp/common-hal/usb_cdc/Serial.c @@ -22,7 +22,8 @@ size_t common_hal_usb_cdc_serial_read(usb_cdc_serial_obj_t *self, uint8_t *data, } size_t common_hal_usb_cdc_serial_write(usb_cdc_serial_obj_t *self, const uint8_t *data, size_t len, int *errcode) { - return common_hal_busio_uart_write(self, data, len, errcode); + common_hal_busio_uart_write_poll(self, data, len); + return len; } uint32_t common_hal_usb_cdc_serial_get_in_waiting(usb_cdc_serial_obj_t *self) { diff --git a/ports/zephyr-cp/supervisor/serial.c b/ports/zephyr-cp/supervisor/serial.c index 8e801de9f2e..3d96f7fc99a 100644 --- a/ports/zephyr-cp/supervisor/serial.c +++ b/ports/zephyr-cp/supervisor/serial.c @@ -78,8 +78,8 @@ void port_serial_write_substring(const char *text, uint32_t length) { if (usb_console == NULL) { return; } - common_hal_usb_cdc_serial_write(usb_console, text, length, NULL); + common_hal_busio_uart_write_poll(usb_console, (const uint8_t *)text, length); #else - common_hal_busio_uart_write(&uart_console, text, length, NULL); + common_hal_busio_uart_write_poll(&uart_console, (const uint8_t *)text, length); #endif } From 833500b606c9bd4811ca6cdcaaf88b5444b55691 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 13 Oct 2026 06:10:21 -0700 Subject: [PATCH 3/4] zephyr-cp: rename the console write helper and say why it is used common_hal_busio_uart_write_poll() is Zephyr-only, so drop the common_hal_ prefix. Explain in its comment why the console and usb_cdc use it instead of common_hal_busio_uart_write(). Co-Authored-By: Claude Opus 5.5 (1M context) --- ports/zephyr-cp/common-hal/busio/UART.c | 2 +- ports/zephyr-cp/common-hal/busio/UART.h | 8 ++++++-- ports/zephyr-cp/common-hal/usb_cdc/Serial.c | 2 +- ports/zephyr-cp/supervisor/serial.c | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ports/zephyr-cp/common-hal/busio/UART.c b/ports/zephyr-cp/common-hal/busio/UART.c index 7fed2923ed0..698c2ccf375 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.c +++ b/ports/zephyr-cp/common-hal/busio/UART.c @@ -273,7 +273,7 @@ size_t common_hal_busio_uart_write(busio_uart_obj_t *self, const uint8_t *data, return len - not_sent; } -void common_hal_busio_uart_write_poll(busio_uart_obj_t *self, const uint8_t *data, size_t len) { +void zephyr_uart_write_blocking(busio_uart_obj_t *self, const uint8_t *data, size_t len) { for (size_t i = 0; i < len; i++) { uart_poll_out(self->uart_device, data[i]); } diff --git a/ports/zephyr-cp/common-hal/busio/UART.h b/ports/zephyr-cp/common-hal/busio/UART.h index 0eb38689c98..c9a1e11fec4 100644 --- a/ports/zephyr-cp/common-hal/busio/UART.h +++ b/ports/zephyr-cp/common-hal/busio/UART.h @@ -50,8 +50,12 @@ mp_obj_t common_hal_busio_uart_construct_from_device(busio_uart_obj_t *self, con // Internal helper for clearing buffer void common_hal_busio_uart_clear_rx_buffer(busio_uart_obj_t *self); -// Write all of data with uart_poll_out(), ignoring Ctrl-C. -void common_hal_busio_uart_write_poll(busio_uart_obj_t *self, const uint8_t *data, size_t len); +// Write all of data with uart_poll_out(). Used by the console and usb_cdc +// instead of common_hal_busio_uart_write(), which stops early on a pending +// Ctrl-C or reload and would cut console output short. With the host port +// closed, the CDC ACM driver drops this output instead of blocking, as on +// TinyUSB ports. +void zephyr_uart_write_blocking(busio_uart_obj_t *self, const uint8_t *data, size_t len); // Zephyr-port-specific write-timeout accessors, used by usb_cdc/Serial.c. mp_float_t common_hal_busio_uart_get_write_timeout(busio_uart_obj_t *self); diff --git a/ports/zephyr-cp/common-hal/usb_cdc/Serial.c b/ports/zephyr-cp/common-hal/usb_cdc/Serial.c index 2096fcd2520..61d24462a51 100644 --- a/ports/zephyr-cp/common-hal/usb_cdc/Serial.c +++ b/ports/zephyr-cp/common-hal/usb_cdc/Serial.c @@ -22,7 +22,7 @@ size_t common_hal_usb_cdc_serial_read(usb_cdc_serial_obj_t *self, uint8_t *data, } size_t common_hal_usb_cdc_serial_write(usb_cdc_serial_obj_t *self, const uint8_t *data, size_t len, int *errcode) { - common_hal_busio_uart_write_poll(self, data, len); + zephyr_uart_write_blocking(self, data, len); return len; } diff --git a/ports/zephyr-cp/supervisor/serial.c b/ports/zephyr-cp/supervisor/serial.c index 3d96f7fc99a..69321ed9955 100644 --- a/ports/zephyr-cp/supervisor/serial.c +++ b/ports/zephyr-cp/supervisor/serial.c @@ -78,8 +78,8 @@ void port_serial_write_substring(const char *text, uint32_t length) { if (usb_console == NULL) { return; } - common_hal_busio_uart_write_poll(usb_console, (const uint8_t *)text, length); + zephyr_uart_write_blocking(usb_console, (const uint8_t *)text, length); #else - common_hal_busio_uart_write_poll(&uart_console, (const uint8_t *)text, length); + zephyr_uart_write_blocking(&uart_console, (const uint8_t *)text, length); #endif } From 25f1057121bf09e141185b0e2b7e915ebdbaa5e4 Mon Sep 17 00:00:00 2001 From: Mikey Sklar Date: Tue, 13 Oct 2026 07:00:26 -0700 Subject: [PATCH 4/4] zephyr-cp: keep the USB console on common_hal_usb_cdc_serial_write() It already uses the blocking write, so only the UART console needs the direct call. Co-Authored-By: Claude Opus 5.5 (1M context) --- ports/zephyr-cp/supervisor/serial.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ports/zephyr-cp/supervisor/serial.c b/ports/zephyr-cp/supervisor/serial.c index 69321ed9955..efc7647df62 100644 --- a/ports/zephyr-cp/supervisor/serial.c +++ b/ports/zephyr-cp/supervisor/serial.c @@ -78,7 +78,7 @@ void port_serial_write_substring(const char *text, uint32_t length) { if (usb_console == NULL) { return; } - zephyr_uart_write_blocking(usb_console, (const uint8_t *)text, length); + common_hal_usb_cdc_serial_write(usb_console, text, length, NULL); #else zephyr_uart_write_blocking(&uart_console, (const uint8_t *)text, length); #endif