4.STM32通信接口之SPI通信(含源码)---软件SPI与W25Q64存储模块通信实战《精讲》

经过研究SPI协议和W25Q64,逐步了解了SPI的通信过程,接下来,就要进行战场实战了!跟进Whappy步伐!

目标:主要实现基于软件的SPI的STM32对W25Q64存储写入和读取操作!

开胃介绍(代码基本实现过程)

  1. 初始化 SPI 接口:
    • 配置微控制器上必要的 GPIO 引脚,将 MOSI、SCK 和 CS 设置为输出模式,MISO 设置为输入模式。
    • 根据 SPI 时钟极性(CPOL)的配置,设置 SCK 引脚的初始状态。
    • 将 CS 引脚设置为高电平,取消对 W25Q64 芯片的选择。
  2. 选择 W25Q64 芯片:
    • 将 CS 引脚拉低,选择 W25Q64 芯片,开始通信。
  3. 发送命令:
    • 在 MOSI 线上移位发送命令字节,同时切换 SCK 线。
    • 命令指示要执行的操作类型,如读、写或擦除。
    • 在命令阶段,W25Q64 可能也会在 MISO 线上移出状态信息,微控制器需要读取。
  4. 发送地址(如果需要):
    • 对于需要指定内存地址的操作,在 MOSI 线上移位发送 24 位地址。
    • W25Q64 会接受地址,并为后续的数据传输做准备。
  5. 执行数据传输:
    • 根据操作类型,微控制器会在 MOSI 线上移位发送数据(写操作),或从 MISO 线上移位读取数据(读操作)。
    • 切换 SCK 线以同步数据传输。
    • 对于读操作,W25Q64 会在 MISO 线上移出请求的数据。
    • 对于写操作,W25Q64 会接受 MOSI 线上的数据,并将其存储到相应的内存位置。
  6. 取消选择 W25Q64 芯片:
    • 数据传输完成后,将 CS 引脚拉高,取消对 W25Q64 的选择,表示本次交互结束。

这就是与 W25Q64 闪存芯片进行软件 SPI 通信的主要步骤。

程序框架和上一节IIC差不多。

第一步:软件SPI协议层实现代码

时序框架:通过时序用C语言实现SPI
初始化相关的GPIO

(1)MySPI_Init(oid)

