*За прикольную идею с изолентой спасибо @grafalex Продвигаясь в изучении программирования микроконтроллеров, я осознал необходимость освоить USB, поскольку это, бесспорно, основной интерфейс НЕ-внутрисхемного подключения устройств. Однако оказалось, что соответствующих материалов в открытом мире немного. Проанализировав различные форумы, сформулировал следующие причины непопулярности USB в проектах: @jaiprakashнапомнил, что обязательное для USB-устройства значение VID нужно покупать за большие деньги. Отсутствие в большинстве проектов необходимости высокоскоростной передачи данных. Высокая сложность самого стандарта и разработки по сравнению с привычным многим интерфейсом UART. Дешевле добавить в устройство готовый USB<->UART переходник. Отсутствие навыков разработки драйверов для ОС Windows/Linux.    В итоге, разработчики, в основном, предпочитают использовать UART (через аппаратный конвертер или, максимум, путем создания VCP-девайса, код которого успешно генерирует CubeMX). Я решил попытаться разобраться с USB хотя бы на базовом уровне, продолжив линию применения шаблонов языка C++. В этом посте описан примененный способ распределения ресурсов (а именно буферной памяти и регистров) между конечными точками устройства.     Основным элементом программы, реализующей USB-устройство, является Конечная точка (Endpoint). Хост обменивается данными с конкретной конечной точкой. Устройство обязано содержать конечную точку с номером 0, через которую происходит управление, запросы различных дескрипторов на этапе энумерации, команды назначения адреса, выбора конфигурации и все остальное управление. Более подробно с понятием конечных точек и в принципе базовыми знаниями USB можно ознакомиться в переводе "USB in NutShell" на ресурсе microsin (ребятам огромное спасибо за проделанный труд, очень полезное дело сделали). Контроллеры Stm32F0/F1 содержат специальную область памяти - Packet Memory Area (PMA), в которой размещается таблица дескрипторов буферов пакетов конечных точек и сами буферы пакетов конечных точек. Все всех примерах низкоуровневой реализации USB-стека предлагается вручную распределять память, что как минимум не очень удобно, а также увеличивает риски допустить ошибку. Например, чтобы изменить размер буфера конечной точки K, необходимо "подвинуть" все смещения для конечных точек K+1, ... , N. (где N - общее количество конечных точек). Лично в себе я уверен: 100% рано или поздно ошибусь. Стоит учитывать, что параметры конечных точек (включая размер буфера) известны на момент компиляции, что позволяет перенести распределение памяти из runtime в compile-time, это дает два основных преимущества: Исключение дублирования. Указания размеров буферов достаточно для распределения смещений. Также отпадает в принципе необходимость ручной инициализации дескрипторов буферов пакетов (ADDRn_TX, COUNTn_TX, ADDRn_RX, COUNTn_RX), так как их значения можно вычислить на этапе компиляции, а в runtime достаточно просто записать в память. Возможность раннего детектирования ошибок, таких как переполнение буфера или попытка некорректной привязки нескольких конечных точек к одному регистру EPnR (этот момент тоже подвержен ошибкам, потому что каждый регистр на самом деле может быть использован для двух разнонаправленных независимых конечных точек, или одной с двойной буферизацией). Каждая конечная точка характеризуется следующими параметрами: Номер конечной точки (0..16). Тип конечной точки (Control, Interrupt, Bulk, Isochronous). Направление (In, Out). Максимальный размер пакета для передачи. Все перечисленные параметры входят в дескриптор конечной точки, который отправляется на хост в процессе энумерации. В рамках устройства для конечной точки также должны быть назначены: Регистр конечной точки (EPnR). Адрес дескриптора буфера пакетов. Адрес буфера пакетов (или двух буферов в случае двунаправленной конечной точки или конечной точки с двойной буферизацией). И вот здесь возникает определенная сложность: перечисленные выше значения конечной точки N зависят от всех других конечных точек. Из различных вариантов, которые пришли в голову, остановился на следующей методике: Объявить все конечные точки с указанием номера, типа, направления и максимального размера пакета. Объявить класс менеджера конечных точек, передав все объявленные конечные точки. Получить от менеджера "расширенные" версии конечных точек. В результате получился следующий класс: Не буду приводить код вспомогательных классов, дам краткое их описание: EndpointEPRn - класс, назначающий регистра EPnR для конечной точки. Реализован достаточно просто: всем конечным точкам регистры назнчаются по порядку. Если две конечные точки корректно делят один номер, то обеим назначается один регистр. BufferOffset - класс, вычисляющий смещение буфера пакетов для конечной точки. Класс тоже достаточно простой, смещение буфера конечной точки N есть сумма размеров конечных точек 0, ..., N-1. SortedUniqueEndpoints - класс, сортирующий все  конечные точки по номерам и направлениям + исключение дубликатов. Текущее мое понимание USB допускает использование одной конечной точки в разных интерфейсах/конфигурациях, что приведет к дублированию в классе Device. IsBidirectionalOrBulkDoubleBufferedEndpoint, IsOutEndpoint, IsBulkDoubleBufferedTxEndpoint - предикаты для поиска конечных точек с определенными параметрами в общем списке.  Использовать менеджера конечных точек нужно таким образом: Внутри класса Device снова объявляется менеджер конечных точек, только в качестве аргументов передаются уже расширенных их версии: В результаты применения шаблонов C++ достигнуты следующие результаты: Часть действий перенесена на этап компиляции, что позволило, пусть и не существенно, снизить размер прошивки (прошивка HID-устройства, позволяющего управлять одним светодиодом, занимает 2400 байтов). Снижен уровень необходимого дублирования кода, хотя полностью избежать его не удалось. Как следствие предыдущего пункта, снижен объем ошибок, которые программист может допустить. Также "бесплатно" получен некоторый контроль над возможным выходом за пределы буфера USB. *лично мой профит. Углублены знания шаблонов C++, разобрался в концептах, хотя в итоге нигде не применил. USB для меня оказался очень сложным. Уверен, что кое-что до сих пор понимаю неправильно и надеюсь все-таки разобраться, потому что USB, по моему мнению, де-факто единственный адекватный интерфейс подключения устройства к компьютеру. Описанный выше подход внедрен в разрабатываемую мною библиотеку, пользоваться им удобно. К сожалению, по факту похвастаться нормальной поддержкой пока USB не могу, однако на текущий момент достиг того, что компьютер "видит" подключенное устройство, определяет его класс. Данный пост был посвящен не части библиотеки, касающейся USB в целом, а небольшом, но важном модулей распределения ресурсов между конечными точками. Буду рад вопросам и комментариям. Посмотреть код целиком (USB пока тестирую только на F072RBT6, потому что есть disco с распаянным miniusb) можно тут. Надеюсь к лету победить USB хотя бы для МК серии F0 и F1. Смотрел на F4 - там все более круто (есть поддержка OTG) и сложно.