Я продолжаю освещать работу с USB на Raspberry Pi Pico. В текущей статье хочу привести пример, как можно использовать Raspberry Pi Pico в качестве загрузочного USB-устройства.

Загрузиться можно с USB-диска или USB CD-ROM / USB DVD-ROM. Загрузка c DVD или CD хотя и является устаревшей технологией, но более интересная в плане изучения. У меня уже была статья, где я рассматривал структуру таких дисков.

Также у меня была статья, посвящённая эмуляции CD и DVD при помощи Raspberry Pi Zero 2 W. Но там рассматривалась тема эмуляции с использованием подсистемы Linux USB gadget, скриптов на Bash, systemd.

Практическая ценность сегодняшнего проекта сама по себе невелика. Однако он интересен тем, что позволяет на реальном примере изучить работу USB Mass Storage. Поэтому основная ценность заключается не в конечном устройстве, а в технологиях и подходах, которые используются при его создании.

Одним из режимов работы USB-устройства является его работа как USB Mass Storage. Если не вдаваться в подробности, то одно устройство (хост) может посылать другому (USB Mass Storage) запросы на чтение и сохранение блоков данных. Блоки данных имеют фиксированный размер, как правило, 512, 2048 и 4096 байт и адресуются при помощи LBA-адресации. В случае CD или DVD работают только запросы на чтение данных.

USB

Если вы знаете и понимаете терминологию в USB, то можете смело пропустить этот раздел.

USB — обширная тема, которой посвящены целые книги. Для разработки устройства нам нужны лишь базовые знания.

USB-устройство участвует в двух независимых моделях взаимодействия:

  • логической (передача данных) — роли Host и Device,

  • энергетической (питание) — роли Source и Sink.

USB имеет несколько ревизий, отличающихся среди прочего максимальной скоростью передачи данных. В Raspberry Pi Pico используется USB 1.1 с максимальной скоростью 12 Мбит/c (Full speed). В реальных сценариях использования скорость передачи полезных данных ещё ниже. Но для исследования и получения навыков работы с USB этого вполне достаточно.

Несмотря на наличие одного физического соединения, USB поддерживает несколько типов логического обмена данными: Control, Bulk, Interrupt и Isochronous. Каждый из них предназначен для решения своих задач и обладает различными характеристиками по задержкам, пропускной способности и надёжности доставки.

  • Control — управляющие запросы (обязательны для всех устройств).

  • Bulk — передача больших объёмов данных с контролем целостности.

  • Interrupt — небольшие данные с гарантированным временем опроса.

  • Isochronous — потоковые данные с гарантированной полосой пропускания.

Назначение USB-устройства определяется его классом. Именно класс сообщает операционной системе, следует ли рассматривать устройство как накопитель, клавиатуру, сетевой адаптер или устройство другого типа.

Обмен данными в USB логически осуществляется через конечные точки (Endpoints). Каждая конечная точка представляет собой канал передачи данных с определёнными характеристиками и назначением.

У любого USB-устройства существует специальная конечная точка EP0. Она используется для управляющих передач (Control Transfer), необходимых для обнаружения, идентификации и настройки устройства. Через EP0 хост получает дескрипторы устройства и выполняет стандартные USB-запросы.

USB-интерфейсы группируют конечные точки с классами устройств, позволяя создавать мультифункциональные устройства. В простейшем случае используется один интерфейс.

Конфигурация USB определяет набор интерфейсов и конечных точек, которые устройство предоставляет хосту. После обнаружения устройства хост выбирает одну из доступных конфигураций и переводит устройство в соответствующий режим работы.

Mass Storage Class

Флешки, USB CD-ROM относятся к классу Mass Storage Class (MSC). Эти устройства содержат в себе интерфейс с данным классом.

Хост и устройство общаются с использованием протокола Bulk-Only Transport (BOT), который не использует точки входа Interrupt Transfer и Сontrol Transfer.

Внутри сообщений BOT используются те же структуры данных, что и в SCSI, только они оборачиваются Command Block Wrapper (CBW) и Command Status Wrapper (CSW).

Существует ещё более современный протокол USB Attached SCSI Protocol (UASP), но он используется в высокоскоростных устройствах USB 3.0 и в статье я его не рассматриваю.

В USB Mass Storage по протоколу BOT SCSI-команды передаются внутри CBW-пакетов.

