В одной из своих статей я уже рассказывал, как из Raspberry Pi Pico сделать USB CD-ROM. Так как размер флеш-памяти Pico ограничен, поместить что-то серьёзное в образ, который будет находиться во флеш-памяти невозможно. Можно попробовать разместить образ на флеш-карте и использовать модуль для неё, но я нашёл более интересное с точки зрения используемых технологий решение. Эмулировать образ, который находится в интернете. Образа многих дистрибутивов Linux доступны для загрузки с сайта.

Raspberry Pi Pico не имеет сетевого интерфейса, а вот Pico W оснащена модулем Wi-Fi, благодаря которому можно получать данные напрямую с HTTP-сервера. Если реализовать чтение ISO-образа не из локальной памяти, а через сеть, то ограничение в 2 МБ флеш-памяти практически исчезает. Во внутренней памяти устройства потребуется хранить только прошивку и параметры подключения к сети.

Идея довольно проста. Компьютер обращается к виртуальному CD-ROM, отправляя команды чтения определённых секторов. Вместо того чтобы извлекать их из локального файла, микроконтроллер определяет смещение внутри ISO-образа и выполняет HTTP-запрос с заголовком Range, получая только необходимые байты.

Что потребуется

Для реализации проекта понадобится совсем немного оборудования:

  • Raspberry Pi Pico W,

  • USB-кабель,

  • точка доступа Wi-Fi,

  • любой HTTP-сервер, на котором размещён ISO-образ.

В качестве сервера можно использовать как собственный веб-сервер, так и официальный сайт дистрибутива Linux, если сервер поддерживает HTTP Range Requests.

Как всё работает

Укрупнённый алгоритм выглядит следующим образом:

  1. Компьютер отправляет команду READ(10).

  2. TinyUSB вызывает callback чтения.

  3. Pico вычисляет смещение внутри ISO.

  4. Отправляется HTTP GET с Range.

  5. Полученные 2048 байт копируются в USB-буфер.

  6. TinyUSB возвращает данные компьютеру.

HTTP

Хотя сейчас везде используется HTTPS, я, чтобы не усложнять статью и ухудшать производительность программы, использовал HTTP.

HTTP Range

Например, сектор CD-ROM имеет размер 2048 байт. Если операционная система запрашивает сектор с номером 12345, то необходимо получить диапазон:

12345 × 2048 = 25282560

Следовательно, HTTP-запрос будет выглядеть примерно так:

GET /debian.iso HTTP/1.1
 Host: example.org
 Range: bytes=25282560-25284607

Если сервер поддерживает частичную выдачу файлов (HTTP Range Requests), он вернёт только один нужный сектор вместо передачи всего ISO-файла объёмом несколько гигабайт.

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

curl -I https://example.org/image.iso

Если в ответе присутствует строка:

Accept-Ranges: bytes

то сервер подходит для использования.

Изначально HTTP подразумевал, что TCP-соединение открывалось и закрывалось для каждого HTTP-запроса. Это добавляет дополнительную нагрузку на сервер и неэффективно для получения секторов из ISO-образа. В HTTP есть так называемые keep-alive запросы, которые отсылаются в пределах открытого соединения.

Keep-alive запросы

Для того чтобы отсылать несколько запросов, не создавая соединение для каждого запроса, необходимо при отсылке запроса передавать заголовок:

Connection: keep-alive

Поддержка keep-alive запросов может различаться от сервера к серверу. Например, на сервере с дистрибутивами Ubuntu на момент написания статья было ограничение по количеству запросов, которые можно отослать в пределах одного соединения (100 или 10 запросов), после чего сервер закрывал TCP-соединение.

Отладка HTTP

Поэкспериментировать с HTTP-запросами можно даже без микроконтроллера и написания программ. Для отсылки HTTP-запросов, параметризированных различными заголовками, в Linux удобно использовать команду curl.

Проанализировать HTTP-трафик можно при помощи Wireshark. Выбираем сетевой интерфейс и ставим следующий фильтр:

http.request or http.response

Стек TCP/IP

Когда нужно, чтобы твоя прошивка работала на конкретном микроконтроллере, следует выбрать и разобраться со стеком TCP/IP, который будешь использовать на этом микроконтроллере. С точки зрения программиста, стек — это библиотека или драйвер, которые обеспечивают механизмы работы основных протоколов TCP/IP — TCP, IP, UDP, DHCP и другие. Чтобы глубже понять TCP/IP, можете почитать мою статью.

lwIP

В Raspberry Pico W канальный и физические уровни (L2 и L1) обеспечиваются чипом Infineon CYW43439 и библиотекой из Pico SDK pico_cyw43_arch_lwip_threadsafe_background. Lwip настраивается в конфигурационном файле lwipopts.h

