上次我们了解了W25Q64芯片,这次我们软件SPI读写。
一.硬件接线电路

二.程序编写
1.程序准备

复制一下OLED显示函数,进行修改

先编译一下,方便我们代码有提示。

快速添加好两个模块。

还有我们的.h文件。
2.引脚初始化

复制一下LED的初始化,然后我们进行修改。

对于主机来说,时钟,片选,主机输出,都是输出引脚,剩下一个是主机输入,是输入引脚。所以这一个角是浮空或上拉,我们选择上拉就可以了。

这个DO就是从机输出,也就是主机输入,所以PA6为上拉模式,剩下的为推完模式。

这样引脚初始话就完成了
3.重新封装函数。
(1)选择外设函数

SS对应的是PA4引脚

BitAction就是这个类型。
(2)时钟函数

CLK就是SCK对应的是PA4引脚
(3)主机输出从机输入

MOSI对应的是PA7引脚
(4)主机输入从机输出

直接返回PA6口电平
这样我们4个SPI的通信引脚,我们就包装换好名字了。
(5)初始化后默认电平设置

初始化之后置引脚电平,SS置高电平,默认不选中从机,SCK计划使用模式0,所以初始化为低电平,MOSI没有明确规定可以不管,MISO是输入引脚不用输出电平。
三.SPI函数编写
1.起始信号

直接把SS置低电平就可以了。

2.终止信号

3.交换一个字节
(1)模式0

交换一个字节 这个ByteSend是我们传进来的参数,要通过交换一个字节发送出去,返回值是ByteReveive是通过交换一个字节收到的数据。通过返回值返回到调用函数的地方。

先定义一个接收变量。之后返回回去,在这中间我们来完成时序。
如图SS下降沿和数据转移几乎是同时发生的,但是并不是,需要先下降沿再转移数据,这个下降沿是触发数据移出这个动作的条件,先有了下降沿,才会有数据移出这个条件。对于硬件SPI来说,因为使用了硬件的移位寄存器电路,所以这两个动作几乎是同时发生的。对于软件SPI来说,由于软件执行程序是一条一条执行的,我们不可能同时完成两个动作,所以软件SPI我们直接把它看成先后执行的逻辑。
逻辑就是先SS下降沿,先把数据移出,再SCK上升沿,移入数据。再SCK下降沿再移出数据。以这个具有先后顺序的逻辑来执行,这样才能对应一条条依次执行的代码。
![]()
写MOSI最高位 0X80= 1000 0000 与的话就是提取最高位
![]()
写SCK为1,产生上升沿,让从机读走数据

主机的任务是上升沿后,把这个从机放到MISO的数据读取,把这个位或上去这样子最高位就是1,就是读到的数据了。

然后我们用一个循环套起来,就可以实现SPI模式0交换一个字节的时序了。
然后这个程序目前是用0x80>>i位的方式,依次取出ByteSend的每一位,或者依次给ByteReceive的每一位置1这个0x80右移i的作用就是用来挑出数据的某一位或者某几位。就是用来屏蔽其他位,那么就可以把这种类型的数据叫做掩码。
我们可以优化一下程序。
![]()
就是把ByteSend最高位移到MOSI,ByteSend移出了一位,他的最低位会自动补0。

把收到数据放在 ByteSend最低位,然后循环。就不需要ByteReveive这个变量了。这里我们暂时使用第一种方法,因为第二种的BYTESEN传输之后改变了。
(2)修改为模式1

只需要改一下相位就可以了。
(3)修改为模式3

再模式1的基础上,我们就可以把所有出现SCK时钟的地方,0变为1,1变为0。就可以了。
(4)修改为模式2
再模式0的基础上,我们就可以把所有出现SCK时钟的地方,0变为1,1变为0。就可以了。
4.声明函数

这四个函数,一个初始化,三个SPI时序基本单元,
![]()
编译一下。
四.W25Q64驱动函数编写
1.新建模块


快速填写.h文件。

调用MySPI头文件。

初始化调用一下我们就要实现业务代码了。拼接完整的时序,主要参考手册的指令集部分。

2.读取ID号验证SPI时序
我们先写SPI读取ID号的时序,然后进行判断,看看SPI通信是否错误。

读取ID号的时序就是,先起始,然后交换发送指令9F,随后连续交换接收3个字节,停止,我该怎么知道,哪个是交换发送,哪个是交换接收呢。

手册中写到,圆括号括起来的,就是我们需要交换接收的数据。这里的三个字节都是括起来的,所以这三个字节都需要我们交换接收。
第一个字节是厂商ID,表示是哪个厂家生产的,后两个字节是设备ID,设备ID的高八位,表示存储器类型,低八位表示容量。
我们计划这个函数是有两个返回值的,我们依旧使用指针来实现多返回值,原来的返回值就不要了

