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

20а. USB OTG и хост; 48 МГц без кварца

Раздел 20 разобрал микроконтроллер в роли устройства: опознание, классы, виртуальный COM-порт, подтяжку на D+ и 48 МГц от кварца. Здесь — две другие стороны USB. Первая: микроконтроллер сам становится хостом, к которому подключают флешку или клавиатуру, а контроллер OTG умеет менять роль. Вторая: как устройство обходится без кварца — HSI48 с подстройкой CRS по кадрам хоста у G4 и MSI, привязанный к часовому кварцу, у L476.

Опоры: справочные руководства RM0090 (F4, глава 34 «OTG_FS»), RM0440 (G4, глава 8 «CRS» и глава RCC), RM0351 (L47x/L48x/L49x/L4Ax: MSI, CRS) и даташиты DS12288 (G474) и DS10198 (L476) из паков Keil; драйвер stm32f4xx_ll_usb.c из пака F4; библиотека USB Host от ST из архива STM32CubeF4 1.28.0 в репозитории CubeMX; примеры хоста из MDK-Middleware 8.1.0. Расчёты CRS и MSI и модель подстройки — на ПК, код собран armclang.

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

Оригинал Перевод
dual-role device (DRD) контроллер двух ролей: хост и устройство
A-device, B-device сторона A (даёт VBUS, по умолчанию хост) и сторона B
ID line вывод, по которому узнают, какой стороной вставлен кабель
HNP, SRP смена ролей по договорённости; запрос сессии у стороны A
charge pump, power switch источник 5 В и ключ питания VBUS
overcurrent перегрузка по току
host channel (pipe) канал хоста
start of frame (SOF) пакет начала кадра, раз в 1 мс
clock recovery system (CRS) блок подстройки HSI48 по внешней метке
RELOAD, FELIM число тактов между метками и порог ошибки
TRIM код подстройки генератора
MSI PLL-mode (MSIPLLEN) MSI, привязанный к часовому кварцу LSE

20а.1 Роли OTG: кто хост, кто устройство

OTG_FS у F407 — контроллер двух ролей (RM0090, 34.1). Хостом он работает на полной (12 Мбит/с) и низкой (1,5 Мбит/с) скорости, устройством — только на полной. Роль задаёт вывод ID (PA10) при вставке кабеля в розетку micro-AB:

Биты FHMOD и FDMOD в OTG_FS_GUSBCFG запирают роль: только хост или только устройство, ID тогда не смотрится. HNPCAP разрешает смену ролей по договорённости (HNP), SRPCAP — запрос сессии (SRP), при котором сторона A может выключить VBUS и включить по просьбе. Одно правило не меняется: VBUS всегда даёт сторона A, какую бы роль она ни играла сейчас (RM0090, 34.4.3).

Выводы OTG_FS у F407: PA9 — контроль VBUS, PA10 — ID, PA11 и PA12 — DM и DP. У G474 контроллер другой — только устройство (RM0440: «full-speed device interface»), хостом G4 не бывает. У L476 тоже OTG_FS — с 12 каналами хоста (20а.4).

Слева F407 хостом: внешний ключ 5 В с ограничением тока включается выводом GPIO, флаг перегрузки идёт на вывод с прерыванием, PA9 контролирует VBUS, PA10 — ID, PA11 и PA12 — линии данных. Справа G474 устройством без кварца: SOF от хоста раз в 1 мс идёт в CRS, CRS меняет TRIM генератора HSI48, HSI48 даёт 48 МГц блоку USB; пороги FELIM 34 такта, 3·FELIM 102, 128·FELIM 4352 против допуска USB 120 тактов


20а.2 Питание VBUS: ключ и перегрузка

Хост обязан дать устройству 5 В, а F407 их не вырабатывает: «On-chip 5 V VBUS generation is not supported» (RM0090, 34.6). Нужен внешний источник — повышающий преобразователь или, если 5 В на плате уже есть, простой ключ. На рисунках RM0090 это ключ с ограничением тока STMPS2141: вход включения — от любого GPIO, выход «перегрузка» — на GPIO с прерыванием.

