返回列表 发新帖我要提问本帖赏金: 200.00元(功能说明)

[MM32软件] 突破边界:在MM32上实现FATFS与TinyUSB的协同作战——UART命令行控制的文件管理系统实现

[复制链接]
 楼主| sujingliang 发表于 2025-6-25 11:37 | 显示全部楼层 |阅读模式
<
本帖最后由 sujingliang 于 2025-6-26 16:08 编辑

#申请原创#   @21小跑堂
基于Mini-F5375-OB开发板的硬件特性(搭载MM32F5系列高性能MCU、外置ZD25WQ32 SPI Flash及全速USB接口),本文提出一种双模存储架构设计方案:通过在Flash介质上构建统一的FAT文件系统,同步实现USB MSC(Mass Storage Class)设备功能和UART命令行文件管理系统。该设计充分利用MCU硬件资源,采用TinyUSB协议栈实现USB大容量存储设备功能,将4MB QSPI Flash虚拟为PC可识别的U盘;同时集成FatFs文件系统模块,通过UART接口提供完整的文件操作命令集(包括文件创建、读写、目录管理等)。两种访问方式共享同一物理存储空间,通过互斥锁机制确保数据一致性,其中USB模式采用SCSI命令直接操作Flash底层扇区,而UART模式通过FatFs API进行文件级管理。

QSPI接口的外置flash(ZD25WQ32)
1.png
USB接口

2.png
一、第三方库使用

1、FATFS是一个面向嵌入式系统的轻量级FAT/exFAT文件系统模块,由ChaN开发并开源。它采用ANSI C编写,具有高度可移植性,支持FAT12、FAT16和FAT32文件系统,可无缝运行在SD卡、Flash等存储介质上。
2、TinyUSB 是一个开源的轻量级 USB 协议栈,专为嵌入式系统设计,支持主机(host)和设备(device)模式。它采用纯 C 语言编写,具有高度可移植性,支持多种 MCU 平台。特别适合实现 USB 虚拟串口、U 盘、键盘等设备功能,是嵌入式 USB 开发的理想选择。

二、工作模式选择
由于FATFS和TinyUSB共存并不容易,所以本设计通过在启动时检测GPIO引脚电平状态,实现两种完全独立的工作模式切换,满足不同场景下的需求:
1.USB存储(TinyUSB)模式(PB0=低电平):
  • 将内部Flash虚拟为U盘
  • 允许PC端直接访问文件系统
  • 适用于数据导出和配置更新


2.MCU文件系统操作(FATFS+UART)模式(PB0=高电平):
  • 启用UART命令行接口
  • 运行数据采集任务
  • 数据记录到FATFS文件系统
  • 适用于现场数据采集场景


也就是说在在按下KEY1(PB0)键时上电进入USB存储模式,不按KEY1(PB0)键时上电进入MCU文件系统操作模式;外置flash做为数据交互媒介。

三、基于GPIO电平判断上电进入不同的运行模式

  1. int main(void)
  2. {
  3.     PLATFORM_Init();                        //板级驱动,不用驱动uart3
  4.                 USART_Configure(115200);        //UART3驱动
  5.                 EXTI_Configure();                        //key1(PB0),key2(PB1)初始化
  6.                
  7.                 if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0) == 0){        //判断PB0电平
  8.                         modeChangeRequest=1;        //如果低电平,设置模式1
  9.                 }else{
  10.                         modeChangeRequest=0;        //如果低电平,设置模式0
  11.                 }

  12.                 if (modeChangeRequest==1) {                //模式1,USB存储(TinyUSB)模式
  13.                         printf("Enter Usb Msc Mode\r\n");
  14.                         QSPI_Configure();                        //QSPI初始化,驱动外部flash
  15.                         TinyUSB_Device_CDC_MSC_Sample();        //进入tiny初始化和循环
  16.                 }
  17.                 else                        //模式0,MCU文件系统操作(FATFS+UART)模式
  18.                 {
  19.                         printf("Enter MCU FatFs Mode\r\n");
  20.                         UART_Sample();                //进入UART接收命令,在FatFs执行模式
  21.                 }
  22.         
  23.     while (1)                //不会被执行
  24.     {
  25.     }
  26. }


四、USB存储(TinyUSB)模式实现


很遗憾在TinyUSB官方提供的device例程中,没有提供基于外部flash的例程,这部分需要自己根据TinyUSB接口函数实现
1、将以下文件加入工程
3.png
其中cdc_device.c不是必须的,它实现了cdc(串口)设备。
4.png
上面圈出的4个文件需要加入工程,这四个文件可以在MM32F5270的例程中找到:
LibSamples_MM32F5270_V1.5.6\3rdPartySoftwarePorting\TinyUSB\Demos\TinyUSB_Device_CDC_MSC
其中msc_disk.c原来是基于SRAM的,需要在后面修改为基于外部flash。

