用STM32F103C8T6打造极简U盘:内部Flash的创意应用指南

你是否曾经遇到过这样的场景:手头急需一个U盘传输几兆的小文件,却发现身边没有可用的存储设备?或者作为一名电子爱好者,总是对那些闲置的开发板感到可惜?今天,我们将探索如何将一块常见的STM32F103C8T6开发板变身为实用的迷你U盘,充分利用其内部Flash的后64K空间。这不仅是一个有趣的DIY项目,更能让你深入理解USB Mass Storage Class(MSC)协议和FAT文件系统的底层实现。

1. 项目准备与环境搭建

在开始之前,我们需要明确几个关键点。STM32F103C8T6虽然标称只有64KB Flash,但实际上大多数芯片都有128KB的物理空间。这个"隐藏"的64KB空间将成为我们自制U盘的存储介质。相比传统U盘方案,这种设计有三大优势:

  • 零成本 :利用现有开发板资源,无需额外购买存储芯片
  • 低功耗 :内部Flash操作比外部SPI Flash更省电
  • 教学价值 :完整实现USB MSC协议栈和FAT文件系统

硬件准备清单:

  • STM32F103C8T6开发板(蓝桥杯板或最小系统板)
  • USB数据线(带D+/D-信号线)
  • ST-Link调试器(可选,用于固件烧录)

软件工具链:

  1. STM32CubeMX(最新版本)
  2. Keil MDK或STM32CubeIDE
  3. USB驱动(通常系统自带)

提示:确保开发板的USB接口电路完整,部分廉价开发板可能省略了USB所需的22Ω阻抗匹配电阻。

2. CubeMX工程配置详解

启动STM32CubeMX,创建一个新工程并选择STM32F103C8T6芯片。我们将逐步配置各个模块的关键参数。

2.1 时钟树配置

时钟是USB稳定工作的基础。按照以下步骤配置:

  1. 在"Pinout & Configuration"选项卡中,设置HSE为外部晶振(通常8MHz)
  2. 切换到"Clock Configuration"标签页
  3. 设置PLL倍频为9,使系统时钟达到72MHz
  4. 确保USB时钟为48MHz(由PLL经过1.5分频得到)

关键参数验证表:

参数名称 期望值 检查方法
SYSCLK 72MHz Clock Configuration页
HCLK 72MHz 同上
PCLK1 36MHz 同上
PCLK2 72MHz 同上
USB Clock 48MHz 必须精确

2.2 USB外设配置

在Connectivity部分启用USB设备,并做如下设置:

  1. 模式选择"Device Only"
  2. 在"Middleware"部分启用USB_DEVICE
  3. 选择Class For FS IP为"MSC"(Mass Storage Class)
  4. 配置MSC_MEDIA_PACKET为1024(与后续FATFS设置保持一致)

2.3 FATFS中间件配置

在Middleware部分添加FATFS组件,关键参数设置:

#define MAX_SS 1024   // 扇区大小
#define MIN_SS 1024   // 必须与MAX_SS相同
#define USE_LFN 1     // 启用长文件名支持

注意:SS(Sector Size)值必须与MSC_MEDIA_PACKET完全一致,否则会导致文件系统错误。

3. 关键代码修改与实现

生成基础工程后,我们需要手动修改几个关键文件以实现Flash存储功能。

3.1 Flash存储区域定义

首先在 usbd_storage_if.c 文件中添加以下宏定义:

#define FLASH_SIZE        128    // 单位KB,实际使用后64KB
#define FLASH_PAGE_SIZE   0x400  // 1KB页大小
#define FLASH_PAGE_NBR    64     // 使用64页(64KB)
#define FLASH_START_ADDR  (0x08000000 + ((FLASH_SIZE - FLASH_PAGE_NBR) * 1024))

这段代码定义了从Flash末尾开始的64KB空间作为我们的"U盘"存储区。

3.2 USB MSC接口实现

修改 usbd_storage_if.c 中的三个核心函数:

  1. 容量获取函数 - 告诉主机我们的"磁盘"有多大:
int8_t STORAGE_GetCapacity_FS(uint8_t lun, uint32_t *block_num, uint16_t *block_size) {
    *block_num = FLASH_PAGE_NBR;    // 总块数
    *block_size = FLASH_PAGE_SIZE;  // 每块大小
    return USBD_OK;
}
  1. 读取函数 - 实现从Flash读取数据:
int8_t STORAGE_Read_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) {
    if(lun == 0) {
        memcpy(buf, (uint8_t *)(FLASH_START_ADDR + blk_addr*FLASH_PAGE_SIZE), 
               blk_len*FLASH_PAGE_SIZE);
        return USBD_OK;
    }
    return USBD_FAIL;
}
  1. 写入函数 - 实现向Flash写入数据(包含擦除操作):
int8_t STORAGE_Write_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len) {
    FLASH_EraseInitTypeDef erase;
    uint32_t PageError = 0;
    
    HAL_FLASH_Unlock();
    
    // 先擦除目标扇区
    erase.TypeErase = FLASH_TYPEERASE_PAGES;
    erase.PageAddress = FLASH_START_ADDR + blk_addr*FLASH_PAGE_SIZE;
    erase.NbPages = blk_len;
    HAL_FLASHEx_Erase(&erase, &PageError);
    
    // 以字(32bit)为单位写入
    for(uint16_t i=0; i<blk_len*FLASH_PAGE_SIZE; i+=4) {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
                         FLASH_START_ADDR + blk_addr*FLASH_PAGE_SIZE + i,
                         *(uint32_t *)(&buf[i]));
    }
    
    HAL_FLASH_Lock();
    return USBD_OK;
}

