Embedded HandbookПриложения
Все разделы › III. Периферия
STM32F103, STM32F407 · уровень продвинутый · 13 мин чтения
Открыть в интерактивном справочнике

20. USB

Самый сложный интерфейс в справочнике — и единственный, который почти никто не реализует с нуля. Здесь важнее не регистры, а понимание, что происходит, и умение правильно настроить готовый стек.

Как это называется в документации

Оригинал Перевод
host / device хост / устройство
endpoint конечная точка
descriptor дескриптор
enumeration опознание устройства при подключении
control / bulk / interrupt / isochronous transfer управляющая / массовая / прерывающая / изохронная передача
packet memory area (PMA) память буферов пакетов
pull-up resistor подтягивающий резистор
composite device составное устройство
vendor / product ID идентификатор производителя / изделия
virtual COM port виртуальный COM-порт
maximum packet size наибольший размер пакета конечной точки
NAK ответ конечной точки «не готова, повтори»

20.1 Почему это сложно

Все предыдущие интерфейсы были про передачу байтов. USB — это протокол с многослойной структурой: физический уровень, пакеты, конечные точки, дескрипторы, классы устройств, драйверы на стороне компьютера. Даже «просто отправить строку» требует, чтобы устройство сначала корректно представилось системе.

Хост всегда главный. Устройство не может начать передачу само — оно отвечает, когда его спросят. Компьютер опрашивает всех участников по расписанию.

Устройство обязано себя описать. При подключении хост запрашивает дескрипторы: кто ты, какого класса, сколько у тебя каналов связи, какого размера пакеты. Этот диалог называется опознанием, и пока он не пройдёт, устройства для системы не существует.

Отсюда практический вывод: писать стек USB самостоятельно не нужно. Берут готовый — библиотеку ST или сгенерированный CubeMX код — и настраивают под задачу.


20.2 ⚠️ Тактирование: обязательно кварц

У F1 и F4 — жёсткое требование, которое нельзя обойти.

USB Full Speed требует ровно 48 МГц с точностью ±0,25 %. Внутренний генератор HSI даёт около ±1 % — этого категорически недостаточно.

Точность
требование USB ±0,25 %
HSI ±1 % — не годится
кварц HSE ±0,005 % — с огромным запасом

Откуда берутся 48 МГц:

У F1 делитель всего один, с двумя положениями — на 1 или на 1,5, и стоит он на выходе PLL. Значит PLL обязана давать либо 48, либо 72 МГц (системной частотой при этом может быть и она, и HSE):

RCC->CFGR &= ~RCC_CFGR_USBPRE;   /* сброшен -> деление на 1,5: 72 / 1,5 = 48 */
RCC->APB1ENR |= RCC_APB1ENR_USBEN;

Частота 64 МГц, которую иногда выбирают ради экономии, для USB непригодна: ни один из двух делителей не даёт из неё 48 МГц.

У F4 за это отвечает делитель Q в настройке PLL — тот самый, про который забывают в разделе 6. При N = 336 подходит Q = 7, и получается ровно 48 МГц.

Откуда берутся 48 МГц: у F1 — делитель на 1,5 от 72 МГц, у F4 — отдельный делитель Q = 7 от 336 МГц

Где кварц не нужен. У многих камней новее — F042 и F072, L0, большей части L4 (кроме L475 и L476), G0B1, G4, U5, H5 — есть свой генератор 48 МГц (HSI48) с блоком подстройки CRS. Блок подгоняет частоту по пакетам начала кадра, которые хост присылает раз в миллисекунду, и устройство USB работает без кварца. Хосту подстраиваться не по чему: в режиме хоста кварц нужен и там.


20.3 Конечные точки и передачи

Конечная точка — это канал связи внутри устройства, у каждого своё направление и назначение. Точка номер 0 обязательна и служит для управления; остальные добавляются под задачу.

Четыре типа передач:

