去年调一块STM32F103R6为核心的采集板,USB设备枚举怎么都过不去。固件用的是标准USB库,串口打印正常,I2C外设正常,偏偏Windows设备管理器里就是“未知USB设备(设备描述符请求失败)”。折腾两天之后发现,问题不在代码,而在D+上的那颗47kΩ电阻——当时画原理图的时候想当然,觉得USB就是“带方向串口”,数据线上拉个电阻就行,完全没想到全速设备的上拉电阻必须接近1.5kΩ,更没意识到F103芯片内部本来就集成了这个上拉。这篇避坑指南主要讲三件事:D+上拉电阻背后的电平逻辑、不同阻值的实测结果、以及围绕STM32F103R6 USB全速设备接口的其他硬件坑。
1. 先搞明白:为什么D+上的电阻非得是1.5kΩ
1.1 主机靠“空闲电平”判断你是什么设备
USB 2.0协议里有三种速度等级:低速1.5Mbps、全速12Mbps、高速480Mbps。设备端区分速度的方式非常“硬件化”:全速设备把D+拉高,低速设备把D-拉高,主机检测哪根线是空闲高电平,就知道该用哪种速度去握手。STM32F103系列是USB全速设备外设,只支持12Mbps,所以在空闲状态下D+必须稳定保持高电平。
这里有个常见的认知误区:很多人把“全速”和“高速”混为一谈,觉得F103既然叫“USB 2.0 Full Speed”,是不是也能跑480Mbps?不是。全速就是12Mbps,F103不支持高速,也没有高速设备才有的Chirp握手序列。它要做的事非常简单:D+拉高,告诉主机“我是全速设备”,然后老老实实等着主机发复位信号和枚举包。
主机侧的原理是另一个关键:USB主机端口的D+和D-上各有一个15kΩ下拉电阻。没有设备插入时,两根线都是低电平;设备插入后,如果D+被上拉到3.3V,与主机内部15kΩ下拉形成一个分压,D+就会稳定在一个接近3V的高电平。主机检测到这个高电平并保持一段时间后,就把端口状态切换为“已连接”。所以D+上拉电阻的取值,直接决定了主机端的空闲电平够不够高、够不够稳定。
1.2 STM32F103R6内部其实已经内置了上拉电阻
这是很多新手画F103 USB电路时的第一个认知盲区。STM32F103系列芯片在USB设备外设内部已经集成了一颗1.5kΩ的D+上拉电阻,由USB外设的软连接逻辑控制。也就是说,理论上你只需要把PA11(USB_DM)和PA12(USB_DP)直接连到USB座的D-和D+,不需要在板上额外放任何上拉电阻。
但网上流传的很多老原理图、开发板参考设计,仍然会在D+上放一颗外部1.5kΩ电阻。有些是历史原因——早期芯片或山寨芯片可能不保证内部上拉的一致性;有些则是照抄别人图纸时根本没细想。问题来了:STM32F103R6内部已经有1.5kΩ,外部再并一颗1.5kΩ,等效上拉电阻就变成了750Ω。750Ω在多数主机上仍然能枚举成功,市面上很多板子就是这么“带病工作”的,于是这个坑被一代代传下来:你觉得板子没问题,其实D+电平、边沿、过冲都已经偏离USB规范了。
我自己做实测时,反而是特意把内部上拉禁用或换用外部电阻,这样才能单独控制变量。量产设计里,除非你有特殊的软断开需求,否则直接信任F103的内部上拉就够了,没必要外接。后面我会给一组完整的实测数据说明为什么。
2. 实测:D+从1.5k换到100k,枚举结果差距有多大
2.1 测试平台与测量方法
测试用的是一块自研STM32F103R6板:8MHz外部晶振,PLL×6得到48MHz USB时钟,VDD为3.3V,固件使用标准USB CDC设备库,功能就是枚举成一个串口设备。为了单独测试不同上拉阻值,我把D+原有的固定电路改为可插拔排针,分别外接不同阻值的电阻,每测一种阻值就对同一个USB口做50次热插拔统计。
测量工具没有用太高级的东西:一块Fluke 15B+万用表量D+静态电压,一台RIGOL DS1054Z示波器看波形,上位机用Windows 10自带的设备管理器配合usbview工具观察枚举状态。测量点统一在USB座D+针对GND的电压。整个过程中,固件、线缆、USB口、电源全部固定不变,唯一变化的只有D+上拉电阻。
2.2 不同阻值下的枚举结果
测试数据整理成表如下。D+空闲电压是设备插入PC、主机端口下拉已经生效后的电压值,计算公式是:V_D+ = 3.3V × 15000Ω / (Rp + 15000Ω)。
| D+上拉方式 | 等效上拉阻值 | D+空闲电压 | 50次热插拔成功枚举次数 | 实测现象 |
|---|---|---|---|---|
| 悬空,无任何上拉 | ∞ | 0V左右 | 0 | 设备管理器完全无反应,像没插一样 |
| 外部100kΩ | 100kΩ | 约0.43V | 0 | 完全无反应 |
| 外部47kΩ | 47kΩ | 约0.80V | 0 | 偶发出现“未知USB设备”,但从未成功读取描述符 |
| 外部10kΩ | 10kΩ | 约1.98V | 3 | 冷启动偶尔能枚举,热插拔基本失败 |
| 外部4.7kΩ | 4.7kΩ | 约2.51V | 47 | 大多数能识别,但偶发复位失败,长线时CRC错误概率明显上升 |
| 外部1.5kΩ | 1.5kΩ | 约3.00V | 50 | 稳定,波形干净 |
| 仅用内部上拉 | 约1.5kΩ | 约3.00V | 50 | 稳定 |
| 内部上拉+外部1.5kΩ并联 | 约750Ω | 约3.17V | 49 | 基本能识别,但示波器看到边沿过冲明显增大 |
这里最关键的一组对比是10kΩ和4.7kΩ。10kΩ对应的D+空闲电压是1.98V,恰好卡在USB接收器高电平阈值2.0V附近,所以主机经常判断“D+没有拉高”,表现为时好时坏;4.7kΩ时D+电压虽然到了2.51V,理论上高于阈值,但余量已经不大,遇到劣质线缆、前置HUB或电平阈值偏高的主机,仍然会随机失败。为什么USB规范把上拉电阻限定为1.5kΩ±5%,原因就在这里:它要在最恶劣的供电、线缆、主机条件下,保证D+空闲电平有足够的电压余量。
2.3 波形上的差别:不仅仅是电压够不够
万用表只能看静态电平,示波器才能看出更多东西。实测1.5kΩ上拉时,D+空闲电平大约3.0V,复位结束后D+从低回高的过程干净利落,边沿过冲在200mV以内。换成内部外部并联的750Ω时,D+空闲电压虽然更高了,但发送数据时的边沿明显更陡,在1米长的USB线上能看到振铃,过冲会冲到3.6V以上。这会让数据眼图变差,短期内看不出问题,长时间运行或环境干扰强时,容易冒出随机CRC错误。
阻值偏大的反面也很致命。以47kΩ为例,主机发送复位SE0结束后,D+要靠上拉电阻把线拉回高电平,这个回高速度和RC时间常数直接相关。假设线缆加PCB的等效电容有100pF,47kΩ×100pF约等于4.7μs的回高时间,而全速12Mbps一个位的时间只有83ns左右,也就是说主机在好几个位周期里看到的D+仍然不是可靠的高电平,枚举自然失败。10kΩ、4.7kΩ只是把这个回高时间从4.7μs缩到1μs、470ns,依然远大于一位时间,所以才会出现“概率性成功”。这也是我强烈建议直接用1.5kΩ的原因:它不是拍脑袋定的值,而是USB协议根据线缆、主机下拉、驱动能力等一系列最坏情况反推出来的标准值。
3. 比上拉电阻更隐蔽的几个硬件坑
3.1 D+和D-接反:主机以为你是个低速设备
这颗坑在原理图阶段就会出现,而且非常隐蔽。STM32F103R6的PA12对应USB_DP,PA11对应USB_DM,也就是USB座上的D+和D-。如果画原理图时把PA12接到USB座的D-,PA11接到USB座的D+,上电后的结果会非常有意思:D+上拉等于没生效,从主机端口看过去,反倒是D-被拉到了高电平。主机看到D-空闲高,就会认为插入的是一个低速设备,然后按1.5Mbps去枚举一个实际跑12Mbps的设备,结果当然是失败。
这种现象在Windows里通常表现为“无法识别的USB设备”,偶尔会被识别成“低速未知设备”。排查方法不复杂:设备上电但还没接PC时,用万用表量USB座D+对GND,如果量到的是约3.3V,说明上拉在D+上,接线正确;如果量到D-约3.3V而D+是0V,那肯定是D+/D-接反了。这个问题一旦流片到量产,改板是跑不掉的,所以画完原理图之后,我现在的习惯是先对着封装引脚图核对一遍PA11/PA12的连接,再往下走。
3.2 VBUS检测与上电顺序
STM32F103的USB设备外设本身没有内部VBUS检测引脚,这一点和F105/107的OTG外设不一样。很多参考设计会额外用一个IO,通常是PA9,通过电阻分压检测VBUS电平,用来让固件感知“USB线有没有插上”。这个电路本身没问题,但有两个常见错误:电阻分压比算错导致IO输入电压超过3.3V,或者分压电阻太大抗干扰能力太差。
还有上电顺序的问题。自供电的板子如果MCU先上电,固件初始化USB后D+被内部上拉拉高,但此时VBUS还没有来自主机的5V。多数主机能容忍这种情况,但部分HUB会把它当成端口异常,导致首次插入时枚举失败,必须手动重插才能恢复。解决方法是把VBUS检测信号接进固件,检测到VBUS有效后再初始化USB外设或者使能软连接,让D+上拉的时机严格晚于主机VBUS就绪。听起来像软件逻辑,实际上是需要硬件先把这个检测引脚留出来。
3.3 48MHz时钟不是你想当然的那个48MHz
STM32F103的USB外设必须工作在48MHz,通常由8MHz外部晶振经PLL×6得到。这块的上拉坑在于:如果你硬件上用了12MHz晶振,但固件仍然按8MHz配置PLL,USB外设得到的时钟是72MHz,位速率完全偏离全速规范。这种情况下设备管理器不一定完全不识别,有时会显示“设备描述符请求失败”,有时会枚举成功后马上掉线,表现很随机。USB全速的位速率容差实际上只有±0.25%,虽然主机会通过SOF做一定同步,但时钟偏差太大时,主机在枚举阶段就直接放弃了。
另一个与硬件强相关的时钟坑是晶振起振不稳。负载电容配错、晶振回路走线太长、ESR偏大,都会导致8MHz晶振频率漂移或起振困难。我遇到过一块板,常温下USB一切正常,放到低温箱里就“时好时坏”,后来才发现是晶振负载电容容差太大,低温下频偏超过了USB能承受的范围。量产设计里,晶振和负载电容不能只图便宜,建议选±30ppm以内的晶振,负载电容按晶振规格书配,而不是随手抓两颗20pF就焊上去。
3.4 布局走线、ESD保护和串阻
USB全速虽然只有12Mbps,看起来不快,但D+/D-毕竟是差分信号,PCB布局上还是要给点面子。差分走线尽量紧耦合、等长,线宽阻抗按90Ω差分去设计。短距离、短走线的板子就算不满足90Ω也能跑,但别在D+/D-之间铺地铜,也别让电源线从两根线中间横穿过去。STM32F103R6的USB引脚在LQFP64封装上是相邻的,走线长度通常很自然就等长了,这点反而不是最难的。
ESD保护芯片的结电容是个容易被忽略的参数。USB数据线上需要加ESD防护,但如果你随便选了一颗结电容20pF的TVS,高速信号边沿会被明显抹平,表现为能识别但传输不稳定。USB全速应用的ESD器件建议选结电容低于2pF的专用器件,比如USBLC6-2这类,放在靠近USB座的位置,串接在D+/D-走线上。至于串阻,常见做法是在D+/D-各串一颗22Ω电阻用于抑制振铃,这个值对12Mbps信号影响很小。但不要把串阻当成万用灵药加到100Ω以上,否则D+信号幅度会被明显削掉,反而制造新的上拉类问题。
4. 遇到“设备描述符请求失败”的完整排查链路
4.1 先看Windows错误信息,判断问题方向
设备管理器会给出几种不同的错误表现,每条信息背后对应的硬件原因差别很大:
- 设备管理器里完全没有任何新设备出现:基本可以确定主机没有检测到D+拉高。优先怀疑D+上拉缺失、断线、PA12没配置好或USB座焊接问题。
- 出现“未知USB设备(设备描述符请求失败)”:主机已经看到了设备插入,但枚举过程失败。这个范围比较宽,可能是D+电平余量不足、时钟偏差、软件USB栈初始化不全,也可能是D+/D-接反。
- 出现“无法启动该硬件设备(代码43)”:通常是枚举已经进行了一部分,但描述符或驱动阶段出错。前期优先检查信号完整性和时钟,再回头查固件端点配置。
如果你手边有USB协议分析仪,或者想用软件抓包工具,最后一个建议是先放一放。设备枚举失败时,很多软件抓包工具根本抓不到有效数据,远不如示波器和万用表直观。此时“usb抓包”的结果往往是空的,反而容易误导你去查固件。
4.2 万用表测D+静态电压,一秒钟锁定上拉问题
排查D+上拉问题,最直接的步骤是用万用表量D+对GND的电压。分两种场景:
| 测量场景 | D+电压 | 结论 |
|---|---|---|
| 设备上电,未接PC | 约3.3V | 上拉存在,基本正常 |
| 设备上电,未接PC | 0V | 上拉缺失,或PA12被固件配置成GPIO输出低电平 |
| 已接PC,主机端口已供电 | 约3.0V | 上拉与主机下拉分压正常,电气连接通畅 |
| 已接PC | 约1V以下 | 上拉阻值偏大,或者上拉到其他电压太低 |
| 已接PC | 约3.3V | 主机下拉没生效,可能线缆断开、HUB端口异常、共地不良 |
| 已接PC,D-反而是约3V | 约0V | D+/D-接反了 |
这个表在之前的实测里已经验证过很多次。不用示波器,一支普通万用表就能把最经典的“上拉电阻不对”坑定位出来。我一般先量未接PC时的D+电压,再量接PC后的D+电压,结合两个数据基本能确定是不是上拉的问题。
4.3 示波器看枚举时序,确认主机有没有被正确“欺骗”
万用表能告诉你“直流电平对不对”,但看不到“时序对不对”。USB枚举的物理过程是可以直接看到的:插上USB线后,D+应该立即被上拉到高电平;主机检测到连接后,会主动发送复位信号,也就是把D+和D-同时拉低至少10ms;复位结束后,设备端的D+再次回到高电平;随后D+上会出现一串数据包。
用示波器抓D+信号时,触发方式设为上升沿触发,然后插入USB线。正常的波形应该是:D+先上升、保持高电平几百毫秒、被拉低10ms左右、再回到高电平、然后出现一堆密集的毛刺状数据包。如果复位结束后D+迟迟回不到高电平,或者回高后很快被拉死,问题基本锁定在D+上拉电阻阻值过大或固件把PA12配置成了普通GPIO。如果插线后D+根本没出现上升沿,但D-反而上去了,则直接坐实接反问题。
4.4 软件侧排错清单,防止被硬件背锅
有些时候D+上拉电路完全正常,问题反而出在固件初始化顺序上。常见的一个坑是:工程师为了低功耗或做IO功能复用,在系统初始化早期把PA12配置成GPIO输出低电平,之后USB库初始化时又没把它重新切回复用功能,结果D+被某个普通GPIO的推挽输出死死拉在地,PC永远看不到设备。排查时检查一下PA11/PA12的GPIO配置是否在USB外设初始化之前被正确设置成复用推挽。
另外,把CubeMX或标准外设库生成的官方USB例程直接烧进板子测试,是排除“自己写的枚举状态机有问题”最快的方法。如果官方例程能识别,说明硬件基本没问题,问题在你自己的应用代码或中断优先级配置;如果官方例程也识别不了,再回头按前面几步查硬件。这个顺序能帮你少走很多弯路。
5. 折腾完这轮板子,我总结出的几条硬件习惯
这块STM32F103R6采集板的最终改版,我把D+外部上拉电阻相关电路全部删掉了,只保留内部上拉。但为了以后实验方便,PCB上预留了一个0Ω电阻位,如果需要外接1.5kΩ电阻做对比测试,焊上去就能用,不焊也不影响正常功能。ESD器件换成了结电容1pF以内的型号,D+/D-各串一颗22Ω电阻,VBUS检测用两颗100kΩ分压到PA9,固件检测到VBUS后再做USB软连接。
经过这次实测,我给自己定了三条基于硬件的设计习惯,现在分享出来作为收尾:第一,新画的STM32F103 USB板子,上电后第一件事是先量D+电压,而不是着急烧固件看设备管理器;第二,原理图上凡是USB数据线上的电阻,不管是上拉、串阻还是分压,都必须能说出取值依据,说不出依据的宁可不放;第三,遇到USB枚举失败,先假设是硬件问题,用万用表和示波器把D+/D-电平、时钟频率、VBUS顺序这三样确认完,再转头去找固件的麻烦。这套顺序帮我在后面几款产品里省下了大量调试时间,也让我彻底记住了“D+上拉电阻就该是1.5kΩ”这件事。