复制代码
void MySPI_Init(void)
{
    // 开启GPIOA时钟
    /*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/	
    // 配置输出引脚(SCK, MOSI, NSS)
	GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 配置输入引脚(MISO)
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	
	MySPI_W_CS(1);
	MySPI_W_SCLK(0);
	
}

SPI的模式0;实现交换数据代码(和下面的收发函数实现的功能一样)

复制代码
uint8_t MySPI_SwapByte(uint8_t Byte)
{
    uint8_t ReceiveData = 0x00;
    uint16_t i;
    
    for(i=0; i<8; i++)
    {
        // 发送最高位
        MySPI_W_MOSI((Byte & 0x80) ? 1 : 0);
        
        // 左移发送数据
        Byte <<= 1;
        
        // 拉高时钟线
        MySPI_W_SCLK(1);
        
        // 左移接收数据
        ReceiveData <<= 1;
        
        // 如果MISO为1,则在接收数据最低位置1
        if(MySPI_R_MISO() == 1)
        {
            ReceiveData |= 0x01;
        }
        
        // 拉低时钟线
        MySPI_W_SCLK(0);
    }
    
    return ReceiveData;
}

这个版本:

  1. 完整发送8个bit
  2. 完整接收8个bit
  3. 返回接收到的完整字节
  4. 保留了原始的SPI通信时序

推荐使用这个版本,它更加准确地模拟了SPI通信的数据交换过程。

(2)SPI的起始和终止

复制代码
/SPI模式0
/**
 * @brief 开始SPI通信,拉低片选信号
 * @note 通常在发送数据前调用,选中从设备
 */
void MySPI_Start(void)
{
    MySPI_W_CS(0);  // 拉低CS(片选)信号,选中从设备
}

/**
 * @brief 结束SPI通信,拉高片选信号
 * @note 通常在数据传输完成后调用,取消从设备选择
 */
void MySPI_Stop(void)
{
    MySPI_W_CS(1);  // 拉高CS(片选)信号,取消从设备选择
}

(3)SPI单字节数据交换(收发)函数

复制代码
/**
 * @brief SPI单字节数据交换(收发)函数
 * @param Byte 要发送的字节
 * @return uint8_t 接收到的字节
 * @note 实现软件模拟SPI的数据交换
 */
uint8_t MySPI_SwapByte(uint8_t Byte)
{
    uint16_t i, ReceiveData = 0x00;
    
    // 按位发送和接收数据
    for(i=0; i<8; i++)
    {
        // 发送一个bit,从最高位开始
        // 使用移位操作判断当前bit是0还是1
        MySPI_W_MOSI(Byte & (0x80>>i));
        
        // 拉高时钟线,从设备在上升沿采样数据
        MySPI_W_SCLK(1);
        
        // 读取MISO上的数据
        if(MySPI_R_MISO()==1)
        {
            // 如果接收到1,则在对应位置置1
            ReceiveData |= (0x80>>i);
        }
        
        // 拉低时钟线,准备下一个bit
        MySPI_W_SCLK(0);        
    }
    
    return ReceiveData;
}
复制代码
/**
 * @brief SPI单字节数据交换(收发)函数
 * @param Byte 要发送的字节
 * @return uint8_t 接收到的字节
 * @note 实现软件模拟SPI的数据交换
 */
uint8_t MySPI_SwapByte(uint8_t Byte)
{
    uint16_t i, ReceiveData = 0x00;
    
    // 按位发送和接收数据
    for(i=0; i<8; i++)
    {
        MySPI_W_SCLK(0);
        // 发送一个bit,从最高位开始
        // 使用移位操作判断当前bit是0还是1
        MySPI_W_MOSI(Byte & (0x80>>i));
        
        // 拉高时钟线,从设备在上升沿采样数据
        MySPI_W_SCLK(1);
        
        // 读取MISO上的数据
        if(MySPI_R_MISO()==1)
        {
            // 如果接收到1,则在对应位置置1
            ReceiveData |= (0x80>>i);
        }
        
        // 拉低时钟线,准备下一个bit
        MySPI_W_SCLK(0);        
    }
    
    return ReceiveData;
}
复制代码
/**
 * @brief SPI单字节数据交换(收发)函数
 * @param Byte 要发送的字节
 * @return uint8_t 接收到的字节
 * @note 实现软件模拟SPI的数据交换
 */
uint8_t MySPI_SwapByte(uint8_t Byte)
{
    uint16_t i, ReceiveData = 0x00;
    
    // 按位发送和接收数据
    for(i=0; i<8; i++)
    {
        // 发送一个bit,从最高位开始
        // 使用移位操作判断当前bit是0还是1
        MySPI_W_MOSI(Byte & (0x80>>i));
        
        // 拉高时钟线,从设备在上升沿采样数据
        MySPI_W_SCLK(0);
        
        // 读取MISO上的数据
        if(MySPI_R_MISO()==1)
        {
            // 如果接收到1,则在对应位置置1
            ReceiveData |= (0x80>>i);
        }
        
        // 拉低时钟线,准备下一个bit
        MySPI_W_SCLK(1);        
    }
    
    return ReceiveData;
}

复制代码
/**
 * @brief SPI单字节数据交换(收发)函数
 * @param Byte 要发送的字节
 * @return uint8_t 接收到的字节
 * @note 实现软件模拟SPI的数据交换
 */
uint8_t MySPI_SwapByte(uint8_t Byte)
{
    uint16_t i, ReceiveData = 0x00;
    
    // 按位发送和接收数据
    for(i=0; i<8; i++)
    {
        MySPI_W_SCLK(1);
        // 发送一个bit,从最高位开始
        // 使用移位操作判断当前bit是0还是1
        MySPI_W_MOSI(Byte & (0x80>>i));
        
        // 拉高时钟线,从设备在上升沿采样数据
        MySPI_W_SCLK(0);
        
        // 读取MISO上的数据
        if(MySPI_R_MISO()==1)
        {
            // 如果接收到1,则在对应位置置1
            ReceiveData |= (0x80>>i);
        }
        
        // 拉低时钟线,准备下一个bit
        MySPI_W_SCLK(1);        
    }
    
    return ReceiveData;
}

总结:

SPI总共就三个函数

  • MySPI_Init():初始化通信接口
  • MySPI_Start():开始通信
  • MySPI_Stop():结束通信
  • MySPI_SwapByte():进行实际的数据交换
复制代码
/**
* @brief 初始化SPI通信接口
* @note 配置GPIO口,设置SPI通信相关引脚模式和时钟
*/
void MySPI_Init(void);

/**
* @brief 开始SPI通信
* @note 拉低片选信号(CS),选中从设备,准备开始数据传输
*/
void MySPI_Start(void);

/**
* @brief 结束SPI通信
* @note 拉高片选信号(CS),取消从设备选择,结束数据传输
*/
void MySPI_Stop(void);

/**
* @brief SPI数据交换函数
* @param Byte 要发送的字节数据
* @return uint8_t 接收到的字节数据
* @note 模拟SPI通信的数据交换过程
* 
* 功能:
* 1. 逐位发送输入字节
* 2. 同时接收从设备返回的数据
* 3. 返回接收到的完整字节
*/
uint8_t MySPI_SwapByte(uint8_t Byte);

W25Q64的

验证SPI时序的正确 实验实例:读取设备W25Q64的MID 和DID

这段文字描述了 W25Q80/16/32 系列存储器芯片如何通过 JEDEC 标准指令读取设备的身份信息。它特别说明了通过 Read JEDEC ID 指令来读取设备的制造商ID、内存类型和容量。

解释:

  1. For compatibility reasons, the W25Q80/16/32 provides several instructions to electronically determine the identity of the device :

    为了兼容性,W25Q80/16/32 提供了几种指令,允许电子设备读取存储器芯片的身份信息。这有助于识别设备及其相关参数。

  2. The Read JEDEC ID instruction is compatible with the JEDEC standard for SPI compatible serial memories that was adopted in 2003 :

    读取 JEDEC ID 指令遵循了 JEDEC 标准,这个标准在 2003 年被采纳,专门用于 SPI 兼容的串行存储器。

  3. The instruction is initiated by driving the /CS pin low and shifting the instruction code "9Fh" :

    该指令通过将芯片选择信号(/CS)拉低来启动,并且发送指令代码 9Fh9Fh 是 JEDEC ID 读取指令的指令代码。

  4. The JEDEC assigned Manufacturer ID byte for Winbond (EFh) and two Device ID bytes, Memory Type (ID15-ID8) and Capacity (ID7-ID0) are then shifted out on the falling edge of CLK with most significant bit (MSB) first as shown in figure 28 :

    启动指令后,设备会通过时钟信号(CLK)的下降沿依次将数据发送出来。具体数据包括:

    • 制造商 ID(Manufacturer ID) :在 Winbond 芯片上是 EFh(十六进制)。
    • 内存类型(Memory Type) :通过位域 ID15-ID8 表示。
    • 容量(Capacity) :通过位域 ID7-ID0 表示。 数据是按 MSB(最重要位)优先的顺序发送的。
  5. For memory type and capacity values refer to Manufacturer and Device Identification table :

    内存类型和容量的具体值可以参考设备的"制造商与设备标识表"。这意味着根据不同的设备型号,内存类型和容量值会有所不同。

第二步:基于SPI的W25Q64驱动代码的实现

通过查找W25Q64指令集来对其操作!主要利用页面编程,擦除,读取数据等基础操作

(1)W25Q64的初始化

复制代码
void W25Q64_Init(void)
{
	MySPI_Init();
}

(2)读取闪存的制造商 ID(MID)和设备 ID(DID)

指令集

复制代码
void W25Q64_ReadID(uint8_t* MID, uint16_t* DID)
{
    // 启动 SPI 通信
    MySPI_Start();

    // 发送 JEDEC ID 命令以读取闪存芯片的 ID
    MySPI_SwapByte(W25Q64_JEDEC_ID); // 发送读取 JEDEC ID 命令

    // 读取 MID(制造商 ID),并将其存入 MID 指针指向的地址
    *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 发送占位符字节并读取 MID

    // 读取 DID(设备 ID),并将其存入 DID 指针指向的地址
    *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 发送占位符字节并读取 DID 的低字节

    // 将 DID 向左移 8 位,将 DID 的高字节移到高位
    *DID <<= 8; // 将 DID 低字节移到高字节的地方

    // 读取 DID 的高字节并合并到 DID 变量中
    *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 读取 DID 高字节并合并

    // 停止 SPI 通信
    MySPI_Stop();
}

代码功能概述:

W25Q64_ReadID 函数通过 SPI 协议与 W25Q64 闪存芯片进行通信,读取闪存的制造商 ID(MID)和设备 ID(DID)。这两个 ID 是通过 JEDEC ID 命令从闪存芯片中获取的。

代码注释与解释

复制代码

c

复制代码

void W25Q64_ReadID(uint8_t* MID, uint16_t* DID) { // 启动 SPI 通信 MySPI_Start(); // 发送 JEDEC ID 命令以读取闪存芯片的 ID MySPI_SwapByte(W25Q64_JEDEC_ID); // 发送读取 JEDEC ID 命令 // 读取 MID(制造商 ID),并将其存入 MID 指针指向的地址 *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 发送占位符字节并读取 MID // 读取 DID(设备 ID),并将其存入 DID 指针指向的地址 *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 发送占位符字节并读取 DID 的低字节 // 将 DID 向左移 8 位,将 DID 的高字节移到高位 *DID <<= 8; // 将 DID 低字节移到高字节的地方 // 读取 DID 的高字节并合并到 DID 变量中 *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 读取 DID 高字节并合并 // 停止 SPI 通信 MySPI_Stop(); }

代码详解

  1. 启动 SPI 通信

    复制代码

    c

    复制代码

    MySPI_Start();

    • 调用 MySPI_Start 函数拉低 CS(片选)引脚,启动与 W25Q64 的 SPI 通信。
  2. 发送 JEDEC ID 命令

    复制代码

    c

    复制代码

    MySPI_SwapByte(W25Q64_JEDEC_ID);

    • W25Q64_JEDEC_ID 是一个常量,它代表 JEDEC ID 命令。调用 MySPI_SwapByte 发送该命令,通知 W25Q64 闪存芯片准备返回其 ID。
  3. 读取 MID(制造商 ID)

    复制代码

    c

    复制代码

    *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);

    • 发送一个占位符字节(通常是 0x00),然后通过 SPI 读取 MID(制造商 ID),将读取的值存入传入的指针 MID 指向的变量中。
  4. 读取 DID(设备 ID)

    复制代码

    c

    复制代码

    *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);

    • 再次发送占位符字节,通过 SPI 读取 DID(设备 ID)的低字节,并将其存入 DID 的低字节部分。
  5. 处理 DID 的高字节

    复制代码

    c

    复制代码

    *DID <<= 8;

    • DID 变量左移 8 位,将低字节腾出位置,为接下来的高字节准备空间。
  6. 读取 DID 的高字节并合并

    复制代码

    c

    复制代码

    *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);

    • 读取 DID 的高字节,并将其合并到 DID 变量中。|= 操作符将高字节与之前的低字节组合成完整的 16 位 DID。
  7. 停止 SPI 通信

    复制代码

    c

    复制代码

    MySPI_Stop();

    • 调用 MySPI_Stop 函数拉高 CS(片选)引脚,结束与 W25Q64 闪存的 SPI 通信。

