STM32F105 USB自定义协议开发指南:从F103迁移到OTG的实践

发布时间:2026/9/2 5:01:03
STM32F105 USB自定义协议开发指南:从F103迁移到OTG的实践 简介基于STM32F105的USB HID设备完整工程带有自定义通信协议可实现64字节长度的数据免驱传输。工程代码基于HAL/LL库编写从USB控制器配置、HID报告描述符定义到中断处理与自定义协议封包/解析都有清晰实现可用于学习STM32的OTG模块、HID类设备开发以及无驱动人机交互设备设计。压缩包共437个文件约9.13MB以C源文件、H头文件、编译生成的O/OBJ/CRF文件、MAP以及uvproj/uvopt工程文件为主便于直接打开MDK工程查看、编译和烧录。工程还涉及USB枚举、数据收发、固件更新等调试要点适合需要快速搭建USB应用或理解STM32底层USB协议的开发者。已有867人学习下载是STM32F105 USB项目开发中较为完整的参考资料。 我前阵子从一个同事手里收到一个压缩包名字就叫“STM32F105_USB带自定义协议.rar”。乍一看平平无奇但接过包之后我干的第一件事不是解压而是先把一个根深蒂固的习惯扔到一边——如果你之前玩过STM32F103的USB再来看F105的USB工程千万别默认“换颗芯片、改改时钟、重新编译就完事”。F105的USB外设和F103的USB外设本质上就是两套完全不同的东西。这篇就把这个工程包里常见的技术点、协议设计思路和调试方法拆开讲清楚适合刚拿到F105芯片准备做USB通信、或者在F103上做过USB想迁移过来的朋友。1. 打开包之前F105的USB外设和F103是两套东西1.1 F103的习惯在这里是最大的坑很多人被F103的USB开发经验“驯化”得很自然芯片的DP/DM脚拉出来接一个1.5k上拉电阻到3.3V然后调一个48MHz时钟给USB外设再用标准外设库那一套USB_Init()、USB_SIL_Write()的接口收发数据。这套玩法本身没问题但它是专门针对STM32F103里那个USB_FS_Device外设的——它只是一个纯粹的USB设备控制器芯片只能作为从机硬件设计上就没有Host模式。到了STM32F105情况完全不同。F105内部集成的USB控制器叫OTG_FSOTG就是On-The-Go它同时支持Host主机、Device设备和OTG双角色切换三种模式而且芯片内部已经集成了全速USB收发器。这一点带来的直接后果是软件库变了、寄存器结构变了、端点机制变了、中断入口也变了。你要是把F103的USB工程文件直接拖进来基本是满屏的“usb_regs.h not found”和无法识别的外设名。1.2 F105这套OTG外设到底多了什么东西F105这个OTG_FS控制器有DMA能力、支持控制/批量/中断/同步四种传输类型并且它内建的DP上拉控制逻辑和F103那套外部上拉方案不是一回事。官方参考电路里F105的USB_DP和USB_DM往往不需要再外接1.5k上拉到3.3V而是由芯片内部软件控制上拉的使能时机用来精确地完成USB枚举阶段的上电握手过程。这一点和F103的最小系统设计习惯很不一样我自己就见过有人把F103的1.5k上拉习惯硬搬到F105板子上结果枚举波形异常、设备反复跳变的糟心事。所以拿到“STM32F105_USB”这类工程包后第一件事千万别急着编译烧录先确认三件事板子上的DP/DM是不是直连USB座的原理图里有没有多余的外部上拉工程用的USB库是OTG版比如STM32CubeMX生成的USB_DEVICE中间件还是老掉牙的F103标准外设库时钟树里USB的48MHz是不是来自PLL某个输出而不是随便配置的前分频。2. “自定义协议”到底定在哪一层四种通道选型先想清楚“带自定义协议”这个说法其实很容易让人误解。有人以为自定义协议是要在USB协议栈底层自己改描述符、自己写事务处理这其实是把问题想复杂了——99%的应用根本不需要碰USB底层事务只需要在某一类标准USB设备通道之上约定自己的数据帧格式。真正要抉择的是以什么类型的USB设备形态出现在电脑或手机上。2.1 HID自定义报告、CDC上层帧、Vendor类和三方驱动的取舍常见的通道形态有四种HID自定义报告把设备枚举成一个HID设备一般是自定义用途页用报告描述符定义输入/输出报告的格式。优点是完全免驱Windows/macOS/Linux都认延迟很低缺点是端点包大小被限制在64字节以内吞吐量不算高而且报告描述符一旦写错设备直接枚举失败。CDC虚拟串口 私有帧协议把设备枚举成一个“虚拟COM口”在上位机里看到的就是一个串口但物理链路其实是USB。你可以在串口数据流之上定义自己的帧协议比如帧头、长度、CRC、命令字。最大的好处是开发调试极其方便——串口助手就能看数据完全免驱全双工。Vendor自定义类 WinUSB/libusb走厂商自定义类Windows下需要装WinUSB驱动或配合libusb使用适合需要高吞吐、主机端自己写上位机的场景。缺点是驱动的分发和安装对普通用户不友好。MSC大容量存储类一般不做双向交互用主要是U盘、读卡器这类场景。2.2 我为什么默认建议从CDC加帧协议起步看这个压缩包的标题里面应该大概率是“CDC/虚拟串口 应用层自定义帧结构”这一类实现。我自己的经验是如果项目里没有特殊的高吞吐要求从CDC加帧协议起步是最稳的。原因很朴素调试链路短。F105枚举成COM口后上位机直接拿串口助手收字节不需要先写一套USB驱动测试工具。出错容易定位。USB枚举、驱动安装、数据传输这几个环节出了状况能快速通过“设备管理器里有没有COM口”来隔离问题。迁移成本低。后续如果要从USB换到普通串口、蓝牙SPP或者以太网透传应用层的帧协议几乎不用改。这里补充一个容易混淆的点CDC虚拟串口和外接CH340/CP2102/FT232R这类USB转串口芯片是两码事。CDC是芯片直接枚举出来一个串口接口不需要在板上再放一颗USB转串口IC而FT232R/CH340方案是单片机走UART再由外部芯片完成USB转换。F105既然自带USB OTG直接用CDC通道是最干净的做法。通道类型是否免驱单包大小适合场景上手难度HID自定义报告是不超过64字节键鼠、遥控器、传感器读取低CDC 自定义帧是可按批量端点配置通常512字节以内数据采集、设备控制、日志回传低Vendor类 WinUSB需要驱动可做大包传输高速数据流、上位机专用高MSC类是以扇区为单位存储读写中3. 一版能直接抄的协议帧格式和解析状态机说完了通道形态回到“自定义协议”本身。下面这版帧格式是我在实际项目里调过、扛过一段时间稳定运行的版本可以直接抄去做参考再根据自己的命令集改。3.1 帧结构设计把帧头、长度、命令字、序号和CRC放在一张表里我推荐的最小帧结构长这样字段长度说明帧头11字节固定0xAA帧头21字节固定0x55协议版本1字节0x01便于以后升级帧长度1字节从命令字到数据区末尾的字节数命令字1字节0x01读设备信息、0x02写参数、0x10升级等序列号1字节每帧加一用于检测丢包数据区0~512字节载荷CRC162字节覆盖命令字到数据区末尾低字节在前帧头和CRC这两个设计点是很多人一开始不太当回事的。帧头用两个0xAA 0x55主要是为了在数据流里对齐边界但这里有个坑如果数据区里恰好也出现连续的0xAA 0x55你的解析器可能会被误导。所以协议里要么做字节填充比如数据里的0xAA转义成0xAA 0x00要么解析时依赖后面的帧长度再做一次复核连续三次长度校验失败就重新找帧头。CRC16比CRC32更适合这种短帧场景计算开销小、实现简单嵌入式端查表法几微秒就完成足以对抗传输中的随机性错误。我用的是CRC-16/MODBUS多项式0x8005初始值0xFFFF主机和从机各维护一张256项的表即可。3.2 解析状态机的代码思路和半包处理USB中断收包的特点是数据是按端点的包长度比如64字节分批次到达的你永远不知道一帧完整协议数据会不会在一次回调里全部到齐。所以绝对不能在CDC_Receive_FS()里用“收到一包就解析一包”的线性思路去处理必须用一个环形缓冲区先攒数据再由状态机逐字节滚动解析。状态机思路非常简单状态0等待帧头1。收到0xAA进状态1否则原地等待。状态1等待帧头2。收到0x55进状态2否则回到状态0重新等0xAA。状态2等待协议版本和帧长度。收满2字节后检查帧长度是否在合法范围内不合法就回到状态0。状态3按帧长度继续收命令字、序列号和数据区。状态4收CRC16两个字节计算校验。通过则回调协议处理函数不通过则丢弃整帧回到状态0。代码上可以用一个protocol_parse(uint8_t byte)函数每来一个字节调用一次。配合环形缓冲区可以这样处理串口数据void CDC_Receive_FS(uint8_t* Buf, uint32_t* Len) { for (uint32_t i 0; i *Len; i) { ring_buffer_write(g_rx_ring, Buf[i]); protocol_parse(Buf[i]); } USBD_CDC_SetRxBuffer(hUsbDeviceFS, g_rx_buf[0]); USBD_CDC_ReceivePacket(hUsbDeviceFS); }注意最后那两行CDC底层在接收完一包后必须重新调用USBD_CDC_ReceivePacket()挂起下一个接收请求否则设备端收到的数据会越来越稀疏甚至直接停摆。这是CubeMX生成的CDC工程里非常容易翻车的地方。3.3 应答与超时重传嵌入式端最简单的ARQ设计有了帧格式之后如果没有应答机制万一主机发的帧在线上被干扰了设备端就只能一直傻等。最简单的做法是ARQ自动重传请求设备收到合法帧后回一帧0x80/0x81的ACK/NACK应答主机发出命令帧后启动一个定时器比如100ms内没收到ACK就重发重发3次失败就向上层报错。序列号在这里有一个实际价值主机每发一帧序列号加一设备端应答时把收到的序列号原样带回去。这样主机能区分“ACK是延迟收到的旧帧应答”还是“当前帧的应答”避免超时重传机制在握手场景下自己把自己搞乱。别小看这个细节很多自测时好好的协议一放到实际环境里就出现重复执行、命令错乱基本都是没做序列号去重导致的。4. 让这个工程真正跑起来CubeMX配置里的几个细节拿到工程包后我建议不管你最后用什么IDE都先用STM32CubeMX把F105的初始化配置重新打开过一遍。原因很简单别人能跑的工程结构不一定能在你的板子上跑时钟树和引脚配置必须和硬件实际对应起来。下面这几个配置项是我每次做F105 USB都会特别检查的。4.1 时钟树USB时钟一定得是精确48MHzF105的OTG_FS要求48MHz的USB时钟但它的系统时钟最高可以跑到72MHz。在CubeMX时钟树里你需要把USB的时钟源选成PLL输出并且确保最终算出来正好是48MHz不是48.1MHz也不是47.9MHz。USB规范对全速设备的时钟精度要求比较严格偏差太大会导致枚举失败或者数据传输频繁出错。我见过一个很典型的情况有人配置时钟树时为了好看把系统主频调到64MHz然后USB时钟随便配了一个编译烧录后设备管理器里时而认出来时而认不出来。后来把时钟树重新拨到系统72MHz、USB 48MHz问题立刻消失。USB时钟这块宁可少超频也别让48MHz跑偏。4.2 引脚和上拉别把F103的板子设计习惯带过来F105的OTG_FS在芯片引脚上一般对应USB_DPPA12USB_DMPA11VBUS感知如果做Host或OTGPA9CubeMX里选中USB_OTG_FS并激活Device模式后PA11/PA12会自动配置为串行线功能不需要自己手工设置成模拟输入或者推挽输出。我之前提过F105内部有DP上拉控制机制所以如果你的板子是F105参考设计USB_DP上再外挂一颗1.5k上拉到3.3V反而可能和内部上拉一起把信号电平拉得异常。动手前务必对着板子的原理图确认一遍以官方参考电路为准。4.3 描述符与VID/PID枚举失败很多时候是驱动缓存惹的祸CubeMX生成的USB工程默认会填一个ST的VID和PID。问题在于如果这个设备之前在电脑上已经被Windows记录过“VID_XXXXPID_YYYY”的驱动绑定而你换了新的工程但VID/PID还是同一组Windows可能会继续加载缓存里旧的驱动配置这在新旧协议切换时非常坑。经验做法在调试阶段把设备描述符里的VID/PID改成你自己定义的一组测试值比如VID 0x1234、PID 0x5678避免和旧设备冲突。如果你只是在一台电脑上调试这种自定义值没有版权风险等真要量产时再去USB-IF申请正式的VID。字符串描述符里的产品名也顺手改一下比如“F105 Custom CDC”这样设备管理器里能一眼认出是不是你自己的设备。4.4 中断和内存池不起眼但特别容易翻车的两个设置CubeMX生成工程后USB中间件会自动创建一个收发缓冲区但CDC类这种涉及动态缓冲的场景需要把工程配置里的Heap Size调大一些。Heap太小会出现一个典型症状程序跑起来正常但只要主机端收发几包数据设备就进入HardFault。我一般直接给到0x400到0x800具体看端点缓冲需求。中断配置上F105的OTG_FS有一个全局中断OTG_FS_IRQHandler中断优先级建议设置得比普通外设高一点否则单片机在处理其他低优先级外设中断时USB主机会因为长时间没有收到设备状态响应而判定设备异常断开。这个问题在做数据采集类项目时特别常见——传感器中断频繁USB被打断然后就掉线。5. 调试USB自定义协议三板斧走天下USB调试不像串口调试那么直白数据在高速差分线上跑示波器探头一上去信号就可能变形。我调这个工程时总结了一套三板斧照着走基本能稳住阵脚。5.1 第一板斧Bus Hound抓URB先确认端点有没有数据拿到一个USB工程最怕的问题就是“设备枚举都成功了但数据收不到”。别急着查代码先用Bus Hound这类USB抓包工具看看主机到底有没有收到设备发来的中断/批量传输事务。抓包里能一眼看到端点号、事务类型、数据长度每次传输是成功完成还是出现NAK/超时设备返回的数据内容对不对。如果我抓到的URB里压根没有设备发来的数据问题大概率在设备端如果U盘般的数据裹着一堆带错误的状态那就要查波形电平、时钟精度和焊接问题了。这一步能把“硬件问题”和“软件问题”隔离看比蒙头改代码效率高一截。5.2 第二板斧串口日志配合协议状态机把“看不见”变成“看得见”这一条听起来有点土但真的管用。F105的USB工程调试阶段保留一路UART打印口比如PA2/PA3的USART2把协议状态机的状态跳转、帧校验结果、收到的命令字和序列号全部打印出来。USB那套收发过程对开发者来说是不可见的但UART日志能让你实时看到设备内部状态。我在调试一个帧解析bug时就是靠UART日志发现状态机在数据区里碰到0xAA 0x55时误判了帧头边界——因为日志里明明收了完整帧校验也过了但解析出来数据总是错位。后来在状态机里加了长度复核这个问题才彻底消失。这种“日志驱动调试”的方法比一遍一遍编译烧录强太多。5.3 第三板斧枚举失败与Unknown Device的三条排查路径F105 USB设备在电脑上识别为“Unknown Device”是提问率极高的问题。按照我的经验按下面顺序排查枚举时序问题查看系统时钟和USB时钟是否精确48MHz确认DP上拉功能是在初始化完成之后才由软件使能的不能过早也不能过晚。供电与接触问题USB座子的VBUS和GND接触不良、电流不够会导致设备枚举到一半就掉电重启。F105的板子如果在USB口直供的情况下最明显。驱动缓存问题设备管理器里把旧设备记录全部删掉或者改VID/PID再重新插入。Windows对同一VID/PID的驱动绑定特别顽固我曾经为了这问题折腾一个下午最后换了PID瞬间解决。另外还有一个小技巧用usbview或设备管理器看设备枚举时的“描述符请求失败/设备描述符请求失败”错误码。如果错误在“请求设备描述符”阶段就发生多半是物理层或时钟问题如果枚举过了但数据传输失败更多是软件端点配置问题。5.4 数据线布线的那点事D/D-不是随便拉两根线就完事F105的全速USB速率是12Mbps虽然不算高速但D/D-两根线属于差分信号布线时更建议按差分线处理保持等长并且尽量短。PCB上D/D-之间不要铺地铜串接的电阻有些参考设计会放22Ω左右也要按官方BOM来不要随手抓一颗大阻值电阻放上去。USB座子用带金属外壳的外壳接地要可靠否则静电打进来很容易导致设备掉线。这个坑在手工飞线的实验板上最明显D/D-两根杜邦线绕来绕去能枚举成功但是一传大块数据就出错换短粗的线或者重新焊板子后问题消失。硬件链路不干净软件上怎么调CRC、重传都只是掩耳盗铃。6. 拆包之后我建议你按照这个目录结构重建工程拿到压缩包后很多人的习惯是把整个工程文件夹拖出来直接用。但别人工程里的中间文件、生成目录、调试配置不一定适合你的项目。我的建议是不直接改原包而是按照下面的分层思想把它拆开重建代码反而更好维护。6.1 分层思想把协议从USB库里彻底剥出来目录划分可以参考Core/ Inc/ Src/ Drivers/ STM32F1xx_HAL_Driver/ USB_DEVICE/ app/ target/ class/ Protocol/ protocol.c protocol.h crc16.c crc16.h App/ main_loop.c command_handler.c Uploader/ protocol_uploader.py关键点在于Protocol/这一层。很多人写USB工程把协议解析直接写在usbd_cdc_if.c里一开始挺方便等要加功能、换通信链路时就会发现自己被绑死在USB库里。正确做法是usbd_cdc_if.c只负责“把端点收到的字节交给协议层把协议层要发送的字节交给端点”协议帧结构、命令表、CRC校验全部放在独立的protocol.c里。这样以后就算把USB换成蓝牙、换成以太网业务代码一行都不用动。6.2 从包到产品还能往哪些方向扩这个工程如果只是跑通CDC收发那只是刚开始。基于F105的OTG能力有两条很实用的扩展路径一是把设备端做成复合设备比如CDC HID的组合CDC用来传输数据HID用来做面板控制键。PC端同时看到COM口和自定义HID交互体验和数据通路可以分开。二是用F105的Host模式接其他USB外设比如直接读取U盘里的配置文件、接入4G模块的虚拟网卡透传数据。这里要说清楚Host模式和Device模式的代码是完全两个栈CubeMX里切换模式后USB Host中间件会生成不同的回调框架和本文前面讲的设备端协议分层不是一个概念。真要做Host建议先把Host库的枚举流程和类驱动跑通再回来复用Protocol/层业务逻辑同样不用大改。经常有人问我“这个包我自己重新造一遍该从哪下手”我的回答一直是先把时钟树和引脚配置用CubeMX从头过一遍然后把协议解析状态机单独抽出来放到PC上用模拟串口数据跑一遍确认所有边界情况都处理了最后再烧到板子上连真机调。这两个步骤看起来绕远路实际省的时间最多。USB这东西硬件链路一旦不稳代码层面再努力都是白搭协议层一旦写乱了后面加任何功能都是往烂摊子上摞砖头。把基础打扎实剩下的都只是时间问题。本文还有配套的精品资源点击获取