串行FLASH文件系统FatFs介绍并在STM32F1上移植



  先对内存存储有一个理解,比如在FALSH中存储数据时,已知在STM32F1 开发板上都有自带有一个外部 FLASH(W25Q128、128Mbit=16MByte,即16M内存),FLASH存储的数据掉电不会丢失里面的数据,MCU和W25Q128是采用SPI进行通信的。可以查看ROM、RAM、DRAM、SRAM和FLASH的区别单片机中为什么有了Flash还有EEPROM?

  W25Q128将16M的空间分成256个块(block),其中每一个块为64KB大小;同时,每一个块再细分为16个扇区(sector),其中每一个扇区为4KB大小,然后一个扇区还可以再分为16页,每一页是256个字节的大小。W25Q128规定每一次最大允许一次写入一页的数据,每一次写之前都需要擦除一个扇区的大小,如果需要写入更多的数据,则需要写完一页再写一页。

  当要向W25Q128写入数据时,先传输目标地址(即要写入哪个sector),然后检查这一块扇区是否为空,如果不为空(即使只有一个位的数据也算不为空),需要先将该扇区进行擦除,擦除干净再写入(擦除其实是将整个扇区都置为1);读取数据时也需要传输需要读取哪个扇区的地址;当一个扇区要多次写入数据时(比如我想接着上一次写完的后面写入),可以定义一个全局变量数组,先将原本该扇区的数据都保存到该数组,再将新数据与这个数组进行拼接,然后一次性写入到FLASH。这里需要至少 4K 的缓存区,要求芯片必须有 4K 以上 SRAM 才能很好的操作。

  比如当需要记录字符“STM32 SPI FLASH”时,这些文字会转化成ASCII码存储在数组中,数组数据通过SPI传输到W25Q128的指定地址上,在需要的时候再该地址把数据读取出来,再对读出来的数据以ASCII码的格式进行解读。

  个人觉得正点原子的代码比较规范和强大,而野火的代码和教程更能让人理解,以下是根据他们的代码理解,增加了一些注释以便理解。主要是关于w25q128的读和写函数,工程下载链接为:移植前工程1(spi flash简单读写).rar【提取码: 9r9h】

w25qxx.c

//读取SPI FLASH  
//在指定地址开始读取指定长度的数据
//pBuffer:数据存储区
//ReadAddr:开始读取的地址(24bit)
//NumByteToRead:要读取的字节数(最大65535)
void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)   
{ 
 	u16 i;   										    
	W25QXX_CS=0;                            	//使能器件   
  	SPI2_ReadWriteByte(W25X_ReadData);         	//发送读取命令   
  	SPI2_ReadWriteByte((u8)((ReadAddr)>>16));  	//发送24bit地址    
  	SPI2_ReadWriteByte((u8)((ReadAddr)>>8));   
  	SPI2_ReadWriteByte((u8)ReadAddr);   
  	for(i=0;i<NumByteToRead;i++)
	{ 
      	pBuffer[i]=SPI2_ReadWriteByte(0XFF);   	//循环读数  
  	}
	W25QXX_CS=1;  				    	      
}

//页面编程指令允许从一个字节到256字节(一页)的数据进行编程
//一个扇区分为16页(1扇区=4096字节=16*256)
//在指定地址开始写入最大256字节的数据
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)
//NumByteToWrite:要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!	 
void W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)
{
 	u16 i;  
  	W25QXX_Write_Enable();                  	//SET WEL 
	W25QXX_CS=0;                            	//使能器件   
  	SPI2_ReadWriteByte(W25X_PageProgram);      	//发送写页命令   
  	SPI2_ReadWriteByte((u8)((WriteAddr)>>16)); 	//发送24bit地址    
  	SPI2_ReadWriteByte((u8)((WriteAddr)>>8));   
  	SPI2_ReadWriteByte((u8)WriteAddr);   
  	for(i=0;i<NumByteToWrite;i++) SPI2_ReadWriteByte(pBuffer[i]);//循环写数  
	W25QXX_CS=1;                            	//取消片选 
	W25QXX_Wait_Busy();					   		//等待写入结束
} 

