ESP-IDF: как создать композитное устройство USB Печать
Добавил(а) microsin   

Ниже показана последовательность действий для создания композитного устройства USB на примере микроконтроллера ESP32-S3 под операционной системой Ubuntu. Подразумевается, что ESP-IDF у вас уже установлена [1].

Будет создано композитное устройство с двумя классами USB:

USB CDC, виртуальный последовательный порт
USB MSC,ass Storage Class (USB-флешка)

Иногда класс USB MSC также называют USB MSD (Mass Storage Device).

Далее описание процесса создания композитного устройства USB по шагам.

1. Создайте копию примера tusb_composite_msc_serialdevice в произвольном каталоге на диске. Предположим, что это каталог ваших проектов ~/myproj, выполните следующую последовательность команд:

source ~/esp/esp-idf/export.sh
cd ~/myproj
cp -r $IDF_PATH/examples/peripherals/usb/device/tusb_composite_msc_serialdevice \
   my_composite_msc_serialdevice
cd my_composite_msc_serialdevice

2. Сконфигурируйте целевой процессор (в этом примере ESP32-S3):

idf.py set-target esp32s3

3. Проверьте конфигурацию компонента TinyUSB Stack, на основе которого реализуется компонентное устройство USB. Обычно по умолчанию уже сконфигурированы классы USB CDC и USB MSC, но проверка не помешает:

idf.py menuconfig

- Перейдите в Component config → TinyUSB Stack.
- Установите галочку Enable TinyUSB stack.
- В этом же разделе одновременно включите (если это не включено):
   Mass Storage Class (MSC)
   Communication Device Class (CDC)

ESP IDF composite USB device fig01

4. Скомпилируйте и прошейте проект.

idf.py set-target esp32s3
idf.py build
idf.py flash monitor -p /dev/ttyACM0

После этого сразу появится на компьютере новая флешка, на которой будет каталог ESP, и в нем файл TEST.TXT, в котором будет текст 'Hello World!'.

Проверьте, что подключается именно составное устройство. Для этого отключите ваше устройство, выполните команду:

sudo dmesg -w

И снова подключите устройство. Вы увидите примерно такой лог операционной системы:

[ 4314.807479] usb 3-1.4: new full-speed USB device number 19 using xhci_hcd
[ 4314.895377] usb 3-1.4: New USB device found, idVendor=303a, idProduct=1001,
 bcdDevice= 1.01
[ 4314.895395] usb 3-1.4: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 4314.895400] usb 3-1.4: Product: USB JTAG/serial debug unit
[ 4314.895405] usb 3-1.4: Manufacturer: Espressif
[ 4314.895408] usb 3-1.4: SerialNumber: DC:B4:D9:06:C4:E8
[ 4314.908987] usb 3-1.4: can't set config #1, error -32
[ 4315.133356] usb 3-1.4: USB disconnect, device number 19
[ 4315.311517] usb 3-1.4: new full-speed USB device number 20 using xhci_hcd
[ 4315.398946] usb 3-1.4: New USB device found, idVendor=303a, idProduct=4001,
 bcdDevice= 1.00
[ 4315.398970] usb 3-1.4: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 4315.398979] usb 3-1.4: Product: Espressif Device
[ 4315.398985] usb 3-1.4: Manufacturer: Espressif Systems
[ 4315.398991] usb 3-1.4: SerialNumber: 123456
[ 4315.419214] cdc_acm 3-1.4:1.0: ttyACM0: USB ACM device
[ 4315.419747] usb-storage 3-1.4:1.2: USB Mass Storage device detected
[ 4315.420863] scsi host1: usb-storage 3-1.4:1.2
[ 4316.463817] scsi 1:0:0:0: Direct-Access TinyUSB TEST MSC Storage 0.1 PQ: 0 ANSI: 2
[ 4316.465593] sd 1:0:0:0: Attached scsi generic sg1 type 0
[ 4316.467837] sd 1:0:0:0: [sdb] 2000 512-byte logical blocks: (1.02 MB/1000 KiB)
[ 4316.468830] sd 1:0:0:0: [sdb] Write Protect is off
[ 4316.468843] sd 1:0:0:0: [sdb] Mode Sense: 03 00 00 00
[ 4316.469639] sd 1:0:0:0: [sdb] No Caching mode page found
[ 4316.469653] sd 1:0:0:0: [sdb] Assuming drive cache: write through
[ 4316.506738]  sdb: sdb1
[ 4316.506935] sd 1:0:0:0: [sdb] Attached SCSI removable disk
[ 4316.709188] FAT-fs (sdb1): Volume was not properly unmounted.
 Some data may be corrupt. Please run fsck.

