基于STM32+外部FLASH(W25Q64)的USB虚拟U盘,实现单片机和电脑的文件传输。
目录
前言
一、硬件连接
二、CubeMX配置
1.工程基础配置
2.SPI配置
3.USB配置
4.USB_DEVICE配置编辑
5.FATFS配置
6.调整堆栈大小
三、导入W25Q64的驱动
四、修改USB接口函数
五、修改FATFS接口函数
六、测试
七、调试建议与常见问题
1. 电脑无法访问U盘
2. 单片机无法挂载文件系统
3. 电脑和单片机的文件系统无法相互兼容
总结
前言
本教程的目的:通过STM32的SPI接口来与外部FLASH(W25Q64)连接,再通过USB功能,制作一个MSC(大容量存储设备)类设备,将W25Q64模拟为一个U盘,使STM32能与计算机进行文件传输。
硬件平台:STM32F407VET6最小系统,板载外部FALSH(W25Q64)
软件平台:Keil uVision5:V5.24.2.0
STM32CubeMX:Version 6.13.0
F4固件包版本:STM32Cube FW_F4V1.28.1
一、硬件连接
W25Q64的原理图如下:
USB连接如下:
二、CubeMX配置
1.工程基础配置
配置RCC
配置调试接口
配置串口方便之后进行调试
2.SPI配置
配置SPI2
注:分频系数可自行调整,以获得更快的通信速率,但通信速率过快可能会出现错误。
3.USB配置
4.USB_DEVICE配置

由于W25Q64的一个扇区是4096字节,所以将缓冲区大小设置为4096字节
5.FATFS配置
将物理驱动器参数中的扇区大小设置为4096,这一步很关键,如果设置错误可能导致计算机和STM32上的文件系统不兼容,导致无法进行文件交互。
6.调整堆栈大小