//写SPI FLASH  
//在指定地址开始写入指定长度的数据
//该函数带擦除操作!
//pBuffer:数据存储区
//WriteAddr:开始写入的地址(24bit)						
//NumByteToWrite:要写入的字节数(最大65535)   
u8 W25QXX_BUFFER[4096];	//全局变量,在SRAM,用于缓冲暂存	 
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   
{ 
	u32 secpos;
	u16 secoff;
	u16 secremain;	   
 	u16 i;    
	u8 * W25QXX_BUF;	  
  	W25QXX_BUF=W25QXX_BUFFER;	     
 	secpos=WriteAddr/4096;//扇区地址  
	secoff=WriteAddr%4096;//在扇区内的偏移
	secremain=4096-secoff;//扇区剩余空间大小(字节)   
 	//printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//测试用
 	if(NumByteToWrite<=secremain) secremain = NumByteToWrite;//不大于4096个字节
	while(1) 
	{	
		W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//读出整个扇区的内容
		for(i=0;i<secremain;i++)//校验数据
		{
			if(W25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除  	  
		}
		if(i<secremain)//需要擦除
		{
			W25QXX_Erase_Sector(secpos);		//擦除这个扇区
			for(i=0;i<secremain;i++)	   		//复制
			{
				W25QXX_BUF[i+secoff]=pBuffer[i];	  
			}
			W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096);//写入整个扇区  

		}else W25QXX_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;		//下一个扇区可以写完了
		}	 
	}	 
}

  正点原子的STM32战舰版,W25Q128和SPI2连在一起,同时在出厂时他们预留了一些文件在该FLASH里了,他们将 W25Q128 的前 12M 字节给 FATFS 管理(用做本地磁盘,是的,里面已经有了FATFS的格式,不过需要对该文件系统进行读写的话还要移植代码来实现),随后,紧跟字库结构体、UNIGBK.bin、和三个字库,这部分内容首地址是:(1024*12)*1024,大小约 3.09M,最后还剩下约 0.9M 给用户自己用。因此在下面的main测试函数里,我们可以将少量数据写入到倒数第100个字节,不会对原有文件造成影响。

main.c

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"	 
#include "w25qxx.h"	 
 				 	
//要写入到W25Q64的字符串数组
const u8 TEXT_Buffer[]={"WarShipSTM32 SPI TEST"};
#define SIZE sizeof(TEXT_Buffer)

