Привет, Хабровчане!

Это продолжение моего дневника разработки DIY струйного принтера предыдущая часть:

В прошлый раз одной из поднятых проблем стал интерфейс между ПК и устройством. В данной статье я постараюсь решить вопрос подключения STM32 по USB и убедить Windows и Linux что это принтер.

Содержание


§1. Кратко о USB
§2. Поиск реализаций USB Printer Class для STM32
§3. Реализация USB Printer Class для STM32
§4. Тест на Windows
§5. Тест на Linux
§6. Как добавить поддержку печати изображений?

§1. Кратко о USB

Для начала стоит разобраться что представляет собой USB. Неплохо с этим справились microtechnics и в статья Создаём своё первое USB-устройство на Хабре.

С аппаратной точки зрения USB проще веника:

  • 2 провода питания (5В и GNG)

  • 2 провода дифф-пара (Data- (D-) и Data+ (D+))

распиновка USB
распиновка USB

Протокольно это кадры с транзакциями.
Кадр начинается пакетом пакет SOF (Start Of Frame), заканчивается EOF (End Of Frame), который и не пакет на самом деле, а интервал времени, во время которого передача данных запрещена:

Транзакция может быть трёх видов:

  • Token

  • Data

  • Status

Data и Status достаточно простые, а Пакеты Token бывают нескольких типов: In, Out. Setup, Start Of Frame.

Для общего понимания этого достаточно. Лишь упомяну, что существуют различные классы USB устройств (смотрим тут), для каждого из которых написана своя спецификация (можно найти тут). Это и обеспечивает работу большого количество устройств независимо от производителя. Нас больше всего интересует USB Printer Class.

§2. Поиск реализаций USB Printer Class для STM32

В целом про реализацию USB устройств написано тут и в вышеупомянутой статье Хабре.

Не хочется заново изобретать велосипед, да ещё и с костылями вместо колёс, поэтому возвращаемся к реализации конкретно USB Printer Class

CubeIDE предлагает нам несколько вариантов в USB_DEVICE:

  • Audio Device Class - динамики, микрофоны и аудио-интерфейсы

  • Communication Device Class - устройства с телекоммуникационной или сетевой функциональностью (модемы, USB-Ethernet адаптеры и т.д.)

  • Download Firmware Update Class - загрузчик прошивки

  • Human Interface Device Class - клавиатуры, мыши, игровые контроллеры и другая периферия, которая позволяет человеку взаимодействовать с компьютером

  • Mass Storage Class - USB-диски, внешние жёсткие диски, считыватели карт и другие устройства хранения данных

Как видим, USB Printer Class тут нет и даже CustomClass нам не дадут. Заветный Printer Class предлагается нам в пакете с AzureRTOS:

На Github есть пример и документация. Звучит крайне заманчиво, но мы уже используем FreeRTOS и пока не намерены её менять. Хотелось бы иметь что-то подобное в пакете USB_DEVICE.

Почти смирившись с тем что придётся портировать решение из AzureRTOS, я наткнулся на Github репозиторий с разными USB Classes, которые, вероятно, используются в пакете USB_Device. Странно что они не включены в пакет из коробки и тяжело найти упоминания (лично я узнал после нескольких дней общения с Chat-GPT, а не на форуме STM32 или других профильных сайтах).
Устроен репозиторий крайне логично:

  • в Core находится ядро для работы USB

  • в Class директории с шаблонами для реализации конкретных классов, которых намного больше, чем в пакете USB_Device у CubeIDE

Самое главное что это можно перенести в свой проект почти без подводных камней.

§3. Реализация USB Printer Class для STM32

Для начала нам нужно ядро для работы с USB в реалиях STM32. Можно взять его из Github репозитория, но я предпочту выбрать в Cube IDE USB Mass Storage Сlass. На вкус и цвет. MSC тоже будет интересен в рамках Composite Device Class в будущем (тссс! Спойлер к следующему сезону!), так что пусть пока останется.

