STM32F407移植FATFS文件系统(版本 R0.09b)到SD卡(硬件SPI总线)

46 篇文章 22 订阅

一、序言

    经常在网上、群里看到很多人问关于STM32的FATFS文件系统移植的问题,刚好自己最近的工程项目需要使用SD卡,为了让大家少走弯路,我把我的学习过程和方法贡献给大家。

二、SD卡简介

    安全数字卡(简称SD卡),最初引进应用于手持式可携带电子产品,在一个小尺寸产品上可靠的存储数据,如移动电话,数码相机等。

     1、SD卡简介请参考如下博文

            https://blog.csdn.net/ba_wang_mao/article/details/108665692

    2、SD卡种类请参考如下博文

            https://blog.csdn.net/kris_fei/article/details/77150967

   3、SD卡简介和种类请参考如下博文

             https://blog.csdn.net/ba_wang_mao/article/details/108467489

   4、MMC、SD、TF、SDIO、SDMMC简介

             https://blog.csdn.net/ba_wang_mao/article/details/109503880

三、SD卡总线协议简介

         SD卡支持2种总线协议,即SDIO总线协议和SPI总线协议。SDIO总线协议速度快,SPI总线相对SDIO总线速度要慢很多,但是目前市面上很多单片机不支持SDIO总线协议,只有中高端单片机(例如:STM32F407)才支持SDIO总线协议。

      1、SDIO总线协议

         利用该总线协议,可以使用最多四条数据线实现主机与SD卡之间的数据传输,所以速度相对而言可以达到最高但是需要主机具有SDIO控制器,才可以使用该协议。

      2、SPI总线协议

         如果主机不支持SDIO协议,那么可以使用SPI协议对SD卡进行操作。虽然速度比SDIO慢,但是硬件上更加简单,只需要四根线便可以实现与SD卡进行通讯。

     3、SDIO协议与SPI协议的比较

       SDIO协议与SPI协议相较而言,SDIO协议读写SD卡的速度更快,再加上其支持4线模式,即利用4条数据线,同时发送4Bits数据,数据的传输效率就更高了,但是由于使用的引脚较多,所以也导致了控制相对比较困难。
       而SPI外设只具有两条数据线MISO和MOSI,分别用作数据的输入和输出,由于引脚较少,所以控制相对较容易。但是,数据的传输效率相对而言就比较低了。
但是,两中协议的共同之处在于:均是通过命令实现对SD卡的控制,仍然是结合状态机实现编程。

      4、SD卡如何工作在SPI模式下

      当SD卡上电之后,只有第一次发送的CMD0命令才可以选择SD卡工作在SPI模式下。这意味着,当SD卡处于SPI模式下时,仅能通过重新上下电,才能再次选择SD卡的通讯模式,即选择SDIO模式或者SPI模式,否则SD卡将一直处于SPI模式下。并且,SPI模式下的SD卡不支持 V2.0版本之后新增的命令。

      5、SD卡 SPI模式读写要点

       5.1 上电时要延时足够长的时间给 SD 卡一个准备过程,在我的单片机程序里是5秒(即5秒后才能调用SD卡初始化程序)。根据不同的卡设置不同的延时时间。 SD 卡初始化第一步在发送 CMD 命令之前,在片选有效的情况下首先要发送至少 74 个时钟,否则将有可能出现 SD 卡不能初始化的问题。

       5.2. SD 卡发送复位命令 CMD0 后,要发送版本查询命令 CMD8 ,返回状态一般分两种,若返回 0x01 表示此 SD 卡接受 CMD8, 也就是说此 SD 卡支持版本 2 ;若返回 0x05 则表示此 SD 卡支持版本 1 。因为不同版本的 SD 卡操作要求有不一样的地方,所以务必查询 SD 卡的版本号,否则也会出现 SD 卡无法正常工作的问题。

       5.3. 理论上要求发送 CMD58 获得 SD 卡电压参数,但实际过程中由于事先都知道了 SD 卡的工作电压,因此可省略这一步简化程序。协议书上也建议尽量不要用这个命令。

      5.4. SD 卡读写超时时间要按照协议说明书书上的给定值 ( 读超时: 100ms ;写超时: 250ms) ,这个值要在程序中准确计算出来,否则将会出现不能正常读写数据的问题。我自己定义了一个计算公式:超时时间 =(8/clk )*arg 。

     5.5. 2GB 以内的 SD 卡 ( 标准卡 ) 和 2GB 以上的 SD 卡 ( 大容量卡 ) 在地址访问形式上不同,这一点尤其要注意,否则将会出现无法读写数据的问题。如标准卡在读写操作时,对读或写命令令牌当中的地址域符初值0x10 ,表示对第 16 个字节以后的地址单元进行操作 ( 前提是此 SD 卡支持偏移读写操作 ) ,而对大容量卡读或写命令令牌当中的地址域符初值 0x10 时,则表示对第 16 块进行读写操作,而且大容量卡只支持块读写操作,块大小固定为 512 字节,对其进行字节操作将会出错。

    5.6. 对某一块要进行写操作时最好先执行擦出命令,这样写入的速度就能大大提高。进行擦除操作时不管是标准卡还是大容量卡都按块操作执行,也就是一次擦除至少 512 字节。

   5.7.  对标准卡进行字节操作时,起始和终止必须在一个物理扇区内,否则将不能进行读写操作。实际操作过程中建议用块操作以提高效率。不管是标准卡还是大容量卡一个读写命令只能对一个块进行操作,不允许跨物理层地址操作。

   5.8. 在写数据块前要先写入若干个 dummy data 字节,写完一个块数据时,主机要监测 MISO 数据线,如果从机处于忙状态这根数据线会保持低电平,这样主机就可以根据这根数据线的状态以决定是否发送下一个命令,在从机没有释放 MISO 数据线之前,主机绝对不能执行其他命令,否则将会导致写入的数据出错,而且从机也不会响应主机的命令。

   5.9. 在 SPI 模式下,CRC 校验是被忽略的,但依然要求主从机发送 CRC 码,只是数值可以是任意值,一般主机的 CRC 码通常设为 0x00 或 0xFF 。

   5.10. SD卡初始化的时候,SPI速度必须为低速模式,当SD卡初始化成功以后,才能切换到高速模式。

   5.10. SD卡读写时的SPI速度必须要综合硬件考虑,假如:单片机和SD卡之间的连线距离比较长,就不太适合特别高的SPI速度模式,最多适合中等速度模式。

   6、SD卡SPI读写流程参考如下博文

          https://blog.csdn.net/ba_wang_mao/article/details/108455980

   7、SD卡SPI读写时序图参考如下博文

      https://blog.csdn.net/ba_wang_mao/article/details/108467050

   8、深入理解SD卡协议

      https://blog.csdn.net/ba_wang_mao/article/details/109484517

   9、SD卡初始化以及命令详解

       https://blog.csdn.net/ba_wang_mao/article/details/108477845