注意细节:

  1. 占位符字节

    • 在读取 MID 和 DID 时,必须发送占位符字节(通常是 W25Q64_DUMMY_BYTE,如 0x00)。这些字节用于占据时钟周期,等待闪存芯片返回数据。
    • W25Q64_DUMMY_BYTE 是定义为常数的值,表示在不进行数据传输时发送的空字节,确保 SPI 时序正确。
  2. 字节顺序

    • 由于 MID 是 8 位,而 DID 是 16 位,因此在读取 DID 时需要先读取低字节,再读取高字节。高字节必须左移 8 位,低字节通过位或操作合并。
    • DID 高字节在低字节之后读取,并且需要将低字节的值"腾空",这通常是通过左移操作(<<= 8)来实现。
  3. SPI 通信时序

    • MySPI_SwapByte() 函数用于执行 SPI 发送和接收操作。每次调用该函数都发送一个字节并接收一个字节,因此它能同时实现发送命令和接收数据。
    • 注意:MySPI_SwapByte() 不仅发送数据,还会返回接收到的字节数据。
  4. 传入指针

    • 函数通过传入指针 MIDDID 传递读取到的 MID 和 DID。这意味着在函数外部可以直接访问这两个 ID。
  5. SPI 配置

    • 假设 MySPI_Start()MySPI_Stop() 正确配置了 SPI 接口的片选信号,并确保 SPI 数据传输时序正确,确保每个字节都能正确传输。

总结

  • W25Q64_ReadID 函数通过 SPI 协议读取 W25Q64 闪存的制造商 ID (MID) 和设备 ID (DID)。
  • 通过发送命令和读取数据字节,并使用位操作处理 DID 的高低字节,将 MID 和 DID 存储在传入的指针所指向的变量中。
  • 函数使用占位符字节与闪存芯片同步,确保 SPI 通信的时序正确。

(3)向 W25Q64 闪存芯片发送写使能命令(Write Enable)

指令集