#ifndef __LWIPOPTS_H__
#define __LWIPOPTS_H__
#ifndef NO_SYS
#define NO_SYS                      1
#endif
// allow override in some examples
#ifndef LWIP_SOCKET
#define LWIP_SOCKET                 0
#endif
#if PICO_CYW43_ARCH_POLL
#define MEM_LIBC_MALLOC             1
#else
#define MEM_LIBC_MALLOC             0
#endif
#define MEM_ALIGNMENT               4
#define MEM_SIZE                    4000
#define MEMP_NUM_TCP_SEG            32
#define MEMP_NUM_ARP_QUEUE          10
#define PBUF_POOL_SIZE              24
#define LWIP_ARP                    1
#define LWIP_ETHERNET               1
#define LWIP_ICMP                   1
#define LWIP_RAW                    1
#define TCP_WND                     (8 * TCP_MSS)
#define TCP_MSS                     1460
#define TCP_SND_BUF                 (8 * TCP_MSS)
#define TCP_SND_QUEUELEN            ((4 * (TCP_SND_BUF) + (TCP_MSS - 1)) / (TCP_MSS))
#define LWIP_NETIF_STATUS_CALLBACK  1
#define LWIP_NETIF_LINK_CALLBACK    1
#define LWIP_NETIF_HOSTNAME         1
#define LWIP_NETCONN                0
#define MEM_STATS                   0
#define SYS_STATS                   0
#define MEMP_STATS                  0
#define LINK_STATS                  0
#define LWIP_CHKSUM_ALGORITHM       3
#define LWIP_DHCP                   1
#define LWIP_IPV4                   1
#define LWIP_TCP                    1
#define LWIP_UDP                    1
#define LWIP_DNS                    1
#define LWIP_TCP_KEEPALIVE          1
#define LWIP_NETIF_TX_SINGLE_PBUF   1
#define DHCP_DOES_ARP_CHECK         0
#define LWIP_DHCP_DOES_ACD_CHECK    0
#ifndef NDEBUG
#define LWIP_DEBUG                  1
#define LWIP_STATS                  1
#define LWIP_STATS_DISPLAY          1
#endif
#define ETHARP_DEBUG                LWIP_DBG_OFF
#define NETIF_DEBUG                 LWIP_DBG_OFF
#define PBUF_DEBUG                  LWIP_DBG_OFF
#define API_LIB_DEBUG               LWIP_DBG_OFF
#define API_MSG_DEBUG               LWIP_DBG_OFF
#define SOCKETS_DEBUG               LWIP_DBG_OFF
#define ICMP_DEBUG                  LWIP_DBG_OFF
#define INET_DEBUG                  LWIP_DBG_OFF
#define IP_DEBUG                    LWIP_DBG_OFF
#define IP_REASS_DEBUG              LWIP_DBG_OFF
#define RAW_DEBUG                   LWIP_DBG_OFF
#define MEM_DEBUG                   LWIP_DBG_OFF
#define MEMP_DEBUG                  LWIP_DBG_OFF
#define SYS_DEBUG                   LWIP_DBG_OFF
#define TCP_DEBUG                   LWIP_DBG_OFF
#define TCP_INPUT_DEBUG             LWIP_DBG_OFF
#define TCP_OUTPUT_DEBUG            LWIP_DBG_OFF
#define TCP_RTO_DEBUG               LWIP_DBG_OFF
#define TCP_CWND_DEBUG              LWIP_DBG_OFF
#define TCP_WND_DEBUG               LWIP_DBG_OFF
#define TCP_FR_DEBUG                LWIP_DBG_OFF
#define TCP_QLEN_DEBUG              LWIP_DBG_OFF
#define TCP_RST_DEBUG               LWIP_DBG_OFF
#define UDP_DEBUG                   LWIP_DBG_OFF
#define TCPIP_DEBUG                 LWIP_DBG_OFF
#define PPP_DEBUG                   LWIP_DBG_OFF
#define SLIP_DEBUG                  LWIP_DBG_OFF
#define DHCP_DEBUG                  LWIP_DBG_OFF
#endif /* __LWIPOPTS_H__ */

Реализация HTTP поверх lwIP

В учебных целях я решил изобрести небольшой велосипед и реализовать минимальный функционал HTTP, необходимый для работы моего эмулятора: отправлять Range HTTP-запросы с использованием Keep-Alive, парсить ответы и возвращать сектора диска.

Стек USB

Стек USB в Raspberry Pi Pico реализуется при помощи библиотеки TinyUSB. Перед использованием её необходимо настроить в конфигурационном файле tusb_config.h и прописать в CMakeLists.txt

#ifndef _TUSB_CONFIG_H_
#define _TUSB_CONFIG_H_

// RP2040
#define CFG_TUSB_MCU OPT_MCU_RP2040

// USB Device, Full Speed
#define CFG_TUSB_RHPORT0_MODE OPT_MODE_FULL_SPEED | OPT_MODE_DEVICE

// EP0
#define CFG_TUD_ENDPOINT0_SIZE 64