Чтобы BIOS или UEFI отличали USB-диск (жёсткий диск, флешку, дисковод для гибких магнитных дисков) от CD-ROM (CD-ROM, DVD-ROM) при получении информации об устройстве при помощи запроса INQUIRY должен возвращаться корректный Periferial Device Type (PDT):

  • 0x00 — Direct Access Device,

  • 0x05 — CDROM.

Одно USB Mass Storage устройство может содержать несколько LUN.

LUN (Logical Unit Number) — это номер логического устройства внутри одного MSC-устройства. В нашем проекте используется одно LUN.

MassStorage на Pico

При разработке на C с использованием Raspberry Pi Pico SDK USB-стек (TinyUSB) подключается через CMakeLists.txt путём добавления соответствующих библиотек и зависимостей. Дополнительная конфигурация стека выполняется в файле конфигурации tusb_config.h, где задаются параметры классов USB и режимы работы устройства.

Для того чтобы устройство работало как USB Mass Storage необходимо реализовать несколько callback-функций:

  • tud_msc_test_unit_ready_cb,

  • tud_msc_inquiry_cb,

  • tud_msc_capacity_cb,

  • tud_msc_is_writable_cb,

  • tud_msc_read10_cb,

  • tud_msc_write10_cb,

  • tud_msc_scsi_cb.

Несмотря на то что устройство подключено по USB, обмен с ним осуществляется с помощью команд протокола SCSI, инкапсулированных в USB Mass Storage. Именно поэтому TinyUSB предоставляет обработчик tud_msc_scsi_cb, через который можно реализовать поддержку нестандартных или дополнительных команд.

В моих экспериментах Linux и UEFI корректно распознавали устройство даже без реализации дополнительных SCSI-команд. Windows оказалась более требовательной и для корректного определения CD-ROM пришлось добавить обработчики нескольких дополнительных команд. С macOS добиться стабильного распознавания устройства пока не удалось.

При изучении TinyUSB выяснилось, что реализация Mass Storage ориентирована в первую очередь на эмуляцию обычных дисков. Тип периферийного устройства, возвращаемый в ответ на SCSI-команду INQUIRY, задавался как Direct Access Device (0x00), что соответствует жёсткому диску или флеш-накопителю. Для корректного распознавания CD-ROM необходимо возвращать тип CD/DVD Device (0x05), поэтому пришлось применить патч к библиотеке.

Сборка отладочного стенда

В Raspberry Pi Pico часто используется его единственный USB-порт для вывода отладочной информации. Но это не наш случай, так как USB-порт будет использоваться эмулятором USB CDROM.

Само устройство для своей работы не требует ничего, кроме Raspberry Pi Pico W и microUSB кабеля, но для удобства разработки и отладки я собрал небольшую схему на плате для прототипирования. Схема приведена ниже.

Схема отладочного стенда
Схема отладочного стенда

Для второго Pico, используемого для отладки, нужно загрузить и установить прошивку отсюда.

Реализация эмулятора CD-ROM

Чтобы устройство корректно распознавалось как CD-ROM, необходимо пропатчить библиотеку TinyUSB (файл $PICO_SDK_PATH/lib/tinyusb/src/class/msc/msc_device.c).

Интересно, почему до сих пор не была добавлена поддержка USB CD-ROM в эту библиотеку. Вероятно, потому что CD-ROM считается устаревшим устройством.