[Изменение размера флешки]

5. Проверьте, сколько реально флеш-памяти установлено на вашем устройстве. Это можно узнать из лога загрузки (важное выделил желтым текстом):

...
I (77) boot.esp32s3: Boot SPI Speed : 80MHz I (81) boot.esp32s3: SPI Mode : DIO I (85) boot.esp32s3: SPI Flash Size : 4MB I (89) boot: Enabling RNG early entropy source... I (93) boot: Partition Table: I (96) boot: ## Label Usage Type ST Offset Length I (102) boot: 0 nvs WiFi data 01 02 00009000 00006000 I (109) boot: 1 phy_init RF data 01 01 0000f000 00001000 I (115) boot: 2 factory factory app 00 00 00010000 00100000 I (122) boot: 3 storage Unknown data 01 81 00110000 00100000 I (129) boot: End of partition table ...
I (294) spi_flash: detected chip: winbond I (296) spi_flash: flash io: dio
W (299) spi_flash: Detected size(16384k) larger than the size in the binary image header(4096k).
Using the size in the binary image header.
...

Из этого лога можно узнать, что в проекте сконфигурирован размер flas-памяти 4 мегабайта, но на самом деле определилась память размером 16 мегабайт. Поэтому первое, что надо сделать - привести сконфигурированный размер с реальным. Это делается той же командой idf.py menuconfig.

6. Конфигурирование размера flash-памяти:

idf.py menuconfig

- Перейдите в раздел Serial flasher config.
- В разделе Flash size установите правильный размер Flash.

ESP IDF composite USB device fig02

Замечание: на этом шаге для прошивки возможно понадобиться ввести процессор ESP32-S3 в режим загрузки. Для этого удерживайте нажатой кнопку BOOT, коротко нажмите кнопку RESET, отпустите кнопку BOOT и выполните команду:

idf.py flash monitor -p /dev/ttyACM0

7. Изменение размера раздела, отведенного под USB MSC.

Чтобы изменить размер диска, который видит компьютер при подключении ESP32-S3 по USB, нужно настроить несколько компонентов. Размер виртуального USB-диска определяется определяется размером раздела во Flash-памяти, который вы для этого выделите. Поэтому процесс изменения размера включает в себя три основных шага: изменение размера раздела в таблице разделов, корректировку кода и проверку конфигурации в menuconfig.

Изменение таблицы разделов (partition table). Размер диска задаётся в файле partitions.csv в корне вашего проекта. В проекте tusb_composite_msc_serialdevice он обычно уже есть. Если файла partitions.csv нет, создайте его. Вот типовое содержимое такого файла:

# Замечание: если вы изменили размер загрузчика (bootloader size),
# то убедитесь, что обновили смещения в столбце offset, чтобы
# избежать наложения разделов друг на друга.
# Name,   Type, SubType, Offset,  Size, Flags
nvs,      data, nvs,     0x9000,  0x6000,
phy_init, data, phy,     0xf000,  0x1000,
factory,  app,  factory, 0x10000, 1M,
storage,  data, fat,     ,        1M,

Определение раздел диска начинается со слова "storage". Измените его размер в столбце Size, например на 8 мегабайт. Убедитесь, что общий объём всех разделов не превышает размер вашей Flash-памяти:

storage,  data, fat,     ,        8M,

Убедитесь, что в конфигурации проекта используется этот файл. В menuconfig (раздел Partition Table) должна быть выбрана опция Custom partition table CSV и указан путь к вашему файлу partitions.csv.

ESP IDF composite USB device fig03

ESP IDF composite USB device fig04

[Адаптация кода программы]

В файле main.c, как правило, есть код, который инициализирует диск, используя этот раздел.