四、移植前准备工作:编写硬件SPI总线驱动程序

         我使用的是STM32F407IG单片机的SPI1,引脚如下。

                            PA4  ----> NSS   
                            PA5   ---> SCK     --->SPI1
                            PA6   ---> MISO   --->SPI1
                            PA7  --->  MOSI   --->SPI1

          需要编写硬件SPI1初始化程序,硬件SPI读程序,硬件SPI写程序、硬件SPI读写程序,设置SPI高速模式和低速模式程序。

       1、SPI1初始化程序

void SPI1_Configuration(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	SPI_InitTypeDef SPI_InitStructure;

	
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);

	GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_SPI1);
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_SPI1);
	GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_SPI1);
	
	//Configure SPI1 Pins: SCK, MISO and MOSI
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF;
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;	
	GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;
  	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//Configure NSS Pin
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT;
	GPIO_InitStructure.GPIO_OType = GPIO_OType_PP;	
	GPIO_InitStructure.GPIO_PuPd  = GPIO_PuPd_UP;
  	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;		//NSS = PA4
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
  	GPIO_Init(GPIOA, &GPIO_InitStructure);
	GPIO_SetBits(GPIOA, GPIO_Pin_4);//低电平选通SD卡,高电平隔离SD卡

	SPI_I2S_DeInit(SPI1);
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	//双线双向全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;		//主器件
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;	//8位数据长度
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;   		//这里要注意,一定要配置为上升沿数据有效,因为SD卡为上升沿数据有效
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;		//这里要注意,一定要配置为SPI_CPHA_2Edge(数据捕获于第2个时钟沿),参见SD卡协议要求
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;			//NSS信号由外部管脚管理
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;//SPI速度为低速
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	//数据传输的第一个字节为MSB
	SPI_InitStructure.SPI_CRCPolynomial = 7;			//CRC的多项式
	SPI_Init(SPI1,&SPI_InitStructure);
	SPI_Cmd(SPI1,DISABLE);
	SPI_Cmd(SPI1,ENABLE);
}

2、SPI1读写程序


void SPI_WriteByte(uint8_t _ucByte)
{
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE )==RESET);	//等待数据发送寄存器清空
	SPI_I2S_SendData(SPI1 , _ucByte);								//通过SPI发送出去一个字节数据
	while(SPI_I2S_GetFlagStatus(SPI1 , SPI_I2S_FLAG_RXNE )==RESET);	//等待接收到一个数据(接收到一个数据就相当于发送一个数据完毕)
	SPI_I2S_ReceiveData(SPI1);										//返回接收到的数据
}



uint8_t SPI_ReadByte(void)
{
	uint8_t ch;
	
	
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE )==RESET);
	SPI_I2S_SendData(SPI1 , 0xFF);
	while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE )==RESET);
	ch = SPI_I2S_ReceiveData(SPI1);
	return (ch);
}



uint8_t SPI_ReadWriteByte(uint8_t _ucByte)
{  
	uint8_t ch;
	
	
	while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);		// 等待发送缓冲区空
	SPI_I2S_SendData(SPI1, _ucByte);
	while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);		// 等待数据接收完毕
	ch = SPI_I2S_ReceiveData(SPI1);
	return (ch);
}

