news 2026/9/28 2:41:49

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103移植CherryUSB实现MSC U盘功能详解

1. 为什么要在 STM32F103 上折腾 CherryUSB

STM32F103 这颗芯片在嵌入式圈子里算是“国民级”的存在,价格便宜、资料多、最小系统板满地都是。但它的 USB 外设只有全速(Full Speed)模式,缓冲区也不大,很多人第一次做 USB 通信时都会被各种描述符、端点配置、中断处理搞得头大。传统做法是直接用 ST 官方的 USB 库,代码能跑,但移植起来比较重,换一颗芯片或者换一个 USB 类就得大改。

CherryUSB 是一个轻量级的 USB 协议栈,特点是分层清晰、代码量小、支持 Device 和 Host 两种模式,而且对 MSC、CDC、HID 这些常用类都有现成支持。把它移植到 STM32F103 上,核心目标就是让这颗小板子能作为一个 U 盘设备被电脑识别,也就是实现 MSC(Mass 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 类驱动已经写好了,它实现了一个标准的 BOT(Bulk-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是 128,block_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 协议要求设备在处理完一个命令后发送 CSW(Command 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 一定要有耐心,因为枚举过程涉及很多细节,一个描述符字段不对就会导致失败。多抓包、多对比标准描述符,问题总能解决。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 2:39:28

Django+ECharts构建网易云音乐可视化大屏:从数据清洗到用户画像实战

简介&#xff1a;一份面向高校计算机专业学生与科研从业者的网易云音乐可视化项目资料包&#xff0c;基于Python与Django框架构建数据大屏&#xff0c;聚焦用户画像与播放行为分析&#xff0c;适合用作毕业设计、课程设计或项目初期演示。资源共54个文件&#xff0c;以24个Pyth…

作者头像 李华
网站建设 2026/9/28 2:38:26

RV1106 ISP调试环境搭建:MATLAB仿真与在线调参工具联动

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华