目录
[3.HAL库实现SPI读写W25Q32 Flash存储器](#3.HAL库实现SPI读写W25Q32 Flash存储器)
[3.5.1 W25Q32.h](#3.5.1 W25Q32.h)
[3.5.2 W25Q32.c](#3.5.2 W25Q32.c)
[3.5.3 main.c](#3.5.3 main.c)
1.SPI通信
1.1SPI总线概括
SPI是全双工三线/四线同步串行外围接口,采用主从模式。
时钟由Master控制,在时钟移位脉冲下,数据按位传输,高位在前,低位在后;SPI接口有2根单向数据线,为全双工通信。
SPI总线被广泛地使用在FLASH、ADC、LCD等设备与MCU间,要求通讯速率较高的场合。

所有的输出口在输出的时候要配置成推挽输出,这样在没有外部上拉电阻的情况的也可以有较强的输出高低电平的能力。但需要考虑一点:当主机跟从机3进行通信的时候,从机2和从机1的MISO线如果是推挽输出(推挽必输出高电平或低电平的一种)那肯定会影响主机跟从机三的正常通信,所以为了消去这一影响,让从机SS片选线为高电平,即非选中状态时对应从机输出口置为高阻态,这样既不是高电平也不是低电平不会影响主机跟所选中从机的正常通信。
1.2字节交换原理
字节交换是基于位交换的
移出:就是把当前移位寄存器的最高位写到信号线上,然后整体往左移动一位
移入:读取信号线的状态,然后把读到的状态放到寄存器的最低位
移入和移出都是主机和从机同时进行的
移入和移出的时机是由时钟线控制约定的,约定好第几个边沿一起移出,第几个边沿一起移入。
1.3时序单元
起始条件:SS从高电平切换到低电平
终止条件:SS从低电平切换到高电平
CPOL:时钟极性
CPHA:时钟相位
1.4SPI模式
由CPOL及CPHA的不同状态,SPI分成了四种模式,主机与从机需要工作在相同的模式下才可以正常通讯,因此通常主机要按照从机支持的模式去设置.
模式0:CPOL:0 CPHA:0 空闲时钟为低电平,奇数边沿采样
模式1:CPOL:0 CPHA:1 空闲时钟为低电平,偶数边沿采样
模式2:CPOL:1 CPHA:0 空闲时钟为高电平,基数边沿采样
模式3:CPOL:1 CPHA:1 空闲时钟为高电平,偶数边沿采样
2.Flash(W25Q32)
2.1基本概括
这里板载Flash是W25Q32型号,32代表的是32M比特的容量,相当于4MB的存储空间。
FLASH将4MB的存储空间划分为64个块,每个块的大小为64KB。每个块进一步细分为16个扇区(Sector),每个扇区的大小为4KB。
具体的大家可以查看下面的链接,详细查看W25Q32的介绍
4M Flash W25Q32的详细介绍-电子发烧友网本文以常见的4M Flash W25Q32为例。https://www.elecfans.com/d/2021087.html
2.2硬件电路及与SPI对应连接引脚

2.3使用注意事项
写操作时:
写数据之前,必须先进行擦除操作,擦除后,所有数据位将变为1。擦除操作必须按照最小擦除单元来进行。
在写入数据之前,要先进行写使能操作。
每个数据位只能从1改写为0,而不能从0改写为1。
在连续写入多字节数据时,最多只能写入一页的数据。
写操作完成后,芯片会进入忙状态。
读操作时:
读操作时,可以直接调用读取时序,无需进行使能,读操作完成后,芯片不会进入忙状态。芯片处于忙状态时,不能进行读操作。
2.4常用指令代码和功能描述

3.HAL库实现SPI读写W25Q32 Flash存储器
3.1初始化
使用CubeMX分别配置SPI模式,时钟速率和SPI引脚选择
初始化GPIO引脚
配置CS引脚为输出,初始状态为高电平。
配置SCK、MOSI为输出,MISO为输入。
3.2擦除
发送写使能:0x06。
发送擦除命令:0x20 + 24位地址(3字节,高位在前)。
等待擦除完成:轮询状态寄存器直至BUSY位为0。
3.3写入(页编程)
页大小:256字节
写使能:发送0x06。
发送页编程命令:0x02 + 24位地址 + 数据。
等待写入完成:轮询状态寄存器。
3.4读取
发送读命令:0x03 + 24位地址。
连续读取数据:地址自动递增,可跨页连续读。
3.5部分代码实现
3.5.1 W25Q32.h
cs
#ifndef __W25Q32__H__
#define __W25Q32__H__
#include "main.h"
#define READ_REGISTER_1 0x05 //读取状态寄存器1
#define READ_REGISTER_2 0x35 //读取状态寄存器2
#define READ_DATA_CMD 0x03 //读取数据命令
#define WRITE_ENABLE_CMD 0x06 //写使能命令
#define SECTOR_ERASE_CMD 0x20 //扇区擦除命令
#define PAGE_PROGRAM_CMD 0x02 //页编程命令
static uint8_t W25Q32_ReadSR(uint8_t reg); //读取W25Q32状态寄存器
int W25Q32_Read(uint8_t* buffer, uint32_t start_addr, uint16_t nbytes); //从W25Q32读取数据
void W25Q32_EraseSector(uint32_t sector_addr); //擦除W25Q32一个扇区
void W25Q32_WriteEnable(void); //发送写使能命令
void W25Q32_PageProgram(uint8_t* dat, uint32_t WriteAddr, uint16_t nbytes); //向W25Q32写入数据
#endif
3.5.2 W25Q32.c
cs
#include "main.h"
#include "W25Q32.h"
extern SPI_HandleTypeDef hspi1;
/**
* @brief 读取W25Q32状态寄存器
* @param reg 状态寄存器编号 (1 或 2)
* @return 状态寄存器的值
*/
uint8_t W25Q32_ReadSR(uint8_t reg)
{
uint8_t cmd = (reg == 2) ? READ_REGISTER_2 : READ_REGISTER_1; //根据reg选择读取状态寄存器1或2
uint8_t sr = 0; //存储读取到的状态寄存器值
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); //拉低片选 (CS)
HAL_SPI_Transmit(&hspi1, &cmd, 1, 1000); //发送读取状态寄存器命令
HAL_SPI_Receive(&hspi1, &sr, 1, 1000); //接收状态寄存器值
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); //拉高片选 (CS)
return sr;
}
/**
* @brief 等待W25Q32的BUSY位清零
*/
void W25Q32_WaitBusy(void)
{
while (W25Q32_ReadSR(1) & 0x01); // 循环读取状态寄存器1, 直到BUSY位为0
}
/**
* @brief 从W25Q32读取数据
* @param buffer 数据接收缓冲区
* @param addr 读取起始地址
* @param len 要读取的字节数
* @return 0:成功,其他值:失败
*/
int W25Q32_Read(uint8_t* buffer, uint32_t addr, uint16_t len)
{
uint8_t cmd[4] = {
READ_DATA_CMD, //读取数据命令
(uint8_t)(addr >> 16), //地址高8位
(uint8_t)(addr >> 8), //地址中8位
(uint8_t)addr //地址低8位
};
W25Q32_WaitBusy(); // 等待Flash就绪
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); //拉低片选 (CS)
HAL_SPI_Transmit(&hspi1, cmd, 4, 1000); //发送读取命令和地址
HAL_SPI_Receive(&hspi1, buffer, len, 1000); //接收数据
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); //拉高片选 (CS)
return 0;
}
/**
* @brief 擦除W25Q32的一个扇区
* @param sector 要擦除的扇区地址
*/
void W25Q32_EraseSector(uint32_t sector)
{
uint8_t cmd[4] = {
SECTOR_ERASE_CMD, //扇区擦除命令
(uint8_t)(sector >> 16), //地址高8位
(uint8_t)(sector >> 8), //地址中8位
(uint8_t)sector //地址低8位
};
W25Q32_WriteEnable(); //发送写使能命令
W25Q32_WaitBusy(); //等待Flash就绪
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); //拉低片选 (CS)
HAL_SPI_Transmit(&hspi1, cmd, 4, 1000); //发送擦除命令和地址
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); //拉高片选 (CS)
W25Q32_WaitBusy(); //等待擦除完成
}
/**
* @brief 发送写使能命令
* @note 必须在每次写入或擦除操作之前调用
*/
void W25Q32_WriteEnable(void)
{
uint8_t cmd = WRITE_ENABLE_CMD; //写使能命令
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); //拉低片选 (CS)
HAL_SPI_Transmit(&hspi1, &cmd, 1, 1000); //发送写使能命令
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); //拉高片选 (CS)
}
/**
* @brief 向W25Q32写入数据
* @param data 要写入的数据
* @param addr 写入起始地址
* @param len 要写入的字节数
*/
void W25Q32_PageProgram(uint8_t* data, uint32_t addr, uint16_t len)
{
uint8_t cmd[4] = {
PAGE_PROGRAM_CMD, //页编程命令
(uint8_t)(addr >> 16), //地址高8位
(uint8_t)(addr >> 8), //地址中8位
(uint8_t)addr //地址低8位
};
W25Q32_WriteEnable(); //发送写使能命令
W25Q32_WaitBusy(); //等待Flash就绪
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET); //拉低片选 (CS)
HAL_SPI_Transmit(&hspi1, cmd, 4, 1000); //发送页编程命令和地址
HAL_SPI_Transmit(&hspi1, data, len, 1000); //发送数据
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET); //拉高片选 (CS)
W25Q32_WaitBusy(); // 等待写入完成
}
3.5.3 main.c
cpp
/**
* @brief 外部中断回调函数,处理GPIO_PIN_0和GPIO_PIN_15的中断事件
* @param GPIO_Pin 触发中断的GPIO引脚号
* @note 当GPIO_PIN_0中断触发时,擦除指定扇区并写入数据;
* 当GPIO_PIN_15中断触发时,读取Flash数据并输出
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if (GPIO_Pin == GPIO_PIN_0) //GPIO_PIN_0 中断处理,写入Flash
{
W25Q32_EraseSector(FLASH_SIZE - 100); //擦除目标扇区
W25Q32_PageProgram((uint8_t *)TEXT_Buffer, FLASH_SIZE - 100, SIZE); //将数据写入Flash
printf("写入FLASH成功\r\n");
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_8);
}
if (GPIO_Pin == GPIO_PIN_15) //GPIO_PIN_15 中断处理,读取Flash
{
W25Q32_Read((uint8_t *)datatemp, FLASH_SIZE - 100, SIZE); //从Flash读取数据
printf("获取的FLASH里的消息为:%s\r\n", datatemp);
HAL_GPIO_TogglePin(GPIOD, GPIO_PIN_2);
}
}