3、SPI速度模式设置程序

    我们知道SD卡初始化时,必须要设置为SPI低速模式,只有当SD卡初始化完毕,进行文件读写时,才允许SPI高速模式。

    注意:设置SPI高速模式必须要适应你的板子硬件布线要求以及SD卡本身的要求,如果速度太高,则SD卡读写会出现不稳定。我刚开始就将SPI速度设置为8分频时,时常出现读写SD卡不稳定,将SPI速度设置为16分频后,读写SD卡一切正常。


//SPI 速度设置函数
void SPI1_SetSpeed(uint8_t SpeedSet)
{
    SPI_InitTypeDef SPI_InitStructure;
 
 
	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	//双线双向全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;		//主器件
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;	//8位数据长度
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;   		//这里要注意,一定要配置为上升沿数据有效,因为SD卡为上升沿数据有效
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;		//这里要注意,一定要配置为SPI_CPHA_2Edge(数据捕获于第2个时钟沿),参见SD卡协议要求
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;			//NSS信号由外部管脚管理

	switch (SpeedSet)
	{
		case enum_SPI_SPEED_LOW:			
			SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;//设置到低速模式
			break;
		case enum_SPI_SPEED_HIGH:
			SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;	//设置到高速模式
			break;	
		default:
			SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;//设置到低速模式
			break;			
	}

	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;	//数据传输的第一个字节为MSB		
	SPI_InitStructure.SPI_CRCPolynomial = 7;			//CRC的多项式
	SPI_Init(SPI1,&SPI_InitStructure);
	SPI_Cmd(SPI1 , DISABLE);
	SPI_Cmd(SPI1 , ENABLE);
}

 五、移植前准备工作:编写SPI 总线操作SD卡驱动程序

      编写SPI总线操作SD卡驱动程序需要用到刚刚编写的硬件SPI总线驱动程序。

      编写SPI总线操作SD卡驱动程序需要完成如下程序:

1、等待卡准备好 u8 SD_WaitReady(void)

2、等待SD卡回应 u8 SD_GetResponse(u8 Response)

3、从sd卡读取一个数据包的内容  u8 SD_RecvData(u8*buf,u16 len)

4、向sd卡写入一个数据包的内容 512字节 u8 SD_SendBlock(u8*buf,u8 cmd)

5、向SD卡发送一个命令 u8 SD_SendCmd(u8 cmd, u32 arg, u8 crc)

6、获取SD卡的CID信息,包括制造商信息 u8 SD_GetCID(u8 *cid_data)

7、获取SD卡的CSD信息,包括容量和速度信息 u8 SD_GetCSD(u8 *csd_data)

8、获取SD卡的总扇区数(扇区数) u32 SD_GetSectorCount(void)

9、初始化SD卡  u8 SD_Initialize(void)

10、读SD卡   u8 SD_ReadDisk(u8*buf,u32 sector,u8 cnt)

11、写SD卡   u8 SD_WriteDisk(u8*buf,u32 sector,u8 cnt)

下面是我的工程项目中的SPI总线操作SD卡驱动程序


u8  SD_Type=0;//SD卡的类型 



//SD卡初始化的时候,需要低速
void SD_SPI_SpeedLow(void)
{
 	//SPI2_SetSpeed(SPI_BaudRatePrescaler_256);//设置到低速模式
	SPI1_SetSpeed(enum_SPI_SPEED_LOW);
}



//SD卡正常工作的时候,可以配置为高速
void SD_SPI_SpeedHigh(void)
{
	//SPI1_SetSpeed(SPI_BaudRatePrescaler_8);//设置到高速模式
	SPI1_SetSpeed(enum_SPI_SPEED_HIGH);
}



//取消选择,释放SPI总线
void SD_DisSelect(void)
{
	GPIO_SetBits(GPIOA,GPIO_Pin_4);
	SPI_WriteByte(0xff);//提供额外的8个时钟
}



//选择sd卡,并且等待卡准备OK
//返回值:0,成功;1,失败;
u8 SD_Select(void)
{
	GPIO_ResetBits(GPIOA,GPIO_Pin_4);
	if(SD_WaitReady()==0)return 0;//等待成功
	SD_DisSelect();
	return 1;//等待失败
}



//等待卡准备好
//返回值:0,准备好了;其他,错误代码
u8 SD_WaitReady(void)
{
	u32 t=0;
	do
	{
		if (SPI_ReadByte()==0XFF)return 0;//OK
		t++;
	}while(t<0XFFF);//等待 
	return 1;
}



//等待SD卡回应
//Response:要得到的回应值
//返回值:0,成功得到了该回应值
//    其他,得到回应值失败
u8 SD_GetResponse(u8 Response)
{
	u16 Count=0xFFFF;//等待次数
	while ((SPI_ReadByte()!=Response) && Count) Count--;//等待得到准确的回应(等待得到读取数据令牌0xfe)
	if (Count==0)
		return MSD_RESPONSE_FAILURE;	//得到回应失败
	else 
		return MSD_RESPONSE_NO_ERROR;	//正确回应
}