2、复制一个msc_disk.c到工程根目录
需要这个文件的结构,实现其中的几个接口函数
void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4])
bool tud_msc_test_unit_ready_cb(uint8_t lun)
void tud_msc_capacity_cb(uint8_t lun, uint32_t* block_count, uint16_t* block_size)
bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject)
int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize)
bool tud_msc_is_writable_cb (uint8_t lun)
int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize)
int32_t tud_msc_scsi_cb (uint8_t lun, uint8_t const scsi_cmd[16], void* buffer, uint16_t bufsize)

3、tud_msc_inquiry_cb实现
tud_msc_inquiry_cb 是 TinyUSB 协议栈中用于响应 USB Mass Storage Class (MSC) INQUIRY 命令 的关键回调函数,其作用是为主机(如 PC)提供存储设备的基本标识信息。
  1. void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4])
  2. {
  3.   (void) lun;

  4.   const char vid[] = "TinyUSB";
  5.   const char pid[] = "Mass Storage";
  6.   const char rev[] = "1.0";

  7.   memcpy(vendor_id  , vid, strlen(vid));
  8.   memcpy(product_id , pid, strlen(pid));
  9.   memcpy(product_rev, rev, strlen(rev));
  10. }
4、tud_msc_test_unit_ready_cb 实现
tud_msc_test_unit_ready_cb 是 TinyUSB 协议栈中用于响应 SCSI Test Unit Ready (TUR) 命令 的关键回调函数,其作用是向主机(如 PC)报告存储设备当前是否就绪并可访问。
  1. bool tud_msc_test_unit_ready_cb(uint8_t lun)
  2. {
  3.   (void) lun;

  4.   // RAM disk is ready until ejected
  5.   if (ejected) {
  6.     // Additional Sense 3A-00 is NOT_FOUND
  7.     tud_msc_set_sense(lun, SCSI_SENSE_NOT_READY, 0x3a, 0x00);
  8.     return false;
  9.   }

  10.   return true;
  11. }
5、tud_msc_capacity_cb函数实现
tud_msc_capacity_cb 是 TinyUSB 协议栈中用于响应 USB Mass Storage Class (MSC) 容量查询请求 的核心回调函数,其作用是向主机(如 PC/Mac)报告存储设备的物理容量参数。
  1. void tud_msc_capacity_cb(uint8_t lun, uint32_t* block_count, uint16_t* block_size)
  2. {
  3.   (void) lun;

  4.   *block_count = DISK_BLOCK_NUM;
  5.   *block_size  = DISK_BLOCK_SIZE;
  6. }
6、tud_msc_start_stop_cb 函数实现
tud_msc_start_stop_cb 是 TinyUSB 协议栈中处理 USB Mass Storage Class (MSC) 启停事件 的关键回调函数,主要用于响应主机的设备加载/弹出指令和电源状态管理
  1. bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject)
  2. {
  3.   (void) lun;
  4.   (void) power_condition;

  5.   if ( load_eject )
  6.   {
  7.     if (start)
  8.     {
  9.       // load disk storage
  10.                          ejected = false;
  11.     }else
  12.     {
  13.       // unload disk storage
  14.       ejected = true;
  15.     }
  16.   }

  17.   return true;
  18. }
7、【重点关注】int32_t tud_msc_read10_cb函数实现
tud_msc_read10_cb 是 TinyUSB 协议栈中处理 USB Mass Storage Class (MSC) 读取请求 的核心回调函数,负责将存储设备的数据通过 USB 传输给主机
  1. int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void* buffer, uint32_t bufsize)
  2. {
  3.   (void) lun;
  4.         //uint32_t addr = lba * DISK_BLOCK_SIZE + offset;
  5.   // out of ramdisk
  6.   if ( lba >= DISK_BLOCK_NUM ) return -1;

  7.                 uint32_t addr = lba * FLASH_SECTOR_SIZE + offset;
  8.     uint32_t remaining = bufsize;
  9.    
  10.     while (remaining > 0) {
  11.         uint32_t read_size = (remaining > FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : remaining;
  12.         QSPI_FLASH_StandardSPI_FastRead(addr, buffer, read_size);
  13.         addr += read_size;
  14.         buffer += read_size;
  15.         remaining -= read_size;
  16.     }

  17.                 return (int32_t) bufsize;
  18. }
8、【重点关注】tud_msc_write10_cb 函数实现
tud_msc_write10_cb 是 TinyUSB 协议栈中处理 USB Mass Storage Class (MSC) 写入请求 的核心回调函数,负责将主机(如 PC/Mac)发送的数据写入存储设备(如 Flash)
  1. int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t* buffer, uint32_t bufsize)
  2. {
  3.   (void) lun;
  4.   if ( lba >= DISK_BLOCK_NUM ) return -1;
  5.         uint32_t addr = lba * DISK_BLOCK_SIZE + offset;
  6.   QSPI_FLASH_SmartEraseThenWrite(addr,buffer,bufsize);
  7.   return (int32_t) bufsize;
  8. }
