stm32入门-----软件SPI读写W25Q64

目录

前言

一、软件SPI读写W25Q64

1.MySPI.c(SPI读写模块)

(1)对SS、SCK、MOSI、MISO的操作

(2)初始化

(3)起始与终止

(4)交换字节

  2.W25Q64.c文件(W25Q64读写模块)

W25Q64_lns.h文件

(1)初始化以及获取设备ID

(2)写使能操作

(3)读取状态寄存器【BUSY】

(4)页编程写入数据

(5)擦除数据

(6)读取数据

 二、项目实操

1.实现掉电不丢失数据

2.证明擦除数据都是后的每一位都为1

3.证明W25Q64每个数据位只能由1改写为0,不能由0改写为1

4.证明数据存储是不能跨页的 


前言

        接着上一期的内容,本期我们就开始学习软件的SPI读写W25Q64项目的编程实现,在我们学习软件I2C读写的时候也是有类似的操作的,下面我们就可以对比一下SPI和I2C的软件读写有什么异同。(相关理论链接:stm32入门-----W25Q64存储器-CSDN博客stm32入门-----SPI通讯协议-CSDN博客。视频:[11-3] 软件SPI读写W25Q64_哔哩哔哩_bilibili

 本期的代码我已上传至百度网盘,可自行下载。

通过百度网盘分享的文件:软件SPI读写W25Q64.rar
链接:https://pan.baidu.com/s/1FHNFNILBULbR-V-Bm67nRA?pwd=0721 
提取码:0721

一、软件SPI读写W25Q64

 电路连线图:

下面展示的是SPI读写功能主要文件。其中MySPI.c 和MySPI.h文件是用来编写SPI通讯操作的功能模块;W25Q64是用前面MySPI模块编好的函数来编写写关于W25Q64元器件的操作。

下面我就对这些主要功能的模块去进行一一讲解。

1.MySPI.c(SPI读写模块)

这里我们选择的是模式0的方法来读写操作,模式0如下:

(1)对SS、SCK、MOSI、MISO的操作

下面我定义了对SS、SCK、MOSI、MISO的操作的函数,然后就可以去调用这些函数来去进行写入指定数据或者读取数据的操作了,就不需要去调用GPIO口的本地函数了。

//对SS的写入
void SPI_W_SS(uint8_t val) {
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)val);
}
//对SCK的写入
void SPI_W_SCK(uint8_t val) {
    GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)val);
}
//对MOSI的写入
void SPI_W_MOSI(uint8_t val) {
    GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)val);
}
// 读取MISO
uint8_t SPI_R_MISO() {
    return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
(2)初始化
void SPI_init() {
    	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	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);					//将PA4、PA5和PA7引脚初始化为推挽输出
	
	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);					//将PA6引脚初始化为上拉输入

    //模式0
    SPI_W_SS(1);//SS默认情况下为高电平
    SPI_W_SCK(0);//SCK默认为低电平
}
(3)起始与终止
  • 起始条件:SS从高电平切换到低电平
  • 终止条件:SS从低电平切换到高电平

                        起始                                                                                 终止 

 对比上面的时序,我们就可以写起始与终止的函数了。

//1.起始
void SPI_Start() {
    //SS置为低电平
    SPI_W_SS(0);
}
// 2.终止
void SPI_Stop() {
    // SS置回高电平
    SPI_W_SS(1);
}
(4)交换字节

 在前面我们学习过SPI通讯实际上就是主机和从机之间的数据交换(有不懂的可以看此处:stm32入门-----SPI通讯协议-CSDN博客),那下面我们就可以根据模式0的特征来去写编程了。

交换一个字节(模式 0 )

  • CPOL=0:空闲状态时,SCK为低电平
  • CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

这里有两种编程的方法,分别是掩码法和移位模型法。二选一使用就行了。