Проверьте функцию инициализации. Найдите место, где вызывается функция монтирования или инициализации диска (например, tinyusb_msc_storage_mount). В параметрах часто указывается метка раздела, которую вы ищете.

    const tinyusb_msc_storage_config_t storage_cfg = {
// Начальная точка монтирования для приложения (APP):
.mount_point = TINYUSB_MSC_STORAGE_MOUNT_APP,
.medium.wl_handle = wl_handle,
.fat_fs = {
// Задаваемый пользователем базовый путь ("/usb"):
.base_path = BASE_PATH,
},
};

ESP_ERROR_CHECK(tinyusb_msc_new_storage_spiflash(&storage_cfg, &storage_hdl));

Проверьте callback-функции. В коде должны быть функции, которые TinyUSB вызывает для получения информации о диске. Наиболее важная из них — tud_msc_capacity_cb. В ней размер диска вычисляется, как правило, автоматически на основе размера раздела.

void tud_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size)
{
*block_size = 512; // Обычно 512 байт
// Размер в блоках вычисляется из размера раздела:
*block_count = storage_partition->size / *block_size; }

Изменять эту функцию обычно не нужно, если она использует реальный размер вашего раздела. Добавьте функцию для получения информации о размере USB MSC и вызовите её перед входом в цикл while функции app_main:

static void MSC_info (tinyusb_msc_storage_handle_t storage_hdl)
{
uint32_t sector_count;
uint32_t sector_size;

ESP_ERROR_CHECK(tinyusb_msc_get_storage_capacity(storage_hdl, &sector_count));
ESP_ERROR_CHECK(tinyusb_msc_get_storage_sector_size(storage_hdl, &sector_size));

ESP_LOGI(TAG, "MSC capacity: %lu sectors x %lu bytes = %lu MB",
sector_count, sector_size,
(sector_count * sector_size) / (1024 * 1024)); }

В логе загрузки вы должны увидеть сообщения:

I (24) boot: ESP-IDF v5.5.3 2nd stage bootloader
I (25) boot: compile time Jun 24 2026 08:26:17
I (25) boot: Multicore bootloader
I (25) boot: chip revision: v0.2
I (28) boot: efuse block revision: v1.3
I (31) boot.esp32s3: Boot SPI Speed : 80MHz
I (35) boot.esp32s3: SPI Mode       : DIO
I (39) boot.esp32s3: SPI Flash Size : 16MB
I (43) boot: Enabling RNG early entropy source...
I (47) boot: Partition Table:
I (50) boot: ## Label            Usage          Type ST Offset   Length
I (56) boot:  0 nvs              WiFi data        01 02 00009000 00006000
I (63) boot:  1 phy_init         RF data          01 01 0000f000 00001000
I (69) boot:  2 factory          factory app      00 00 00010000 00100000
I (76) boot:  3 storage          Unknown data     01 81 00110000 00800000
I (82) boot: End of partition table
I (86) esp_image: segment 0: paddr=00010020 vaddr=3c030020 size=0cc44h ( 52292) map
I (102) esp_image: segment 1: paddr=0001cc6c vaddr=3fc92d00 size=033ach ( 13228) load
I (105) esp_image: segment 2: paddr=00020020 vaddr=42000020 size=27cc0h (163008) map
I (137) esp_image: segment 3: paddr=00047ce8 vaddr=3fc960ac size=00368h (   872) load
I (137) esp_image: segment 4: paddr=00048058 vaddr=40374000 size=0ecb4h ( 60596) load
I (154) esp_image: segment 5: paddr=00056d14 vaddr=50000000 size=00020h (    32) load
I (160) boot: Loaded app from partition at offset 0x10000
I (160) boot: Disabling RNG early entropy source...
I (171) cpu_start: Multicore app
I (180) cpu_start: GPIO 44 and 43 are used as console UART I/O pins
I (181) cpu_start: Pro cpu start user code
I (181) cpu_start: cpu freq: 160000000 Hz
I (182) app_init: Application information:
I (186) app_init: Project name:     tusb_composite
I (190) app_init: App version:      1
I (194) app_init: Compile time:     Jun 24 2026 08:26:15
I (199) app_init: ELF file SHA256:  c96497188...
I (203) app_init: ESP-IDF:          v5.5.3
I (207) efuse_init: Min chip rev:     v0.0
I (211) efuse_init: Max chip rev:     v0.99 
I (215) efuse_init: Chip rev:         v0.2
I (219) heap_init: Initializing. RAM available for dynamic allocation:
I (225) heap_init: At 3FC97788 len 00051F88 (327 KiB): RAM
I (230) heap_init: At 3FCE9710 len 00005724 (21 KiB): RAM
I (235) heap_init: At 3FCF0000 len 00008000 (32 KiB): DRAM
I (240) heap_init: At 600FE000 len 00001FE8 (7 KiB): RTCRAM
I (247) spi_flash: detected chip: winbond
I (250) spi_flash: flash io: dio
I (253) sleep_gpio: Configure to isolate all GPIO pins in sleep state
I (259) sleep_gpio: Enable automatic switching of GPIO sleep configuration
I (266) main_task: Started on CPU0
I (276) main_task: Calling app_main()
I (276) example_main: Initializing storage...
I (276) example_main: Initializing wear levelling
W (316) tinyusb_msc_storage: Default MSC event callback called,
 event ID: 0, mount point: 0