复制代码
void W25Q64_WriteEnable(void)
{
    // 启动 SPI 通信
    MySPI_Start();

    // 发送写使能命令(0x06)以允许闪存写入
    MySPI_SwapByte(W25Q64_WRITE_ENABLE);

    // 停止 SPI 通信
    MySPI_Stop();
}

代码功能概述:

W25Q64_WriteEnable 函数通过 SPI 协议向 W25Q64 闪存芯片发送写使能命令(Write Enable)。此命令用于启用写操作,确保在进行闪存写操作(如写入数据、页编程等)之前,芯片允许进行写入。

代码注释与解释

复制代码

c

复制代码

void W25Q64_WriteEnable(void) { // 启动 SPI 通信 MySPI_Start(); // 发送写使能命令(0x06)以允许闪存写入 MySPI_SwapByte(W25Q64_WRITE_ENABLE); // 停止 SPI 通信 MySPI_Stop(); }

代码详解

  1. 启动 SPI 通信

    复制代码

    c

    复制代码

    MySPI_Start();

    • 调用 MySPI_Start 函数,通过拉低 SPI 的 CS(片选)引脚来开始与 W25Q64 闪存的通信。此时芯片被选中,开始接收命令。
  2. 发送写使能命令

    复制代码

    c

    复制代码

    MySPI_SwapByte(W25Q64_WRITE_ENABLE);

    • W25Q64_WRITE_ENABLE 是定义的常量,其值为 0x06,这是 W25Q64 闪存的写使能命令。
    • 通过调用 MySPI_SwapByte(),向闪存发送该命令。在 SPI 总线上,MySPI_SwapByte 不仅发送数据,还接收从闪存芯片返回的数据。
    • 这里的 MySPI_SwapByte() 会发送一个字节 0x06(写使能命令)到 W25Q64,并确保 SPI 时序正确。
  3. 停止 SPI 通信

    复制代码

    c

    复制代码

    MySPI_Stop();

    • 调用 MySPI_Stop 函数,将 CS(片选)引脚拉高,停止与 W25Q64 闪存的 SPI 通信。此时,芯片被取消选中,通信结束。

注意细节

  1. 写使能命令的作用

    • 写使能命令 0x06 是 W25Q64 闪存芯片的一个基本命令。它使能闪存的写操作。如果不发送写使能命令,后续的写操作将被闪存芯片忽略,无法进行。
    • 在实际的闪存操作中(如擦除、写入数据等),每次都需要先发送写使能命令以允许写操作。
  2. 命令的发送顺序和时序

    • 在 SPI 通信中,必须遵循正确的时序和命令顺序。MySPI_SwapByte 函数确保发送的命令按照 SPI 协议正确发送,同时接收返回的数据。
    • 即使写使能命令没有返回数据,MySPI_SwapByte 也会等待 SPI 时钟周期,确保数据传输完成。
  3. SPI 总线上的 CS 管脚

    • 在该函数中,MySPI_StartMySPI_Stop 控制 CS(片选)引脚的状态。CS 必须在发送命令之前拉低,命令发送完成后再拉高,确保闪存芯片接收到整个命令。
    • CS 管脚的控制确保只有一个设备在某个时刻与 MCU 通信。这个操作必须非常精确,否则可能导致与其他 SPI 设备发生冲突。
  4. 写使能命令后的操作

    • 写使能命令发送成功后,W25Q64 闪存芯片就会允许进行后续的写入操作(如页面编程、数据写入等)。因此,执行此命令后可以安全地进行其他写入操作,如写页、擦除扇区等。
  5. 性能优化

    • W25Q64_WriteEnable 是一个非常常见的操作,每次写操作前都需要发送该命令。如果在实际应用中频繁调用该函数,可能会增加通信的延迟。如果希望优化性能,可以在写操作时减少多次调用 WriteEnable

总结

  • W25Q64_WriteEnable 函数通过 SPI 协议向 W25Q64 闪存芯片发送 0x06 的写使能命令,确保后续的写操作可以成功执行。
  • 通过 MySPI_StartMySPI_Stop 控制 SPI 通信的开始和结束。
  • 写使能命令是所有写操作的前置条件,必须在每次写操作之前执行

(4)等待 W25Q64 闪存芯片完成当前操作(如写入、擦除等)

Busy 位的作用

  • W25Q64 的状态寄存器 1 中的 Busy 位(第 0 位)表示芯片是否正在进行操作。忙碌时该位为 1,表示正在进行擦除、编程等操作;如果该位为 0,则表示闪存芯片的操作已经完成,可以进行后续操作。

  • void W25Q64_WaitBusy(void)
    {
    // 定义一个超时计数器,防止死循环
    uint32_t Timeout = 100000;

    复制代码
      // 启动 SPI 通信
      MySPI_Start();
      
      // 发送读取状态寄存器 1 命令,W25Q64 的状态寄存器 1 用于获取芯片的忙碌状态
      MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
      
      // 通过 SPI 读取状态寄存器 1 的数据,检查忙碌位
      while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)  // 判断 Busy 位
      {
          Timeout--;  // 超时计数器递减
          if(Timeout == 0)  // 如果超时,跳出循环
          {
              break;
          }
      }
      
      // 停止 SPI 通信
      MySPI_Stop();

    }

(5)通过 SPI 向 W25Q64 闪存芯片的指定地址进行页面编程

复制代码
void W25Q64_PageProgram(uint32_t Address, uint8_t* Array, uint16_t Lenght)
{
    uint16_t i;
    
    // 启用写操作(发送写使能命令)
    W25Q64_WriteEnable();

    // 启动 SPI 通信
    MySPI_Start();
    
    // 发送页面编程命令(0x02)
    MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
    
    // 发送目标地址(24 位地址:高 8 位、中 8 位、低 8 位)
    MySPI_SwapByte((Address >> 16) & 0xFF);  // 地址的高 8 位
    MySPI_SwapByte((Address >> 8) & 0xFF);   // 地址的中 8 位
    MySPI_SwapByte(Address & 0xFF);           // 地址的低 8 位

    // 发送数据(最多 256 字节,写入指定地址)
    for(i = 0; i < Lenght; i++)
    {
        MySPI_SwapByte(Array[i]);  // 发送数据字节
    }

    // 停止 SPI 通信
    MySPI_Stop();
    
    // 等待闪存操作完成(等待闪存忙碌标志清除)
    W25Q64_WaitBusy();
}