//从sd卡读取一个数据包的内容
//buf:数据缓存区
//len:要读取的数据长度.
//返回值:0,成功;其他,失败;
u8 SD_RecvData(u8*buf,u16 len)
{
	if (SD_GetResponse(0xFE)) return 1;//等待SD卡发回数据起始令牌0xFE
	while(len--)//开始接收数据
	{
		*buf=SPI_ReadByte();
		buf++;
	}
	//下面是2个伪CRC(dummy CRC)
	SPI_WriteByte(0xFF);
	SPI_WriteByte(0xFF);
	return 0;//读取成功
}



//向sd卡写入一个数据包的内容 512字节
//buf:数据缓存区
//cmd:指令
//返回值:0,成功;其他,失败;
u8 SD_SendBlock(u8*buf,u8 cmd)
{
	u16 t;
	if(SD_WaitReady())return 1;//等待准备失效
	SPI_WriteByte(cmd);
	if(cmd!=0XFD)//不是结束指令
	{
		for(t=0; t<512; t++)SPI_WriteByte(buf[t]); //提高速度,减少函数传参时间
		SPI_WriteByte(0xFF);//忽略crc
		SPI_WriteByte(0xFF);
		t=SPI_ReadByte();//接收响应
		if((t&0x1F)!=0x05)return 2;//响应错误
	}
	return 0;//写入成功
}



//向SD卡发送一个命令
//输入: u8 cmd   命令
//      u32 arg  命令参数
//      u8 crc   crc校验值
//返回值:SD卡返回的响应
u8 SD_SendCmd(u8 cmd, u32 arg, u8 crc)
{
	u8 r1;
	u8 Retry=0;
	
	SD_DisSelect();//取消上次片选
	if(SD_Select())return 0XFF;//片选失效
	//发送
	SPI_WriteByte(cmd | 0x40);//分别写入命令
	SPI_WriteByte(arg >> 24);
	SPI_WriteByte(arg >> 16);
	SPI_WriteByte(arg >> 8);
	SPI_WriteByte(arg);
	SPI_WriteByte(crc);
	if(cmd==CMD12)SPI_WriteByte(0xff);//Skip a stuff byte when stop reading
	//等待响应,或超时退出

	Retry=0X1F;
	do
	{
		r1=SPI_ReadByte();
	}
	while((r1&0X80) && Retry--);
	//返回状态值
	return r1;
}



//获取SD卡的CID信息,包括制造商信息
//输入: u8 *cid_data(存放CID的内存,至少16Byte)
//返回值:0:NO_ERR
//		 1:错误
u8 SD_GetCID(u8 *cid_data)
{
	u8 r1;
	//发CMD10命令,读CID
	r1=SD_SendCmd(CMD10,0,0x01);
	if(r1==0x00)
	{
		r1=SD_RecvData(cid_data,16);//接收16个字节的数据
	}
	SD_DisSelect();//取消片选
	if(r1)return 1;
	else return 0;
}



//获取SD卡的CSD信息,包括容量和速度信息
//输入:u8 *cid_data(存放CID的内存,至少16Byte)
//返回值:0:NO_ERR
//		 1:错误
u8 SD_GetCSD(u8 *csd_data)
{
	u8 r1;
	r1=SD_SendCmd(CMD9,0,0x01);//发CMD9命令,读CSD
	if(r1==0)
	{
		r1=SD_RecvData(csd_data, 16);//接收16个字节的数据
	}
	SD_DisSelect();//取消片选
	if(r1)return 1;
	else return 0;
}



//获取SD卡的总扇区数(扇区数)
//返回值:0: 取容量出错
//       其他:SD卡的容量(扇区数/512字节)
//每扇区的字节数必为512,因为如果不是512,则初始化不能通过.
u32 SD_GetSectorCount(void)
{
	u8 csd[16];
	u32 Capacity;
	u8 n;
	u16 csize;
	//取CSD信息,如果期间出错,返回0
	if(SD_GetCSD(csd)!=0) return 0;
	//如果为SDHC卡,按照下面方式计算
	if((csd[0]&0xC0)==0x40)	 //V2.00的卡
	{
		csize = csd[9] + ((u16)csd[8] << 8) + 1;
		Capacity = (u32)csize << 10;//得到扇区数
	}
	else //V1.XX的卡
	{
		n = (csd[5] & 15) + ((csd[10] & 128) >> 7) + ((csd[9] & 3) << 1) + 2;
		csize = (csd[8] >> 6) + ((u16)csd[7] << 2) + ((u16)(csd[6] & 3) << 10) + 1;
		Capacity= (u32)csize << (n - 9);//得到扇区数
	}
	return Capacity;
}