int main(void)
{	 
	u8 key;
	u16 i=0;
	u8 datatemp[SIZE];
	u32 FLASH_SIZE=16*1024*1024; //总共16M的大小
	 
	delay_init();	    	 //延时函数初始化	  
  	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 	//串口初始化为115200
	LED_Init();		  		//初始化与LED连接的硬件接口
	KEY_Init();				//按键初始化		 	 	
	W25QXX_Init();			//W25QXX初始化
	
	printf("KET0: 读出\tKEY1: 写入\r\n\n");
	while(W25QXX_ReadID()!=W25Q128)	LED0 = 0;
	  
	while(1)
	{
		key=KEY_Scan(0);
		if(key==KEY1_PRES)	//KEY1按下,写入W25QXX
		{
			W25QXX_Write((u8*)TEXT_Buffer,FLASH_SIZE-100,SIZE);	//从倒数第100个地址处开始,写入SIZE长度的数据
			printf("写入数据:%s,长度: %d\r\n\r\n",TEXT_Buffer,SIZE);
		}
		if(key==KEY0_PRES)	//KEY0按下,读取字符串并显示
		{
			W25QXX_Read(datatemp,FLASH_SIZE-100,SIZE);					//从倒数第100个地址处开始,读出SIZE个字节
			printf("读出的数据为:%s,长度: %d\r\n\r\n",datatemp,SIZE);
		}
		i++;
		delay_ms(10);
		if(i==20)
		{
			LED0=!LED0;//提示系统正在运行	
			i=0;
		}		   
	}
}

 运行结果如下:

  从以上的概述来看,存储和读取数据怎么这么困难呢?今天我写入了某些数据在某个单元,明天起床我想再写入数据或者读出数据的时候,地址我忘了怎么办?并且想读取的这份数据又多长(几个字节)我也不知道,这样子就很麻烦。在以后实际的应用中,其实还会涉及到以怎样的格式进行读取FLASH,比如电脑中一些文件都有默认的打开方式,打开mp3文件就自动播放音乐,mp4就是视频,过程其实就是将FLASH读取出来的数据直接当作需要解码的数据,这样就会很快。如果仅仅是上面这样子存储数据在FLASH,那就太难了。

  有一个思路:可以将W25Q128中前面一部分扇区专门用来存储有关文件信息,比如文件存在哪里,有多长,文件的内容数据再存储到其他的扇区,这样一来,以后读取一个文件前,可以先读取前面的扇区取出有关文件的信息。

  文件系统就是专门用来管理这些文件的储存的,它统筹了每个文件存储的位置、大小长度、格式等记录信息,说白了就是对数据进行管理的方式。使用文件系统可有效地管理存储介质。首次使用文件系统时,先对存储介质进行格式化(类似于划定说哪一部分负责什么什么),即在存储介质建立了一些组织结构,这些结构包括操作系统引导区、目录和文件,也就是文件分配表和目录了。

  先看看FatFs移植到stm32后得样子,以前保存数据到FLASH,都是要一次一次得保存,裸着的数据,现在有了文件系统,就能够和在电脑上一样用代码对文件操作了

  存储一份数据时,都以文件的形式存储,此时可以将文件理解为“特定格式说明信息+原本的数据”,文件系统先在目录中创建一个文件索引,它指示了文件存放的物理地址,再把数据存储到该地址中。当需要读取数据时,可以从目录中找到该文件的索引,进而在相应的地址中读取出数据。这个读写过程并不是和文首提到的那样直接读写,而是要遵循文件系统的读写格式。如经过逻辑转换,一个完整的文件可能被分开成多段存储到不连续的物理地址,使用目录或链表的方式来获知下一段的位置。

  簇,可以理解为扇区,每一部分的扇区都有自己特定的任务使命,关于目录比较容易理解,只是简单的一个索引,它指明了一个文件的基本信息和它的存储位置起始地址,和存储的扇区空间大小,那结束地址呢?是不是起始地址加上大小就是结束地址呢?并不是,如前文提到的一个完整的文件可能被分开成多段存储到不连续的物理地址。这时候就用到了文件分配表,它非常重要。

  接上图,B文件大小为54个扇区,这是假如将它删除,则在A文件和C文件之间就会有空出54个扇区,下一次当要保存新文件比如D文件大小为61个扇区,这时候文件系统就会将D文件拆分成两部分,一部分存在A和C之间,另一部分则存在C的后面。这就是文件分配表的魅力所在。目录记录了某个文件存储在哪一个扇区,而文件分配表则记录了某个扇区存储了哪些文件。

  文件分配表记录了第一簇记录的是目录,第二簇记录的是数据,并且读完第二簇之后指引系统接着读取第三簇的数据,第三簇记录的是数据并且指引到第四簇,以此类推,直到读完第11簇,指引指向FF,表示为空,此时文件读取结束。

  B文件删除之前的文件分配表长这个样子:

  删除B文件之后并新建了D文件后的文件分配表长这个样子:读D文件时,当第65簇读完了,则接着读第87个簇。

  具体如何通过文件分配表和目录来读取出一个文件里的数据,然后目录里的哪些字节代表什么之类的这些细节,这就是用代码来实现并管理,本文介绍的是嵌入式中常用的FatFs文件系统(理解白了,它其实就是一些代码 .c .h 文件)。

  FatFs文件系统源码下载地址

  利用前面写好的SPI Flash芯片驱动(即对W25Q128进行读写),把FatFs文件系统代码移植到工程之中,就可以利用文件系统的各种函数,对SPI Flash芯片以“文件”格式进行读写操作了。

  • 最顶层是应用层,使用者无需理会 FATFS 的内部结构和复杂的 FAT 协议,只需要调用FATFS 模块提供给用户的一系列应用接口函数,如 f_open,f_read,f_write 和 f_close 等,就可以像在 PC 上读/写文件那样简单。
  • 中间层 FATFS 模块,实现了 FAT 文件读/写协议。FATFS 模块提供的是 ff.c 和 ff.h。除非有必要,使用者一般不用修改,使用时将头文件直接包含进去即可。
  • 对于底层接口则需要我们自己来编写,因为对存储媒介读/写接口(disk I/O)有比如IIC读取,SPI读取,具体读什么芯片和怎么读这些操作则根据自己的情况来写。比如我们的是SPI FLASH,关于W25Q128的读写我们在上面已经能够实现,则接下来要做的就是把这些读写、初始化等W25Q128函数“绑定”到FatFs中就好了。

  比如调用f_write函数时,FatFs就会调用到disk_write函数,需要用户自己实现要写入的数据、写入哪个扇区、使用多少扇区、扇区有多大等。

  以下是具体的移植过程,需要将FATFS文件加载到工程直接编译时会报错,没关系,跟着下面修改diskio.c和ffconf.f就好。

  先将文件导入到工程,并包含头文件路径

  
 将diskio.c更改成下面的,正点原子的例程是强制将其扇区定义为 512 字节,这样带来的好处就是设计使用相对简单,但同时写入相同的数据里量却增加了擦除次数,频繁写的话,很容易将 SPI FLASH 写坏。因此下面是以原本一个扇区为4K这样子来操作的,其占用的内存为前12M。