代码功能概述:

W25Q64_PageProgram 函数用于通过 SPI 向 W25Q64 闪存芯片的指定地址进行页面编程。页面编程将数据写入指定地址的页中,每个页的大小通常为 256 字节。该函数首先发送写使能命令,然后发送页面编程命令,最后传输数据。

代码详解

  1. 写使能命令

    复制代码

    c

    复制代码

    W25Q64_WriteEnable();

    • 通过调用 W25Q64_WriteEnable 向 W25Q64 闪存发送写使能命令 0x06,使闪存允许执行写操作。
  2. 启动 SPI 通信

    复制代码

    c

    复制代码

    MySPI_Start();

    • 通过 MySPI_Start 函数拉低 CS(片选)引脚,开始与 W25Q64 闪存的 SPI 通信。
  3. 发送页面编程命令

    复制代码

    c

    复制代码

    MySPI_SwapByte(W25Q64_PAGE_PROGRAM);

    • 发送页面编程命令 0x02W25Q64_PAGE_PROGRAM),该命令用于启动页面写入操作。
  4. 发送地址

    复制代码

    c

    复制代码

    MySPI_SwapByte((Address >> 16) & 0xFF); MySPI_SwapByte((Address >> 8) & 0xFF); MySPI_SwapByte(Address & 0xFF);

    • 由于 W25Q64 闪存支持 24 位地址(3 字节),所以需要将传入的 32 位地址 Address 拆分成高 8 位、中 8 位和低 8 位,依次通过 SPI 发送。
    • 使用位移操作将地址拆分成 3 个字节。
  5. 发送数据

    复制代码

    c

    复制代码

    for(i = 0; i < Lenght; i++) { MySPI_SwapByte(Array[i]); }

    • 通过循环遍历 Array 中的数据,将每个字节依次发送到 W25Q64 闪存芯片。Lenght 参数表示要写入的字节数(最多 256 字节)。
    • 这里假设写入的数据不会超过闪存页面的大小,即最多 256 字节。如果需要写入更大的数据,必须分多次进行页面写入。
  6. 停止 SPI 通信

    复制代码

    c

    复制代码

    MySPI_Stop();

    • 通过 MySPI_Stop 函数将 CS(片选)引脚拉高,结束与 W25Q64 闪存的 SPI 通信。
  7. 等待闪存完成操作

    复制代码

    c

    复制代码

    W25Q64_WaitBusy();

    • 调用 W25Q64_WaitBusy 函数,等待闪存完成当前的编程操作。该函数会检查闪存的忙碌状态,直到写入操作完成。

注意细节

  1. 页面编程命令

    • W25Q64 的页面编程命令 0x02 是用于将数据写入指定地址的页面中。每个页面的大小通常为 256 字节,因此在调用该命令时,最大写入数据长度为 256 字节。
    • 在实际操作中,如果数据长度超过 256 字节,需要将数据分成多个页面进行写入。可以通过地址自增和多次调用此函数来实现。
  2. 地址范围

    • W25Q64 支持最大 24 位地址(3 字节地址),即最大地址为 0xFFFFFF,范围为 0 到 16MB。因此,函数中的地址参数 Address 应在 0 到 16MB 之间。
  3. 数据长度

    • Lenght 参数指定写入的数据字节数。由于每个页面的大小为 256 字节,所以该函数最多一次性支持写入 256 字节的数据。如果需要写入的数据超过 256 字节,需要拆分为多个页面写入。
  4. 闪存的写入周期

    • 每次页面编程后,W25Q64 闪存需要一段时间来完成写入操作。在此期间,芯片处于忙碌状态。W25Q64_WaitBusy 函数确保在闪存完成写入操作后才会进行下一步操作。
  5. 数据验证

    • 本函数没有实现数据验证。在实际应用中,可能需要在编程后通过读取该地址的数据并与原数据进行比较,来确保写入成功。
  6. 性能优化

    • 该函数每次写入最多 256 字节,如果数据长度较大,可能需要多次调用 W25Q64_PageProgram 函数进行数据写入。如果有较大的数据块需要写入,可以考虑优化为批量写入模式,减少重复的 SPI 命令传输。

总结

W25Q64_PageProgram 函数通过 SPI 协议将指定的数据写入 W25Q64 闪存的指定页面。它首先通过 W25Q64_WriteEnable 启用写操作,随后发送页面编程命令和地址,并将数据逐字节写入闪存。最后,通过 W25Q64_WaitBusy 函数等待闪存完成写入操作。

(6)W25Q64 闪存芯片进行 4KB 扇区的擦除操作

指令集

函数概述:

该函数 W25Q64_SectorErase 用于对 W25Q64 闪存芯片进行 4KB 扇区的擦除操作。擦除操作是通过 SPI 总线向 W25Q64 发送擦除命令和目标地址来实现的。函数内部包括启用写权限、发送擦除命令、传输地址以及等待擦除完成的步骤。