Тип Для чего Гарантии
управляющая опознание, команды обязательная, с подтверждением
массовая большие объёмы: флешка, виртуальный порт доставка гарантирована, время — нет
прерывающая мышь, клавиатура, датчики опрос с заданным периодом
изохронная звук, видео время гарантировано, доставка нет

Разница между двумя последними важна: для звука лучше потерять пакет, чем задержать поток, для клавиатуры — наоборот.

У F103 буферы всех конечных точек живут в 512 байтах выделенной памяти, которую он делит с контроллером CAN (раздел 19). Часть этой памяти занимает таблица описателей буферов — по 8 байт на конечную точку, — поэтому пакетов по 64 байта в ней помещается не больше семи. Виртуальному порту нужно около 300 байт: таблица на три точки, два буфера точки 0, два буфера данных и маленький буфер прерывающей точки — на практике хватает.


20.4 Классы устройств: что выбрать

Готовые классы избавляют от написания драйвера на стороне компьютера — система опознаёт устройство штатными средствами.

Класс Что видит компьютер Когда брать
CDC виртуальный COM‑порт отладка, обмен данными, замена UART
HID клавиатура, мышь или произвольное устройство когда нельзя ставить драйвер
MSC флешка доступ к файлам на карте памяти (разделы 41–42)
DFU загрузчик обновление прошивки по USB

CDC — самый ходовой выбор. Устройство появляется как обычный последовательный порт, работает со всеми терминалами, программа на компьютере пишется как для UART. По сути это UART без переходника, но на скорости в десятки раз выше. Скорость, выставленная в терминале, лишь передаётся устройству управляющим запросом и на обмен не влияет.

HID хорош тем, что не требует драйверов вообще — ни на Windows, ни на Linux, ни на macOS. Класс позволяет передавать произвольные данные, а не только нажатия клавиш, поэтому его часто используют для приборов, которые должны заработать сразу после подключения.

Составное устройство объединяет несколько классов сразу — например, виртуальный порт и флешку одновременно.


20.5 ⚠️ Подтяжка на линии D+

Аппаратная деталь, из-за которой «устройство не определяется» чаще всего.

Хост узнаёт о подключении по подтягивающему резистору на линии данных: 1,5 кОм от D+ к питанию означает устройство Full Speed. Нет резистора — для компьютера ничего не подключено.

Плата Как сделано
BluePill внешний резистор, часто неверного номинала
Nucleo, Discovery по схеме производителя, обычно верно
F4 (OTG) подтяжка встроена в контроллер, внешняя не нужна; включается, только когда виден VBUS на PA9

⚠️ Известная беда BluePill. На многих клонах вместо 1,5 кОм установлен резистор на 10 кОм или 4,7 кОм. Устройство при этом определяется через раз, только на некоторых портах, или не определяется вовсе. Лечится заменой резистора R10 на 1,5 кОм — это первое, что стоит проверить, если USB на BluePill «не работает без причины».

⚠️ F4 не определяется, хотя подтяжка встроенная. Модуль OTG подключает её, только когда видит напряжение VBUS на PA9 — если контроль VBUS включён. На платах, где PA9 с разъёмом USB не соединён, контроль выключают в настройке PCD (у CubeMX — в usbd_conf.c): Init.vbus_sensing_enable = DISABLE. Библиотека тогда ставит в GCCFG бит NOVBUSSENS (у F446 и F412 — снимает VBDEN и подменяет сигнал сессии), и модуль считает VBUS присутствующим всегда.

Второй приём, который применяют осознанно: управляемая подтяжка. Резистор подключают через транзистор к выводу микроконтроллера, и программа может «отключиться и подключиться заново» — это заставляет хост повторить опознание без физического выдёргивания кабеля. Полезно при обновлении прошивки и при отладке.


20.6 Как это делают на практике

Разумный путь — CubeMX (раздел 4):

  1. включить USB в режиме устройства;
  2. выбрать класс — обычно CDC;
  3. проверить, что дерево тактов даёт ровно 48 МГц (конфигуратор подсветит несоответствие);
  4. сгенерировать проект.

