stm32之软件SPI读写W25Q64存储器应用案例

server/2024/10/18 14:22:58/

系列文章目录

1. stm32SPI通信协议


文章目录

  • 系列文章目录
  • 前言
  • 一、电路接线图
  • 二、应用案例代码
  • 三、应用案例分析


前言

提示:本文主要用作在学习江科大自化协STM32入门教程后做的归纳总结笔记,旨在学习记录,如有侵权请联系作者

本案例使用软件SPI通信的方式实现了STM32与W25Q64 Flash存储器的通信,完成了常见的Flash存储器操作如读ID、页写、扇区擦除、读取数据等。


一、电路接线图

下图所示为W25Q64模块硬件接线图,左边是W25Q64模块作为从机,右边是stm32作为主机。为了方便下一章节硬件SPI的接线,这里直接就选择了硬件SPI1外设的接线方式。其中PA4对应主机的从机选择线SPI1_NSS连接到从机的CS引脚,PA5对应主机的时钟同步线SPI1_SCK连接到从机的CLK引脚,PA6对应主机的主机输入从机输出线SPI1_MISO连接到从机的DO引脚,PA7对应主机的主机输出从机输入线SPI1_MOSI连接到从机的DI引脚。最后,W25Q64模块的VCC和GND分别接到stm32的电源正负极进行供电。

在这里插入图片描述

二、应用案例代码

MySPI.h:

#ifndef __MYSPI_H
#define __MYSPI_Hvoid MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);#endif

MySPI.c:

#include "stm32f10x.h"                  // Device headervoid MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}void MySPI_W_SCK(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}void MySPI_W_MOSI(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}uint8_t MySPI_R_MISO(void)
{return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}void MySPI_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);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);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_SS(1);MySPI_W_SCK(0);
}void MySPI_Start(void)
{MySPI_W_SS(0);
}void MySPI_Stop(void)
{MySPI_W_SS(1);
}uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i, ByteReceive = 0x00;for (i = 0; i < 8; i ++){MySPI_W_MOSI(ByteSend & (0x80 >> i));MySPI_W_SCK(1);if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}MySPI_W_SCK(0);}return ByteReceive;
}

W25Q64.h:

#ifndef __W25Q64_H
#define __W25Q64_Hvoid W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count);#endif

W25Q64_Ins.h:

#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H#define W25Q64_WRITE_ENABLE							0x06
#define W25Q64_WRITE_DISABLE						0x04
#define W25Q64_READ_STATUS_REGISTER_1				0x05
#define W25Q64_READ_STATUS_REGISTER_2				0x35
#define W25Q64_WRITE_STATUS_REGISTER				0x01
#define W25Q64_PAGE_PROGRAM							0x02
#define W25Q64_QUAD_PAGE_PROGRAM					0x32
#define W25Q64_BLOCK_ERASE_64KB						0xD8
#define W25Q64_BLOCK_ERASE_32KB						0x52
#define W25Q64_SECTOR_ERASE_4KB						0x20
#define W25Q64_CHIP_ERASE							0xC7
#define W25Q64_ERASE_SUSPEND						0x75
#define W25Q64_ERASE_RESUME							0x7A
#define W25Q64_POWER_DOWN							0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE				0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET			0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID		0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID				0x90
#define W25Q64_READ_UNIQUE_ID						0x4B
#define W25Q64_JEDEC_ID								0x9F
#define W25Q64_READ_DATA							0x03
#define W25Q64_FAST_READ							0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT				0x3B
#define W25Q64_FAST_READ_DUAL_IO					0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT				0x6B
#define W25Q64_FAST_READ_QUAD_IO					0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO				0xE3#define W25Q64_DUMMY_BYTE							0xFF#endif

W25Q64.c:

#include "stm32f10x.h"                  // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"void W25Q64_Init(void)
{MySPI_Init();
}void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start();MySPI_SwapByte(W25Q64_JEDEC_ID);*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);*DID <<= 8;*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);MySPI_Stop();
}void W25Q64_WriteEnable(void)
{MySPI_Start();MySPI_SwapByte(W25Q64_WRITE_ENABLE);MySPI_Stop();
}void W25Q64_WaitBusy(void)
{uint32_t Timeout;MySPI_Start();MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);Timeout = 100000;while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01){Timeout --;if (Timeout == 0){break;}}MySPI_Stop();
}void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable();MySPI_Start();MySPI_SwapByte(W25Q64_PAGE_PROGRAM);MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);for (i = 0; i < Count; i ++){MySPI_SwapByte(DataArray[i]);}MySPI_Stop();W25Q64_WaitBusy();
}void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable();MySPI_Start();MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);MySPI_Stop();W25Q64_WaitBusy();
}void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start();MySPI_SwapByte(W25Q64_READ_DATA);MySPI_SwapByte(Address >> 16);MySPI_SwapByte(Address >> 8);MySPI_SwapByte(Address);for (i = 0; i < Count; i ++){DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);}MySPI_Stop();
}

