单片机教程:基于M25P16的极简文件系统实现
MCU:stm32f407
Flash:M25P16
一、说明
所谓“极简”就是极其简陋,是针对M25P16这种仅有2MB大小的flash芯片设计的,如果是大一点的如W25Q128或者SD卡等,那么就得上FatFs文件系统了。所以本篇并不具备任何技术力可言,仅仅是对同为初学者的技术参考。
本篇起于:在M25P16上存储wav文件中的PCM数据时,不方便读取。于是这个极简文件系统只涉及一个主要功能——查找。
定位:存储一些简单的语音素材,并不需要一些复杂的文件操作,单纯是当做一个可以格式化的只读存储器
二、大致思路
1,框架
①基本思路
一个最基本的文件系统,理应包含写入、读取和格式化(擦除)这三个基本功能。为了解决前面所提到的问题“不方便读取”,那么最朴素的想法就是建立一个索引(目录),一个索引指代一个文件的物理地址,根据索引可以快速寻找到文件的位置。
②索引
而这个索引如何建立呢?为了方便记忆,不采用0为开始文件,那么文件1对应的是索引1,文件2对应的是索引2,以此类推。同时为了节省存储,不搞什么文件名称,可以用宏定义来代表所有的文件如下,也可以使用枚举体。虽然失去了很多优点,但是还能凑合着用
光有逻辑概念可不行,于是我们为这个索引表分配一个页的存储大小(虽然只能进行扇擦除,但是由于我们这个方案的定位就不是可以随意进行文件操作,所以就不考虑)
/*英语、拼音、汉字随意*/
#define Hello 1
#define chi_le_ma 2
#define 吃了 3
③索引到物理地址的映射
那么这个索引与物理地址的对应关系如何建立呢?不考虑什么哈希表之类的,这里以最简单的线性映射为例,地址直接对应一块连续的区域。由于一个地址需要4个字节来表示,那么索引n与物理地址m的就是简单的m=4*n。注意哈,索引本身是一系列自然数数值,这个索引所占用的物理区域里的数值才是一段数据的真正地址,可以类比指针。
由于一段数据不可能由一个地址确定,那么就可以想到用两个地址来表示,即起始地址与结束地址。由于这个极简方案的数据存储是一个紧挨着一个,那么为了减少占用,使用一个文件的起始地址-1来代表前一个文件结束地址,那么这样一直串下去,一页256字节的索引表,一共可以分配64个索引。由于结束地址也需要一个,那么就是63个索引,为了与文件对应,那么采用1~63表示文件1~63,那么索引n与物理地址m的就是简单的m=4*(n-1),当然你可以按照自己的习惯来规定索引范围
于此,文件索引n与物理地址的映射关系如下,需要强调的是索引本身只是个文件序号,其所代表的四个字节大小的存储空间内的数值是,该文件所存放的数据的物理地址(好像更绕了)
总的来说就是,索引本身是有个物理地址的,文件也是有物理地址的,只不过索引可以指向文件的物理地址
【起始地址】4*(n-1)处存储空间内所存储的值
【结束地址】4*n处存储空间所存储的值
此处应有一幅图
既然基本关系已经确立好了,接下来我们考虑具体的操作流程,即创建对外的接口。
获得索引对应的起始地址
//获取文件起始地址
inline uint32_t StorageManage_M25P16_GetStartAddr(uint8_t fileIndex)
{
return SPI_FLASH_ReadDWord((fileIndex - 1) << 2);//左移2位即为乘以4
}
获得索引对应的结束地址
//获取文件结束地址
inline uint32_t StorageManage_M25P16_GetEndAddr(uint8_t fileIndex)
{
return SPI_FLASH_ReadDWord(fileIndex << 2);
}
需要特别注意的是这个读取和写入都是按照小端字节序的
uint32_t SPI_FLASH_ReadDWord(uint32_t ReadAddr)
{
SPI_FLASH_CS_LOW();
// 发送Read Data指令(0x03)
SPI_FLASH_SendByte(ReadData);
// 发送地址
SPI_FLASH_SendByte((ReadAddr >> 16) & 0xFF);// 高8位
SPI_FLASH_SendByte((ReadAddr >> 8) & 0xFF); // 中8位
SPI_FLASH_SendByte(ReadAddr & 0xFF); // 低8位
// 读取4个字节的数据
uint32_t data = 0;
for (uint8_t i = 0; i < 4; ++i)
{
data |=(SPI_FLASH_SendByte(Dummy_Byte)<<8*i);
}
SPI_FLASH_CS_HIGH();
return data;
}
2,写入
首先我们先考虑写入操作,由于有了索引,所以不能像从前一样直接把数据写入。于是接下来构思一个简单的写入流程。
/**
* @brief 先查看索引获得起始地址,再写入数据,写入完成后建立索引。
* @note 写入数据使用的是页写入,写入过程使用的是两个256字节的缓冲数组分别存储接收的数据和准备写入M25P16的数据,
* 两个指针分别用于对应接收和写入,一个缓冲索引用于交换指针,以便接收缓冲区满后可以立即写入M25P16。对外开放的是接收指针和缓冲索引,
* 如果缓冲索引已满,那么将交换指针,释放信号量,以便写入任务可以将数据写入M25P16。
* 查找索引表依靠的是宏定义的代码
* @param fileIndex 文件索引,最大为63。值的范围在1-63
* @param prxBuffer 接收缓冲区指针,用于交换接收缓冲区和写入缓冲区
* @param writeFlag 文件传输标志,用于判断文件是否写入完成,如果值为0,说明文件正在写入,如果值为1,说明文件写入完成
*/
void StorageManage_M25P16_Write();
从这个流程中,可以把写入操作分为三步:第一步变成了查索引(后面会解释),获得索引对应的地址,第二步就是写入前面到的文件地址,第三步就是写入完成后,追加索引,即创建下一个索引的起始地址。
那么之前的一个void StorageManage_M25P16_Write();函数就得裂开成3个,于是就有了
void StorageManage_M25P16_Writing_Start();
void StorageManage_M25P16_Writing();
void StorageManage_M25P16_Writing_End();
现在就可以实现这三个函数了
①写入数据前—查索引
查到的索引需要一个变量来承载,且之后写入数据过程中都需要一个变量来指示写入的位置。因此可以定义一个全局变量addr。这样在写入前获得起始地址,之后便从这个地址不断写下去
/**
* @brief 写入数据的开始
* @param fileIndex
*/
void StorageManage_M25P16_Writing_Start(uint8_t fileIndex)
{
//查找索引
m25p15addr = StorageManage_M25P16_GetStartAddr(fileIndex);//获取存储起始地址
}
②写入数据中—写入
写入数据嘛,这里需要调用SPI_FLASH的接口。同时考虑到页写入的特殊性,为了提升性能,那么写入的过程应该以完整地一页一页写为主。因此刚开始写入时,先把起始地址所在的这一页的剩余部分写满,那么判断条件就可以设为“地址是否正好是页的起始地址”,即对一页的大小256取模是否为0。
只要满足就立即调用写入M25P16的函数,不过写入哪里呢?由于为了接收数据与写入数据不冲突,那么需要两个缓冲区,接收缓冲区和写入缓冲区。写入过程中如何确保写满了接收缓冲区之后还能再写同时写入M25P16呢?那么需要两个指针分别指向接收缓冲区和写入缓冲区,这样满了之后直接交换指针,然后各安其职。
接下来写入多长呢?那么再添加一个变量来记录写入接收缓存区的字节数,
extern uint8_t *prxdBuffer;//对外提供的接收缓冲区
extern uint16_t writeNumber;
extern osSemaphoreId_t WriteM25P16BinarySemHandle;
extern uint32_t m25p15addr;//存储地址
extern uint8_t *pwriteBuffer;
由于直接把一整页数据写入M25P16效率最高,所以放弃了直接使用BufferWrite来完成不定长数据的写入。思路,只要写入的地址达到页码的起始地址(说明前面一页满了),就立即把接收缓冲区里的数据写入M25P16
/**
* @brief 写入数据的过程
* @note 写入数据的过程,写入过程使用的是两个256字节的缓冲数组分别存储接收的数据和准备写入M25P16的数据。
* 因为要交换指针,所以指针必须是引用型
* @param fileIndex 文件索引,最大为63。值的范围在1-63
* @param prxBuffer
*/
void StorageManage_M25P16_Writing(uint8_t fileIndex, uint8_t *&prxBuffer)//正在写入
{
++m25p15addr;
++writeNumber; //用于交代写入的字节数
if ((m25p15addr%256) == 0)// 对m25p15addr取模256,如果等于0,则说明该页已经写满,需要换页
{
uint8_t *pTemp = prxdBuffer;
prxdBuffer = pwriteBuffer;
pwriteBuffer = pTemp;
osSemaphoreRelease(WriteM25P16BinarySemHandle);//信号量,如果是裸机的话就放一个flag标志,然后在后台程序(主循环)里完成写入M25P16的操作
}
}
③写入数据后—追加索引
当接收停止时,我们需要考虑把剩余还残留在缓冲区里未满256字节的写入M25P16,然后把这个【结束地址+1】的m25p16addr存入下一个索引的所占用的空间
/**
* @brief 写入过程的结束
* @note 追加索引,同时写入索引表
* @param fileIndex 文件索引,最大为63。值的范围在1-63
*/
void StorageManage_M25P16_Writing_End(uint8_t fileIndex)
{
//把缓冲区残余的数据写入M25P16
if (writeNumber)
{
SPI_FLASH_PageWrite(pwriteBuffer, m25p15addr-writeNumber, writeNumber);
}
//追加索引
SPI_FLASH_PageWrite((uint8_t *) (&m25p15addr), fileIndex<<2, 4);
}
④后台写入M25P16
我使用的是FreeRTOS,如果你是裸机开发的话可以到主循环里。
void WriteM25P16_Task(void *argument)
{
for (;;)
{
osSemaphoreAcquire(WriteM25P16BinarySemHandle, osWaitForever);
SPI_FLASH_PageWrite(pwriteBuffer, m25p15addr-writeNumber, writeNumber);
writeNumber = 0;//重置写入的字节数
}
}
while(1)
{
if(writeM25P16_flag)
{
SPI_FLASH_PageWrite(pwriteBuffer, m25p15addr-writeNumber, writeNumber);
writeNumber = 0;//重置写入的字节数
}
}
3,读取
①读1字节
考虑到使用情景是,一个字节一字节地读取值并发送给DAC用来播放语音,所以先写了读1字节的接口。
/**
* @brief 读取一字节数据
* @param data
* @param Index
* @return 1表示读取正在读取,0表示读取结束
*/
void StorageManage_M25P16_ReadByte(uint8_t *data, uint8_t Index)
{
uint32_t readAddr = StorageManage_M25P16_GetStartAddr(Index);
uint32_t endAddr = StorageManage_M25P16_GetEndAddr(Index);
if (readAddr < endAddr)
{
*data = SPI_FLASH_ReadByte((readAddr++));
}
}
同时考虑到这个接口需要频繁调用,那么就需要判断当下这个文件是否播放完,以及是否更换了文件,那么有
/**
* @brief 读取一字节数据
* @note 读取字节,读取完成后,返回读取是否结束。由于可能会经常调用,所以有了这三个静态变量。
* @param data
* @param Index
* @return 1表示读取正在读取,0表示读取结束
*/
bool StorageManage_M25P16_ReadByte(uint8_t *data, uint8_t Index)
{
static uint8_t oldIndex = Index;
static uint32_t readAddr = StorageManage_M25P16_GetStartAddr(Index);
static uint32_t endAddr = StorageManage_M25P16_GetEndAddr(Index);
//索引改变,重新获取起始地址
if (oldIndex != Index)
{
oldIndex = Index;
readAddr = StorageManage_M25P16_GetStartAddr(Index);
endAddr = StorageManage_M25P16_GetEndAddr(Index);
}
if (readAddr < endAddr)
{
*data = SPI_FLASH_ReadByte((readAddr++));
return true;
}
else
{
return false;
}
}
②读不定长数据
/**
* @brief 读取缓冲区数据
* @note 读取缓冲区数据,读取完成后,返回读取是否结束。由于往往只会会调用一次,所以就用临时变量代替
* @param pBuffer
* @param NumByteToRead
* @param Index
* @return
*/
bool StorageManage_M25P16_BufferRead(uint8_t *pBuffer,
uint16_t NumByteToRead, uint8_t Index)
{
volatile uint32_t readAddr = StorageManage_M25P16_GetStartAddr(Index);
volatile uint32_t endAddr = StorageManage_M25P16_GetEndAddr(Index);
if (endAddr - endAddr == NumByteToRead)
{
SPI_FLASH_BufferRead(pBuffer, readAddr, NumByteToRead);
readAddr += NumByteToRead;
return true;
}
return false;
}
4,格式化
考虑到格式化后所有单元都被清为0xFF,假如现在我们要写入第一个文件,那么按照刚才的写入流程你会发现,查索引这步只能读到0xFFFFFFFF,这显然不是我们希望的。于是我们得给它规定一个起始地址,鉴于前面索引表分配了一页的存储大小,那么这个起始地址可以紧随其后
/**宏定义*/
#define StorageManage_M25P16_Index_StartAddr 0x00000100// 由于索引表一共256字节,故索引1所对应的文件起始地址256
/**
* @brief 格式化,同时建立索引1的起始地址
*/
void StorageManage_M25P16_EraseAll()
{
uint32_t addr = StorageManage_M25P16_Index_StartAddr;
//先格式化
SPI_FLASH_ChipErase();
//写入索引1的起始地址
SPI_FLASH_PageWrite((uint8_t *) (&addr), 0, 4);//在M25P16地址0处开始写入索引1的起始地址
}
5,初始化
首先初始化M25P16,然后把变量重置
/**
* @brief M25P16存储管理初始化
* @note 初始化SPI与M25P16,然后创建写入任务,以便可以在后台完成写入操作。
*/
void StorageManage_M25P16_Init()
{
SPI_FLASH_Init();
writeNumber = 0;
WriteM25P16BinarySemHandle=osSemaphoreNew(1, 0, nullptr);
osThreadNew(WriteM25P16_Task, &WriteM25P16BinarySemHandle, nullptr);//创建后台任务
}
裸机上就是
/**
* @brief M25P16存储管理初始化
* @note 初始化SPI与M25P16,然后创建写入任务,以便可以在后台完成写入操作。
*/
inline void StorageManage_M25P16_Init()
{
SPI_FLASH_Init();
writeNumber = 0;
writeM25P16_flag=0;
}
三、代码
总的来说,本篇并不具备太大的实用价值(因为我的SD卡快到了),更适合作为一个思维引导,从分析问题→解决思路→搭建框架→完善填充→实现系统,从框架到系统的这样一个思路历程
1,应用情景
在与电脑传输数据的过程中,那么就可以编写一个模块用来调用刚才所写的接口
void TransData_Init()
{
StorageManage_M25P16_Init();//初始化存储管理模块
PCtoMCU_USART_Init(115200); //初始化PC串口传输模块
}
void TransData_Start()
{
//开始写入
if (StorageManage_M25P16_Writing_Start(file_id))
PCtoMCU_USART_Start(prxdBuffer);//开始接收数据
else
Error_Handler();
}
void TransData_Stop()
{
PCtoMCU_USART_Stop(); //停止接收数据
StorageManage_M25P16_Writing_End(file_id);//结束写入
}
/**中断回调函数*/
void TransData_Callback()
{
StorageManage_M25P16_Writing(file_id, prxdBuffer);
PCtoMCU_USART_Receive(prxdBuffer,writeNumber);
}
2,功能模块层
之前的代码只是个基础框架,后续应增加各种条件判断及处理以应对不同情景
StorageManage_M25P16.h
/**
* @brief M25P16存储管理
* @note 存储管理,基于M25P16,以256字节的页为基本单位。同时兼有索引功能。目前分为两部分,一部分是索引表,另一部分是数据存储。
* 索引表: 由一页构成,一个索引为4个字节,末尾追加一个索引作为结束地址,管理的最小单位为一页。最大管理文件数为63
* 数据存储: 最小占用单位为一页,即便写入一个字节,也至少占用一页。
*/
#ifndef STORAGEMANAGE_M25P16_H
#define STORAGEMANAGE_M25P16_H
#include "ZQ_conf.h"
#if USE_StorageManage_M25P16
/**头文件*/
#include "MySPI_M25P16.h"
#include "TransData.h"
#include "cmsis_os2.h"
#include "stm32f4xx_hal.h"
/**宏定义*/
#define StorageManage_M25P16_Index_StartAddr 0x00000100// 由于索引表一共256字节,故索引1所对应的文件起始地址256
/**变量声明*/
extern uint8_t *prxdBuffer;//对外提供的接收缓冲区
extern uint16_t writeNumber;
extern osSemaphoreId_t WriteM25P16BinarySemHandle;
extern uint32_t m25p15addr;//存储地址
extern uint8_t *pwriteBuffer;
/**函数声明*/
void WriteM25P16_Task(void *argument);
//获取文件起始地址
inline uint32_t StorageManage_M25P16_GetStartAddr(uint8_t fileIndex)
{
return SPI_FLASH_ReadDWord((fileIndex - 1) << 2);
}
//获取文件结束地址
inline uint32_t StorageManage_M25P16_GetEndAddr(uint8_t fileIndex)
{
return SPI_FLASH_ReadDWord(fileIndex << 2);
}
/**
* @brief M25P16存储管理初始化
* @note 初始化SPI与M25P16,然后创建写入任务,以便可以在后台完成写入操作。
*/
inline void StorageManage_M25P16_Init()
{
SPI_FLASH_Init();
writeNumber = 0;
WriteM25P16BinarySemHandle=osSemaphoreNew(1, 0, nullptr);
osThreadNew(WriteM25P16_Task, &WriteM25P16BinarySemHandle, nullptr);
}
/**
* @brief 先查看索引获得起始地址,再写入数据,写入完成后建立索引。
* @note 写入数据使用的是页写入,写入过程使用的是两个256字节的缓冲数组分别存储接收的数据和准备写入M25P16的数据,
* 两个指针分别用于对应接收和写入,一个缓冲索引用于交换指针,以便接收缓冲区满后可以立即写入M25P16。对外开放的是接收指针和缓冲索引,
* 如果缓冲索引已满,那么将交换指针,释放信号量,以便写入任务可以将数据写入M25P16。
* 查找索引表依靠的是宏定义的代码
* @param fileIndex 文件索引,最大为63。值的范围在1-63
* @param prxBuffer 接收缓冲区指针,用于交换接收缓冲区和写入缓冲区
* @param writeFlag 文件传输标志,用于判断文件是否写入完成,如果值为0,说明文件正在写入,如果值为1,说明文件写入完成
*/
/**
* @brief 写入数据的开始
* @param fileIndex
*/
inline bool StorageManage_M25P16_Writing_Start(uint8_t fileIndex)
{
//查找索引
m25p15addr = StorageManage_M25P16_GetStartAddr(fileIndex);//获取存储起始地址
return m25p15addr != 0xFFFFFFFF;
}
/**
* @brief 写入数据的过程
* @note 写入数据的过程,写入过程使用的是两个256字节的缓冲数组分别存储接收的数据和准备写入M25P16的数据。
* 因为要交换指针,所以指针必须是引用型
* @param fileIndex 文件索引,最大为63。值的范围在1-63
* @param prxBuffer
*/
inline void StorageManage_M25P16_Writing(uint8_t fileIndex, uint8_t *&prxBuffer)//正在写入
{
++m25p15addr;
++writeNumber; //用于交代写入的字节数
if ((m25p15addr%256) == 0)// 对m25p15addr取模256,如果等于0,则说明该页已经写满,需要换页
{
uint8_t *pTemp = prxdBuffer;
prxdBuffer = pwriteBuffer;
pwriteBuffer = pTemp;
osSemaphoreRelease(WriteM25P16BinarySemHandle);
}
}
/**
* @brief 写入过程的结束
* @note 追加索引,同时写入索引表
* @param fileIndex 文件索引,最大为63。值的范围在1-63
*/
inline void StorageManage_M25P16_Writing_End(uint8_t fileIndex)
{
//把缓冲区残余的数据写入M25P16
if (writeNumber)
{
SPI_FLASH_PageWrite(pwriteBuffer, m25p15addr-writeNumber, writeNumber);
}
//追加索引
SPI_FLASH_PageWrite((uint8_t *) (&m25p15addr), fileIndex<<2, 4);
}
/**
* @brief 读取一字节数据
* @note 读取字节,读取完成后,返回读取是否结束。由于可能会经常调用,所以有了这三个静态变量。
* @param data
* @param Index
* @return 1表示读取正在读取,0表示读取结束
*/
inline bool StorageManage_M25P16_ReadByte(uint8_t *data, uint8_t Index)
{
static uint8_t oldIndex = Index;
static uint32_t readAddr = StorageManage_M25P16_GetStartAddr(Index);
static uint32_t endAddr = StorageManage_M25P16_GetEndAddr(Index);
//索引改变,重新获取起始地址
if (oldIndex != Index)
{
oldIndex = Index;
readAddr = StorageManage_M25P16_GetStartAddr(Index);
endAddr = StorageManage_M25P16_GetEndAddr(Index);
}
if (readAddr < endAddr)
{
*data = SPI_FLASH_ReadByte((readAddr++));
return true;
}
else
{
return false;
}
}
/**
* @brief 读取缓冲区数据
* @note 读取缓冲区数据,读取完成后,返回读取是否结束。由于往往只会会调用一次,所以就用临时变量代替
* @param pBuffer
* @param NumByteToRead
* @param Index
* @return
*/
inline bool StorageManage_M25P16_BufferRead(uint8_t *pBuffer,
uint16_t NumByteToRead, uint8_t Index)
{
volatile uint32_t readAddr = StorageManage_M25P16_GetStartAddr(Index);
volatile uint32_t endAddr = StorageManage_M25P16_GetEndAddr(Index);
if (endAddr - endAddr == NumByteToRead)
{
SPI_FLASH_BufferRead(pBuffer, readAddr, NumByteToRead);
readAddr += NumByteToRead;
return true;
}
return false;
}
/**
* @brief 格式化,同时建立索引1的起始地址
*/
inline void StorageManage_M25P16_EraseAll()
{
uint32_t addr = StorageManage_M25P16_Index_StartAddr;
//先格式化
SPI_FLASH_ChipErase();
//在写入起始地址
SPI_FLASH_PageWrite((uint8_t *) (&addr), 0, 4);
}
获取文件结束索引
//inline uint32_t StorageManage_M25P16_GetEndIndex(uint32_t addr)
//{
// return ((addr / 256) + 1) * 256;//整除得到页码,加一后获得该地址的末尾页码,再乘以256得到页码的终止地址
//}
#endif//USE_StorageManage_M25P16
#endif//STORAGEMANAGE_M25P16_H
StorageManage_M25P16.c
#include "StorageManage_M25P16.h"
#if USE_StorageManage_M25P16
#include "MySPI.h"
#include "MySPI_M25P16.h"
#include "cmsis_os2.h"
/**变量*/
uint8_t rxdBuffer1[256];
uint8_t rxdBuffer2[256];
uint8_t *prxdBuffer = rxdBuffer1;
uint8_t *pwriteBuffer = rxdBuffer2;
uint16_t writeNumber;//接收缓冲区索引,用于交换指针
uint32_t m25p15addr;//存储地址
osSemaphoreId_t WriteM25P16BinarySemHandle;
void WriteM25P16_Task(void *argument)
{
for (;;)
{
osSemaphoreAcquire(WriteM25P16BinarySemHandle, osWaitForever);
SPI_FLASH_PageWrite(pwriteBuffer, m25p15addr-writeNumber, writeNumber);
writeNumber = 0;//重置写入的字节数
}
}
#endif
/*为了提高写入效率,先把剩余页数写完,然后在一页一页写入*/
//void StorageManage_M25P16_Writing(uint8_t fileIndex, uint8_t *&prxBuffer)
//{
// //查找索引
// static bool isFirst = true;
// if (isFirst)
// {
// isFirst = false;
// m25p15addr = StorageManage_M25P16_GetStartAddr(fileIndex);//获取存储起始地址
// }
//
// if (writeNumber == 0)
// {
// uint8_t *pTemp = prxdBuffer;
// prxdBuffer = pwriteBuffer;
// pwriteBuffer = pTemp;
// osThreadNew(WriteM25P16_Task, &WriteM25P16BinarySemHandle, nullptr);
// }
//}
MySPI_M25P16.h
#ifndef __SPI_FLASH__H__
#define __SPI_FLASH__H__
#include "ZQ_conf.h"
#if USE_SPI_FLASH
/**头文件*/
#include "MySPI.h"
#include "stm32f4xx_hal.h"
#include "FreeRTOS.h"
#include "task.h"
/*标志*/
#define Dummy_Byte 0xFF
#define WIP_Flag 0x01
/*FLASH常用命令*/
#define WriteEnable 0x06 // 写使能
#define ReadStatusReg 0x05// 读状态寄存器
#define WriteStatusReg 0x01
#define ReadData 0x03
#define PageProgram 0x02// 页写入
#define BlockErase 0xD8 // 块擦除
#define SectorErase 0x20
#define ChipErase 0xC7// 整片擦除
/**宏定义*/
#define FLASH_SpiHandle hspi3
/**函数*/
void SPI_FLASH_Init();// flash芯片地址初始化——解锁保护
inline uint8_t SPI_FLASH_SendByte(uint8_t byte)// 送写
{
uint8_t temp = 0;
HAL_SPI_TransmitReceive(&FLASH_SpiHandle, &byte, &temp, 1, 1);
return temp;
}
inline void SPI_FLASH_WriteEnable() // 送写使能
{
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(WriteEnable); /* 发送写使能命令*/
SPI_FLASH_CS_HIGH();
}
inline void SPI_FLASH_WaitForWriteEnd() // 等待写完成
{
uint8_t FLASH_Status;
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(ReadStatusReg); /* 发送 读状态寄存器 命令 */
do
{
taskENTER_CRITICAL();
FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);
taskEXIT_CRITICAL();
} while (FLASH_Status & WIP_Flag);// 检测BUSY位是否为0,0表示已完成不忙
SPI_FLASH_CS_HIGH();
}
uint8_t SPI_FLASH_ReadByte(uint32_t ReadAddr);
void SPI_FLASH_WriteByte(uint32_t WriteAddr, uint8_t byte);
inline uint32_t SPI_FLASH_ReadDWord(uint32_t ReadAddr)
{
SPI_FLASH_CS_LOW();
// 发送Read Data指令(0x03)
SPI_FLASH_SendByte(ReadData);
// 发送地址
SPI_FLASH_SendByte((ReadAddr >> 16) & 0xFF);// 高8位
SPI_FLASH_SendByte((ReadAddr >> 8) & 0xFF); // 中8位
SPI_FLASH_SendByte(ReadAddr & 0xFF); // 低8位
// 读取4个字节的数据
uint32_t data = 0;
for (uint8_t i = 0; i < 4; ++i)
{
data |=(SPI_FLASH_SendByte(Dummy_Byte)<<8*i);
}
SPI_FLASH_CS_HIGH();
return data;
}
void SPI_FLASH_SectorErase(uint32_t SectorAddr);// 擦除扇区
inline void SPI_FLASH_ChipErase()// 整片擦除
{
SPI_FLASH_WriteEnable(); /* 发送FLASH写使能命令 */
/* 擦除扇区 */
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(ChipErase);// 低位
SPI_FLASH_CS_HIGH();
SPI_FLASH_WaitForWriteEnd();// 等待擦除完毕
}
inline void SPI_FLASH_PageWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)// 页写入
{
// SPI_FLASH_WaitForWriteEnd(); // 千辛万苦才得来的
SPI_FLASH_WriteEnable(); /* 发送FLASH写使能命令 */
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(PageProgram);
SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
SPI_FLASH_SendByte(WriteAddr & 0xFF);
/* 写入数据*/
while (NumByteToWrite--)
{
SPI_FLASH_SendByte(*(pBuffer++));
}
SPI_FLASH_CS_HIGH();
// SPI_FLASH_WaitForWriteEnd(); /* 等待写入完毕*/
}
void SPI_FLASH_BufferRead(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead); // 读数据流
void SPI_FLASH_BufferWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);// 写数据流
#endif// USE_SPI_FLASH
#endif//!__SPI_FLASH__H__
MySPI_M25P16.c
#include "MySPI_M25P16.h"
#if USE_SPI_FLASH
/**头文件*/
#include "MySPI.h"
/**SPI初始化*/
void SPI_FLASH_Init()
{
/*初始化可以不进行,默认为0x00*/
SPI_FLASH_WriteEnable();// 使能读写
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(0x50); /*这是反复改变BP位的关键*/
SPI_FLASH_CS_HIGH();
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(WriteStatusReg);
SPI_FLASH_SendByte(0x00);
SPI_FLASH_CS_HIGH();
SPI_FLASH_WaitForWriteEnd();
}
/**
* @brief 擦除FLASH扇区
* @param SectorAddr:要擦除的扇区地址
* @retval 无
*/
void SPI_FLASH_SectorErase(uint32_t SectorAddr)
{
SPI_FLASH_WriteEnable(); /* 发送FLASH写使能命令 */
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(SectorErase);
SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);// 高位
SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8); // 中位
SPI_FLASH_SendByte(SectorAddr & 0xFF); // 低位
SPI_FLASH_CS_HIGH();
SPI_FLASH_WaitForWriteEnd();// 等待擦除完毕
}
/**
* @brief 读取FLASH数据
* @param pBuffer,存储读出数据的指针
* @param ReadAddr,读取地址
* @param NumByteToRead,读取数据长度
* @retval 无
*/
void SPI_FLASH_BufferRead(uint8_t *pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(ReadData);
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
SPI_FLASH_SendByte((ReadAddr & 0xFF00) >> 8);
SPI_FLASH_SendByte(ReadAddr & 0xFF);
/* 读取数据 */
while (NumByteToRead--)
{
*pBuffer = SPI_FLASH_SendByte(Dummy_Byte);
pBuffer++;
}
SPI_FLASH_CS_HIGH();
}
uint8_t SPI_FLASH_ReadByte(uint32_t ReadAddr)
{
uint8_t temp;
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(ReadData);
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
SPI_FLASH_SendByte((ReadAddr & 0xFF00) >> 8);
SPI_FLASH_SendByte(ReadAddr & 0xFF);
temp = SPI_FLASH_SendByte(Dummy_Byte);
SPI_FLASH_CS_HIGH();
return temp;
}
void SPI_FLASH_WriteByte(uint32_t WriteAddr, uint8_t byte)
{
// SPI_FLASH_WaitForWriteEnd(); //
SPI_FLASH_WriteEnable(); /* 发送FLASH写使能命令 */
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(PageProgram);
SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
SPI_FLASH_SendByte(WriteAddr & 0xFF);
SPI_FLASH_SendByte(byte);
SPI_FLASH_CS_HIGH();
// SPI_FLASH_WaitForWriteEnd(); // 94.8us,如果是写入慢的可以加入这句
}
// 自定义的min函数
inline uint32_t min(uint32_t a, uint32_t b)
{
return a < b ? a : b;
}
void SPI_FLASH_BufferWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
const uint32_t pageSize = 256; // M25P16的页大小是256字节
uint32_t currentPageAddr = WriteAddr & ~(pageSize - 1); // 获取当前页的起始地址
uint16_t remainingBytesInPage = pageSize - (WriteAddr % pageSize);// 计算当前页剩余空间
// 函数内联写使能和页编程命令
auto sendWriteCommandAndAddress = [&](uint32_t addr) {
SPI_FLASH_WriteEnable();
SPI_FLASH_CS_LOW();
SPI_FLASH_SendByte(PageProgram);
SPI_FLASH_SendByte((addr & 0xFF0000) >> 16);
SPI_FLASH_SendByte((addr & 0xFF00) >> 8);
SPI_FLASH_SendByte(addr & 0xFF);
};
while (NumByteToWrite > 0)
{
if (WriteAddr != currentPageAddr) // 如果开始新的页
{
sendWriteCommandAndAddress(currentPageAddr); // 发送写使能命令和页编程地址
remainingBytesInPage = pageSize;
}
uint16_t bytes_written = min(NumByteToWrite, remainingBytesInPage);
// 写入数据到当前页
for (uint16_t i = 0; i <bytes_written; ++i)
{
SPI_FLASH_SendByte(*(pBuffer++));
}
SPI_FLASH_CS_HIGH();
// 更新状态
NumByteToWrite -= bytes_written;
WriteAddr += bytes_written;
// 跳转到下一页
currentPageAddr += pageSize;
}
// 确保最后的写入操作完成
SPI_FLASH_WaitForWriteEnd();
}
//void SPI_FLASH_BufferWrite(uint8_t *pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
//{
// const uint32_t pageSize = 256; // M25P16的页大小是256字节
// uint32_t currentPageAddr = WriteAddr & ~(pageSize - 1); // 获取当前页的起始地址
// uint16_t remainingBytesInPage = pageSize - (WriteAddr % pageSize);// 计算当前页剩余空间
//
// while (NumByteToWrite > 0)
// {
// // 如果剩余的页空间大于或等于要写入的数据量
// if (remainingBytesInPage >= NumByteToWrite)
// {
// // 发送写使能命令
// SPI_FLASH_WriteEnable();
// SPI_FLASH_CS_LOW();
// SPI_FLASH_SendByte(PageProgram);
// SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
// SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
// SPI_FLASH_SendByte(WriteAddr & 0xFF);
//
// // 写入数据到当前页
// for (uint16_t i = 0; i < NumByteToWrite; ++i)
// {
// SPI_FLASH_SendByte(*pBuffer++);
// }
//
// // 更新状态
// SPI_FLASH_CS_HIGH();
// remainingBytesInPage -= NumByteToWrite;
// NumByteToWrite = 0;// 所有数据已写入,退出循环
// }
// else
// {
// // 否则,写满当前页剩余空间
// SPI_FLASH_WriteEnable();
// SPI_FLASH_CS_LOW();
// SPI_FLASH_SendByte(PageProgram);
// SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
// SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
// SPI_FLASH_SendByte(WriteAddr & 0xFF);
//
// // 写入剩余空间的数据
// for (uint16_t i = 0; i < remainingBytesInPage; ++i)
// {
// SPI_FLASH_SendByte(*pBuffer++);
// }
//
// // 更新状态
// SPI_FLASH_CS_HIGH();
// NumByteToWrite -= remainingBytesInPage;
// pBuffer += remainingBytesInPage; // 移动缓冲区指针
// WriteAddr += remainingBytesInPage;// 更新写地址
// currentPageAddr += pageSize; // 移动到下一页
// remainingBytesInPage = pageSize; // 下一页有完整的空间
// }
// }
//
// // 确保最后的写入操作完成
// SPI_FLASH_WaitForWriteEnd();
//}
#endif//USE_SPI_FLASH
作者:浮梦终焉