复制代码
void W25Q64_SectorErase(uint32_t Address)
{
    // 1. 启用写操作:调用 W25Q64_WriteEnable 函数,设置闪存为可写状态
    //    这步是必须的,因为 W25Q64 的擦除、写入操作都需要先启用写权限。
    W25Q64_WriteEnable();
    
    // 2. 启动 SPI 通信:调用 MySPI_Start 启动 SPI 总线通信
    //    SPI 总线用于与 W25Q64 芯片进行数据交换。
    MySPI_Start();
    
    // 3. 发送扇区擦除命令:发送擦除命令 0x20,表示擦除 4KB 扇区
    //    该命令告知 W25Q64 执行一个 4KB 扇区擦除操作。
    MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
    
    // 4. 发送地址:地址分成 3 个字节进行传输(从高字节到低字节)
    //    W25Q64 使用 24 位地址来定位擦除区域,按大端顺序发送地址的三个字节。
    MySPI_SwapByte((Address >> 16) & 0xFF);  // 发送地址的高字节
    MySPI_SwapByte((Address >> 8) & 0xFF);   // 发送地址的中间字节
    MySPI_SwapByte(Address & 0xFF);          // 发送地址的低字节
    
    // 5. 停止 SPI 通信:调用 MySPI_Stop 停止 SPI 总线通信
    //    在完成命令发送后,关闭 SPI 总线。
    MySPI_Stop();
    
    // 6. 等待擦除完成:调用 W25Q64_WaitBusy 函数,确保擦除操作完成
    //    擦除操作可能需要一定的时间,调用此函数等待闪存芯片的忙碌标志变为"未忙"状态,
    //    表示擦除操作已完成,可以进行其他操作。
    W25Q64_WaitBusy();
}

代码功能:

该函数的作用是擦除指定地址处的 4KB 扇区。整个过程包括以下步骤:

  1. 启用写操作:在擦除操作之前,必须先启用写操作,这通常是为了防止误操作对闪存内容进行修改。

  2. SPI 启动与数据传输:通过 SPI 协议与 W25Q64 芯片通信,发送擦除命令以及目标地址。擦除命令是 0x20,后面跟着的是 24 位地址数据。

  3. 等待操作完成 :擦除操作是一个相对较长的过程,因此在擦除命令发出后,程序通过 W25Q64_WaitBusy 函数等待闪存芯片的忙碌状态变为未忙碌,确保擦除完成。

关键点总结:

  • W25Q64_WriteEnable:开启写操作权限,允许对 W25Q64 进行擦除操作。
  • W25Q64_SECTOR_ERASE_4KB:执行 4KB 扇区擦除命令(0x20)。
  • SPI 通信 :通过 MySPI_StartMySPI_SwapByteMySPI_Stop 与 W25Q64 进行数据传输。
  • W25Q64_WaitBusy:等待擦除操作完成,确保擦除过程不被中断。

这段代码的目的就是精确控制 W25Q64 闪存的擦除操作,确保数据的完整性和操作的成功执行。

(7)从 W25Q64 闪存芯片读取指定地址的数据

函数概述:

该函数 W25Q64_ReadData 用于从 W25Q64 闪存芯片读取指定地址的数据。通过 SPI 协议发送读取命令、地址和所需读取的字节数,并将读取的数据存入 Array 数组中。函数支持任意长度的数据读取。

复制代码
void W25Q64_ReadData(uint32_t Address, uint8_t* Array, uint32_t Length)
{
    uint32_t i;
    
    // 1. 启动 SPI 通信:通过 MySPI_Start 启动 SPI 总线
    MySPI_Start();
    
    // 2. 发送读取命令:发送 W25Q64 的读取数据命令(通常为 0x03)
    MySPI_SwapByte(W25Q64_READ_DATA);  // 发送读取命令(0x03)
    
    // 3. 发送 24 位地址:将目标地址分成三个字节并按大端格式发送
    MySPI_SwapByte((Address >> 16) & 0xFF);  // 发送地址的高字节
    MySPI_SwapByte((Address >> 8) & 0xFF);   // 发送地址的中间字节
    MySPI_SwapByte(Address & 0xFF);          // 发送地址的低字节
    
    // 4. 读取数据:读取指定长度的数据,并将读取的数据存入 Array 数组
    //    每次从 SPI 总线接收一个字节,并存储到 Array[i] 中
    for(i = 0; i < Length; i++)
    {
        Array[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);  // 发送占位符字节并接收返回数据
    }
    
    // 5. 停止 SPI 通信:通过 MySPI_Stop 停止 SPI 总线
    MySPI_Stop();
}

代码功能:

该函数的主要功能是从指定的地址读取数据并将其存储到数组 Array 中。以下是具体步骤:

  1. SPI 启动 :通过 MySPI_Start 启动与 W25Q64 闪存的 SPI 通信。

  2. 发送读取命令 :通过 SPI 向闪存发送读取数据命令,通常为 0x03,表示读取操作。

  3. 发送地址:闪存地址是 24 位的,函数通过拆分地址的三个字节,并按大端格式(高字节先)发送。

  4. 读取数据 :通过 MySPI_SwapByte 发送一个占位符字节(通常是 0xFF 或其他不重要的值),同时接收从闪存返回的数据并存储到 Array 数组中。每次读取一个字节,直到读取指定长度的所有数据。

  5. SPI 停止 :通过 MySPI_Stop 停止 SPI 通信,完成数据读取操作。

关键点总结:

  • W25Q64_READ_DATA :读取数据命令,通常为 0x03
  • 地址格式:W25Q64 使用 24 位地址,发送时按高字节到低字节顺序。
  • 数据读取 :通过 SPI 接收数据,将其存储到 Array 数组中,读取的字节数由 Length 参数指定。
  • 占位符字节MySPI_SwapByte(W25Q64_DUMMY_BYTE) 用于发送一个占位符字节(0xFF),同时接收返回的有效数据。W25Q64_DUMMY_BYTE 通常是一个不重要的字节,用于生成时钟并接收数据。

使用场景:

此函数适用于需要从 W25Q64 闪存芯片读取数据的场景,例如读取存储在闪存中的配置数据、文件数据、程序代码等。可以根据实际需要调整读取的长度,读取任意数量的字节。

第三步:STM32F10x 单片机控制 OLED 显示屏和 W25Q64 闪存芯片

代码概述:

该程序通过 STM32F10x 单片机控制 OLED 显示屏和 W25Q64 闪存芯片,实现了读取和写入闪存数据的操作,并通过 OLED 屏幕显示相关的 MID(制造商ID)和 DID(设备ID),以及闪存中的数据读取和写入值。

