ARTICLE DETAIL

资讯详情

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

STM32F103移植CherryUSB实现MSC设备:从底层驱动到U盘功能

STM32F103移植CherryUSB实现MSC设备:从底层驱动到U盘功能 1. 为什么要在 STM32F103 上折腾 CherryUSBSTM32F103 这颗芯片在嵌入式圈子里算是老熟人了Cortex-M3 内核、72MHz 主频、64KB~128KB Flash、20KB SRAM外设资源够用价格也压得住很多最小系统板就是拿它做的。但它的 USB 外设只有一个全速 USB 2.0 Device 控制器没有 OTG也没有高速模式所以能做的事情其实比较有限——做个人机接口设备、做虚拟串口、做 MSC 大容量存储设备基本就是它的主战场。问题在于ST 官方的 USB 库写得太绕了。早期标准外设库里的 USB-FS-Device 那一套中断回调层层嵌套端点配置散落在好几个文件里想加一个新类或者改一个描述符得翻半天代码。HAL 库虽然好一点但 USB Device 那部分依然不够清爽尤其是想做复合设备的时候各种配置描述符拼起来很容易出错。CherryUSB 就是在这个背景下进入视野的。它是一个开源的小型 USB 协议栈特点是代码量小、结构清晰、支持 Device 和 Host 双模式而且对各类 USB Class 的抽象做得比较干净。MSC、CDC、HID、Audio 这些常见类都有现成实现移植的时候主要工作就是对接底层硬件寄存器操作和提供一个可靠的延时函数。这次要做的就是在 STM32F103 上把 CherryUSB 跑起来并且实现 MSC 功能——也就是让 STM32F103 通过 USB 接口被电脑识别成一个 U 盘电脑可以往里面读写文件。这个功能在实际项目里很有用比如做数据记录仪的时候插上电脑就能直接导出数据不用再装驱动或者写上位机软件。适合看这篇内容的人应该是对 STM32 有一定了解、能看懂寄存器操作、知道 USB 基本概念端点、描述符、枚举的嵌入式开发者。如果你之前只用过串口通信对 USB 协议完全没概念建议先补一下 USB 2.0 规范里关于 Device 枚举的那部分内容不然移植过程中遇到问题会比较难排查。2. CherryUSB 的架构设计与移植思路拆解2.1 CherryUSB 的分层结构CherryUSB 的代码结构分得比较清楚从上到下大致是这么几层最上面是Class 层也就是 USB 类的具体实现。MSC、CDC、HID 这些都在这一层每个类有自己的接口函数和状态机。比如 MSC 类就实现了 BOTBulk-Only Transport协议处理 SCSI 命令、读写扇区这些逻辑。中间是Core 层负责 USB 协议的核心状态机。设备枚举、端点管理、控制传输、标准请求处理都在这里。这一层不关心你用的是哪颗芯片只关心 USB 协议本身。最下面是Port 层也就是硬件相关的部分。CherryUSB 把这一层抽象成了几个函数需要移植者自己实现主要包括端点初始化与配置端点数据收发中断处理延时函数这种分层的好处是换芯片的时候只需要改 Port 层上面的 Class 和 Core 完全不用动。对于 STM32F103 来说Port 层要做的就是操作它的 USB 外设寄存器。2.2 为什么选 CherryUSB 而不是官方库我对比过几种方案最后选 CherryUSB 主要基于这几点考虑代码可读性。CherryUSB 的核心代码加起来也就几千行而且注释比较到位遇到问题可以直接跟进去看。ST 官方库的 USB 部分代码量要大得多而且很多地方是自动生成的改起来心里没底。复合设备支持。CherryUSB 对复合设备的支持比较自然配置描述符的拼接逻辑清晰。如果后面想在这个项目基础上加一个 CDC 虚拟串口改动量不会太大。社区活跃度。CherryUSB 在 GitHub 上更新比较频繁issue 回复也及时。遇到问题去翻 issue 或者提新 issue一般都能得到反馈。当然也有代价。CherryUSB 的文档相对简略很多细节需要看代码才能搞清楚。而且它不像 ST 官方库那样有 CubeMX 图形化配置工具所有配置都得手写。对于习惯用 CubeMX 生成代码的人来说初期会有点不适应。2.3 STM32F103 USB 外设的关键特性在动手移植之前得先把 STM32F103 的 USB 外设摸清楚。它的 USB 控制器有这些特点全速设备支持 12Mbps 速率8 个双向端点每个端点可以配置为控制、批量、中断或同步类型专用 512 字节 SRAM用于端点数据缓冲这块 SRAM 是 USB 外设独占的不能当普通内存用缓冲区描述表每个端点的缓冲区地址和长度都记录在这张表里表的位置由BTABLE寄存器指定这里有个容易踩坑的地方STM32F103 的 USB 缓冲区访问机制比较特殊。CPU 不能直接读写那块 512 字节的 SRAM必须通过USB_EP_DR寄存器间接访问。写数据的时候先写USB_EP_DR硬件会自动把数据搬到对应的缓冲区地址读数据的时候反过来。这个机制在移植端点收发函数的时候必须处理正确否则会出现数据错位或者丢包。另外STM32F103 的 USB 中断向量有独立的入口需要把 CherryUSB 的中断处理函数挂上去。中断标志的清除也要注意顺序有些标志必须在特定操作之后清除不然会反复进中断。3. 从零开始的移植实操过程3.1 源码获取与工程结构规划CherryUSB 的源码可以直接从 GitHub 仓库克隆下来。我一般习惯把第三方库放在工程目录下的Middlewares文件夹里和Drivers、User这些目录平级。这样结构比较清晰后面换芯片或者升级库版本的时候也好管理。克隆下来之后主要关注这几个目录core/核心协议栈代码包括设备状态机、端点管理、标准请求处理class/msc/MSC 类的实现port/各种芯片的移植示例可以参考port/stm32f103或者类似的目录common/一些通用工具函数和头文件工程里需要添加的源文件包括core下的所有.c文件、class/msc下的.c文件以及你自己写的 Port 层文件。头文件路径也要相应添加。3.2 底层端点读写的实现细节Port 层最核心的部分就是端点读写函数。STM32F103 的 USB 外设操作起来有点繁琐我先把关键点列出来。端点初始化。每个端点在使用前都要配置USB_EPnR寄存器设置端点类型、地址、状态。对于 MSC 来说需要用到 EP0控制端点、EP1批量输入、EP2批量输出。EP0 的缓冲区要单独处理因为控制传输的数据长度是可变的。缓冲区描述表的设置。STM32F103 的 USB 缓冲区描述表是一块内存区域每个端点占 4 个字节前两个字节是发送缓冲区地址后两个字节是接收缓冲区地址。地址是相对于 USB SRAM 起始位置的偏移量不是绝对地址。这块表的位置通过BTABLE寄存器告诉硬件。我一般这样分配缓冲区端点用途缓冲区偏移长度EP0控制发送0x0064EP0控制接收0x4064EP1批量发送0x8064EP2批量接收0xC064总共用了 256 字节剩下的 256 字节留着后面扩展用。注意每个端点的缓冲区地址要 4 字节对齐这是硬件要求。数据收发函数。写数据的时候先设置USB_EPnR里的发送状态为有效然后往USB_EP_DR寄存器里逐个写字节。硬件会自动把数据搬到对应的缓冲区。读数据的时候反过来先读USB_EP_DR硬件会把缓冲区里的数据搬出来。这里有个细节STM32F103 的 USB 外设对写操作有顺序要求。必须先写USB_EPnR设置好端点状态再写数据寄存器。如果顺序反了数据会丢。我在调试的时候在这上面卡了半天后来用逻辑分析仪抓 USB 信号才定位到问题。3.3 中断处理与事件分发STM32F103 的 USB 中断处理是整个移植过程中最容易出问题的环节。它的中断标志分两类一类是全局中断标志比如复位、挂起、唤醒另一类是端点中断标志每个端点有自己的发送完成、接收完成标志。CherryUSB 的中断处理入口是一个统一的函数需要你在中断服务程序里调用。这个函数会根据当前的标志位判断发生了什么事件然后分发给对应的处理逻辑。关键点在于标志清除的时机。STM32F103 的USB_ISTR寄存器里的标志是只读的要清除必须操作对应的USB_EPnR寄存器。而且有些标志是“读后自动清除”有些需要手动写 0。我在移植的时候遇到过中断反复触发的问题最后发现是端点接收完成的标志没有正确清除。另外STM32F103 的 USB 中断优先级要设置得当。如果系统里还有别的中断比如定时器、串口USB 中断的优先级不能太低否则枚举过程中容易超时。我一般把 USB 中断设成中等优先级比 SysTick 低一点但比普通外设中断高。3.4 MSC 类的对接与磁盘操作实现MSC 类跑起来之后STM32F103 在电脑上会被识别成一个 U 盘。但这个 U 盘背后得有实际的存储介质电脑发过来的读写命令需要被正确处理。CherryUSB 的 MSC 类提供了一组回调函数需要你实现msc_read从存储介质读数据msc_write往存储介质写数据msc_get_capacity返回存储介质的容量信息存储介质可以是 SPI Flash、SD 卡也可以是芯片内部的 Flash。用内部 Flash 的话要注意 STM32F103 的 Flash 擦写寿命有限大概 1 万次左右不适合频繁写入的场景。如果只是做数据导出读操作居多那问题不大。我这次用的是外部 SPI Flash通过 SPI 接口读写。SPI 的驱动用标准库或者 HAL 库都行关键是读写速度要跟得上 USB 的传输速率。全速 USB 的批量传输理论速率是 12Mbps实际能到 1MB/s 左右。SPI Flash 如果跑在 18MHz读速度大概能到 2MB/s够用了。MSC 的 SCSI 命令处理里读写命令会指定起始扇区和扇区数量。一个扇区是 512 字节所以msc_read和msc_write里要按扇区地址去换算 Flash 的实际地址。这里要注意 Flash 的擦除粒度SPI Flash 一般是 4KB 一个扇区写之前要先擦除。如果电脑发过来的写命令跨了扇区边界处理起来要小心。4. 调试过程中踩过的坑与排查方法4.1 枚举失败电脑识别不到设备这是最常见的问题。插上 USB 线之后电脑没有任何反应或者提示“无法识别的 USB 设备”。排查思路是这样的先看硬件。USB 的 D 和 D- 线有没有接反上拉电阻有没有焊。STM32F103 做全速设备的时候D 线上需要一个 1.5k 的上拉电阻有些最小系统板已经集成了有些没有。如果没有这个上拉电阻电脑根本检测不到设备插入。再看时钟。STM32F103 的 USB 外设需要 48MHz 时钟这个时钟是从 PLL 分频来的。系统时钟 72MHz 的时候USB 预分频要设置成 1.5也就是 72 / 1.5 48MHz。如果这个分频系数设错了USB 外设根本不会工作。然后看中断。在 USB 中断服务程序里加个计数器看看插上 USB 线之后中断有没有触发。如果没有触发说明硬件连接或者时钟配置有问题。如果有触发但枚举失败那就是协议栈或者描述符的问题。最后看描述符。用 USB 抓包工具比如 Wireshark 配合 USBPcap抓一下枚举过程看看电脑发了什么请求设备回了什么数据。如果设备返回的描述符格式不对电脑会直接放弃枚举。4.2 数据传输不稳定读写大文件时出错枚举成功之后小文件读写正常但一读写大文件就出错或者速度特别慢。这个问题通常出在端点缓冲区的管理上。STM32F103 的 USB 缓冲区只有 512 字节每个端点分到的缓冲区大小有限。如果一次传输的数据量超过了缓冲区大小就需要分包处理。CherryUSB 的 Core 层会处理分包逻辑但 Port 层的端点读写函数必须正确响应“发送完成”和“接收完成”中断及时切换缓冲区状态。我遇到过一次问题是批量输出端点的接收完成中断没有及时处理导致电脑发过来的数据被覆盖。后来在中断处理里加了一个标志位收到数据后先拷贝到用户缓冲区再重新使能端点接收问题就解决了。还有一个可能是 SPI Flash 的读写速度跟不上。如果msc_read函数里阻塞时间太长USB 主机会认为设备超时然后重试或者报错。解决办法是把 Flash 读写做成非阻塞的或者用 DMA 搬运数据减少 CPU 占用。4.3 常见问题速查表现象可能原因排查方法电脑完全没反应D 上拉电阻缺失检查硬件测量 D 线电压提示“无法识别的 USB 设备”描述符格式错误用抓包工具看枚举过程枚举成功但盘符不出现MSC 回调函数未实现检查msc_read、msc_write是否返回正确读写小文件正常大文件出错端点缓冲区溢出检查分包逻辑和中断处理传输速度特别慢SPI 时钟太低或阻塞太久提高 SPI 时钟改用 DMA插拔几次之后死机中断标志未清除检查USB_EPnR的清除操作4.4 几个实用的调试技巧用 GPIO 做状态指示。在枚举成功、读写开始、读写结束这几个关键节点翻转一个 GPIO用示波器或者逻辑分析仪看波形能很直观地判断程序执行到了哪一步。打印调试信息。通过串口输出 CherryUSB 的日志能看到枚举过程中的状态变化。CherryUSB 有日志开关打开之后会输出不少有用信息。但要注意串口打印不能太频繁否则会影响 USB 的实时性。分段测试。不要一上来就测 MSC 读写先确保枚举成功再测简单的控制传输最后再测批量传输。每一步都确认没问题了再往下走这样出问题的时候容易定位。对比官方示例。CherryUSB 的仓库里有 STM32F103 的移植示例虽然可能和你的硬件不完全一样但 Port 层的实现思路是可以参考的。遇到问题的时候对比一下自己的代码和示例的差异往往能找到线索。5. 性能优化与后续扩展方向5.1 提升 MSC 读写速度的几个手段MSC 功能跑通之后如果觉得速度不够理想可以从这几个方面优化SPI 时钟提上去。STM32F103 的 SPI 最高能跑到 36MHz但实际能跑多快取决于 Flash 芯片的支持。我用的那颗 Flash 标称支持 50MHz实际跑 36MHz 没问题。SPI 时钟提高之后读写速度能翻倍。用 DMA 搬运数据。SPI 读写用 DMA 的话CPU 只需要配置好 DMA 通道数据搬运由硬件完成。这样 CPU 可以腾出来处理 USB 中断整体吞吐量会好很多。STM32F103 的 SPI1 有对应的 DMA 通道配置起来不算复杂。优化端点缓冲区分配。如果 MSC 只用一对批量端点可以把缓冲区设大一点比如 128 字节或者 256 字节。但要注意总缓冲区不能超过 512 字节而且 EP0 必须留够 64 字节。减少不必要的拷贝。数据从 USB 缓冲区到用户缓冲区再到 SPI 发送缓冲区中间如果拷贝次数太多会浪费不少时间。理想情况下USB 收到的数据直接通过 DMA 发给 SPI中间不经过 CPU 拷贝。5.2 从 MSC 扩展到复合设备MSC 跑通之后如果想再加一个 CDC 虚拟串口做成复合设备CherryUSB 是支持的。主要改动在配置描述符的拼接上需要把 MSC 和 CDC 的接口描述符、端点描述符按顺序排列好然后修改bNumInterfaces和bNumEndpoints这些字段。端点分配也要重新规划。MSC 用了 EP1 和 EP2CDC 一般需要三个端点控制、数据输入、数据输出STM32F103 有 8 个端点够用。但缓冲区分配要重新算一下确保不超过 512 字节。复合设备的枚举过程比单设备要复杂一些电脑会先读配置描述符的总长度然后一次性把整个配置描述符读走。所以配置描述符的拼接必须一次成型不能分多次返回。5.3 在 FreeRTOS 环境下使用 CherryUSB如果项目里用了 FreeRTOSCherryUSB 也可以跑。需要注意的是CherryUSB 的中断处理函数是在中断上下文里执行的不能调用 FreeRTOS 的阻塞式 API。如果需要在中断里通知任务可以用信号量或者任务通知。另外MSC 的读写回调函数如果耗时较长建议放到单独的任务里执行不要在中断里直接做 Flash 读写。可以用一个队列把读写请求传给任务任务处理完之后再通知 USB 中断继续。FreeRTOS 的 SysTick 中断优先级一般设得比较低USB 中断优先级要比它高否则 USB 枚举可能会超时。但也不能设成最高不然会影响系统的实时性。我一般把 USB 中断设成比 SysTick 高一个等级实测下来比较稳。5.4 一些个人经验总结CherryUSB 在 STM32F103 上的移植难点主要集中在 Port 层的端点读写和中断处理上。这两部分搞定之后上面的 Class 和 Core 基本不用动。调试的时候USB 抓包工具是必备的。没有抓包工具枚举失败的时候基本靠猜效率很低。Wireshark 配合 USBPcap 就能抓 USB 数据虽然不如专业的 USB 分析仪方便但胜在免费。还有一点STM32F103 的 USB 外设对时序要求比较严格中断处理函数里不要做太耗时的操作。如果确实需要处理大量数据用 DMA 或者把数据搬到任务里处理。最后CherryUSB 的版本更新比较快移植的时候最好锁定一个稳定的版本不要直接用 master 分支。等当前版本跑稳了再考虑升级。
返回列表