由于flash檫除需要按照扇区(4096)檫除,而写入时为避免对原扇区的内容覆盖需要先读取原内容再补充写入内容,再按照page(256)写入
  1. uint8_t sector_buffer[FLASH_SECTOR_SIZE]; // 临时扇区缓冲区
  2. int QSPI_FLASH_SmartEraseThenWrite(uint32_t Address, uint8_t *Buffer, uint32_t Length) {
  3.     #define SECTOR_SIZE (4 * 1024)   // 4KB扇区
  4.     #define PAGE_SIZE   256          // 页大小
  5.    
  6.     if (Buffer == NULL || Length == 0) return -1;
  7.    
  8.    
  9.     uint32_t start_sector = Address / SECTOR_SIZE;
  10.     uint32_t end_sector = (Address + Length - 1) / SECTOR_SIZE;
  11.    
  12.     for (uint32_t sector = start_sector; sector <= end_sector; sector++) {
  13.         uint32_t sector_addr = sector * SECTOR_SIZE;
  14.         uint32_t sector_start = (sector == start_sector) ? (Address % SECTOR_SIZE) : 0;
  15.         uint32_t sector_end = (sector == end_sector) ? ((Address + Length - 1) % SECTOR_SIZE) : (SECTOR_SIZE - 1);
  16.         uint32_t sector_len = sector_end - sector_start + 1;
  17.         
  18.         // 1. 读取整个扇区到缓冲区(如果需要保留未修改部分)
  19.         // 这里假设需要保留未修改部分,所以先读取整个扇区
  20.         // 如果确定是全新写入,可以跳过读取步骤
  21.         
  22.         // 实现一个读取函数(您需要提供类似QSPI_FLASH_StandardSPI_Read)
  23.          QSPI_FLASH_StandardSPI_FastRead(sector_addr, sector_buffer, SECTOR_SIZE);
  24.         
  25.         // 2. 修改缓冲区中需要更新的部分
  26.         uint32_t buf_offset = (sector == start_sector) ? (Address % SECTOR_SIZE) : 0;
  27.         uint32_t data_offset = (sector == start_sector) ? 0 : (sector * SECTOR_SIZE - Address);
  28.         uint32_t copy_len = (Length - data_offset) > sector_len ? sector_len : (Length - data_offset);
  29.         
  30.         memcpy(sector_buffer + buf_offset, Buffer + data_offset, copy_len);
  31.         
  32.         // 3. 擦除整个扇区
  33.         QSPI_FLASH_StandardSPI_SectorErase(sector);
  34.         
  35.         // 4. 逐页写回整个扇区
  36.         for (uint32_t offset = 0; offset < SECTOR_SIZE; offset += PAGE_SIZE) {
  37.             uint32_t write_size = (SECTOR_SIZE - offset) > PAGE_SIZE ? PAGE_SIZE : (SECTOR_SIZE - offset);
  38.             QSPI_FLASH_StandardSPI_PageProgram(sector_addr + offset, sector_buffer + offset, write_size);
  39.         }
  40.     }
  41.    
  42.     return 0;
  43. }
9、【重点关注】tud_msc_scsi_cb 函数实现
tud_msc_scsi_cb 是 TinyUSB 协议栈中处理 所有未单独实现的 SCSI 命令 的通用回调函数,作为 MSC(Mass Storage Class)设备的底层命令处理中枢

处理未被 TinyUSB 单独回调函数(如read10_cb/write10_cb)覆盖的 SCSI 命令,包括:
设备信息查询(如 SCSI_INQUIRY)
介质状态检查(如 SCSI_TEST_UNIT_READY)
模式参数配置(如 SCSI_MODE_SENSE_6)

  1. int32_t tud_msc_scsi_cb (uint8_t lun, uint8_t const scsi_cmd[16], void* buffer, uint16_t bufsize)
  2. {
  3.   // read10 & write10 has their own callback and MUST not be handled here


  4.          void const* response = NULL;
  5.     int32_t resplen = 0;
  6.     bool in_xfer = true;

  7.     switch (scsi_cmd[0]) {
  8.         case SCSI_CMD_MODE_SENSE_6:
  9.          
  10.         case 0x5A://SCSI_CMD_MODE_SENSE_10:
  11.             // 返回相同的模式页数据
  12.                                                 {
  13.                                                 static uint8_t mode_sense_data[] = {
  14.                                                         0x03, 0x00, 0x00, 0x00, // Header
  15.                                                         // Block descriptor
  16.                                                         (DISK_BLOCK_NUM >> 24) & 0xFF, (DISK_BLOCK_NUM >> 16) & 0xFF,
  17.                                                         (DISK_BLOCK_NUM >> 8) & 0xFF, DISK_BLOCK_NUM & 0xFF,
  18.                                                         0x00, 0x00, // Reserved
  19.                                                         (DISK_BLOCK_SIZE >> 8) & 0xFF, DISK_BLOCK_SIZE & 0xFF,
  20.                                                         // Mode page
  21.                                                         0x1C, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
  22.                                         };
  23.                                         response=mode_sense_data;
  24.                                         resplen= sizeof(mode_sense_data);
  25.                                 }
  26.             break;
  27.          
  28.         case SCSI_CMD_READ_FORMAT_CAPACITY:        //0x23
  29.             // 返回格式容量数据
  30.             {
  31.                 static uint8_t read_capacity_data[] = {
  32.                 0x00, 0x00, 0x00, 0x08, // Capacity List Length
  33.                 (DISK_BLOCK_NUM >> 24) & 0xFF, (DISK_BLOCK_NUM >> 16) & 0xFF,
  34.                 (DISK_BLOCK_NUM >> 8) & 0xFF, DISK_BLOCK_NUM & 0xFF,
  35.                 (DISK_BLOCK_SIZE >> 8) & 0xFF, DISK_BLOCK_SIZE & 0xFF,
  36.                 0x02 // Formatted Media
  37.             };
  38.             response = read_capacity_data;
  39.             resplen = sizeof(read_capacity_data);

  40.             }
  41.             break;
  42.             
  43.         default:
  44.             tud_msc_set_sense(lun, SCSI_SENSE_ILLEGAL_REQUEST, 0x20, 0x00);
  45.             resplen = -1;
  46.             break;
  47.     }

  48.     if (response && (resplen > 0)) {
  49.         if (in_xfer) {
  50.             memcpy(buffer, response, (size_t) resplen);
  51.         }
  52.     }

  53.     return (resplen > bufsize) ? bufsize : resplen;

  54. }