下面是通过掩码操作的方法,这个方法是对要操作指定的数据位进行处理,当我们要移入或者移出这个数据的某一个位的时候,就可以通过掩码去遮住其他不需要的位,只看这个位就行了,这个方法是不会改变原来数据的。 

// 3.交换字节   模式0
// 掩码操作
uint8_t SPI_SwapByte(uint8_t byteSend) {
    uint8_t i, byteRecive = 0x00; //byteRecive存储接受的数据

    for (i = 0;i < 8;i++) {
        SPI_W_MOSI(byteSend & (0x80 >> i)); //主机移出第这个字节的最大位数据,
        SPI_W_SCK(1); //sck为上升沿,开始移入数据
        if (SPI_R_MISO() == 1) {//此时接受从机在MISO线上的数据
            byteRecive |= (0x80 >> i);
        }
        SPI_W_SCK(0);//sck下降沿,主机和从机移出数据
    }
    return byteRecive;
}

下面是移位模型法, 对于移位模型法,我们可以看出这个方法其实跟SPI交换置换数据位是相当贴合的,SPI本身来说就是移出数据位然后移入位,通过移位模型可以更加直观理解这个方法,但是移位模型会改变原来数据的本身,数据本身就是进行移位处理的,如果后面需要用到原来数据的话就是不能直接拿来用的。

// 移位模型方法
uint8_t SPI_SwapByte(uint8_t byteSend) {
    uint8_t i;
    for (i = 0;i < 8;i++) {
        SPI_W_MOSI(byteSend & 0x80); //主机移出第这个字节的最大位数据,
        byteSend <<= 1;
        SPI_W_SCK(1); //sck为上升沿
        if (SPI_R_MISO() == 1) {//此时接受从机在MISO线上的数据
            byteSend |= 0x01;
        }
        SPI_W_SCK(0);//sck下降沿,主机和从机移出数据
    }
    return byteSend;
}

  2.W25Q64.c文件(W25Q64读写模块)

下面是对于W25Q64的读写要求,我们编程必须按照这些要求来去写。

写入操作时:

  1. 写入操作前,必须先进行写使能
  2. 每个数据位只能由1改写为0,不能由0改写为1
  3. 写入数据前必须先擦除,擦除后,所有数据位变为1
  4. 擦除必须按最小擦除单元进行
  5. 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入
  6. 写入操作结束后,芯片进入忙状态,不响应新的读写操作

读取操作时:

  • 直接调用读取时序,无需使能,无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
W25Q64_lns.h文件

下面是 W25Q64_lns.h文件,这个文件存储了关于W25Q64的相关指令的定义,我们把这个模块之间引入到W25Q64.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_DUMMY_BYTE  这个对应的是0xFF, 这个数据表示的是在交换数据的时候其中一个设备只进行单方向的读取或者发送的时候,而另外一个设备所发送的无用数据,打比方就是主机要读取从机设备的数据的时候,主机本身来说就是执行获取数据的操作而不需要发送什么数据,但是SPI通讯规定数据是置换的,所以主机就发送一个随便的数据给从机就行了,所以这里指定发送为0xFF,而真正需要的是从机传回来的数据。

#define W25Q64_DUMMY_BYTE							0xFF
(1)初始化以及获取设备ID

下面是0x9F指令来去获取厂商和设备ID指令的,可以看到返回的数据有两个,MF是厂商的ID,为一个字节。ID是指这个设备的ID,长度是16位,是两个字节拼接到一起的。

// 初始化
void W25Q64_init() {
    SPI_init();
}

