经过研究SPI协议和W25Q64,逐步了解了SPI的通信过程,接下来,就要进行战场实战了!跟进Whappy步伐!
目标:主要实现基于软件的SPI的STM32对W25Q64存储写入和读取操作!
开胃介绍(代码基本实现过程)
- 初始化 SPI 接口:
- 配置微控制器上必要的 GPIO 引脚,将 MOSI、SCK 和 CS 设置为输出模式,MISO 设置为输入模式。
- 根据 SPI 时钟极性(CPOL)的配置,设置 SCK 引脚的初始状态。
- 将 CS 引脚设置为高电平,取消对 W25Q64 芯片的选择。
- 选择 W25Q64 芯片:
- 将 CS 引脚拉低,选择 W25Q64 芯片,开始通信。
- 发送命令:
- 在 MOSI 线上移位发送命令字节,同时切换 SCK 线。
- 命令指示要执行的操作类型,如读、写或擦除。
- 在命令阶段,W25Q64 可能也会在 MISO 线上移出状态信息,微控制器需要读取。
- 发送地址(如果需要):
- 对于需要指定内存地址的操作,在 MOSI 线上移位发送 24 位地址。
- W25Q64 会接受地址,并为后续的数据传输做准备。
- 执行数据传输:
- 根据操作类型,微控制器会在 MOSI 线上移位发送数据(写操作),或从 MISO 线上移位读取数据(读操作)。
- 切换 SCK 线以同步数据传输。
- 对于读操作,W25Q64 会在 MISO 线上移出请求的数据。
- 对于写操作,W25Q64 会接受 MOSI 线上的数据,并将其存储到相应的内存位置。
- 取消选择 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,则在接收数据最低位置1if(MySPI_R_MISO() == 1){ReceiveData |= 0x01;}// 拉低时钟线MySPI_W_SCLK(0);}return ReceiveData; }
这个版本:
- 完整发送8个bit
- 完整接收8个bit
- 返回接收到的完整字节
- 保留了原始的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还是1MySPI_W_MOSI(Byte & (0x80>>i));// 拉高时钟线,从设备在上升沿采样数据MySPI_W_SCLK(1);// 读取MISO上的数据if(MySPI_R_MISO()==1){// 如果接收到1,则在对应位置置1ReceiveData |= (0x80>>i);}// 拉低时钟线,准备下一个bitMySPI_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还是1MySPI_W_MOSI(Byte & (0x80>>i));// 拉高时钟线,从设备在上升沿采样数据MySPI_W_SCLK(1);// 读取MISO上的数据if(MySPI_R_MISO()==1){// 如果接收到1,则在对应位置置1ReceiveData |= (0x80>>i);}// 拉低时钟线,准备下一个bitMySPI_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还是1MySPI_W_MOSI(Byte & (0x80>>i));// 拉高时钟线,从设备在上升沿采样数据MySPI_W_SCLK(0);// 读取MISO上的数据if(MySPI_R_MISO()==1){// 如果接收到1,则在对应位置置1ReceiveData |= (0x80>>i);}// 拉低时钟线,准备下一个bitMySPI_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还是1MySPI_W_MOSI(Byte & (0x80>>i));// 拉高时钟线,从设备在上升沿采样数据MySPI_W_SCLK(0);// 读取MISO上的数据if(MySPI_R_MISO()==1){// 如果接收到1,则在对应位置置1ReceiveData |= (0x80>>i);}// 拉低时钟线,准备下一个bitMySPI_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、内存类型和容量。
解释:
For compatibility reasons, the W25Q80/16/32 provides several instructions to electronically determine the identity of the device:
为了兼容性,W25Q80/16/32 提供了几种指令,允许电子设备读取存储器芯片的身份信息。这有助于识别设备及其相关参数。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 兼容的串行存储器。The instruction is initiated by driving the /CS pin low and shifting the instruction code "9Fh":
该指令通过将芯片选择信号(/CS)拉低来启动,并且发送指令代码9Fh
。9Fh
是 JEDEC ID 读取指令的指令代码。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(最重要位)优先的顺序发送的。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 命令以读取闪存芯片的 IDMySPI_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(); }
代码详解
启动 SPI 通信:
c
复制代码
MySPI_Start();
- 调用
MySPI_Start
函数拉低CS
(片选)引脚,启动与 W25Q64 的 SPI 通信。发送 JEDEC ID 命令:
c
复制代码
MySPI_SwapByte(W25Q64_JEDEC_ID);
W25Q64_JEDEC_ID
是一个常量,它代表 JEDEC ID 命令。调用MySPI_SwapByte
发送该命令,通知 W25Q64 闪存芯片准备返回其 ID。读取 MID(制造商 ID):
c
复制代码
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
- 发送一个占位符字节(通常是 0x00),然后通过 SPI 读取 MID(制造商 ID),将读取的值存入传入的指针
MID
指向的变量中。读取 DID(设备 ID):
c
复制代码
*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);
- 再次发送占位符字节,通过 SPI 读取 DID(设备 ID)的低字节,并将其存入
DID
的低字节部分。处理 DID 的高字节:
c
复制代码
*DID <<= 8;
- 将
DID
变量左移 8 位,将低字节腾出位置,为接下来的高字节准备空间。读取 DID 的高字节并合并:
c
复制代码
*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);
- 读取 DID 的高字节,并将其合并到
DID
变量中。|=
操作符将高字节与之前的低字节组合成完整的 16 位 DID。停止 SPI 通信:
c
复制代码
MySPI_Stop();
- 调用
MySPI_Stop
函数拉高CS
(片选)引脚,结束与 W25Q64 闪存的 SPI 通信。注意细节:
占位符字节:
- 在读取 MID 和 DID 时,必须发送占位符字节(通常是
W25Q64_DUMMY_BYTE
,如 0x00)。这些字节用于占据时钟周期,等待闪存芯片返回数据。W25Q64_DUMMY_BYTE
是定义为常数的值,表示在不进行数据传输时发送的空字节,确保 SPI 时序正确。字节顺序:
- 由于 MID 是 8 位,而 DID 是 16 位,因此在读取 DID 时需要先读取低字节,再读取高字节。高字节必须左移 8 位,低字节通过位或操作合并。
- DID 高字节在低字节之后读取,并且需要将低字节的值“腾空”,这通常是通过左移操作(
<<= 8
)来实现。SPI 通信时序:
MySPI_SwapByte()
函数用于执行 SPI 发送和接收操作。每次调用该函数都发送一个字节并接收一个字节,因此它能同时实现发送命令和接收数据。- 注意:
MySPI_SwapByte()
不仅发送数据,还会返回接收到的字节数据。传入指针:
- 函数通过传入指针
MID
和DID
传递读取到的 MID 和 DID。这意味着在函数外部可以直接访问这两个 ID。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(); }
代码详解
启动 SPI 通信:
c
复制代码
MySPI_Start();
- 调用
MySPI_Start
函数,通过拉低 SPI 的CS
(片选)引脚来开始与 W25Q64 闪存的通信。此时芯片被选中,开始接收命令。发送写使能命令:
c
复制代码
MySPI_SwapByte(W25Q64_WRITE_ENABLE);
W25Q64_WRITE_ENABLE
是定义的常量,其值为0x06
,这是 W25Q64 闪存的写使能命令。- 通过调用
MySPI_SwapByte()
,向闪存发送该命令。在 SPI 总线上,MySPI_SwapByte
不仅发送数据,还接收从闪存芯片返回的数据。- 这里的
MySPI_SwapByte()
会发送一个字节0x06
(写使能命令)到 W25Q64,并确保 SPI 时序正确。停止 SPI 通信:
c
复制代码
MySPI_Stop();
- 调用
MySPI_Stop
函数,将CS
(片选)引脚拉高,停止与 W25Q64 闪存的 SPI 通信。此时,芯片被取消选中,通信结束。注意细节
写使能命令的作用:
- 写使能命令
0x06
是 W25Q64 闪存芯片的一个基本命令。它使能闪存的写操作。如果不发送写使能命令,后续的写操作将被闪存芯片忽略,无法进行。- 在实际的闪存操作中(如擦除、写入数据等),每次都需要先发送写使能命令以允许写操作。
命令的发送顺序和时序:
- 在 SPI 通信中,必须遵循正确的时序和命令顺序。
MySPI_SwapByte
函数确保发送的命令按照 SPI 协议正确发送,同时接收返回的数据。- 即使写使能命令没有返回数据,
MySPI_SwapByte
也会等待 SPI 时钟周期,确保数据传输完成。SPI 总线上的 CS 管脚:
- 在该函数中,
MySPI_Start
和MySPI_Stop
控制CS
(片选)引脚的状态。CS
必须在发送命令之前拉低,命令发送完成后再拉高,确保闪存芯片接收到整个命令。CS
管脚的控制确保只有一个设备在某个时刻与 MCU 通信。这个操作必须非常精确,否则可能导致与其他 SPI 设备发生冲突。写使能命令后的操作:
- 写使能命令发送成功后,W25Q64 闪存芯片就会允许进行后续的写入操作(如页面编程、数据写入等)。因此,执行此命令后可以安全地进行其他写入操作,如写页、擦除扇区等。
性能优化:
W25Q64_WriteEnable
是一个非常常见的操作,每次写操作前都需要发送该命令。如果在实际应用中频繁调用该函数,可能会增加通信的延迟。如果希望优化性能,可以在写操作时减少多次调用WriteEnable
。总结
W25Q64_WriteEnable
函数通过 SPI 协议向 W25Q64 闪存芯片发送0x06
的写使能命令,确保后续的写操作可以成功执行。- 通过
MySPI_Start
和MySPI_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 字节。该函数首先发送写使能命令,然后发送页面编程命令,最后传输数据。代码详解
写使能命令:
c
复制代码
W25Q64_WriteEnable();
- 通过调用
W25Q64_WriteEnable
向 W25Q64 闪存发送写使能命令0x06
,使闪存允许执行写操作。启动 SPI 通信:
c
复制代码
MySPI_Start();
- 通过
MySPI_Start
函数拉低CS
(片选)引脚,开始与 W25Q64 闪存的 SPI 通信。发送页面编程命令:
c
复制代码
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);
- 发送页面编程命令
0x02
(W25Q64_PAGE_PROGRAM
),该命令用于启动页面写入操作。发送地址:
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 个字节。
发送数据:
c
复制代码
for(i = 0; i < Lenght; i++) { MySPI_SwapByte(Array[i]); }
- 通过循环遍历
Array
中的数据,将每个字节依次发送到 W25Q64 闪存芯片。Lenght
参数表示要写入的字节数(最多 256 字节)。- 这里假设写入的数据不会超过闪存页面的大小,即最多 256 字节。如果需要写入更大的数据,必须分多次进行页面写入。
停止 SPI 通信:
c
复制代码
MySPI_Stop();
- 通过
MySPI_Stop
函数将CS
(片选)引脚拉高,结束与 W25Q64 闪存的 SPI 通信。等待闪存完成操作:
c
复制代码
W25Q64_WaitBusy();
- 调用
W25Q64_WaitBusy
函数,等待闪存完成当前的编程操作。该函数会检查闪存的忙碌状态,直到写入操作完成。注意细节
页面编程命令:
W25Q64
的页面编程命令0x02
是用于将数据写入指定地址的页面中。每个页面的大小通常为 256 字节,因此在调用该命令时,最大写入数据长度为 256 字节。- 在实际操作中,如果数据长度超过 256 字节,需要将数据分成多个页面进行写入。可以通过地址自增和多次调用此函数来实现。
地址范围:
- W25Q64 支持最大 24 位地址(3 字节地址),即最大地址为
0xFFFFFF
,范围为 0 到 16MB。因此,函数中的地址参数Address
应在 0 到 16MB 之间。数据长度:
Lenght
参数指定写入的数据字节数。由于每个页面的大小为 256 字节,所以该函数最多一次性支持写入 256 字节的数据。如果需要写入的数据超过 256 字节,需要拆分为多个页面写入。闪存的写入周期:
- 每次页面编程后,W25Q64 闪存需要一段时间来完成写入操作。在此期间,芯片处于忙碌状态。
W25Q64_WaitBusy
函数确保在闪存完成写入操作后才会进行下一步操作。数据验证:
- 本函数没有实现数据验证。在实际应用中,可能需要在编程后通过读取该地址的数据并与原数据进行比较,来确保写入成功。
性能优化:
- 该函数每次写入最多 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 扇区。整个过程包括以下步骤:
启用写操作:在擦除操作之前,必须先启用写操作,这通常是为了防止误操作对闪存内容进行修改。
SPI 启动与数据传输:通过 SPI 协议与 W25Q64 芯片通信,发送擦除命令以及目标地址。擦除命令是 0x20,后面跟着的是 24 位地址数据。
等待操作完成:擦除操作是一个相对较长的过程,因此在擦除命令发出后,程序通过
W25Q64_WaitBusy
函数等待闪存芯片的忙碌状态变为未忙碌,确保擦除完成。关键点总结:
- W25Q64_WriteEnable:开启写操作权限,允许对 W25Q64 进行擦除操作。
- W25Q64_SECTOR_ERASE_4KB:执行 4KB 扇区擦除命令(0x20)。
- SPI 通信:通过
MySPI_Start
、MySPI_SwapByte
和MySPI_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
中。以下是具体步骤:
SPI 启动:通过
MySPI_Start
启动与 W25Q64 闪存的 SPI 通信。发送读取命令:通过 SPI 向闪存发送读取数据命令,通常为
0x03
,表示读取操作。发送地址:闪存地址是 24 位的,函数通过拆分地址的三个字节,并按大端格式(高字节先)发送。
读取数据:通过
MySPI_SwapByte
发送一个占位符字节(通常是0xFF
或其他不重要的值),同时接收从闪存返回的数据并存储到Array
数组中。每次读取一个字节,直到读取指定长度的所有数据。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和DIDOLED_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){// 主循环为空,程序在此运行时不会执行任何其他操作} }
代码功能说明:
初始化模块:
OLED_Init()
:初始化 OLED 显示模块,为显示做准备。W25Q64_Init()
:初始化 W25Q64 闪存模块,设置 SPI 接口并准备与闪存进行通信。显示MID和DID:
- 使用
W25Q64_ReadID(&MID, &DID)
从 W25Q64 获取制造商 ID(MID)和设备 ID(DID)。- 然后通过
OLED_ShowHexNum()
函数将这些 ID 显示在 OLED 屏幕上,显示格式为十六进制。闪存操作:
W25Q64_SetorErase(0x000000)
:擦除 W25Q64 闪存芯片的第一个 4KB 扇区,地址为 0x000000。W25Q64_PageProgram(0x000000, ArrayWrite, 4)
:将ArrayWrite
数组中的 4 个字节数据写入到闪存的地址 0x000000。W25Q64_ReadData(0x000000, ArrayRead, 4)
:从闪存地址 0x000000 读取 4 个字节数据到ArrayRead
数组。显示读写的数据:
- 显示写入数据:通过
OLED_ShowHexNum()
显示ArrayWrite
数组中写入的 4 个字节数据。- 显示读取数据:通过
OLED_ShowHexNum()
显示ArrayRead
数组中读取的 4 个字节数据。主循环:
- 在主循环中,程序保持空闲状态,不会执行其他操作。
总结:
这个程序通过 STM32F10x 微控制器与 W25Q64 闪存芯片进行交互,执行以下操作:
- 读取并显示闪存的制造商 ID(MID)和设备 ID(DID)。
- 执行擦除操作、写入操作和读取操作,将数据写入闪存并从中读取。
- 显示写入的数据和读取的数据到 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芯片的基本操作。让我们来详细阐述这个实验的意义和关键步骤。
实验目的:
- 掌握软件模拟SPI通信的基本原理
- 学习W25Q64 Flash芯片的操作流程
- 理解数据读写和擦除的底层实现
关键技术点:
- SPI通信协议
- GPIO模拟时钟和数据线
- 字节级数据交换
- Flash芯片指令集
实验步骤:
- 硬件连接
- MOSI:数据输出线
- MISO:数据输入线
- SCK:时钟线
- CS:片选线
- 软件实现
- MySPI_Init():初始化GPIO
- MySPI_SwapByte():底层数据交换
- W25Q64_ReadID():验证通信
- W25Q64_ReadData():读取数据
- W25Q64_PageProgram():写入数据
- W25Q64_SectorErase():擦除扇区
推荐实验流程:
- 先实现SPI通信
- 读取芯片ID验证通信
- 测试数据读取
- 尝试数据写入和擦除
需要特别注意的是,软件SPI的时序控制至关重要,每个时钟周期和数据传输都需要精确控制。
工程源码:【免费】STM32与W25Q64闪存芯片的SPI通信资源-CSDN文库
下一节:我们将要进行硬件的SPI实验,本节的软件SPI的实现还是比较简单的,通过SPI作为通信双方的桥梁,连接STM32与W25Q64的交互,