//初始化SD卡
u8 SD_Initialize(void)
{
	u8 r1;      // 存放SD卡的返回值
	u16 retry;  // 用来进行超时计数
	u8 buf[4];
	u16 i;
	

	SPI1_Configuration();
	SD_SPI_SpeedLow();	//设置到低速模式	

    Delay(1000);   //SPI总线初始化延时一会再初始化SD卡 

	for(i=0; i<10; i++)SPI_WriteByte(0XFF); //发送最少74个脉冲
	retry=2000;
	do
	{
		r1=SD_SendCmd(CMD0,0,0x95);//进入IDLE状态
	}
	while((r1!=0X01) && retry--);
	SD_Type=0;//默认无卡
	if(r1==0X01)
	{
		if(SD_SendCmd(CMD8,0x1AA,0x87)==1)//SD V2.0
		{
			for(i=0; i<4; i++)buf[i] = SPI_ReadWriteByte(0XFF);	//Get trailing return value of R7 resp
			if(buf[2]==0X01&&buf[3]==0XAA)//卡是否支持2.7~3.6V
			{
				retry=0XFFFE;
				do
				{
					SD_SendCmd(CMD55,0,0X01);	//发送CMD55
					r1=SD_SendCmd(CMD41,0x40000000,0X01);//发送CMD41
				}
				while(r1&&retry--);
				if(retry&&SD_SendCmd(CMD58,0,0X01)==0)//鉴别SD2.0卡版本开始
				{
					for(i=0; i<4; i++)buf[i] = SPI_ReadWriteByte(0XFF); //得到OCR值
					if(buf[0]&0x40)SD_Type=SD_TYPE_V2HC;    //检查CCS
					else SD_Type=SD_TYPE_V2;
				}
			}
		}
		else //SD V1.x/ MMC	V3
		{
			SD_SendCmd(CMD55,0,0X01);		//发送CMD55
			r1=SD_SendCmd(CMD41,0,0X01);	//发送CMD41
			if(r1<=1)
			{
				SD_Type=SD_TYPE_V1;
				retry=0XFFFE;
				do //等待退出IDLE模式
				{
					SD_SendCmd(CMD55,0,0X01);	//发送CMD55
					r1=SD_SendCmd(CMD41,0,0X01);//发送CMD41
				}
				while(r1&&retry--);
			}
			else
			{
				SD_Type=SD_TYPE_MMC;//MMC V3
				retry=0XFFFE;
				do //等待退出IDLE模式
				{
					r1=SD_SendCmd(CMD1,0,0X01);//发送CMD1
				}
				while(r1&&retry--);
			}
			if(retry==0||SD_SendCmd(CMD16,512,0X01)!=0)SD_Type=SD_TYPE_ERR;//错误的卡
		}
	}
	SD_DisSelect();//取消片选
	SD_SPI_SpeedHigh();//SPI配置为高速模式
	
	if (SD_Type)
		return 0;
	else if(r1)
		return r1;
	return (0xaa);//其他错误
}



//读SD卡
//buf:数据缓存区
//sector:扇区
//cnt:扇区数
//返回值:0,ok;其他,失败.
u8 SD_ReadDisk(u8*buf,u32 sector,u8 cnt)
{
	u8 r1;
	if(SD_Type!=SD_TYPE_V2HC)sector <<= 9;//转换为字节地址
	if(cnt==1)
	{
		r1=SD_SendCmd(CMD17,sector,0X01);//读命令
		if(r1==0)//指令发送成功
		{
			r1=SD_RecvData(buf,512);//接收512个字节
		}
	}
	else
	{
		r1=SD_SendCmd(CMD18,sector,0X01);//连续读命令
		do
		{
			r1=SD_RecvData(buf,512);//接收512个字节
			buf+=512;
		}
		while(--cnt && r1==0);
		SD_SendCmd(CMD12,0,0X01);	//发送停止命令
	}
	SD_DisSelect();//取消片选
	return r1;//
}



//写SD卡
//buf:数据缓存区
//sector:起始扇区
//cnt:扇区数
//返回值:0,ok;其他,失败.
u8 SD_WriteDisk(u8*buf,u32 sector,u8 cnt)
{
	u8 r1;
	if(SD_Type!=SD_TYPE_V2HC)sector *= 512;//转换为字节地址
	if(cnt==1)
	{
		r1=SD_SendCmd(CMD24,sector,0X01);//读命令
		if(r1==0)//指令发送成功
		{
			r1=SD_SendBlock(buf,0xFE);//写512个字节
		}
	}
	else
	{
		if(SD_Type!=SD_TYPE_MMC)
		{
			SD_SendCmd(CMD55,0,0X01);
			SD_SendCmd(CMD23,cnt,0X01);//发送指令
		}
		r1=SD_SendCmd(CMD25,sector,0X01);//连续读命令
		if(r1==0)
		{
			do
			{
				r1=SD_SendBlock(buf,0xFC);//接收512个字节
				buf+=512;
			}
			while(--cnt && r1==0);
			r1=SD_SendBlock(0,0xFD);//接收512个字节
		}
	}
	SD_DisSelect();//取消片选
	return r1;//
}

 SPI总线操作SD卡驱动程序可参考如下博文

        https://blog.csdn.net/ba_wang_mao/article/details/108455941

        https://blog.csdn.net/ba_wang_mao/article/details/108370483

        https://blog.csdn.net/ba_wang_mao/article/details/108330261

 还可参考如下博文:SD卡在SPI模式下的初始化和详细的代码分析

        https://blog.csdn.net/ba_wang_mao/article/details/108475977