10、其他参数
ffconf.h

#define FF_USER_DISK_ENABLE
#define FF_USE_MKFS                1
#define FF_CODE_PAGE        936
#define FF_MIN_SS                4096
#define FF_MAX_SS                4096


另外根据flash型号,
#define FLASH_SECTOR_SIZE 4096
#define FLASH_BLOCK_SIZE           16
#define FLASH_SECTOR_COUNT (4*1024*1024/FLASH_SECTOR_SIZE)


11、Tinyusb Device配置

  1. void TinyUSB_Device_Configure(void)
  2. {
  3.     USB_DeviceClockInit();

  4.     // init device stack on configured roothub port
  5.     tud_init(BOARD_TUD_RHPORT);
  6. }

  1. void TinyUSB_Device_CDC_MSC_Sample(void)
  2. {
  3.     printf("\r\nTest %s", __FUNCTION__);
  4.                
  5.                 TinyUSB_Device_Configure();
  6.                         
  7.     while (1)
  8.     {
  9.           tud_task();  // TinyUSB任务处理
  10.                                         cdc_task();
  11.                                         //led_blinking_task();
  12.                  }
  13. }


五、MCU文件系统操作(FATFS+UART)模式实现
1、将以下文件加入工程

5.png
其中user_diskio.c需要修改为对flash支持
2、user_diskio.c需要实现的接口函数
DSTATUS USER_GetDiskStatus(BYTE lun)
DSTATUS USER_DiskInitialize(BYTE lun)
DRESULT USER_DiskRead(BYTE lun, BYTE *buff, DWORD sector, UINT count)
DRESULT USER_DiskWrite(BYTE lun, const BYTE *buff, DWORD sector, UINT count)
DRESULT USER_DiskIoctl(BYTE lun, BYTE cmd, void *buff)

3、DSTATUS USER_GetDiskStatus(BYTE lun)函数实现
Get Drive Status
  1. DSTATUS USER_GetDiskStatus(BYTE lun)
  2. {
  3.     //DSTATUS stat = STA_NOINIT;
  4.     /* Add User Code Begin GetDiskStatus */
  5.                 if (lun != 0) return STA_NOINIT;
  6.     return 0;  // 假设始终就绪(实际可检查Flash状态寄存器)
  7.     /* Add User Code End GetDiskStatus */
  8.     //return stat;
  9. }
4、DSTATUS USER_DiskInitialize(BYTE lun)函数实现
Initialize Disk Drive
需要调用QSPI示例中的QSPI_Configure函数。
  1. DSTATUS USER_DiskInitialize(BYTE lun)
  2. {
  3.    DSTATUS stat = STA_NOINIT;
  4.    /* Add User Code Begin DiskInitialize */
  5.    if (lun != 0) return STA_NOINIT;  // 仅支持LUN 0
  6.    
  7.     // 初始化QSPI接口
  8.     QSPI_Configure();
  9.     stat=RES_OK;
  10.         
  11.     /* Add User Code End DiskInitialize */
  12.    return stat;
  13. }
5、USER_DiskRead函数实现
Read Sector
需要调用QSPI示例中的QSPI_FLASH_StandardSPI_FastRead函数。
  1. DRESULT USER_DiskRead(BYTE lun, BYTE *buff, DWORD sector, UINT count)
  2. {
  3.     DRESULT res = RES_PARERR;
  4.     /* Add User Code Begin DiskRead */
  5.                 if (lun != 0) return RES_PARERR;
  6.    
  7.                 for(; count > 0; count--)
  8.                 {
  9.                         QSPI_FLASH_StandardSPI_FastRead(sector * FLASH_SECTOR_SIZE, buff, FLASH_SECTOR_SIZE);
  10.                         sector++;
  11.                         buff += FLASH_SECTOR_SIZE;
  12.                 }
  13.         
  14.     res= RES_OK;
  15.     /* Add User Code End DiskRead */
  16.     return res;
  17. }
