From 42eea4f9068db3a39ab9b5061cd4038973afc40e Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:17:53 +0200 Subject: [PATCH] usb fix --- zephcore/adapters/usb/ZephyrCompanionUSB.cpp | 61 ++++++++++++++++---- zephcore/adapters/usb/ZephyrCompanionUSB.h | 8 ++- zephcore/adapters/usb/ZephyrUSBCDC.cpp | 50 +++++++++++++--- zephcore/adapters/usb/ZephyrUSBCDC.h | 16 ++++- 4 files changed, 113 insertions(+), 22 deletions(-) diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp index c0885ea..9b8eaf0 100644 --- a/zephcore/adapters/usb/ZephyrCompanionUSB.cpp +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.cpp @@ -100,9 +100,16 @@ static const struct companion_link_cbs *s_link; K_MSGQ_DEFINE(usb_recv_queue, sizeof(struct frame), USB_RECV_QUEUE_DEPTH, 4); /* A session starts on the first traffic after open (a binary frame or a CLI - * line) and ends when the host drops DTR. */ + * line) and ends when the host drops DTR or goes away without one (VBUS loss, + * bus reset). */ static bool usb_session_active; +/* The session can carry frames: it is open and the bus is not suspended (host + * asleep with the port open). Main hears each edge as a connect or a + * disconnect, so a suspended session is not a client: nothing is queued for + * it, and it does not hold up the last-client cleanup. */ +static bool usb_link_up; + /* CLI text line callback — fired when a complete line arrives in text mode. */ static void (*s_cli_line_cb)(const char *line); static char usb_text_line[USB_TEXT_LINE_MAX]; @@ -189,6 +196,31 @@ static void usb_uart_isr(const struct device *dev, void *user_data) } } +/* Recompute usb_link_up and tell main about an edge. Arduino shows "connected" + * for serial transports too, so a text session raises the edges as well. */ +static void usb_link_update(void) +{ + bool up = usb_session_active; + +#if COMPANION_HAS_DTR + up = up && !zephcore_usbd_is_suspended(); +#endif + if (up == usb_link_up) { + return; + } + usb_link_up = up; + if (!s_link) { + return; + } + if (up) { + if (s_link->on_connected) { + s_link->on_connected(); + } + } else if (s_link->on_disconnected) { + s_link->on_disconnected(); + } +} + /* Start a session on its first inbound traffic — a binary frame or a complete * CLI line. The official client opens with CMD_DEVICE_QUERY (0x16), not * CMD_APP_START, so any first traffic starts it. Other transports (BLE, WiFi) @@ -202,10 +234,7 @@ static void usb_session_begin(uint8_t log_tag, bool is_text) usb_session_is_text = is_text; LOG_INF("usb_rx: first traffic 0x%02x, session started (%s)", log_tag, is_text ? "text" : "binary"); - /* Arduino shows "connected" for serial transports too */ - if (s_link && s_link->on_connected) { - s_link->on_connected(); - } + usb_link_update(); } /* USB RX work - parses V3 frames from ring buffer */ @@ -347,7 +376,8 @@ static void usb_rx_work_fn(struct k_work *work) #if COMPANION_HAS_DTR /* DTR-transition callback from the shared ZephyrUSBCDC module. - * On drop: host closed the port → reset parser, hand control back to BLE. + * On drop (the host closed the port, or went away: VBUS loss, bus reset): + * reset the parser and end the session. * CDC-only — a plain-UART backend has no DTR and never registers this. */ static void on_dtr_change(bool dtr_active) { @@ -355,7 +385,6 @@ static void on_dtr_change(bool dtr_active) return; } LOG_INF("usb_dtr: DTR dropped, USB disconnected"); - bool was_active = usb_session_active; usb_session_active = false; ring_buf_reset(&usb_ring_buf); @@ -375,8 +404,19 @@ static void on_dtr_change(bool dtr_active) /* The session is over: main runs its per-session cleanup (contact dump, * sync, sign buffer) once no other transport is still connected. */ - if (was_active && s_link && s_link->on_disconnected) { - s_link->on_disconnected(); + usb_link_update(); +} + +/* Bus suspend, resume or reset, from the shared ZephyrUSBCDC module. */ +static void on_bus_change(void) +{ + bool was_up = usb_link_up; + + usb_link_update(); + if (usb_link_up && !was_up) { + /* Resumed. The class does not restart TX by itself: frames queued + * before the suspend wait for this kick. */ + uart_irq_tx_enable(usb_dev); } } #endif /* COMPANION_HAS_DTR */ @@ -455,7 +495,7 @@ size_t zephcore_usb_companion_recv(uint8_t *dest) bool zephcore_usb_companion_is_connected(void) { - return usb_session_active && !usb_session_is_text; + return usb_link_up && !usb_session_is_text; } /* Only a connected session is ever busy: MultiSerialInterface asks every @@ -516,6 +556,7 @@ void zephcore_usb_companion_init(const struct companion_link_cbs *link) * shared usbd_msg_callback — no polling work needed. Plain-UART * backends have no DTR; their session reset is protocol-driven. */ zephcore_usbd_set_dtr_cb(on_dtr_change); + zephcore_usbd_set_bus_cb(on_bus_change); #endif } else { LOG_WRN("Companion UART not ready"); diff --git a/zephcore/adapters/usb/ZephyrCompanionUSB.h b/zephcore/adapters/usb/ZephyrCompanionUSB.h index a71f402..8cd2562 100644 --- a/zephcore/adapters/usb/ZephyrCompanionUSB.h +++ b/zephcore/adapters/usb/ZephyrCompanionUSB.h @@ -21,8 +21,9 @@ extern "C" { /** * Initialize the transport: ring buffers, UART ISR, DTR tracking. - * @param link frame waiting / TX drained / session start / session end - * (DTR drop). Raised from the ISR and sysworkq: post events only. + * @param link frame waiting / TX drained / session start (also a bus resume) + * / session end (DTR drop, VBUS loss, bus reset; also a bus + * suspend). Raised from the ISR and sysworkq: post events only. */ void zephcore_usb_companion_init(const struct companion_link_cbs *link); @@ -36,7 +37,8 @@ size_t zephcore_usb_companion_write_frame(const uint8_t *src, size_t len); /** Take the next received binary frame into dest (MAX_FRAME_SIZE). Returns its length, 0 if none. */ size_t zephcore_usb_companion_recv(uint8_t *dest); -/** A binary companion session is open (a text-CLI session is not a client). */ +/** A binary companion session is open and the bus is not suspended (a + * text-CLI session is not a client). */ bool zephcore_usb_companion_is_connected(void); /** The TX ring cannot take another full-size frame. */ diff --git a/zephcore/adapters/usb/ZephyrUSBCDC.cpp b/zephcore/adapters/usb/ZephyrUSBCDC.cpp index b5eafa2..ef3a349 100644 --- a/zephcore/adapters/usb/ZephyrUSBCDC.cpp +++ b/zephcore/adapters/usb/ZephyrUSBCDC.cpp @@ -8,8 +8,9 @@ * SET_LINE_CODING requests reach us as events (no DTR polling) * - Emits a k_event when DTR transitions high — the boot path waits on it * instead of a fixed sleep - * - Fires a user callback on every DTR transition (companion: reset RX + - * flip active_iface on drop) + * - Fires a user callback on every DTR transition (companion: end the + * session on drop), and reports VBUS loss and a bus reset as a drop + * - Fires a user callback on bus suspend, resume and reset * - Handles the Arduino-style 1200-baud touch → reboot-to-bootloader flow * for nRF52 boards */ @@ -71,6 +72,7 @@ static K_EVENT_DEFINE(usb_cdc_events); static bool s_dtr_active; static bool s_initialized; static zephcore_usbd_cdc_dtr_cb_t s_dtr_cb; +static zephcore_usbd_cdc_bus_cb_t s_bus_cb; /* ZephyrBoard for bootloader-magic write before reset. The class is stateless * (only static methods over GPREGRET + sys_reboot), so a local instance here @@ -86,6 +88,17 @@ static void enter_bootloader(void) CODE_UNREACHABLE; } +/* The host went away without a DTR=0: report it as a DTR drop. */ +static void host_gone(void) +{ + if (s_dtr_active) { + s_dtr_active = false; + if (s_dtr_cb) { + s_dtr_cb(false); + } + } +} + /* ===== USBD message callback ===== */ static void usbd_msg_callback(struct usbd_context *const ctx, const struct usbd_msg *msg) @@ -142,11 +155,24 @@ static void usbd_msg_callback(struct usbd_context *const ctx, * connection until reboot). Treat VBUS loss as a DTR drop so the * interface is handed back to BLE. */ LOG_INF("CDC ACM: VBUS removed (device unplug)"); - if (s_dtr_active) { - s_dtr_active = false; - if (s_dtr_cb) { - s_dtr_cb(false); - } + host_gone(); + } else if (msg->type == USBD_MSG_RESET) { + /* Bus reset. At enumeration no port is open and this does nothing. + * Later it means the host is enumerating us again (a hub upstream + * was reset, or the host lost the device): the port it had open is + * gone, and no DTR=0 says so, since the class keeps the line state + * of the old session. VBUS can stay up throughout, so treat it as + * a DTR drop too. A reset also ends a suspend, with no RESUME. */ + LOG_INF("CDC ACM: bus reset"); + host_gone(); + if (s_bus_cb) { + s_bus_cb(); + } + } else if (msg->type == USBD_MSG_SUSPEND || msg->type == USBD_MSG_RESUME) { + LOG_INF("CDC ACM: bus %s", + msg->type == USBD_MSG_SUSPEND ? "suspended" : "resumed"); + if (s_bus_cb) { + s_bus_cb(); } } } @@ -268,6 +294,16 @@ extern "C" void zephcore_usbd_set_dtr_cb(zephcore_usbd_cdc_dtr_cb_t cb) s_dtr_cb = cb; } +extern "C" bool zephcore_usbd_is_suspended(void) +{ + return s_initialized && usbd_is_suspended(&zephcore_usbd); +} + +extern "C" void zephcore_usbd_set_bus_cb(zephcore_usbd_cdc_bus_cb_t cb) +{ + s_bus_cb = cb; +} + extern "C" void zephcore_usbd_detach(void) { if (!s_initialized) { diff --git a/zephcore/adapters/usb/ZephyrUSBCDC.h b/zephcore/adapters/usb/ZephyrUSBCDC.h index dc7e732..12a2339 100644 --- a/zephcore/adapters/usb/ZephyrUSBCDC.h +++ b/zephcore/adapters/usb/ZephyrUSBCDC.h @@ -20,9 +20,14 @@ extern "C" { #endif /* Per-transition DTR callback. Fires on every host CONTROL_LINE_STATE change, - * value = current DTR level. NULL is allowed (cb cleared). */ + * value = current DTR level, and with false when the host goes away without + * one (VBUS loss, bus reset). NULL is allowed (cb cleared). */ typedef void (*zephcore_usbd_cdc_dtr_cb_t)(bool dtr_active); +/* Bus state callback: the bus was suspended, resumed or reset. Read the state + * with zephcore_usbd_is_suspended(). NULL is allowed (cb cleared). */ +typedef void (*zephcore_usbd_cdc_bus_cb_t)(void); + /* Initialize USB device + CDC ACM class and enumerate. * Idempotent; subsequent calls are no-ops. * Returns 0 on success or a negative errno. */ @@ -37,9 +42,16 @@ int zephcore_usbd_wait_dtr(uint32_t timeout_ms); bool zephcore_usbd_is_dtr_active(void); /* Register a callback fired on every DTR transition. - * Companion uses this to reset its RX state and flip active_iface on drop. */ + * Companion uses this to end its session on a drop. */ void zephcore_usbd_set_dtr_cb(zephcore_usbd_cdc_dtr_cb_t cb); +/* The host has suspended the bus (asleep with the port open): nothing we send + * leaves the device until it resumes. The USB stack's own state. */ +bool zephcore_usbd_is_suspended(void); + +/* Register a callback fired on a bus suspend, resume or reset. */ +void zephcore_usbd_set_bus_cb(zephcore_usbd_cdc_bus_cb_t cb); + /* Detach from the USB bus (drop the D+ pull-up) so the host sees an unplug. * Call before a reset: a soft reset leaves the PHY attached on some SoCs, and * the host then keeps a stale endpoint open across the reboot. No-op if the