// Классы USB
#define CFG_TUD_CDC    0
#define CFG_TUD_HID    0
#define CFG_TUD_MSC    1
#define CFG_TUD_MIDI   0
#define CFG_TUD_VENDOR 0

// Размер буфера MSC
#define CFG_TUD_MSC_EP_BUFSIZE 2048

#define CFG_TUD_MSC_LUN0_PDT 0x05

// Выравнивание памяти
#define CFG_TUSB_MEM_SECTION
#define CFG_TUSB_MEM_ALIGN __attribute__((aligned(4)))

#endif

Я уже описывал ранее основной алгоритм работы прошивки. А сейчас просто скажу, что отсылка запроса HTTP должна быть асинхронная, чтобы не блокировать callback для READ(10). В этом колбек tud_msc_read10_cb должен вернуть TUD_MSC_RET_ASYNC, а не количество байт, а когда уже будет получен полный HTTP-ответ, необходимо вызвать функцию tud_msc_async_io_done

Так как мы эмулируем CDROM, библиотеку TinyUSB необходимо пропатчить. Как это сделать, я писал в одной из прошлых статей. A так как текущая версия Pico SDK 2.2 использует версию TinyUSB, не поддерживающую асинхронные колбеки, необходимо подключить более свежую версию TinyUSB.

Как проверить, что эмулятор CDROM работает

Самый простой способ — подключить Pico к компьютеру с установленным современным дистрибутивом Linux с графической оболочкой. Linux должен распознать USB СDROM, и в файловом менеджере вы должны увидеть этот диск и файлы на нём.

Также в выводе команды sudo lsblk должно появиться блочное устройство srX, где X — номер.

Из устройства можно считать несколько секторов с помощью команды dd и сравнить с такими из образа ISO.

dd if=/dev/sr0 of=/dev/null bs=2048 count=3 status=progress

Или попробовать загрузиться с Pico, выбрав в BIOS Pico как устройство, с которого загружаться.

Скорость работы

Полная скорость работы зависит сразу от нескольких факторов:

  • скорости Wi-Fi-соединения,

  • производительности веб-сервера,

  • времени ответа сети,

  • размера кэша,

  • эффективности реализации USB Mass Storage.

Даже несмотря на то, что Pico W обладает весьма скромными ресурсами, пропускной способности Wi-Fi обычно достаточно для загрузки большинства дистрибутивов Linux. После первоначального чтения файловой системы дальнейшие обращения часто попадают в кэш как операционной системы компьютера, так и самого микроконтроллера.

Возможные проблемы

У такого решения есть и ограничения.

Во-первых, загрузка полностью зависит от доступности сети. Если соединение пропадёт, компьютер начнёт получать ошибки чтения.

Во-вторых, сервер обязательно должен поддерживать диапазонные запросы. Без них пришлось бы скачивать ISO целиком, что полностью лишает проект смысла.

Наконец, задержки в беспроводной сети значительно выше, чем при чтении с обычного USB-накопителя. Поэтому для комфортной работы желательно использовать стабильную сеть Wi-Fi с хорошим уровнем сигнала.

Выводы

Что у меня получилось, вы можете посмотреть в моём репозитории на GitHub.

Работа над прототипом в итоге оказалась гораздо ценнее самого устройства. Именно она позволила по-настоящему глубоко разобраться в том, как на низком уровне взаимодействуют протоколы USB, TCP/IP и HTTP. Когда ты сам реализуешь стек от железа до приложения — многое встаёт на свои места совсем по-другому, чем при чтении документации и туториалов.

Реалистично оценивая результат: практической ценности у получившегося девайса в 2026 году почти нет. Производительность по современным меркам откровенно слабая, и конкурировать с любым нормальным микроконтроллером или одноплатником он не сможет. Но это и не было главной целью.

Главная ценность — в наработках и понимании. Код, структура проекта, найденные решения и даже совершённые ошибки дают отличную базу для следующих, более серьёзных экспериментов. Есть куда расти, и это самое приятное. Если хочется продолжить копать в эту тему, интересных задач хватит не на один вечер:

  1. Использовать оба ядра RP2040.

  2. Использовать FreeRTOS вместо baremetal.

  3. Заменить HTTP на HTTPS.

  4. Сделать полноценную обработку ошибок и краевых случаев.

  5. Сделать кеширование.

Можно было, конечно, сразу попытаться сделать «взрослую» версию со всеми этими фичами. Но тогда проект рисковал превратиться в ещё один бесконечный «pet project», который так и остался бы в папке unfinished.

А сделав сначала простую, но работающую версию, получаешь очень важное ощущение: цель достижима. Именно это ощущение мотивирует сильнее всего и заставляет возвращаться к проекту снова и снова.

В общем, если вы тоже любите ковыряться в железе и протоколах «просто чтобы понять, как оно работает» — такие прототипы определённо стоят того.

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