#include "diskio.h"		/* FatFs lower layer API */
#include "w25qxx.h"		/*包含对flash初始化、读写相关函数*/
#include "usart.h"	 


//以两个设备为例,SD卡的先不写,只是让系统知道有两个选择,在应用层指明用哪个就行
#define SD_CARD   0		//存储介质为SD卡
#define W25QXX    1		//存储介质为FLASH

#define FLASH_SECTOR_SIZE 	4096  //一个扇区的大小			  
//前12M字节给fatfs用,12M字节后,用于存放字库,字库占用3.09M.	剩余部分,给客户自己用	 			    
#define	FLASH_SECTOR_COUNT 3072;	//总扇区数:256*16个,FATFS占12M,即(12/16)*256*16=3072
#define FLASH_BLOCK_SIZE   	16     	//每个BLOCK有16个扇区


/*-----------------------------------------------------------------------*/
/* Get Drive Status  获取设备的状态,比如  初始化是否成功                   */
/*-----------------------------------------------------------------------*/
DSTATUS disk_status (
	BYTE pdrv		/* Physical drive nmuber to identify the drive */
)
{
	DSTATUS stat;

	switch (pdrv) {
	case SD_CARD :
		//result = ATA_disk_status();//暂时还不用SD卡
		return stat;

	case W25QXX:
		{
			//通过读取FLASH的ID判断是否正常
			if(W25QXX_ReadID()!= W25Q128)
			{
				//状态不正常
				stat = STA_NOINIT;//#define STA_NOINIT	0x01
			}
			else
			{
				//状态正常
				stat = 0;		
			}
			return stat;
		}
	}
	return STA_NOINIT;
}

/*-----------------------------------------------------------------------*/
/* Inidialize a Drive   使用f_mount挂载文件系统时时,就调用到这个函数,把W25Q128顺便初始化了          */
/*-----------------------------------------------------------------------*/
DSTATUS disk_initialize (
	BYTE pdrv				/* Physical drive nmuber to identify the drive */
)
{
	DSTATUS stat;

	switch (pdrv) {
	case SD_CARD :
		//result = ATA_disk_initialize();
		return stat;

	case W25QXX :
		W25QXX_Init();
	  W25QXX_WAKEUP();//防止FLASH设备处于低功耗状态
		return  disk_status(W25QXX);//通过读取设备ID来检查设备是否初始化正常
	}
	return STA_NOINIT;
}


//读扇区
//pdrv:磁盘编号0~9
//*buff:数据接收缓冲首地址
//sector:扇区编号
//count:需要读取的扇区数
DRESULT disk_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 */
)
{
	DRESULT res;

	switch (pdrv) {
	case SD_CARD :
		//result = ATA_disk_read(buff, sector, count);
		return res;

	case W25QXX :
		//FatFs使用扇区编号访问扇区
		//扇区是有编号的,0号扇区对应的地址为0~4096,1号扇区对应地址4096~8192,。。。
	  W25QXX_Read(buff, sector*FLASH_SECTOR_SIZE, count*FLASH_SECTOR_SIZE);
	  res = RES_OK;//这里只是简单的,读函数可以自行设置返回值
		return res;
	}
	return RES_PARERR;
}

//写扇区
//pdrv:磁盘编号0~9
//*buff:发送数据首地址
//sector:扇区编号
//count:需要写入的扇区数
#if _USE_WRITE
DRESULT disk_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 */
)
{
	DRESULT res;

	switch (pdrv) {
		case SD_CARD :
			return res;

		case W25QXX :
			//W25QXX_Erase_Sector(sector*FLASH_SECTOR_SIZE);//一定要先擦除再写入
			//不能使用 writePageProgram,要能够支持多页写入的
		  W25QXX_Write((u8*)buff,sector*FLASH_SECTOR_SIZE, count*FLASH_SECTOR_SIZE);
	    res = RES_OK;
			return res;
	}
	return RES_PARERR;
}
#endif

