上周有个做数据采集终端的兄弟发来一张原理图芯片是STM32F103R6USB口画的是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_FS1.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_FSHost/Device/DRD内置收发器LQFP64/100等STM32F107系列USB OTG_FS OTG_HSHost/Device/DRDOTG_FS内置PHYOTG_HS需外置ULPI PHYLQFP64/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的PCDPeripheral 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是256KBRAM是64KB比F103R6的32KB Flash、10KB RAM充裕太多跑USB Host库和中间件完全没压力。如果你的应用还要跑文件系统、GUI或者复杂协议栈F103R6的资源其实非常紧张F105这个升级几乎是刚需。替换的时候有几个注意点晶振电路、电源引脚基本一致但USB引脚功能配置不同需要重新看一下数据手册的系统存储器和GPIO复用表。F105/F107的OTG_FS_VBUS引脚在PA9OTG_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_DMUSB差分数据负走差分线等长PA12 / OTG_FS_DPUSB差分数据正走差分线等长PA9 / OTG_FS_VBUSVBUS检测5V分压到3.3V用于会话和角色判断PA10 / OTG_FS_IDID线检测接地表示Host角色悬空表示Device角色3.3V电源收发器电源必须加退耦电容建议1uF100nF组合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模式软件里需要自己处理角色切换。中间件选择上如果要实现MSCU盘或者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还是HostUSB协议栈的调试都离不开对枚举过程的理解。很多新手拿到“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 HoundWindows可以抓取主机和USB设备之间的URB请求和响应对Device模式调试非常有用能看主机发过来的每一个控制请求。Wireshark usbmonLinuxLinux下通过usbmon模块抓包Wireshark里自动解码USB协议比Windows下方便很多。如果全志、树莓派这类开发板上跑了Linux系统直接用它来验证USB Gadget的枚举问题很方便。逻辑分析仪采样率至少需要50MHz才能比较完整地捕捉USB全速信号12Mbps的速率下一个位元周期约83ns50MHz采样每个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的芯片省下的调试时间足够你把应用层做得更完善。