W (316) tinyusb_msc_storage: Default MSC event callback called,
 event ID: 1, mount point: 1
I (326) example_main: Reading file
I (326) example_main: Read from file: 'Hello World!'
I (336) example_main: USB Composite initialization
W (336) tusb_desc: No Device descriptor provided, using default.
W (346) tusb_desc: No Full-speed configuration descriptor provided,
 using default.
W (346) tusb_desc: No String descriptors provided, using default.
I (356) tusb_desc: 
┌─────────────────────────────────┐ │ USB Device Descriptor Summary │ ├───────────────────┬─────────────┤ │bDeviceClass │ 239 │ ├───────────────────┼─────────────┤ │bDeviceSubClass │ 2 │ ├───────────────────┼─────────────┤ │bDeviceProtocol │ 1 │ ├───────────────────┼─────────────┤ │bMaxPacketSize0 │ 64 │ ├───────────────────┼─────────────┤ │idVendor │ 0x303a │ ├───────────────────┼─────────────┤ │idProduct │ 0x4001 │ ├───────────────────┼─────────────┤ │bcdDevice │ 0x100 │ ├───────────────────┼─────────────┤ │iManufacturer │ 0x1 │ ├───────────────────┼─────────────┤ │iProduct │ 0x2 │ ├───────────────────┼─────────────┤ │iSerialNumber │ 0x3 │ ├───────────────────┼─────────────┤ │bNumConfigurations │ 0x1 │ └───────────────────┴─────────────┘
I (526) TinyUSB: TinyUSB Driver installed on port 0 I (526) example_main: USB Composite initialization DONE I (536) example_main: MSC capacity: 16224 sectors x 512 bytes = 7 MB W (2716) tinyusb_msc_storage: Default MSC event callback called, event ID: 0, mount point: 1 W (2716) tinyusb_msc_storage: Default MSC event callback called, event ID: 1, mount point: 0 W (4006) tinyusb_msc_storage: tud_msc_scsi_cb() invoked: 161

Иногда параметры в menuconfig могут влиять на работу диска, хотя основной размер всё равно берётся из таблицы разделов.

В разделе Component config → TinyUSB Stack → Massive Storage Class (MSC) убедитесь, что опция CONFIG_TINYUSB_MSC_ENABLED включена.

Проверьте значение CONFIG_TINYUSB_MSC_BUFSIZE. Это размер внутреннего буфера, а не размер самого диска. Важно, чтобы он был не меньше размера сектора (обычно 512 байт), иначе сборка может не пройти. Если используете wear leveling, убедитесь, что размер буфера соответствует размеру сектора WL (обычно 4096 байт).

Важные предостережения. Имейте в виду, что внутренняя Flash-память ESP32-S3 имеет ограниченный ресурс циклов перезаписи. Использование её в качестве постоянно перезаписываемого USB-диска может сократить срок службы микроконтроллера. Для интенсивной записи лучше подумать о внешней памяти (например, SD-карте).

Пересечение областей: убедитесь, что ваш новый раздел для диска не пересекается с разделами приложения (app0, app1), данными NVS и другими важными областями. Ошибка в таблице разделов может сделать прошивку неработоспособной.

Утилита Disks показывает, что на диске 8 мегабайт присутствует раздел 1 мегабайт:

ESP IDF composite USB device fig06

Проблема с размером FAT-раздела в 1 МБ обычно возникает, когда система использует старый образ файловой системы, сохраненный во Flash-памяти, а не форматирует раздел заново при подключении.

[Почему размер остался прежним?]

Дело в том, что FAT-файловая система создается не в коде, а форматируется на Flash-памяти. Когда вы изменили размер раздела в файле partitions.csv, физическое место для диска увеличилось, но данные старой файловой системы FAT (1 МБ) остались на этом месте. Поэтому ваш компьютер видит новое, большее "устройство", но внутри него находится старая, меньшая по размеру файловая система.

Как это исправить: принудительное переформатирование. Вам нужно заставить ESP32 заново отформатировать раздел storage с учетом его нового размера. Это достигается принудительным вызовом форматирования при инициализации. Самый простой и надежный способ — установить флаг format_if_mount_failed в true.

В коде, где вы монтируете FAT-раздел (скорее всего, это функция esp_vfs_fat_spiflash_mount или подобная), убедитесь, что эта опция включена:

esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = true, // <-- br=""> .max_files = 5,
.allocation_unit_size = 4096 // Рекомендуется для Flash-памяти };

Важный момент: этот флаг сработает только один раз, если монтирование не удастся (например, из-за того, что файловая система повреждена или не соответствует новому размеру). После успешного форматирования флаг проигнорируется, и обычное монтирование пройдет без проблем.

Если этот метод не сработает, можно использовать более прямой подход: вызвать функцию f_mkfs из библиотеки FatFS [2] для принудительного создания файловой системы на разделе перед его монтированием. Это часто помогает, когда format_if_mount_failed по каким-то причинам не активируется.

[Что делать, если это не помогло?]

Если после переформатирования проблема сохранится, вероятнее всего, проект использует не тот файл partitions.csv при сборке. Это частая причина, когда изменения в файле игнорируются.

Вот обязательные шаги для проверки:

1. Очистите и пересоберите проект: это удалит все кэшированные файлы, включая старую таблицу разделов.

idf.py fullclean
idf.py build

Это самый важный шаг, так как VSCode или другие IDE могут не заметить изменение CSV-файла.

2. Проверьте настройки в menuconfig: убедитесь, что выбран ваш файл partitions.csv.

Зайдите в `Partition Table`.
Выберите `Custom partition table CSV`.
В поле `Custom partition table CSV file` укажите имя вашего файла (обычно partitions.csv).

3. Проверьте логи при загрузке: после перепрошивки посмотрите вывод в idf.py monitor. В начале загрузки будет выведена таблица разделов. Убедитесь, что в ней размер раздела storage отображается как 8 МБ. Это лучшее доказательство того, что сборка использует правильную конфигурацию.

После того как ESP32 успешно отформатирует раздел с новым размером, ваш компьютер должен увидеть диск объемом примерно 8 МБ.

Отформатируйте диск стандартными утилитами операционной системы.

Медленное копирование файлов на USB MSC через ESP32-S3 — это известное ограничение, и причины кроются как в аппаратной части, так и в программной реализации. Из-за этой проблемы обычно нет смысла делать размер диска USB MSC на запись больше 1 мегабайта - процесс записи оказывается неприемлемо долгим.

Давайте разберем основные факторы этой проблемы.

[Основные причины медленной работы]

1. Ограниченная скорость USB Full-Speed (12 Мбит/с)

ESP32-S3 поддерживает только USB Full-Speed (12 Мбит/с), режим High-Speed (480 Мбит/с) недоступен.

- Теоретический максимум: ~1.2 МБ/с.
- Реальная скорость с учетом накладных расходов: обычно 300-600 КБ/с.

Это аппаратное ограничение, и его нельзя обойти программно.

2. Медленная запись во Flash-память

Внутренняя SPI Flash ESP32-S3 имеет ограниченную скорость записи:

- Типичная скорость записи: 200-400 КБ/с.
- Стирание блоков (erase): перед записью Flash нужно стирать целые сектора (обычно 4 КБ), что создает дополнительные задержки

Когда вы копируете файл, происходит следующее:

- Компьютер отправляет данные по USB (быстро).
- ESP32 принимает данные в буфер.
- ESP32 стирает блок Flash (медленно, ~50-100 мс на блок 4 КБ).
- ESP32 записывает данные в Flash (медленно).
- ESP32 подтверждает запись компьютеру.

Именно этап стирания/записи во Flash является главным "тормозом".

3. Использование SPIFFS или FAT с включенной функцией wear leveling

Если вы используете wear leveling (выравнивание износа flash) для продления срока службы Flash, это добавляет дополнительный слой абстракции:

- Каждая операция записи преобразуется в несколько операций чтения/записи.
- Это может снизить скорость еще на 30-50%.

4. Маленький буфер TinyUSB

В настройках menuconfig есть параметр:

Component config → TinyUSB Stack → Massive Storage Class (MSC) → MSC buffer size

По умолчанию он может быть всего 512 байт или 4096 байт. Это означает, что данные передаются маленькими порциями, увеличивая количество транзакций и накладные расходы.

5. Отсутствие кэширования записи (write caching)

В отличие от обычных USB-флешек, у ESP32 нет аппаратного кэша. Каждая операция записи сразу идет во Flash, без возможности группировки и оптимизации.

Типичные скорости для разных конфигураций:

Конфигурация Скорость записи
USB MSC + FAT + SPI Flash (без WL) ~200-400 КБ/с
USB MSC + FAT + SPI Flash (с WL) ~100-250 КБ/с
USB MSC + SPIFFS ~50-150 КБ/с
USB MSC + SD-карта (SPI) ~300-600 КБ/с

[Как ускорить копирование?]

1. Увеличьте буфер MSC

В menuconfig:

Component config → TinyUSB Stack → Massive Storage Class (MSC) → MSC buffer size

Установите значение 4096 или 8192 байт. Это уменьшит количество транзакций USB.

2. Отключите wear leveling (если допустимо)

Если вы готовы пожертвовать долговечностью Flash ради скорости, отключите wear leveling. В коде используйте прямую работу с FAT на разделе.

3. Используйте внешнюю SD-карту

SD-карта, подключенная по SPI, может работать быстрее, чем внутренняя Flash, особенно если использовать режим SDMMC (если доступен).

4. Включите кэширование записи в FatFS

Добавьте флаг FS_FS_TINY или настройте буферы в ffconf.h:

#define FF_FS_TINY 0   // Использовать стандартные буферы (медленнее, но надежнее)
#define FF_MAX_SS 512 // Размер сектора
#define FF_USE_MKFS 1

5. Используйте собственный протокол вместо MSC

Если скорость критична, рассмотрите возможность реализации собственного протокола поверх USB-CDC:

- Передавайте файл большими блоками (например, по 64 КБ).
- Записывайте данные напрямую во Flash, минуя файловую систему.
- Используйте сжатие или отключайте подтверждение каждого блока.

Такой подход может дать скорость 500-800 КБ/с.

6. Настройте размер кластера FAT

При форматировании FAT-раздела используйте больший размер кластера (например, 32 КБ или 64 КБ). Это уменьшит количество операций записи и ускорит копирование больших файлов.

В коде:

esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = true,
.max_files = 5,
.allocation_unit_size = 32768 // 32 КБ };

Краткий вывод. Медленное копирование — это норма для ESP32-S3 в режиме USB MSC. Если скорость 200-400 КБ/с — это стандартный результат. Увеличение буфера и настройка размера кластера могут немного улучшить ситуацию, но кардинально (до 1 МБ/с+) ускорить запись во внутреннюю Flash невозможно из-за аппаратных ограничений.

Если вам нужна скорость выше 1 МБ/с, единственный реальный путь — использовать внешнюю Flash с Quad-SPI или SD-карту в SDMMC-режиме.

[Альтернативный подход: TinyUF2]

Если ваша цель — просто обновлять прошивку или загружать файлы, можно рассмотреть компонент esp_tinyuf2. Он использует специальный формат UF2 и позволяет легко задать размер виртуального диска в конфигурации menuconfig (опция USB Virtual Disk size(MB)). Однако это несколько иной подход, чем реализация чистого MSC-диска с произвольным доступом к данным.

[Восстановление функционала монитора на классе USB CDC]

По умолчанию монитор работает через UART0, выводы портов GPIO:

GPIO43 TXD
GPIO44 RXD

Поэтому все сообщения макросов ESP_LOGI, ESP_LOGW, ESP_LOGE выводятся через UART0. Однако есть возможность перенаправить вывод сообщений через USB CDC.

Запустите idf.py menuconfig, в разделе Component config → ESP System Settings → Channel for console output выберите USB Serial/JTAG Controller.

ESP IDF composite USB device fig05

Добавьте подключение заголовка в основной файл, где находится функция app_main:

#include "tinyusb_console.h"

После вызова функции tinyusb_cdcacm_init добавьте вызов функции инициализации консоли tinyusb_console_init:

    ESP_ERROR_CHECK(tinyusb_cdcacm_init(&acm_cfg));
/* Второй способ зарегистрировать callback-функцию */
ESP_ERROR_CHECK(tinyusb_cdcacm_register_callback(
TINYUSB_CDC_ACM_0,
CDC_EVENT_LINE_STATE_CHANGED,
&tinyusb_cdc_line_state_changed_callback));

ESP_LOGI(TAG, "USB Composite initialization DONE");
tinyusb_console_init(TINYUSB_CDC_ACM_0);

Перекомпилируйте и прошейте проект, как обычно.

idf.py flash monitor -p /dev/ttyACM0

Теперь сообщения лога будут выводиться через USB CDC.

При передаче символов в USB CDC появляются ошибки (введенный символ показан между палочками '|'):

I (1766121) example_main: Data from channel 0:
I (1766131) example_main: 0x3fc999c0   0d                                     |.|
E (1766131) example_main: CDC ACM write flush error: ESP_ERR_NOT_FINISHED I (1885411) example_main: Data from channel 0: I (1885411) example_main: 0x3fc999c0 66 |f|
fE (1885411) example_main: CDC ACM write flush error: ESP_ERR_NOT_FINISHED I (1885561) example_main: Data from channel 0: I (1885561) example_main: 0x3fc999c0 73 |s| sI (1885581) example_main: Data from channel 0: I (1885581) example_main: 0x3fc999c0 64 |d|
dE (1885581) example_main: CDC ACM write flush error: ESP_ERR_NOT_FINISHED I (1885691) example_main: Data from channel 0: I (1885691) example_main: 0x3fc999c0 6c |l|
lE (1885691) example_main: CDC ACM write flush error: ESP_ERR_NOT_FINISHED

Обработка событий USB CDC происходит в главном цикле while функции app_main:

    ...
while (1) {
if (xQueueReceive(app_queue, &msg, portMAX_DELAY)) {
if (msg.buf_len) {
/* Печать принятых данных */
ESP_LOGI(TAG, "Data from channel %d:", msg.itf);
ESP_LOG_BUFFER_HEXDUMP(TAG,
msg.buf,
msg.buf_len,
ESP_LOG_INFO);

/* Запись этих данных обратно */
tinyusb_cdcacm_write_queue(msg.itf, msg.buf, msg.buf_len);
esp_err_t err = tinyusb_cdcacm_write_flush(msg.itf, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "CDC ACM write flush error: %s",
esp_err_to_name(err));
}
}
}
}
...

Проблема в том, что tinyusb_cdcacm_write_flush с таймаутом 0 может не успеть завершить передачу. Нужно увеличить таймаут или проверять состояние.

Основная проблема ESP_ERR_NOT_FINISHED — нужно либо использовать portMAX_DELAY для таймаута flush, либо ждать готовности endpoint.

Вариант решения проблемы с использованием portMAX_DELAY для вызова tinyusb_cdcacm_write_flush:

    while (1) {
if (xQueueReceive(app_queue, &msg, portMAX_DELAY)) {
if (msg.buf_len) {
ESP_LOGI(TAG, "Data from channel %d (%lu bytes):",
msg.itf,
(unsigned long)msg.buf_len);
ESP_LOG_BUFFER_HEX(TAG, msg.buf, msg.buf_len);
tinyusb_cdcacm_write_queue(msg.itf, msg.buf, msg.buf_len);
tinyusb_cdcacm_write_flush(msg.itf, portMAX_DELAY);
}
}
}

[Ссылки]

1. Установка среды разработки ESP-IDF для ESP32.
2. Библиотека FatFS: FATFS.