/*-----------------------------------------------------------------------*/
/*用来格式化的函数,通过cmd与底层flash交互,获取flash信息等                            */
/*-----------------------------------------------------------------------*/
#if _USE_IOCTL
DRESULT disk_ioctl (
	BYTE pdrv,		/* Physical drive nmuber (0..) */
	BYTE cmd,		/* Control code */
	void *buff		/* Buffer to send/receive control data */
)
{
	DRESULT res;

	switch (pdrv) {
	case SD_CARD :
		return res;

	case W25QXX :
	{
		switch(cmd)
		{
			case CTRL_SYNC:
				res = RES_OK; 
		    break;
			
			//返回扇区个数
			case GET_SECTOR_COUNT:
				//typedef unsigned long	DWORD;
				*(DWORD*)buff = FLASH_SECTOR_COUNT;//12M空间用于fatfs
			  res = RES_OK;
				break;
			
			//返回每个扇区大小
			case GET_SECTOR_SIZE:
				*(WORD*)buff = FLASH_SECTOR_SIZE;
			  res = RES_OK;
				break;
			
			//返回擦除扇区的最小个数(单位为扇区)
			case GET_BLOCK_SIZE:
			 *(WORD*)buff = 1;
			  res = RES_OK;
				break;
			default:
		    res = RES_PARERR;
		    break;
		}	
		return res;
	}
 }
 return RES_PARERR;
}
#endif

//返回时间,这里没用到,直接返回一个值
DWORD get_fattime (void)
{

	return 0;
}

ffconf.f:跟着下面的更改即可,同时也加入了cc936.c文件,主要提供 UNICODE 到 GBK 以及 GBK 到 UNICODE 的码表转换,里面就是两个大数组,并提供一个 ff_convert 的转换函数,供 UNICODE 和 GBK 码互换,这个在中文长文件名支持的时候必须用到。该部分编码数组(很大)会保存到内部的内存,所以烧录代码的时间明显增长(后面可以采用将编码数组放在w25q128中)。

#define _FFCONF 64180	/* Revision ID */

#define _FS_READONLY	0

#define _FS_MINIMIZE	0

#define	_USE_STRFUNC	1 //为了使用f_printf写函数

#define _USE_FIND		0

#define	_USE_MKFS		1 //使能格式化功能

#define	_USE_FASTSEEK	1 //使能或禁用快速搜索功能,使能后,可以加快f_lseek、f_read和f_write函数执行。

#define _USE_LABEL		0

#define	_USE_FORWARD	0

#define _CODE_PAGE	936 //中文编码

#define	_USE_LFN	1
#define	_MAX_LFN	255

#define	_LFN_UNICODE	0

#define _STRF_ENCODE	3

#define _FS_RPATH	0

#define _VOLUMES	2 //两个存储介质,一个是SD_CARD,一个是flash,默认为1的

#define _STR_VOLUME_ID	0
#define _VOLUME_STRS	"RAM","NAND","CF","SD1","SD2","USB1","USB2","USB3"

#define	_MULTI_PARTITION	0

#define	_MIN_SS		512
#define	_MAX_SS		4096//因为FATFS.win[_MAX_SS] 和 case GET_SECTOR_SIZE: *(WORD*)buff = 4096;

#define	_USE_TRIM	0

#define _FS_NOFSINFO	0

#define	_FS_TINY	0

#define _FS_NORTC	0
#define _NORTC_MON	1
#define _NORTC_MDAY	1
#define _NORTC_YEAR	2015

#define	_FS_LOCK	0

#define _FS_REENTRANT	0
#define _FS_TIMEOUT		1000
#define	_SYNC_t			HANDLE

#define _WORD_ACCESS	0

main.c:因为出厂即带有了FATFS系统,因此第一次挂载时按理论来说会挂载失败,因为其定义为512字节一个扇区,而代码是4K一个扇区,因此应该会重新格式化,并在第二次挂载成功,如果不成功,可以在diskio.c的写函数中增加擦除的函数。

#include "led.h"
#include "delay.h"
#include "key.h"
#include "sys.h"
#include "usart.h"	 
#include "w25qxx.h"
#include "ff.h"
#include <string.h>
 				 	
FATFS fsObject;//一个很大的结构体对象,fatfs依赖它存储文件系统运行时的一些状态、数据等
FIL fp; //文件句柄
const char wData[]="哇咔咔,FatFs真好用!";
char rData[4096]="";//读取flash接收缓存
UINT bw;//写入成功的数据长度
UINT br;//读取成功的数据长度