6、USER_DiskWrite函数实现

  1. DRESULT USER_DiskWrite(BYTE lun, const BYTE *buff, DWORD sector, UINT count)
  2. {
  3.          DRESULT res = RES_PARERR;
  4.     /* Add User Code Begin DiskWrite */
  5.          if (lun != 0) return RES_PARERR;
  6.          for(;count>0;count--)
  7.    {                                                                                    
  8.                                 QSPI_FLASH_Write((uint8_t*)buff,sector*FLASH_SECTOR_SIZE,FLASH_SECTOR_SIZE);
  9.                                 sector++;
  10.                                 buff+=FLASH_SECTOR_SIZE;
  11.                 }
  12.     res= RES_OK;
  13.     /* Add User Code End DiskWrite */
  14.     return res;
  15. }


其中,QSPI_FLASH_Write
  1. /**
  2. * [url=home.php?mod=space&uid=247401]@brief[/url] 向 Flash 写入数据(自动处理擦除和分页)
  3. * @param pData     要写入的数据指针
  4. * @param WriteAddr 写入起始地址(字节单位)
  5. * @param Size      要写入的字节数
  6. * [url=home.php?mod=space&uid=536309]@NOTE[/url] 基于 W25QXX_Write 逻辑改写,适配 QSPI_FLASH_StandardSPI_* 函数
  7. */
  8. void QSPI_FLASH_Write(const uint8_t *pData, uint32_t WriteAddr, uint16_t Size)
  9. {
  10.     uint32_t sector_pos;
  11.     uint16_t sector_offset;
  12.     uint16_t sector_remain;  
  13.     uint16_t i;
  14.     uint8_t *pSectorBuf = sector_buffer; // 指向扇区缓冲区
  15.    
  16.     sector_pos = WriteAddr / FLASH_SECTOR_SIZE;    // 计算扇区位置
  17.     sector_offset = WriteAddr % FLASH_SECTOR_SIZE; // 扇区内偏移量
  18.     sector_remain = FLASH_SECTOR_SIZE - sector_offset; // 当前扇区剩余空间
  19.    
  20.     // 如果请求写入的字节数不超过当前扇区剩余空间,则调整实际写入长度
  21.     if (Size <= sector_remain) {
  22.         sector_remain = Size;
  23.     }
  24.    
  25.     while (1) {
  26.         // 1. 读取整个扇区到缓冲区
  27.         QSPI_FLASH_StandardSPI_FastRead(sector_pos * FLASH_SECTOR_SIZE, pSectorBuf, FLASH_SECTOR_SIZE);
  28.         
  29.         // 2. 检查是否需要擦除(当前扇区是否有非0xFF数据需要覆盖)
  30.         for (i = 0; i < sector_remain; i++) {
  31.             if (pSectorBuf[sector_offset + i] != 0xFF) {
  32.                 break; // 需要擦除
  33.             }
  34.         }
  35.         
  36.         // 3. 如果需要擦除
  37.         if (i < sector_remain) {
  38.             // 3.1 擦除整个扇区
  39.             QSPI_FLASH_StandardSPI_SectorErase(sector_pos);
  40.             
  41.             // 3.2 将新数据合并到缓冲区
  42.             for (i = 0; i < sector_remain; i++) {
  43.                 pSectorBuf[sector_offset + i] = pData[i];
  44.             }
  45.             
  46.             // 3.3 写回整个扇区
  47.             QSPI_FLASH_StandardSPI_WriteSector(pSectorBuf, sector_pos * FLASH_SECTOR_SIZE, FLASH_SECTOR_SIZE);
  48.         }
  49.         else {
  50.             // 4. 如果不需要擦除,直接写入数据
  51.             QSPI_FLASH_StandardSPI_WriteSector(pData, WriteAddr, sector_remain);
  52.         }
  53.         
  54.         // 5. 判断是否写入完成
  55.         if (Size == sector_remain) {
  56.             break; // 全部写入完成
  57.         }
  58.         else {
  59.             // 6. 调整指针和地址,继续写入剩余数据
  60.             sector_pos++;       // 下一个扇区
  61.             sector_offset = 0;  // 从扇区起始位置开始
  62.             pData += sector_remain;
  63.             WriteAddr += sector_remain;
  64.             Size -= sector_remain;
  65.             
  66.             // 计算下一个扇区要写入的长度
  67.             sector_remain = (Size > FLASH_SECTOR_SIZE) ? FLASH_SECTOR_SIZE : Size;
  68.         }
  69.     }
  70. }


