尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F103移植CherryUSB实现MSC U盘功能详解

STM32F103移植CherryUSB实现MSC U盘功能详解 1. 为什么要在 STM32F103 上折腾 CherryUSBSTM32F103 这颗芯片在嵌入式圈子里算是“国民级”的存在价格便宜、资料多、最小系统板满地都是。但它的 USB 外设只有全速Full Speed模式缓冲区也不大很多人第一次做 USB 通信时都会被各种描述符、端点配置、中断处理搞得头大。传统做法是直接用 ST 官方的 USB 库代码能跑但移植起来比较重换一颗芯片或者换一个 USB 类就得大改。CherryUSB 是一个轻量级的 USB 协议栈特点是分层清晰、代码量小、支持 Device 和 Host 两种模式而且对 MSC、CDC、HID 这些常用类都有现成支持。把它移植到 STM32F103 上核心目标就是让这颗小板子能作为一个 U 盘设备被电脑识别也就是实现 MSCMass Storage Class功能。这个场景很实用比如你的设备采集了一堆数据想直接通过 USB 口把内部 Flash 或者 SD 卡里的文件拖到电脑上不需要额外装驱动插上就能用。这篇文章面向的是有一定 STM32 基础、用过 CubeMX 或者标准库、想尝试第三方 USB 协议栈的开发者。我会从源码下载开始一步步讲到 MSC 功能跑通中间涉及到的描述符配置、端点分配、中断处理、读写回调都会拆开说清楚。你不需要事先了解 CherryUSB 的内部架构跟着做就能复现。2. 移植前的整体思路与方案选型2.1 为什么选 CherryUSB 而不是官方库ST 官方的 USB Device 库比如旧版的STM32_USB-FS-Device_Lib和 CubeMX 生成的中间件都能用但它们的耦合度比较高。举个例子官方库把 USB 标准请求、类请求、端点操作都揉在几个文件里你想加一个自定义的类或者改一下端点缓冲区分配得翻好几层代码。CherryUSB 的设计思路不一样它把 USB 协议栈分成三层最底层是usb_dc设备控制器驱动中间是usb_core协议核心最上面是class各类驱动。你移植的时候只需要实现usb_dc里的几个函数剩下的协议解析、枚举过程、类请求处理都由协议栈自己完成。另一个原因是 CherryUSB 的 MSC 类驱动已经写好了它实现了一个标准的 BOTBulk-Only Transport协议你只需要提供底层的读写函数也就是“怎么从存储介质读一个扇区”和“怎么写一个扇区”。对于 STM32F103 来说存储介质可以是内部 Flash、SPI Flash 或者 SD 卡。我这次用的是内部 Flash 模拟一个小的 U 盘容量不大但足够验证流程。2.2 硬件和软件环境确认硬件方面我用的是常见的 STM32F103C8T6 最小系统板也就是俗称的“蓝板”。它引出了 USB 的 D 和 D- 引脚分别是 PA12 和 PA11。注意STM32F103 的 USB 外设需要外部提供一个 1.5k 的上拉电阻在 D 线上有些板子已经自带了有些没有。如果你用的是最小系统板最好先确认一下 D 有没有通过电阻上拉到 3.3V否则电脑可能识别不到设备。我手头这块板子自带了一个可控制的 1.5k 上拉通过一个三极管控制但为了简化我直接把它短接到 3.3V 了。软件方面我用的开发环境是 Keil MDK 5编译器是 ARMCC V6。CherryUSB 本身是纯 C 写的不依赖特定编译器但你需要确保工程里开启了 C99 模式。STM32 的底层驱动我用的是标准库不是 HAL 库。原因是我觉得标准库的寄存器操作更直接中断处理也更可控。如果你习惯用 HAL 库移植思路是一样的只是底层初始化代码换成 HAL 的写法。2.3 整体移植路线图整个移植过程可以分成四步第一步是下载 CherryUSB 源码把必要的文件加入工程第二步是编写usb_dc驱动也就是 STM32F103 USB 外设的初始化、端点配置、中断处理第三步是配置 MSC 类实现读写回调第四步是调试枚举过程解决电脑识别不到或者识别成未知设备的问题。每一步都有一些容易踩的坑我会在对应章节里详细说明。3. 源码下载与工程文件组织3.1 获取 CherryUSB 源码CherryUSB 的源码托管在 GitHub 上你可以直接克隆仓库或者下载 zip 包。我建议用 git clone方便后续更新。仓库地址是https://github.com/sakumisu/CherryUSB。下载下来之后目录结构大概是这样的core目录放的是 USB 协议核心class目录是各类驱动port目录是不同芯片的底层驱动模板demo目录是一些示例工程。你需要关注的文件有core/usbd_core.c、core/usbd_core.h、class/msc/usbd_msc.c、class/msc/usbd_msc.h、port/dwc2/usb_dc_dwc2.c但 STM32F103 用的是全速设备控制器不是 DWC2所以这个不能用。STM32F103 的 USB 外设是 ST 自己设计的不是 Synopsys 的 DWC2所以你需要自己写一个usb_dc驱动。CherryUSB 在port目录下有一个template文件夹里面有一个usb_dc_template.c你可以基于这个模板来改。3.2 把源码加入 Keil 工程在 Keil 里新建一个分组比如叫CherryUSB然后把下面这些文件添加进去usbd_core.c、usbd_msc.c以及你自己写的usb_dc_stm32f103.c。头文件路径也要加进去包括core、class/msc、port/template这几个目录。注意CherryUSB 用了一些宏来控制功能裁剪你需要在usb_config.h里定义好。这个文件可以放在你的工程目录下内容大概是这样#ifndef USB_CONFIG_H #define USB_CONFIG_H #define CONFIG_USBDEV #define CONFIG_USBDEV_MSC #define CONFIG_USBDEV_MSC_MAX_LUN 1 #define CONFIG_USBDEV_MSC_MAX_BUFSIZE 512 #define CONFIG_USBDEV_MSC_MANUFACTURER_STRING MyCompany #define CONFIG_USBDEV_MSC_PRODUCT_STRING STM32F103 MSC Demo #define CONFIG_USBDEV_MSC_VERSION_STRING 1.0 #endif这里的CONFIG_USBDEV_MSC_MAX_BUFSIZE要和你实际使用的端点缓冲区大小匹配。STM32F103 的 USB 缓冲区总共只有 512 字节所以你要合理分配。我后面会详细讲怎么分配。3.3 注意事项文件依赖与编译顺序CherryUSB 的核心文件之间有一些依赖关系比如usbd_core.c会调用usb_dc_*系列函数而这些函数需要你在usb_dc_stm32f103.c里实现。如果你编译时报“未定义符号”大概率是某个文件没加进去或者头文件路径不对。另外usbd_msc.c里会引用一个usbd_msc_get_cap函数这个函数需要你在应用层实现用来告诉协议栈你的存储介质有多大、扇区大小是多少。这个函数不实现的话编译能过但枚举会失败。4. STM32F103 USB 外设底层驱动实现4.1 USB 外设初始化与时钟配置STM32F103 的 USB 外设挂在 APB1 总线上时钟使能位是RCC_APB1ENR的USBEN位。在初始化之前你需要先配置好系统时钟确保 USB 时钟是 48MHz。STM32F103 的 USB 外设要求时钟精度比较高所以外部晶振最好用 8MHz然后通过 PLL 倍频到 72MHz再分频给 USB。具体来说USB 时钟来自 PLL 的 1.5 分频所以 PLL 输出必须是 72MHz 或者 48MHz。如果你用的是 8MHz 晶振PLL 倍频到 72MHz那么 USB 时钟就是 48MHz正好。初始化代码大概是这样void usb_dc_low_level_init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11 | GPIO_Pin_12; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); USB_Init(); }这里的USB_Init()是标准库里的函数它会配置 USB 外设的基本寄存器比如中断使能、设备地址等。但 CherryUSB 需要自己管理端点所以你不能直接用标准库的中断处理函数而是要自己写USB_LP_CAN1_RX0_IRQHandler。4.2 端点缓冲区分配策略STM32F103 的 USB 缓冲区是一块 512 字节的 SRAM通过USB_BTABLE来管理。每个端点都需要分配发送和接收缓冲区而且地址必须是 2 字节对齐的。CherryUSB 的 MSC 类默认使用端点 1 作为控制端点端点 2 作为批量输出端点 3 作为批量输入。但 STM32F103 的端点 0 是控制端点端点 1 到 7 可以配置成其他类型。我实际用的是端点 1 作为批量输出端点 2 作为批量输入端点 0 作为控制。缓冲区分配我用了下面的方案端点 0 的发送和接收各 64 字节端点 1 的接收 64 字节端点 2 的发送 64 字节。总共用了 256 字节剩下的 256 字节留着备用。分配代码如下#define USB_BUFFER_ADDR_EP0_TX 0x00 #define USB_BUFFER_ADDR_EP0_RX 0x40 #define USB_BUFFER_ADDR_EP1_RX 0x80 #define USB_BUFFER_ADDR_EP2_TX 0xC0然后在USB_Init()之后通过SetUSBDevAddr和SetEPType等函数配置端点。注意CherryUSB 的usb_dc_ep_start_xfer函数会调用你实现的底层发送函数你需要把数据从用户缓冲区拷贝到 USB 缓冲区然后设置USB_EP_CTR_TX位触发发送。4.3 中断处理与事件回调STM32F103 的 USB 中断有两个一个是低优先级中断USB_LP_CAN1_RX0_IRQHandler一个是高优先级中断USB_HP_CAN1_TX_IRQHandler。CherryUSB 只需要低优先级中断就够了因为它的批量传输不要求高优先级。中断处理函数里你需要读取USB_ISTR寄存器判断是哪种事件然后调用 CherryUSB 的回调函数。常见的事件有正确传输CTR、复位RESET、挂起SUSP、唤醒WKUP。复位事件里你需要重新配置端点并调用usbd_core_reset通知协议栈。正确传输事件里你需要判断是哪个端点然后调用usbd_core_ep_handler。下面是一个简化的中断处理框架void USB_LP_CAN1_RX0_IRQHandler(void) { uint16_t istr USB-ISTR; if (istr USB_ISTR_RESET) { USB-ISTR ~USB_ISTR_RESET; usb_dc_reset(); usbd_core_reset(); } if (istr USB_ISTR_CTR) { uint8_t ep istr USB_ISTR_EP_ID; if (istr USB_ISTR_DIR) { // 接收事件 usbd_core_ep_handler(ep, USB_EVENT_RX); } else { // 发送事件 usbd_core_ep_handler(ep, USB_EVENT_TX); } USB-ISTR ~USB_ISTR_CTR; } }这里要注意USB_ISTR的清除方式是写 0不是写 1。标准库里的USB_Istr函数已经帮你处理了但如果你自己写一定要小心。5. MSC 类配置与读写回调实现5.1 MSC 描述符配置要点CherryUSB 的 MSC 类驱动会自动生成大部分描述符但你需要提供一些字符串描述符比如厂商名、产品名、序列号。这些在usb_config.h里通过宏定义。另外MSC 类需要一个接口描述符里面包含两个批量端点。CherryUSB 会根据你定义的端点号自动填充。你需要注意的是端点的最大包长要设置成 64 字节因为 STM32F103 的全速 USB 批量端点最大就是 64 字节。还有一个细节是bInterfaceSubClass和bInterfaceProtocol对于 MSC 设备这两个值通常是0x06和0x50表示 SCSI 透明命令集和 BOT 协议。CherryUSB 已经帮你填好了你不需要改。5.2 实现usbd_msc_get_cap回调这个回调函数用来告诉协议栈你的存储介质容量和扇区大小。函数原型是void usbd_msc_get_cap(uint8_t lun, uint32_t *block_num, uint16_t *block_size);我用的内部 Flash 模拟了一个 64KB 的 U 盘扇区大小是 512 字节所以block_num是 128block_size是 512。实现如下void usbd_msc_get_cap(uint8_t lun, uint32_t *block_num, uint16_t *block_size) { *block_num 128; *block_size 512; }注意block_num和block_size的乘积不能超过你实际能读写的存储空间。如果你用外部 SPI Flash需要根据 Flash 的实际容量来设置。5.3 读写扇区函数与 Flash 操作MSC 类驱动会调用usbd_msc_read_sector和usbd_msc_write_sector两个函数你需要实现它们。对于内部 Flash读操作很简单直接 memcpy 就行。写操作比较麻烦因为 STM32F103 的内部 Flash 写之前必须先擦除而且擦除是按页进行的一页是 1KB 或者 2KB。如果你要写一个 512 字节的扇区可能需要先擦除一整页这会影响其他扇区的数据。我的做法是把整个 64KB 的 Flash 空间划分成 128 个扇区每个扇区 512 字节。写之前先判断目标地址所在的页是否已经被擦除过。如果没擦除就先把那一页的数据读到内存里擦除页再把新数据合并写回去。这个过程比较慢但能保证数据不丢。实际代码里我用了一个简单的缓存机制但为了简化这里只给出核心逻辑int usbd_msc_write_sector(uint8_t lun, uint32_t sector, uint8_t *buffer) { uint32_t addr FLASH_BASE sector * 512; FLASH_Unlock(); FLASH_ErasePage(addr ~(PAGE_SIZE - 1)); for (int i 0; i 512; i 2) { FLASH_ProgramHalfWord(addr i, *(uint16_t *)(buffer i)); } FLASH_Lock(); return 0; }这里省略了数据合并的步骤实际使用中如果你要保留其他扇区的数据必须先读出来再写回去。5.4 注意事项Flash 写寿命与性能内部 Flash 的擦写次数有限大概 1 万次左右。如果你频繁写 U 盘Flash 很快就会坏。所以这个方案只适合做演示或者低频写入的场景。如果你需要频繁读写建议用外部 SPI Flash 或者 SD 卡。另外Flash 写入速度比较慢一个 512 字节的扇区写下来可能要几毫秒电脑端会感觉复制文件很慢。这是正常的因为全速 USB 的批量传输带宽本来就不高加上 Flash 擦除时间整体速度能到几十 KB/s 就不错了。6. 枚举调试与常见问题排查6.1 电脑识别不到设备的排查思路这是移植过程中最常见的问题。电脑插上 USB 线之后设备管理器里没有任何反应或者显示“未知 USB 设备”。排查步骤是这样的先确认硬件上 D 有没有上拉。用万用表量一下 PA12 对 3.3V 的电阻如果是 1.5k 左右说明上拉正常。如果没有上拉电脑根本不会检测到设备插入。然后确认 USB 时钟是不是 48MHz可以用示波器量 PA8 引脚MCO 输出或者直接看代码里的时钟配置。如果硬件没问题那就是软件问题。先看中断有没有进去。在USB_LP_CAN1_RX0_IRQHandler里加一个 GPIO 翻转用示波器看有没有波形。如果没有说明中断没使能检查NVIC_EnableIRQ和USB-CNTR寄存器的中断使能位。如果有中断但枚举失败那就用 USB 抓包工具看一下电脑发了什么请求设备回了什么。常见的错误是设备描述符里的bMaxPacketSize0不是 64或者端点配置不对。6.2 枚举成功但无法格式化或复制文件有时候电脑能识别到 U 盘但提示“需要格式化”或者复制文件时出错。这通常是读写回调返回了错误或者扇区数据不对。先检查usbd_msc_get_cap返回的容量和扇区大小是否正确。如果容量是 0电脑会认为设备没有存储介质。然后检查usbd_msc_read_sector有没有正确填充缓冲区。你可以在函数里加一个断点看看电脑读的是哪个扇区数据是不是全 0。还有一个坑是 MSC 的 BOT 协议要求设备在处理完一个命令后发送 CSWCommand Status Wrapper如果 CSW 里的状态不对电脑会认为命令失败。CherryUSB 会自动处理 CSW但你需要确保读写函数返回 0 表示成功返回非 0 表示失败。6.3 常见问题速查表现象可能原因解决方法电脑无反应D 无上拉检查 1.5k 上拉电阻未知 USB 设备时钟不对确认 USB 时钟 48MHz枚举失败描述符错误用抓包工具对比标准描述符需要格式化容量为 0检查usbd_msc_get_cap复制文件出错写回调失败检查 Flash 擦除和编程设备频繁掉线中断处理超时优化中断服务函数6.4 实操心得抓包工具的选择调试 USB 最有效的工具是硬件抓包器比如 Beagle USB 480 或者类似的协议分析仪。如果没有硬件抓包器可以用软件工具比如 Wireshark 配合 USBPcap。但软件抓包只能看到电脑端的请求看不到设备端的响应所以定位问题会慢一些。我个人的经验是先确保硬件没问题然后用抓包工具看枚举过程重点看GET_DESCRIPTOR请求的响应数据。如果设备描述符的前 8 个字节不对后面的枚举肯定失败。7. 性能优化与后续扩展方向7.1 提高读写速度的几个手段STM32F103 的全速 USB 理论带宽是 12Mbps实际批量传输能到 1MB/s 左右。但受限于 Flash 擦写速度实际文件复制速度可能只有几十 KB/s。如果你想提高速度可以考虑用外部 SPI Flash因为 SPI Flash 的页编程时间比内部 Flash 短很多。另外你可以把 MSC 的缓冲区设大一点比如 512 字节减少 USB 传输次数。但 STM32F103 的 USB 缓冲区总共只有 512 字节所以你要在端点之间做权衡。还有一个优化点是使用 DMA。STM32F103 的 USB 外设不支持 DMA所以只能靠中断搬运数据。但你可以优化中断处理函数减少不必要的判断。比如在批量传输时直接根据端点号调用对应的处理函数而不是遍历所有端点。7.2 从 MSC 扩展到复合设备CherryUSB 支持复合设备也就是一个 USB 接口同时实现 MSC 和 CDC。比如你可以让 STM32F103 既作为一个 U 盘又作为一个串口。这样电脑上会同时出现一个磁盘和一个 COM 口。实现方法是配置两个接口一个给 MSC一个给 CDC。CherryUSB 的usbd_core支持多接口你只需要在描述符里把两个接口都列出来然后在usbd_core_interface_handler里分别处理。不过要注意STM32F103 的端点数量有限MSC 用了两个批量端点CDC 又需要两个批量端点和一个中断端点加起来可能不够。你可以把 CDC 的中断端点去掉只用批量端点但这样就不符合 CDC 规范了。所以实际做复合设备时通常要选端点更多的芯片比如 STM32F103 的增强型或者 STM32F4 系列。7.3 后续可以尝试的方向如果你已经跑通了 MSC下一步可以试试把文件系统加进来。比如在 STM32F103 上移植 FatFS然后把 Flash 模拟成一个磁盘这样电脑上就能看到一个真正的文件系统而不是一个未格式化的磁盘。FatFS 的移植比较简单你只需要实现disk_read和disk_write两个函数底层调用 MSC 的读写回调就行。另一个方向是 USB Host。CherryUSB 也支持 Host 模式你可以让 STM32F103 作为主机去读写 U 盘。但 STM32F103 的 USB 外设只支持全速而且 Host 模式需要提供 5V 电源和过流保护硬件上要复杂一些。如果你有兴趣可以找一块带 USB Host 的板子试试。我个人在实际操作中的体会是CherryUSB 的移植难度主要在于底层驱动的实现尤其是端点缓冲区的分配和中断处理。一旦底层跑通上层的 MSC 类几乎不需要改代码。另外调试 USB 一定要有耐心因为枚举过程涉及很多细节一个描述符字段不对就会导致失败。多抓包、多对比标准描述符问题总能解决。
返回列表