Перенесём к себе поддержку класса Printer для USB и реализацию интерфейса. Для этого создаём папку Middlewares/ST/STM32_USB_Device_Library/Class/PRINTERи переносим в неё низкоуровневый драйвер (ядро) - Inc/usbd_printer.h + Src/usbd_printer.c. Эта часть останется почти без изменений.

Большая часть изменений будет касаться интерфейса, который мы поместим в USB_DEVICE\App. Копируем туда usbd_printer_if.h и usbd_printer_if.с. Тут содержится необходимый интерфейс, который будет взаимодействовать с логикой основной программы.

Далее пойдут необходимые финальные изменения, которые уже вошли в основную ветку проекта.

Core

В примере не объявлен RxBuffer, поэтому программа упадёт при первом же подключении, так что в начало usbd_printer.c необходимо объявить его и отдать указатель структуре, представляющей собой наш принтер, а ещё я на всякий случай добавил подготовку буфера после приёма данных (сейчас нет реальной печати, так что вполне допустимо):

Изменения для usbd_printer.c
/** @defgroup USBD_PRNT_Private_Defines
  * @{
  */
  
  #define PRNT_RX_DATA_SIZE  512

...

/** @defgroup USBD_PRNT_Private_Variables
  * @{
  */
  
static uint8_t PrinterRxBuffer[PRNT_RX_DATA_SIZE];

...