main.c:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"uint8_t MID;
uint16_t DID;uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};
uint8_t ArrayRead[4];int main(void)
{OLED_Init();W25Q64_Init();OLED_ShowString(1, 1, "MID:   DID:");OLED_ShowString(2, 1, "W:");OLED_ShowString(3, 1, "R:");W25Q64_ReadID(&MID, &DID);OLED_ShowHexNum(1, 5, MID, 2);OLED_ShowHexNum(1, 12, DID, 4);W25Q64_SectorErase(0x000000);W25Q64_PageProgram(0x000000, ArrayWrite, 4);W25Q64_ReadData(0x000000, ArrayRead, 4);OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);OLED_ShowHexNum(3, 3, ArrayRead[0], 2);OLED_ShowHexNum(3, 6, ArrayRead[1], 2);OLED_ShowHexNum(3, 9, ArrayRead[2], 2);OLED_ShowHexNum(3, 12, ArrayRead[3], 2);while (1){}
}

完整工程:stm32之软件SPI读写W25Q64存储器

三、应用案例分析

从整体架构来看主要分为SPI通信模块、W25Q64模块,接下来重点分析一下SPI通信模块以及W25Q64模块。

3.1 SPI通信模块

SPI通信模块主要封装了一个模块初始化函数和三个时序单元模块。

SPI模块初始化函数

void MySPI_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);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);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_SS(1);MySPI_W_SCK(0);
}

这个没什么好讲的了,都是一样的套路了。

这里要注意一下的就是PA4(SS)、PA5(SCK)、PA7(MOSI)引脚配置为推挽输出模式(GPIO_Mode_Out_PP),用于主设备主动输出信号。PA6(MISO)配置为上拉输入模式(GPIO_Mode_IPU),表示从设备的数据通过此引脚传输到主设备。

最后再设置一下总线的初始状态即可,设置 SS 引脚为高电平,即取消选择从设备,然后初始化时钟引脚为低电平。

三个时序单元模块:

三个时序单元分别是起始信号、终止信号以及交换一个字节。

起始信号、终止信号:

void MySPI_Start(void)
{MySPI_W_SS(0);
}void MySPI_Stop(void)
{MySPI_W_SS(1);
}

起始条件:SS从高电平切换到低电平
终止条件:SS从低电平切换到高电平

交换一个字节:

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i, ByteReceive = 0x00;for (i = 0; i < 8; i ++){MySPI_W_MOSI(ByteSend & (0x80 >> i));MySPI_W_SCK(1);if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);}MySPI_W_SCK(0);}return ByteReceive;
}

交换一个字节(模式0):
CPOL=0:空闲状态时,SCK为低电平
CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

交换一个字节的时序这里用的是模式0。起始信号之后先立刻将数据的最高位B7移出,然后在SCK的第一个上升沿后读入B7,然后在SCK的第一个下降沿移出次高位B6,然后在SCK的第二个上升沿读入B6,然后在SCK的第二个下降沿移出B5…如此循环8次,最终在SCK的第八个上升沿读入B0完成一个字节的交换。

至于关于代码部分的按位与(提取字节中的每一位)和按位或(设置字节中的某位)以及左移、右移操作的解析在我之前的一篇文章里已经详细地分析过了,如果有不懂的就翻回去看看就行了,这里就不再累述了。

文章传送门:计算机常见运算之左移操作、右移操作以及按位与、按位或

W25Q64_392">3.2 W25Q64模块

W25Q64模块主要由模块初始化函数、写使能、页编程(写入数据)、读取数据、忙等待、读取ID号以及扇区擦除等模块组成。

W25Q64模块初始化函数:

void W25Q64_Init(void)
{MySPI_Init();
}

调用MySPI_Init()函数,完成引脚配置和SPI的初始化。

写使能:

void W25Q64_WriteEnable(void)
{MySPI_Start();MySPI_SwapByte(W25Q64_WRITE_ENABLE);// 发送写使能命令MySPI_Stop();
}

发送写使能命令,以允许后续的写入或擦除操作。

页编程(写入数据):