Порядок из RM0090 (34.6.2):

  1. Включить ключ выводом GPIO и поставить бит PPWR в OTG_FS_HPRT.
  2. При перегрузке обработчик прерывания сразу выключает ключ и снимает PPWR. Сам контроллер тоже снимает PPWR, если видит перегрузку.
  3. Если включены HNP или SRP, PA9 соединяют с VBUS: падение ниже 4,25 В даёт прерывание SEDET, и питание надо снять. Без HNP и SRP PA9 к VBUS не подключают, и он свободен как GPIO.
void otg_host_port_power(int on)
{
    uint32_t h = OTG_HPRT & HPRT_KEEP;
    if (on) {
        vbus_switch(1);                             /* сначала сам ключ, потом бит PPWR */
        OTG_HPRT = h | USB_OTG_HPRT_PPWR;
    } else {
        OTG_HPRT = h & ~USB_OTG_HPRT_PPWR;
        vbus_switch(0);
    }
}

HPRT_KEEP убирает из прочитанного значения четыре бита. PENA программа может только сбросить — и выключит этим порт; PCDET, PENCHNG и POCCHNG сбрасываются записью единицы. Записать регистр обратно «как прочитали» — значит выключить порт и потерять события. Так же поступает и HAL (USB_DriveVbus, USB_ResetPort).

Бюджет тока задают в стеке: в MDK-Middleware это USBH0_HC_POWER — по умолчанию 500 мА, допустимо от 2 до 500. Обратная сторона: если устройство на STM32 питается от шины, стабилизатор 3,3 В ставят от VBUS (RM0090, примечание к 34.5).


20а.3 Опознание устройства хостом

RM0090 (34.6.2) описывает, что делает хост после подключения:

  1. Прерывание PCDET — устройство подключено: его подтяжка появилась на DP (полная скорость) или DM (низкая).
  2. Подождать DBCDNE — линия успокоилась после подключения.
  3. Сброс шины: PRST держать от 10 до 20 мс, затем снять.
  4. Прерывание PENCHNG, скорость — в поле PSPD (01 — полная, 10 — низкая). Хост начинает слать SOF раз в миллисекунду.
  5. Дальше — запросы опознания по нулевой конечной точке.
void otg_host_port_reset(void)
{
    uint32_t h = OTG_HPRT & HPRT_KEEP;
    OTG_HPRT = h | USB_OTG_HPRT_PRST;
    delay_ms(15u);
    OTG_HPRT = h & ~USB_OTG_HPRT_PRST;
}

HAL F4 держит PRST 100 мс, а потом ждёт ещё 10 (USB_ResetPort). Это не противоречит описанию бита в RM0090: там минимум 10 мс, и «стандарт USB верхней границы не задаёт». Но в 34.6.2 написано «от 10 до 20 мс», и в своём коде лучше держаться этого.

Запросы опознания — в USBH_HandleEnum библиотеки ST (usbh_core.c):

Шаг Запрос Зачем
1 GET_DESCRIPTOR (0x06), устройство, 8 байт узнать bMaxPacketSize0 и переоткрыть каналы точки 0
2 GET_DESCRIPTOR, устройство, 18 байт VID, PID, номера строк
3 SET_ADDRESS (0x05), адрес 1 уйти с адреса 0; пауза 2 мс
4 GET_DESCRIPTOR, конфигурация, 9 байт узнать wTotalLength
5 GET_DESCRIPTOR, конфигурация целиком интерфейсы и конечные точки
6 строки производителя, изделия, номера для журнала
7 SET_CONFIGURATION (0x09) устройство готово к работе

Класс библиотека выбирает по bInterfaceClass первого интерфейса. Новое устройство всегда начинает с адреса 0, а библиотека ST всегда даёт ему адрес 1 (USBH_DEVICE_ADDRESS). Шаги 1, 2, 4 и 5 при ошибке повторяются со сброса до трёх раз, потом библиотека сдаётся и просит вынуть устройство; ошибка SET_ADDRESS — сразу отказ.

Задержки в библиотеке: 200 мс после подключения, сброс (100 + 10 мс в HAL) и 100 мс после сброса — 410 мс до первого запроса.


20а.4 Каналы хоста, MSC и HID

