基于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_initializeUSER_statusUSER_readUSER_writeUSER_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

物联沃分享整理
物联沃-IOTWORD物联网 » 基于STM32+外部FLASH(W25Q64)的USB虚拟U盘,实现单片机和电脑的文件传输。

发表回复