STM32片内FLASH模拟U盘:MSC协议、FatFs与FLASH驱动实战

发布时间:2026/7/29 5:30:53

STM32片内FLASH模拟U盘:MSC协议、FatFs与FLASH驱动实战 1. 项目概述为什么要在STM32上实现U盘功能几年前我在一个工业数据采集器的项目上遇到了一个头疼的问题设备在现场运行每隔一段时间就会产生几十兆的日志文件维护人员需要带着笔记本电脑用串口或者以太网去连接设备再用上位机软件把数据导出来。整个过程繁琐不说现场环境复杂电脑不一定好带网络也不一定稳定。当时我就在想要是这个设备能直接“变”成一个U盘插上电脑就能像访问普通文件夹一样拷贝数据那该多方便。这就是我们今天要聊的核心基于STM32的片内FLASH实现一个MSCMass Storage Class大容量存储设备类的USB设备俗称“制作一个小U盘”。听起来很高大上其实拆解开来就是让我们的STM32芯片通过USB接口把自己内部的一块存储区域FLASH模拟成电脑操作系统能识别的可移动磁盘。你可能会问为什么不直接用SD卡或者外挂一个SPI FLASH芯片来做U盘当然可以而且方案更成熟。但使用片内FLASH有几个独特的优势首先是成本与集成度对于存储需求不大的应用比如几KB到几百KB的配置参数、小批量日志、字库等省去一颗外置芯片和对应的PCB空间能显著降低BOM成本和板子尺寸。其次是可靠性片内FLASH与MCU同寿命焊接牢固没有连接器接触不良的风险适合振动、高低温等恶劣环境。最后是学习价值这个项目能让你透彻理解USB MSC协议、FAT文件系统以及MCU内部FLASH的读写管理是嵌入式开发中一次非常综合的实战演练。简单来说这个项目适合两类朋友一是产品开发者需要为设备添加一个极其简便的数据交换接口二是嵌入式学习者希望深入掌握USB和文件系统这两大核心技能。接下来我会带你从原理到代码一步步实现它。2. 核心方案设计与技术栈选型要实现“STM32变U盘”我们需要打通三条关键链路物理存储介质FLASH的驱动、逻辑文件系统FAT的支撑、以及与主机通信的协议USB MSC。这三者环环相扣缺一不可。2.1 技术栈拆解MSC FatFs FLASH Driver我们的方案核心由三个部分组成USB MSC大容量存储类这是USB协议中专门为硬盘、U盘等设备定义的一个标准类。STM32的USB外设如USB FS-全速在配置为Device模式后我们可以编写代码实现MSC类所要求的接口描述符和请求处理。最关键的是MSC类定义了一套基于SCSI透明命令集如INQUIRY, READ CAPACITY, READ/WRITE的通信方式。电脑USB主机会发送这些命令来询问设备信息、读写扇区我们的STM32需要正确响应这些命令。FatFs文件系统模块FatFs是一个为小型嵌入式系统设计的通用FAT文件系统模块。它独立于底层存储介质和硬件平台我们只需要为它提供几个基础的磁盘I/O接口如disk_read,disk_write。FatFs负责解析FAT表、目录项管理文件和文件夹的创建、读写、删除等逻辑。这样我们就不需要自己去处理复杂的FAT32/exFAT结构大大降低了开发难度。片内FLASH驱动与抽象层这是连接FatFs和物理硬件的桥梁。STM32的片内FLASH通常按页Page或扇区Sector组织读写有特殊要求如写前需擦除只能按字/半字编程。我们需要编写驱动实现对指定地址的擦除和编程。更重要的是我们需要建立一个虚拟的“磁盘”模型将FLASH的物理空间划分成连续的、大小固定的逻辑扇区例如512字节并提供对应的读写函数给FatFs调用。为什么选择这个组合这是一个在嵌入式圈内经过无数次验证的“黄金组合”。FatFs模块成熟、稳定、资源占用可控STM32的USB库和HAL库对MSC有较好的支持而片内FLASH的驱动是标准操作。三者通过清晰的接口耦合使得整个架构模块化调试和维护都非常方便。市面上几乎所有的商业U盘主控和读卡器方案其软件架构思想也与此类似。2.2 硬件资源评估与规划在动手写代码前我们必须先算一笔“账”我们的STM32片内FLASH到底能提供多大的“U盘”空间又该如何规划这片空间以常见的STM32F103C8T6为例它拥有64KB的片内FLASH。但请注意FLASH的起始部分通常存放着我们的程序代码Code。我们不能让“U盘”的数据区覆盖掉程序本身否则设备一运行就崩溃了。空间规划实战假设我们的程序编译后大小为40KB0xA000字节。我们可以从某个安全的地址之后开始划分U盘空间。STM32 FLASH通常以1KB或2KB为一页。为了对齐和管理方便我们选择从0x0800A000这个地址开始作为“U盘”的起始地址。那么可用的空间就是0x0801000064KB终点 -0x0800A000 24KB。这24KB就是我们的“U盘”总容量。是不是觉得很小没错但这正是片内FLASH方案的典型应用场景——存储小数据。对于存储配置参数、少量日志、证书文件等完全足够。扇区大小设定FatFs和USB MSC通常默认使用512字节的扇区。因此我们需要将这24KB的物理空间在逻辑上划分为48个扇区24KB / 512B 48。后续所有的读写操作FatFs和MSC驱动都会以“第N个扇区”为单位来请求数据。注意这个规划必须在项目初期就确定下来并写入代码中作为常量。一旦产品量产这个布局就绝对不能更改否则之前设备上存储的数据将无法被识别。3. 底层基石片内FLASH的驱动与磁盘抽象这是整个项目最底层、也是最关键的一环。如果FLASH读写不可靠上层的一切都是空中楼阁。3.1 STM32片内FLASH操作精要与RAM随存随取不同FLASH存储器有两大特性写前必须擦除且擦除的最小单位是一个扇区或页写入时只能将bit从1变为0而不能从0变回1擦除操作会将整个扇区所有bit置1。操作流程标准化解锁UnlockSTM32的FLASH控制器有写保护防止程序跑飞误擦写。在修改FLASH前需要向特定的控制寄存器写入密钥序列。HAL_FLASH_Unlock(); // 使用HAL库解锁擦除Erase擦除一个或多个扇区。需要填充一个FLASH_EraseInitTypeDef结构体指定擦除类型和扇区编号。FLASH_EraseInitTypeDef EraseInitStruct; uint32_t SectorError 0; EraseInitStruct.TypeErase FLASH_TYPEERASE_PAGES; // 按页擦除 EraseInitStruct.PageAddress FLASH_START_ADDR; // 要擦除的起始地址 EraseInitStruct.NbPages TOTAL_PAGES; // 要擦除的页数 HAL_FLASHEx_Erase(EraseInitStruct, SectorError);编程Program以字32位、半字16位或字节8位取决于型号为单位写入数据。必须确保目标地址已经擦除全为0xFF。HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, address, data);上锁Lock操作完成后重新上锁确保安全。HAL_FLASH_Lock();实操心得中断与耗时擦除和编程操作是阻塞式的并且耗时较长擦除一页可能需要几十ms。在此期间必须禁止所有中断或者确保你的系统能容忍这段时间的延迟。通常HAL库的擦写函数内部会处理中断开关__disable_irq但自己要清楚这个影响。寿命问题STM32的片内FLASH典型擦写次数是1万次。频繁地擦写同一个扇区会使其提前失效。切忌在循环或高频任务中直接擦写FLASH对于需要频繁更新的数据如日志应采用“磨损均衡”策略轮流使用不同扇区。3.2 构建磁盘抽象层diskio.cFatFs并不关心底层是SD卡、SPI FLASH还是片内FLASH它只通过一个名为diskio.c的文件中定义的几个函数来交互。我们的核心工作就是实现这个文件。diskio.c需要实现以下关键函数disk_status获取磁盘状态是否初始化、是否写保护等。disk_initialize初始化磁盘驱动对于我们来说就是检查FLASH相关硬件是否就绪。disk_read从指定扇区读取数据到缓冲区。这是最常用的操作电脑浏览U盘文件时会频繁调用此函数。disk_write将缓冲区数据写入指定扇区。这是最需要小心的操作因为它内部必须包含擦除流程。disk_ioctl提供控制命令如获取扇区数量GET_SECTOR_COUNT、获取扇区大小GET_SECTOR_SIZE等。这些信息是FatFs和USB MSC初始化时必须的。disk_write函数的实现要点这是难度最高的部分必须正确处理擦除。因为FLASH擦除以页为单位而FatFs写入以扇区512B为单位。如果一页是2KB那么它包含4个扇区。当FatFs请求写入这4个扇区中的任何一个时我们不能直接擦除整页否则会丢失其他3个扇区的数据。解决方案扇区缓冲管理我们需要在RAM中开辟一个页大小的缓冲区如2KB。写入流程如下当收到写入某个扇区的请求时先判断该扇区所属的物理页。将该物理页当前的全部数据4个扇区读取到RAM缓冲区。将FatFs要写入的新数据更新到缓冲区中对应的位置。擦除整个物理页。将整个RAM缓冲区包含已更新的扇区和其他未变的扇区的数据重新编程回该物理页。这个过程被称为“读-改-擦-写”虽然效率不高但对于小容量、低频写的场景是可靠且必要的。踩坑记录我曾因为忘记在disk_write中实现这个缓冲机制导致U盘每次写入后其他文件全部损坏。调试时发现电脑写入文件A后文件B的内容变成了乱码。根本原因就是直接擦除了一整页把其他扇区也清除了。这个坑务必避开。4. 中间层FatFs文件系统的集成与配置FatFs模块的集成相对标准化但配置选项决定了系统的能力和资源消耗。4.1 FatFs模块裁剪与配置ffconf.hffconf.h是FatFs的配置文件里面有大量的宏定义。对于资源紧张的STM32合理的裁剪至关重要。关键配置项解析_FS_READONLY设置为0因为我们肯定需要写功能。_FS_MINIMIZE这个值可以设为1或2以裁减掉不用的API如f_opendir,f_stat等节省代码空间。_USE_STRFUNC如果不需要f_puts,f_gets等字符串操作可以设为0。_USE_LFN长文件名支持。这是一个“内存杀手”。如果设为0只支持经典的8.3格式文件名如LOGFILE.TXT。如果设为1或2则需要额外的缓冲区会消耗较多RAM。对于小U盘我建议暂时关闭长文件名支持先让系统跑起来。_VOLUMES卷的数量我们只有1个“U盘”设为1即可。_SECTOR_SIZE必须设置为512与我们的磁盘抽象层和USB MSC的设定一致。_MAX_SS和_MIN_SS都设为512表示我们只支持512字节扇区。集成步骤从FatFs官网下载源码将ff.c,ff.h,diskio.c,diskio.h,ffconf.h等文件加入工程。根据上述说明修改ffconf.h。实现好我们上一节讲的diskio.c。在应用程序中包含ff.h然后调用f_mount函数来注册我们的磁盘。FATFS fs; // 文件系统对象 FRESULT res; res f_mount(fs, 0:, 1); // 将磁盘0挂载到路径“0:” if (res ! FR_OK) { // 挂载失败可能是FLASH没有格式化或损坏 }4.2 格式化“磁盘”与创建文件第一次使用这片FLASH空间时它里面是空的没有FAT文件系统结构。因此我们需要先对其进行格式化。格式化操作格式化不能在STM32上电后自动进行因为那会清空所有数据。通常有两种方式通过应用程序触发在代码中设置一个“格式化标志”比如检测某个GPIO引脚电平首次烧录程序后触发一次格式化流程。if (need_format) { uint8_t work[_MAX_SS]; // 格式化需要的缓冲区 res f_mkfs(0:, FM_FAT32, 0, work, sizeof(work)); // 格式化为FAT32 }使用PC端工具预先格式化在烧录程序后通过USB连接电脑。虽然此时电脑可能因无文件系统而报错但我们可以借助一些底层扇区读写工具或自己写个简单的USB通信程序将标准的FAT32引导扇区、FAT表等数据写入FLASH的起始扇区。这种方式更专业但门槛较高。创建测试文件格式化成功后我们就可以像在PC上一样操作文件了。FIL fil; UINT bw; // 创建并打开一个文件 res f_open(fil, 0:/test.txt, FA_CREATE_ALWAYS | FA_WRITE); if (res FR_OK) { // 写入字符串 f_write(fil, Hello, STM32 U盘!\r\n, 20, bw); // 关闭文件 f_close(fil); }至此只要diskio.c工作正常文件操作就应该能成功。你可以通过调试器读取FLASH对应地址的内容验证数据是否被正确写入。5. 顶层协议USB MSC设备类的实现这是让电脑识别我们的STM32为U盘的最后一步。我们需要配置STM32的USB外设工作在Device模式并初始化为MSC类。5.1 USB MSC描述符与端点配置USB设备通过一系列描述符来告诉主机“我是什么”。对于MSC设备关键描述符包括设备描述符Device Descriptor指明这是一个USB设备指定厂商IDVID、产品IDPID等。你可以使用公开的测试PID/VID或者申请自己的。配置描述符Configuration Descriptor包含接口描述符和端点描述符。接口描述符Interface Descriptor指明这个接口属于MSC类Class Code: 0x08、SCSI透明命令集子类SubClass: 0x06、以及Bulk-Only传输协议Protocol: 0x50。端点描述符Endpoint DescriptorMSC类需要两个Bulk端点一个IN设备到主机一个OUT主机到设备。通常端点1作为Bulk IN端点2作为Bulk OUT。使用STM32CubeMX配置强烈推荐在Connectivity中使能USB (FS)模式选择Device_Only。在Middleware and Software Packs中选择USB_DEVICEClass选择Mass Storage Class (MSC)。在Project Manager的Code Generator里选择“为外设生成独立的.c/.h文件”这样USB的代码会单独放在USB_DEVICE文件夹里结构清晰。生成代码。CubeMX会自动生成所有USB描述符和MSC类框架代码。5.2 SCSI命令处理与BOT协议电脑通过Bulk-Only Transport (BOT)协议与MSC设备通信。其核心是CBWCommand Block Wrapper和CSWCommand Status Wrapper结构体。通信流程主机发送一个31字节的CBW里面包含了具体的SCSI命令如READ(10)以及数据传输长度和方向。设备解析CBW。如果是读请求如READ(10)设备就通过Bulk IN端点发送相应长度的数据如果是写请求如WRITE(10)设备就从Bulk OUT端点接收数据。数据传输完毕后设备发送一个13字节的CSW给主机报告命令执行状态成功、失败等。我们需要实现的核心回调函数在生成的代码中我们需要在usbd_storage_if.c文件中实现以下函数STORAGE_Init初始化底层存储其实就是调用我们disk_initialize。STORAGE_GetCapacity返回磁盘的扇区总数和扇区大小。这里返回的值必须与diskio.c中disk_ioctl函数返回的完全一致否则电脑会识别出错误的容量。STORAGE_IsReady报告磁盘是否就绪。STORAGE_IsWriteProtected报告磁盘是否写保护。STORAGE_Read处理读扇区请求。直接调用disk_read。STORAGE_Write处理写扇区请求。直接调用disk_write。关键连接点STORAGE_Read和STORAGE_Write的参数是逻辑块地址LBA和扇区计数。这正是FatFs和MSC的通用语言。我们的disk_read/write函数接收的也是LBA地址。因此这个对接非常直接。重要提示STORAGE_GetCapacity返回的容量信息是电脑识别U盘大小的唯一依据。务必确保计算准确总扇区数 * 扇区大小512。例如我们有48个扇区总容量就是48 * 512 24576字节电脑会显示为“24.0 KB”。如果这里算错会导致电脑无法识别或识别容量错误。6. 系统整合与调试实战当三层架构FLASH驱动、FatFs、USB MSC都准备好后我们需要将它们整合起来并处理一些系统级的问题。6.1 主程序流程与任务调度一个典型的程序流程如下int main(void) { // 1. HAL初始化系统时钟配置 HAL_Init(); SystemClock_Config(); // 2. 初始化底层硬件GPIO, USB等 MX_GPIO_Init(); MX_USB_DEVICE_Init(); // 这会初始化USB并启动MSC设备 // 3. 初始化磁盘FLASH和文件系统 disk_initialize(0); f_mount(fs, 0:, 0); // 先尝试挂载不强制 // 4. 检查是否需要格式化例如首次运行标志位 if (is_first_time()) { format_disk(); create_default_files(); // 可创建一些默认文件或目录 } // 5. 主循环 while (1) { // USB的轮询处理通常由CubeMX生成的代码在中断中完成主循环可以处理其他任务 // 例如监测按键触发文件操作 if (user_button_pressed()) { log_data_to_file(); } HAL_Delay(100); } }中断处理USB通信严重依赖中断。CubeMX生成的代码已经为我们配置好了USB全局中断USB_LP_CAN1_RX0_IRQn和端点中断。我们切勿在中断服务程序中进行复杂的文件操作或FLASH擦写这会导致中断阻塞USB通信超时进而被电脑断开。所有耗时的操作如响应STORAGE_Write都应放在主循环或低优先级任务中。6.2 电脑端识别与文件操作将STM32设备通过USB线连接到电脑。如果一切顺利你应该能听到电脑发现新硬件的提示音并在“设备管理器”的“磁盘驱动器”下看到一个新的设备。稍等片刻它就会出现在“我的电脑”里作为一个可移动磁盘。首次使用的可能情况最佳情况电脑直接识别出U盘并显示正确容量可以正常打开、读写文件。提示格式化电脑识别到了磁盘但认为其文件系统损坏或未格式化。这说明我们的FAT表等元数据可能不对或者STORAGE_GetCapacity返回的信息有误。此时千万不要在电脑上点格式化这会导致电脑按照它的理解可能是GB级去格式化我们的FLASH发送巨大的擦写命令很可能导致程序跑飞或FLASH损坏。正确的做法是回到STM32代码中检查格式化流程和容量报告函数。无法识别设备管理器里可能显示为“未知USB设备”或带感叹号。这说明USB枚举失败问题出在USB描述符、端点配置或底层USB驱动上。需要检查CubeMX配置并用USB分析仪如Bus Hound抓包分析。安全移除硬件和普通U盘一样在拔掉USB线之前最好在电脑上点击“安全弹出硬件”。这个操作会让电脑发送一个SCSI STOP命令我们可以在这个命令的回调函数里确保所有缓存数据都已写入FLASH即调用f_sync实现安全断电。7. 常见问题排查与性能优化即使按照步骤操作也难免会遇到问题。这里汇总一些典型的坑和解决办法。7.1 问题排查速查表现象可能原因排查思路电脑完全不识别USB设备1. USB硬件连接问题DP/DM接反、没供电2. USB时钟配置错误必须是48MHz3. 描述符错误1. 检查硬件连接和电压。2. 用示波器看USB数据线是否有信号。3. 使用STM32CubeProgrammer的USB DFU模式测试USB PHY是否正常。电脑提示“无法识别的USB设备”1. 端点配置错误2. 设备描述符/配置描述符响应错误1. 核对usbd_conf.c中的端点Buffer大小和地址。2. 用Bus Hound等工具抓取枚举过程的数据包。电脑提示“需要格式化”1. FLASH未格式化或FAT表损坏2.STORAGE_GetCapacity返回值错误3. 扇区大小不是512字节1. 确认已成功执行f_mkfs。2. 调试STORAGE_GetCapacity看其返回的块数量和块大小。3. 确保_SECTOR_SIZE、disk_ioctl、STORAGE_GetCapacity三处的扇区大小定义一致。可以识别但写入文件失败/文件损坏1.disk_write函数实现错误未做读-改-擦-写2. FLASH擦写期间发生中断导致数据错乱3. USB传输缓冲区溢出1. 重点检查disk_write逻辑特别是缓冲区管理。2. 在FLASH擦写操作前后加临界区保护__disable_irq/__enable_irq。3. 检查USB端点Buffer大小是否大于等于512字节。读写速度极慢1. FLASH擦写时间过长2. 文件系统碎片化在小容量下不明显3. 每次读写都重新挂载文件系统1. 这是片内FLASH的物理限制无法根本解决。2. 避免频繁地打开关闭文件可以长时间保持文件打开状态进行读写。3. 确保f_mount只在初始化时调用一次。7.2 稳定性与性能优化技巧写保护与掉电保护在disk_write函数开始时可以检查一个全局的“写使能”标志。只有系统状态稳定如电压正常时才允许写入。在检测到即将掉电通过电压监控电路中断时立即置位写保护标志并尽快完成当前扇区的写入操作防止文件系统结构被破坏。减少擦写次数对于需要频繁记录的数据如运行日志不要每次写都立即保存到FLASH。可以在RAM中开辟一个环形缓冲区积累到一定量如攒够一个扇区再一次性写入FLASH。这能大幅提升FLASH寿命。使用RAM Disk进行测试在开发初期为了排除FLASH驱动的不稳定性可以先用一片RAM区域模拟磁盘。只需修改diskio.c中的读写函数使其指向一个全局数组。这样可以快速验证FatFs和USB MSC层的逻辑是否正确极大提高调试效率。添加状态指示在板上增加一个LED在STORAGE_Read/Write函数被调用时闪烁。这样你可以直观地看到电脑在访问U盘对于判断设备是否被正确识别非常有帮助。实现STM32片内FLASH U盘的过程是一次对嵌入式系统存储、文件系统和USB外设的深度整合。它没有太多高深的算法但对细节的把握要求极高。每一个环节——从FLASH的页擦除到FatFs的扇区映射再到USB的SCSI命令解析——都必须严丝合缝。当你第一次在电脑上看到自己手写的代码所创造的“U盘”并成功拷贝进一个文件时那种成就感是对所有调试过程中抓耳挠腮的最好回报。这个项目获得的最大经验就是模块化测试和重视数据手册。先把FLASH读写调通再挂上FatFs测试文件操作最后整合USB每一步都确保稳固最终的系统才会可靠。

相关新闻