3. 嵌入式USB控制器:STM32系列USB外设架构、USB时钟配置、GPIO与DP/DM引脚配置
好,咱们进入正题。STM32的USB外设,说白了就是芯片内部一个专门干USB活儿的硬件模块。你不需要自己用GPIO去模拟时序,那太累了,也不现实。STM32把USB协议栈的底层物理层和数据链路层大部分工作都给你做好了,你只需要配置好它,然后往缓冲区里塞数据就行。
我个人习惯把STM32的USB外设分成三块来看:时钟域、数据路径和引脚接口。这三块搞不定,上层协议栈跑得再欢也是白搭。
3.1 STM32 USB外设架构概览
STM32的USB模块,不同系列差异还挺大。我最早用F103的时候,那个USB是Device Only的,想搞Host还得外挂PHY芯片。后来到了F4、F7系列,才集成了OTG控制器,既能当设备也能当主机。
咱们先看一张架构图,把整体脉络理清楚。
你看这个图,从CPU到DP/DM引脚,中间经过了寄存器接口、协议引擎、缓冲区管理和收发器。每一层都有它的活儿,也都有它的坑。
3.2 USB时钟配置——最容易翻车的地方
时钟配置是USB能不能正常工作的前提。STM32的USB外设对时钟精度要求很高,尤其是全速模式(12Mbps),时钟误差必须控制在±0.25%以内。我曾经在一个项目里,因为用了内部RC振荡器做时钟源,结果USB枚举时断时续,查了两天才发现是时钟漂移了。
3.2.1 时钟源选择
不同系列的STM32,USB时钟源不太一样。我整理了一个表格,方便你对照。
| STM32系列 | USB时钟源 | 典型频率 | 注意事项 |
|---|---|---|---|
| F1 / F4 | PLL输出 (48MHz) | 48MHz | 必须由HSE倍频得到,HSI精度不够 |
| F7 / H7 | PLL48CK (独立PLL) | 48MHz | 有独立的PLL专门给USB用 |
| G0 / G4 | HSI48 | 48MHz | 内部专用48MHz振荡器,精度尚可 |
| L4 / L5 | MSI / PLL | 48MHz | MSI可自动校准,但建议用PLL |
3.2.2 时钟配置实战(以STM32F4为例)
配置USB时钟,说白了就是三步:开HSE → 配PLL → 选USB时钟源。下面这段代码是我在项目里常用的配置片段。
// 1. 使能HSE外部晶振
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 8; // 8MHz / 8 = 1MHz
RCC_OscInitStruct.PLL.PLLN = 336; // 1MHz * 336 = 336MHz
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2; // 336 / 2 = 168MHz (系统时钟)
RCC_OscInitStruct.PLL.PLLQ = 7; // 336 / 7 = 48MHz (USB时钟)
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// 2. 配置时钟总线
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
| RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4; // 42MHz
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2; // 84MHz
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5);
// 3. 使能USB时钟 (48MHz来自PLLQ)
__HAL_RCC_USB_OTG_FS_CLK_ENABLE();
嗯,这里要注意:PLLQ的值必须保证输出正好是48MHz。你算一下,336 / 7 = 48,完美。如果PLLQ配错了,USB时钟不对,设备枚举就会失败。
3.3 GPIO与DP/DM引脚配置
时钟搞定了,接下来就是引脚。USB的DP和DM是差分信号线,对引脚配置有特殊要求。你不能像配置普通GPIO那样随便设个推挽输出就完事了。
3.3.1 引脚功能与复用
STM32的USB引脚通常是固定的,比如F4的OTG_FS用的是PA11(DM)和PA12(DP)。但有些系列支持引脚重映射,你得查数据手册确认。
| STM32系列 | USB模式 | DP引脚 | DM引脚 | 复用功能 |
|---|---|---|---|---|
| F103 | Device (全速) | PA12 | PA11 | AF14 (USB) |
| F407 | OTG_FS | PA12 | PA11 | AF10 (OTG_FS) |
| F429 | OTG_HS (内置PHY) | PB15 | PB14 | AF12 (OTG_HS) |
| G070 | Device (全速) | PA12 | PA11 | AF2 (USB) |
3.3.2 GPIO配置要点
配置USB引脚时,有几个关键参数必须设对。我直接给你看代码,注释里写了为什么这么配。
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 使能GPIOA时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置PA11 (DM) 和 PA12 (DP)
GPIO_InitStruct.Pin = GPIO_PIN_11 | GPIO_PIN_12;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL; // USB是差分信号,不需要上下拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 高速模式,适应12Mbps
GPIO_InitStruct.Alternate = GPIO_AF10_OTG_FS; // 复用为OTG_FS功能
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
3.3.3 上拉电阻与设备识别
USB设备枚举时,主机是通过检测D+或D-上的上拉电阻来判断设备速度的。全速设备在D+上拉,低速设备在D-上拉。STM32内部集成了这个上拉电阻,但需要软件控制。
我记得在F103上,这个上拉电阻是通过USB模块的BCDR寄存器里的DPPU位来控制的。在F4的OTG模块里,则是通过GCCFG寄存器的PWRDWN和VBUSASEN位来管理。
// F4 OTG_FS 使能内部上拉 (D+上拉,标识为全速设备)
USB_OTG_GlobalTypeDef *USBx = USB_OTG_FS;
USBx->GCCFG |= USB_OTG_GCCFG_PWRDWN; // 退出掉电模式
USBx->GCCFG |= USB_OTG_GCCFG_VBUSASEN; // 使能VBUS感知
// 等待一段时间,让主机检测到设备连接
HAL_Delay(100);
3.4 常见问题与调试思路
USB硬件配置这块,出问题最多的就三个地方。我按优先级给你排个序。
- 时钟不对:用示波器量一下PLL输出的48MHz,误差超过0.5%基本就没戏。
- 引脚复用配错:检查GPIO的AF值是不是对的,别把USB配成了UART。
- 上拉电阻没使能:主机检测不到设备连接,枚举根本不会开始。
你想想看,如果设备插上电脑后,电脑一点反应都没有(没有“叮咚”声),那八成是硬件配置的问题。先查供电,再查时钟,最后查引脚。按这个顺序来,能省不少时间。
好了,这一章的内容就到这儿。USB外设的硬件配置是基础中的基础,后面讲协议栈的时候,你会发现很多问题追根溯源都是硬件没配好。把这一章吃透,后面的路就好走了。