Cherry USB在STM32上的移植实践与优化指南

📅 发布时间:2026/7/5 7:00:21 👁️ 浏览次数:
Cherry USB在STM32上的移植实践与优化指南
1. 为什么选择Cherry USB从零开始的认知如果你正在用STM32做项目需要用到USB功能比如做个U盘、虚拟个串口或者搞个自定义的HID设备那你肯定绕不开USB协议栈。市面上选择不少有ST官方自带的USB库有开源的TinyUSB还有我们今天要聊的主角——Cherry USB。我第一次接触Cherry USB是因为一个客户项目。当时用ST的官方库功能是能实现但代码结构感觉有点“重”想裁剪点不用的功能吧又怕动到核心逻辑。后来在网上看到Cherry USB被它“小而美”的设计吸引了。简单来说Cherry USB是一个用C语言编写的、高度可裁剪的USB设备协议栈。它的核心优势在于模块化和可移植性。整个协议栈被拆分成核心层、设备控制器驱动层和类Class驱动层就像搭积木一样。你需要什么功能比如MSC大容量存储、CDC虚拟串口、HID人机接口就把对应的“积木”加进来不需要的就别加这样生成的代码体积非常紧凑特别适合资源紧张的STM32系列MCU。我实测下来在STM32F103C8T6这种只有64KB Flash的“小钢炮”上只跑一个CDC虚拟串口Cherry USB的固件体积能比用某些全功能库节省好几KB这对于快要爆Flash的项目来说可能就是“救命稻草”。而且它的代码风格很清晰注释也到位对于想深入了解USB底层交互细节的开发者来说是个很好的学习材料。当然它也不是没有门槛最大的挑战就在于“移植”。官方文档虽然提供了指南但对于第一次接触的嵌入式新手那些需要自己实现的底层函数像usb_dc_low_level_init到底该怎么填还是容易让人懵圈。别急接下来我就带你一步步踩平这些坑把Cherry USB稳稳地跑在你的STM32板上。2. 动手之前环境准备与源码获取磨刀不误砍柴工在开始敲代码之前咱们先把“战场”布置好。这里我假设你已经在用Keil MDK或者IAR这样的IDE进行STM32开发了这是最常用的场景。第一步获取Cherry USB源码。最直接的方式是去它的GitHub仓库。你可以直接搜索“CherryUSB GitHub”找到那个叫“cherry-embedded/CherryUSB”的仓库。我建议直接下载最新的master分支的ZIP包或者用git克隆下来这样能确保拿到最新的特性和修复。下载后你会看到一个包含core、class、port等目录的源码结构。别被吓到我们初期只需要关注其中几个关键部分。第二步在你的STM32工程中引入源码。我个人的习惯是在工程目录下新建一个Middlewares/CherryUSB的文件夹然后把下载的源码里device目录下的core和class两个文件夹复制进去。port目录里是一些官方已经做好的移植示例我们可以参考但先不直接复制。接着在你的IDE里把这些源文件.c文件和头文件路径添加进去。记住只添加你计划用到的类。比如你只做U盘功能MSC那就只添加class/msc下的文件这样编译时就不会把HID、CDC的代码也链进来最大化节省空间。第三步准备一个基本的STM32工程框架。确保你的工程已经有正确的芯片型号、时钟配置特别是USB时钟通常是48MHz需要从PLL分频得到、以及GPIO和中断的初始化框架。这些是STM32能正常工作的基础也是后续Cherry USB底层驱动依赖的关键。如果你用的是STM32CubeMX那么生成一个带USB外设的初始化代码会非常方便我们可以基于那个来改。这里有个小提示Cherry USB本身不负责时钟树的配置这部分硬件相关的初始化必须由你在usb_dc_low_level_init函数里自己完成。3. 移植核心实现那四个关键底层函数这是整个移植过程最核心、也是最需要耐心的一步。Cherry USB为了保持硬件平台的独立性把跟具体MCU芯片强相关的操作抽象成了四个函数需要我们根据自己用的STM32型号来“填空”。原始文章里提到了它们但说得比较简略我这里展开讲讲每个函数到底要干嘛以及我踩过的坑。3.1 usb_dc_low_level_init硬件初始化管家这个函数是USB设备控制器DC的底层初始化入口必须在这里完成所有让USB硬件能工作的准备工作。你可以把它想象成给USB模块“通上电、接好线、设置好工作模式”。根据原始文章里的示例和我的经验它通常需要做以下几件事使能相关时钟这是最容易出错的地方。首先USB模块本身比如RCC_APB1Periph_USB的时钟必须使能。其次USB数据线DP/DM对应的GPIO通常是PA11和PA12的时钟和复用功能时钟RCC_APB2Periph_AFIO也要打开。忘记开AFIO时钟会导致GPIO无法切换到USB的复用功能电脑根本识别不到设备。配置USB GPIO将PA11和PA12设置为复用推挽输出模式GPIO_Mode_AF_PP速度可以设为50MHz。这一步和普通外设的GPIO配置类似。配置USB时钟源USB模块需要精确的48MHz时钟。在STM32F1系列中这通常来自PLL时钟经过一个专用的分频器/1.5。所以需要调用RCC_USBCLKConfig(RCC_USBCLKSource_PLLCLK_1Div5)。在其他系列如F4、H7上可能配置方式不同需要查对应芯片的参考手册。配置USB中断USB通信是靠中断驱动的所以必须配置好USB的中断通道比如F1的USB_LP_CAN1_RX0_IRQn设置好优先级并使其能。中断服务函数的名字是固定的我们后面会写。我当初在这里踩过一个坑在低功耗模式下唤醒后USB需要重新初始化。如果只在usb_dc_low_level_init里配置时钟唤醒后时钟可能不对。所以更健壮的做法是在这个函数里判断一下如果是唤醒后的重新初始化可能需要先执行一些复位操作。3.2 usb_dc_low_level_deinit清理现场这个函数是init的反操作用于反初始化USB硬件。在实际项目中你可能用得不多比如在设备需要彻底进入停机模式时为了省电需要关闭USB模块。它的实现通常包括关闭USB时钟、将GPIO恢复为输入模式、禁用USB中断等。原始文章里只是打印了一句话在实际产品中建议根据需求实现真正的反初始化逻辑防止功耗泄漏。3.3 usbd_dwc2_delay_ms提供时间基准Cherry USB内部某些操作比如连接上主机后的复位延时需要一个毫秒级的延时。它没有自己实现而是通过调用这个函数来让你提供。所以你需要在这里对接你工程里已有的延时函数比如HAL_Delay或者你自己写的delay_ms。这里的关键点是这个延时必须是阻塞式的并且精度要求不高几十毫秒的误差通常可以接受。你只需要确保函数名和参数匹配里面调用你自己的延时就行。3.4 usbd_get_dwc2_gccfg_confDWC2控制器配置可留空这个函数的名字看起来有点吓人“DWC2”是DesignWare Core 2的缩写是一种USB IP核。对于STM32系列尤其是F1/F4等使用Synopsys IP核的型号这个函数通常不需要做任何实质性的配置。STM32的USB外设寄存器配置已经由Cherry USB的核心部分完成了。所以像原始文章示例里那样打印一条调试信息或者直接留空都是可以的。但函数必须存在否则链接时会报错。这是为了保持框架的兼容性。4. 连接主机中断与初始化的临门一脚硬件底层函数填好了接下来就要让Cherry USB的核心“动”起来并处理好与主机的通信。这部分涉及到中断和主初始化流程。首先是中断服务函数ISR。USB在数据传输过程中会不断产生各种事件如数据包接收完毕、发送完成、总线复位等这些事件都以中断的形式通知MCU。Cherry USB已经写好了一个通用的中断处理函数USBD_IRQHandler。我们需要做的就是在STM32的中断向量表中找到对应的USB中断入口然后在这个入口函数里调用它。例如在STM32F1上USB低优先级中断的服务函数名是USB_LP_CAN1_RX0_IRQHandler。你只需要在你的工程里通常是在stm32f1xx_it.c文件中定义这个函数并在其中调用USBD_IRQHandler(0)。参数0是总线ID对于单USB口的STM32就是0。然后是主程序中的初始化和启动。在main函数里在你完成了系统时钟、GPIO等基础初始化之后就需要进行Cherry USB的初始化和类设备的初始化。这里有个顺序问题初始化类设备比如你要用MSCU盘功能就需要调用msc_ram_init(0, USB_BASE)。这个函数名字可能因版本略有不同会向Cherry USB核心注册一个MSC类设备实例。第一个参数是总线ID0第二个参数是USB寄存器的基地址对于STM32F103就是USB_BASE通常定义在芯片头文件里。这一步必须在USB核心初始化之前完成因为核心初始化后可能立刻就要处理主机枚举它需要知道设备支持哪些类。初始化并启动USB核心调用usbd_initialize(0, USB_BASE)。这个函数会调用我们之前写的usb_dc_low_level_init来初始化硬件然后设置USB核心寄存器最后使能USB中断。调用之后你的USB设备在硬件上就已经“插上”了主机的端口前提是硬件连接正确。我遇到过一种情况设备枚举失败电脑反复提示“无法识别的USB设备”。排查了半天发现是msc_ram_init和usbd_initialize的顺序反了。Cherry USB核心启动时如果发现没有注册任何类设备它可能无法正确构建描述符回复主机导致枚举失败。所以记住这个顺序先注册功能类再上电启动核心。5. 功能实战以MSCU盘为例的代码详解理论说了这么多咱们看一个实实在在的例子就用原始文章里提到的MSC大容量存储设备也就是模拟U盘功能。我会把代码拆开揉碎了讲让你知道每一行是干嘛的。假设我们要用STM32的内部SRAM模拟一个很小的U盘。首先我们需要定义一个存储区域作为“磁盘”。在文件开头我们可以定义一块内存#define DISK_BLOCK_SIZE 512 // 每个扇区512字节标准U盘格式 #define DISK_BLOCK_COUNT 1024 // 总共1024个扇区模拟一个512KB的U盘 uint8_t msc_disk[DISK_BLOCK_SIZE * DISK_BLOCK_COUNT] {0};然后我们需要实现MSC类驱动要求的几个回调函数。这些函数是Cherry USB的MSC模块在主机进行读写操作时会调用的。最关键的两个是read和write// 当主机要读取磁盘数据时调用 int32_t msc_read_callback(uint8_t lun, uint32_t lba, uint8_t *buffer, uint32_t size) { // lun: 逻辑单元号我们只有一个磁盘就是0 // lba: 逻辑块地址从哪个扇区开始读 // buffer: 数据读取后存放的缓冲区 // size: 要读取的扇区数 if (lba size DISK_BLOCK_COUNT) { return -1; // 地址越界返回错误 } // 从我们模拟的磁盘内存中拷贝数据 memcpy(buffer, msc_disk[lba * DISK_BLOCK_SIZE], size * DISK_BLOCK_SIZE); return 0; // 返回0表示成功 } // 当主机要写入磁盘数据时调用 int32_t msc_write_callback(uint8_t lun, uint32_t lba, uint8_t *buffer, uint32_t size) { if (lba size DISK_BLOCK_COUNT) { return -1; } // 将主机发来的数据写入我们模拟的磁盘内存 memcpy(msc_disk[lba * DISK_BLOCK_SIZE], buffer, size * DISK_BLOCK_SIZE); // 这里可以额外触发一个事件通知应用层有数据更新了 return 0; }除了读写通常还需要实现get_capacity获取磁盘容量、get_status查询磁盘是否就绪等回调。Cherry USB的MSC模块提供了结构体让我们填写这些函数指针。最后在main函数里我们这样初始化int main(void) { // 1. 你的系统初始化时钟、延时、串口等 SystemInit(); delay_init(); USART1_Init(); printf(Cherry USB MSC Demo Start...\r\n); // 2. 初始化MSC类设备并传入我们实现好的回调函数结构体 // 注意这个函数名和参数可能随Cherry USB版本变化请以实际源码为准 extern void msc_init(uint8_t busid, uintptr_t reg_base, msc_callback_t *cb); msc_callback_t my_cb { .read msc_read_callback, .write msc_write_callback, .get_capacity msc_get_capacity_callback, // ... 其他回调 }; msc_init(0, USB_BASE, my_cb); // 先注册功能 // 3. 初始化并启动USB设备控制器 usbd_initialize(0, USB_BASE); // 再启动核心 while (1) { // 主循环可以处理其他任务比如通过串口打印状态 // USB的传输完全由中断服务程序在后台处理 if (usb_device_is_configured()) { // 假设有这么一个查询函数 printf(USB Connected and Configured.\r\n); delay_ms(1000); } } }这样一个最简单的RAM Disk U盘就做好了。编译、下载到STM32用USB线连接到电脑你应该能在“我的电脑”里看到一个可移动磁盘。你可以尝试格式化它注意会清空msc_disk数组、拷贝小文件进去试试。通过这个例子你就能透彻理解Cherry USB“类驱动”的工作模式了框架处理复杂的USB协议你只需要关心“数据从哪里来、到哪里去”这个业务逻辑。6. 避坑指南与高级优化技巧移植成功功能跑通只是第一步。要想在产品中稳定可靠地使用还得避开一些坑并做些优化。下面是我在实际项目中总结的几个要点。避坑指南电源与上拉电阻USB的DPD线上需要一个1.5kΩ的上拉电阻连接到3.3V这个电阻通常在STM32内部集成并通过软件控制连接/断开。在usb_dc_low_level_init中确保正确配置了USB外设这个内部上拉才会生效。如果电脑一直无法识别检查原理图和初始化代码里的上拉电阻配置。描述符配置设备枚举成功与否70%的问题出在描述符设备描述符、配置描述符、字符串描述符等。Cherry USB的类驱动通常会提供默认的描述符模板。你需要仔细核对里面的VID厂商ID、PID产品ID、设备类/子类/协议码、端点大小和数量等信息是否与你的设计一致。特别是端点大小Max Packet Size如果设置小于实际传输的数据包会导致数据传输不完整。中断优先级USB中断的优先级需要合理设置。如果优先级过低可能被其他高优先级中断长时间阻塞导致USB数据传输超时被主机认为设备无响应。如果优先级过高又可能影响系统实时性。一般设置为中等偏上的优先级比较合适。堆栈大小USB中断服务函数以及Cherry USB内部的一些处理可能会用到一定的栈空间。如果你的工程在接入USB后出现莫名其妙的崩溃或数据错误可以尝试适当增大启动文件startup_stm32fxxx.s中分配的堆栈Stack大小。高级优化技巧内存优化Cherry USB本身很省内存但你可以更极致。在usb_config.h或类似的配置头文件中可以关闭不用的调试打印USB_LOG_LEVEL减少字符串描述符或者调整缓冲区数量。对于MSC类可以调整CONFIG_MSC_MAX_LUN逻辑单元数和读写缓存区大小。功耗优化在电池供电设备中当USB断开连接时可以在usb_dc_low_level_deinit中彻底关闭USB外设时钟并将GPIO设为模拟输入模式最省电。在USB挂起Suspend状态时Cherry USB会进入中断你可以在这里让MCU进入低功耗模式如Stop模式当有恢复Resume事件时再唤醒。使用DMA对于高速、大数据量的传输比如MSC读写大文件使用DMA可以极大解放CPU。Cherry USB的某些端口层实现比如针对STM32F4/F7/H7的DWC2驱动已经支持了DMA。你需要根据芯片型号在底层驱动中正确配置USB OTG的DMA通道并在初始化时使能DMA模式。这能显著降低CPU占用率提升整体系统性能。多复合设备Cherry USB很好地支持了USB复合设备Composite Device。比如你可以同时实现一个虚拟串口CDC和一个U盘MSC。这需要在配置描述符中正确组合多个接口并在初始化时分别调用cdc_init和msc_init。注意分配不同的端点地址给不同的接口使用避免冲突。移植和优化是一个不断迭代的过程。我的经验是先用最简配置让基础功能跑起来然后再逐步添加复杂功能、优化性能和资源。每次修改后都进行完整的枚举和功能测试确保稳定性。Cherry USB的社区和文档也在不断更新遇到棘手的问题去翻翻源码和Issue往往能找到灵感。希望这份结合了原始指南和实战经验的梳理能帮你更顺畅地在STM32上驾驭Cherry USB做出稳定好用的USB设备。