Я патчил библиотеку следующим образом:

  1. В свой проект добавил библиотеку TinyUSB версии 0.18.0, которая используется в PicoSDK 2.2.0:

    git subtree add \
       --prefix=lib/tinyusb \
       https://github.com/hathach/tinyusb.git \
       0.18.0 \
       --squash
    
  2. В файле CMakeLists.txt своего проекта перед вызовом pico_sdk_init() добавил инструкцию, что искать библиотеку TinyUSB нужно в директории моего проекта:

    set(PICO_TINYUSB_PATH "${CMAKE_CURRENT_LIST_DIR}/lib/tinyusb/")
    
  3. В файле lib/tinyusb/src/class/msc/msc_device.c между

    case SCSI_CMD_INQUIRY: {
          scsi_inquiry_resp_t inquiry_rsp =
          {
            .is_removable = 1,
            .version = 2,
            .response_data_format = 2,
            .additional_length = sizeof(scsi_inquiry_resp_t) - 5,
          };
    

    и

          // vendor_id, product_id, product_rev is space padded string
          memset(inquiry_rsp.vendor_id  , ' ', sizeof(inquiry_rsp.vendor_id));
          memset(inquiry_rsp.product_id , ' ', sizeof(inquiry_rsp.product_id));
          memset(inquiry_rsp.product_rev, ' ', sizeof(inquiry_rsp.product_rev));
    
    

    добавил:

    if (lun == 0) {
             #ifdef CFG_TUD_MSC_LUN0_PDT
             inquiry_rsp.peripheral_device_type = CFG_TUD_MSC_LUN0_PDT;
             #endif
          } else if (lun == 1) {
             #ifdef CFG_TUD_MSC_LUN1_PDT
             inquiry_rsp.peripheral_device_type = CFG_TUD_MSC_LUN1_PDT;
             #endif
          }
    
  4. Для LUN0 указал PDT в файле tusb_config.h :

    #define CFG_TUD_MSC_LUN0_PDT 0x05
    

    Используется только LUN0, поэтому этой строчки будет достаточно.

  5. В файле конфигурации необходимо прописать, что класс устройства MSC и размер буфера 2048 байта, чтобы весь сектор CD-диска (2048 байт) помещался в буфер

    #define CFG_TUD_MSC 1 
    #define CFG_TUD_MSC_BUFSIZE 2048 // Standard USB MSC block size
    
  6. Добавил информацию о дескрипторах в файле usb_descriptors.c

    #include "tusb.h"
    
    #define USB_PID 0x0121
    #define USB_VID 0x2209
    #define USB_BCD 0x0200
    //--------------------------------------------------------------------+
    // Device Descriptor
    //--------------------------------------------------------------------+
    
    tusb_desc_device_t const desc_device =
    {
        .bLength            = sizeof(tusb_desc_device_t),
        .bDescriptorType    = TUSB_DESC_DEVICE,
        .bcdUSB             = USB_BCD,
        .bDeviceClass       = 0x00,
        .bDeviceSubClass    = 0x00,
        .bDeviceProtocol    = 0x00,
        .bMaxPacketSize0    = CFG_TUD_ENDPOINT0_SIZE,
        .idVendor           = USB_VID,
        .idProduct          = USB_PID,
        .bcdDevice          = 0x0100,
        .iManufacturer      = 0x01,
        .iProduct           = 0x02,
        .iSerialNumber      = 0x03,
        .bNumConfigurations = 0x01
    };
    
    uint8_t const* tud_descriptor_device_cb(void)
    {
        return (uint8_t const*) &desc_device;
    }
    
    //--------------------------------------------------------------------+
    // Configuration Descriptor
    //--------------------------------------------------------------------+
    
    enum
    {
        ITF_NUM_MSC = 0,
        ITF_NUM_TOTAL
    };
    
    #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MSC_DESC_LEN)
    
    uint8_t const desc_configuration[] =
    {
        TUD_CONFIG_DESCRIPTOR(
            1,
            ITF_NUM_TOTAL,
            0,
            CONFIG_TOTAL_LEN,
            0,
            100),
        TUD_MSC_DESCRIPTOR(
            ITF_NUM_MSC,
            0,
            0x81,   // EP IN
            0x02,   // EP OUT
           64)
    };
    
    uint8_t const* tud_descriptor_configuration_cb(uint8_t index)
    {
        (void) index;
        return desc_configuration;
    }
    
    //--------------------------------------------------------------------+
    // String Descriptors
    //--------------------------------------------------------------------+
    
    #define USB_STR_DESC(name, val)                                     \
    struct {                                                            \
        uint8_t len;                                                    \
        uint8_t type;                                                   \
        uint16_t arr[sizeof(u##val)/2 - 1];                             \
    } _##name = {                                                       \
        .len  = sizeof(u##val),                                         \
        .type = 0x03,                                                   \
        .arr  = u##val                                                  \
    };                                                                  \
    
    uint16_t const* const name = (uint16_t*)&_##name
    uint16_t lang[] = {4, 0x03, 0x0409};
    
    USB_STR_DESC(manufacturer, "Home Ltd.");
    USB_STR_DESC(product, "USB CD-ROM");
    USB_STR_DESC(serial, "12345678");
    
    const uint16_t* string_desc_arr[] =
    {
        lang,
        manufacturer,
        product,
        serial
    };
    
    uint16_t const* tud_descriptor_string_cb(uint8_t index, uint16_t langid)
    {
        (void) langid;
        return string_desc_arr[index];
    }
    
  7. Реализовал необходимые callback-функции для обработки SCSI-команд:

    #include <string.h>
    
    #include "image.h"
    #include "tusb.h"
    
    #define DISK_BLOCK_SIZE 2048
    #define DISK_BLOCK_COUNT (sizeof(data) / 2048)
    
    bool tud_msc_test_unit_ready_cb(uint8_t lun) {
      (void)lun;
      return true;
    }
    
    void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8],
                            uint8_t product_id[16], uint8_t product_rev[4]) {
      (void)lun;
    
      memcpy(vendor_id, "PICO    ", 8);
      memcpy(product_id, "USB CDROM      ", 16);
      memcpy(product_rev, "1.0 ", 4);
    }
    
    void tud_msc_capacity_cb(uint8_t lun, uint32_t* block_count,
                             uint16_t* block_size) {
      (void)lun;
      *block_count = DISK_BLOCK_COUNT;
      *block_size = DISK_BLOCK_SIZE;
    }
    
    bool tud_msc_is_writable_cb(uint8_t lun) {
      (void)lun;
      return false;
    }
    
    int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset,
                              void* buffer, uint32_t bufsize) {
      (void)lun;
      printf("lun=%u lba=%lu offset=%lu bufsize=%lu\n", lun, (unsigned long)lba,
             (unsigned long)offset, (unsigned long)bufsize);
      memcpy(buffer, data + lba * DISK_BLOCK_SIZE + offset, bufsize);
      return bufsize;
    }
    
    int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset,
                               uint8_t* buffer, uint32_t bufsize) {
     (void)lun;
      (void)lba;
      (void)offset;
      (void)buffer;    
      return -1;
    }
    
    int32_t tud_msc_scsi_cb(uint8_t lun, uint8_t const scsi_cmd[16], void* buffer,
                            uint16_t bufsize) {
      (void)lun;
      void const* response;
      int32_t resplen = 0;
      printf("SCSI cmd = 0x%02X, len=%u\n", scsi_cmd[0], bufsize);
    
      switch (scsi_cmd[0]) {
        case 0x43: {  // READ TOC
          static uint8_t toc[20] = {
              0x00, 0x12, 0x01, 0x01, 0x00, 0x14, 0x01, 0x00, 0x00, 0x00,
              0x00, 0x00, 0x00, 0x14, 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00,
          };
          uint32_t blocks = DISK_BLOCK_COUNT;
          toc[16] = (blocks >> 24) & 0xFF;
          toc[17] = (blocks >> 16) & 0xFF;
          toc[18] = (blocks >> 8) & 0xFF;
          toc[19] = blocks & 0xFF;
          response = toc;
          resplen = sizeof(toc);
          break;
        }
        case 0x46: {  // GET CONFIGURATION
          static const uint8_t cfg[8] = {
              0x00, 0x00, 0x00, 0x04,
              0x00, 0x00, 0x00, 0x08,  // current profile: CD-ROM
          };
          response = cfg;
          resplen = sizeof(cfg);
          break;
        }
        case 0x4A: {  // GET EVENT STATUS NOTIFICATION
          static const uint8_t evt[8] = {
              0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
          };
          response = evt;
          resplen = sizeof(evt);
          break;
        }
        case 0x51: {  // READ DISC INFORMATION
          static const uint8_t disc[34] = {
             0x00, 0x20, 0x0E, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00,
             0x00, 0x00, 0x00, 0,    0,    0,    0,    0,    0,
             0,    0,    0,    0,    0,    0,    0,    0,    0,
             0,    0,    0,    0,    0,    0,    0,
          };
          response = disc;
          resplen = sizeof(disc);
          break;
         }
        default:
          tud_msc_set_sense(lun, SCSI_SENSE_ILLEGAL_REQUEST, 0x20, 0x00);
          return -1;
       }
    
      if (resplen > bufsize) resplen = bufsize;
      if (response && buffer && resplen > 0) memcpy(buffer, response, resplen);
      return resplen;
    }
    
    
  8. Внедрил образ диска в исходный код командой:

    xxd -i image.iso > image.h
    

    В качестве примера я использовал образ, который я создавал в статье по ISO-образам.

    В сгенерированном заголовочном файле для переменной, хранящей образ в виде массива байтов, необходимо добавить спецификатор const, чтобы при работе программы образ находился на флеш, а не загружался в оперативную память, которой всего 130 КБ.