FRESULT getDiskInfo(void)
{
  FATFS *pfs;
  DWORD fre_clust, fre_sect, tot_sect;
	FRESULT res_flash;                /* 文件操作结果 */
	char readbuffer[512];	
  
  printf("\n*************** 设备信息获取 ***************\r\n");
  /* 获取设备信息和空簇大小 */
  res_flash = f_getfree("1:", &fre_clust, &pfs);

  /* 计算得到总的扇区个数和空扇区个数 */
  tot_sect = (pfs->n_fatent - 2) * pfs->csize;//n_fatent为簇的数目+2
  fre_sect = fre_clust * pfs->csize;

  /* 打印信息(4096 字节/扇区) */
  printf("》设备总空间:%10lu KB。\n》可用空间:  %10lu KB。\n", tot_sect *4, fre_sect *4);
  
  printf("\n******** 文件定位和格式化写入功能测试 ********\r\n");
  res_flash = f_open(&fp, "1:FatFs读写测试文件.txt",FA_OPEN_ALWAYS|FA_WRITE|FA_READ);
	if ( res_flash == FR_OK )
	{
    /*  文件定位到文件末尾 */
    res_flash = f_lseek(&fp,f_size(&fp));
    if (res_flash == FR_OK)
    {
      /* 格式化写入,参数格式类似printf函数 */
      f_printf(&fp,"在原来文件新添加一行内容\n");
      f_printf(&fp,"》正在测试,设备总空间:%10lu KB。\t可用空间:  %10lu KB。\n", tot_sect *4, fre_sect *4);
      /*  文件定位到文件起始位置 */
      res_flash = f_lseek(&fp,0);
      /* 读取文件所有内容到缓存区 */
      res_flash = f_read(&fp,readbuffer,f_size(&fp),&br);
      if(res_flash == FR_OK)
      {
        printf("》文件内容:%s",readbuffer);
      }
    }
    f_close(&fp);
		//f_unlink("1:/FatFs读写测试文件.txt");		//删除某个文件
	}		
  return res_flash;
}

FRESULT scan_files(char* path) 
{ 
  FRESULT res; 		//部分在递归过程被修改的变量,不用全局变量	
  FILINFO fno; 
  DIR dir; 
  int i;            
  char *fn;        // 文件名	
	
#if _USE_LFN 
  /* 长文件名支持 */
  /* 简体中文需要2个字节保存一个“字”*/
  static char lfn[_MAX_LFN*2 + 1]; 	
  fno.lfname = lfn; 
  fno.lfsize = sizeof(lfn); 
#endif 
  //打开目录
  res = f_opendir(&dir, path); 
  if (res == FR_OK) 
	{ 
    i = strlen(path); 
    for (;;) 
		{ 
      //读取目录下的内容,再读会自动读下一个文件
      res = f_readdir(&dir, &fno); 								
      //为空时表示所有项目读取完毕,跳出
      if (res != FR_OK || fno.fname[0] == 0) break; 	
#if _USE_LFN 
      fn = *fno.lfname ? fno.lfname : fno.fname; 
#else 
      fn = fno.fname; 
#endif 
      //点表示当前目录,跳过			
      if (*fn == '.') continue; 	
      //目录,递归读取      
      if (fno.fattrib & AM_DIR)         
			{ 			
        //合成完整目录名        
        sprintf(&path[i], "/%s", fn); 		
        //递归遍历         
        res = scan_files(path);	
        path[i] = 0;         
        //打开失败,跳出循环        
        if (res != FR_OK) 
					break; 
      } 
			else 
			{ 
				printf("%s/%s\r\n", path, fn);								//输出文件名	
        /* 可以在这里提取特定格式的文件路径 */        
      }//else
    } //for
  } 
  return res; 
}
	
