单片机教程:基于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

 

 

作者:浮梦终焉

物联沃分享整理
物联沃-IOTWORD物联网 » 单片机教程:基于M25P16的极简文件系统实现

发表回复