软件SPI读写W25Q64在STM32上的实践详解(江科大笔记分享)
目录
接线图
原理图
整体框架
初始化配置
GPIO初始化
起始和终止信号
编辑封装交换字节函数
the First way
the Sencond way
建立W25Q64.c和W25Q64.h文件
先封装初始化函数
封装一个读ID的函数
写使能
忙状态
页编程的函数
扇区擦除
读数据的时序
测试部分
代码部分
接线图
原理图
整体框架
1.通信引脚封装,初始化,SPI通信的3个拼图(起始,终止,交换一个字节)的软件驱动层
2.建立一个W25Q64的模块,作为硬件驱动层,拼接各种指令和功能的完整时序,写使能,页编程,擦除,读数据
初始化配置
GPIO初始化
根据主机图,对GPIO进行模式配置,对照接线图,主机输入PA6为上拉输出。时钟,主机输出和片选接的是PA4,PA5,PA7为推挽输出
使用封装函数来封装1个输入Pin口和3个输出Pin口,这是江科大采用的方式,这里不用define形式来定义封装函数,是为了之后可以使用延时函数,但SPI的通讯传输速度过快,不需要调用Delay函数,所以两种方式皆可
以下是我自己用define定义的封装函数,已经注释
CS选择高电平,默认不选择主机,SCK使用模式0,MISO是输入引脚,不用输入电平,MOSI没有明确规定,不用管
起始和终止信号
起始信号,把SS置为低电平,这里命名为CS,就把CS置为低电平
终止信号,把CS置高电平
根据PPT的交换字节的原理图,在模式0下,定义交换字节函数,重点看途中上升沿和下降沿的那条线来看三个定义的函数从而分析他们的功能。
封装交换字节函数
MySPI_SwapByte(uint8_t ByteSend)的两种方法
the First way
封装函数MySPI_SwapByte,用以定义交换一个字节。
定义一个字节i,定义一个字节发送的函数,调用for循环,循环8次,读取8个字节。
把发送的MOSI的最高位读出来,每次循环之后,就把需要与的字节位向右移动一位,保证这个函数能够从左往右依次发送一个字节,发送出来之后,先把SCK置为高电平,使用if函数,如果每次接收到的字节为1,接收函数就会与最高位到最低位的字节(0x80,0x40,0x20…0x01)进行或运算,依次接收一个1字节,将SCK置0,跳出循环。最后返回ReveiveByte
the Sencond way
定义一个8字节数据i,利用for循环,发送字节的最高位,发送以后,把次高位左移一位,SCK置为高电平,如果接收的字节为1,ByteSend就会或上一位1,把该字节接收出来,把SCK置低电平,返回值也就返回ByteSend,不用再多定义一个ReceiveByte
注:模式0,模式1,模式2,模式3只需要在循环函数里改掉SCK高低电平的顺序就行,不需要多余操作。
建立W25Q64.c和W25Q64.h文件
先封装初始化函数
封装一个读ID的函数
定义两个指针,8位的ID为厂商ID,16位的ID为设备ID,设置起始和终止条件,将字节交换完的数据包装成地址,赋值给指针。
在main函数里面,先includeW25Q64的头文件,定义ID号和设备号,把W25Q64初始化,再调用读ID的函数,读出MID和DID的指针地址。
根据手册,厂商ID和设备ID分别为EF和4017
以下是实测测出来的数据,与手册一致
宏定义的代码#include "W25Q64_Ins.h"会在文章末尾显示出来,需要自取,江科大的源文件和教学在这里并没有给注释,我自己加上去了
写使能
开始-交换写使能字节-结束
忙状态
开始-交换一个读取寄存器忙状态1的函数-如果读出的字节与上0x01为1,则退出循环,为0则继续等待-但是while1循环等待的过程中如果次数过多,程序就会卡死-所以定义一个时间停止的函数-让他循环5000次之后自动退出-结束
等待状态分为事前等待和事后等待
页编程的函数
发送指令02-发送3位地址-发送数据
定义一个32字节的地址,因为实际需要的24位的地址,定义的字节数必须必实际需要的要大。8位字节的指针,16字节的Count表示一次写多少个,写使能,忙状态,起始地址,结束地址地址,放在首尾。
用交换字节函数写入页编程来发送指令,Address三次分别向右移去16位,8位,0位地址,因为定义了地址是8位字节,会舍弃最高位最高位,比如,0x123456,三次分别取到的字节数就是0x12,0x34,0x56。
接着来发送数据,第一次发送DataArray[0],数组的第一个字节,第二次发送DataArray[1]数组的第二个数组…用for函数封装起来,一共刚发送Count位。
扇区擦除
根据手册,要实现扇区擦除,先发送指令20,再发送3个字节的地址就行。
老样子,写使能和忙状态,开始和结束放在首尾-SwapByte发送指令定义里面给4KB的扇区擦除函数。
读数据的时序
根据数据手册,发送指令03-发送3个字节的指令-接收数据
该函数不用写使能,只有涉及写字节的函数需要写使能
最后别忘了加一个事后等待W25Q64_WaitBusy();
事后等待是为了同步主控(STM32)与存储器的操作状态,确保物理操作完成后再执行后续逻辑,这是SPI Flash操作的关键流程。
测试部分
读写测试部分
定义数组和数组读字节,在OLED屏幕上显示,分别显示MID,DID号,W,R,还有MID和DID的十六进制数。
以下为实测部分
W25Q64_Sector_Erase(0X000456)目的是为了擦除之前读到的字节,擦除函数之后,调用PageProgram和ReadData函数进行读写测试, 读写测试完之后,把写入和读到的字节显示在OLED屏幕上。
把擦除和写入的代码注释掉,显示的函数不变。
如果只注释掉写入的函数,那么读出的字节会全部变成FF,因为没有写入,读出的字节全部显示错误。
Flah只能1写0,不能0写1
写入的字节还是
如果不执行擦除,读出的数据 = 原始数据 & 写入的数据
FLash闪存要求擦除是写入的前提。
以下是不执行擦除函数的测试情况
至于为什么读出的数据是这个结果我目前还不清楚,江科大也没有明确解释,但如果按原理来的话,那么之前的原始数据就是0x00,0xAA,0xCC,0x88,读出的数据才会是00 22 44 88.
将写入的数据变成
W25Q64_PageProgram(0X0000FF, ArrayWrite, 4);
读取是跨页的
读取的数据,不能跨页写入,写到55位置,会从第二页写入66 77 88
代码部分
main.c
#include "stm32f10x.h" // Device header
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"
#include "W25Q64_Ins.h"
uint8_t MID;
uint16_t DID;
uint8_t ArrayWrite[] = {0x55,0x66,0x77,0x88};
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); //显示EF
OLED_ShowHexNum(1,12,DID,4); //显示4017
W25Q64_Sector_Erase(0X000000);//先擦除之前读到的字节
W25Q64_PageProgram(0X0000FF, 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)
{
}
}
MySPI.C
#include "stm32f10x.h" // Device header
/*
#define MySPI_W_CS(BitValue) GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
#define MySPI_W_SCK(BitValue) GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
#define MySPI_W_MOSI(BitValue) GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
#define MySPI_R_MISO() GPIO_ReadBit(GPIOA, GPIO_Pin_6, (BitAction)BitValue);
*/
void MySPI_W_CS(uint8_t BitValue)//封装写GPIO的名字,换一个名字,写CS的引脚
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
void MySPI_W_SCK(uint8_t BitValue)//写SCK的引脚
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
void MySPI_W_MOSI(uint8_t BitValue)//写MOSI的引脚
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
uint8_t MySPI_R_MISO(void) //读MISO
{
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_CS(1);//置为高电平,默认不选择主机
MySPI_W_SCK(0);//SPI使用0模式,默认0模式,空闲状态下,SCK置0
}
void MySPI_Start(void)//起始信号
{
MySPI_W_CS(0);
}
void MySPI_Stop(void)//终止信号
{
MySPI_W_CS(1);
}
//uint8_t MySPI_SwapByte(uint8_t ByteSend)//交换字节
//{
// uint8_t i;
// uint8_t ReceiveByte = 0x00;//定义一个接收字节用于接收
// for(i=0;i<8;i++)
// {
// MySPI_W_MOSI(ByteSend & (0X80 >> i));//依次读出ByteSend的每一位,也可以认为是为了屏蔽其他位
// MySPI_W_SCK(1); //上升沿
// if(MySPI_R_MISO() == 1){ReceiveByte |= (0X80 >> i)};
// MySPI_W_SCK(0); //下降沿
// }
// //MySPI_W_MOSI((SendByte >> 8) | 0X40);//主机写入从机的次高位
// return ReceiveByte;
//}
uint8_t MySPI_SwapByte(uint8_t ByteSend)//交换字节
{
uint8_t i;
for(i=0;i<8;i++)
{
MySPI_W_MOSI(ByteSend & 0X80);//读出输出字节最高位
ByteSend <<= 1; //左移一位,该位为原始数据的次高位
MySPI_W_SCK(1); //上升沿
if(MySPI_R_MISO() == 1){ByteSend |= 1;}
MySPI_W_SCK(0); //下降沿
}
return ByteSend;
}
MySPI.h
#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
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);//主机发给从机读ID号指令
*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//垃圾信息,但是可以发送ID号
*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID的高8位
*DID <<= 8; //第一次读到的数据运送到高8位里
*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE);//设备ID的低8位,为了不让高8位置0,写或等于
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 = 5000;
while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)//busy为1,否则进入循环等待
{
Timeout--;
if(Timeout == 0)
{
break;//while可能会进入死循环,所以定义一个循环数,让他能够顺利地循环出来
}
}
MySPI_Stop();
}
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)//页编程
{
W25Q64_WriteEnable(); //写使能
uint16_t i;
MySPI_Start();
MySPI_SwapByte(W25Q64_PAGE_PROGRAM);//页编程函数,发送指令
MySPI_SwapByte(Address >> 16);//移去最右边4位,address是为了给起始地址
MySPI_SwapByte(Address >> 8);//移去最右边2位
MySPI_SwapByte(Address);//不移动
for(i=0;i<Count;i++)
{
MySPI_SwapByte(DataArray[i]);//数据总共发送i位
}
MySPI_Stop();
W25Q64_WaitBusy();//事后等待 //忙状态
}
void W25Q64_Sector_Erase(uint32_t Address)//扇区擦除函数
{
W25Q64_WriteEnable(); //写使能
MySPI_Start();
MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
MySPI_SwapByte(Address >> 16);//移去最右边4位,address是为了给起始地址
MySPI_SwapByte(Address >> 8);//移去最右边2位
MySPI_SwapByte(Address);//不移动
MySPI_Stop();
W25Q64_WaitBusy(); //事后等待
}
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint16_t Count)//读取数据的时序
{
//在耗时操作时,已经等待过,不用再等待
uint32_t i;
MySPI_Start();
MySPI_SwapByte(W25Q64_READ_DATA);//单页SPI编程
MySPI_SwapByte(Address >> 16);//移去最右边4位,address是为了给起始地址
MySPI_SwapByte(Address >> 8);//移去最右边2位
MySPI_SwapByte(Address);//不移动
for(i=0; i<Count; i++)
{
DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE);//存储器内部的指针会自动自增
}
MySPI_Stop();
}
W25Q64.h
#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_Sector_Erase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint16_t Count);
#endif
W25Q64Ins.h(注释部分)
#ifndef __W25Q64_H
#define __W25Q64_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// 读取状态寄存器1(包含忙标志、写使能等状态)
#define W25Q64_READ_STATUS_REGISTER_2 0x35// 读取状态寄存器2(包含额外保护位和性能模式状态)
#define W25Q64_WRITE_STATUS_REGISTER 0x01// 写入状态寄存器(配置保护区域和存储锁)
#define W25Q64_PAGE_PROGRAM 0x02// 单线SPI页编程(按页写入数据,每页256字节)
#define W25Q64_QUAD_PAGE_PROGRAM 0x32// 四线SPI页编程(高速四线模式写入)
#define W25Q64_BLOCK_ERASE_64KB 0xD8// 擦除64KB块
#define W25Q64_BLOCK_ERASE_32KB 0x52// 擦除32KB块
#define W25Q64_SECTOR_ERASE_4KB 0x20// 擦除4KB扇区(最细粒度擦除)
#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// 唤醒设备并读取ID
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90// 读取制造商ID+设备ID(传统方式)
#define W25Q64_READ_UNIQUE_ID 0x4B// 读取64位唯一设备标识符
#define W25Q64_JEDEC_ID 0x9F// 读取JEDEC标准ID(含制造商/内存类型/容量)
#define W25Q64_READ_DATA 0x03// 标准SPI读取(单线模式)
#define W25Q64_FAST_READ 0x0B// 快速读取(带dummy cycle)
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B// 双线输出(数据线DO+IO1)
#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
#endif
作者:logicin