static uint8_t USBD_PRNT_Init(USBD_HandleTypeDef *pdev, uint8_t cfgidx)
{

  hPRNT->RxLength = PRNT_RX_DATA_SIZE;
  hPRNT->RxBuffer = PrinterRxBuffer;

...
static uint8_t USBD_PRNT_DataOut(USBD_HandleTypeDef *pdev, uint8_t epnum)
{

    USBD_LL_PrepareReceive(pdev, epnum, hPRNT->RxBuffer, PRNT_DATA_FS_OUT_PACKET_SIZE);
  return (uint8_t)USBD_OK;
}

Есть функция USBD_PRNT_SetRxBuffer, который можно было бы воспользоваться, но пока пойдёт и так.

Print интерфейс

А вот в интерфейсе дело обстоит интереснее.

Для начала заменим все TEMPLATE в названиях на PRNT для большей чистоты кода.
Изменим DeviceID - в примере он и его объявление выглядят достаточно страшно, обойдёмся простой строкой:

static char PRNT_DEVICE_ID[] = "MFG:STM32;MDL:DIY_Printer;CMD:RAW;CLS:PRINTER;";

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

static int8_t PRNT_Receive(uint8_t *Buf, uint32_t *Len)
{
  char _buf[64] = {0};
  for (uint32_t i = 0; i < *Len; i++) {
    _buf[i] = Buf[i];
  }
  _buf[*Len] = '\r';
  _buf[*Len + 1] = '\n';
  LOG_Debug("%s", _buf);

  return (USBD_OK);
}

LOG_Debug — это функция одного из моих модулей, в конечном итоге это printf в терминал порта.

Немного перепишем обработку некоторых команд.
Во-первых, в примере перед DeviceId была зашита его длина в один массив, мы же сделали строку. Таким образом нам необходимо в начало массива вручную положить длину, а затем скопировать саму строку с DeviceId и обработка PRNT_GET_DEVICE_ID превратится в следующее:

case PRNT_GET_DEVICE_ID:
      /* Not using for loop here due to MISRA-C2012-Rule-16.1 */
      pbuf[0] = (sizeof(PRNT_DEVICE_ID) - 1) >> 8;
      pbuf[1] = (sizeof(PRNT_DEVICE_ID) - 1) & 0xFF;
      memcpy(&pbuf[2], PRNT_DEVICE_ID, sizeof(PRNT_DEVICE_ID) - 1);
      *length = sizeof(PRNT_DEVICE_ID) + 2;
      break;

Во-вторых, чисто для галочки сбросим буфер при PRNT_SOFT_RESET:

 case PRNT_SOFT_RESET:
      // сбросить приёмные буферы
      USBD_PRNT_HandleTypeDef *hPRNT = (USBD_PRNT_HandleTypeDef *)hUsbDeviceFS.pClassDataCmsit[hUsbDeviceFS.classId];
      USBD_LL_PrepareReceive(&hUsbDeviceFS, PRNT_OUT_EP, hPRNT->RxBuffer, PRNT_DATA_FS_OUT_PACKET_SIZE);
      *length = 0;
      break;

Изменим инициализацию USB Device.
Добавим #define USE_USBD_PRINTER_ONLY в usbd_printer_if.h и отрежем с его помощью сгенерированный CubeIDE код для MSC:

MX_USB_DEVICE_Init
void MX_USB_DEVICE_Init(void)
{
  /* USER CODE BEGIN USB_DEVICE_Init_PreTreatment */
#ifndef USE_USBD_PRINTER_ONLY
  /* USER CODE END USB_DEVICE_Init_PreTreatment */

  /* Init Device Library, add supported class and start the library. */
  if (USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS) != USBD_OK)
  {
    Error_Handler();
  }
  if (USBD_RegisterClass(&hUsbDeviceFS, &USBD_MSC) != USBD_OK)
  {
    Error_Handler();
  }
  if (USBD_MSC_RegisterStorage(&hUsbDeviceFS, &USBD_Storage_Interface_fops_FS) != USBD_OK)
  {
    Error_Handler();
  }
  if (USBD_Start(&hUsbDeviceFS) != USBD_OK)
  {
    Error_Handler();
  }

  /* USER CODE BEGIN USB_DEVICE_Init_PostTreatment */
#else // USE_USBD_PRINTER_ONLY

    /* Init Device Library, add supported class and start the library. */
    if (USBD_Init(&hUsbDeviceFS, &FS_Desc, DEVICE_FS) != USBD_OK)
    {
      Error_Handler();
    }
    if (USBD_RegisterClass(&hUsbDeviceFS, USBD_PRNT_CLASS) != USBD_OK)
    {
      Error_Handler();
    }
    if (USBD_PRNT_RegisterInterface(&hUsbDeviceFS, &USBD_Printer_fops_FS) != USBD_OK)
    {
      Error_Handler();
    }
    if (USBD_Start(&hUsbDeviceFS) != USBD_OK)
    {
      Error_Handler();
    }
  

#endif // USE_USBD_PRINTER_ONLY
  /* USER CODE END USB_DEVICE_Init_PostTreatment */
}

§4. Тест на Windows

Попробуем подключить наш принтер к Windows.

Ставим Windows SDK, посмотрим как наше устройство видит usbview:

usbview
[Port4]  :  Поддержка USB принтера


Is Port User Connectable:         yes
Is Port Debug Capable:            no
Companion Port Number:            0
Companion Hub Symbolic Link Name: 
Protocols Supported:
 USB 1.1:                         yes
 USB 2.0:                         yes
 USB 3.0:                         no

Device Power State:               PowerDeviceD0


       ---===>USB4 Information<===---
Root hub port is USB4 Capable:     No
Device is tunneled through USB4:   No

       ---===>Device Information<===---
English product name: "STM32 USB Printer"

ConnectionStatus:                  
Current Config Value:              0x01  -> Device Bus Speed: Full (is not SuperSpeed or higher capable)
Device Address:                    0x28
Open Pipes:                           2

          ===>Device Descriptor<===