开始传输
![]()
写入要传输字节开始,交换第一个字节 ,返回值是交换接收的,因为交换接受的没有意义,所以这里我们就不要了。

然后把数据存在指针变量里面,高八位和低八位分开存。第二次读取变为或等于。不能直接写等于,否则高八位置0了。

最后加上结束。
3.小测试

声明函数。
![]()
引用头文件。
![]()
初始化。

定义两个变量

把ID号和厂商ID的地址传送过去。等函数执行完,ID号我们就拿到了。

用OLED显示一下。

编译下载。

可以看到屏幕上显示了两个数,厂商ID是EF,设备ID是4017。

对照手册,厂商ID是EF,设备ID我们用的是9F指令,所以读取的就是4017。
4.封装指令函数
然后我们,把指令函数封装一下
![]()
新建一个指令模块。

快速写好.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

我们进行一下复制。

引用头文件。

(1)写使能函数
定义一个写使能的函数。

这样就可以了。
(2)读状态寄存器1
我们读状态寄存器的原因就是看看芯片在不在忙。

我们要读取他的最低位,BUSY看看是不是还在忙,在忙的话就是1,不忙了就是0。

手册中写道,这个状态寄存器可以被连续读出,这个连续读出的特性,就方便我们进行等待操作。

所以这就是我们的等待函数。加一个break是为了跳出循环。防止程序卡死
(3)发送页函数编写。

函数我都会注释出来用处。最后来一个结束函数。
我们调用这个函数,给一个指定的起始的地址,再给一个要写入的数组和数量,他就能帮我们写入数据了。
(3)扇区擦除

调用这个函数,所在地址的扇区就会被擦除。
(4)读取数据
![]()

我们调用这个函数,给一个指定的起始地址再给接收数据的数组和数量,他就能帮我们读取数据芯片了。

我们在页编程和擦除数据之前,给一个写使能,就不用到时候我们再去写使能了。这样我们在调用写使能的函数,这样我们调用写入的函数,就不需要写使能了。
这个写使能仅对之后跟随的一条时序有效。所以我们在每次写入之前都加一条写使能。
之后我们还需要完成等待,可以进行事前等待和事后等待,事前等待就是在每次要写入之前,进行等待,事后就是,在每次写入之后进行等待,结束了在退出函数。这两者的区别就是,事后等待更保险,在函数之外的地方,芯片肯定是不忙的。事前等待效率会高一些。可能等待时候执行别的代码,回来等待完成了。事后等待只需要在写入后调用就行,事前等待,需要在写入操作和读取操作之前,都需要调用。因为芯片忙的时候,读取也是不可以的。如果采取事前等待,那么读取操作之前,就需要等待。这就是两中等待方式的区别。

我们在页编程之后,实行等待。

之后扇区擦除也来一个等待。
(5)测试

把三个函数放在头文件声明一下。

定义要发送和接收的数组。

OLED显示的ID号和数据都换个位置。

下载就是这样子。


然后我们写入数据并且显示出来。

在显示一下读出来的。如果没用问题的话,写入的和读取的都会是一样的数据。

编译下载。

显示没用问题。

修改一下写入数组。

读出A1,B2,C3,D4,也没有问题。

我们把擦除和写入的代码注释掉

数据目前没用变化。

断电重启。

读取的仍外元数据

我们解除擦除的数据,只擦除不写入。

可以看到读取的数据全是FF。说明flash擦除之后都是FF。
我们验证一下flash只能1写0,不能0写1的特点


这是有擦除的写入。
![]()
把擦除注释掉,然后只更改不擦除。
![]()
直接更改为新数据。

可以看到写入的和读出的数据不一样。如果不擦除,读出的数据=原始数据&写入的数据(按位与)。
我们再来看一下写入数据不能跨页的现象,上次我们知道,页地址范围是xxxx00~xxxxFF。所以最后两个数是页内地址,前面是页地址

我们直接从第一页最后写,看看能不能跨页写道下一页。
![]()
读取数据也从FF开始读。

可以看到写入的数据到了写出的,就只有一位起作用。这说明写入数据没用跨页,这个55写入的地址是0FF后面的66并没有跨越到下一个地址100。而是返回第一页页首,000了。又因为读取时可以跨页的所以读取到的FF是第二页的数据。第二页是擦出了的,没有写入,所以默认是FF。

再把读取,改到前一个地址00,看看之前写入的,是不是在第一个字节的起始位置。

可以看到果然这三个字节是在第一页起始地址位置。这就是我们flash写入不能跨页的现象。


被折叠的 条评论
为什么被折叠?