Дальше работа сводится к двум функциям: отправить буфер и обработать принятый. Всё остальное — опознание, дескрипторы, управляющие передачи — делает библиотека.

Дескрипторы менять всё же придётся, если устройство идёт дальше макета: там прописаны идентификаторы производителя и изделия, а также текстовые названия, которые увидит пользователь в системе. Идентификаторы из примеров годятся для отладки, но не для серийного изделия — их выделяет организация‑владелец стандарта.


20.7 🔧 Виртуальный COM-порт: приём команд

CubeMX с классом CDC создаёт файл usbd_cdc_if.c, куда вписывается своё: CDC_Receive_FS вызывается, когда пришёл пакет, а CDC_Transmit_FS отправляет буфер. Выглядит как UART, но устроено иначе — и почти все проблемы отсюда.

Пакет — не строка. Данные приходят пакетами до 64 байт, и где пакет кончается, решает компьютер, а не отправитель. Команда toggle_led может прийти целиком, двумя кусками или слипшись с соседней. Терминал, в котором печатают руками, шлёт каждый символ отдельным пакетом. Если сравнивать пакет с командой, её почти никогда не узнать.

Обратный вызов работает в прерывании. CDC_Receive_FS зовётся из обработчика прерывания USB, поэтому разбирать там строки, вызывать printf или чего-то ждать нельзя. Правильно — сложить байты в кольцевой буфер и отпустить пакет, если следующий поместится, а строки собирать в главном цикле:

#define CDC_PKT 64u                                     /* наибольший пакет Full Speed */
static volatile uint8_t rx_paused;                      /* пакет не отпущен: в кольце не было места */

int8_t CDC_Receive_FS(uint8_t *Buf, uint32_t *Len)
{
    cdc_rx_push(Buf, *Len);                             /* только сложить — разбор в главном цикле */
    USBD_CDC_SetRxBuffer(&hUsbDeviceFS, &Buf[0]);
    if (cdc_rx_room() >= CDC_PKT)
        USBD_CDC_ReceivePacket(&hUsbDeviceFS);          /* следующий пакет точно поместится */
    else
        rx_paused = 1;                                  /* не отпускать: хост получит NAK и подождёт */
    return USBD_OK;
}

/* главный цикл, после разбора строк: место освободилось — отпустить пакет */
void cdc_rx_resume(void)
{
    if (rx_paused && cdc_rx_room() >= CDC_PKT) {
        rx_paused = 0;
        NVIC_DisableIRQ(USB_LP_CAN1_RX0_IRQn);          /* F4: OTG_FS_IRQn */
        USBD_CDC_ReceivePacket(&hUsbDeviceFS);
        NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
    }
}

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

#define RING 256u                           /* степень двойки: переполнение счётчиков не страшно */

static volatile uint8_t  ring[RING];
static volatile uint32_t head;              /* пишет только прерывание */
static volatile uint32_t tail;              /* пишет только главный цикл */
static volatile uint32_t lost;              /* не поместилось в буфер */

/* из CDC_Receive_FS: пакет buf длиной len; вернуть — сколько поместилось */
uint32_t cdc_rx_push(const uint8_t *buf, uint32_t len)
{
    uint32_t n = 0;
    while (n < len && head - tail < RING) {
        ring[head % RING] = buf[n++];
        head++;                             /* после данных — цикл увидит байт готовым */
    }
    lost += len - n;
    return n;
}

/* свободно в кольце: по нему решают, отпускать ли следующий пакет */
uint32_t cdc_rx_room(void)
{
    return RING - (head - tail);
}