QSPI_FLASH_StandardSPI_WriteSector:
  1. /**
  2.   * [url=home.php?mod=space&uid=247401]@brief[/url]  写入整个扇区数据(自动分页编程)
  3.   * @param  pData: 要写入的数据指针
  4.   * @param  WriteAddr: 写入起始地址(需4KB对齐)
  5.   * @param  Size: 写入字节数(必须为FLASH_SECTOR_SIZE)
  6.   * @retval 无
  7.   */
  8. void QSPI_FLASH_StandardSPI_WriteSector(const uint8_t *pData, uint32_t WriteAddr, uint16_t Size)
  9. {
  10.     uint16_t page_size = 256; // Flash页编程大小
  11.     uint16_t pages = Size / page_size;
  12.    
  13.     /* 参数检查 */
  14.     if((WriteAddr % FLASH_SECTOR_SIZE != 0) || (Size != FLASH_SECTOR_SIZE)) {
  15.         printf("Error: Addr/size not aligned!\r\n");
  16.         return;
  17.     }

  18.     /* 分页写入 */
  19.     for(uint16_t i = 0; i < pages; i++) {
  20.         QSPI_FLASH_StandardSPI_PageProgram(
  21.             WriteAddr + (i * page_size),  // 目标地址
  22.             pData + (i * page_size),     // 数据指针
  23.             page_size                    // 固定写入256字节
  24.         );
  25.         
  26.         /* 无需额外等待,PageProgram内部已调用WaitBusy */
  27.     }
  28. }
7、USER_DiskIoctl函数实现
I/O Control
  1. DRESULT USER_DiskIoctl(BYTE lun, BYTE cmd, void *buff)
  2. {
  3.     DRESULT res = RES_PARERR;
  4.     /* Add User Code Begin DiskIoctl */
  5.                 DWORD *pdword = NULL;
  6.         switch (cmd) {
  7.         case GET_SECTOR_SIZE:  *(WORD*)buff = FLASH_SECTOR_SIZE;  return RES_OK; break;  // 4KB扇区
  8.         case GET_BLOCK_SIZE:   *(DWORD*)buff = FLASH_BLOCK_SIZE;  return RES_OK; break;  // 擦除块=1扇区
  9.         case GET_SECTOR_COUNT:
  10.                                         *(DWORD*)buff= FLASH_SECTOR_COUNT;
  11.                                         return RES_OK; break;  // 4MB/4KB=1024扇区
  12.                                  case CTRL_SYNC:      return RES_OK;      break; // 同步操作(无实际Flash操作)
  13.         default: return RES_PARERR;
  14.     }
  15.     /* Add User Code End DiskIoctl */
  16.     return res;
  17. }
8、get_fattime函数实现
  1. // 软件RTC结构体
  2. typedef struct {
  3.     uint16_t year;   // 年份(如2023)
  4.     uint8_t month;   // 1-12
  5.     uint8_t day;     // 1-31
  6.     uint8_t hour;    // 0-23
  7.     uint8_t min;     // 0-59
  8.     uint8_t sec;     // 0-59
  9. } SoftRTC_Time;

  10. // 全局变量存储当前时间
  11. volatile SoftRTC_Time current_time = {
  12.     .year = 2025,
  13.     .month = 6,
  14.     .day = 20,
  15.     .hour = 0,
  16.     .min = 0,
  17.     .sec = 0
  18. };

  19. DWORD get_fattime(void)
  20. {
  21.     return ((DWORD)(current_time.year - 1980) << 25)  // 年份(1980为基础)
  22.          | ((DWORD)current_time.month << 21)         // 月份
  23.          | ((DWORD)current_time.day << 16)           // 日
  24.          | ((DWORD)current_time.hour << 11)          // 小时
  25.          | ((DWORD)current_time.min << 5)            // 分钟
  26.          | ((DWORD)current_time.sec / 2);            // 秒/2(FatFs精度)
  27. }



9、相关参数
#define FF_USER_DISK_ENABLE

#define FLASH_SECTOR_SIZE 4096
#define FLASH_BLOCK_SIZE           16
#define FLASH_SECTOR_COUNT (4*1024*1024/FLASH_SECTOR_SIZE)


六、UART实现发送FatFs指令

1、需要UART不定长接收处理
这里没有实现这一部分,接收到数据调用Process_Input()
  1. void UART_Sample(void)
  2. {
  3.                
  4.         init_filesystem();
  5.         show_welcome();
  6.         printf("Command processor ready\r\n");
  7.         
  8.         USART_RxData_Interrupt(255);
  9.         while(1)
  10.         {
  11.                
  12.                 if (1== USART_RxStruct.CompleteFlag&&USART_RxStruct.CurrentCount>0)
  13.     {
  14.                         USART_RxStruct.CompleteFlag=0;
  15.                         Process_Input();
  16.                         
  17.     }
  18.         }
  19. }
  20.         