Хост не держит конечные точки — у него каналы. У OTG_FS F407 их 8 (RM0090, 34.6.3; USB_OTG_FS_HOST_MAX_CHANNEL_NBR = 8), у OTG_FS L476 — 12 (RM0351). Каждый канал настраивается регистром HCCHARx на одну передачу: адрес устройства, номер конечной точки, направление, тип, наибольший пакет, низкая скорость. Больше передач, чем у него каналов, контроллер сразу не ведёт: драйвер перераспределяет каналы по мере их освобождения. Очереди в железе: до 8 периодических запросов (прерывающие и изохронные) и до 8 непериодических (управляющие и массовые). Общая память FIFO — 1,25 КБ: общий приёмный FIFO и два передающих.

Управляющей точке нужно два канала — вход и выход (pipe_in, pipe_out в библиотеке ST). Флешке сверх этого — ещё два под массовые точки: из восьми каналов заняты четыре. В HAL канал открывает HAL_HCD_HC_Init (номер канала, точка, адрес, скорость, тип, наибольший пакет), передачу — HAL_HCD_HC_SubmitRequest.

Классы коротко:

Класс Код Что внутри MDK-Middleware
MSC, флешка 0x08 Bulk-Only (0x50) + команды SCSI (0x06) USBH_MSC_GetStatus, диск U0: через fmount
HID, клавиатура и мышь 0x03 загрузочный протокол: клавиатура 0x01, мышь 0x02 USBH_HID_GetKeyboardKey, USBH_HID_GetMouseState

Коды — из usbh_msc.h и usbh_hid.h библиотеки ST. В MDK-Middleware хост запускает USBH_Initialize(0), дальше пример флешки опрашивает USBH_MSC_DriveGetMediaStatus("U0:") и пишет файл обычным fopen. В шаблоне настройки хоста USBH_Config.h — не больше 3 каналов сразу (USBH0_HC_PIPE_NUM) и пул памяти 640 байт: не меньше наибольшего пакета плюс 128. Пример флешки берёт пул 4224 байта.


20а.5 Точность 48 МГц и блок CRS

USB Full Speed требует 48 МГц с точностью ±0,25 % (раздел 20.2). За кадр 1 мс это 48000 тактов и допуск ±120 тактов.

HSI48 у G474 после заводской калибровки — ±3 % при VDD 3,0–3,6 В и −15…85 °C, ±4,5 % во всём диапазоне (DS12288, таблица 44). Это в 12 и 18 раз хуже нужного. Поэтому HSI48 не работает для USB один — его подстраивает CRS. Шаг подстройки TRIM по даташиту — 0,11 % типично, 0,18 % наибольший; RM0440 для расчёта берёт 0,14 %. У G4 TRIM — 7 бит, после сброса 64 (середина); у L4 — 6 бит, после сброса 32.

Как работает CRS (RM0440, 8.4.3–8.4.4). По каждой метке SYNC — обычно SOF от хоста — 16-битный счётчик загружается числом RELOAD и считает такты HSI48 вниз до нуля, потом вверх. В момент следующей метки его значение защёлкивается в FECAP, направление — в FEDIR. Метка пришла, пока счёт шёл вниз, — генератор медленный, TRIM надо увеличить; когда шёл вверх — быстрый. Дальше ошибка сравнивается с порогами:

Ошибка Флаг Автоподстройка
меньше FELIM SYNCOK TRIM не меняется
от FELIM до 3·FELIM SYNCOK ±1 шаг
от 3·FELIM до 128·FELIM SYNCWARN ±2 шага
128·FELIM и больше SYNCERR или SYNCMISS TRIM не меняется

Формулы из RM0440 (8.4.5):

RELOAD = fTARGET / fSYNC − 1, FELIM = (fTARGET / fSYNC) × STEP / 100 % / 2, с округлением вверх.

Метка fSYNC RELOAD FELIM при шаге 0,14 % CRS_CFGR
SOF 1 кГц 47999 (0xBB7F) 33,6 → 34 (0x22) 0x2022BB7F
LSE / 32 1024 Гц 46874 (0xB71A) 33 0x1521B71A

Для SOF расчёт дал ровно значение CRS_CFGR после сброса — 0x2022BB7F: источник SOF, делитель 1, FELIM 34, RELOAD 47999. RM0440 так и пишет: сброс рассчитан на 48 МГц, 1 кГц и шаг 0,14 %. Для часового кварца делитель 32 даёт 1024 Гц, и 48 МГц делятся на них нацело — 46875 тактов. При шаге 0,11 % FELIM был бы 27, при 0,18 % — 44. Условие RM «RELOAD больше 128·FELIM» выполняется во всех трёх случаях: самое большое 128·FELIM — 5632 против 47999.