/* главный цикл: 1 — в line готова строка без '\r' и '\n' */
int cdc_line(char *line, uint32_t size)
{
    static uint32_t pos;
    while (tail != head) {
        char c = (char)ring[tail % RING];
        tail++;
        if (c == '\r')
            continue;
        if (c == '\n') {
            line[pos] = 0;
            pos = 0;
            return 1;
        }
        if (pos + 1 < size)
            line[pos++] = c;                /* длинная строка обрезается, но не портит память */
    }
    return 0;
}

Счётчики head и tail только растут, и каждый пишет лишь одна сторона. Разность head − tail — число байт в буфере; она верна и после перехода счётчиков через 2³², пока размер кольца — степень двойки. head растёт уже после записи байта, поэтому главный цикл не увидит недописанного.

Нулевые байты. Строковые функции вроде strncpy останавливаются на первом нуле, а в двоичных данных нули обычны. Кольцо копирует байты, а не строки.

Что показала модель. На компьютере один и тот же текст из четырёх команд подан разными нарезками; между пакетами главный цикл то успевает, то не успевает забрать строки.

Как пришло Кольцо и сборка строк Пакет как строка
1000 случайных нарезок по 1–64 байта все 1000 28
по одному символу, как с терминала 4 команды из 4 ни одной

Переполнение. Если главный цикл не забирает данные, кольцо на 256 байт примет ровно 256, остальное потеряет и посчитает: в модели из 300 байт пропало 44. Лучше не терять вовсе. Библиотека не примет следующий пакет, пока не вызвана USBD_CDC_ReceivePacket, — хост будет ждать. Если места в кольце меньше пакета, вызов откладывают до главного цикла — так и сделано выше: rx_paused помнит, что пакет не отпущен, а cdc_rx_resume отпускает его, когда место появилось. Компьютер тем временем сам притормозит отправку.

Передача. CDC_Transmit_FS отвечает USBD_BUSY, пока не ушёл прошлый пакет, и не копирует данные: отданный ей буфер нельзя менять до конца передачи. Ответ собирают в отдельном буфере и отправляют, когда прошлый ушёл, — с повтором, а не по принципу «отправил и забыл».

Пакет USB — не строка: команды собирают из кольца по концу строки, а не берут пакет целиком


20.8 Грабли

Устройство не определяется. По порядку: подтяжка на D+ (см. выше; у F4 — контроль VBUS на PA9), частота ровно 48 МГц, кварц вместо HSI, целостность линий D+ и D−.

Определяется как «неизвестное устройство». Ошибка в дескрипторах или опознание прерывается на середине. Смотреть анализатором протокола на стороне компьютера — он покажет, на каком запросе всё встало.

Работает на одном компьютере и не работает на другом. Классический признак проблем с подтяжкой или качеством линий: разные хосты по‑разному терпимы к отклонениям.

Пропадает после ухода в сон. USB требует периодической активности; режимы пониженного потребления (раздел 10) с ним сочетаются плохо и требуют отдельной обработки пробуждения.

CAN и USB вместе на F103. Не заработают, память общая.

Данные теряются при быстрой отправке. Хост опрашивает устройство по расписанию, а не когда вам удобно. Нужен буфер и отправка по мере готовности, а не в цикле.

Кабель. Дешёвые кабели без экрана и с тонкими проводниками дают нестабильную работу, особенно длинные. При странном поведении стоит попробовать другой кабель прежде, чем искать ошибку в коде.

Команда не узнаётся, хотя пришла. Пакет сравнивали с командой, а она пришла кусками — с терминала по одному символу. Собирать строки из кольца (20.7).

Двоичные данные обрезаются. Пакет копировали strncpy, и она остановилась на первом нулевом байте.

Ответ приходит испорченным. Буфер, отданный CDC_Transmit_FS, изменили до конца передачи.


20.9 Итоги

Дальше: раздел 20а «USB OTG и хост; 48 МГц без кварца» — микроконтроллер сам становится хостом для флешки или клавиатуры, а устройство обходится без кварца: HSI48 с подстройкой CRS по кадрам хоста и MSI от часового кварца.

Открыть в интерактивном справочнике