六、FATFS文件系统移植

1 、FATFS简介    

FATFS是一个完全免费开源的FAT 文件系统模块,专门为小型的嵌入式系统而设计。它完全用标准C 语言编写,所以具有良好的硬件平**立性,可以移植到8051、PIC、AVR、SH、Z80、H8、ARM 等系列单片机上而只需做简单的修改。它支持FATl2、FATl6 和FAT32,支持多个存储媒介;有独立的缓冲区,可以对多个文件进行读/写,并特别对8 位单片机和16 位单片机做了优化。

FATFS的特点有:

Windows兼容的FAT文件系统(支持FAT12/FAT16/FAT32)

与平台无关,移植简单

代码量少、效率高

多种配置选项

支持多卷(物理驱动器或分区,最多10个卷)

多个ANSI/OEM代码页包括DBCS

支持长文件名、ANSI/OEM或Unicode

支持RTOS

支持多种扇区大小

只读、最小化的API和I/O缓冲区等

FATFS的这些特点,加上免费、开源的原则,使得FATFS应用非常广泛。FATFS模块的层次结构如图所示:

        最顶层是应用层,使用者无需理会FATFS的内部结构和复杂的FAT 协议,只需要调用FATFS模块提供给用户的一系列应用接口函数,如f_open,f_read,f_write 和f_close等,就可以像在PC 上读/写文件那样简单。中间层FATFS模块,实现了FAT 文件读/写协议。FATFS模块提供的是ff.c和ff.h。除非有必要,使用者一般不用修改,使用时将头文件直接包含进去即可。需要我们编写移植代码的是FATFS模块提供的底层接口,它包括存储媒介读/写接口(disk I/O)和供给文件创建修改时间的实时时钟。

2、FATFS的源码

        FATFS的源码,大家可以在:http://elm-chan.org/fsw/ff/00index_e.html 这个网站下载到,目前最新版本为R0.10b。本章我们就使用最新版本的FATFS作为介绍,下载最新版本的FATFS软件包,解压后可以得到两个文件夹:doc和src。doc里面主要是对FATFS的介绍,而src里面才是我们需要的源码。

下载解压后,有两个文件夹,doc文件夹是帮助文档,src里面是是源码。

doc里面很多资料,详细介绍了fatfs系统的架构和使用说明,一些接口函数不明白怎么使用的话可以在里面找到说明。下面介绍src文件:

其中,与平台无关的是:

ffconf.h                 FATFS模块配置文件                               需要我们修改 

ff.h                       FATFS和应用模块公用的包含文件

ff.c                       FATFS模块

diskio.h                 FATFS和disk I/O模块公用的包含文件

interger.h               数据类型定义                                        需要根据单片机型号来决定是否修改(如果是8位单片机则需要修改)

option                   可选的外部功能(比如支持中文等)

与平台相关的代码(需要用户提供)是:

diskio.c                 FATFS和disk I/O模块接口层文件           需要我们修改

FATFS模块在移植的时候,我们一般只需要修改2个文件,即ffconf.h和diskio.c。FATFS模块的所有配置项都是存放在ffconf.h里面,我们可以通过配置里面的一些选项,来满足自己的需求。接下来我们介绍几个重要的配置选项。

1)_FS_TINY。这个选项在R0.07版本中开始出现,之前的版本都是以独立的C文件出现(FATFS和Tiny FATFS),有了这个选项之后,两者整合在一起了,使用起来更方便。我们使用FATFS,所以把这个选项定义为0即可。

2)_FS_READONLY。这个用来配置是不是只读,本章我们需要读写都用,所以这里设置为0即可。

3)_USE_STRFUNC。这个用来设置是否支持字符串类操作,比如f_putc,f_puts等,本章我们需要用到,故设置这里为1。

4)_USE_MKFS。这个用来定时是否使能格式化,本章需要用到,所以设置这里为1。

5)_USE_FASTSEEK。这个用来使能快速定位,我们设置为1,使能快速定位。

6)_USE_LABEL。这个用来设置是否支持磁盘盘符(磁盘名字)读取与设置。我们设置为1,使能,就可以通过相关函数读取或者设置磁盘的名字了。

7)_CODE_PAGE。这个用于设置语言类型,包括很多选项(见FATFS官网说明),我们这里设置为936,即简体中文(GBK码,需要c936.c文件支持,该文件在option文件夹)。

8)_USE_LFN。该选项用于设置是否支持长文件名(还需要_CODE_PAGE支持),取值范围为0~3。0,表示不支持长文件名,1~3是支持长文件名,但是存储地方不一样,我们选择使用3,通过ff_memalloc函数来动态分配长文件名的存储区域。

9)_VOLUMES。用于设置FATFS支持的逻辑设备数目,我们设置为2,即支持2个设备。

10)_MAX_SS。扇区缓冲的最大值,一般设置为512。

