系列文章目录
文章目录
- 系列文章目录
- 前言
- 一、电路接线图
- 二、应用案例代码
- 三、应用案例分析
-
- [3.1 SPI通信模块](#3.1 SPI通信模块)
- [3.2 W25Q64模块](#3.2 W25Q64模块)
- [3.3 主程序](#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:
c
#ifndef __MYSPI_H
#define __MYSPI_H
void MySPI_Init(void);
void MySPI_Start(void);
void MySPI_Stop(void);
uint8_t MySPI_SwapByte(uint8_t ByteSend);
#endif
MySPI.c:
c
#include "stm32f10x.h" // Device header
void 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:
c
#ifndef __W25Q64_H
#define __W25Q64_H
void 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:
c
#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:
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:
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)
{
}
}
三、应用案例分析
从整体架构来看主要分为SPI通信模块、W25Q64模块,接下来重点分析一下SPI通信模块以及W25Q64模块。
3.1 SPI通信模块
SPI通信模块主要封装了一个模块初始化函数和三个时序单元模块。
SPI模块初始化函数:
c
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 引脚为高电平,即取消选择从设备,然后初始化时钟引脚为低电平。
三个时序单元模块:
三个时序单元分别是起始信号、终止信号以及交换一个字节。
起始信号、终止信号:
c
void MySPI_Start(void)
{
MySPI_W_SS(0);
}
void MySPI_Stop(void)
{
MySPI_W_SS(1);
}
起始条件:SS从高电平切换到低电平
终止条件:SS从低电平切换到高电平
交换一个字节:
c
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模块初始化函数:
c
void W25Q64_Init(void)
{
MySPI_Init();
}
调用MySPI_Init()函数,完成引脚配置和SPI的初始化。
写使能:
c
void W25Q64_WriteEnable(void)
{
MySPI_Start();
MySPI_SwapByte(W25Q64_WRITE_ENABLE);// 发送写使能命令
MySPI_Stop();
}
发送写使能命令,以允许后续的写入或擦除操作。
页编程(写入数据):
c
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);
两种方法都可以,我这里只是提一下。
读数据:
c
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中。
忙等待:
c
void W25Q64_WaitBusy(void)
{
uint32_t Timeout;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);// 读取状态寄存器1
Timeout = 100000;
while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01){// 检查忙标志位
Timeout --;
if (Timeout == 0)
{
break;
}
}
MySPI_Stop();
}
发送读取状态寄存器1命令,进入循环等待存储器空闲(即状态寄存器的最低位变为0),避免在存储器仍处于忙碌时进行新的操作。
读厂商、设备ID:
c
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)为两个字节。
扇区擦除:
c
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上显示。
c
#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)
{
}
}