//厂商ID,设备ID
void W25Q64_ReadID(uint8_t* MID, uint16_t* DID) {
    SPI_Start();
    SPI_SwapByte(W25Q64_JEDEC_ID);
    *MID = SPI_SwapByte(W25Q64_DUMMY_BYTE);
    *DID = SPI_SwapByte(W25Q64_DUMMY_BYTE);
    *DID <<= 8;
    *DID |= SPI_SwapByte(W25Q64_DUMMY_BYTE);
    SPI_Stop();
}
(2)写使能操作
//写入数据的时候,进行写使能
void W25Q64_WriteEnable() {
    SPI_Start();
    SPI_SwapByte(W25Q64_WRITE_ENABLE);
    SPI_Stop();
}
(3)读取状态寄存器【BUSY】

 在我们写入数据之后,W25Q64会执行数据搬运的操作,也就是把缓存上的数据放入到块区中存储起来,这时候芯片就会进入到“忙”状态,这时候芯片是无法再次执行写入或者擦除等操作的。所以我们需要一个函数来去获取当前芯片的状态是否处于“忙”状态,如果是那就继续等待这个状态完成,另外就是,在“忙”状态的时候,读取芯片寄存器的数据是不影响的,直接读取就行了。

//读取状态寄存器 BUSY
void W25Q64_WaitBusy() {
    uint32_t time_out = 100000;//超时处理
    SPI_Start();
    SPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
    while ((SPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) {
        time_out--;
        if (time_out == 0) {
            break;
        }
    }
    SPI_Stop();
}
(4)页编程写入数据

 写入数据是存储到页当中,这里是属于页编程,一个页是有256个字节,对应的也就是有24为地址(0~23),所以当我们发送指定的地址的时候要分三次发送(三个字节),紧接着的才是写入的数据字节。

  •  指定地址写
  • 向 SS 指定的设备,发送写指令( 0x02 ),随后在指定地址(Address[23:0] )下,写入指定数据( Data )

//页编程,写入数据,多位
void W25Q64_PagePro(uint32_t address, uint8_t* DataArr, uint16_t count) {
    uint16_t i;
    W25Q64_WriteEnable();
    SPI_Start();
    
    SPI_SwapByte(W25Q64_PAGE_PROGRAM);
    SPI_SwapByte(address>>16);
    SPI_SwapByte(address>>8);
    SPI_SwapByte(address);
    for (i = 0;i < count;i++) {
        SPI_SwapByte(DataArr[i]);
    }
    SPI_Stop();
    /* 这里说明一下,这里页编程写入数据之后可以选择两种方式来取等待busy,一个是事前等待,另外一个是事后等待
       对于事前等待是效率比较高的,因为事前等待是对于写入数据的时候等待的操作,这个时间段处于忙的时候无法写入数据,但是可以执行其他功能
       对于事后等待是等写入数据之后忙结束了之后才去结束写入的操作,这个过程中是无法执行任何数据的,因为还没有结束写入函数的功能。*/
    W25Q64_WaitBusy(); //这里我选择事后等待,比较保险一些,程序比较稳定
}
(5)擦除数据

 擦除数据其本质上是写入数据的一种,也就是把全部数据位都置1,因为在W25Q64是中写入数据的时候1是可以改为0,0是不能改为1的。还有要根据要求擦除操作的时候是必须按照大于最小单元擦除,也就是至少按一个扇区去擦除。下面是按照一个扇区擦除。

//擦除,扇区(本身就是一种写入操作,把全部都写入0xFF,每一位都为1)
void W25Q64_SectorErase(uint32_t address) {
    W25Q64_WriteEnable();
    SPI_Start();
    SPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
    SPI_SwapByte(address>>16);
    SPI_SwapByte(address>>8);
    SPI_SwapByte(address);
    SPI_Stop();
    W25Q64_WaitBusy(); //这里我选择事后等待,比较保险一些,程序比较稳定
}
(6)读取数据

这里传入一个指针进去来获取从机返回的数据。

//读取数据,传入回数组中
void W25Q64_ReadData(uint32_t address, uint8_t* DataArr, uint32_t count) {
    uint32_t i;
    SPI_Start();
    SPI_SwapByte(W25Q64_READ_DATA);
    SPI_SwapByte(address>>16);
    SPI_SwapByte(address>>8);
    SPI_SwapByte(address);

    for (i = 0;i < count;i++) {
        DataArr[i] = SPI_SwapByte(W25Q64_DUMMY_BYTE);
    }
    SPI_Stop();

}

 二、项目实操

1.实现掉电不丢失数据

main.c代码如下,这里我们写入的数据是{ 0x01,0x02,0x03,0x04 },那么我们只需要去把写入这部分删除,然后只执行读取,然后看看结果就行了。先把下面这个写入数据的代码编译烧录进去。

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"

uint8_t MID;
uint16_t DID;

uint8_t arrWrite[] = { 0x01,0x02,0x03,0x04 };
uint8_t arrRead[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, 13, DID, 4);

	W25Q64_SectorErase(0x000000);//扇区的地址取决于前面三位,后面三位无论怎么变都是表示同一个扇区
	W25Q64_PagePro(0x000000, arrWrite, 4);//写入
	W25Q64_ReadData(0x000000, arrRead, 4);//读取

	// 展示发送的数据
	for (uint8_t i = 0;i < 4;i++) {
		OLED_ShowHexNum(2, 3+3*i, arrWrite[i],2);
	}
	// 展示读取的数据
	for (uint8_t i = 0;i < 4;i++) {
		OLED_ShowHexNum(3, 3+3*i, arrRead[i],2);
	}

	while (1) {
		
	}
}