bLength:                           0x12
bDescriptorType:                   0x01
bcdUSB:                          0x0200
bDeviceClass:                      0x00  -> This is an Interface Class Defined Device
bDeviceSubClass:                   0x00
bDeviceProtocol:                   0x00
bMaxPacketSize0:                   0x40 = (64) Bytes
idVendor:                        0x0483 = STMicroelectronics
idProduct:                       0x572A
bcdDevice:                       0x0200
iManufacturer:                     0x01
     English (United States)  "STMicroelectronics"
iProduct:                          0x02
     English (United States)  "STM32 USB Printer"
iSerialNumber:                     0x03
     English (United States)  "366E34823435"
bNumConfigurations:                0x01

          ---===>Open Pipes<===---

          ===>Endpoint Descriptor<===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x82  -> Direction: IN - EndpointID: 2
bmAttributes:                      0x02  -> Bulk Transfer Type
wMaxPacketSize:                  0x0040 = 0x40 bytes
bInterval:                         0x00

          ===>Endpoint Descriptor<===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x02  -> Direction: OUT - EndpointID: 2
bmAttributes:                      0x02  -> Bulk Transfer Type
wMaxPacketSize:                  0x0040 = 0x40 bytes
bInterval:                         0x00

       ---===>Full Configuration Descriptor<===---

          ===>Configuration Descriptor<===
bLength:                           0x09
bDescriptorType:                   0x02
wTotalLength:                    0x0020  -> Validated
bNumInterfaces:                    0x01
bConfigurationValue:               0x01
iConfiguration:                    0x00
bmAttributes:                      0xC0  -> Self Powered
MaxPower:                          0x32 = 100 mA

          ===>Interface Descriptor<===
bLength:                           0x09
bDescriptorType:                   0x04
bInterfaceNumber:                  0x00
bAlternateSetting:                 0x00
bNumEndpoints:                     0x02
bInterfaceClass:                   0x07  -> This is a Printer USB Device Interface Class
bInterfaceSubClass:                0x01
bInterfaceProtocol:                0x02
iInterface:                        0x00

          ===>Endpoint Descriptor<===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x82  -> Direction: IN - EndpointID: 2
bmAttributes:                      0x02  -> Bulk Transfer Type
wMaxPacketSize:                  0x0040 = 0x40 bytes
bInterval:                         0x00

          ===>Endpoint Descriptor<===
bLength:                           0x07
bDescriptorType:                   0x05
bEndpointAddress:                  0x02  -> Direction: OUT - EndpointID: 2
bmAttributes:                      0x02  -> Bulk Transfer Type
wMaxPacketSize:                  0x0040 = 0x40 bytes
bInterval:                         0x00

Важно что данные в секциях Device Information и Device Descriptor совпадают с тем что указано в USB_DEVICE\App\usbd_desc.c.

Спускаемся ниже к Interface Descriptor и видим что bInterfaceClass == 0x07 - пол дела сделано. Открываем Диспетчер устройств (для удобства я сменил вид на "Устройство по контейнеру") и находим наш STM32 DIY_Printer - DeviceId, который возвращается при вызове команды PRNT_GET_DEVICE_ID.Можно даже убедиться что Windows подхватил стандартный драйвер usbprint.sys:

Ура! Windows понял что это принтер и выдал соответствующий стандартный драйвер. Однако, нового принтера в списке не появилось. Дело в том что для стандартного драйвера нужна ещё одна логическая обёртка, которая создаётся Windows.

Создаём логический принтер

Отправляемся в настройки, находим "Принтеры и сканеры" и тут нам необходимо вручную добавить новое устройство:

Выбираем "Добавить локальный или сетевой принтер с параметрами, заданными вручную":

Выбираем тот, на котором указан наш DeviceId:

Далее необходимо выбрать драйвер, т.к. своего драйвера у нас нет, а как работают чужие мы пока не знаем, воспользуемся "Generic / Text Only":

Далее используем уже установленный драйвер (Windows уже определил нам usbprint.sys)

Дальше при желании можно поменять название принтера, я этого не делал (как видите - не первая моя попытка):

Теперь можно увидеть новое устройство:

при этом "STM32 DIY_Printer" никуда не делся, а загвоздка том что он работает как физически подключённое устройство USB Printer Class - грубо говоря, низкоуровневый драйвер, а "Generic / Text Only" - это логическая обёртка над ним, предоставляющая нам более удобный высокоуровневый драйвер. Можно отдалённо сравнить сравнить это с логическими томами у HDD и SSD.

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

Пример текста
HELLO!

There is a text
with separated lines
in English

More text

Bla-bla-bla

BYE!

Во время печати терминал отладки показал следующее:

Вывод отладчика

[DEBUG]

1 HELLO!
2
3 There is a text

[DEBUG] 4 with separated lines
5 in English
6

[DEBUG] 7 More text
8
9 Bla-bla-bla
10

[DEBUG]
11 BYE!

И уже можно сказать, что это работающий USB Printer Device. Стоит заметить что данные приходят кусками по 64 байта. Это именно то значение, которое я использовал вUSBD_LL_PrepareReceive. Хотя RxBuffer имеет размер PRNT_RX_DATA_SIZE = 512, я использовал PRNT_DATA_FS_OUT_PACKET_SIZE = 64.

Почему так сложно добавить устройство в Windows?

Поскольку я использую стандартные для STM32 VID/PID, микроконтроллер и определяется соответственно. Насколько я понял, нельзя просто так взять и и поменять VID и PID, обычно, они покупаются за немаленькую цену, но есть проекты‑инициативы вроде pid.codes, где можно бесплатно получить PID внутри общего VID для open‑source устройств. Возможно, когда проект будет чуть ближе к полноценно работающему прототипу, можно воспользоваться этим.

§5. Тест на Linux

Из устройств на Linux у меня есть китайский WukongPi c убитой флешкой и калькулятор ноутбук на Debian, им и воспользуемся:

При подключении сразу появилось устройство /deb/usb/lp0, а в настройках оно сразу определилось как Generic Text-Only Printer

Отправляем echo "HELLO" | sudo tee /dev/usb/lp0 и в терминале отладке тут же появляется сообщение [DEBUG] HELLO.

Идём дальше. Устанавливаем CUPS (в принципе не обязательно, просто интересно что это такое)

sudo apt install cups
sudo usermod -aG lpadmin $USER

Открываем в браузере http://localhost:631 и добавляем принтер (скриншоты делать больно, поэтому только шаги):

  • В веб‑интерфейсе CUPS → Administration → Add Printer.

  • В списке устройств появится выбираем наш USB‑принтер (STM32 DIY_Printer).

  • Когда спросят драйвер, выбираем Raw Queue или Generic → Raw.

Создадим тестовый файл:

printTest.txt
HELLO, Linux!

This is a first message
Second line as second message
Third message

BYE, Linux!

Вызываем в терминале: lp -d STM32_DIY_Printer printTest.txtи смотрим в отладчик:

[DEBUG] HELLO, Linux!

                     This is a first message
                                            Second line as second mes

[DEBUG] sage
            Third message

                         BYE, Linux!

Всё хорошо, принтер может работать и с Linux тоже.

Ещё я попробовал печать из стандартного GUI редактора текста в Debian, отладчик выдал:

[DEBUG] PDF-1.5
               ����
                   4 0 obj
                          << /Length 5 0 R
                                             /Filter /FlateDecode


