历史上的今天
今天是:2025年07月26日(星期六)
2021年07月26日 | STM32CubeMX | 30-使用硬件SPI读写FLASH(W25Q64)
2021-07-26 来源:eefocus
本篇详细的记录了如何使用STM32CubeMX配置 STM32G070RBT6 的硬件SPI外设与 SPI Flash 通信(W25Q64)。
【STM32Cube_09】重定向printf函数到串口输出的多种方法。
4. 封装 SPI Flash(W25Q64)的命令和底层函数
MCU 通过向 SPI Flash 发送各种命令 来读写 SPI Flash内部的寄存器,所以这种裸机驱动,首先要先宏定义出需要使用的命令,然后利用 HAL 库提供的库函数,封装出三个底层函数,便于移植:
向 SPI Flash 发送数据的函数
从 SPI Flash 接收数据的函数
发送数据的同时读取数据的函数
接下来开始编写代码~
宏定义操作命令
#define ManufactDeviceID_CMD 0x90
#define READ_STATU_REGISTER_1 0x05
#define READ_STATU_REGISTER_2 0x35
#define READ_DATA_CMD 0x03
#define WRITE_ENABLE_CMD 0x06
#define WRITE_DISABLE_CMD 0x04
#define SECTOR_ERASE_CMD 0x20
#define CHIP_ERASE_CMD 0xc7
#define PAGE_PROGRAM_CMD 0x02
封装发送数据的函数
/**
* @brief SPI发送指定长度的数据
* @param buf —— 发送数据缓冲区首地址
* @param size —— 要发送数据的字节数
* @retval 成功返回HAL_OK
*/
static HAL_StatusTypeDef SPI_Transmit(uint8_t* send_buf, uint16_t size)
{
return HAL_SPI_Transmit(&hspi1, send_buf, size, 100);
}
封装接收数据的函数
/**
* @brief SPI接收指定长度的数据
* @param buf —— 接收数据缓冲区首地址
* @param size —— 要接收数据的字节数
* @retval 成功返回HAL_OK
*/
static HAL_StatusTypeDef SPI_Receive(uint8_t* recv_buf, uint16_t size)
{
return HAL_SPI_Receive(&hspi1, recv_buf, size, 100);
}
封装发送数据同时读取数据的函数
/**
* @brief SPI在发送数据的同时接收指定长度的数据
* @param send_buf —— 接收数据缓冲区首地址
* @param recv_buf —— 接收数据缓冲区首地址
* @param size —— 要发送/接收数据的字节数
* @retval 成功返回HAL_OK
*/
static HAL_StatusTypeDef SPI_TransmitReceive(uint8_t* send_buf, uint8_t* recv_buf, uint16_t size)
{
return HAL_SPI_TransmitReceive(&hspi1, send_buf, recv_buf, size, 100);
}
5. 编写W25Q64的驱动程序
接下来开始利用上一节封装的宏定义和底层函数,编写W25Q64的驱动程序:
读取Manufacture ID和Device ID
读取 Flash 内部这两个ID有两个作用:
检测SPI Flash是否存在
可以根据ID判断Flash具体型号
数据手册上给出的操作时序如图:

根据该时序,编写代码如下:
/**
* @brief 读取Flash内部的ID
* @param none
* @retval 成功返回device_id
*/
uint16_t W25QXX_ReadID(void)
{
uint8_t recv_buf[2] = {0}; //recv_buf[0]存放Manufacture ID, recv_buf[1]存放Device ID
uint16_t device_id = 0;
uint8_t send_data[4] = {ManufactDeviceID_CMD,0x00,0x00,0x00}; //待发送数据,命令+地址
/* 使能片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);
/* 发送并读取数据 */
if (HAL_OK == SPI_Transmit(send_data, 4)) {
if (HAL_OK == SPI_Receive(recv_buf, 2)) {
device_id = (recv_buf[0] << 8) | recv_buf[1];
}
}
/* 取消片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
return device_id;
}
读取状态寄存器数据并判断Flash是否忙碌
上文中提到,SPI Flash的所有操作都是靠发送命令完成的,但是 Flash 接收到命令后,需要一段时间去执行该操作,这段时间内 Flash 处于“忙”状态,MCU 发送的命令无效,不能执行,在 Flash 内部有2-3个状态寄存器,指示出 Flash 当前的状态,有趣的一点是:
当 Flash 内部在执行命令时,不能再执行 MCU 发来的命令,但是 MCU 可以一直读取状态寄存器,这下就很好办了,MCU可以一直读取,然后判断Flash是否忙完:

读取协议如下:
根据此协议实现的读取状态寄存器的代码如下:
/**
* @brief 读取W25QXX的状态寄存器,W25Q64一共有2个状态寄存器
* @param reg —— 状态寄存器编号(1~2)
* @retval 状态寄存器的值
*/
static uint8_t W25QXX_ReadSR(uint8_t reg)
{
uint8_t result = 0;
uint8_t send_buf[4] = {0x00,0x00,0x00,0x00};
switch(reg)
{
case 1:
send_buf[0] = READ_STATU_REGISTER_1;
case 2:
send_buf[0] = READ_STATU_REGISTER_2;
case 0:
default:
send_buf[0] = READ_STATU_REGISTER_1;
}
/* 使能片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);
if (HAL_OK == SPI_Transmit(send_buf, 4)) {
if (HAL_OK == SPI_Receive(&result, 1)) {
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
return result;
}
}
/* 取消片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
return 0;
}
然后编写阻塞判断Flash是否忙碌的函数:
/**
* @brief 阻塞等待Flash处于空闲状态
* @param none
* @retval none
*/
static void W25QXX_Wait_Busy(void)
{
while((W25QXX_ReadSR(1) & 0x01) == 0x01); // 等待BUSY位清空
}
读取数据
SPI Flash读取数据可以任意地址(地址长度32bit)读任意长度数据(最大 65535 Byte),没有任何限制,数据手册给出的时序如下:

根据该时序图编写代码如下:
/**
* @brief 读取SPI FLASH数据
* @param buffer —— 数据存储区
* @param start_addr —— 开始读取的地址(最大32bit)
* @param nbytes —— 要读取的字节数(最大65535)
* @retval 成功返回0,失败返回-1
*/
int W25QXX_Read(uint8_t* buffer, uint32_t start_addr, uint16_t nbytes)
{
uint8_t cmd = READ_DATA_CMD;
start_addr = start_addr << 8;
W25QXX_Wait_Busy();
/* 使能片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
if (HAL_OK == SPI_Transmit((uint8_t*)&start_addr, 3)) {
if (HAL_OK == SPI_Receive(buffer, nbytes)) {
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
return 0;
}
}
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
return -1;
}
写使能/禁止
Flash 芯片默认禁止写数据,所以在向 Flash 写数据之前,必须发送命令开启写使能,数据手册中给出的时序如下:


编写函数如下:
/**
* @brief W25QXX写使能,将S1寄存器的WEL置位
* @param none
* @retval
*/
void W25QXX_Write_Enable(void)
{
uint8_t cmd= WRITE_ENABLE_CMD;
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
W25QXX_Wait_Busy();
}
/**
* @brief W25QXX写禁止,将WEL清零
* @param none
* @retval none
*/
void W25QXX_Write_Disable(void)
{
uint8_t cmd = WRITE_DISABLE_CMD;
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
W25QXX_Wait_Busy();
}
擦除扇区
SPI Flash有个特性:
数据位可以由1变为0,但是不能由0变为1。
所以在向 Flash 写数据之前,必须要先进行擦除操作,并且 Flash 最小只能擦除一个扇区,擦除之后该扇区所有的数据变为 0xFF(即全为1),数据手册中给出的时序如下:

根据此时序编写函数如下:
/**
* @brief W25QXX擦除一个扇区
* @param sector_addr —— 扇区地址 根据实际容量设置
* @retval none
* @note 阻塞操作
*/
void W25QXX_Erase_Sector(uint32_t sector_addr)
{
uint8_t cmd = SECTOR_ERASE_CMD;
sector_addr *= 4096; //每个块有16个扇区,每个扇区的大小是4KB,需要换算为实际地址
sector_addr <<= 8;
W25QXX_Write_Enable(); //擦除操作即写入0xFF,需要开启写使能
W25QXX_Wait_Busy(); //等待写使能完成
/* 使能片选 */
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_RESET);
SPI_Transmit(&cmd, 1);
SPI_Transmit((uint8_t*)§or_addr, 3);
HAL_GPIO_WritePin(W25Q64_CHIP_SELECT_PORT, W25Q64_CHIP_SELECT_PIN, GPIO_PIN_SET);
W25QXX_Wait_Busy(); //等待扇区擦除完成
}
页写入操作
向 Flash 芯片写数据的时候,因为 Flash 内部的构造,可以按页写入:

页写入的时序如图:

编写代码如下:
/**
* @brief 页写入操作
* @param dat —— 要写入的数据缓冲区首地址
* @param WriteAddr —— 要写入的地址
* @param byte_to_write —— 要写入的字节数(0-256)
* @retval none
*/
void W25QXX_Page_Program(uint8_t* dat, uint32_t WriteAddr, uint16_t nbytes)
{
uint8_t cmd = PAGE_PROGRAM_CMD;
WriteAddr <<= 8;
W25QXX_Write_Enable();
史海拾趣
|
自从美国Intel公司1971年设计制造出4位微处a理器芯片以来,在20多年时间内,CPU从Intel4004、80286、80386、80486发展到Pentium和PentiumⅡ,数位从4位、8位、16位、32位发展到64位;主频从几兆到今天的400MHz以上,接近GHz;CPU芯片里集成的晶体管 ...… 查看全部问答> |
|
许多过程人员都听到过“无线仪表”这个术语,并且认为这非常了不起,不需要导线,但是它的可靠性怎样呢?怎样才能知道你是否在准时接收正确的数据呢?是否每次都能够做到准时接收正确的数据呢?让我们更加深入的研究这个问题,自组织无线网络的可靠 ...… 查看全部问答> |
|
职位名称:DSP信号处理工程师职位要求:1、计算机、通信类相关专业,本科及以上学历,本科需从事DSP软件设计三年以上经验;2、掌握数字信号处理相关技术、数字逻辑电路设计;3、熟练掌握MATLAB等仿真工具;4、掌握通用的定点和浮点DSP的应用和算法 ...… 查看全部问答> |
|
目录: 详细信息: 书名:非线性半导体电阻及其应用 作者:В.В. 帕塞科夫, Г.А. 萨维利也夫, Л.К. 契尔金著 出版社:国防工业出版社 出版时间:1964 页数:217页… 查看全部问答> |
|
有没有这个一个API,通过给logfont的lpcharset设置成CHINESEBIG5_CHARSET,直接获取系统当前使用的字体的FACENAME? ( 不用枚举, 我查找半天了MSDN,没找到,所以上来问问,) SystemParametersInfo可以用SPI_GETNONCLIENTMETRICS这个参数直接获取系统当 ...… 查看全部问答> |
|
ddr2 128M/32BIT 运行ce6正常,可另一个板子是DDR2 64M/16bit ,ce6跑不起来 ce6的硬件最低配置是多少?… 查看全部问答> |
|
三月份是三网融合和3D高清智能电视热潮涌动的一个月.首先声明一下,阿牛哥主业是推进广电大屏屏接矩阵方案 ,对三网融合,机顶盒,3D高清智能电视等话题只是个人观点. 3月22日下 ...… 查看全部问答> |