С целью упрощения реализации всё содержимое эмулируемого CD-ROM я храню на SPI Flash чипе Raspberry Pi Pico. Что накладывает ограничение на максимальный размер образа CD-ROM. Если используется оригинальный Raspberry Pi Pico вся программа и образ вместе не должны превышать размер в 2 МБ. В Raspberry Pi Pico 2 — 4 МБ, некоторых китайских клонах — 16 МБ.

Логика работы tud_msc_read10_cb проста: необходимо просто скопировать запрашиваемую часть массива в предоставляемый буфер.

Просмотр информации об USB-устройствах

Команды Linux позволяет узнать много информации об USB-устройствах, подключённых к хосту.

  1. Посмотреть список всех подключённых USB-устройств:

    lsusb
    
  2. Узнать информацию об USB-устройстве, зная его VendorID и ProductID

    sudo lsusb -v -d <VendorID>:<ProductID>
    
  3. Узнать информацию об USB-устройстве, зная его VendorID и ProductID

    sudo lsusb -v -s <BusID>:<DeviceID>
    

Подробная информация включает вывод дескрипторов: устройства, конфигурации, интерфейсов, конечных точек в виде иерархического дерева. Иногда чтение этой информации может помочь с пониманием причины некорректной работы устройства.

Отладка USB с использованием Wireshark