[DEBUG] >>
          stream
                x�}P�N�0��+�[���vvPšRBA���8��A�&@D?�M��h�������

[DEBUG] #
[DEBUG] r�m�t&��v����   �!��R�O�L6������&D������:Ԝ���?�=76n��W�A�5a�

[DEBUG] �!�ǿ��i�!P�S�.���۳#�2���ڗ�����
                                      ��yߝ��P��JzZ�~��y�,

Очевидно что нужен драйвер на ПК, либо какой-то интерпретатор (PCL, PostScript). Но уже можно сказать что у нас есть заготовка под принтер.

§6. Как добавить поддержку печати изображений?

Вот тут мы выходим за пределы Generic / Text Only. Этот драйвер умеет только слать «сырые» байты текста, и никакой графики Windows через него не рендерит. Чтобы печатать изображения, нужно решить два вопрос: Как Windows будет готовить данные?

  1. Чтобы Windows могла «рисовать» картинку и отправлять её на принтер, нужен драйвер, который умеет преобразовывать графику в команды устройства.

    • В мире обычных принтеров это PCL, PostScript, ESC/POS или собственный протокол.

    • В Windows это реализуется через Unidrv/PSCRIPT (старые v3‑драйверы) или v4‑драйверы (XPSDrv).

  2. Можно написать приложение, которое преобразует картинку поток байтов (например, bitmap → монохромные точки) и отправит напрямую в спулер печати как RAW‑задание.

Я не хочу использовать дополнительный софт для формирования задания - поддержка нативной печати важна (это же не CNC). Конечно, свой софт тоже придётся написать, но, в идеале нужен драйвер, но пока что модуль управления печатающей головкой не готов, поэтому останемся на Generic / Text Only.

В будущем нужно будет реализовать XPSDrv с фильтром, который преобразует XPS‑графику в протокол, который будет поддерживать принтер.


Заключение

Можно сказать что у меня получилось реализовать заготовку для USB Printer Class, как минимум теперь будет проще заняться разработкой протокольной части для печати. Уже можно принимать сырые данные и на основе их выполнять какое-то задание на печать.

Код, использованный для данной статьи уже лежит в main ветке репозитория проекта на Github. В будущем нужно подумать о реализации более продвинутого протокола для печати изображений.

Немного пояснений к фразе:

MSC тоже будет интересен в рамках Composite Device Class в будущем

Есть идея в будущем реализовать CompositeDevice и дать доступ к флешке (или её части). Так будет проще добираться до логов и выкидывать задание на печать прямо на принтер. Ещё меня заинтересовал классы Image и Video. Их вполне можно использовать для работы сканера (он есть в моём принтере - чего добру пропадать? :D). Где-то натыкался что CompositeDevice поддерживает максимум 4 интерфейса, но мне кажется что можно и больше. Непонятно только смогут ли они работать "параллельно".

Но сачала разберёмся с управлением печатающей головой!


Послесловие

С момента прошлой части добавились прототип модуля для работы с шаговыми двигателями, который будет контролироваться модулем печати, и небольшой модуль для сигнализации ошибок, ему пока ещё не хватает записи в энергонезависимую память (как и логгеру), но теперь, когда I2C или SPI не находят собеседников, я знаю об ошибке, а терминал не захламляется ошибками и я этому рад. А ещё получилось прикрутить PVS-Studio к CI/CD, спасибо статье на Хабре, правда я так и не смог заставить работать фильтр файлов так что код от STM тоже попадает в анализ.

Китайский логический анализатор не тянет обещанные 24 Мб/c, даже на 4 Мб/С не всегда ведёт себя стабильно. Будем надеяться что это не помешает заняться самым важным вопросом - управлением печатающей головой непосредственно, по крайней мере необходимо отработать алгоритм на контроллере без картриджа, а потом уже можно приниматься жечь головы ^(#`∀´)_Ψ

С точки зрения разработки было бы намного логичнее начать с управления печатающей головой. Но пока что проще решать чисто программные задачи и быть уверенным что проблема в коде, а не в железке.

Спасибо всем кто давал советы и подкидывал идеи в предыдущей части! Ещё увидимся!

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


  1. alekseypro
    25.09.2025 23:21

    Спасибо. Интересно. Лет 5 назад, экспериментировал с USB и STM32, особо в этом не шаря, но удалось реализовать передачу температуры с датчика DS18B20 с МК в приложение на смартфон на Android-е по USB , а с кнопки в приложении, включать/выключать светодиод на плате с STM32. Магия :)


  1. KivApple
    25.09.2025 23:21

    нельзя просто так взять и и поменять VID и PID

    Технически, можно всё. Просто если самодеятельствовать, то нельзя лепить шильдик о совместимости с USB (который торговая марка). Но можно как-то намекнуть, что ваше устройство втыкается в порт вот примерно такой формы.

    Но проблема в том, что винда будет относиться к неизвестному vid:pid также как и к демо vid:pid.

    Более того, есть подозрения, что и большая часть принтеров начинают определяться полноценно только после установки драйвера, который прописывает vid:pid (если принтер реализует стандартный класс, этим можно ограничиться). Просто для настоящих принтеров у MS есть база драйверов с автозагрузкой.


    1. Kostec26 Автор
      25.09.2025 23:21

      Да, по факту, никто не мешает использовать любую понравившуюся комбинацию vid:pid.

      большая часть принтеров начинают определяться полноценно только после установки драйвера, который прописывает vid:pid

      Так работает почти всё что подключается к Windows. Честно говоря, я туда пока не погружался - полезу и буду пытаться писать свой драйвер когда заработает печатающая голова (уже в процессе).

      У меня сложилось впечатление что Windows смотрит на класс устройства и представляет стандартный драйвер. Однако, если есть специальный драйвер для какой-то комбинации vid:pid, то подхватываться будет он.

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

      Можно написать свой inf файл, в котором указываетсч какой драйвер должен использоваться для конкретного устройства и принципе описывается что это за устройство. Это будет иметь смысл только если комбинация vid:pid действительно уникальна. Уникальность обеспечивается наличием официально зарегистрированных vid:pid и подписей у драйвера и т.е. наличием организации, которая выдаёт vid (какой дали, с таким и живёшь) и через которую, вроде, можно (а может и не нужно) зарегистрировать pid (производитель выбирает сам для своих устройств).

      В процессе разработки демо vid:pid более чем достаточно. Меня совершенно никак не коробит использовать левый vid:pid в релизе (вдруг он случится - можно помечтать) и драйвер, на который будет ругаться ОС т.к. это моё устройство и я знаю что это такое и что оно делает. Других пользователей подобное может напугать, да и выглядит не очень профессионально ;)


      1. KivApple
        25.09.2025 23:21

        У меня сложилось впечатление что Windows смотрит на класс устройства и представляет стандартный драйвер.

        Даже для стандартного класса как правило нужно чтобы был хотя бы простенький INF файл, который говорит "используй вот этот стандартный драйвер с этим устройством" (INF может ссылаться и на системный драйвер, а не только свой).

        Исключение сделано только для очевидных USB Mass Storage и USB HID (чтобы всё многообразие китайских флешек, мышей и клавиатур точно работало без настройки), если ничего не поменялось с тех пор. По этой причине, кстати, много самоделок реализуют USB HID совсем не являясь HID устройствами в привычном смысле этого слова (USB HID Class поддерживает кастомные report, позволяя слать произвольные данные). Тупо чтобы пользователям не нужно было ставить никаких драйверов, а было достаточно user space приложения даже не требующего установки (WinAPI содержит функции для отправки/приёма кастомных USB HID reports). Для задач где не нужно передавать много данных (протокол HID не позволяет передавать слишком много и слишком часто) подходит отлично.

        Даже с USB CDC уже надо либо использовать VID:PID, которые винда из коробки ассоциирует с COM-портами (благо, у большинства производителей микроконтроллеров есть те самые демо идентификаторы, вы могли заметить, что обычно их несколько - один для HID, один для CDC, один для MSC и т. д. - как раз потому что у них разные ассоциации в винде), либо написать INF и поставить. Для принтеров - тем более (только производители могут не давать отдельный демо идентификатор для него, потому что более редкий use-case и у производителей принтеров всё равно есть свои VID:PID).

        При этом пока вы укладываетесь в функционал стандартного USB Printer Class, вы можете ограничиться только тем самым INF файлом (и даже распространять его, даже если ваш VID:PID не уникален, достаточно чтобы у пользователя не было одновременно вашего устройства и того с которым у вас коллизия). А если, например, ограничиться всеми не стандартизированными функциями в отдельном приложении, то можно сделать composite device реализующий не только принтер, но и HID, чтобы не пришлось писать полноценный драйвер.


        1. Kostec26 Автор
          25.09.2025 23:21

          Composite device потом ещё попробую. Моё внимание привлёк не столько HID, сколько Image, MSC. Хочется сразу иметь доступ хотябы к логам на SD карте, возможно отдавать задание на печать (что потребует рендера на устройстве, но вполне можно и RAW отправить). Ну а через Image можно сканер попробовать завести.

          Ещё, возможно, имеет смысл познакомиться с Download Firmware Update, насколько понял, его можно использовать для обновления прошивки, но надо будет заиметь минимальный бутлодер для запуска и обновления этой самой прошивки (двойной буфер?).


          1. KivApple
            25.09.2025 23:21

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

            Бутлоадер запускается перед основной прошивкой, проверяет какое-то условие и либо запускается полноценно, либо отдаёт контроль ей.

            Условие обычно либо какая-нибудь кнопка (тогда нужно при включении/перезагрузке контроллера одновременно зажать специальную кнопку), либо программный флаг (нужно отдать команду основной прошивке любым удобным образом, она взведёт флаг в памяти и перезагрузит контроллер, а бутлоадер проверит этот флаг пользуясь тем, что при soft-reset память у многих МК не очищается сама собой). Хотя всё равно хорошо бы продублировать кнопкой (на случай если нечаянно будет записана глючная прошивка, которая не может принимать команды).

            У STM32 старших моделей (например, линейки STM32F4), кстати, есть встроенный с завода USB MSC бутлоадер активируемый по какой-то комбинации BOOT-пинов (достаточно вывести наружу устройства кнопку сброса и кнопку замыкающую один из BOOT-пинов). У младших моделей типа линейки STM32F1 встроен только UART бутлоадер. Но можно прошить дополнительно свой с поддержкой USB.


            1. Kostec26 Автор
              25.09.2025 23:21

              У STM32 старших моделей (например, линейки STM32F4), кстати, есть встроенный с завода USB MSC бутлоадер

              Не знал, посмотрю как дойдёт дело.
              В плане логики работы загрузчика пока в голове три варианта:

              1. Хранить дамп старой и новой прошивки в энергонезависимой памяти снаружи (например, SD карта, EEPROM, внешняя flash) и перешивать область основной прошивки по необходимости (с соблюдением каких-то условий). Так обновление разделится на 2 этапа - загрузка новой прошивки и непосредственно перезапись flash

              2. Иметь двойной буфер во flash для основной прошивки. При глюках новой прошивки будет возможность запустить старую без перепрошивки контроллера лёгким движением руки, но я пока не уверен что мне хватит памяти на такое. Самое главное, так можно сдеать бутлоадер потоком внутри RTOS, а не отдельной программой со своим местом внутри flash

              3. Прошивка так прошивка. Останавливаем основную прошивку и переключаемся на бутлоадер при прошивке. Перешивается область памяти где лежит основная прошивка.

              Сейчас я использую Nucleo - у неё есть ST-Link с возможностью подключения как MSC к ПК и на неё можно сбросить новый файл прошивки, который при каких-то условиях загрузится во flash подключённого к ST-Link контроллеру. В принципе, в этом плане можно ничего не менять, разве что объединить 2 USB в один через хаб, но это уже звучить как излишняя заморочка.

              Я ещё изучу этот вопрос, для меня это пока не изученная тема.