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:
- кабель вставлен стороной B, провод ID висит — встроенная подтяжка видит единицу, роль устройства (она и по умолчанию);
- стороной A, где ID замкнут на землю, — прерывание
CIDSCHG, контроллер сам переходит в роль хоста.
Биты 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).
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):
- Включить ключ выводом GPIO и поставить бит
PPWRвOTG_FS_HPRT. - При перегрузке обработчик прерывания сразу выключает ключ
и снимает
PPWR. Сам контроллер тоже снимаетPPWR, если видит перегрузку. - Если включены 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) описывает, что делает хост после подключения:
- Прерывание
PCDET— устройство подключено: его подтяжка появилась на DP (полная скорость) или DM (низкая). - Подождать
DBCDNE— линия успокоилась после подключения. - Сброс шины:
PRSTдержать от 10 до 20 мс, затем снять. - Прерывание
PENCHNG, скорость — в полеPSPD(01 — полная, 10 — низкая). Хост начинает слать SOF раз в миллисекунду. - Дальше — запросы опознания по нулевой конечной точке.
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 Итоги
- OTG_FS F407 — две роли: вывод ID на земле — хост, висит — устройство;
FHMODиFDMODзапирают роль. VBUS всегда даёт сторона A. - Хост на F407 — внешний ключ 5 В с ограничением тока, бит
PPWR, флаг перегрузки на прерывание; порог VBUS при HNP и SRP — 4,25 В. - Опознание: сброс 10–20 мс, 8 байт дескриптора, 18 байт, адрес 1, конфигурация 9 байт и целиком, SET_CONFIGURATION; в библиотеке ST 410 мс задержек до первого запроса.
- Каналов хоста у OTG_FS F407 — 8, у L476 — 12; FIFO — 1,25 КБ; MSC — класс 0x08, HID — 0x03.
- Допуск USB ±0,25 % — ±120 тактов за кадр; HSI48 G474 без подстройки —
±3 %. CRS по SOF: RELOAD 47999, FELIM 34,
CRS_CFGR= 0x2022BB7F — значение после сброса; от LSE / 32 — 46874 и 33. - Модель: от ±3 % CRS выходит на точность за 9–14 мс, остаток 0,05–0,09 %; FELIM меньше половины шага — TRIM качается каждый кадр.
- L476: MSI в режиме PLL от LSE — 1465 × 32768 = 48005120 Гц, +0,0107 %.
- Без кварца — только устройство; хосту нужен кварц.
Полные исходники
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.