本文主要是介绍stm32之软件SPI读写W25Q64存储器应用案例,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
系列文章目录
1. stm32之SPI通信协议
文章目录
- 系列文章目录
- 前言
- 一、电路接线图
- 二、应用案例代码
- 三、应用案例分析
- 3.1 SPI通信模块
- 3.2 W25Q64模块
- 3.3 主程序
前言
提示:本文主要用作在学习江科大自化协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完成一个字节的交换。
至于关于代码部分的按位与(提取字节中的每一位)和按位或(设置字节中的某位)以及左移、右移操作的解析在我之前的一篇文章里已经详细地分析过了,如果有不懂的就翻回去看看就行了,这里就不再累述了。
文章传送门:计算机常见运算之左移操作、右移操作以及按位与、按位或
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){}
}
这篇关于stm32之软件SPI读写W25Q64存储器应用案例的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!