然后把下面指定的部分内容注释掉,再次编译烧录。

    //W25Q64_SectorErase(0x000000);//扇区的地址取决于前面三位,后面三位无论怎么变都是表示同一个扇区
    //W25Q64_PagePro(0x000000, arrWrite, 4);//写入
    W25Q64_ReadData(0x000000, arrRead, 4);//读取

效果如下,两次编译后烧录的效果是一样的,说明成功实现掉电不丢失数据的功能。

2.证明擦除数据都是后的每一位都为1

这里我们把main.c代码这部分内容进行修改。

改为: 

    W25Q64_SectorErase(0x000000);//扇区的地址取决于前面三位,后面三位无论怎么变都是表示同一个扇区
	//W25Q64_PagePro(0x000000, arrWrite, 4);//写入
	W25Q64_ReadData(0x000000, arrRead, 4);//读取

效果如下,可以看到读取后的每一位都是0xFF,对应的二进制数也就是都是1。

3.证明W25Q64每个数据位只能由1改写为0,不能由0改写为1

下面这个是最开始的数据。

然后我们把main.c的相关内容修改为下面的样子,再次编译烧录。

uint8_t arrWrite[] = { 0xAA,0xBB,0xCC,0xDD };
    //W25Q64_SectorErase(0x000000);//扇区的地址取决于前面三位,后面三位无论怎么变都是表示同一个扇区
	W25Q64_PagePro(0x000000, arrWrite, 4);//写入
	W25Q64_ReadData(0x000000, arrRead, 4);//读取

现象如下,这里我们可以看到我们在没有执行擦除的时候再次写入数据 { 0xAA,0xBB,0xCC,0xDD }会跟我们预期的不一样,这里实际上我们写入的数据与原来的数据进行了与运算后的结果,这也说明了W25Q64每个数据位只能由1改写为0,不能由0改写为1。

4.证明数据存储是不能跨页的 

下面是修改的部分,读取的页数为第一页最后一个位置开始,当存储第二个数据的时候按道理来说就会放入到第二个页面上去存储。

	W25Q64_SectorErase(0x000000);//扇区的地址取决于前面三位,后面三位无论怎么变都是表示同一个扇区
	W25Q64_PagePro(0x0000FF, arrWrite, 4);//写入
	W25Q64_ReadData(0x0000FF, arrRead, 4);//读取

然后编译烧录,效果如下,这里我们就看出除了第一个数据没问题,而后面都变为0xFF了,这也说明了第二个数据之后的数据是没有存储到第二个页面的,因为存储出现了穿越页面层,是不允许的。

以上就是本期的全部内容了,我们下次见!

今日壁纸:

  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fitz&

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值