复制代码
#include "stm32f10x.h"                  // 设备头文件,包含了STM32F10x系列的所有硬件抽象接口
#include "Delay.h"                       // 延时函数头文件
#include "OLED.h"                        // OLED显示模块的头文件
#include "W25Q64.h"                      // W25Q64闪存模块的头文件

// 定义变量用于存储W25Q64芯片的制造商ID和设备ID
uint8_t MID;    // 存储制造商ID
uint16_t DID;   // 存储设备ID

// 定义用于写入W25Q64的数组,模拟写入数据
uint8_t ArrayWrite[] = {0xAA, 0xBB, 0xCC, 0xDD};  // 写入的数据数组
uint8_t ArrayRead[4];  // 用于存储从W25Q64读取的数据

int main(void)
{
    /* 模块初始化 */
    OLED_Init();  // 初始化OLED显示屏
    W25Q64_Init(); // 初始化W25Q64闪存模块
    
    // 在OLED屏幕上显示一些提示信息
    OLED_ShowString(1, 1, "MID:   DID:"); // 第1行显示 "MID: DID:"
    OLED_ShowString(2, 1, "W:");  // 第2行显示 "W:",用于显示写入数据
    OLED_ShowString(3, 1, "R:");  // 第3行显示 "R:",用于显示读取数据
    
    // 从W25Q64读取MID和DID并显示
    W25Q64_ReadID(&MID, &DID);  // 读取W25Q64的MID和DID
    OLED_ShowHexNum(1, 5, MID, 2);  // 显示MID(制造商ID)到OLED屏幕的第1行,从第5列开始,显示2个十六进制数
    OLED_ShowHexNum(1, 12, DID, 4); // 显示DID(设备ID)到OLED屏幕的第1行,从第12列开始,显示4个十六进制数
    
    // 执行闪存操作:擦除扇区,写入数据并读取数据
    W25Q64_SetorErase(0x000000); // 擦除W25Q64芯片的第一个4KB扇区
    W25Q64_PageProgram(0x000000, ArrayWrite, 4); // 写入数据ArrayWrite到闪存地址0x000000,写入4个字节
    W25Q64_ReadData(0x000000, ArrayRead, 4); // 从闪存地址0x000000读取4个字节数据到ArrayRead数组
    
    // 在OLED屏幕上显示写入的数据
    OLED_ShowHexNum(2, 4, ArrayWrite[0], 2);  // 显示写入数据的第1个字节
    OLED_ShowHexNum(2, 7, ArrayWrite[1], 2);  // 显示写入数据的第2个字节
    OLED_ShowHexNum(2, 10, ArrayWrite[2], 2); // 显示写入数据的第3个字节
    OLED_ShowHexNum(2, 13, ArrayWrite[3], 2); // 显示写入数据的第4个字节
    
    // 在OLED屏幕上显示读取的数据
    OLED_ShowHexNum(3, 4, ArrayRead[0], 2);  // 显示读取数据的第1个字节
    OLED_ShowHexNum(3, 7, ArrayRead[1], 2);  // 显示读取数据的第2个字节
    OLED_ShowHexNum(3, 10, ArrayRead[2], 2); // 显示读取数据的第3个字节
    OLED_ShowHexNum(3, 13, ArrayRead[3], 2); // 显示读取数据的第4个字节
    
    // 主循环:程序将在此循环中不断运行
    while (1)
    {
        // 主循环为空,程序在此运行时不会执行任何其他操作
    }
}

代码功能说明:

  1. 初始化模块

    • OLED_Init():初始化 OLED 显示模块,为显示做准备。
    • W25Q64_Init():初始化 W25Q64 闪存模块,设置 SPI 接口并准备与闪存进行通信。
  2. 显示MID和DID

    • 使用 W25Q64_ReadID(&MID, &DID) 从 W25Q64 获取制造商 ID(MID)和设备 ID(DID)。
    • 然后通过 OLED_ShowHexNum() 函数将这些 ID 显示在 OLED 屏幕上,显示格式为十六进制。
  3. 闪存操作

    • W25Q64_SetorErase(0x000000):擦除 W25Q64 闪存芯片的第一个 4KB 扇区,地址为 0x000000。
    • W25Q64_PageProgram(0x000000, ArrayWrite, 4):将 ArrayWrite 数组中的 4 个字节数据写入到闪存的地址 0x000000。
    • W25Q64_ReadData(0x000000, ArrayRead, 4):从闪存地址 0x000000 读取 4 个字节数据到 ArrayRead 数组。
  4. 显示读写的数据

    • 显示写入数据:通过 OLED_ShowHexNum() 显示 ArrayWrite 数组中写入的 4 个字节数据。
    • 显示读取数据:通过 OLED_ShowHexNum() 显示 ArrayRead 数组中读取的 4 个字节数据。
  5. 主循环

    • 在主循环中,程序保持空闲状态,不会执行其他操作。

总结:

这个程序通过 STM32F10x 微控制器与 W25Q64 闪存芯片进行交互,执行以下操作:

  1. 读取并显示闪存的制造商 ID(MID)和设备 ID(DID)。
  2. 执行擦除操作、写入操作和读取操作,将数据写入闪存并从中读取。
  3. 显示写入的数据和读取的数据到 OLED 显示屏上,便于用户观察。

使用场景:

此程序适用于嵌入式系统中需要使用闪存进行数据存储的应用,能够验证 W25Q64 闪存芯片的基本功能(读取 ID、写入和读取数据)。

总结:

复制代码
/**
* @brief W25Q64 Flash芯片初始化
* @note 初始化SPI接口,准备与Flash芯片通信
*/
void W25Q64_Init(void);