void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) 
{uint16_t i;W25Q64_WriteEnable(); // 写使能MySPI_Start();MySPI_SwapByte(W25Q64_PAGE_PROGRAM); // 发送页编程命令MySPI_SwapByte(Address >> 16); // 发送地址高8位MySPI_SwapByte(Address >> 8);  // 发送地址中8位MySPI_SwapByte(Address);       // 发送地址低8位for (i = 0; i < Count; i ++) { // 发送要写入的数据MySPI_SwapByte(DataArray[i]);}MySPI_Stop();W25Q64_WaitBusy(); // 等待存储器空闲
}

页编程操作首先通过W25Q64_WriteEnable()函数启用写操作,然后发送页编程命令W25Q64_PAGE_PROGRAM,接着发送地址,并逐字节将数据写入存储器。

关于依次提取3字节24位中的每一个字节的问题有几点要注意一下,假设我传入的地址是Address = 0x123456,那么我们就需要依次发送0x12、0x34、0x56,那如何提取呢?

我们可以这样操作:

原始值Address:0x123456
0001 0010 0011 0100 0101 0110

右移十六位—> 0x12
0000 0000 0000 0000 0001 0010

右移八位—> 0x1234
0000 0000 0001 0010 0011 0100

可以看到如果是右移八位,那么得到的是0x1234,但是我们想要的是0x34才对啊?但其实代码里这样写也是没问题的,因为MySPI_SwapByte函数的形参是uint8_t,传入0x1234就只会接收低八位的0x34,但是这样的话理解起来就有点那啥了。

其实我们还可以这样做,直接右移以后再做个按位与操作就可以了,比如MySPI_SwapByte((Address >> 8) & 0xFF);这样就直接得到0x34了,当然第三位也需要执行按位与操作,MySPI_SwapByte(Address & 0xFF);

两种方法都可以,我这里只是提一下。

读数据:

void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) 
{uint32_t i;MySPI_Start();MySPI_SwapByte(W25Q64_READ_DATA); // 发送读取数据命令MySPI_SwapByte(Address >> 16); // 发送地址高8位MySPI_SwapByte(Address >> 8);  // 发送地址中8位MySPI_SwapByte(Address);       // 发送地址低8位for (i = 0; i < Count; i ++) { // 读取数据DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);}MySPI_Stop();
}

读取数据命令W25Q64_READ_DATA,发送要读取的地址,然后通过SPI逐字节读取指定数量的数据到DataArray中。

忙等待:

void W25Q64_WaitBusy(void)
{uint32_t Timeout;MySPI_Start();MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);// 读取状态寄存器1Timeout = 100000;while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01){// 检查忙标志位Timeout --;if (Timeout == 0){break;}}MySPI_Stop();
}

发送读取状态寄存器1命令,进入循环等待存储器空闲(即状态寄存器的最低位变为0),避免在存储器仍处于忙碌时进行新的操作。

读厂商、设备ID:

void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) 
{MySPI_Start(); // 片选低电平,开始通信MySPI_SwapByte(W25Q64_JEDEC_ID); // 发送读取ID命令*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 接收制造商ID*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 接收设备ID高8位*DID <<= 8;*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); // 接收设备ID低8位MySPI_Stop(); // 片选高电平,结束通信
}
  • 首先,调用MySPI_Start()开始SPI通信,然后发送读取设备ID命令W25Q64_JEDEC_ID。随后,通过MySPI_SwapByte函数接收制造商ID和设备ID。
  • 制造商ID(MID)为一个字节,设备ID(DID)为两个字节。

扇区擦除:

void W25Q64_SectorErase(uint32_t Address) 
{W25Q64_WriteEnable(); // 写使能MySPI_Start();MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); // 发送扇区擦除命令MySPI_SwapByte(Address >> 16); // 发送地址高8位MySPI_SwapByte(Address >> 8);  // 发送地址中8位MySPI_SwapByte(Address);       // 发送地址低8位MySPI_Stop();W25Q64_WaitBusy(); // 等待存储器空闲
}

扇区擦除命令W25Q64_SECTOR_ERASE_4KB用于擦除特定地址所在的扇区,地址通过三次发送高、中、低8位地址来指定。

3.3 主程序

