STM32 norflash W25Q64移植FatFS
W25Q64移植FatFS
- 前言
- 获取FATFS源码
- 下载完成后解压代码
- W25Q64源码
- 移植
- 注意
前言
对于
w25q64
这种小容量norflash
设备,最好的文件系统应该是littleFS
这种小型文件系统,原生支持耐磨损机制,提高flash
寿命,代码占用开销小。但该系统只能原生使用在嵌入式系统内部,外部win
、linux
系统不能直接读取文件内容,fatfs
由于在各系统的兼容性仍具有很强的跨平台优势。
获取FATFS源码
fatfs官网地址
这里演示代码为R0.14b
版本
下载完成后解压代码
下载源码包为ff14b.zip
解压该压缩包
将压缩包中diskio.c diskio.h ff.c ff.h ffconf.h
复制到你的工程
W25Q64源码
这里使用的是spi1
通道,片选引脚为PA4
,HAL库驱动,移植了rtthread
系统,延时函数使用的是rt_thread_mdelay
,这里根据自身情况修改延时函数。
//c文件
#include "w25q64.h"
#include "rtthread.h"// 初始化W25Q64
void W25Q64_Init(void)
{// 初始化片选引脚(已在CubeMX中配置为推挽输出)W25Q64_CS_HIGH();rt_thread_mdelay(10);// 唤醒芯片(防止处于睡眠模式)W25Q64_WakeUp();
}// 读取JEDEC ID
uint32_t W25Q64_ReadID(void)
{uint32_t id = 0;uint8_t tmp[3];uint8_t id_cmd = W25Q_JEDEC_ID;W25Q64_CS_LOW();// 发送读ID命令HAL_SPI_Transmit(&hspi1, &id_cmd, 1, 100);// 读取3字节IDHAL_SPI_Receive(&hspi1, tmp, 3, 100);W25Q64_CS_HIGH();// 组合ID ( manufacturer ID: 0xEF, device ID: 0x4017 for W25Q64 )id = ((uint32_t)tmp[0] << 16) | ((uint32_t)tmp[1] << 8) | tmp[2];return id;
}// 写使能
void W25Q64_WriteEnable(void)
{uint8_t write_en_cmd = W25Q_WRITE_ENABLE;W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &write_en_cmd, 1, 100);W25Q64_CS_HIGH();
}// 写禁止
void W25Q64_WriteDisable(void)
{uint8_t write_dis_cmd = W25Q_WRITE_DISABLE;W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1,&write_dis_cmd, 1, 100);W25Q64_CS_HIGH();
}// 读状态寄存器1
uint8_t W25Q64_ReadStatusReg1(void)
{uint8_t reg1;uint8_t read_stu_reg1_cmd = W25Q_READ_STATUS_REG1;W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &read_stu_reg1_cmd, 1, 100);HAL_SPI_Receive(&hspi1, ®1, 1, 100);W25Q64_CS_HIGH();return reg1;
}// 读状态寄存器2
uint8_t W25Q64_ReadStatusReg2(void)
{uint8_t reg2;uint8_t read_stu_reg2_cmd = W25Q_READ_STATUS_REG2;W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &read_stu_reg2_cmd, 1, 100);HAL_SPI_Receive(&hspi1, ®2, 1, 100);W25Q64_CS_HIGH();return reg2;
}// 写状态寄存器
void W25Q64_WriteStatusReg(uint8_t reg1, uint8_t reg2)
{uint8_t write_stu_reg_cmd = W25Q_WRITE_STATUS_REG;W25Q64_WriteEnable();W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &write_stu_reg_cmd, 1, 100);HAL_SPI_Transmit(&hspi1, ®1, 1, 100);HAL_SPI_Transmit(&hspi1, ®2, 1, 100);W25Q64_CS_HIGH();W25Q64_WaitBusy();
}// 等待设备空闲
void W25Q64_WaitBusy(void)
{while((W25Q64_ReadStatusReg1() & 0x01) == 0x01); // 忙标志位为1时等待
}// 擦除4KB扇区
void W25Q64_EraseSector(uint32_t addr)
{uint8_t sec_era_4k_cmd = W25Q_SECTOR_ERASE_4K;// 确保地址在有效范围内if(addr >= W25Q64_FLASH_SIZE) return;W25Q64_WriteEnable();W25Q64_WaitBusy();W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &sec_era_4k_cmd, 1, 100);// 发送24位地址uint8_t addr_buf[3];addr_buf[0] = (addr >> 16) & 0xFF;addr_buf[1] = (addr >> 8) & 0xFF;addr_buf[2] = addr & 0xFF;HAL_SPI_Transmit(&hspi1, addr_buf, 3, 100);W25Q64_CS_HIGH();W25Q64_WaitBusy(); // 等待擦除完成(约40ms)
}// 擦除32KB块
void W25Q64_EraseBlock32K(uint32_t addr)
{uint8_t block_era_32k_cmd = W25Q_BLOCK_ERASE_32K;if(addr >= W25Q64_FLASH_SIZE) return;W25Q64_WriteEnable();W25Q64_WaitBusy();W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &block_era_32k_cmd, 1, 100);uint8_t addr_buf[3] = {(addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF};HAL_SPI_Transmit(&hspi1, addr_buf, 3, 100);W25Q64_CS_HIGH();W25Q64_WaitBusy(); // 等待擦除完成(约150ms)
}// 擦除64KB块
void W25Q64_EraseBlock64K(uint32_t addr)
{uint8_t block_era_64k_cmd = W25Q_BLOCK_ERASE_64K;if(addr >= W25Q64_FLASH_SIZE) return;W25Q64_WriteEnable();W25Q64_WaitBusy();W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &block_era_64k_cmd, 1, 100);uint8_t addr_buf[3] = {(addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF};HAL_SPI_Transmit(&hspi1, addr_buf, 3, 100);W25Q64_CS_HIGH();W25Q64_WaitBusy(); // 等待擦除完成(约200ms)
}// 整片擦除
void W25Q64_EraseChip(void)
{uint8_t chip_erase_cmd = W25Q_CHIP_ERASE;W25Q64_WriteEnable();W25Q64_WaitBusy();W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &chip_erase_cmd, 1, 100);W25Q64_CS_HIGH();W25Q64_WaitBusy(); // 等待擦除完成(约10s)
}// 页编程(最多256字节,地址需页对齐)
void W25Q64_WritePage(uint32_t addr, uint8_t *data, uint16_t len)
{uint8_t page_pro_cmd = W25Q_PAGE_PROGRAM;if(addr >= W25Q64_FLASH_SIZE || len == 0 || len > W25Q64_PAGE_SIZE) return;W25Q64_WriteEnable();W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &page_pro_cmd, 1, 100);uint8_t addr_buf[3] = {(addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF};HAL_SPI_Transmit(&hspi1, addr_buf, 3, 100);HAL_SPI_Transmit(&hspi1, data, len, 100);W25Q64_CS_HIGH();W25Q64_WaitBusy();
}// 无校验写(需确保扇区已擦除)
void W25Q64_WriteNoCheck(uint32_t addr, uint8_t *data, uint16_t len)
{uint16_t page_remain = W25Q64_PAGE_SIZE - (addr % W25Q64_PAGE_SIZE);if(len <= page_remain) page_remain = len;while(1) {W25Q64_WritePage(addr, data, page_remain);if(len == page_remain) break;else {data += page_remain;addr += page_remain;len -= page_remain;if(len > W25Q64_PAGE_SIZE) page_remain = W25Q64_PAGE_SIZE;else page_remain = len;}}
}
// 分配扇区缓存(需要足够的栈空间或使用静态数组)uint8_t flash_read_buf[W25Q64_SECTOR_SIZE];
// 带擦除的写操作(自动处理跨页和扇区擦除)
void W25Q64_Write(uint32_t addr, uint8_t *data, uint16_t len)
{uint32_t sec_pos;uint16_t sec_off;uint16_t sec_remain;uint8_t *wbuf;wbuf = flash_read_buf;while(len > 0) {sec_pos = addr / W25Q64_SECTOR_SIZE; // 扇区地址sec_off = addr % W25Q64_SECTOR_SIZE; // 在扇区内的偏移sec_remain = W25Q64_SECTOR_SIZE - sec_off; // 扇区剩余空间if(len <= sec_remain) sec_remain = len; // 本次写入不超过当前扇区// 读取整个扇区内容W25Q64_Read(sec_pos * W25Q64_SECTOR_SIZE, wbuf, W25Q64_SECTOR_SIZE);// 检查是否需要擦除扇区uint8_t need_erase = 0;for(uint16_t i = 0; i < sec_remain; i++) {if((wbuf[sec_off + i] & data[i]) != data[i]) {need_erase = 1;break;}}if(need_erase) {W25Q64_EraseSector(sec_pos * W25Q64_SECTOR_SIZE); // 擦除扇区// 复制原有数据for(uint16_t i = 0; i < sec_off; i++) {wbuf[sec_off + i] = data[i];}// 写入新数据for(uint16_t i = 0; i < sec_remain; i++) {wbuf[sec_off + i] = data[i];}// 写回整个扇区W25Q64_WriteNoCheck(sec_pos * W25Q64_SECTOR_SIZE, wbuf, W25Q64_SECTOR_SIZE);} else {// 不需要擦除,直接写入W25Q64_WriteNoCheck(addr, data, sec_remain);}// 更新地址和长度addr += sec_remain;data += sec_remain;len -= sec_remain;}
}// 读取数据
void W25Q64_Read(uint32_t addr, uint8_t *data, uint32_t len)
{uint8_t read_data_cmd = W25Q_READ_DATA;if(addr >= W25Q64_FLASH_SIZE || len == 0) return;W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &read_data_cmd, 1, 100);uint8_t addr_buf[3] = {(addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF};HAL_SPI_Transmit(&hspi1, addr_buf, 3, 100);HAL_SPI_Receive(&hspi1, data, len, 1000); // 超时时间长一些W25Q64_CS_HIGH();
}// 进入睡眠模式
void W25Q64_EnterSleepMode(void)
{uint8_t sleep_mode_cmd = W25Q_SLEEP_MODE;W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &sleep_mode_cmd, 1, 100);W25Q64_CS_HIGH();rt_thread_mdelay(1); // 等待进入睡眠模式
}// 唤醒
void W25Q64_WakeUp(void)
{uint8_t wake_up_cmd = W25Q_WAKE_UP;W25Q64_CS_LOW();HAL_SPI_Transmit(&hspi1, &wake_up_cmd, 1, 100);W25Q64_CS_HIGH();rt_thread_mdelay(1); // 等待唤醒
}
#ifndef __W25Q64_H
#define __W25Q64_H#include "stm32f4xx_hal.h"
#include "spi.h"
#include "gpio.h"
#define W25Q64_ID 0xEF4017 // W25Q64命令定义
#define W25Q_WRITE_ENABLE 0x06 // 写使能
#define W25Q_WRITE_DISABLE 0x04 // 写禁止
#define W25Q_READ_STATUS_REG1 0x05 // 读状态寄存器1
#define W25Q_READ_STATUS_REG2 0x35 // 读状态寄存器2
#define W25Q_WRITE_STATUS_REG 0x01 // 写状态寄存器
#define W25Q_PAGE_PROGRAM 0x02 // 页编程
#define W25Q_SECTOR_ERASE_4K 0x20 // 4KB扇区擦除
#define W25Q_BLOCK_ERASE_32K 0x52 // 32KB块擦除
#define W25Q_BLOCK_ERASE_64K 0xD8 // 64KB块擦除
#define W25Q_CHIP_ERASE 0xC7 // 整片擦除
#define W25Q_READ_DATA 0x03 // 读取数据
#define W25Q_FAST_READ 0x0B // 快速读取
#define W25Q_JEDEC_ID 0x9F // 读取JEDEC ID
#define W25Q_SLEEP_MODE 0xB9 // 睡眠模式
#define W25Q_WAKE_UP 0xAB // 唤醒// W25Q64容量定义(8MB)
#define W25Q64_FLASH_SIZE 0x800000 // 8MB
#define W25Q64_PAGE_SIZE 256 // 页大小
#define W25Q64_SECTOR_SIZE 0x1000 // 4KB扇区
#define W25Q64_BLOCK32_SIZE 0x8000 // 32KB块
#define W25Q64_BLOCK64_SIZE 0x10000 // 64KB块// 片选控制宏定义
#define W25Q64_CS_PORT W25QXX_CS_GPIO_Port
#define W25Q64_CS_PIN W25QXX_CS_Pin
#define W25Q64_CS_HIGH() HAL_GPIO_WritePin(W25Q64_CS_PORT, W25Q64_CS_PIN, GPIO_PIN_SET)
#define W25Q64_CS_LOW() HAL_GPIO_WritePin(W25Q64_CS_PORT, W25Q64_CS_PIN, GPIO_PIN_RESET)// 函数声明
void W25Q64_Init(void);
uint32_t W25Q64_ReadID(void);
void W25Q64_WriteEnable(void);
void W25Q64_WriteDisable(void);
uint8_t W25Q64_ReadStatusReg1(void);
uint8_t W25Q64_ReadStatusReg2(void);
void W25Q64_WriteStatusReg(uint8_t reg1, uint8_t reg2);
void W25Q64_WaitBusy(void);
void W25Q64_EraseSector(uint32_t addr);
void W25Q64_EraseBlock32K(uint32_t addr);
void W25Q64_EraseBlock64K(uint32_t addr);
void W25Q64_EraseChip(void);
void W25Q64_WritePage(uint32_t addr, uint8_t *data, uint16_t len);
void W25Q64_WriteNoCheck(uint32_t addr, uint8_t *data, uint16_t len);
void W25Q64_Write(uint32_t addr, uint8_t *data, uint16_t len);
void W25Q64_Read(uint32_t addr, uint8_t *data, uint32_t len);
void W25Q64_EnterSleepMode(void);
void W25Q64_WakeUp(void);#endif
移植
1、diskio.c
填充相应的接口函数
DSTATUS disk_status (BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{uint32_t id = W25Q64_ReadID();if(id == W25Q64_ID){return 0;}return STA_NOINIT;
}/*-----------------------------------------------------------------------*/
/* Inidialize a Drive */
/*-----------------------------------------------------------------------*/DSTATUS disk_initialize (BYTE pdrv /* Physical drive nmuber to identify the drive */
)
{W25Q64_Init();return disk_status(pdrv);
}/*-----------------------------------------------------------------------*/
/* Read Sector(s) */
/*-----------------------------------------------------------------------*/DRESULT disk_read (BYTE pdrv, /* Physical drive nmuber to identify the drive */BYTE *buff, /* Data buffer to store read data */LBA_t sector, /* Start sector in LBA */UINT count /* Number of sectors to read */
)
{if(count == 0) return RES_PARERR; // 参数错误// 计算实际地址:扇区号 * 扇区大小uint32_t addr = sector * W25Q64_SECTOR_SIZE;// 检查地址范围 超出寻址范围if(addr + count * W25Q64_SECTOR_SIZE > W25Q64_FLASH_SIZE) {return RES_PARERR;}// 读取数据W25Q64_Read(addr, buff, count * W25Q64_SECTOR_SIZE);return RES_OK;
}/*-----------------------------------------------------------------------*/
/* Write Sector(s) */
/*-----------------------------------------------------------------------*/#if FF_FS_READONLY == 0DRESULT disk_write (BYTE pdrv, /* Physical drive nmuber to identify the drive */const BYTE *buff, /* Data to be written */LBA_t sector, /* Start sector in LBA */UINT count /* Number of sectors to write */
)
{if(count == 0) return RES_PARERR; // 参数错误// 计算实际地址uint32_t addr = sector * W25Q64_SECTOR_SIZE;// 检查地址范围if(addr + count * W25Q64_SECTOR_SIZE > W25Q64_FLASH_SIZE) {return RES_PARERR;}// 写入数据(W25Q64_Write函数已处理扇区擦除)W25Q64_Write(addr, (uint8_t*)buff, count * W25Q64_SECTOR_SIZE);return RES_OK;
}#endif/*-----------------------------------------------------------------------*/
/* Miscellaneous Functions */
/*-----------------------------------------------------------------------*/DRESULT disk_ioctl (BYTE pdrv, /* Physical drive nmuber (0..) */BYTE cmd, /* Control code */void *buff /* Buffer to send/receive control data */
)
{switch(cmd) {case GET_SECTOR_COUNT:// 返回总扇区数:总容量 / 扇区大小*(DWORD*)buff = W25Q64_FLASH_SIZE / W25Q64_SECTOR_SIZE;break;case GET_SECTOR_SIZE:// 返回扇区大小*(WORD*)buff = W25Q64_SECTOR_SIZE;break;case GET_BLOCK_SIZE:// 返回擦除块大小(单位:扇区)*(DWORD*)buff = 1; // 4KB扇区擦除break;case CTRL_SYNC:// 同步缓存,W25Q64无缓存,直接返回成功break;default:return RES_PARERR;}return RES_OK;
}
2、ffconf.h
根据宏裁剪相关功能,这只是其中一小部分
/*---------------------------------------------------------------------------/
/ FatFs - Generic FAT file system module configuration file R0.14b
/----------------------------------------------------------------------------*/#define FF_FS_READONLY 0 /* 0:读写 / 1:只读 */
#define FF_FS_MINIMIZE 0 /* 0:完整功能 / 1:无f_stat等 / 2:无f_getfree等 / 3:无f_mkdir等 */#define FF_USE_LFN 1 /* 0:不支持长文件名 / 1:动态分配缓存 / 2:静态缓存 */
#define FF_MAX_LFN 255 /* 长文件名最大长度 (12-255) */
#define FF_LFN_UNICODE 0 /* 0:ANSI/OEM / 1:Unicode */
#define FF_CODE_PAGE 936 /* 代码页 (936=GBK, 437=美国, 932=日文, 950=繁体中文) */#define FF_USE_STRFUNC 1 /* 0:不支持字符串函数 / 1:支持f_gets等 / 2:额外支持f_puts等 */
#define FF_USE_FASTSEEK 1 /* 0:关闭快速定位 / 1:开启 */
#define FF_USE_MKFS 1 /* 0:不支持格式化 / 1:支持 */
#define FF_USE_TRIM 0 /* 0:关闭TRIM / 1:开启(仅SSD/EMMC) */#define FF_VOLUMES 1 /* 逻辑驱动器数量 (1-10) */
#define FF_STR_VOLUME_ID 0 /* 0:卷ID为数字 / 1:卷ID为字符串 */
#define FF_MULTI_PARTITION 0 /* 0:单分区 / 1:多分区 */#define FF_FS_EXFAT 0 /* 0:不支持exFAT / 1:支持(需R0.14+) */
#define FF_FS_NORTC 0 /* 0:使用实时时钟 / 1:无实时时钟(时间戳为0) */
#define FF_USE_TIMESTAMP 1 /* 0:关闭时间戳 / 1:开启 */
#define FF_SYNC 0 /* 0:关闭同步 / 1:写操作立即同步到磁盘 */#define FF_FS_TINY 0 /* 0:正常模式 / 1:微型模式(减少RAM占用) */
#define FF_STRF_ENC 3 /* 字符串编码 (0:ANSI/OEM, 1:UTF-16LE, 2:UTF-16BE, 3:UTF-8) *//* 低级设备接口配置(由用户实现) */
#define FF_DMA_USED 1 /* 0:不使用DMA / 1:使用DMA(STM32可开启提升速度) */
测试函数
#include "ff.h"
#include "w25q64.h"
#include "rtthread.h"
FATFS fs; // 文件系统对象
FIL fil; // 文件对象
FRESULT fr; // 操作结果
UINT bw, br; // 写入/读取字节数
uint8_t work_buf[512] = {0}; // 工作缓冲区(建议≥4KB)
void fatfs_test(void)
{MKFS_PARM mkfs_opt; // 格式化参数结构体// 1. 配置格式化参数mkfs_opt.fmt = 1; // 1 = 强制FAT32格式(0=FAT12/16,2=exFAT)mkfs_opt.n_fat = 0; // 0 = 使用默认值(2个FAT表)mkfs_opt.align = 0; // 0 = 按扇区对齐(默认)mkfs_opt.au_size = 0; // 0 = 自动计算簇大小(推荐)// 1. 挂载文件系统fr = f_mount(&fs, "0:", 0);if(fr != FR_OK) {rt_kprintf("mount failed: %d,try mkfs...\r\n", fr);// 格式化Flash(首次使用必须执行)fr = f_mkfs("", &mkfs_opt, work_buf, 512);if(fr != FR_OK) {rt_kprintf("mkfs failed: %d\r\n", fr);return;}// 格式化后重新挂载fr = f_mount(&fs, "", 1);if(fr != FR_OK) {rt_kprintf("re mount failed: %d\r\n", fr);return;}rt_kprintf("mkfs success mount success\r\n");} else {rt_kprintf("fs mount success\r\n");}// 2. 创建并写入文件fr = f_open(&fil, "test.txt", FA_WRITE | FA_CREATE_ALWAYS);if(fr == FR_OK) {char *text = "Hello, W25Q64 + FatFs!";fr = f_write(&fil, text, rt_strlen(text), &bw);if(fr == FR_OK) {rt_kprintf("write success,write num: %d\r\n", bw);} else {rt_kprintf("write failed: %d\r\n", fr);}f_close(&fil);} else {rt_kprintf("open file failed: %d\r\n", fr);}// 3. 读取文件内容fr = f_open(&fil, "test.txt", FA_READ);if(fr == FR_OK) {char buf[128] = {0};fr = f_read(&fil, buf,127, &br);if(fr == FR_OK) {rt_kprintf("read success,read num: %d,payload: %s\r\n", br, buf);} else {rt_kprintf("read failed: %d\r\n", fr);}f_close(&fil);} else {rt_kprintf("open file failed: %d\r\n", fr);}// 4. 卸载文件系统//f_mount(&fs,"",0);rt_kprintf("test over\r\n");
}
注意
w25q64
只能按扇区擦除,flash
底层只能由1
变0
,因此写入数据的时候,需要预先读出写入数据所在扇区的数据到缓存,将缓存的扇区数据按写入的数据地址进行相应更改后,擦除要写入地址的扇区,再将扇区缓存数据重新写入。因此写函数至少需要4k
缓存区,根据单片机堆栈分配和是否运行系统,来选择该4k
空间的分布,这很重要,否则会导致栈空间溢出等情况发生。