/**
* @brief 读取W25Q64 Flash芯片的制造商ID和设备ID
* @param MID 指向存储制造商ID的指针
* @param DID 指向存储设备ID的指针
* @note 通过JEDEC标准协议读取芯片唯一标识信息
* 
* 通信流程:
* 1. 开始SPI通信
* 2. 发送读取ID指令
* 3. 读取制造商ID和设备ID
* 4. 结束SPI通信
*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);

/**
* @brief 向W25Q64 Flash指定地址写入数据页
* @param Address 目标写入地址
* @param DataArray 待写入的数据数组
* @param Count 写入的数据长度(字节数)
* @note 
* 1. 写入前需要先发送写使能指令
* 2. 一次写入不能跨页
* 3. 每页最大256字节
* 
* 通信流程:
* 1. 发送写使能指令
* 2. 发送页编程指令和地址
* 3. 逐字节写入数据
* 4. 等待写入完成
*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);

/**
* @brief 擦除W25Q64 Flash指定扇区
* @param Address 要擦除的扇区地址
* @note 
* 1. 擦除前需要先发送写使能指令
* 2. 擦除最小单位为扇区(4KB)
* 3. 擦除将把扇区内所有数据置为0xFF
* 
* 通信流程:
* 1. 发送写使能指令
* 2. 发送扇区擦除指令和地址
* 3. 等待擦除完成
*/
void W25Q64_SectorErase(uint32_t Address);

/**
* @brief 从W25Q64 Flash读取数据
* @param Address 读取起始地址
* @param DataArray 存储读取数据的缓冲区
* @param Count 读取的数据长度(字节数)
* @note 
* 1. 支持任意长度的连续读取
* 2. 可以跨页、跨扇区读取
* 
* 通信流程:
* 1. 发送读取指令
* 2. 发送读取起始地址
* 3. 连续读取指定数量的数据
*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);

代码解释:

/**

* 函 数:W25Q64初始化

* 参 数:无

* 返 回 值:无

*/

void W25Q64_Init(void)
/**

* 函 数:MPU6050读取ID号

* 参 数:MID 工厂ID,使用输出参数的形式返回

* 参 数:DID 设备ID,使用输出参数的形式返回

* 返 回 值:无

*/

void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
/**

* 函 数:W25Q64写使能

* 参 数:无

* 返 回 值:无

*/

void W25Q64_WriteEnable(void)

/**

* 函 数:W25Q64等待忙

* 参 数:无

* 返 回 值:无

*/

void W25Q64_WaitBusy(void)

/**

* 函 数:W25Q64页编程

* 参 数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF

* 参 数:DataArray 用于写入数据的数组

* 参 数:Count 要写入数据的数量,范围:0~256

* 返 回 值:无

* 注意事项:写入的地址范围不能跨页

*/

void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
/**

* 函 数:W25Q64读取数据

* 参 数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF

* 参 数:DataArray 用于接收读取数据的数组,通过输出参数返回

* 参 数:Count 要读取数据的数量,范围:0~0x800000

* 返 回 值:无

*/

void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
软件SPI与W25Q64 Flash通信实验是非常经典的嵌入式系统通信实践。这个实验不仅能帮助你深入理解串行通信协议,还能掌握外部Flash芯片的基本操作。让我们来详细阐述这个实验的意义和关键步骤。

实验目的:

  1. 掌握软件模拟SPI通信的基本原理
  2. 学习W25Q64 Flash芯片的操作流程
  3. 理解数据读写和擦除的底层实现

关键技术点:

  • SPI通信协议
  • GPIO模拟时钟和数据线
  • 字节级数据交换
  • Flash芯片指令集

实验步骤:

  1. 硬件连接
    • MOSI:数据输出线
    • MISO:数据输入线
    • SCK:时钟线
    • CS:片选线
  2. 软件实现
    • MySPI_Init():初始化GPIO
    • MySPI_SwapByte():底层数据交换
    • W25Q64_ReadID():验证通信
    • W25Q64_ReadData():读取数据
    • W25Q64_PageProgram():写入数据
    • W25Q64_SectorErase():擦除扇区

推荐实验流程:

  1. 先实现SPI通信
  2. 读取芯片ID验证通信
  3. 测试数据读取
  4. 尝试数据写入和擦除

需要特别注意的是,软件SPI的时序控制至关重要,每个时钟周期和数据传输都需要精确控制。

工程源码:【免费】STM32与W25Q64闪存芯片的SPI通信资源-CSDN文库

下一节:我们将要进行硬件的SPI实验,本节的软件SPI的实现还是比较简单的,通过SPI作为通信双方的桥梁,连接STM32与W25Q64的交互,

相关推荐
郦7778 分钟前
价格性价比高系列的高性能单片机MS32C001-C
单片机·嵌入式硬件
iCxhust22 分钟前
汇编字符串比较函数
c语言·开发语言·汇编·单片机·嵌入式硬件
小智学长 | 嵌入式42 分钟前
Arduino入门教程:1-1、先跑起来(点亮LED&打印Helloworld)
单片机·嵌入式硬件
码小文1 小时前
MCU、MPU、GPU、Soc、DSP、FPGA、CPLD……它们到底是什么?
笔记·单片机·嵌入式硬件·学习·ic常识
我命由我123453 小时前
STM32 开发 - 中断案例(中断概述、STM32 的中断、NVIC 嵌套向量中断控制器、外部中断配置寄存器组、EXTI 外部中断控制器、实例实操)
c语言·开发语言·c++·stm32·单片机·嵌入式硬件·嵌入式
宋一平工作室3 小时前
单片机队列功能模块的实战和应用
c语言·开发语言·stm32·单片机·嵌入式硬件
SY师弟3 小时前
台湾TEMI协会竞赛——2、足球机器人组装教学
c语言·单片机·嵌入式硬件·机器人·嵌入式·台湾temi协会
挨踢玩家4 小时前
stm32---dma串口发送+fifo队列框架
stm32·单片机·嵌入式硬件
youcans_4 小时前
【EdgeAI实战】(3)边缘AI开发套件 STM32N6570X0 用户手册
stm32·单片机·嵌入式硬件·边缘计算·边缘ai
JXNL@8 小时前
STM32外设学习之串口
stm32·单片机·学习