int main(void)
{	
	FRESULT res;
	u8 key;
	uint16_t i=0;
	 
	delay_init();	    	 //延时函数初始化	  
  NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 	//串口初始化为115200
	LED_Init();		  		//初始化与LED连接的硬件接口
	KEY_Init();				//按键初始化		 	 	
	
	/*
	FRESULT f_mount (
	  FATFS*       fs,    
	  const TCHAR* path,  //路径
	  BYTE         opt    
	);
	//FRESULT是返回类型,一个枚举变量,表示文件系统挂载时的多种状态,0表示正常
	*/
	res = f_mount(&fsObject,"1:",1); //挂载文件系统,"1:"表示1号设备(0是SD_CARD),1表示立即挂载(默认即可)
	printf("\r\n挂载状态(0:成功,其他:失败): res =%d\r\n\r\n",res);
	
	if(res == FR_NO_FILESYSTEM)//存储介质还没有格式化(类似于文件分配表和目录的初始结构)
	{
		//第一个参数1号存储介质,即路径
		//第二个参数0表示使用usb、SD卡、flash等,如果用1表示软盘(现在基本不用了)
		//第三个参数0表示自动分配簇的大小
		//格式化会将存储介质上的数据全都擦除
		res = f_mkfs("1:",0,0);//格式化存储介质,让存储介质具有一定的文件系统结构
		printf("\r\n格式化状态(0:成功,其他:失败): res =%d",res);
		//格式化后要取消挂载再重新挂载文件系统
		res = f_mount(NULL,"1:",1);
		res = f_mount(&fsObject,"1:",1);
		printf("\r\n第二次挂载状态(0:成功,其他:失败): res =%d\r\n\r\n",res);
	}
	if(!res)
	{
		getDiskInfo();
		printf("key_WAKE_UP:删除测试文件\tkey0:查看目录\tkey1:写入\tkey2:读出\r\n\r\n");
		while(1)
		{
			key=KEY_Scan(0);
			if(key==WKUP_PRES)
			{
				res = f_unlink("1:/FatFs读写测试文件.txt");		//删除某个文件
				if(!res) printf("删除成功!\r\n");
				else printf("删除失败!\r\n");
			}
			if(key==KEY0_PRES)
			{
				scan_files("1:");
			}
			if(key==KEY1_PRES)//按键1,写入文件
			{
				//FA_WRITE:以写的方式打开文件
				//FA_OPEN_ALWAYS:如果文件不存在,则新建;如果存在,直接打开
				res = f_open(&fp,"1:a.txt",FA_OPEN_ALWAYS|FA_WRITE);
				if(res == FR_OK)
				{
					res = f_write(&fp,wData,sizeof(wData),&bw);
					if(res == FR_OK)
					{
						printf ("\r\n写入文件:%s\t长度:%d\r\n",wData,bw);
					}
				}
				f_close(&fp);
			}
			if(key==KEY2_PRES)//按键2,读取文件
			{
				res = f_open(&fp,"1:a.txt",FA_READ|FA_OPEN_ALWAYS);
				if(res == FR_OK)
				{
					//f_size(&fp)读取整个文件的所有内容
					res = f_read (&fp,rData,f_size(&fp),&br);
					if(res == FR_OK)
					{
						printf ("\r\n读出文件:%s 长度= %d\r\n",rData,br);
					}
				}
				f_close(&fp);
			}
			i++;
			delay_ms(10);
			if(i==20)
			{
				LED0 = !LED0;//提示系统正在运行	
				i=0;
			}
		}
	}
	while(1){LED0 = 0;}
}

  将上面的代码改完就可以烧录了,运行结果如下:

  上面的工程下载链接为:移植后工程2(FATFS基础功能测试).rar【提取码: 4z28】



 下面是打开文件时的一些模式:

 假如w25q128原有文件被破坏了,下面是不用SD卡进行恢复的方法(W25Q128恢复默认出厂文件,需要4.3寸电容触摸屏辅助,作为显示)

  • 测试文件是否被损坏:先下载这个工程文件,实验54 综合测试实验.rar【提取码: 59k8】 ,将USB线接入到USB SLAVE口(不是常用的USB 232),然后烧录到开发板,系统文件和字库文件都是存在 SPI FLASH(W25Q128)里面的,如这些文件被破坏了,在启动的时候,会提示 Font Error / SYSTEM File Error。
  • 根据提示,按 KEY1选择使用 USB 更新 SPI FLASH Files,等待系统提示:USB Connected 后,电脑会找到一个:ALIENTEK 的磁盘,然后将:光盘根目录-》SD 卡根目录文件-》SYSTEM 文件夹,拷贝SYSTEM 文件夹到 ALIENTEK 磁盘根目录。
  • 按 KEY0,此时系统自动重启,并自动更新所有文件(全自动,无需用户干预),等所有操作完成后,即可进入主界面。

猜你喜欢

转载自blog.csdn.net/fengge2018/article/details/107742243