2、解析命令
  1. void Process_Input(void)
  2. {
  3.         parse_command((char*)USART_RxStruct.Buffer);
  4.         USART_RxData_Interrupt(255);
  5.         USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);
  6. }
  1. // 解析并执行命令
  2. void parse_command(char *cmd) {
  3.         

  4.                 if(strncmp(cmd,"\r",1)==0) cmd=cmd+1;        //puTTY终端用
  5.     // 保存到历史记录
  6.     if (history_count < MAX_HISTORY) {
  7.         strncpy(cmd_history[history_count], cmd, UART_BUF_SIZE-1);
  8.         history_count++;
  9.     } else {
  10.         // 滚动历史记录
  11.         for (int i = 0; i < MAX_HISTORY-1; i++) {
  12.             strcpy(cmd_history[i], cmd_history[i+1]);
  13.         }
  14.         strncpy(cmd_history[MAX_HISTORY-1], cmd, UART_BUF_SIZE-1);
  15.     }

  16.     // 检查特殊命令
  17.     if (strncmp(cmd, "help",4) == 0) {
  18.         show_help();
  19.         return;
  20.     }
  21.    
  22.     if (strncmp(cmd, "history",7) == 0) {
  23.         show_history();
  24.         return;
  25.     }
  26.    
  27.     if (strncmp(cmd, "clear\r\n",5) == 0) {
  28.         clear_screen();
  29.         return;
  30.     }
  31.    
  32.                 if (strncmp(cmd, "dir",3) == 0) {
  33.         handle_dir();
  34.                         return;
  35.     }
  36.     // 解析带参数的命令
  37.     char *token = strtok(cmd, ",");
  38.     if (token == NULL) return;
  39.    
  40.     if (strncmp(token, "mkfile",6) == 0) {
  41.         char *filename = strtok(NULL, ",");
  42.         char *content = strtok(NULL, "");
  43.         
  44.         if (filename && content) {
  45.             // 跳过可能的空格
  46.             while (*filename == ' ') filename++;
  47.             while (*content == ' ') content++;
  48.             handle_mkfile(filename, content);
  49.         } else {
  50.             printf("Invalid format. Usage: mkfile,"filename",content\r\n");
  51.         }
  52.     }
  53.    
  54.     else if (strncmp(token, "type",4) == 0) {
  55.         char *filename = strtok(NULL, "");
  56.         if (filename) {
  57.             // 跳过可能的空格
  58.             while (*filename == ' ') filename++;
  59.             handle_type(filename);
  60.         } else {
  61.             printf("Invalid format. Usage: type,"filename"\r\n");
  62.         }
  63.     }
  64.     else {
  65.         printf("Unknown command: %s\r\n", token);
  66.     }
  67. }