В процентах: FELIM 34 такта — 0,0708 %, 3·FELIM — 0,2125 %, 128·FELIM — 9,0667 %. Шаг 0,14 % — 67,2 такта за кадр.


20а.6 Подстройка: модель, код, MSI у L476

Модель на ПК. Частота HSI48 линейно зависит от TRIM, начальная ошибка — от −3 до +3 % через 0,01 % (601 случай), по каждому SOF — правило из таблицы выше:

Шаг FELIM TRIM качается До покоя, мс Наибольший остаток
0,14 % 34 0 из 601 11 0,07 %
0,11 % 27 0 из 601 14 0,05 %
0,18 % 44 0 из 601 9 0,09 %
0,18 % 34 102 из 601 9 0,10 %

Пример: при ошибке +2,5 % и шаге 0,14 % TRIM идёт 64 → 46 по два шага за кадр и за 9 мс встаёт; остаток −0,02 %. Последняя строка — FELIM после сброса у кристалла с наибольшим шагом: окно ±34 такта уже шага в 86,4 такта, и в 102 случаях из 601 подходящего TRIM нет — код прыгает между двумя значениями каждый кадр. В допуск это всё равно укладывается (0,10 % против 0,25 %), но правило RM «FELIM — половина шага» модель подтверждает.

Код G474. Сначала HSI48 и выбор его для USB:

void usb_clock_hsi48(void)
{
    RCC->CRRCR |= RCC_CRRCR_HSI48ON;
    while (!(RCC->CRRCR & RCC_CRRCR_HSI48RDY)) {
    }
    RCC->CCIPR &= ~RCC_CCIPR_CLK48SEL;              /* 00: HSI48 → USB и RNG */
    RCC->APB1ENR1 |= RCC_APB1ENR1_CRSEN | RCC_APB1ENR1_USBEN;
}

Потом CRS. CRS_CFGR пишется только при CEN = 0: после включения счётчика регистр закрыт.

void crs_start_sof(void)
{
    CRS->CFGR = CFGR_SOF;
    CRS->CR |= CRS_CR_AUTOTRIMEN | CRS_CR_CEN;
}

CFGR_SOF считается макросом по формулам RM0440, а _Static_assert в файле проверяет, что это 0x2022BB7F. armclang собирает функцию так:

MOVW     r0,#0x2000
MOV      r1,#0xbb7f
MOVT     r0,#0x4000
MOVT     r1,#0x2022
STR      r1,[r0,#4]
LDR      r1,[r0,#0]
ORR      r1,r1,#0x60
STR      r1,[r0,#0]
BX       lr

0x2022BB7F — в CRS_CFGR (0x4000 2004), затем биты 6 и 5 (AUTOTRIMEN и CEN, 0x60) — в CRS_CR (0x4000 2000). Ошибку последнего кадра в тактах со знаком даёт crs_error_ticks() из FECAP и FEDIR.

MSI в режиме PLL у L476. HSI48 в линейке RM0351 есть только у L49x и L4Ax; у L476 его нет. Взамен MSI на 48 МГц привязывают к часовому кварцу LSE битом MSIPLLEN (RM0351, 6.2.3): «At 48 MHz, the MSI in PLL-mode can be used for the USB OTG FS device». DS10198 даёт в этом режиме для диапазона 11 типично 48,005 МГц. Модель — ближайшее целое кратное 32768 Гц — совпала с таблицей 60 даташита во всех 12 диапазонах. Для 48 МГц: 48000000 / 32768 = 1464,84375 → 1465, 1465 × 32768 = 48005120 Гц, ошибка +106,7 ppm = +0,0107 %. К ней добавляется погрешность часового кварца — по словам RM, обычно несколько десятков ppm. Даташит обещает «лучше ±0,25 %».

void usb_clock_msi_pll(void)
{
    RCC->APB1ENR1 |= RCC_APB1ENR1_PWREN;
    PWR->CR1 |= PWR_CR1_DBP;                        /* LSE живёт в домене резервного питания */
    RCC->BDCR |= RCC_BDCR_LSEON;
    while (!(RCC->BDCR & RCC_BDCR_LSERDY)) {
    }
    /* после сброса MSI — системный такт: сначала такты ожидания флеш-памяти */
    FLASH->ACR = (FLASH->ACR & ~FLASH_ACR_LATENCY) | FLASH_ACR_LATENCY_2WS;
    while ((FLASH->ACR & FLASH_ACR_LATENCY) != FLASH_ACR_LATENCY_2WS) {
    }
    while (!(RCC->CR & RCC_CR_MSIRDY)) {            /* MSIRANGE менять только при MSIRDY = 1 */
    }
    RCC->CR = (RCC->CR & ~RCC_CR_MSIRANGE) | RCC_CR_MSIRANGE_11 | RCC_CR_MSIRGSEL;
    RCC->CR |= RCC_CR_MSIPLLEN;                     /* только после LSERDY: иначе бит не встанет */
    RCC->CCIPR |= RCC_CCIPR_CLK48SEL;               /* 11: MSI → USB OTG FS */
}

48 МГц при напряжении ядра в диапазоне 1 требуют 2 такта ожидания флеш-памяти (RM0351, таблица 11) — а MSI после сброса и есть системный такт, поэтому задержку ставят до смены диапазона.

И HSI48 с CRS, и MSI от LSE документы обещают только для устройства. Хосту подстраиваться не по чему — он сам шлёт SOF, и его кадр должен быть точным: при 48 МГц драйвер F4 пишет в HFIR 48000 тактов на кадр. Хосту нужен кварц (раздел 20.2).


20а.7 Грабли

Порт хоста выключился сам после записи в HPRT. Записали регистр обратно с прочитанным PENA = 1 — это сброс бита, то есть выключение порта. Перед записью убирать PENA, PCDET, PENCHNG, POCCHNG.

Флешка не видна, 5 В на разъёме нет. F407 не вырабатывает VBUS: нужен внешний ключ, включённый GPIO, и бит PPWR.

Короткое замыкание в устройстве — и горит ключ или стабилизатор. Флаг перегрузки не заведён на прерывание. По RM0090 обработчик выключает питание немедленно.

Хост не видит подключения при включённых HNP или SRP. В этом режиме RM0090 сообщает о подключении, только когда VBUS на допустимом уровне: PA9 должен быть соединён с VBUS.

CRS_CFGR не меняется. Его пишут до CEN = 1 — потом регистр защищён. Чтобы сменить источник, сначала снять CEN.

Ошибки CRS во сне с LPM. В режиме сна LPM хост не шлёт SOF, и подстраивать нечем; RM0440 советует брать метку от LSE или внешнего сигнала на выводе — это crs_start_lse().

После Stop USB молчит. Бит HSI48ON снимается железом при входе в Stop, регистры CRS замерзают; после Standby CRS надо настроить заново (RM0440, 8.5). HSI48 включают снова после пробуждения.

Без кварца на морозе. DS12288 разрешает USB без кварца только от −15 до 85 °C.

MSIPLLEN не ставится. LSE ещё не готов: железо защищает бит, пока нет LSERDY, и снимает его, если LSE пропал.

L476 зависает при переходе MSI на 48 МГц. Не поставлены 2 такта ожидания флеш-памяти до смены диапазона.

Хост на HSI48 или MSI. Подстраиваться не по чему — нужен кварц.


20а.8 Итоги

Полные исходники

usbh20a_f4.c — F407: порт хоста OTG_FS — питание VBUS и сброс шины.

/* 20а: F407 — порт хоста OTG_FS по регистрам: питание VBUS и сброс шины (RM0090, 34.6.2).
   Собирается armclang: -DSTM32F407xx -include stm32f4xx.h. */
#include <stdint.h>

#define OTG_HPRT    (*(volatile uint32_t *)(USB_OTG_FS_PERIPH_BASE + USB_OTG_HOST_PORT_BASE))
/* PENA программа может только сбросить (и выключить порт), три флага сбрасываются записью 1 */
#define HPRT_KEEP   (~(USB_OTG_HPRT_PENA | USB_OTG_HPRT_PCDET | USB_OTG_HPRT_PENCHNG | USB_OTG_HPRT_POCCHNG))

_Static_assert(USB_OTG_FS_HOST_MAX_CHANNEL_NBR == 8u, "у OTG_FS F407 восемь каналов хоста");
_Static_assert(USB_OTG_FS_PERIPH_BASE + USB_OTG_HOST_PORT_BASE == 0x50000440u, "OTG_FS_HPRT — 0x5000 0440");
_Static_assert(USB_OTG_HPRT_PPWR == (1u << 12) && USB_OTG_HPRT_PRST == (1u << 8), "PPWR — бит 12, PRST — бит 8");
_Static_assert(USB_OTG_GUSBCFG_FHMOD == (1u << 29), "FHMOD — только хост, вывод ID не смотрится");

extern void vbus_switch(int on);                    /* ключ 5 В с ограничением тока — выбранный GPIO */
extern void delay_ms(uint32_t ms);

void otg_host_port_power(int on)
{
    uint32_t h = OTG_HPRT & HPRT_KEEP;
    if (on) {
        vbus_switch(1);                             /* сначала сам ключ, потом бит PPWR */
        OTG_HPRT = h | USB_OTG_HPRT_PPWR;
    } else {
        OTG_HPRT = h & ~USB_OTG_HPRT_PPWR;
        vbus_switch(0);
    }
}

/* сброс шины: PRST держать от 10 до 20 мс; конец — прерывание PENCHNG, скорость в PSPD */
void otg_host_port_reset(void)
{
    uint32_t h = OTG_HPRT & HPRT_KEEP;
    OTG_HPRT = h | USB_OTG_HPRT_PRST;
    delay_ms(15u);
    OTG_HPRT = h & ~USB_OTG_HPRT_PRST;
}

usbh20a_g4.c — G474: USB без кварца — HSI48 и подстройка CRS по SOF.

/* 20а: G474 — USB-устройство без кварца: HSI48 и подстройка CRS по SOF (RM0440, глава 8).
   Собирается armclang: -DSTM32G474xx -include stm32g4xx.h. */
#include <stdint.h>

#define F_TARGET    48000000u               /* такт USB */
#define F_SOF       1000u                   /* SOF от хоста — раз в 1 мс */
#define STEP_X100   14u                     /* шаг TRIM 0,14 % — из RM0440, в сотых долях процента */

/* RM0440, 8.4.5: RELOAD = fTARGET / fSYNC − 1; FELIM = (fTARGET / fSYNC) · STEP / 100 % / 2, вверх */
#define CRS_RELOAD(fs)      (F_TARGET / (fs) - 1u)
#define CRS_FELIM(fs)       (((F_TARGET / (fs)) * STEP_X100 + 19999u) / 20000u)
#define CRS_CFGR_VAL(src, div, fs) \
    (((src) << CRS_CFGR_SYNCSRC_Pos) | ((div) << CRS_CFGR_SYNCDIV_Pos) \
     | (CRS_FELIM(fs) << CRS_CFGR_FELIM_Pos) | (CRS_RELOAD(fs) << CRS_CFGR_RELOAD_Pos))

#define SRC_LSE     1u                      /* SYNCSRC = 01 */
#define SRC_SOF     2u                      /* SYNCSRC = 10 */
#define DIV_32      5u                      /* SYNCDIV = 101: 32 768 / 32 = 1024 Гц */
#define F_LSE_32    (32768u / 32u)

#define CFGR_SOF    CRS_CFGR_VAL(SRC_SOF, 0u, F_SOF)
#define CFGR_LSE    CRS_CFGR_VAL(SRC_LSE, DIV_32, F_LSE_32)

_Static_assert(CRS_RELOAD(F_SOF) == 47999u, "RELOAD для SOF — 47999 (0xBB7F)");
_Static_assert(CRS_FELIM(F_SOF) == 34u, "FELIM: 48000 · 0,14 % / 2 = 33,6 → 34");
_Static_assert(CFGR_SOF == 0x2022BB7Fu, "для 48 МГц и SOF CRS_CFGR совпадает со значением после сброса");
_Static_assert(CRS_RELOAD(F_SOF) > 128u * CRS_FELIM(F_SOF), "RELOAD больше OUTRANGE = 128 · FELIM");
_Static_assert(F_TARGET % F_LSE_32 == 0u, "48 МГц делится на 1024 Гц нацело: 46875 тактов");
_Static_assert(CFGR_LSE == 0x1521B71Au, "LSE / 32: RELOAD 46874, FELIM 33");
_Static_assert(CRS_CR_TRIM_Msk == (0x7Fu << 8), "у G4 TRIM — 7 бит (у L4 — 6)");
_Static_assert(RCC_CCIPR_CLK48SEL_Pos == 26u, "CLK48SEL — биты 27:26, 00 — HSI48");

/* включить HSI48 и отдать его USB */
void usb_clock_hsi48(void)
{
    RCC->CRRCR |= RCC_CRRCR_HSI48ON;
    while (!(RCC->CRRCR & RCC_CRRCR_HSI48RDY)) {
    }
    RCC->CCIPR &= ~RCC_CCIPR_CLK48SEL;              /* 00: HSI48 → USB и RNG */
    RCC->APB1ENR1 |= RCC_APB1ENR1_CRSEN | RCC_APB1ENR1_USBEN;
}

/* подстройка по SOF: CFGR пишется только при CEN = 0, потом он закрыт */
void crs_start_sof(void)
{
    CRS->CFGR = CFGR_SOF;
    CRS->CR |= CRS_CR_AUTOTRIMEN | CRS_CR_CEN;
}

/* подстройка по часовому кварцу LSE / 32 — когда SOF нет (сон LPM, нет хоста) */
void crs_start_lse(void)
{
    CRS->CR &= ~CRS_CR_CEN;
    CRS->CFGR = CFGR_LSE;
    CRS->CR |= CRS_CR_AUTOTRIMEN | CRS_CR_CEN;
}

/* ошибка последнего кадра в тактах HSI48: плюс — частота выше 48 МГц */
int32_t crs_error_ticks(void)
{
    uint32_t isr = CRS->ISR;
    int32_t fecap = (int32_t)((isr & CRS_ISR_FECAP_Msk) >> CRS_ISR_FECAP_Pos);
    return (isr & CRS_ISR_FEDIR) ? -fecap : fecap;  /* FEDIR = 1: счёт вниз, частота ниже */
}

usbh20a_l4.c — L476: MSI 48 МГц в режиме PLL от LSE.

/* 20а: L476 — USB без кварца 8 МГц: MSI 48 МГц в режиме PLL от LSE (RM0351, 6.2.3).
   Собирается armclang: -DSTM32L476xx -include stm32l4xx.h. */
#include <stdint.h>

_Static_assert(RCC_CR_MSIRANGE_11 == (0xBu << RCC_CR_MSIRANGE_Pos), "диапазон 11 — около 48 МГц");
_Static_assert(RCC_CR_MSIPLLEN == (1u << 2), "MSIPLLEN — бит 2 RCC_CR");
_Static_assert(RCC_CCIPR_CLK48SEL == (3u << 26), "CLK48SEL = 11 — MSI для USB OTG FS");
_Static_assert(FLASH_ACR_LATENCY_2WS == 2u, "48 МГц в диапазоне 1 — 2 такта ожидания");

void usb_clock_msi_pll(void)
{
    RCC->APB1ENR1 |= RCC_APB1ENR1_PWREN;
    PWR->CR1 |= PWR_CR1_DBP;                        /* LSE живёт в домене резервного питания */
    RCC->BDCR |= RCC_BDCR_LSEON;
    while (!(RCC->BDCR & RCC_BDCR_LSERDY)) {
    }
    /* после сброса MSI — системный такт: сначала такты ожидания флеш-памяти */
    FLASH->ACR = (FLASH->ACR & ~FLASH_ACR_LATENCY) | FLASH_ACR_LATENCY_2WS;
    while ((FLASH->ACR & FLASH_ACR_LATENCY) != FLASH_ACR_LATENCY_2WS) {
    }
    while (!(RCC->CR & RCC_CR_MSIRDY)) {            /* MSIRANGE менять только при MSIRDY = 1 */
    }
    RCC->CR = (RCC->CR & ~RCC_CR_MSIRANGE) | RCC_CR_MSIRANGE_11 | RCC_CR_MSIRGSEL;
    RCC->CR |= RCC_CR_MSIPLLEN;                     /* только после LSERDY: иначе бит не встанет */
    RCC->CCIPR |= RCC_CCIPR_CLK48SEL;               /* 11: MSI → USB OTG FS */
}

Дальше: раздел 20б «USB-C и UCPD: питание и роли по кабелю» — как разъём USB-C сам договаривается, кто хост и кто даёт питание, и как G474 делает это блоком UCPD.

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