上周有个做数据采集终端的兄弟发来一张原理图,芯片是STM32F103R6,USB口画的是OTG Micro-B,旁边标注写着“USB_OTG_FS/DRD”。他的需求很明确:电脑插上这根线,设备能被识别成一个U盘或者虚拟串口;拔下来插U盘,设备又能自动去读写U盘里的文件。这个需求听起来就是典型的USB双重角色设备(DRD),但问题恰恰出在原理图的标注上——F103R6这颗料,根本没有OTG_FS外设。
这篇文章就围绕这个项目展开,把F103R6的USB能力边界、OTG_FS与DRD的真实含义、实际可行的硬件方案、以及USB枚举和调试的实战经验都过一遍。如果你正准备用STM32做USB设备端开发,或者想在一颗低成本的M3上实现“既能当U盘又能读U盘”的功能,这篇内容应该能帮你少走不少弯路。先给结论:DRD的本质是硬件控制器具备Host和Device两个角色切换能力,不是软件里随便切两个枚举逻辑就能糊弄过去的。
1. 先理清楚:F103R6到底有没有USB OTG_FS
1.1 STM32F1系列的USB外设家族差异
STM32F1系列对USB的支持很容易让人犯迷糊,因为型号后缀长得太像了。F103、F105、F107都是Cortex-M3核心,引脚很多也兼容,但USB外设完全不同。
我列个表方便对照:
| 型号系列 | USB控制器类型 | 支持角色 | 内置PHY | 典型封装 |
|---|---|---|---|---|
| STM32F103系列 | USB 2.0 Full Speed Device | 仅Device(从设备) | 内置收发器 | LQFP48/64/100等 |
| STM32F105系列 | USB OTG_FS | Host/Device/DRD | 内置收发器 | LQFP64/100等 |
| STM32F107系列 | USB OTG_FS + OTG_HS | Host/Device/DRD | OTG_FS内置PHY,OTG_HS需外置ULPI PHY | LQFP64/100等 |
F103R6里的“R”代表LQFP64封装,“6”代表Flash容量为32KB,这颗料属于F103系列里的小容量型号,内部只有一个USB Device Full Speed外设。它连Host模式都做不了,更谈不上OTG或者DRD。
很多网友在咨询里提到“USB枚举过程详解”“stm32无法识别usb设备”,其实不少问题就是选型阶段埋下的雷。你以为芯片支持OTG,照着OTG电路去画,结果F103R6的PA11/PA12只有USB Device功能,没有ID检测、没有VBUS会话控制,硬件上就不具备双角色切换的基础。
1.2 为什么网上总有人把F103和OTG混为一谈
我总结下来有三个原因:
第一,STM32CubeMX里面F103系列也有USB配置选项卡,很多人看到“USB”两个字就以为全功能都有,实际上它只有USB Device的PCD(Peripheral Controller Driver)选项,没有Host相关的HCD选项。
第二,一些开发板商家宣传文案里写“USB OTG”,实际用的是F105/F107或者F4系列芯片,但资料标题里只写STM32F103,很容易误导人。
第三,OTG这个缩写被滥用得太厉害,经常有人把“能切换成Device和Host”的系统级方案也叫OTG,哪怕控制器本身不支持。
在做任何PCB设计之前,先去芯片选型手册里确认外设列表,不要只看数据手册首页的方框图。STM32F103参考手册里USB章节的标题写得很清楚:USB device full-speed,根本没有“OTG”三个字母。
1.3 DRD/OTG的基本概念,别搞混了
DRD全称Dual Role Device,意思是同一根USB口既能作为Host去主动枚举外设,又能作为Device被主机枚举。OTG规范在DRD的基础上还增加了SRP(会话请求协议)和HNP(主机协商协议),让两个DRD设备可以在不换线的情况下动态交换主从角色。
用大白话说:我们平时用的U盘,它只有Device功能,插到电脑上只能被动等电脑来读;电脑那边是Host,主动发起枚举。如果某一天U盘也想主动去读另一个U盘,它就需要具备Host能力。DRD就是在同一个USB物理接口上,让这个角色切换成为可能。
但要强调一点:DRD是“分时复用”,同一个时刻只有一个角色生效。不是说你插着电脑的同时还能去读U盘,物理层面就做不了这件事。很多项目方对“双角色”的预期其实是两个USB口同时工作,那就不属于DRD的范畴了,而是系统级多USB口方案。这一点搞清楚了,后面的方案选型才不会乱。
2. 想实现“双角色”,三条可行路线怎么选
假设你的需求确实是标题描述的场景:需要设备既能被上位机枚举,又能主动访问外部USB设备。下面三种方案是实际项目中我见过用得最多的,各有优劣。
2.1 方案一:换用STM32F105/F107系列,走标准OTG_FS
如果产品还在选型阶段,最推荐的方案就是把主控从STM32F103R6换成STM32F105R6或者STM32F105R8。F105/F107内置USB OTG_FS控制器,硬件上原生支持Device、Host、DRD三种模式,而且LQFP64封装的引脚和F103R6基本兼容,PCB改动很小。
F105R6的Flash是256KB,RAM是64KB,比F103R6的32KB Flash、10KB RAM充裕太多,跑USB Host库和中间件完全没压力。如果你的应用还要跑文件系统、GUI或者复杂协议栈,F103R6的资源其实非常紧张,F105这个升级几乎是刚需。
替换的时候有几个注意点:
- 晶振电路、电源引脚基本一致,但USB引脚功能配置不同,需要重新看一下数据手册的系统存储器和GPIO复用表。
- F105/F107的OTG_FS_VBUS引脚在PA9,OTG_FS_ID引脚在PA10,这两个引脚在F103上分别复用为USART1_TX和USART1_RX,如果原来的板子用串口做调试,引脚冲突必须提前处理。
- OTG_FS的D+/D-还是原来的PA11/PA12,但F105/F107的PHY内部集成了D+/D-的上下拉控制,电路设计上比F103的设备模式更简单,不需要手动控制外部上拉电阻。
2.2 方案二:坚持用F103R6,增加第二路USB Host控制器
如果产品已经定死了F103R6,模具、成本、供应链都不允许换芯片,那么还可以走“双USB口”方案:一路用F103R6内置的USB Device连接上位机,另一路通过SPI/UART接口外接一颗USB Host控制器芯片,比如CH376或者CH374,用来主动读写U盘。
CH376是沁恒的经典USB控制芯片,支持Host和Device两种模式切换,通过SPI或者UART和MCU通信。在Host模式下,它可以直接操作U盘里的文件系统,MCU只需要发命令给它,不需要自己处理USB协议,非常适合F103这种资源紧张的MCU。
这个方案的本质是“系统级双角色”,不是单口DRD,但很多实际产品就是这么做的。比如考勤机、数据采集终端、银行U盾读取器,通常是设备本身被PC连接,同时又需要读取员工插入的U盘或者USB Key数据,两个USB口独立工作,完全不冲突。
这样做的好处是F103R6不用换,软件上Device部分用官方USB库,Host部分用CH376的命令封装,两边互不干扰。缺点是物料成本多了一颗USB Host芯片,PCB上多一个USB口,产品面板也需要多开一个孔。如果你不需要同时工作,而是要求同一个USB口分时切换角色,那这个方案就不适合了。
2.3 方案三:同一个口做分时切换,F103R6硬扛
有人会问:如果电源只有一路USB口,同时又必须分时扮演Host和Device,那能不能在F103R6上硬做?
从理论上说,可以在一个USB物理接口上加模拟开关,把D+/D-在F103内置USB Device和外置USB Host控制器(比如CH376的Device或者Host模式)之间切换,软件上根据外部信号决定当前角色。但这个东西工程实践起来非常难受:
- USB D+/D-是高速差分信号,模拟开关会引入寄生电容和导通电阻,信号质量很难保证,尤其在高速模式下更容易翻车。
- 角色切换时总线状态、上拉/下拉电阻都要跟着切,时序稍有问题就会导致主机侧枚举失败。
- 软件状态机极其复杂,一个中断没处理好,两边都挂起。
我见过有人这么干,最后调试了两个月还是不稳定,产品一过静电测试就掉线。我的建议是:除非是纯学习验证,否则量产项目不要走这条路。USB这种涉及总线时序的东西,硬件原生支持和靠外挂模拟开关硬搭,稳定性完全不是一个量级。
3. 关键硬件设计与PCB布线实操
如果决定走方案一(F105/F107 + OTG_FS),下面这些硬件设计细节是实战中真正影响稳定性的地方。
3.1 OTG_FS模式下的引脚分配与电路
STM32F105/F107的OTG_FS在DRD模式下,核心引脚有以下几个:
| 引脚名 | 功能 | 设计要点 |
|---|---|---|
| PA11 / OTG_FS_DM | USB差分数据负 | 走差分线,等长 |
| PA12 / OTG_FS_DP | USB差分数据正 | 走差分线,等长 |
| PA9 / OTG_FS_VBUS | VBUS检测 | 5V分压到3.3V,用于会话和角色判断 |
| PA10 / OTG_FS_ID | ID线检测 | 接地表示Host角色,悬空表示Device角色 |
| 3.3V电源 | 收发器电源 | 必须加退耦电容,建议1uF+100nF组合 |
VBUS检测不能用MCU的ADC直接量5V,必须用电阻分压网络。我习惯用两个10k电阻分压,中间点接PA9,同时在PA9脚加一个100nF滤波电容,电压约2.5V,在ADC采样范围内。如果你还想检测外部5V电源是否过流,可以串一个采样电阻加运放做电流检测,但这个别混在OTG_FS_VBUS信号里,会干扰分压点。
ID引脚的处理比较讲究。标准OTG连接器里,ID引脚在主机端(A设备)是接地,在设备端(B设备)是悬空。F105/F107的OTG_FS_ID内部有上拉/下拉机制,但硬件上最好还是要根据你的产品形态处理。如果产品只做固定角色,比如固定作为Host读U盘,可以直接把ID引脚接地。如果做DRD,则把ID接到OTG Micro-B连接器,让外部线缆决定角色。
3.2 VBUS供电与会话管理
DRD模式下,VBUS的供电方向是角色切换的关键:当设备作为Host时,需要向VBUS提供5V电源给外接设备;当设备作为Device时,VBUS由上游主机提供,自己不能反向供电。
实际电路上,我通常用一颗带使能控制的5V升压/降压芯片给VBUS供电,使能脚接到MCU的GPIO。软件切换到Host角色时先打开这个5V电源,延时100ms等待VBUS稳定,再去初始化OTG控制器。切回Device模式时,先关掉5V输出,等待VBUS放电到安全电平以下,再初始化Device端。
这个先后顺序一旦反了,轻则枚举失败,重则烧坏对端设备。我在测试中就遇到过,程序复位瞬间5V还没断开,又接到了电脑主机上,直接把电脑的USB口保护触发,整个口子暂时失效。后来在VBUS输出关断后加了至少50ms的放电等待,问题才彻底解决。
3.3 PCB布局布线的几条经验
USB Full Speed的速率只有12Mbps,对差分阻抗的要求不如High Speed那么苛刻,但也不能随便拉飞线。根据我几次打板调试的经验:
- D+/D-尽量走同一层,长度差控制在5mm以内,线宽和间距保持一致。
- 特性阻抗做到90欧姆±10%比较合适,两层板可以通过计算走线宽度和介质厚度来近似实现。
- D+/D-两侧包地,并打上过孔连接地平面,减少来自其他信号的干扰。
- OTG_FS_VBUS、ID这类控制信号可以不走差分规则,但要远离晶振和电源电感。
很多人会忽略串阻的问题。F105/F107的OTG_FS收发器内部已经做好了匹配,串阻不是必须的,但很多参考设计会在D+/D-上各自串联22欧姆电阻,主要作用是ESD保护器件和PHY之间做一个缓冲。如果硬件设计参考了ST官方NUCLEO-F105的评估板,直接照抄它的USB部分就行,那套电路经过了大量验证。
另外强烈建议在D+/D-靠近连接器的位置加一个USBLC6-2或者类似的低电容ESD保护管,USB口经常被带电插拔,静电问题在干燥环境里非常致命。不加ESD管的产品,打±8kV静电测试时大概率死机或者USB控制器直接损坏。
4. 软件架构与DRD角色切换的核心逻辑
4.1 用STM32CubeMX做最基础的配置
F105/F107的软件配置建议直接用STM32CubeMX生成工程,省去手工配置寄存器的痛苦。关键点如下:
- 时钟树里把USB时钟配置为48MHz。F105/F107的OTG_FS需要精确的48MHz,如果外部晶振是8MHz,主频跑到72MHz后USB时钟从PLL输出Q分频得到48MHz,这几个数字必须在CubeMX里正确填好,否则上电枚举就失败。
- 在USB OTG_FS配置页面选择“Device Only”“Host Only”或者“OTG”。如果选OTG模式,软件里需要自己处理角色切换。
- 中间件选择上,如果要实现MSC(U盘)或者CDC(虚拟串口),在USB Device中间件里选好对应Class。如果要做Host读U盘,则在USB Host中间件里选MSC类。
注意一点:CubeMX默认生成的Device工程和Host工程往往是分开的,不利于做动态切换。我实际做DRD的时候,更习惯把OTG控制器的HAL层代码同时拉进工程,然后自己写一个状态机管理角色。
4.2 双角色切换的状态机设计
下面这个示意流程是我在项目里用的简化版,可以给自己的状态机做参考:
typedef enum { ROLE_DETACH, ROLE_HOST, ROLE_DEVICE } usb_role_t; usb_role_t USB_Role_Detect(void) { // ID引脚电平由OTG连接器决定 if (ID_Pin_Is_Low()) { return ROLE_HOST; } // 没有ID线时,用VBUS电压判断是否被主机连接 if (VBUS_Voltage_Is_Present()) { return ROLE_DEVICE; } return ROLE_DETACH; } void USB_Role_Task(void) { usb_role_t role = USB_Role_Detect(); if (role != current_role) { // 先彻底关闭当前角色 USB_DeInit_All(); // 等待总线放电,至少50ms HAL_Delay(100); if (role == ROLE_HOST) { USB_Host_Init(); // 打开VBUS供电 VBUS_Power_On(); // 等待外设枚举 USB_Host_Enumerate(); } else if (role == ROLE_DEVICE) { VBUS_Power_Off(); USB_Device_Init(); // 等待主机枚举本设备 } current_role = role; } }这里的核心思想是:角色切换前必须先做完整的去初始化,确保USB控制器回到复位状态,总线上没有残留的上下拉或者电平。总线放电时间不能省,我踩过坑,只延时10ms就切换,下次枚举非常不稳定,有时候能被识别,有时候直接超时。
如果使用HAL库,HAL_PCD_Start和HAL_HCD_Start都不要在切换完成后马上调用,建议先等待50ms到100ms的稳定窗口。这一步不是为了CPU,而是为了给连接器触点、ESD保护器件、线缆上的杂散电容放电留出时间。
4.3 不做DRD的时候,这个状态机有什么用
如果你最终选择方案二(F103R6 + CH376双口方案),上面这个角色状态机其实用不上,因为两个USB口是独立硬件,不需要在同一对外设上切换。
CH376的软件控制相对简单:MCU上电后通过SPI向CH376发送“设置模式”命令,主模式或者从模式根据产品场景写死就行。主机模式下,CH376自己处理USB枚举、Mass Storage协议、FAT文件系统,MCU这边只发“打开文件”“写扇区”“断开设备”这类高层命令。这个芯片的驱动代码网上很多,注意不要直接抄那些老掉牙的51版本,改到STM32F103的SPI驱动时要注意时序参数,CH376的SPI最高频率有限制,跑太快容易丢字节。
5. USB枚举过程与调试工具实战
不管做Device还是Host,USB协议栈的调试都离不开对枚举过程的理解。很多新手拿到“stm32无法识别usb设备”的问题,第一反应是检查代码,结果问题出在硬件上拉或者时钟上。学会看枚举过程,比盲猜代码有效得多。
5.1 全速设备枚举的标准流程
一个USB全速设备接入主机后,枚举过程大致是:
- 主机检测到D+线被上拉到3.3V,判断有设备接入。
- 主机向D+/D-发出复位信号(SE0状态,持续至少10ms)。
- 设备在复位结束后进入默认地址0,主机发送GET_DESCRIPTOR请求获取设备描述符。
- 主机向设备发送SET_ADDRESS请求,分配唯一地址。
- 主机再次发送GET_DESCRIPTOR请求,获取完整设备描述符和配置描述符。
- 主机根据设备描述符里的类代码加载对应驱动,发起SET_CONFIGURATION,设备进入配置状态。
- 后续按Class协议进行传输。
如果在第3步失败,大概率是设备端D+上拉没做好或者USB时钟不准确。如果第5步失败,多半是描述符内容错误,比如配置描述符长度和实际发送长度不一致,主机直接中止枚举。我在调试F105的MSC设备时遇到过,配置描述符里接口数填多了,主机一直返回错误,用Bus Hound抓包才发现返回的数据长度少了几字节。
5.2 实用抓包和调试工具
调试USB,我日常会用三种工具:
- 逻辑分析仪:几十块钱的8通道逻辑分析仪就能看D+/D-波形,配合PulseView软件可以解码USB全速信号。虽然没有协议高层解析,但能快速确认设备是否收到总线复位、有没有返回ACK。
- Bus Hound(Windows):可以抓取主机和USB设备之间的URB请求和响应,对Device模式调试非常有用,能看主机发过来的每一个控制请求。
- Wireshark + usbmon(Linux):Linux下通过usbmon模块抓包,Wireshark里自动解码USB协议,比Windows下方便很多。如果全志、树莓派这类开发板上跑了Linux系统,直接用它来验证USB Gadget的枚举问题很方便。
逻辑分析仪采样率至少需要50MHz才能比较完整地捕捉USB全速信号,12Mbps的速率下,一个位元周期约83ns,50MHz采样每个bit能采4到5个点,基本够看。
5.3 描述符常见错误和Class选型
USB描述符是枚举阶段的产品说明书,常见的错误包括:
- 设备描述符的idVendor/idProduct没有实际注册,或者使用厂商默认值,导致主机加载不了正确驱动。
- 配置描述符里bConfigurationValue为0,主机认为设备处于未配置状态。
- 字符串描述符的语言ID不匹配,主机发GET_DESCRIPTOR请求字符串时返回错误。
Class选型则看应用场景:如果需要上位机直接收发自定义数据,选CDC类最省事,枚举出来就是一个虚拟串口,和USB转TTL模块的体验一样,热词里那些CH340、FT232R其实就是这类功能的专用芯片。如果做U盘功能,选MSC类,Windows和Linux都原生支持,但要注意大数据量读写时USB Buffer要开够,否则速率上不去。
6. 实战中的高频坑与排查清单
下面这些是这些年调试STM32 USB总结出来的高频问题,按现象列了一个速查表,遇到问题可以对照排查。
| 故障现象 | 可能原因 | 处理办法 |
|---|---|---|
| 插上USB后PC完全无反应 | D+上拉缺失、USB时钟不为48MHz | 检查上拉电阻,确认PLL配置,用逻辑分析仪看D+电平 |
| 设备管理器黄叹号 | 描述符错误、设备中途断开 | 抓包对比描述符返回长度和内容,检查供电 |
| 能识别但驱动安装失败 | VID/PID未定义,Class类型不匹配 | 核对描述符中Class字段,确认使用的中间件配置 |
| Host模式下枚举U盘失败 | VBUS供电不足、D+/D-走线过长 | 用带外部供电的USB Hub排除供电问题,实测走线信号 |
| DRD切换角色后不稳定 | 总线放电不充分、去初始化不彻底 | 增加切换延时,检查是否重复初始化HCD/PCD |
| 插上Type-C转接后不识别 | ID引脚状态没跟上、CC电阻缺失 | 确认使用了支持OTG的线缆,检查ID引脚电压 |
| USB工作一段时间后掉线 | 电源纹波大、ESD击穿保护管 | 增加电源滤波,测试ESD管是否漏电 |
采集这几个问题时,还要注意一个细节:USB线的质量对实验影响巨大。有些廉价USB线里面只有电源线和两根数据线,屏蔽层都没有,在Host模式往外接设备时特别容易出现掉线。开发调试阶段建议用尽量短的、质量好的线,先排除线缆因素再怀疑芯片。
关于“mcu没有usb差分信号数据引脚怎么办”这类问题,其实也是选型时的误解。USB差分信号必须使用支持USB复用功能的引脚,不是任意GPIO都能模拟。如果选完型发现没有USB引脚,要么换MCU型号,要么干脆外接一颗USB转串口芯片,把USB协议转换成UART,再从UART GPIO接入MCU。但这种方式和原生USB不是一个量级的,只适合数据量要求不高的场景。
还有一个容易被忽视的坑:F103的PA11/PA12同时复用为CAN_RX和CAN_TX。如果板子上既用了USB又用了CAN,这两个外设会抢引脚,必须在CubeMX里确认外设映射不冲突。我在一个项目里就吃过这个亏,USB设备枚举正常,但CAN通信一直发不出去,查了半天才发现引脚冲突。
最后分享一个我自己常用的调试习惯:在USB_Device_Init或者USB_Host_Init函数入口加一块空闲的GPIO翻转,用示波器看这个IO电平变化就能判断初始化是否真的被执行。很多时候代码逻辑看着没问题,实际运行中可能压根没走到USB初始化这一步,被前面某个驱动卡死了。这个土办法比DEBUG卡点还直观,尤其是在bootloader里做USB升级时,能快速定位是跳转失败还是USB初始化失败。
如果你的项目只是想在STM32F103R6上练手,理解USB协议和描述符,那就老老实实把内置的Device模式玩明白,写个HID或者CDC应用,再配一个逻辑分析仪观察枚举过程,这套基本功对后面做Host或者OTG都有很大帮助。如果产品需求真的明确了要DRD,尽早切换到原生支持OTG_FS的芯片,省下的调试时间足够你把应用层做得更完善。