ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103C8T6实现USB自定义HID+MSC复合设备详解

STM32F103C8T6实现USB自定义HID+MSC复合设备详解 简介一套面向STM32开发者的USB复合设备工程包基于F103C8T6芯片将定制HID与大容量存储类设备复合在一起。端点零负责枚举端点一、二分别用作键盘与鼠标鼠标支持绝对和相对两种模式端点三挂载存储设备并给出官方示例与FAT16文件系统两个版本默认使用FAT16。针对IBM AIX7等主机在启动引导阶段不请求报告描述符、导致共用接口的键鼠无法使用的问题工程将键鼠分配到不同接口既增强了兼容性也为F1、F4、F0等系列实现HID与MSC、CDC与MSC、HID与CDC乃至三重复合提供参考。压缩包共1420个文件以C源码和头文件为主另有汇编、调试配置、链接脚本、IAR与Keil工程文件及hex固件整体约22.53MB已有833人学习下载。借助这份完整工程可对照学习端点分配、报告描述符组织、枚举流程和多接口协同适合有USB基础、想进阶复合设备开发的工程师。 如果你手头有一块 STM32F103C8T6 的最小系统板又正好在做需要“既能像 U 盘一样存取文件又能通过自定义协议实时通信”的 USB 设备那么这个F103C8T6_USB_CUSTOMHIDMSC.rar工程基本就是为你准备的。它把一个 USB 口同时做成自定义 HID 设备和 MSC 大容量存储设备插到电脑上之后设备管理器里会同时出现一个 HID 设备和一个磁盘。这种“一个 USB 口干两件事”的做法在数据记录仪、加密存储、设备配置工具里非常常见。我用这个工程跑通了不少项目也踩过不少坑。下面这篇文章不是简单的资源介绍而是把这个工程背后的硬件要求、描述符组织、HID/MSC 协议栈实现以及抓包排查的方法都拆开讲清楚。无论你是想把代码移植到自己的板子上还是想彻底搞懂复合设备怎么工作这篇都能给你省下不少时间。1. 为什么要把 CUSTOMHID 和 MSC 合体1.1 单一接口的局限先想清楚一个问题为什么非要把两个东西合在一起如果只用 CUSTOMHID免装驱动这一点确实很香上位机用 hidapi 或者 Windows 的 HID API 就能收发数据通信延迟也可控。但 HID 只是“通道”它没有文件系统的概念PC 端用户想把设备里的数据导出、把配置参数拖进去体验非常差。想一想数据记录仪要给客户导出几百 KB 的日志总不能让人用一个串口助手去收十六进制数组吧。MSC 正好补上这一环。MSC 一枚举出来Windows 就把它当 U 盘资源管理器里能看到盘符可以直接拖文件、批量拷贝甚至可以自动挂载。但 MSC 只是一个块设备它不是为“实时命令”设计的你想让它立刻执行“开始采集”这种控制指令用 U 盘协议根本无从下手。所以复合设备的思路就是一个 USB 物理连接多个接口并存。HID 接口负责实时命令和状态上报MSC 接口负责文件级存储。上位机一边通过 HID 通道下指令一边通过资源管理器操作盘符各取所长。1.2 这类设备最常见的落地场景我实际接触过的项目里这种复合结构集中在这几类数据记录仪传感器数据存在 SPI Flash 或 SD 卡里HID 通道下发启动采集、停止、校时指令MSC 通道让用户像 U 盘一样拖出 CSV 或 BIN 日志文件。加密/授权 U 盘MSC 暴露出来的盘符只是密文用户拿不到明文HID 通道承担密钥协商、权限认证软件确认身份后才允许导出明文或者格式化。产线配置工具HID 通道做固件升级握手和命令交互MSC 通道批量放置配置文件生产调试极大提速。小型数据采集卡实时波形用 HID 快速上报离线缓存用 MSC 目录读走。这款工程提供的正是这些场景共用的底层骨架一个配置描述符下挂两个接口一个走自定义 HID一个走 MSC代码能直接改改 VID/PID 就用。1.3 从 USB 主机的视角看复合设备从主机角度一个复合设备仍然只是一个“设备”它的设备描述符只有一份但配置描述符里包含多个接口描述符。总线枚举时主机读到配置描述符后会逐个检查接口看到接口的类代码为 0x03 就加载 HID 驱动看到接口类代码为 0x08 就加载大容量存储驱动于是设备管理器里就出现了两个“叶子节点”。需要留意的是 USB 的接口关联描述符IAD。IAD 是用来把多个接口绑成一个功能的比如音频设备的控制接口和音频流接口需要成组匹配驱动。但这里 HID 和 MSC 是两个独立功能通常不需要 IAD直接把两个接口依次写在配置描述符里就行。工程里如果没有出现 IAD 相关结构体那是对的。2. F103C8T6 的 USB 硬件与时钟配置枚举失败先从这查2.1 F103 内部 USB 外设的能力边界F103C8T6 的 USB 是 device-only 的 USB 2.0 Full Speed 从设备PA11 固定是 USB_DMPA12 固定是 USB_DP。它不是 OTG不能当主机去接 U 盘或键鼠。很多刚接触的人会误以为 STM32F103 都带 OTG拿到 F103C8T6 想去做 Host结果片上 USB 根本没法切主机模式。这个工程只做从设备F103C8T6 完全够用如果哪天要做 USB Host得换 F105/F107 或者接个外部主控芯片。端点资源方面F103 的 USB 外设不像某些芯片那样每个端点只能配一个方向它的端点寄存器同时有 IN 和 OUT 收发缓冲区所以同一个端点号可以双向使用。我通常这样分配端点 0 固定控制传输HID 使用端点 1 IN 和端点 1 OUTMSC 使用端点 2 OUT 和端点 3 IN。这样端点号不冲突也方便在标准库里用ENDP1、ENDP2、ENDP3直接编程。2.2 48MHz 时钟配置第一道生死线USB Full Speed 要求设备提供精确的 48MHz 时钟。STM32F103 的 USB 时钟不是独立的它来自 PLL 输出再分频典型配置是外部 8MHz 晶振经 PLL 九倍频到 72MHz再除以 1.5 得到 48MHz。代码里对应的关键配置是这样RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE);如果你把外部晶振换成了 25MHz或者干脆没焊晶振、程序用 HSI 运行USB 时钟就不可能落在 48MHz主机端最常见的现象就是“无法识别的 USB 设备设备描述符请求失败”。所以从别人工程拷代码时第一个检查点必须放在时钟函数里确认 HSE 是否启用、PLL 倍频和 USBCLK 分频是否一致。这个问题一旦存在我们在调试里做再多别的优化都是白费功夫。2.3 D/D- 上拉、串联电阻与 Layout 细节硬件上全速 USB 设备要求在 D 引脚接一个 1.5kΩ 上拉电阻到 3.3V主机靠检测这个上拉来识别设备插入。很多现成的 F103C8T6 核心板已经把上拉画进去了但如果你自己做板记得检查三件事上拉电阻是否接到了 3.3V误接到 5V 会导致电平不匹配轻则枚举失败重则损伤引脚。D 和 D- 上各串联一个 22Ω 左右的电阻放在 MCU 和 USB 座子之间用于阻抗匹配和限制振铃。热搜里常有人问“USB DD- 电容大小”D/D- 走线上不建议加太大的对地电容。见过有人加 100nF 去“滤波”结果信号边沿被拉得惨不忍睹。最多放 10pF~22pF 的电容或者干脆不放。布局上D/D- 尽量等长、短距离、少打过孔USB 座子的外壳地要单独处理。有些板子只是原理图对了但因为 Layout 太随意D/D- 被长走线绕了十几厘米最后枚举不稳定插拔频繁出错。3. 复合设备描述符编排HID 与 MSC 共存的核心3.1 描述符的层次结构复合设备能不能被正确识别关键在配置描述符的编排。枚举时主机会先取设备描述符再以“配置描述符”为入口得到整棵配置树。一个典型结构是配置描述符bNumInterfaces 2接口 0HID 接口bInterfaceClass 0x03HID 描述符其中指定报告描述符长度中断 IN 端点如 EP1 IN最大包 64 字节中断 OUT 端点如 EP1 OUT最大包 64 字节接口 1MSC 接口bInterfaceClass 0x08bInterfaceSubClass 0x06bInterfaceProtocol 0x50批量 OUT 端点如 EP2 OUT批量 IN 端点如 EP3 INMSC 走的是 Bulk-Only TransportBOT协议所以不需要额外类描述符只有接口描述符加两个 Bulk 端点。HID 接口则必须带一个类特殊描述符HID 描述符报告描述符是单独通过GET_DESCRIPTOR(Report)获取的。千万注意端点描述符里的bmAttributesHID 写 0x03中断MSC 写 0x02批量写错后主机驱动行为会非常怪。3.2 接口顺序对 Windows 识别的影响接口的排列顺序在某些系统上会直接影响 MSC 盘符的挂载。按理说 HID 接口在 0、MSC 接口在 1 也能工作但我确实在某个 Win10 版本上遇到过MSC 接口放第二位时磁盘管理器里偶尔不刷新需要重新插拔才能看到盘符把 MSC 接口换到第一位后问题消失。这个问题很玄跟 PnP 驱动的匹配顺序有关。如果你的设备在部分电脑上“U 盘不显示但 HID 正常”不用急着怀疑硬件先尝试把两个接口的顺序换一下。这个工程里如果usb_desc.c的配置描述符数组是“HID 接口在前”你可以按需把 MSC 接口挪到前面重新编译测试。3.3 字符串描述符与序列号复合设备最好为每个子接口提供清晰的字符串描述符尤其是产品字符串和序列号。Windows 设备管理器里显示的名称来自字符串描述符的iProduct调试时如果两个接口都叫“STM32”很容易认错目标。序列号在复合设备里承担额外作用Windows 通过序列号判断多个接口是否来自同一个物理设备也经常用它做盘符绑定的注册表键。所以序列号必须固定不能让每次枚举都随机变化否则用户插上设备后盘符会乱漂移。工程里一般在设备描述符的iSerialNumber字段指定一个固定字符串比如产品型号加批次号。3.4 HID 报告描述符设计HID 接口能被识别成“自定义”而不是“键盘鼠标”完全由报告描述符决定。最常用的做法是把 Usage Page 设置为厂商自定义0xFF00。下面是我经常用的 64 字节输入报告加 64 字节输出报告的最小描述符const uint8_t CustomHID_ReportDescriptor[] { 0x06, 0x00, 0xFF, // USAGE_PAGE (Vendor Defined) 0x09, 0x01, // USAGE (Vendor Usage 1) 0xA1, 0x01, // COLLECTION (Application) 0x09, 0x01, // USAGE (Vendor Usage 1) 0x15, 0x00, // LOGICAL_MINIMUM (0) 0x26, 0xFF, 0x00, // LOGICAL_MAXIMUM (255) 0x75, 0x08, // REPORT_SIZE (8 bits) 0x95, 0x40, // REPORT_COUNT (64) 0x81, 0x02, // INPUT (Data, Var, Abs) 0x09, 0x02, // USAGE (Vendor Usage 2) 0x75, 0x08, // REPORT_SIZE (8 bits) 0x95, 0x40, // REPORT_COUNT (64) 0x91, 0x02, // OUTPUT (Data, Var, Abs) 0xC0 // END_COLLECTION };这里用的 64 字节报告大小与全速中断端点最大包长一致上位机读写时不会遇到半包问题。如果报告长度比端点包长还大HID 协议会用多包拼接你就得自己处理分包没必要。4. 代码侧的收发与存储HID 消息流和 MSC 块设备4.1 HID 收发的几个关键函数不管使用 ST 标准外设库还是 HAL 库CUSTOMHID 都要把报告描述符注册进 USB 配置并处理两类传输控制传输的SET_REPORT/GET_REPORT和中断端点的 IN/OUT 传输。实际开发中我建议把 OUT 端点收到的报文先放到一个环形缓冲区再在主循环里解析处理而不是在 USB 中断回调里直接执行业务逻辑。因为 HID 的中断回调频率不低一旦在里面执行 Flash 写入或复杂计算会拖累整个 USB 中断造成数据丢包。发送侧则要查询前一个 IN 传输是否完成确认端点空闲之后再写缓冲区否则会覆盖之前的发送。HID 中断传输受主机轮询间隔限制F103 全速下典型 bInterval 是 1ms每包最多 64 字节理论带宽大约 64KB/s实际能稳定跑一半就不错。如果带宽不够就要考虑换用批量传输的厂商自定义类这也是选型时就要提前明确的事情。4.2 MSC 的 Bulk-Only 协议与 SCSI 命令MSC 侧的协议核心是 BOTBulk-Only Transport。主机通过 Bulk OUT 端点发来 31 字节的 CBWCommand Block Wrapper里面包含命令块、传输方向和长度设备执行完 SCSI 命令后通过 Bulk IN 端点回一个 13 字节的 CSWCommand Status Wrapper报告执行结果。整个流程是收 CBW → 传输数据 → 发 CSW。SCSI 命令不需要全部实现但基础子集必须正确处理命令操作码主要作用INQUIRY0x12返回厂商、产品名、版本TEST UNIT READY0x00报告设备是否就绪READ CAPACITY(10)0x25报告总扇区数和扇区大小READ(10)0x28读取扇区WRITE(10)0x2A写入扇区MODE SENSE(6/10)0x1A/0x5A返回介质参数固定响应即可START STOP UNIT0x1B处理停止/弹出一般直接返回成功很多“U 盘不识别”的问题并不是硬件坏了而是 INQUIRY 或 TEST UNIT READY 回包有误。另外别忽略 CSW 里的dCSWSignature它必须是0x53425355即 “USBS”一旦写错主机就会视为协议错误Windows 直接报“无法访问”或者“设备不可用”。4.3 存储介质选择与缓存的坑MSC 底层挂什么介质是这个工程里最影响体验的部分。F103C8T6 内部 Flash 只有 64KB代码本身还占一部分如果你只是想临时存少量配置参数可以用内部 Flash 模拟小容量 U 盘但要做到能拷日志、存固件建议外挂 SPI FlashW25Q64/W25Q128或者 SD 卡。SPI Flash 方案电路简单但要注意扇区擦除比较慢。W25Q 的 4KB 扇区擦除通常要几百毫秒如果 Windows 连续发来多个写扇区命令而你的代码还在阻塞等待擦除完成上位机就会明显卡顿。工程里如果提供 SPI Flash 驱动通常应该有“扇区缓存”机制先把 512 字节的扇区读到 RAM修改后再整块写回并且在 MSC 状态机里以非阻塞方式管理擦写。SD 卡方案则需要接 FATFS 或者直接把 SD 卡扇区映射给 MSC代码量会大不少但容量、速度和兼容性都更接近真实 U 盘。5. 用 USB 抓包定位枚举和传输问题5.1 抓包工具体验调试复合设备我强烈建议从最开始就学会抓 USB 包。Windows 下可以用 Wireshark 加 USBPcapLinux 下直接用 usbmon 模块。抓包能看到主机和设备的每一笔控制交互比在代码里加串口日志高效得多。正常枚举流程大概是主机先发GET_DESCRIPTOR(Device)取前 8 字节设备回包主机发SET_ADDRESS主机再次GET_DESCRIPTOR(Device)取完整 18 字节然后GET_DESCRIPTOR(Configuration)拿完整配置设备把配置描述符分段返回最后SET_CONFIGURATION完成配置。抓到包之后一眼就能看出卡在哪一步。卡在前面设备描述符阶段基本是硬件和时钟问题卡在配置描述符阶段通常是wTotalLength写错、接口描述符长度不对、端点地址冲突卡在枚举之后的 HID/SCSI 阶段则是报告描述符或 SCSI 响应的问题。5.2 “设备描述符请求失败”的高频原因“未知 USB 设备设备描述符请求失败”是讨论区出现频率极高的问题我碰到的原因主要集中在三种一是 48MHz 时钟没配好设备对 SETUP 包没有响应二是 D 上拉电阻缺失或接到了 5V导致主机根本没检测到全速设备三是 PA11/PA12 的 GPIO 复用配置错误比如没有把引脚设置成复用推挽输出。有一个很好用的检查方法上电后用万用表量 D 引脚对地电压正常全速设备枚举前应该被上拉到 3.3V 附近。如果 D 一直是低电平主机根本不会尝试枚举这时候再怎么调软件都是白费。5.3 枚举成功但 HID/MSC 单独异常枚举成功不代表万事大吉。HID 设备不出现在“符合 HID 标准的用户控制设备”分类里多半是报告描述符没有使用厂商自定义 Usage PageWindows 把它当成标准鼠标键盘去匹配驱动了。MSC 设备插上后提示需要格式化多数是READ CAPACITY返回的扇区数不对或者 INQUIRY 响应中的可移动介质位没有置位。还有一类问题很隐蔽MSC 接口的 CBW 接收不完整或者 CSW 返回过早Windows 会卸载驱动并报告“延迟写入失败”。这类问题需要用抓包确认 CBW/CSW 的时序单纯盯着代码看很难发现。6. 移植这个工程时值得注意的几个坑6.1 先分清库的版本再动手这种网上的 .rar 工程很多是基于 ST 早期标准外设库SPL写的目录结构通常长这样Libraries/ CMSIS/ STM32F10x_StdPeriph_Driver/ Project/ usb_desc.c usb_prop.c usb_endp.c usb_pwr.c main.c如果你平时用 HAL 库这里不要急着把整个工程往 CubeMX 里塞。先确认启动文件是startup_stm32f10x_md.sF103C8T6 属于中等容量换成高密度启动文件会导致中断向量错乱。我自己移植的做法是先建一个空的 SPL 最小工程把 USB 相关文件逐个加入编译通过后再加业务逻辑这样能最快定位问题。6.2 内部 Flash 和 SPI Flash 的介质切换如果工程默认用内部 Flash 模拟 U 盘你的板子却有 SPI Flash那至少有两层要改。第一层是msc_mem.c里的介质读写函数要换成 SPI Flash 驱动第二层是容量和扇区参数内部 Flash 页大小和 SPI Flash 的扇区大小不一样但 MSC 向主机暴露的块大小最好固定成 512 字节具体映射由代码完成。这里有个常见误区直接把 MSC 的READ(10)/WRITE(10)按主机给的 LBA 去操作 SPI Flash但 SPI Flash 需要按扇区擦除只改 512 字节可能要把整块 4KB 擦掉重写。没有缓存和回写机制的话Windows 连续写入时迟早出错。6.3 上下位机联调时的注意细节联调时HID 侧的上位机如果用 hidapihid_open的 VID/PID 一定要和工程里usb_desc.c的设备描述符完全一致。MSC 侧先复制小文件、再复制大文件观察 Windows 是否报“延迟写入失败”。延迟写入失败基本都是写命令处理不及时或者 CSW 返回时机不对去检查写命令的状态机。另外如果你是做量产设备的建议在 HID 命令里预留一个“切换 MSC 只读/可写”的控制项。默认插上处于只读状态可以防止误操作通过 HID 下发授权后才切换为可写。这种小功能在工程骨架上加代码不难但能在后续使用里省不少事。我最后想说的是这类复合设备最容易出问题的其实不是代码能不能编译而是描述符、端点、时钟这些底层细节没有理顺。你先把这个工程跑通再对照抓包结果去看每一条描述符很多原来想不通的问题会一下子清晰起来。本文还有配套的精品资源点击获取
返回列表