在之后调试程序的过程中,很可能会出现程序莫名卡死的情况,大概率是由于堆栈大小不够造成的栈溢出,所以要增大堆栈大小防止程序卡死。
至此,CubeMX的配置全部完成,接下来生成代码后再Keil中进行代码编写。
三、导入W25Q64的驱动
如果使用其他的W25QXX系列的芯片,需要在本代码的基础上稍作改动。
如果使用自己的W25Q64驱动库要注意,W25Q64_Write函数必须要自带擦除,能够从任意地址开始写入指定长度的数据,并且不影响同一扇区中,该地址之前的其他数据。详情可以参考本库中的W25Q64_Write函数
W25Q64.c
#include "W25Q64.h"
/*协议层*/
void MySPI_Init(void)
{
/*设置默认电平*/
HAL_GPIO_WritePin(WQ64_CS_GPIO_Port,WQ64_CS_Pin,GPIO_PIN_SET); //SS默认高电平
}
void MySPI_Start(void)
{
HAL_GPIO_WritePin(WQ64_CS_GPIO_Port,WQ64_CS_Pin,GPIO_PIN_RESET); //拉低SS,开始时序
}
void MySPI_Stop(void)
{
HAL_GPIO_WritePin(WQ64_CS_GPIO_Port,WQ64_CS_Pin,GPIO_PIN_SET); //拉高SS,终止时序
}
/**
* 函 数:SPI交换传输一个字节,使用SPI模式0
* 参 数:ByteSend 要发送的一个字节
* 返 回 值:接收的一个字节
* 备 注:可自行修改本函数,将硬件SPI改为软件SPI
*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
uint8_t ByteReceive = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
HAL_SPI_TransmitReceive(&hspi2,&ByteSend,&ByteReceive,1,50);
return ByteReceive; //返回接收到的一个字节数据
}
/**
* 函 数:W25Q64初始化
* 参 数:无
* 返 回 值:无
*/
void W25Q64_Init(void)
{
MySPI_Init(); //先初始化底层的SPI
}
uint32_t W25Q64_ReadID(void)
{
uint32_t ID;
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令
ID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回
ID <<= 8; //高8位移到高位
ID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位
ID <<= 8; //高8位移到高位
ID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位,通过输出参数返回
MySPI_Stop(); //SPI终止
return ID; //正常情况下W25Q64读取到的设备ID是0xef4017
}
/**
* 函 数:W25Q64写使能
* 参 数:无
* 返 回 值:无
*/
void W25Q64_WriteEnable(void)
{
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令
MySPI_Stop(); //SPI终止
}
/**
* 函 数:W25Q64等待忙
* 参 数:无
* 返 回 值:无
*/
void W25Q64_WaitBusy(void)
{
uint32_t Timeout;
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令
Timeout = 100000; //给定超时计数时间
while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //循环等待忙标志位
{
Timeout --; //等待时,计数值自减
if (Timeout == 0) //自减到0后,等待超时
{
/*超时的错误处理代码,可以添加到此处*/
break; //跳出等待,不等了
}
}
MySPI_Stop(); //SPI终止
}
/**
* 函 数:W25Q64页编程
* 参 数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF
* 参 数:DataArray 用于写入数据的数组
* 参 数:Count 要写入数据的数量,范围:0~256
* 返 回 值:无
* 注意事项:写入的地址范围不能跨页
*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{
uint16_t i;
W25Q64_WriteEnable(); //写使能
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令
MySPI_SwapByte(Address >> 16); //交换发送地址23~16位
MySPI_SwapByte(Address >> 8); //交换发送地址15~8位
MySPI_SwapByte(Address); //交换发送地址7~0位
for (i = 0; i < Count; i ++) //循环Count次
{
MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据
}
MySPI_Stop(); //SPI终止
W25Q64_WaitBusy(); //等待忙
}
//无检验写SPI FLASH
//必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
//具有自动换页功能
//在指定地址开始写入指定长度的数据,但是要确保地址不越界!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
//CHECK OK
void W25Q64_Write_NoCheck(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint16_t pageremain;
pageremain = 256 - WriteAddr % 256; //单页剩余的字节数
if (NumByteToWrite <= pageremain)
pageremain = NumByteToWrite; //不大于256个字节
while (1)
{
W25Q64_PageProgram(WriteAddr, pBuffer, pageremain);
if (NumByteToWrite == pageremain)
break; //写入结束了
else //NumByteToWrite>pageremain
{
pBuffer += pageremain;
WriteAddr += pageremain;
NumByteToWrite -= pageremain; //减去已经写入了的字节数
if (NumByteToWrite > 256)
pageremain = 256; //一次可以写入256个字节
else
pageremain = NumByteToWrite; //不够256个字节了
}
};
}
//写SPI FLASH
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大65535)
uint8_t W25Q64_BUFFER[4096];
void W25Q64_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint32_t secpos;
uint16_t secoff;
uint16_t secremain;
uint16_t i;
uint8_t *W25Q64_BUF;
W25Q64_BUF = W25Q64_BUFFER;
secpos = WriteAddr / 4096; //扇区地址
secoff = WriteAddr % 4096; //在扇区内的偏移
secremain = 4096 - secoff; //扇区剩余空间大小
if (NumByteToWrite <= secremain)
secremain = NumByteToWrite; //不大于4096个字节
while (1)
{
W25Q64_Read(W25Q64_BUF, secpos * 4096, 4096); //读出整个扇区的内容
for (i = 0; i < secremain; i++) //校验数据
{
if (W25Q64_BUF[secoff + i] != 0XFF)
break; //需要擦除
}
if (i < secremain) //需要擦除
{
W25Q64_SectorErase(secpos); //擦除这个扇区
for (i = 0; i < secremain; i++) //复制
{
W25Q64_BUF[i + secoff] = pBuffer[i];
}
W25Q64_Write_NoCheck(W25Q64_BUF, secpos * 4096, 4096); //写入整个扇区
}
else
W25Q64_Write_NoCheck(pBuffer, WriteAddr, secremain); //写已经擦除了的,直接写入扇区剩余区间.
if (NumByteToWrite == secremain)
break; //写入结束了
else //写入未结束
{
secpos++; //扇区地址增1
secoff = 0; //偏移位置为0
pBuffer += secremain; //指针偏移
WriteAddr += secremain; //写地址偏移
NumByteToWrite -= secremain; //字节数递减
if (NumByteToWrite > 4096)
secremain = 4096; //下一个扇区还是写不完
else
secremain = NumByteToWrite; //下一个扇区可以写完了
}
};
}
/**
* 函 数:W25Q64扇区擦除(4KB)
* 参 数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF 0xXX0000~0xXXFFFF
* 返 回 值:无
*/
void W25Q64_SectorErase(uint32_t Address)
{
W25Q64_WriteEnable(); //写使能
Address *= 4096;
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令
MySPI_SwapByte(Address >> 16); //交换发送地址23~16位
MySPI_SwapByte(Address >> 8); //交换发送地址15~8位
MySPI_SwapByte(Address); //交换发送地址7~0位
MySPI_Stop(); //SPI终止
W25Q64_WaitBusy(); //等待忙
}
void W25Q64_ChipErase(void)
{
W25Q64_WriteEnable(); //写使能
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_CHIP_ERASE); //交换发送芯片擦除的指令
// MySPI_SwapByte(Address >> 16); //交换发送地址23~16位
// MySPI_SwapByte(Address >> 8); //交换发送地址15~8位
// MySPI_SwapByte(Address); //交换发送地址7~0位
MySPI_Stop(); //SPI终止
W25Q64_WaitBusy(); //等待忙
}
/**
* 函 数:W25Q64读取数据
* 参 数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF
* 参 数:DataArray 用于接收读取数据的数组,通过输出参数返回
* 参 数:Count 要读取数据的数量,范围:0~0x800000
* 返 回 值:无
*/
void W25Q64_Read(uint8_t *DataArray,uint32_t Address,uint32_t Count)
{
uint32_t i=0;
MySPI_Start(); //SPI起始
MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令
MySPI_SwapByte(Address >> 16); //交换发送地址23~16位
MySPI_SwapByte(Address >> 8); //交换发送地址15~8位
MySPI_SwapByte(Address); //交换发送地址7~0位
for (i = 0; i < Count; i ++) //循环Count次
{
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据
}
MySPI_Stop(); //SPI终止
}
W25Q64.h
#ifndef __W25Q64_H
#define __W25Q64_H
#include "main.h"
#include "spi.h"
#define FLASH_SECTOR_COUNT 2048 //W25Q64共有2048个扇区
#define FLASH_SECTOR_SIZE 4096 //W25Q64每个扇区的大小为4096字节
#define W25Q64_WRITE_ENABLE 0x06
#define W25Q64_WRITE_DISABLE 0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#define W25Q64_WRITE_STATUS_REGISTER 0x01
#define W25Q64_PAGE_PROGRAM 0x02
#define W25Q64_QUAD_PAGE_PROGRAM 0x32
#define W25Q64_BLOCK_ERASE_64KB 0xD8
#define W25Q64_BLOCK_ERASE_32KB 0x52
#define W25Q64_SECTOR_ERASE_4KB 0x20
#define W25Q64_CHIP_ERASE 0xC7
#define W25Q64_ERASE_SUSPEND 0x75
#define W25Q64_ERASE_RESUME 0x7A
#define W25Q64_POWER_DOWN 0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90
#define W25Q64_READ_UNIQUE_ID 0x4B
#define W25Q64_JEDEC_ID 0x9F
#define W25Q64_READ_DATA 0x03
#define W25Q64_FAST_READ 0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q64_FAST_READ_DUAL_IO 0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q64_FAST_READ_QUAD_IO 0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3
#define W25Q64_DUMMY_BYTE 0xFF
void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);
void W25Q64_Init(void);
uint32_t W25Q64_ReadID(void);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ChipErase(void);
void W25Q64_Write(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
void W25Q64_Read(uint8_t *DataArray, uint32_t Address, uint32_t Count);
#endif
四、修改USB接口函数
usbd_storage_if.c
文件的作用是实现USB大容量存储设备的接口功能。
在usbd_storage_if.c中,添加include和宏定义:
#include "W25Q64.h"
//用户自定义数据,防止生成代码时被覆盖
#define USER_STORAGE_LUN_NBR 1
#define USER_STORAGE_BLK_NBR 2048
#define USER_STORAGE_BLK_SIZ 4096
并修改以下5个函数:
int8_t STORAGE_Init_FS(uint8_t lun)
{
/* USER CODE BEGIN 2 */
W25Q64_Init();
return (USBD_OK);
/* USER CODE END 2 */
}
int8_t STORAGE_GetCapacity_FS(uint8_t lun, uint32_t *block_num, uint16_t *block_size)
{
/* USER CODE BEGIN 3 */
*block_num = USER_STORAGE_BLK_NBR; //更换为用户自定义数据
*block_size = USER_STORAGE_BLK_SIZ; //更换为用户自定义数据
return (USBD_OK);
/* USER CODE END 3 */
}
int8_t STORAGE_Read_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
/* USER CODE BEGIN 6 */
W25Q64_Read(buf, blk_addr * USER_STORAGE_BLK_SIZ, blk_len * USER_STORAGE_BLK_SIZ);
return (USBD_OK);
/* USER CODE END 6 */
}
int8_t STORAGE_Write_FS(uint8_t lun, uint8_t *buf, uint32_t blk_addr, uint16_t blk_len)
{
/* USER CODE BEGIN 7 */
W25Q64_Write(buf, blk_addr * USER_STORAGE_BLK_SIZ, blk_len * USER_STORAGE_BLK_SIZ);
return (USBD_OK);
/* USER CODE END 7 */
}
int8_t STORAGE_GetMaxLun_FS(void)
{
/* USER CODE BEGIN 8 */
return (USER_STORAGE_LUN_NBR - 1); //更换为用户自定义数据
/* USER CODE END 8 */
}
五、修改FATFS接口函数
在usbd_storage_if.c中,添加include和宏定义:
#include "W25Q64.h"
#define PAGE_SIZE 256
#define SECTOR_SIZE 4096
#define SECTOR_COUNT 2048
#define BLOCK_SIZE 65536
#define FLASH_PAGES_PER_SECTOR SECTOR_SIZE/PAGE_SIZE
在user_diskio.c中,主要需要修改以下几个函数:
DSTATUS USER_initialize (
BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{
/* USER CODE BEGIN INIT */
Stat = USER_status(pdrv);
return Stat;
/* USER CODE END INIT */
}
DSTATUS USER_status (
BYTE pdrv /* Physical drive number to identify the drive */
)
{
/* USER CODE BEGIN STATUS */
Stat = STA_NOINIT;
if(W25Q64_ReadID() != 0)
{
Stat &= ~STA_NOINIT;
}
return Stat;
/* USER CODE END STATUS */
}
DRESULT USER_read (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
BYTE *buff, /* Data buffer to store read data */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to read */
)
{
/* USER CODE BEGIN READ */
uint32_t Addr = sector * FLASH_SECTOR_SIZE;
uint32_t CNT = count * FLASH_SECTOR_SIZE;
W25Q64_Read((uint8_t *)buff,Addr,CNT);
return RES_OK;
/* USER CODE END READ */
}
DRESULT USER_write (
BYTE pdrv, /* Physical drive nmuber to identify the drive */
const BYTE *buff, /* Data to be written */
DWORD sector, /* Sector address in LBA */
UINT count /* Number of sectors to write */
)
{
/* USER CODE BEGIN WRITE */
uint32_t Addr = sector * FLASH_SECTOR_SIZE;
uint32_t CNT = count * FLASH_SECTOR_SIZE;
W25Q64_Write((uint8_t *)buff,Addr,CNT);
/* USER CODE HERE */
return RES_OK;
/* USER CODE END WRITE */
}
DRESULT USER_ioctl (
BYTE pdrv, /* Physical drive nmuber (0..) */
BYTE cmd, /* Control code */
void *buff /* Buffer to send/receive control data */
)
{
/* USER CODE BEGIN IOCTL */
DRESULT res = RES_OK;
switch(cmd)
{
case CTRL_SYNC: break;
case GET_SECTOR_COUNT: *(DWORD*)buff = FLASH_SECTOR_COUNT;break;
case GET_SECTOR_SIZE: *(DWORD*)buff = FLASH_SECTOR_SIZE;break;
case GET_BLOCK_SIZE: *(DWORD*)buff = 16;break;
default: res = RES_PARERR; break;
}
return res;
/* USER CODE END IOCTL */
}
六、测试
在主函数初始化后,添加以下代码:
//尝试挂载文件系统,若文件系统挂载失败,则重新格式化。
FRESULT res;
printf("Mounting the FATFS...\n");
res = f_mount(&USERFatFS,"0:",1);
if(res == FR_OK)
{
printf("Mount_OK\n");
}
else
{
printf("Mount_Error:%d\n",res);
BYTE WorkBuffer[4096];
DWORD cluster_size = 0;
printf("MKFS-ing...\n");
res = f_mkfs("0:",FM_FAT,cluster_size,WorkBuffer,FLASH_SECTOR_SIZE);
if(res == FR_OK)
{
printf("MKFS_OK\n");
res = f_mount(&USERFatFS, "0:", 1); // 重新挂载文件系统
if (res == FR_OK)
{
printf("MOUNT_OK\n");
}
else
{
printf("Mount_Error:%d\n",res);
}
}
else
{
printf("MKFS_ERROR:%d\n",res);
}
}
// 文件读写测试
char FILE_NAME[32] = "test.txt"; // 测试文件名
char FILE_CONTENT[32] = "Hello, FATFS!"; // 写入文件的内容
FIL file; // 文件句柄
UINT bytes_written; // 写入的字节数
UINT bytes_read; // 读取的字节数
char read_buffer[100]; // 读取缓冲区
// 打开文件(如果文件不存在,则创建)
res = f_open(&file, FILE_NAME, FA_CREATE_ALWAYS | FA_WRITE);
if (res == FR_OK)
{
printf("File opened/created successfully.\n");
// 写入数据到文件
res = f_write(&file, FILE_CONTENT, strlen(FILE_CONTENT), &bytes_written);
if (res == FR_OK)
{
printf("Data written successfully. Bytes written: %u\n", bytes_written);
}
else
{
printf("Write error: %d\n", res);
}
// 关闭文件
f_close(&file);
}
else
{
printf("Failed to open/create file. Error: %d\n", res);
}
// 再次打开文件进行读取
res = f_open(&file, FILE_NAME, FA_READ);
if (res == FR_OK)
{
printf("File opened for reading successfully.\n");
// 清空读取缓冲区
memset(read_buffer, 0, sizeof(read_buffer));
// 读取文件内容
res = f_read(&file, read_buffer, sizeof(read_buffer) - 1, &bytes_read);
if (res == FR_OK)
{
printf("Data read successfully. Bytes read: %u\n", bytes_read);
printf("File content: %s\n", read_buffer);
}
else
{
printf("Read error: %d\n", res);
}
// 关闭文件
f_close(&file);
}
else
{
printf("Failed to open file for reading. Error: %d\n", res);
}
编译下载代码后,通过串口助手查看发来的数据:
由此可知,文件系统可以正常的挂载和读写。
我们使用一根USB线将单片机连接到电脑,可以检测到一个U盘,里面的文件正是我们刚刚写入的,至此,实验成功。
七、调试建议与常见问题
1. 电脑无法访问U盘
在修改USB接口函数后,可以先将代码下载到单片机中,然后连接USB线,查看电脑是否能检测到U盘设备,并且是否能成功对其进行格式化。如果无法正常格式化,大概率是Flash的读写函数出了问题。我在调试时就遇到了这种情况,经过排查发现,我的FLASH写函数没有自带擦除功能。由于Flash在写入前必须先进行擦除,而擦除的最小单位是扇区,因此需要在写入前对Flash进行擦除。同时,为了避免影响同一扇区中写入地址之前的数据,需要先将其保存到缓冲区,然后再对整个扇区进行写入。具体实现可参考我的W25Q64_Write
函数。
2. 单片机无法挂载文件系统
确保user_diskio
中的五个函数(USER_initialize
、USER_status
、USER_read
、USER_write
、USER_ioctl
)都已正确修改。
确认相关宏定义是否正确。
建议在单片机上电前,先不要连接USB线。等单片机初始化完成后,再连接USB线。
由于未知原因,如果一开始在电脑上将U盘格式化,之后再在单片机上挂载文件系统时,f_mount
函数的返回值可能会是13(FR_NOT_ENABLED
)。这种情况下,需要在单片机上调用f_mkfs
函数对U盘进行格式化。
3. 电脑和单片机的文件系统无法相互兼容
我在调试过程中遇到了一个非常棘手的问题:当我在单片机上将文件系统格式化后,单片机可以正常挂载和读写文件,但连接到电脑后,电脑却无法访问U盘内的文件。相反,如果我从电脑上将U盘格式化,虽然电脑可以正常读写文件,但在单片机上却无法挂载文件系统,f_mount
函数的返回值一直是13(FR_NOT_ENABLED
)。
经过排查,我发现这个问题通常是由于物理驱动器的扇区大小配置错误导致的。对于W25Q64芯片,扇区大小是4096字节。因此,需要在FATFS的配置界面中,将“Physical Drive Parameters”部分的扇区大小设置为4096字节。
总结
项目的工程文件下载地址:
GitCode – 全球开发者的开源社区,开源代码托管平台
在完成这个项目的过程中,我参考了众多前辈们的教程,也遇到了不少棘手的问题。为了攻克这些难题,我查阅了大量资料,向AI寻求帮助,经过不懈的努力,最终成功实现了这个USB虚拟U盘项目。在这个过程中,我深刻体会到知识共享的力量,也希望能将我的经验传递给更多人。
如果你在进行类似项目时,遇到了和我一样的问题,希望这篇教程能成为你的参考,为你提供一些思路和解决方案。这正是我撰写这篇博客的初衷——希望能帮助到更多在相同道路上探索的开发者,为大家提供有价值的参考和启发。
本文到这里就结束了。这不仅是我的第一个博客,也是对我学习历程的一次记录。感谢大家的阅读与支持!
参考资料:
基于stm32的USB虚拟U盘+FATFS+W25Q64
【12.1】FatFS嵌入式文件管理系统——Kevin带你读《STM32Cube高效开发教程高级篇》【STM32学习】基于STM32F411CEU6的USB储存设备
作者:鸽者394