尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

STM32中USB为何抢占DP/DM引脚?GPIO配置失效的底层原理与解决

STM32中USB为何抢占DP/DM引脚?GPIO配置失效的底层原理与解决 “为什么我把 DP/DM 配置成普通 GPIO 之后电平还是乱跳甚至不受控制”、“为什么我初始化了 USB但没碰那两个引脚它们却再也无法当作 GPIO 使用了”这几个问题在这些年玩 STM32 的过程中被问过无数次也是社区里反复出现的典型问题。最近把“FAQ: Why USB can take control of shared DP/DM pins on STM32”这个问题重新整理了一遍今天干脆把所有底层逻辑、实际场景和排查办法一次性讲清楚。这篇文章适合正在用 STM32F1/F4/H7 系列做 USB 相关开发的朋友尤其是想把 USB 引脚复用成普通 GPIO、或者反过来想彻底搞懂 USB 为什么能“抢”走引脚控制权的开发者。1. 先搞清楚现象背后的本质到底谁在“抢”引脚先直接说结论DP/DM 引脚被 USB 控制不是因为 USB 外设比 GPIO 外设优先级高也不是因为芯片“默认把引脚分给 USB”而是因为你在系统层面同时满足了两个条件——USB 外设时钟被使能并且引脚被配置成了复用功能Alternate Function模式。STM32 的引脚并不是由某个外设“永久拥有”的。所有引脚默认归 GPIO 模块管理你可以通过配置寄存器把引脚交给某个复用外设比如 USART、SPI、USB这就是所谓的“复用功能”。关键在于GPIO 模块本身只是一个“开关矩阵”它控制的是引脚最终连接到哪个电路模块而不是只能连接到一个固定的外设。1.1 引脚控制权切换的核心寄存器以 F4 系列为例每个 GPIO 引脚的控制权由下面这些寄存器共同决定GPIOx_MODER模式选择。00 表示输入01 表示普通输出10 表示复用功能11 表示模拟输入。GPIOx_AFRL / GPIOx_AFRH复用功能选择。每个引脚 4 位用于在这个引脚的可用复用功能列表里选择一个具体的外设。RCC-AHB1ENR外设时钟使能必须先把 GPIO 外设和 USB 外设的时钟打开。当 USB 外设时钟被使能并且 DP/DM 引脚被设置为复用模式AF10USB_OTG_FS时这两个引脚的电路路径就接通到了 USB 物理层收发器Transceiver上。此时你再用 GPIO 寄存器去读写这个引脚物理上已经无从下手因为引脚内部已经和 GPIO 输入输出驱动电路断开了连接。这里有第一个容易被忽略的细节GPIO 的输入数据寄存器IDR在复用模式下仍然可以读到引脚的电平因为输入通路是共用的。但输出通路已经不再受输出数据寄存器ODR控制。很多人就是在这里踩坑——明明配置了输出模式为什么 ODR 写了 0 引脚还是高电平因为引脚的输出路径已经被 USB 收发器接管了。1.2 一个通俗的类比你可以把引脚想象成一个会议室门口的分线器。默认情况下会议室的控制权在“GPIO 管理员”手里管理员可以决定这个门是进输入、出输出还是做别的用途。当你把引脚配置成复用功能时相当于管理员把门禁卡交给了“USB 外设”这个小队从此这扇门的进出规则由 USB 小队决定管理员再去按门铃、刷卡都没用因为门禁系统的控制端已经切换到 USB 小队那边了。这也是为什么很多初学者明明在 CubeMX 里把 PA11/PA12 配置成了普通输出但板子上电后引脚电平却异常——时钟、复用配置、外设初始化这三者只要有一个环节无意中把 USB 功能激活了DP/DM 就会被“抢”走。2. 深入解析 USB 外设凭什么能“霸占”DP/DM 引脚要彻底理解这个问题不能只看寄存器还得知道 USB 的工作原理。USB 和 SPI、UART 这类外设最大的不同在于USB 通信不只是一个数字信号进出的过程它需要物理层参与电平检测、速度识别和线路状态控制。2.1 USB 物理层与上拉电阻的秘密标准 USB 2.0 低速/全速设备端DP/DM 线上会有一个 1.5 kΩ 上拉电阻全速设备上拉到 DP低速设备上拉到 DM主机端通过检测这个上拉电阻来识别设备是否插入。这个上拉电阻通常集成在 MCU 内部 USB 收发器电路里由 USB 外设的寄存器控制。这个设计带来的直接后果是USB 外设必须维持对 DP/DM 引脚的直接控制才能实现上拉电阻的接入和断开。否则USB 设备将无法被主机正确识别。STM32 内部 USB FS PHY物理层收发器包含高速/全速差分信号驱动器差分输出差分接收器差分输入内部上拉/下拉电阻的控制逻辑线路状态监测电路SE0、J 态、K 态这些功能全都需要直接连接到 DP/DM 引脚。而普通 GPIO 模式只提供简单的数字输入/输出没有差分驱动能力和线路状态感知能力所以 USB 外设一旦启用就必须完全接管这些引脚没有任何折中方案。2.2 为什么“只是打开了 USB 时钟”也会导致问题很多 STM32 标准外设库的老代码有这样一个习惯把所有外设的 RCC 时钟一次性全部打开用了RCC-AHB1ENR | ...或者__HAL_RCC_USB_OTG_FS_CLK_ENABLE()之后就不管了。这里的关键是仅仅使能 USB 时钟并不会让 USB 外设立刻接管引脚。真正让引脚被接管的是“USB 外设时钟使能 USB 外设初始化代码执行包括软件复位、启用收发器 GPIO 配置为 AF 模式”这个组合。但问题是初始化代码里的顺序稍微一变就可能产生不可预期的状态。举个例子F4 系列标准库/LL 库常见的 USB 初始化流程__HAL_RCC_USB_OTG_FS_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_11 | GPIO_PIN_12; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF10_OTG_FS; HAL_GPIO_Init(GPIOA, GPIO_InitStruct);这段代码执行完之后PA11/PA12 就已经完全交给 USB 控制器了。但如果你的应用只在别的地方用 GPIO 模式初始化了 PA11/PA12却没有把Alternate改回来那么后来者USB 初始化就会覆盖之前所有 GPIO 配置。问题往往出现在这里即使你不打算用 USB只要工程里某个库文件、或者启动代码里默认调用了 USB 相关的初始化函数DP/DM 就可能被切到复用模式。2.3 真正“抢控制权”的三个条件结合应用场景可以总结出 USB 真正夺取 DP/DM 引脚控制权的完整条件链USB 外设时钟已使能RCC 层。GPIO 引脚的 MODER 被设置为复用模式10并且 AFRL/AFRH 被配置为 OTG_FS 功能。USB 内核被初始化即通过USB_OTG_FS-GCCFG或 HAL 库中的HAL_PCD_Init()使能了内部收发器以及上拉电阻。三者缺一不可。如果你只是开了 USB 时钟但没有执行 USB 初始化和 GPIO 复用配置引脚不会真的被接管。反过来说只要这三个条件中的前两个被满足——哪怕 USB 初始化代码执行到一半就崩了——引脚也已经被切到 USB 模式通常表现为引脚保持某个固定电平或出现不可控的波形。3. 从实际开发场景拆解“USB 接管 DP/DM”的几种典型情况这里结合我在实际项目中踩过的坑把最常见的几种“USB 抢引脚”场景整理一下。3.1 场景 ACubeMX 里配置了 USB但后面又想把引脚改成 GPIO这是最典型的一个场景。用 CubeMX 创建工程时勾选了 USB_OTG_FSDevice 模式CubeMX 自动生成了MX_USB_DEVICE_Init()和HAL_PCD_MspInit()等函数。之后你发现不需要 USB 功能了于是把引脚改成普通 GPIO但忘了在 main 函数里删掉MX_USB_DEVICE_Init()调用。结果就是GPIO 初始化之后紧接着又执行了 USB 初始化PA11/PA12 再次被切到复用模式最终结果和你预期完全相反。这个场景的排查办法很简单在调试器里查看GPIOA-MODER寄存器的值。如果第 11、12 位的值是10复用模式那就说明确实有代码把引脚切到了复用功能。3.2 场景 B外设库默认使能了 USB 时钟但代码里没有显式初始化 USB有些封装好的 BSP 或板级支持包类似潘多拉、正点原子等开发板例程会在SystemInit()或者某个bsp.c文件里提前把所有外设时钟都打开包括 USB。这种“一刀切”式时钟管理方式很容易把 USB 的 PHY 供电也打开虽然没有执行完整初始化但 OTG_FS 内核已经处于上电状态。典型表现是程序里完全没碰 USB 相关 API但 DP/DM 引脚测量下来有电平变化甚至有些开发板上的 USB 指示灯会亮。这种问题在标准外设库的stm32f4xx_hal_msp.c里特别常见因为很多模板工程的HAL_MspInit()里会默认调用__HAL_RCC_USB_OTG_FS_CLK_ENABLE()。3.3 场景 CDFU 或 Bootloader 残留状态干扰另外一个容易被忽略的场景是芯片启动时如果当前处于 DFU 模式Device Firmware UpgradeUSB Bootloader 会接管 DP/DM 引脚。这种情况下即使你自己的应用程序还没来得及初始化 GPIOUSB 引导代码已经在上拉电阻上工作DP 引脚会一直保持高电平。这种情况的表现非常迷惑程序刚刚启动时一切正常很快 PA11/PA12 出现波形但你的代码里根本没有 USB 初始化。实际原因就是芯片进入了系统存储器 Bootloader通过 BOOT0/BOOT1 引脚或选项字节设置而不是用户 Flash 启动。提示如果你用 ST-Link Utility 或者 STM32CubeProgrammer 连接目标板时偶尔会发现芯片枚举出一个 USB 设备那基本可以肯定是 Bootloader 或调试工具把 USB 接口激活了。4. 实操如何正确地把共享 DP/DM 引脚收回为普通 GPIO说完了原理和场景直接给出一套完整的操作流程。这里以 STM32F407 的 PA11/PA12OTG_FS_DM/OTG_FS_DP为例分别给出 CubeMX 配置方式和寄存器操作方式。4.1 第一步确认当前引脚状态在动手之前先用调试器读取以下寄存器确认引脚当前的实际配置// 读取 GPIOA 的模式配置 uint32_t moder GPIOA-MODER; uint32_t afrl GPIOA-AFRL; // PA11 对应 MODER[23:22]PA12 对应 MODER[25:24] // AFRL[15:12] 对应 PA11 的复用功能AFRL[19:16] 对应 PA12如果 MODER 的对应位是0b10并且 AFRL 的值是0b1010AF10 即 OTG_FS说明引脚确实处于 USB 复用模式下。4.2 第二步关闭 USB 相关时钟和初始化调用如果确定完全不需要 USB 功能最简单的办法是不要使能 USB 外设时钟。检查RCC-AHB2ENRF4 系列 USB OTG FS 挂在 AHB2 上// 读取并清除 OTG_FS 时钟使能位 uint32_t ahb2enr RCC-AHB2ENR; if (ahb2enr RCC_AHB2ENR_OTGFSEN) { RCC-AHB2ENR ~RCC_AHB2ENR_OTGFSEN; }当然更推荐在工程初期就直接删掉 CubeMX 生成的 USB 相关初始化代码包括MX_USB_DEVICE_Init()HAL_PCD_MspInit()中与 USB 时钟、GPIO、中断相关的代码usbd_*相关源文件可以不从工程中编译4.3 第三步正确配置为普通 GPIO用标准 HAL 库方式把 PA11/PA12 配置成普通推挽输出__HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_11 | GPIO_PIN_12; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_11, GPIO_PIN_RESET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_12, GPIO_PIN_RESET);这里有一个非常关键的细节只要 USB 外设时钟还处于使能状态即使 GPIO 配置成输出模式DP/DM 也可能被内部上拉电阻影响。因为 USB 收发器的上拉控制逻辑和 GPIO 输出驱动是并联在引脚上的内部上拉电阻会把引脚拉到一个中间电平导致输出 0 时引脚实际电压不是理想的 0V。解决办法就是上面说的彻底关闭 USB 时钟或者在不需要 USB 时把USB_OTG_FS-GCCFG中的上拉使能位清除。// 关闭 USB 内部上拉电阻 USB_OTG_FS-GCCFG ~USB_OTG_FS_GCCFG_PWRDWN;4.4 第四步确认是否真的释放成功配置完成后再读一次寄存器uint32_t new_moder GPIOA-MODER; // 期望PA11/PA12 的 MODER 位为 0b01普通输出同时用万用表或示波器测量引脚电平正常情况下应该能被 GPIO 的 ODR 寄存器稳定控制。5. 如果只是临时借用 DP/DM 引脚更优雅的切换方案有时候也不是完全不需要 USB只是希望在某些特定工况下把 DP/DM 临时充当普通 IO等退出这个工况后再切回 USB。这种“动态切换”需求在项目里也很常见比如“先检测一个外部信号再启动 USB 通信”。这种场景不能简单地“初始化个 GPIO 就算完”必须做好完整的切换流程。5.1 从 USB 模式切到 GPIO 模式的标准步骤停用 USB 外设调用HAL_PCD_Stop()或直接置位USB_OTG_FS-GCCFG中的掉电位PWRDWN关闭收发器。禁用 USB 中断NVIC_DisableIRQ(OTG_FS_IRQn)避免切换过程中 USB 中断来捣乱。清除 USB 时钟关闭RCC-AHB2ENR中的 OTGFSEN 位。重新配置 GPIO把 PA11/PA12 的 MODER 改为普通输出/输入模式并按需配置 AFRL。延时稳定切换后加一个小延时例如 1~2 ms让引脚电平稳定后再操作。void usb_to_gpio_switch(void) { // 1. 停止 USB HAL_PCD_Stop(hpcd); // 2. 关闭中断 NVIC_DisableIRQ(OTG_FS_IRQn); // 3. 关闭 USB 时钟 __HAL_RCC_USB_OTG_FS_CLK_DISABLE(); // 4. 重新配置引脚为 GPIO GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_11 | GPIO_PIN_12; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); }5.2 从 GPIO 模式切回 USB 模式的标准步骤切换回来相对复杂一些因为 USB 外设需要完整的重新初始化包括时钟、GPIO、中断和 PHY 配置。void gpio_to_usb_switch(void) { // 1. 重新使能 USB 时钟 __HAL_RCC_USB_OTG_FS_CLK_ENABLE(); // 2. 重新配置 PA11/PA12 为 AF10 GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_11 | GPIO_PIN_12; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate GPIO_AF10_OTG_FS; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); // 3. 重新初始化 USB HAL_PCD_Init(hpcd); HAL_PCD_Start(hpcd); // 4. 打开中断 HAL_NVIC_SetPriority(OTG_FS_IRQn, 0, 0); HAL_NVIC_EnableIRQ(OTG_FS_IRQn); }注意动态切换时USB 主机端会认为设备断开了一次重新切回去后需要重新枚举Enumeration才能正常工作。这就意味着应用程序的 USB 协议栈要做好“设备断开重连”的处理通常需要重新走一遍HAL_PCD_Start() 回调函数里处理USB_STATE变化。6. 常见问题速查表与避坑指南把这些年遇到的典型问题整理成一个速查表方便大家快速对照排查。现象可能原因排查/解决办法PA11/PA12 配置成 GPIO 输出但电平乱跳USB 时钟和 PHY 仍处于使能状态内部上拉冲突关闭RCC-AHB2ENR的 OTGFSEN 位清除GCCFG的 PWRDWN 位程序没有初始化 USB但 DP 引脚有波形开启了 System BootloaderDFU或 BSP 默认初始化了 USB检查 BOOT0/BOOT1 状态检查启动代码是否进入系统存储器查找并删除 BSP 中的 USB 初始化代码GPIO 配置成输入模式后读到的电平不准USB 内部上拉电阻接入全速设备 DP 上拉把引脚拉高关闭 USB 时钟和 PHY配置 GPIO 时把内部上下拉设置为需要的值切换回 USB 后无法枚举切换过程中没有完整重新初始化 USB PHY 或中断配置按 5.2 节的完整流程重新初始化检查 VBUS 检测引脚配置动态切换 GPIO/USB 后系统卡死切换顺序错误USB 中断在 GPIO 模式下仍然触发切换前先HAL_PCD_Stop()并关中断切换完成后统一开中断USB 虚拟串口VCP能识别但发送接收乱码DP/DM 线路信号质量问题PCB 走线过长/差分阻抗不匹配检查 PCB 布线DP/DM 保持差分等长避免在引脚上接过大电容6.1 独家避坑技巧如何用调试器快速判断引脚被谁占用很多时候靠猜不如直接看寄存器。在调试器里添加以下表达式可以快速知道 PA11/PA12 当前归属// 查看模式0b00 输入0b01 输出0b10 复用0b11 模拟 (GPIOA-MODER 22) 0x3 // PA11 模式 (GPIOA-MODER 24) 0x3 // PA12 模式 // 查看复用功能编号 (GPIOA-AFRL 12) 0xF // PA11 AF (GPIOA-AFRL 16) 0xF // PA12 AF如果模式是0b10并且 AF 值是0b101010那基本可以断定引脚被切到了 OTG_FS 功能。不要只看到表达式就着急改代码先看清楚哪个环节把它配成了这样再动手。很多时候是因为初始化顺序问题——GPIO 初始化写在 USB 初始化前面USB 初始化又把配置覆盖了。6.2 如果 PCB 已经做好DP/DM 无法改成 GPIO 怎么办有些朋友在画板时已经固定了 DP/DM 连接方式不能随意改板。如果 DFU 或者 Bootloader 会强占 PA11/PA12同时你又想在应用程序里用 GPIO可以这样做在应用程序启动早期就把 System Bootloader 的 USB 相关动作打断具体方式是读取SYSCFG-MEMRMP寄存器确认启动源如果是系统存储器启动则尽早跳转到用户 Flash。确保应用程序代码中不使能 USB 外设时钟不用 USB 外设。如果 USB 供电引脚比如 VBUS 检测脚会影响状态需要检查对应的 GPIO 配置比如 PA9OTG_FS_VBUS在有些板子上是接在 USB 检测电路上的它也会影响 USB 外设的状态机。6.3 为什么有的例程里“USB 没有初始化”但 VCP 依然能用这是一种非常容易让人误解的现象。一些开发板的出厂程序或者 ST-Link 的虚拟串口功能是由板载调试器芯片比如 ST-Link/V2 上的 STM32F103实现的目标芯片的 PA11/PA12 并没有参与 USB 通信。这时候你测量目标芯片的 DP/DM 引脚会发现它们其实就是普通 GPIO 或者悬空状态和 USB 无关。所以当你在开发板上看到 USB 设备枚举成功先确认是“目标芯片自己枚举的”还是“调试器芯片枚举的”不要被现象误导白白浪费时间去查引脚配置。7. 从根上避免“USB 抢引脚”的设计习惯最后聊一点长远建议也算是这些年做 STM32 项目总结出来的习惯。第一个建议不要把 USB 和其他复用功能混在一个 GPIO 上“备用”。STM32 的引脚复用矩阵虽然灵活但 USB 的物理层特性决定了它不能和其他外设共享引脚做动态切换除非你确实花了大量时间设计切换协议和硬件电路。如果项目里 USB 是主要通信方式建议直接固定分配不要在后期去“借”DP/DM 干别的。第二个建议理顺代码初始化顺序把 USB 初始化放在最后。很多问题的根源是初始化顺序混乱比如先初始化 USB再初始化 GPIO结果 GPIO 配置把 USB 的 AF 配置覆盖了。工程上统一的习惯是先配置时钟树和所有 GPIO再初始化外设USB、UART、SPI 等。这样即使某个外设的 MspInit 里有 GPIO 配置也不会影响已初始化的引脚。第三个建议用 CubeMX 生成工程时哪怕暂时不用 USB也别删掉 USB 模块而是用“禁用”的方式处理。CubeMX 的图形化配置界面里把 USB_OTG_FS 设为 Disabled生成代码时会自动跳过 USB 的外设初始化但保留相关文件方便以后启用。这样就不会出现“删代码时误删了 GPIO 配置”或者“忘了调用 USB 初始化导致全部乱套”的情况。按经验来说遇到“USB 抢引脚”的问题80% 以上都是时钟和初始化顺序的问题而不是芯片本身的行为异常。把寄存器读一遍基本就能锁定问题所在。希望这篇文章能帮你少走一些弯路。
返回列表