其他配置项,我们这里就不一一介绍了,FATFS的说明文档里面有很详细的介绍,大家自己阅读即可。

3、FATFS的移植步骤

下面我们来讲讲FATFS的移植,FATFS的移植主要分为3步:

步骤①  数据类型:在integer.h 里面去定义好数据的类型。这里需要了解你用的编译器的数据类型,并根据编译器定义好数据类型。(一般不需要修改)

步骤②  配置:通过ffconf.h配置FATFS的相关功能,以满足你的需要。(一般不需要修改)

步骤③  函数编写:打开diskio.c,进行底层驱动编写,一般需要编写6 个接口函数,如下图所示(需要移植修改如下图所示的6个接口函数)

通过以上三步,我们即可完成对FATFS的移植。

4、diskio.c需要移植的6个接口函数

/*
功能:设备初始化函数
参数:pdrc是设备号,fatfs系统可以同时挂载多个设备(SD卡、MMC等)
*/
1.  DSTATUS disk_initialize (BYTE pdrv);
/*获取设备状态*/
2.  DSTATUS disk_status (BYTE pdrv);
/*
功能:从设备读取若干个扇区的数据
buff: 读取的数据存放的地址
sector:扇区地址
count:所读取的扇区总个数
*/
3.  DRESULT disk_read (BYTE pdrv, BYTE* buff, DWORD sector, UINT count);
/*
功能:往设备写入若干个扇区的数据
buff: 写入的数据地址
sector:扇区地址
count:所写的扇区总个数
*/
4.  DRESULT disk_write (BYTE pdrv, const BYTE* buff, DWORD sector, UINT count);
/*
功能:设备控制,或获取设备的参数
pdrv:设备号
cmd:命令
buff:发送/接收缓冲区指针
*/
5.  DRESULT disk_ioctl (BYTE pdrv, BYTE cmd, void* buff);

下面是我的diskio.c的移植代码



#define SD_CARD	 0  //SD卡,卷标为0



//初始化磁盘
DSTATUS disk_initialize (
	BYTE drv				/* Physical drive nmuber (0..) */
)
{	
	u8 res=0;
	
	switch (drv)
	{
		case SD_CARD:	//SD卡
			res = SD_Initialize();	//SD卡初始化
		 	if (res)				//STM32 SPI的bug,在SD卡操作失败的时候如果不执行下面的语句,可能导致SPI读写异常
			{
				SD_SPI_SpeedLow();
				SPI_ReadWriteByte(0xFF);//提供额外的8个时钟
				SD_SPI_SpeedHigh();
			}
  			break;
		case 1:	
			res=1;
 			break;
		default:
			res=1; 
	}		 
	
	if (res)return  STA_NOINIT;
	else return 0; //初始化成功
}   



//获得磁盘状态
DSTATUS disk_status (
	BYTE drv		/* Physical drive nmuber (0..) */
)
{		   
    return 0;
}



//读扇区
//drv:磁盘编号0~9
//*buff:数据接收缓冲首地址
//sector:扇区地址
//count:需要读取的扇区数
DRESULT disk_read (
	BYTE drv,		/* Physical drive nmuber (0..) */
	BYTE *buff,		/* Data buffer to store read data */
	DWORD sector,	/* Sector address (LBA) */
	BYTE count		/* Number of sectors to read (1..255) */
)
{
	u8 res=0;
	
    if (!count)return RES_PARERR;//count不能等于0,否则返回参数错误
	switch(drv)
	{
		case SD_CARD:	//SD卡
			res = SD_ReadDisk(buff,sector,count);	 
		 	if (res)//STM32 SPI的bug,在sd卡操作失败的时候如果不执行下面的语句,可能导致SPI读写异常
			{
				SD_SPI_SpeedLow();
				SPI_ReadWriteByte(0xFF);//提供额外的8个时钟
				SD_SPI_SpeedHigh();
			}
			break;
		case 1:
			res=1;
			break;
		default:
			res=1; 
	}
   //处理返回值,将SPI_SD_driver.c的返回值转成ff.c的返回值
    if (res==0x00)return RES_OK;	 
    else return RES_ERROR;	   
}  



 //写扇区
 //drv:磁盘编号0~9
 //*buff:发送数据首地址
 //sector:扇区地址
 //count:需要写入的扇区数	    
#if _READONLY == 0
DRESULT disk_write (
	BYTE drv,			/* Physical drive nmuber (0..) */
	const BYTE *buff,	/* Data to be written */
	DWORD sector,		/* Sector address (LBA) */
	BYTE count			/* Number of sectors to write (1..255) */
)
{
	u8 res=0;
	
    if (!count)return RES_PARERR;//count不能等于0,否则返回参数错误
	switch(drv)
	{
		case SD_CARD:	//SD卡
			res=SD_WriteDisk((u8*)buff,sector,count);
			break;
		case 1:
			res=1;
			break;
		default:
			res=1; 
	}
    //处理返回值,将SPI_SD_driver.c的返回值转成ff.c的返回值
    if(res == 0x00)return RES_OK;	 
    else return RES_ERROR;		 
}
#endif /* _READONLY */