Иногда удобно посмотреть, что реально передаётся по USB соединению. Можно посмотреть фактический сигнал с использованием логического анализатора или осциллографа, но часто достаточно посмотреть USB-трафик на уровне ядра операционной системы при помощи Wireshark.

В Linux, чтобы Wireshark нашёл интерфейс, с которого он мог перехватывать USB-пакеты, необходимо загрузить модуль usbmon

sudo modprobe usbmon
sudo wireshark

в появившемся окне выбрать нужный интерфейс.

Не забудьте выгрузить модуль usbmon после того, как вам станет ненужным перехват USB-трафика.

Ссылки

  1. Проект pico-usb-cdrom на GitHub.

  2. Статья о загрузочных ISO-образам.

  3. Статья об эмуляции CD-ROM на Raspberry Pi Zero 2 W.

  4. Universal Serial Bus Mass Storage Class: Bulk-Only Transport.

  5. Universal Serial Bus Mass Storage Specification For Bootability.

Заключение

Несмотря на простоту устройства, проект позволяет познакомиться сразу с несколькими важными технологиями: архитектурой USB, классом Mass Storage, протоколом SCSI и особенностями процесса загрузки операционной системы. Эти знания пригодятся не только при работе с Raspberry Pi Pico, но и при разработке любых USB-устройств.

Проект можно дальше развивать. Например, поместить на Raspberry Pi Pico загрузочный образ Linux. Для этого понадобится китайский клон Raspberry Pi Pico с SPI Flash на 16 МБ. Как я ни старался сделать Linux минимальным, 2МБ флеша явно недостаточно.

Можно попробовать сделать эмуляцию CD ROM и загрузку его содержимого из сети по HTTP или HTTPS.

© 2026 ООО «МТ ФИНАНС»

Комментарии (5)


  1. dlinyj
    09.06.2026 13:05

    Очень прикольная статья. Спасибо!

    Как вообще впечатления от этого контроллера? У меня постоянно были какие-то грабли, чем меня бесил и я так и не осилил его. Помню что пытался во многопоточку, нифига не получилось. Хотя всё очевидно же было.


    1. artyomsoft Автор
      09.06.2026 13:05

      Нормально. Особенно удобно с плагином для VS Code и вторым пико для дебага и заливки. С многопоточностью не работал на нем. Только использовал два ядра в своем эмуляторе "Ну, погоди!". Для многопоточности нужно использовать FreeRTOS или ZephyrOS, как я понимаю.


      1. dlinyj
        09.06.2026 13:05

        Не, там если открыть мануал, то видно что там два ядра и можно пустить в двух потоках. Что удобно, для опроса кнопок и вывода одно ядро, я для обработки другое. Я так для эмулятора тетриса делал. Но сломался.


        1. artyomsoft Автор
          09.06.2026 13:05

          Я так и делал в Ну, Погоди!. Я думал вопрос был в многопоточности на одном ядре.


          1. dlinyj
            09.06.2026 13:05

            Класс. Но я не осилил.