3.3 FATFS磁盘接口适配

修改 user_diskio.c 文件,实现FATFS所需的底层驱动:

  1. 磁盘状态检测
DSTATUS USER_status(BYTE pdrv) {
    static DSTATUS stat = STA_NOINIT;
    stat &= ~STA_NOINIT;  // 标记磁盘已初始化
    return stat;
}
  1. 磁盘读取
DRESULT USER_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) {
    uint8_t *src = (uint8_t *)(FLASH_START_ADDR + sector*FLASH_PAGE_SIZE);
    for(uint16_t i=0; i<count*FLASH_PAGE_SIZE; i++) {
        buff[i] = src[i];
    }
    return RES_OK;
}
  1. 磁盘写入
DRESULT USER_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) {
    // 重用之前实现的STORAGE_Write_FS逻辑
    uint16_t i;
    HAL_FLASH_Unlock();
    FLASH_EraseInitTypeDef f;
    f.TypeErase = FLASH_TYPEERASE_PAGES;
    f.PageAddress = FLASH_START_ADDR + sector*FLASH_PAGE_SIZE;
    f.NbPages = count;
    uint32_t PageError = 0;
    HAL_FLASHEx_Erase(&f, &PageError);
    
    for(i=0; i<count*FLASH_PAGE_SIZE; i+=4) {
        HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD,
                         FLASH_START_ADDR + sector*FLASH_PAGE_SIZE + i,
                         *(uint32_t *)(&buff[i]));
    }
    
    HAL_FLASH_Lock();
    return RES_OK;
}
  1. 磁盘控制
DRESULT USER_ioctl(BYTE pdrv, BYTE cmd, void *buff) {
    switch(cmd) {
        case CTRL_SYNC: return RES_OK;
        case GET_SECTOR_SIZE: *(DWORD*)buff = FLASH_PAGE_SIZE; break;
        case GET_SECTOR_COUNT: *(DWORD*)buff = FLASH_PAGE_NBR; break;
        case GET_BLOCK_SIZE: *(DWORD*)buff = 1; break; // 擦除块大小(以扇区为单位)
        default: return RES_PARERR;
    }
    return RES_OK;
}

4. 优化与实用技巧

虽然基础功能已经实现,但在实际使用中还需要考虑一些优化和可靠性问题。

4.1 Flash寿命管理

STM32F103的Flash通常有10,000次擦写寿命,我们需要采取措施延长使用寿命:

  • 写缓存 :实现一个RAM缓存,累积到一定量再实际写入Flash
  • 磨损均衡 :在64KB空间内轮换使用不同区块
  • 减少擦除 :尽量在单次操作中写入更多数据

示例写缓存实现:

#define CACHE_SIZE 1024  // 1KB缓存
uint8_t write_cache[CACHE_SIZE];
uint32_t cache_pos = 0;

void flush_cache(uint32_t sector) {
    if(cache_pos > 0) {
        STORAGE_Write_FS(0, write_cache, sector, 1);
        cache_pos = 0;
    }
}

void cache_write(uint8_t *data, uint32_t size, uint32_t sector) {
    if(cache_pos + size > CACHE_SIZE) {
        flush_cache(sector);
    }
    memcpy(&write_cache[cache_pos], data, size);
    cache_pos += size;
}

4.2 性能优化技巧

内部Flash的写入速度较慢,以下方法可以改善用户体验:

  1. 增大MSC_MEDIA_PACKET :在CubeMX中设置为最大允许值(需与Flash页大小匹配)
  2. 批量写入 :累积多个扇区数据后一次性写入
  3. 后台擦除 :提前擦除下一个可能使用的扇区

4.3 文件系统自动初始化

在main函数中添加自动格式化逻辑,当检测到未格式化的存储时自动执行:

FATFS_Status = f_mount(&USERFatFS, path, 1);
if(FATFS_Status == FR_NO_FILESYSTEM) {
    uint8_t work[FF_MAX_SS]; // 格式化缓冲区
    f_mkfs(path, FM_FAT, 0, work, sizeof(work)); // 快速格式化
    FATFS_Status = f_mount(NULL, path, 0); // 卸载
    FATFS_Status = f_mount(&USERFatFS, path, 1); // 重新挂载
}

if(FATFS_Status == FR_OK) {
    f_mkdir("0:/FW"); // 创建固件目录
}

5. 实际应用场景扩展

这个自制的迷你U盘虽然容量有限,但在特定场景下非常实用:

  1. 固件升级 :将新固件拖拽到U盘,设备自动检测并更新
  2. 配置存储 :保存设备配置文件,方便在不同设备间迁移
  3. 数据采集 :临时存储传感器数据,通过USB快速导出
  4. 教学演示 :直观展示USB MSC协议和文件系统工作原理

对于需要更大容量的场景,可以扩展使用外部SPI Flash或SD卡,只需修改 user_diskio.c 中的底层驱动即可。这种架构设计体现了良好的分层思想,使我们可以灵活更换存储介质而不影响上层应用。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