//其他表参数的获得
//drv:磁盘编号0~9
//ctrl:控制代码
//*buff:发送/接收缓冲区指针
DRESULT disk_ioctl (
	BYTE drv,		/* Physical drive nmuber (0..) */
	BYTE ctrl,		/* Control code */
	void *buff		/* Buffer to send/receive control data */
)
{	
	DRESULT res;
	
	if (drv==SD_CARD)	//SD卡
	{
	    switch(ctrl)
	    {
		    case CTRL_SYNC:
				SD_CS=0;
		        if(SD_WaitReady()==0)res = RES_OK; 
		        else res = RES_ERROR;	  
				SD_CS=1;
		        break;	 
		    case GET_SECTOR_SIZE:
		        *(WORD*)buff = 512;
		        res = RES_OK;
		        break;	 
		    case GET_BLOCK_SIZE:
		        *(WORD*)buff = 8;
		        res = RES_OK;
		        break;	 
		    case GET_SECTOR_COUNT:
		        *(DWORD*)buff = SD_GetSectorCount();
		        res = RES_OK;
		        break;
		    default:
		        res = RES_PARERR;
		        break;
	    }
	}
	else res=RES_ERROR;//其他的不支持
    return res;
}



//获得时间
//User defined function to give a current time to fatfs module      */
//31-25: Year(0-127 org.1980), 24-21: Month(1-12), 20-16: Day(1-31) */
DWORD get_fattime (void)
{				 
	return 0;
}

七、验证

          验证前大家需要了解FATFS文件系统函数,可以参考如下链接。

                  FatFS文件系统(版本 R0.09b)所有函数总结    https://blog.csdn.net/ba_wang_mao/article/details/109478655

编写验证程序

char string[100] = "FATFS R0.09b ...";

void DemoFatFS(void)
{
	FRESULT result;
	FATFS fs;
	FIL file;
	DIR DirInf;
	uint32_t bw;
	uint32_t fil_size;
	
	
	
 	/* 挂载文件系统 */
	result = f_mount(FS_SD, &fs);		/* Mount a logical drive */
	if (result != FR_OK)
	{
		//printf("挂载文件系统失败 (%d)\r\n", result);
		return ;
	}

	/* 打开根文件夹 */
	result = f_opendir(&DirInf, "/"); 	/* 如果不带参数,则从当前目录开始 */
	if (result != FR_OK)
	{
		//printf("打开根目录失败 (%d)\r\n");
		/* 卸载文件系统 */
		f_mount(FS_SD, NULL);
		return ;
	}

	/* 打开文件 */	
	result = f_open(&file, "123.csv", FA_OPEN_ALWAYS | FA_WRITE);
	if (result == FR_OK)
	{
		fil_size = f_size(&file);
		result = f_lseek(&file,fil_size);
		if (result == FR_OK)
		{		
			result = f_write(&file, string , strlen(string), &bw);
			if (result == FR_OK)
			{
				//printf("文件写入成功\r\n");
			}//if (result == FR_OK)
		}//if (result == FR_OK)
	}//if (result == FR_OK)
	
	/* 关闭文件*/
	f_close(&file);
	/* 卸载文件系统 */
	f_mount(FS_SD, NULL);
}

       

     

本程序已经在很多工程项目上成功运用。

源程序参见:https://download.csdn.net/download/ba_wang_mao/20319069

  • 9
    点赞
  • 50
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
要在STM32F407移植FATFS文件系统,首先需要了解FATFS是一个开源的文件系统,它可以在嵌入式设备上实现存储器的文件读写操作。 在移植FATFS之前,需要准备一些基本的材料,如软件开发环境、相关的文档和资料。可以从FATFS的官方网站下载最新版本的库文件和相关的示例代码。 首先,创建一个新的工程,并在工程中添加所需的库文件和头文件。将fatfs.c和diskio.c两个源文件添加到工程,并在工程设置中包含相关的头文件目录。 接下来,在工程中添加适配底层硬件的代码,在这里是针对STM32F407的。需要编写适配FATFS的底层I/O读写操作的代码,包括初始化SD卡、读取扇区数据和写入扇区数据。 然后,根据具体需求配置FATFS的参数。可以在fatfs_conf.h文件中修改相关的参数,如簇大小、扇区大小和最大文件数等。这些参数根据实际应用需求进行调整。 最后,编写应用程序代码。可以利用FATFS提供的API来实现文件的打开、读取和写入等操作。需要注意的是在使用FATFS API之前,需要先调用f_mount函数挂载文件系统。 完成以上步骤后,就可以进行编译和烧录操作,将程序下载到STM32F407上进行测试。可以通过读取和写入文件来验证FATFS的正常工作。 总结来说,STM32F407移植FATFS文件系统需要准备相关的材料,包括库文件和文档。然后添加相关的源文件和头文件到工程中,并编写适配底层硬件的代码。之后根据需求配置FATFS参数,并编写应用程序代码。最后进行编译和烧录操作,进行测试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值