3、各种命令处理函数
  1. // 显示命令历史
  2. void show_history() {
  3.     printf("\r\nCommand History:\r\n");
  4.     for (int i = 0; i < history_count; i++) {
  5.         printf("%d: %s\r\n", i+1, cmd_history[i]);
  6.     }
  7.     printf("\r\n");
  8. }
  9. // 显示帮助信息
  10. void show_help() {
  11.     printf("\r\nAvailable Commands:\r\n");
  12.     printf("----------------------------------------\r\n");
  13.     printf("mkfile, "filename", content - Create file\r\n");
  14.     printf("dir                       - List directory\r\n");
  15.     printf("type, "filename"         - Show file content\r\n");
  16.     printf("history                   - Show command history\r\n");
  17.     printf("clear                     - Clear screen\r\n");
  18.     printf("help                      - Show this help\r\n");
  19.     printf("----------------------------------------\r\n\r\n");
  20. }
  21. // 清除屏幕
  22. void clear_screen() {
  23.     // ANSI转义序列清除屏幕
  24.     printf("\033[2J\033[H");
  25. }
  26. // 显示欢迎信息和系统状态
  27. void show_welcome() {
  28.     clear_screen();
  29.     printf("\033[1;34m"); // 蓝色
  30.     printf("========================================\r\n");
  31.     printf("    UART File System Command Processor\r\n");
  32.     printf("========================================\r\n");
  33.     printf("\033[0m"); // 重置颜色
  34.    
  35.     if (fs_status.fs_mounted) {
  36.         printf("File System: FAT32 | Total: %lu KB | Free: %lu KB\r\n",
  37.                    fs_status.total_space*8, fs_status.free_space*8);
  38.     } else {
  39.         printf("File System: NOT MOUNTED\r\n");
  40.     }
  41.    
  42.     printf("----------------------------------------\r\n");
  43.     printf("Type 'help' for available commands\r\n");
  44.     printf("========================================\r\n\r\n");
  45. }


  46. void Echo_Back(void)
  47. {
  48.         USART_TxData_Interrupt((uint8_t *)USART_RxStruct.Buffer, USART_RxStruct.CurrentCount);
  49.         while(USART_TxStruct.CompleteFlag!=1){};

  50. }






  51. // 初始化文件系统
  52. void init_filesystem() {
  53.     res = f_mount(&fs, "", 1);  // 挂载文件系统
  54.     if (res != FR_OK) {
  55.         printf("Failed to mount filesystem\r\n");
  56.     }
  57.                
  58.                 // 获取存储空间信息
  59.     FATFS *fs_ptr;
  60.     DWORD fre_clust;
  61.     res = f_getfree("", &fre_clust, &fs_ptr);
  62.     if (res == FR_OK) {
  63.         fs_status.total_space = (fs_ptr->n_fatent - 2) * fs_ptr->csize / 2; // KB
  64.         fs_status.free_space = fre_clust * fs_ptr->csize / 2; // KB
  65.         fs_status.fs_mounted = 1;
  66.     }
  67. }


  68. // 处理mkfile命令
  69. void handle_mkfile(char *filename, char *content) {
  70.     res = f_open(&file, filename, FA_WRITE | FA_CREATE_ALWAYS);
  71.     if (res != FR_OK) {
  72.         printf("Failed to create file\r\n");
  73.         return;
  74.     }
  75.    
  76.     UINT bytes_written;
  77.     res = f_write(&file, content, strlen(content), &bytes_written);
  78.     if (res != FR_OK || bytes_written != strlen(content)) {
  79.         printf("Failed to write file\r\n");
  80.     } else {
  81.         printf("File created successfully\r\n");
  82.     }
  83.    
  84.     f_close(&file);
  85. }

  86. // 处理dir命令
  87. void handle_dir() {
  88.     DIR dir;
  89.     FILINFO fno;
  90.     uint32_t total_files = 0;
  91.     uint32_t total_size = 0;
  92.    
  93.     res = f_opendir(&dir, "/");
  94.     if (res != FR_OK) {
  95.         printf("Error opening directory: %d\r\n", res);
  96.         return;
  97.     }
  98.    
  99.     printf("\r\nDirectory Listing:\r\n");
  100.     printf("----------------------------------------\r\n");
  101.     printf("Name                     Size     Date      Time\r\n");
  102.     printf("----------------------------------------\r\n");
  103.    
  104.     while (1) {
  105.         res = f_readdir(&dir, &fno);
  106.         if (res != FR_OK || fno.fname[0] == 0) break;
  107.         
  108.         if (fno.fattrib & AM_DIR) {
  109.             // 目录
  110.             printf("[%s]                   <DIR>    ", fno.fname);
  111.         } else {
  112.             // 文件
  113.             printf("%-24s %8lu  ", fno.fname, fno.fsize);
  114.             total_files++;
  115.             total_size += fno.fsize;
  116.         }
  117.         
  118.         // 显示日期和时间
  119.         printf("%04d-%02d-%02d %02d:%02d\r\n",
  120.                    (fno.fdate >> 9) + 1980,
  121.                    (fno.fdate >> 5) & 0x0F,
  122.                    fno.fdate & 0x1F,
  123.                    fno.ftime >> 11,
  124.                    (fno.ftime >> 5) & 0x3F);
  125.     }
  126.    
  127.     f_closedir(&dir);
  128.    
  129.     printf("----------------------------------------\r\n");
  130.     printf("%d file(s), %lu bytes\r\n", total_files, total_size*8);
  131.     printf("Free space: %lu KB\r\n\r\n", fs_status.free_space*8);
  132. }


  133. // 处理type命令
  134. void handle_type(char *filename) {
  135.     // 移除文件名可能存在的引号
  136.     if (filename[0] == '"' && filename[strlen(filename)-1] == '"') {
  137.         memmove(filename, filename+1, strlen(filename)-2);
  138.         filename[strlen(filename)-2] = '\0';
  139.     }
  140.    
  141.     res = f_open(&file, filename, FA_READ);
  142.     if (res != FR_OK) {
  143.         printf("Error opening file: %d\r\n", res);
  144.         return;
  145.     }
  146.    
  147.     printf("\r\nContents of '%s':\r\n", filename);
  148.     printf("----------------------------------------\r\n");
  149.    
  150.     char buffer[128];
  151.     UINT bytes_read;
  152.     UINT total_read = 0;
  153.    
  154.     while (1) {
  155.         res = f_read(&file, buffer, sizeof(buffer) - 1, &bytes_read);
  156.         if (res != FR_OK || bytes_read == 0) break;
  157.         
  158.         buffer[bytes_read] = '\0';
  159.         printf(buffer);
  160.         total_read += bytes_read;
  161.     }
  162.    
  163.     f_close(&file);
  164.    
  165.     printf("\r\n----------------------------------------\r\n");
  166.     printf("%u bytes read\r\n\r\n", total_read);
  167. }



七、运行



https://www.bilibili.com/video/BV1xfK7ztEwG/?vd_source=5b0f94f2f57c38a43471771787964a99










打赏榜单

21小跑堂 打赏了 200.00 元 2025-06-30
理由:恭喜通过原创审核!期待您更多的原创作品~

评论

FATFS和TinyUSB协同作战,在MM32上实现UART命令行控制的文件管理系统 。实现过程的代码展示详细,关键步骤解释到位,视频演示清晰,原创佳作!  发表于 2025-6-30 16:30
LiuDW091 发表于 2025-7-3 16:17 | 显示全部楼层
支持大佬
goyhuan 发表于 2025-7-5 10:34 来自手机 | 显示全部楼层
具体的应用场景是什么?
ytfdhb 发表于 2025-7-8 14:01 | 显示全部楼层
goyhuan 发表于 2025-7-15 17:27 | 显示全部楼层
相互不会干扰?
cooldog123pp 发表于 2025-7-24 17:28 | 显示全部楼层
FATFS和TinyUSB协同作战,在MM32上实现UART命令行控制的文件管理系统 。

您需要登录后才可以回帖 登录 | 注册

本版积分规则

84

主题

146

帖子

3

粉丝
快速回复 在线客服 返回列表 返回顶部