/** * Copyright (c) 2020 Raspberry Pi (Trading) Ltd. * * SPDX-License-Identifier: BSD-3-Clause */ #ifndef LIB_TINYUSB_HOST #include "tusb.h" #include "pico/stdio_usb.h" // these may not be set if the user is providing tud support (i.e. LIB_TINYUSB_DEVICE is 1 because // the user linked in tinyusb_device) but they haven't selected CDC #if (CFG_TUD_ENABLED | TUSB_OPT_DEVICE_ENABLED) && CFG_TUD_CDC #include "pico/binary_info.h" #include "pico/time.h" #include "pico/stdio/driver.h" #include "pico/mutex.h" #include "hardware/irq.h" #include "device/usbd_pvt.h" // for usbd_defer_func static mutex_t stdio_usb_mutex; #if PICO_STDIO_USB_SUPPORT_CHARS_AVAILABLE_CALLBACK static void (*chars_available_callback)(void*); static void *chars_available_param; #endif // when tinyusb_device is explicitly linked we do no background tud processing #if !LIB_TINYUSB_DEVICE // if this crit_sec is initialized, we are not in periodic timer mode, and must make sure // we don't either create multiple one shot timers, or miss creating one. this crit_sec // is used to protect the one_shot_timer_pending flag static critical_section_t one_shot_timer_crit_sec; static volatile bool one_shot_timer_pending; #ifdef PICO_STDIO_USB_LOW_PRIORITY_IRQ static_assert(PICO_STDIO_USB_LOW_PRIORITY_IRQ >= NUM_IRQS - NUM_USER_IRQS, ""); #define low_priority_irq_num PICO_STDIO_USB_LOW_PRIORITY_IRQ #else static uint8_t low_priority_irq_num; #endif static int64_t timer_task(__unused alarm_id_t id, __unused void *user_data) { int64_t repeat_time; if (critical_section_is_initialized(&one_shot_timer_crit_sec)) { critical_section_enter_blocking(&one_shot_timer_crit_sec); one_shot_timer_pending = false; critical_section_exit(&one_shot_timer_crit_sec); repeat_time = 0; // don't repeat } else { repeat_time = PICO_STDIO_USB_TASK_INTERVAL_US; } if (irq_is_enabled(low_priority_irq_num)) { irq_set_pending(low_priority_irq_num); return repeat_time; } else { return 0; // don't repeat } } static void low_priority_worker_irq(void) { if (mutex_try_enter(&stdio_usb_mutex, NULL)) { tud_task(); #if PICO_STDIO_USB_SUPPORT_CHARS_AVAILABLE_CALLBACK uint32_t chars_avail = tud_cdc_available(); #endif mutex_exit(&stdio_usb_mutex); #if PICO_STDIO_USB_SUPPORT_CHARS_AVAILABLE_CALLBACK if (chars_avail && chars_available_callback) chars_available_callback(chars_available_param); #endif } else { // if the mutex is already owned, then we are in non IRQ code in this file. // // it would seem simplest to just let that code call tud_task() at the end, however this // code might run during the call to tud_task() and we might miss a necessary tud_task() call // // if we are using a periodic timer (crit_sec is not initialized in this case), // then we are happy just to wait until the next tick, however when we are not using a periodic timer, // we must kick off a one-shot timer to make sure the tud_task() DOES run (this method // will be called again as a result, and will try the mutex_try_enter again, and if that fails // create another one shot timer again, and so on). if (critical_section_is_initialized(&one_shot_timer_crit_sec)) { bool need_timer; critical_section_enter_blocking(&one_shot_timer_crit_sec); need_timer = !one_shot_timer_pending; one_shot_timer_pending = true; critical_section_exit(&one_shot_timer_crit_sec); if (need_timer) { add_alarm_in_us(PICO_STDIO_USB_TASK_INTERVAL_US, timer_task, NULL, true); } } } } static void usb_irq(void) { irq_set_pending(low_priority_irq_num); } #endif static void stdio_usb_out_chars(const char *buf, int length) { static uint64_t last_avail_time; if (!mutex_try_enter_block_until(&stdio_usb_mutex, make_timeout_time_ms(PICO_STDIO_DEADLOCK_TIMEOUT_MS))) { return; } if (stdio_usb_connected()) { for (int i = 0; i < length;) { int n = length - i; int avail = (int) tud_cdc_write_available(); if (n > avail) n = avail; if (n) { int n2 = (int) tud_cdc_write(buf + i, (uint32_t)n); tud_task(); tud_cdc_write_flush(); i += n2; last_avail_time = time_us_64(); } else { tud_task(); tud_cdc_write_flush(); if (!stdio_usb_connected() || (!tud_cdc_write_available() && time_us_64() > last_avail_time + PICO_STDIO_USB_STDOUT_TIMEOUT_US)) { break; } } } } else { // reset our timeout last_avail_time = 0; } mutex_exit(&stdio_usb_mutex); } static void stdio_usb_out_flush(void) { if (!mutex_try_enter_block_until(&stdio_usb_mutex, make_timeout_time_ms(PICO_STDIO_DEADLOCK_TIMEOUT_MS))) { return; } do { tud_task(); } while (tud_cdc_write_flush()); mutex_exit(&stdio_usb_mutex); } int stdio_usb_in_chars(char *buf, int length) { // note we perform this check outside the lock, to try and prevent possible deadlock conditions // with printf in IRQs (which we will escape through timeouts elsewhere, but that would be less graceful). // // these are just checks of state, so we can call them while not holding the lock. // they may be wrong, but only if we are in the middle of a tud_task call, in which case at worst // we will mistakenly think we have data available when we do not (we will check again), or // tud_task will complete running and we will check the right values the next time. // int rc = PICO_ERROR_NO_DATA; if (stdio_usb_connected() && tud_cdc_available()) { if (!mutex_try_enter_block_until(&stdio_usb_mutex, make_timeout_time_ms(PICO_STDIO_DEADLOCK_TIMEOUT_MS))) { return PICO_ERROR_NO_DATA; // would deadlock otherwise } if (stdio_usb_connected() && tud_cdc_available()) { int count = (int) tud_cdc_read(buf, (uint32_t) length); rc = count ? count : PICO_ERROR_NO_DATA; } else { // because our mutex use may starve out the background task, run tud_task here (we own the mutex) tud_task(); } mutex_exit(&stdio_usb_mutex); } return rc; } #if PICO_STDIO_USB_SUPPORT_CHARS_AVAILABLE_CALLBACK void stdio_usb_set_chars_available_callback(void (*fn)(void*), void *param) { chars_available_callback = fn; chars_available_param = param; } #endif stdio_driver_t stdio_usb = { .out_chars = stdio_usb_out_chars, .out_flush = stdio_usb_out_flush, .in_chars = stdio_usb_in_chars, #if PICO_STDIO_USB_SUPPORT_CHARS_AVAILABLE_CALLBACK .set_chars_available_callback = stdio_usb_set_chars_available_callback, #endif #if PICO_STDIO_ENABLE_CRLF_SUPPORT .crlf_enabled = PICO_STDIO_USB_DEFAULT_CRLF #endif }; bool stdio_usb_init(void) { if (get_core_num() != alarm_pool_core_num(alarm_pool_get_default())) { // included an assertion here rather than just returning false, as this is likely // a coding bug, rather than anything else. assert(false); return false; } #if !PICO_NO_BI_STDIO_USB bi_decl_if_func_used(bi_program_feature("USB stdin / stdout")); #endif #if !defined(LIB_TINYUSB_DEVICE) // initialize TinyUSB, as user hasn't explicitly linked it tusb_init(); #else assert(tud_inited()); // we expect the caller to have initialized if they are using TinyUSB #endif if (!mutex_is_initialized(&stdio_usb_mutex)) mutex_init(&stdio_usb_mutex); bool rc = true; #if !LIB_TINYUSB_DEVICE #ifdef PICO_STDIO_USB_LOW_PRIORITY_IRQ user_irq_claim(PICO_STDIO_USB_LOW_PRIORITY_IRQ); #else low_priority_irq_num = (uint8_t) user_irq_claim_unused(true); #endif irq_set_exclusive_handler(low_priority_irq_num, low_priority_worker_irq); irq_set_enabled(low_priority_irq_num, true); if (irq_has_shared_handler(USBCTRL_IRQ)) { critical_section_init_with_lock_num(&one_shot_timer_crit_sec, spin_lock_claim_unused(true)); // we can use a shared handler to notice when there may be work to do irq_add_shared_handler(USBCTRL_IRQ, usb_irq, PICO_SHARED_IRQ_HANDLER_LOWEST_ORDER_PRIORITY); } else { // we use initialization state of the one_shot_timer_critsec as a flag memset(&one_shot_timer_crit_sec, 0, sizeof(one_shot_timer_crit_sec)); rc = add_alarm_in_us(PICO_STDIO_USB_TASK_INTERVAL_US, timer_task, NULL, true) >= 0; } #endif if (rc) { stdio_set_driver_enabled(&stdio_usb, true); #if PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS #if PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS > 0 absolute_time_t until = make_timeout_time_ms(PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS); #else absolute_time_t until = at_the_end_of_time; #endif do { if (stdio_usb_connected()) { #if PICO_STDIO_USB_POST_CONNECT_WAIT_DELAY_MS != 0 sleep_ms(PICO_STDIO_USB_POST_CONNECT_WAIT_DELAY_MS); #endif break; } sleep_ms(10); } while (!time_reached(until)); #endif } return rc; } bool stdio_usb_deinit(void) { if (get_core_num() != alarm_pool_core_num(alarm_pool_get_default())) { // included an assertion here rather than just returning false, as this is likely // a coding bug, rather than anything else. assert(false); return false; } assert(tud_inited()); // we expect the caller to have initialized when calling sdio_usb_init bool rc = true; stdio_set_driver_enabled(&stdio_usb, false); #if PICO_STDIO_USB_DEINIT_DELAY_MS != 0 sleep_ms(PICO_STDIO_USB_DEINIT_DELAY_MS); #endif #if !LIB_TINYUSB_DEVICE if (irq_has_shared_handler(USBCTRL_IRQ)) { spin_lock_unclaim(spin_lock_get_num(one_shot_timer_crit_sec.spin_lock)); critical_section_deinit(&one_shot_timer_crit_sec); // we can use a shared handler to notice when there may be work to do irq_remove_handler(USBCTRL_IRQ, usb_irq); } else { // timer is disabled by disabling the irq } irq_set_enabled(low_priority_irq_num, false); user_irq_unclaim(low_priority_irq_num); #endif return rc; } bool stdio_usb_connected(void) { #if PICO_STDIO_USB_CONNECTION_WITHOUT_DTR return tud_ready(); #else // this actually checks DTR return tud_cdc_connected(); #endif } #else #warning stdio USB was configured along with user use of TinyUSB device mode, but CDC is not enabled bool stdio_usb_init(void) { return false; } #endif // CFG_TUD_ENABLED && CFG_TUD_CDC #else #warning stdio USB was configured, but is being disabled as TinyUSB host is explicitly linked bool stdio_usb_init(void) { return false; } #endif // !LIB_TINYUSB_HOST