主程序整体代码逻辑大概是,首先读取从机的ID信息并显示在OLED屏幕上,然后执行一个扇区擦除操作,接着在该扇区中写入4个字节的数据,最后将写入的数据读取出来,并在OLED上显示。

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"uint8_t MID;// 制造商ID (Manufacturer ID)
uint16_t DID;// 设备ID (Device ID)uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};
uint8_t ArrayRead[4];int main(void)
{// 硬件初始化OLED_Init();W25Q64_Init();OLED_ShowString(1, 1, "MID:   DID:");OLED_ShowString(2, 1, "W:");OLED_ShowString(3, 1, "R:");// 读取并显示W25Q64的ID信息W25Q64_ReadID(&MID, &DID);OLED_ShowHexNum(1, 5, MID, 2);OLED_ShowHexNum(1, 12, DID, 4);// 擦除W25Q64的一个扇区并写入数据W25Q64_SectorErase(0x000000);W25Q64_PageProgram(0x000000, ArrayWrite, 4);// 读取数据并显示W25Q64_ReadData(0x000000, ArrayRead, 4);OLED_ShowHexNum(2, 3, ArrayWrite[0], 2);OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);OLED_ShowHexNum(3, 3, ArrayRead[0], 2);OLED_ShowHexNum(3, 6, ArrayRead[1], 2);OLED_ShowHexNum(3, 9, ArrayRead[2], 2);OLED_ShowHexNum(3, 12, ArrayRead[3], 2);while (1){}
}

http://www.ppmy.cn/server/114817.html

相关文章

第十七题:电话号码的字母组合

题目描述 给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有可能的由它组成的字母组合。你可以假设输入字符串至少包含一个数字&#xff0c;并且不超过3位数字。 实现思路 使用哈希表或数组存储每个数字对应的字符&#xff0c;然后通过递归或迭代的方式生成所有可能的组…

【STM32】SPI通信-软件与硬件读写SPI

SPI通信-软件与硬件读写SPI 软件SPI一、SPI通信协议1、SPI通信2、硬件电路3、移位示意图4、SPI时序基本单元(1)开始通信和结束通信(2)模式0---用的最多(3)模式1(4)模式2(5)模式3 5、SPI时序(1)写使能(2)指定地址写(3)指定地址读 二、W25Q64模块介绍&#xff11;、W25Q64简介&am…

《生成式人工智能行业自律倡议》发布,BAT、华为小米 OPPO 荣耀、北大清华等参编

8 月 29 日,在成都举办的 2024 年中国网络文明大会上,《生成式人工智能行业自律倡议》正式发布。 《生成式人工智能行业自律倡议》从保障数据和算法模型安全合规、促进内容生态建设、追求技术创新与质量提升、遵循价值观与伦理道德标准和促进交流合作与开放共建等方面发出行…

动手学习RAG: 向量模型

在世界百年未有之变局与个人自暴自弃的间隙中&#xff0c;我们学一点RAG。 RAG是一种独特的应用&#xff0c;“一周写demo&#xff0c;优化搞半年”&#xff0c;我甚至听说它能破解幻术。 为了理解其优化中的关键一环&#xff0c;我们先看下文本向量。文本向量除了是RAG检索的…

农业小气候观测站

农业小气候观测站的主要功能包括&#xff1a; 气象观测&#xff1a;观测和记录气温、湿度、降水量、风速和风向等气象要素&#xff0c;以了解当地的气候状况和变化趋势。 土壤观测&#xff1a;监测土壤湿度、温度、酸碱度和养分含量等指标&#xff0c;以帮助农民合理调控灌溉和…

鸿蒙开发(API 12 Beta6版)【通用属性协议】 网络篇

简介 通用属性协议是GATT&#xff08;Generic Attribute&#xff09;的缩写&#xff0c;它是一种用于在蓝牙低功耗设备之间传输数据的协议&#xff0c;定义了一套通用的属性和服务框架。通过GATT协议&#xff0c;蓝牙设备可以向其他设备提供服务&#xff0c;也可以从其他设备获…

93. UE5 GAS RPG 应用负面效果表现

在上一篇文章里&#xff0c;我们实现了添加负面效果GE&#xff0c;并且在添加GE时&#xff0c;也会给角色应用一个负面效果标签作为标识。在这一篇里&#xff0c;我们将通过负面效果标签标识&#xff0c;应用角色身上展现对应的负面效果的表现。 我们将在这篇文章里添加一个自定…

【电力系统】复杂网络分析在电力系统规范中的应用

摘要 复杂网络分析在电力系统中的应用为理解和优化电力系统的运行提供了新的视角。本文探讨了复杂网络理论在电力系统规范中的应用&#xff0c;通过分析电力系统的拓扑结构、节点重要性和脆弱性&#xff0c;提出了优化电力系统设计和运行的新策略。仿真结果表明&#xff0c;复…