FatFs 是面向小型嵌入式系统的一种通用的 FAT 文件系统。它完全是由 ANSI C 语言编写并且完全独立于底层的 I/O 介质。因此它可以很容易地不加修改地移植到其他的处理器当中,如 8051、PIC、AVR、SH、Z80、H8、ARM 等。FatFs 支持 FAT12、FAT16、FAT32 等格式,所以我们利用前面写好的 SPI Flash 芯片驱动,把 FatFs 文件系统代码移植到工程之中,就可以利用文件系统的各种函数,对 SPI Flash 芯片以“文件”格式进行读写操作了。
FatFs 文件系统的源码可以从 fatfs 官网下载: http://elm-chan.org/fsw/ff/00index_e.html
1.1 FatFs文件系统布局簇是文件存储的最小单元,FAT32分区大小与对应簇空间大小关系如下表示:
分区空间大小簇空间大小每个簇包含的扇区数< 8GB4KB8[ 8GB, 16GB )8KB16[ 16GB, 32GB )16KB32= 32GB | 32KB | 64
例如:创建一个50字节的test.txt文件,文件大小是50字节,但是占用磁盘空间为4096字节(一个簇)
1.2 FatFs层次结构最顶层是应用层:使用者只需要调用FATFS模块提供给用户的一系列应用接口函数(如f_open, f_read, f_write和f_close等),就可以像在PC上读写文件那样简单
中间层FATFS模块:实现了FAT文件读写协议;它提供了ff.c和ff.h文件,一般情况下不用修改,使用时将头文件包含进去即可
最底层是FATFS模块的底层接口:包括存储媒介读写接口和供给文件创建修改时间的实时时钟,需要在移植时编写对应的代码
FATFS源码相关文件介绍如下表示;移植FATFS模块时,一般只需要修改2个文件(即ffconf.h和diskio.c)
与平台无关:
文件说明ffconf.hFATFS模块配置文件ff.hFATFS和应用模块公用的包含文件ff.cFATFS模块diskio.hFATFS和disk I/O模块公用的包含文件interger.h数据类型定义option可选的外部功能(比如支持中文)与平台相关:
文件说明diskio.cFATFS和disk I/O模块接口层文件 1.3 FatFs API 1.3.1 f_mount 功能在FatFs模块上注册、注销一个工作区(文件系统对象)函数定义FRESULT f_mount(FATFS* fs, const TCHAR* path, BYTE opt)参数fs:工作区(文件系统对象)指针path:注册/注销工作区的逻辑驱动器号opt:注册或注销选项返回操作结果 1.3.2 f_open 功能创建/打开一个文件对象函数定义FRESULT f_open(FIL* fp, const TCHAR* path, BYTE mode)参数fp:将被创建的文件对象结构的指针path:文件名指针,指定将创建或打开的文件名mode:访问类型和打开方法,由一下标准的一个组合指定的返回操作结果 模式描述FA_READ指定读访问对象。可以从文件中读取数据。 与FA_WRITE 结 合可以进行读写访问。FA_WRITE指定写访问对象。可以向文件中写入数据。与FA_READ 结合 可以进行读写访问。FA_OPEN_EXISTING打开文件。如果文件不存在,则打开失败。(默认)FA_OPEN_ALWAYS如果文件存在,则打开;否则,创建一个新文件。FA_CREATE_NEW创建一个新文件。如果文件已存在,则创建失败。FA_CREATE_ALWAYS创建一个新文件。如果文件已存在,则它将被截断并覆盖。 1.3.3 f_close 功能关闭一个打开的文件函数定义FRESULT f_close(FIL* fp)参数fp:指向将被关闭的已打开的文件对象结构的指针返回操作结果 1.3.4 f_read 功能从一个打开的文件中读取数据函数定义FRESULT f_read(FIL* fp, void* buff, UINT btr, UINT* br)参数fp:指向将被读取的已打开的文件对象结构的指针buff:指向存储读取数据的缓冲区的指针btr:要读取的字节数br:指向返回已读取字节数的UINT变量的指针,返回为实际读取的字节数返回操作结果 1.3.5 f_write 功能写入数据到一个已打开的文件函数定义FRESULT f_write(FIL* fp, void* buff, UINT btw, UINT* bw)参数fp:指向将被写入的已打开的文件对象结构的指针buff:指向存储写入数据的缓冲区的指针btw:要写入的字节数bw:指向返回已写入字节数的UINT变量的指针,返回为实际写入的字节数返回操作结果另外FatFs还有很多API操作函数,在这里不再作详细的介绍,详细信息请查看FatFs文件系统官网。
二、新建工程1. 打开 STM32CubeMX 软件,点击“新建工程”
2. 选择 MCU 和封装
3. 配置时钟 RCC 设置,选择 HSE(外部高速时钟) 为 Crystal/Ceramic Resonator(晶振/陶瓷谐振器) 选择 Clock Configuration,配置系统时钟 SYSCLK 为 72MHz 修改 HCLK 的值为 72 后,输入回车,软件会自动修改所有配置
4. 配置调试模式 非常重要的一步,否则会造成第一次烧录程序后续无法识别调试器 SYS 设置,选择 Debug 为 Serial Wire
三、SPI1 3.1 参数配置在 Connectivity 中选择 SPI1 设置,并选择 Full-Duplex Master 全双工主模式,不开启 NSS 即不使用硬件片选信号
原理图中虽然将 CS 片选接到了硬件 SPI1 的 NSS 引脚,因为硬件 NSS 使用比较麻烦,所以后面直接把 PA4 配置为普通 GPIO,手动控制片选信号。
在右边图中找到 SPI1 NSS 对应引脚,选择 GPIO_Output。纠正:野火STM32F103指南者开发板SPI1 NSS须配置为PC0
修改输出高电平 High,标签为 W25Q64_CHIP_SELECT。
SPI 为默认设置不作修改。只需注意一下,Prescaler 分频系数最低为 4,波特率 (Baud Rate) 为 18.0 MBits/s。这里被限制了,SPI1 最高通信速率可达 36Mbtis/s。
Clock Polarity(CPOL):SPI 通讯设备处于空闲状态时,SCK 信号线的电平信号(即 SPI 通讯开始前、 NSS 线为高电平时 SCK 的状态)。CPOL=0 时, SCK 在空闲状态时为低电平,CPOL=1 时,则相反。Clock Phase(CPHA):指数据的采样的时刻,当 CPHA=0 时,MOSI 或 MISO 数据线上的信号将会在 SCK 时钟线的“奇数边沿”被采样。当 CPHA=1 时,数据线在 SCK 的“偶数边沿”采样。 根据 FLASH 芯片的说明,它支持 SPI 模式0 及 模式 3,支持双线全双工,使用 MSB 先行模式,数据帧长度为 8 位。 所以这里配置 CPOL 为 Low,CPHA 为 1 Edge 即 SPI 模式0。 四、FATFS 4.1 参数配置在 Middleware 中选择 FATFS 设置,并勾选 User-defined 因为 SPI Flash 在上面没有
Function Parameters 跳过
Locale and Namespace Parameters:
CODE_PAGE(Code page on target): Simplified Chinese GBK(DBCS,OEM,Windows) 支持简体中文编码USE_LFN(Use Long Filename): Enabled with dynamic working buffer on the STACK 支持长文件名,并指定使用栈空间为缓冲区缓存工作区为什么放在栈?其实fatfs提供了三个选项:BSS,STACK , HEAP,根据个人情况选一个。 在BSS上启用带有静态工作缓冲区的LFN,不能动态分配。 如果选择了HEAP(堆)且自己有属于自己的malloc就去重写ff_memalloc ff_memfree函数。如果是库的malloc就不需要。 一般都选择使用STACK(栈),能动态分配。 当使用堆栈作为工作缓冲区时,请注意堆栈溢出。
Physical Drive Parameters: VOLUMES(Logical drivers): 2 指定物理设备数量,这里设置为 2,包括预留 SD 卡和 SPI Flash 芯片MAX_SS(Maximum Sector Size): 4096 指定扇区大小的最大值。SD 卡扇区大小一般都为 512 字节,SPI Flash 芯片扇区大小一般设置为 4096 字节,所以需要把 _MAX_SS 改为 4096MIN_SS(Minimum Sector Size): 512 指定扇区大小的最小值 4.2 增大栈空间将最小栈空间改到 0x1000
注意:由于刚才设置长文件名动态缓存存储在堆中,故需要增大栈大小,如果不修改则程序运行时栈会生成溢出,程序进入硬件错误中断(HardFault),死循环。
4.3 生成代码输入项目名和项目路径 选择应用的 IDE 开发环境 MDK-ARM V5 每个外设生成独立的 ’.c/.h’ 文件 不勾:所有初始化代码都生成在 main.c 勾选:初始化代码生成在对应的外设文件。 如 GPIO 初始化代码生成在 gpio.c 中。 点击 GENERATE CODE 生成代码
五、添加SPI Flash操作函数在 user_diskio.c 中加入
//#define SPI_FLASH_PageSize 4096#define SPI_FLASH_PageSize 256#define SPI_FLASH_PerWritePageSize 256#define ManufactDeviceID_CMD 0x90#define READ_STATU_REGISTER_1 0x05#define READ_STATU_REGISTER_2 0x35#define READ_DATA_CMD 0x03#define WRITE_ENABLE_CMD 0x06#define WRITE_DISABLE_CMD 0x04#define SECTOR_ERASE_CMD 0x20#define CHIP_ERASE_CMD 0xc7#define PAGE_PROGRAM_CMD 0x02 #define SPI_FLASH_CS_LOW() HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_RESET);#define SPI_FLASH_CS_HIGH() HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_GPIO_Port, W25Q64_CHIP_SELECT_Pin, GPIO_PIN_SET);extern SPI_HandleTypeDef hspi1; /** * @brief SPI发送指定长度的数据 * @param buf —— 发送数据缓冲区首地址 * @param size —— 要发送数据的字节数 * @retval 成功返回HAL_OK */static HAL_StatusTypeDef SPI_Transmit(uint8_t* send_buf, uint16_t size){ return HAL_SPI_Transmit(&hspi1, send_buf, size, 100);}/** * @brief SPI接收指定长度的数据 * @param buf —— 接收数据缓冲区首地址 * @param size —— 要接收数据的字节数 * @retval 成功返回HAL_OK */static HAL_StatusTypeDef SPI_Receive(uint8_t* recv_buf, uint16_t size){ return HAL_SPI_Receive(&hspi1, recv_buf, size, 100);}/** * @brief SPI在发送数据的同时接收指定长度的数据 * @param send_buf —— 接收数据缓冲区首地址 * @param recv_buf —— 接收数据缓冲区首地址 * @param size —— 要发送/接收数据的字节数 * @retval 成功返回HAL_OK */static HAL_StatusTypeDef SPI_TransmitReceive(uint8_t* send_buf, uint8_t* recv_buf, uint16_t size){ return HAL_SPI_TransmitReceive(&hspi1, send_buf, recv_buf, size, 100);}/*等待超时时间*/#define SPIT_FLAG_TIMEOUT ((uint32_t)0x1000)#define SPIT_LONG_TIMEOUT ((uint32_t)(10 * SPIT_FLAG_TIMEOUT))static __IO uint32_t SPITimeout = SPIT_LONG_TIMEOUT; /** * @brief 等待超时回调函数 * @param None. * @retval None. */static uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode){ /* 等待超时后的处理,输出错误信息 */ printf("SPI 等待超时!errorCode = %d",errorCode); return 0;} /** * @brief 使用SPI发送一个字节的数据 * @param byte:要发送的数据 * @retval 返回接收到的数据 */uint8_t SPI_FLASH_SendByte(uint8_t byte){ SPITimeout = SPIT_FLAG_TIMEOUT; /* 等待发送缓冲区为空,TXE事件 */ while (__HAL_SPI_GET_FLAG( &hspi1, SPI_FLAG_TXE ) == RESET) { if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0); } /* 写入数据寄存器,把要写入的数据写入发送缓冲区 */ WRITE_REG(hspi1.Instance->DR, byte); SPITimeout = SPIT_FLAG_TIMEOUT; /* 等待接收缓冲区非空,RXNE事件 */ while (__HAL_SPI_GET_FLAG( &hspi1, SPI_FLAG_RXNE ) == RESET) { if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1); } /* 读取数据寄存器,获取接收缓冲区数据 */ return READ_REG(hspi1.Instance->DR);}/** * @brief 读取Flash内部的ID * @param none * @retval 成功返回device_id */uint16_t W25QXX_ReadID(void){ uint8_t recv_buf[2] = {0}; //recv_buf[0]存放Manufacture ID, recv_buf[1]存放Device ID uint16_t device_id = 0; uint8_t send_data[4] = {ManufactDeviceID_CMD,0x00,0x00,0x00}; //待发送数据,命令+地址 /* 使能片选 */ SPI_FLASH_CS_LOW(); /* 发送并读取数据 */ if (HAL_OK == SPI_Transmit(send_data, 4)) { if (HAL_OK == SPI_Receive(recv_buf, 2)) { device_id = (recv_buf[0] 16); /* 发送擦除扇区地址的中位 */ SPI_FLASH_SendByte((sector_addr & 0xFF00) >> 8); /* 发送擦除扇区地址的低位 */ SPI_FLASH_SendByte(sector_addr & 0xFF); /* 取消片选 */ SPI_FLASH_CS_HIGH(); W25QXX_Wait_Busy(); //等待扇区擦除完成}/** * @brief 页写入操作 * @param dat —— 要写入的数据缓冲区首地址 * @param WriteAddr —— 要写入的地址 * @param byte_to_write —— 要写入的字节数(0-256) * @retval none */void W25QXX_PageProgram(uint8_t* dat, uint32_t WriteAddr, uint16_t nbytes){ uint8_t cmd = PAGE_PROGRAM_CMD; // WriteAddr 16; status = SPI_Transmit(&addr, 1); /* 发送 读 地址中位 */ addr = (WriteAddr & 0xFF00) >> 8; status = SPI_Transmit(&addr, 1); /* 发送 读 地址低位 */ addr = WriteAddr & 0xFF; status = SPI_Transmit(&addr, 1); SPI_Transmit(dat, nbytes); /* 取消片选 */ SPI_FLASH_CS_HIGH(); W25QXX_Wait_Busy();}/** * @brief 对FLASH写入数据,调用本函数写入数据前需要先擦除扇区 * @param pBuffer,要写入数据的指针 * @param WriteAddr,写入地址 * @param NumByteToWrite,写入数据长度 * @retval 无 */void W25QXX_BufferWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite){ uint8_t NumOfPage = 0; uint8_t NumOfSingle = 0; uint8_t Addr = 0; uint8_t count = 0; uint8_t temp = 0; /*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/ Addr = WriteAddr % SPI_FLASH_PageSize; /*差count个数据值,刚好可以对齐到页地址*/ count = SPI_FLASH_PageSize - Addr; /*计算出要写多少整数页*/ NumOfPage = NumByteToWrite / SPI_FLASH_PageSize; /*mod运算求余,计算出剩余不满一页的字节数*/ NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; /* Addr=0,则WriteAddr 刚好按页对齐 aligned */ if(Addr == 0) { /* NumByteToWrite < SPI_FLASH_PageSize */ if(NumOfPage == 0) { W25QXX_PageProgram(pBuffer, WriteAddr, NumByteToWrite); } /* NumByteToWrite > SPI_FLASH_PageSize */ else { /*先把整数页都写了*/ while(NumOfPage--) { W25QXX_PageProgram(pBuffer, WriteAddr, SPI_FLASH_PageSize); WriteAddr += SPI_FLASH_PageSize; pBuffer += SPI_FLASH_PageSize; } /*若有多余的不满一页的数据,把它写完*/ W25QXX_PageProgram(pBuffer, WriteAddr, NumOfSingle); } } /* 若地址与 SPI_FLASH_PageSize 不对齐 */ else { /* NumByteToWrite < SPI_FLASH_PageSize */ if(NumOfPage == 0) { /*当前页剩余的count个位置比NumOfSingle小,写不完*/ if(NumOfSingle > count) { temp = NumOfSingle - count; /*先写满当前页*/ W25QXX_PageProgram(pBuffer, WriteAddr, count); WriteAddr += count; pBuffer += count; /*再写剩余的数据*/ W25QXX_PageProgram(pBuffer, WriteAddr, temp); } /*当前页剩余的count个位置能写完NumOfSingle个数据*/ else { W25QXX_PageProgram(pBuffer, WriteAddr, NumByteToWrite); } } /* NumByteToWrite > SPI_FLASH_PageSize */ else { /*地址不对齐多出的count分开处理,不加入这个运算*/ NumByteToWrite -= count; NumOfPage = NumByteToWrite / SPI_FLASH_PageSize; NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize; W25QXX_PageProgram(pBuffer, WriteAddr, count); WriteAddr += count; pBuffer += count; /*把整数页都写了*/ while(NumOfPage--) { W25QXX_PageProgram(pBuffer, WriteAddr, SPI_FLASH_PageSize); WriteAddr += SPI_FLASH_PageSize; pBuffer += SPI_FLASH_PageSize; } /*若有多余的不满一页的数据,把它写完*/ if(NumOfSingle != 0) { W25QXX_PageProgram(pBuffer, WriteAddr, NumOfSingle); } } }} /** * @brief 读取FLASH数据 * @param pBuffer,存储读出数据的指针 * @param ReadAddr,读取地址 * @param NumByteToRead,读取数据长度 * @retval 无 */void W25QXX_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead){ W25QXX_Wait_Busy(); /* 选择FLASH: CS低电平 */ SPI_FLASH_CS_LOW(); /* 发送 读 指令 */ uint8_t cmd = READ_DATA_CMD; SPI_Transmit(&cmd, 1);// 不知道为什么连起来发不行// ReadAddr = ReadAddr > 16; status = SPI_Transmit(&addr, 1); /* 发送 读 地址中位 */ addr = (ReadAddr& 0xFF00) >> 8; status = SPI_Transmit(&addr, 1); /* 发送 读 地址低位 */ addr = ReadAddr & 0xFF; status = SPI_Transmit(&addr, 1); if(HAL_OK == status) { SPI_Receive(pBuffer, NumByteToRead); } /* 停止信号 FLASH: CS 高电平 */ SPI_FLASH_CS_HIGH();} 六、修改diskio接口函数在 user_diskio.c 中修改以下几个函数:
/* Private functions ---------------------------------------------------------*//** * @brief Initializes a Drive * @param pdrv: Physical drive number (0..) * @retval DSTATUS: Operation status */DSTATUS USER_initialize (BYTE pdrv /* Physical drive nmuber to identify the drive */){ /* USER CODE BEGIN INIT */ /* 延时一小段时间 */ uint16_t i; i = 500; while(--i); Stat = STA_NOINIT;if(W25QXX_ReadID() != 0) { Stat &= ~STA_NOINIT; } return Stat; /* USER CODE END INIT */}/** * @brief Gets Disk Status * @param pdrv: Physical drive number (0..) * @retval DSTATUS: Operation status */DSTATUS USER_status (BYTE pdrv /* Physical drive number to identify the drive */){ /* USER CODE BEGIN STATUS */ Stat &= ~STA_NOINIT; return Stat; /* USER CODE END STATUS */}/** * @brief Reads Sector(s) * @param pdrv: Physical drive number (0..) * @param *buff: Data buffer to store read data * @param sector: Sector address (LBA) * @param count: Number of sectors to read (1..128) * @retval DRESULT: Operation result */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 */DRESULT status = RES_PARERR; if(!count) { return RES_PARERR; //count不能等于0,否则返回参数错误 } // /* 扇区偏移2MB,外部Flash文件系统空间放在SPI Flash后面6MB空间 */ // sector += 512; W25QXX_BufferRead(buff, sector