Update esp_tinyusb to v2.2.1

+ add error screen for usb mount failure at boot
This commit is contained in:
Ken Van Hoeylandt 2026-09-09 21:49:00 +02:00
parent 8556103eb1
commit 3d71ba5407
12 changed files with 194 additions and 65 deletions

View File

@ -9,7 +9,7 @@
#include <tactility/log.h>
#include <tinyusb.h>
#include <tusb_cdc_acm.h>
#include <tinyusb_cdc_acm.h>
#include <esp_log.h>
#include <cstdarg>
@ -112,15 +112,14 @@ static error_t cdc_device_build_contribution(struct Device* device, struct Devic
static error_t cdc_device_start_console(struct Device* device) {
(void)device;
const tinyusb_config_cdcacm_t acm_cfg = {
.usb_dev = TINYUSB_USBDEV_0,
.cdc_port = TINYUSB_CDC_ACM_0,
.callback_rx = nullptr,
.callback_rx_wanted_char = nullptr,
.callback_line_state_changed = nullptr,
.callback_line_coding_changed = nullptr,
};
if (tusb_cdc_acm_init(&acm_cfg) != ESP_OK) {
LOG_E(TAG, "tusb_cdc_acm_init failed - CDC console unavailable");
if (tinyusb_cdcacm_init(&acm_cfg) != ESP_OK) {
LOG_E(TAG, "tinyusb_cdcacm_init failed - CDC console unavailable");
return ERROR_RESOURCE;
}
previous_vprintf = esp_log_set_vprintf(cdc_mirroring_vprintf);
@ -135,7 +134,7 @@ static error_t cdc_device_stop_console(struct Device* device) {
}
esp_log_set_vprintf(previous_vprintf ? previous_vprintf : vprintf);
previous_vprintf = nullptr;
tusb_cdc_acm_deinit(TINYUSB_CDC_ACM_0);
tinyusb_cdcacm_deinit(TINYUSB_CDC_ACM_0);
cdc_console_installed = false;
return ERROR_NONE;
}

View File

@ -12,8 +12,10 @@
#include <freertos/task.h>
#include <tinyusb.h>
#include <tinyusb_default_config.h>
#include <tusb.h>
#include <cstdio>
#include <cstring>
#if CONFIG_IDF_TARGET_ESP32P4
@ -33,6 +35,15 @@ static constexpr size_t COMPOSITE_CONFIG_DESCRIPTOR_MAX = TUD_CONFIG_DESC_LEN +
// Worst case: primary's own table (lang/mfr/product/serial/interface = 5) + CDC's own interface string.
static constexpr size_t COMPOSITE_STRING_DESCRIPTOR_MAX = 6;
// Detail for the most recent claim() failure - the console can be unreachable by the time this
// fails (route_phy_for_device_mode() already reassigned the PHY away from it), so callers read
// this instead of relying on the LOG_E above having been visible.
static char last_claim_error[48] = "";
extern "C" const char* usb_device_controller_get_last_error() {
return last_claim_error;
}
// ---- Controller state ----
// One TinyUSB device-mode slot, shared by every USB device class (each a separate child device
// under usbdevice0 in the devicetree; \see esp32_usb_hid_device.cpp, esp32_usb_device_msc.cpp,
@ -256,13 +267,14 @@ static error_t claim(struct Device* device, enum UsbDeviceClass usb_class, const
}
ctx->composite_tusb_cfg = {};
ctx->composite_tusb_cfg.device_descriptor = &ctx->composite_device_descriptor;
ctx->composite_tusb_cfg.string_descriptor = ctx->composite_string_descriptor;
ctx->composite_tusb_cfg.string_descriptor_count = string_count;
ctx->composite_tusb_cfg.external_phy = false;
ctx->composite_tusb_cfg.descriptor.device = &ctx->composite_device_descriptor;
ctx->composite_tusb_cfg.descriptor.string = ctx->composite_string_descriptor;
ctx->composite_tusb_cfg.descriptor.string_count = (int)string_count;
ctx->composite_tusb_cfg.phy.skip_setup = false;
ctx->composite_tusb_cfg.task = TINYUSB_TASK_DEFAULT(); // task.size/priority can't be left 0 - rejected by tinyusb_task_check_config()
#if (TUD_OPT_HIGH_SPEED)
ctx->composite_tusb_cfg.fs_configuration_descriptor = ctx->composite_fs_config_descriptor;
ctx->composite_tusb_cfg.hs_configuration_descriptor = ctx->composite_hs_config_descriptor;
ctx->composite_tusb_cfg.descriptor.full_speed_config = ctx->composite_fs_config_descriptor;
ctx->composite_tusb_cfg.descriptor.high_speed_config = ctx->composite_hs_config_descriptor;
// The qualifier descriptor's class triad must mirror the device descriptor's (same IAD
// reasoning). Built here rather than supplied per-primary since its content is boilerplate
// (same bMaxPacketSize0/bNumConfigurations for every class) and the controller already knows
@ -278,18 +290,20 @@ static error_t claim(struct Device* device, enum UsbDeviceClass usb_class, const
.bNumConfigurations = 0x01,
.bReserved = 0x00,
};
ctx->composite_tusb_cfg.qualifier_descriptor = &ctx->composite_device_qualifier;
ctx->composite_tusb_cfg.descriptor.qualifier = &ctx->composite_device_qualifier;
#else
ctx->composite_tusb_cfg.configuration_descriptor = ctx->composite_fs_config_descriptor;
ctx->composite_tusb_cfg.descriptor.full_speed_config = ctx->composite_fs_config_descriptor;
#endif
ctx->composite_tusb_cfg.self_powered = false;
ctx->composite_tusb_cfg.vbus_monitor_io = 0;
ctx->composite_tusb_cfg.phy.self_powered = false;
ctx->composite_tusb_cfg.phy.vbus_monitor_io = 0;
route_phy_for_device_mode();
ctx->phy_routed = true;
if (tinyusb_driver_install(&ctx->composite_tusb_cfg) != ESP_OK) {
LOG_E(TAG, "claim: tinyusb_driver_install failed for class %d", usb_class);
esp_err_t install_result = tinyusb_driver_install(&ctx->composite_tusb_cfg);
if (install_result != ESP_OK) {
LOG_E(TAG, "claim: tinyusb_driver_install failed for class %d: %s", usb_class, esp_err_to_name(install_result));
snprintf(last_claim_error, sizeof(last_claim_error), "tinyusb_driver_install: %s", esp_err_to_name(install_result));
if (ctx->phy_routed) {
restore_default_phy_route();
ctx->phy_routed = false;

View File

@ -9,14 +9,24 @@
#include <tactility/log.h>
#include <tinyusb.h>
#include <tusb_msc_storage.h>
#include <tinyusb_msc.h>
#include <wear_levelling.h>
#include <cstdio>
#include <cstring>
#define TAG "esp32_usb_device_msc"
#define GET_CONFIG(device) ((const Esp32UsbDeviceChildConfig*)(device)->config)
// Detail for the most recent msc_device_start() failure - the console can be unreachable by the
// time this fails (the controller already reassigned the PHY away from it), so callers read this
// instead of relying on the LOG_E calls below having been visible.
static char last_msc_error[48] = "";
extern "C" const char* usb_msc_device_get_last_error() {
return last_msc_error;
}
// ---- MSC device descriptor set ----
// Mutable, matching the other primary classes' descriptors, even though MSC's own
@ -62,6 +72,7 @@ static uint8_t msc_hs_configuration_descriptor[TUD_MSC_DESC_LEN];
struct UsbMscDeviceCtx {
UsbMscDeviceMountChangedCallback mount_changed_cb = nullptr;
void* mount_changed_context = nullptr;
tinyusb_msc_storage_handle_t storage_handle = nullptr;
bool storage_active = false;
};
@ -70,12 +81,20 @@ struct UsbMscDeviceCtx {
// device-mode slot the controller already enforces.
static struct Device* active_msc_device = nullptr;
static void storage_mount_changed_cb(tinyusb_msc_event_t* event) {
// esp_tinyusb 2.x's MOUNT_COMPLETE event fires for either owner taking the storage. Kernel-facing
// UsbMscDeviceMountChangedCallback's `mounted` (\see usb_msc_device.h) means "mounted into our own
// filesystem" (the pre-2.x meaning, kept for callers like the boot-into-MSC flow that reboot back
// to normal OS once mounted==true) - i.e. ownership has passed to TINYUSB_MSC_STORAGE_MOUNT_APP,
// the opposite of the initial USB-owned state this driver starts storage in.
static void storage_mount_changed_cb(tinyusb_msc_storage_handle_t handle, tinyusb_msc_event_t* event, void* arg) {
(void)handle;
(void)arg;
if (event->id != TINYUSB_MSC_EVENT_MOUNT_COMPLETE) return;
if (active_msc_device == nullptr) return;
auto* ctx = static_cast<UsbMscDeviceCtx*>(device_get_driver_data(active_msc_device));
if (ctx == nullptr) return;
const bool mounted = event->mount_changed_data.is_mounted;
const bool mounted = event->mount_point == TINYUSB_MSC_STORAGE_MOUNT_APP;
LOG_I(TAG, "%s", mounted ? "MSC mounted" : "MSC unmounted");
if (ctx->mount_changed_cb != nullptr) {
ctx->mount_changed_cb(mounted, ctx->mount_changed_context);
@ -131,6 +150,7 @@ static error_t msc_device_start(struct Device* device, enum UsbMscDeviceSource s
error_t claim_result = usb_device_controller_claim(controller, USB_DEVICE_CLASS_MSC, &claim_config);
if (claim_result != ERROR_NONE) {
snprintf(last_msc_error, sizeof(last_msc_error), "claim: %s", usb_device_controller_get_last_error());
return claim_result;
}
@ -139,44 +159,66 @@ static error_t msc_device_start(struct Device* device, enum UsbMscDeviceSource s
ctx->mount_changed_context = context;
active_msc_device = device;
esp_err_t result;
if (source == USB_MSC_DEVICE_SOURCE_SDMMC) {
const tinyusb_msc_sdmmc_config_t config_sdmmc = {
.card = static_cast<sdmmc_card_t*>(source_handle),
.callback_mount_changed = storage_mount_changed_cb,
.callback_premount_changed = nullptr,
.mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 0,
.disk_status_check_enable = false,
.use_one_fat = false,
},
};
result = tinyusb_msc_storage_init_sdmmc(&config_sdmmc);
} else {
const tinyusb_msc_spiflash_config_t config_flash = {
.wl_handle = *static_cast<wl_handle_t*>(source_handle),
.callback_mount_changed = storage_mount_changed_cb,
.callback_premount_changed = nullptr,
.mount_config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 0,
.disk_status_check_enable = false,
.use_one_fat = false,
},
};
result = tinyusb_msc_storage_init_spiflash(&config_flash);
}
// esp_tinyusb 2.x auto-remounts storage on every tud_mount_cb()/tud_umount_cb(), including
// the bus resets that happen as a normal part of USB enumeration - not just a genuine host
// eject. Left enabled, that fires a spurious "mounted into our own fs" transition almost
// immediately after claim(), which callers like the boot-into-MSC flow read as "the host is
// done with it" and act on right away. Disabling it restores the old (pre-2.x) behavior:
// mount_point only changes via an explicit host SCSI eject or our own start/stop calls.
const tinyusb_msc_driver_config_t driver_cfg = {
.user_flags = { .val = 1 }, // auto_mount_off
.callback = storage_mount_changed_cb,
.callback_arg = nullptr,
};
esp_err_t result = tinyusb_msc_install_driver(&driver_cfg);
if (result != ESP_OK) {
LOG_E(TAG, "storage init failed: %s", esp_err_to_name(result));
LOG_E(TAG, "MSC driver install failed: %s", esp_err_to_name(result));
snprintf(last_msc_error, sizeof(last_msc_error), "install_driver: %s", esp_err_to_name(result));
active_msc_device = nullptr;
usb_device_controller_release(controller, USB_DEVICE_CLASS_MSC);
return ERROR_RESOURCE;
}
const tinyusb_msc_fatfs_config_t fat_fs_config = {
.base_path = nullptr, // use CONFIG_TINYUSB_MSC_MOUNT_PATH default
.config = {
.format_if_mount_failed = false,
.max_files = 5,
.allocation_unit_size = 0,
.disk_status_check_enable = false,
.use_one_fat = false,
},
.do_not_format = false,
.format_flags = 0, // FM_ANY
};
tinyusb_msc_storage_handle_t handle = nullptr;
if (source == USB_MSC_DEVICE_SOURCE_SDMMC) {
const tinyusb_msc_storage_config_t config_sdmmc = {
.medium = { .card = static_cast<sdmmc_card_t*>(source_handle) },
.fat_fs = fat_fs_config,
.mount_point = TINYUSB_MSC_STORAGE_MOUNT_USB,
};
result = tinyusb_msc_new_storage_sdmmc(&config_sdmmc, &handle);
} else {
const tinyusb_msc_storage_config_t config_flash = {
.medium = { .wl_handle = *static_cast<wl_handle_t*>(source_handle) },
.fat_fs = fat_fs_config,
.mount_point = TINYUSB_MSC_STORAGE_MOUNT_USB,
};
result = tinyusb_msc_new_storage_spiflash(&config_flash, &handle);
}
if (result != ESP_OK) {
LOG_E(TAG, "storage init failed: %s", esp_err_to_name(result));
snprintf(last_msc_error, sizeof(last_msc_error), "new_storage: %s", esp_err_to_name(result));
tinyusb_msc_uninstall_driver();
active_msc_device = nullptr;
usb_device_controller_release(controller, USB_DEVICE_CLASS_MSC);
return ERROR_RESOURCE;
}
ctx->storage_handle = handle;
ctx->storage_active = true;
return ERROR_NONE;
}
@ -194,8 +236,10 @@ static error_t msc_device_stop(struct Device* device) {
auto* controller = device_get_parent(device);
usb_device_controller_release(controller, USB_DEVICE_CLASS_MSC);
tinyusb_msc_storage_deinit();
tinyusb_msc_delete_storage(ctx->storage_handle);
tinyusb_msc_uninstall_driver();
ctx->storage_handle = nullptr;
ctx->storage_active = false;
ctx->mount_changed_cb = nullptr;
ctx->mount_changed_context = nullptr;

View File

@ -16,6 +16,8 @@ enum class BootMode {
};
bool startMassStorageWithSdmmc(bool fromBootMode = false);
// Reason the most recent startMassStorageWith*() call failed, or "" if it succeeded.
const char* getLastError();
void stop();
Mode getMode();
bool isSupported();

View File

@ -4,4 +4,7 @@ bool tusbIsSupported();
bool tusbStartMassStorageWithSdmmc(bool fromBootMode = false);
bool tusbStartMassStorageWithFlash(bool fromBootMode = false);
void tusbStop();
bool tusbCanStartMassStorageWithFlash();
bool tusbCanStartMassStorageWithFlash();
// Reason the most recent tusbStartMassStorageWith*() call failed, or "" if it succeeded.
const char* tusbGetLastError();

View File

@ -1,3 +1,4 @@
#include "Tactility/app/alertdialog/AlertDialog.h"
#include "tactility/memory.h"
#include "tactility/system_event.h"
@ -56,6 +57,10 @@ std::atomic<bool> isUsbBootSplash = false;
// window to an error screen and halts before starting the next app.
std::atomic<bool> sdCardMissing = false;
// Set when setupUsbBootMode() fails
std::atomic<bool> usbBootModeFailed = false;
std::string usbBootModeFailedReason;
uint32_t bootAppInstanceId = 0;
WindowId bootWindowId = 0;
@ -108,6 +113,8 @@ void setupDisplay() {
device_put(display);
}
// Returns true if either the MSC screen or the failure screen now owns the display, in which
// case the caller must not proceed to the normal boot path (registerApps()/startNextApp()).
bool setupUsbBootMode() {
if (!hal::usb::isUsbBootMode()) {
return false;
@ -116,16 +123,18 @@ bool setupUsbBootMode() {
LOG_I(TAG, "Rebooting into mass storage device mode");
auto mode = hal::usb::getUsbBootMode(); // Get mode before reset
hal::usb::resetUsbBootMode();
delay_millis(3000);
bool started = false;
if (mode == hal::usb::BootMode::Flash) {
if (!hal::usb::startMassStorageWithFlash(true)) {
LOG_E(TAG, "Unable to start flash mass storage");
return false;
}
started = hal::usb::startMassStorageWithFlash(true);
} else if (mode == hal::usb::BootMode::Sdmmc) {
if (!hal::usb::startMassStorageWithSdmmc(true)) {
LOG_E(TAG, "Unable to start SD mass storage");
return false;
}
started = hal::usb::startMassStorageWithSdmmc(true);
}
if (!started) {
usbBootModeFailedReason = hal::usb::getLastError();
usbBootModeFailed = true;
}
return true;
@ -237,6 +246,36 @@ void showSdCardMissingScreen() {
bootWindowId = window_manager_create(bootAppInstanceId, createSdCardMissingWidgets, nullptr);
}
void createUsbBootModeFailedWidgets(lv_obj_t* root, void*) {
lvgl::obj_set_style_bg_blacken(root);
lv_obj_set_style_border_width(root, 0, LV_STATE_DEFAULT);
lv_obj_set_style_radius(root, 0, LV_STATE_DEFAULT);
lv_obj_set_flex_flow(root, LV_FLEX_FLOW_COLUMN);
lv_obj_set_flex_align(root, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER);
auto* label = lv_label_create(root);
lv_label_set_text(label, std::format("Failed to start USB mass storage:\n{}", usbBootModeFailedReason).c_str());
lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, LV_STATE_DEFAULT);
lv_obj_set_style_text_color(label, lv_color_white(), LV_STATE_DEFAULT);
auto* button = lv_button_create(root);
lv_obj_set_style_margin_top(button, 16, LV_STATE_DEFAULT);
auto* button_label = lv_label_create(button);
lv_label_set_text(button_label, "Reboot");
lv_obj_add_event_cb(button, [](lv_event_t*) {
#ifdef ESP_PLATFORM
esp_restart();
#endif
}, LV_EVENT_SHORT_CLICKED, nullptr);
}
void showUsbBootModeFailedScreen() {
if (bootWindowId != 0) {
window_manager_remove(bootWindowId);
}
bootWindowId = window_manager_create(bootAppInstanceId, createUsbBootModeFailedWidgets, nullptr);
}
void startNextApp() {
if (sdCardMissing) {
showSdCardMissingScreen();
@ -278,8 +317,10 @@ void runBootSequence(TickType_t startTime) {
#endif
if (setupUsbBootMode()) {
// Stay open: the splash's "Return to OS" button is this app's only way to leave mass
// storage mode, so it must not self-close here like the normal boot path does below.
if (usbBootModeFailed) {
showUsbBootModeFailedScreen();
}
// Stay open: the splash's "Return to OS" button or the failure screen's "Reboot" button.
return;
}

View File

@ -67,6 +67,10 @@ bool startMassStorageWithSdmmc(bool fromBootMode) {
}
}
const char* getLastError() {
return tusbGetLastError();
}
void stop() {
if (canStartNewMode()) {
return;

View File

@ -5,6 +5,7 @@
namespace tt::hal::usb {
bool startMassStorageWithSdmmc(bool /*fromBootMode*/) { return false; }
const char* getLastError() { return ""; }
void stop() {}
Mode getMode() { return Mode::Default; }
BootMode getUsbBootMode() { return BootMode::None; }

View File

@ -12,6 +12,8 @@
#include <freertos/task.h>
#include <wear_levelling.h>
#include <cstdio>
#include <tactility/device.h>
#include <tactility/drivers/esp32_sdcard.h>
#include <tactility/drivers/usb_msc_device.h>
@ -25,6 +27,8 @@ namespace tt::hal::usb {
// Used to decide whether ejecting the volume should automatically reboot back to normal OS.
static bool startedFromBootMode = false;
static char lastErrorMessage[96] = "";
static sdmmc_card_t* getCard() {
sdmmc_card_t* card = nullptr;
@ -70,12 +74,14 @@ bool tusbStartMassStorageWithSdmmc(bool fromBootMode) {
auto* device = usb_msc_device_get();
if (device == nullptr) {
LOG_E(TAG, "USB MSC device not available");
snprintf(tt::hal::usb::lastErrorMessage, sizeof(tt::hal::usb::lastErrorMessage), "USB MSC device not available");
return false;
}
auto* card = tt::hal::usb::getCard();
if (card == nullptr) {
LOG_E(TAG, "SD card not mounted");
snprintf(tt::hal::usb::lastErrorMessage, sizeof(tt::hal::usb::lastErrorMessage), "SD card not mounted");
device_put(device);
return false;
}
@ -86,6 +92,7 @@ bool tusbStartMassStorageWithSdmmc(bool fromBootMode) {
tt::hal::usb::onMountChanged, nullptr);
if (result != ERROR_NONE) {
LOG_E(TAG, "TinyUSB SDMMC init failed: %s", error_to_string(result));
snprintf(tt::hal::usb::lastErrorMessage, sizeof(tt::hal::usb::lastErrorMessage), "SDMMC init failed: %s (%s)", error_to_string(result), usb_msc_device_get_last_error());
} else {
LOG_I(TAG, "TinyUSB SDMMC init success");
}
@ -100,12 +107,14 @@ bool tusbStartMassStorageWithFlash(bool fromBootMode) {
auto* device = usb_msc_device_get();
if (device == nullptr) {
LOG_E(TAG, "USB MSC device not available");
snprintf(tt::hal::usb::lastErrorMessage, sizeof(tt::hal::usb::lastErrorMessage), "USB MSC device not available");
return false;
}
wl_handle_t handle = tt::getDataPartitionWlHandle();
if (handle == WL_INVALID_HANDLE) {
LOG_E(TAG, "WL not mounted for /data");
snprintf(tt::hal::usb::lastErrorMessage, sizeof(tt::hal::usb::lastErrorMessage), "WL not mounted for /data");
device_put(device);
return false;
}
@ -116,6 +125,7 @@ bool tusbStartMassStorageWithFlash(bool fromBootMode) {
&handle, tt::hal::usb::onMountChanged, nullptr);
if (result != ERROR_NONE) {
LOG_E(TAG, "TinyUSB flash init failed: %s", error_to_string(result));
snprintf(tt::hal::usb::lastErrorMessage, sizeof(tt::hal::usb::lastErrorMessage), "Flash init failed: %s (%s)", error_to_string(result), usb_msc_device_get_last_error());
} else {
LOG_I(TAG, "TinyUSB flash init success");
}
@ -123,6 +133,10 @@ bool tusbStartMassStorageWithFlash(bool fromBootMode) {
return result == ERROR_NONE;
}
const char* tusbGetLastError() {
return tt::hal::usb::lastErrorMessage;
}
void tusbStop() {
auto* device = usb_msc_device_get();
if (device != nullptr) {
@ -142,6 +156,7 @@ bool tusbStartMassStorageWithSdmmc(bool /*fromBootMode*/) { return false; }
bool tusbStartMassStorageWithFlash(bool /*fromBootMode*/) { return false; }
void tusbStop() {}
bool tusbCanStartMassStorageWithFlash() { return false; }
const char* tusbGetLastError() { return ""; }
#endif // CONFIG_TINYUSB_MSC_ENABLED

View File

@ -71,7 +71,7 @@ dependencies:
- if: "target in [esp32p4]"
espressif/esp_lcd_panel_io_additions: "1.0.1"
espressif/esp_tinyusb:
version: "1.7.6~1"
version: "2.2.1"
rules:
- if: "target in [esp32s3, esp32p4]"
espressif/esp_lvgl_port: "2.7.2"

View File

@ -183,6 +183,9 @@ error_t usb_device_controller_allocate_interfaces(struct Device* device, uint8_t
struct UsbInterfaceAllocation* out_allocation);
error_t usb_device_controller_claim(struct Device* device, enum UsbDeviceClass usb_class,
const struct UsbDeviceClaimConfig* config);
/** Detail string (e.g. an esp_err_t name) for the most recent claim() failure, or "" if none. */
const char* usb_device_controller_get_last_error(void);
error_t usb_device_controller_release(struct Device* device, enum UsbDeviceClass usb_class);
enum UsbDeviceClass usb_device_controller_get_active_class(struct Device* device);
bool usb_device_controller_is_cdc_enabled(struct Device* device);

View File

@ -85,6 +85,9 @@ error_t usb_msc_device_start(struct Device* device, enum UsbMscDeviceSource sour
error_t usb_msc_device_stop(struct Device* device);
bool usb_msc_device_is_connected(struct Device* device);
/** Detail string for the most recent usb_msc_device_start() failure, or "" if none. */
const char* usb_msc_device_get_last_error(void);
#ifdef __cplusplus
}
#endif