STM32标准库SPI通信协议与W25Q64-1
https://developer.aliyun.com/article/1508404
2.硬件电路
- 首先看一下引脚定义,VCC、GND是电源供电,引脚供电电压是2.7~3.6V,是一个典型的3.3V供电设备不能直接接入5V电压,然后1号脚cs,这个cs左边画了个斜杠代表是低电平有效,或者这边cs上面画了个横线也是低电平有效,那这里cs对应之前我们讲spi的名称就是SS,意思是spi的片选引脚,6号脚clk对应就是sck,是spi的时钟线,然后5号引脚di对应mosi,是spi主机输出从机输入,2号do对应miso,是spi主机输入从机输出,这四个引脚就是spi通信的四个引脚。
- 3号引脚wp,他的意思是写保护配合内部的寄存器器配置,可以实现保硬件的写保护,写保护低电平有效,wp接低电平保护住不让写,wp接高电平不保护可以,最后7号hold意思就是数据保持哈,低电平有效,这个用的不多了解一下,就是如果你在进行正常读写时突然产生中断,然后想用spi通信线去操控其他器件,这时如果把cs置回高电平,那时序就终止,但如果你又不想终止总线,又想操作其他器件,这就可以hold引脚置低电平,这样芯片就hold住了,芯片释放总线,但是芯片时序也不会终止,它会记住当前的状态,当你操作完其他器件时,可以回过来哈,hold置回高电平,然后继续hold之前的时序,相当于spi总线进来一次中断,并且在中断里还可以用spi干别的事情,这就是hold的功能。
- di、do、wp、和HOLD,旁边都有括号,写了lO0、lO1、lO2、lO3 ,这个就对应我们刚才这里说的双重spi和四重spi,如果是普通的spi模式,那括号里的都不用看,如果是双重spi,那di和do就变成lO0和lO1,也就是数据同时收和同时发的两个数据位,如果是四重spi,那就再加上wp当做lO2 ,HOLD当做lO3 ,这四个引脚都作为数据收发引脚,一个时钟四个数据位。
3.框图
- 首先右边这一整个矩形空间里是所有的存储器,存储器以字节为单位,每个字节都有唯一的地址,这样说了w25q64的地址宽度是24位3个字节,所以可以看到左下角第一个字节,它的地址是00 00 00h,h代表16进制,之后的空间地址依次自增,直到最后一个字节,地址是7F FF FF h,那最后一个字节为啥是7f开头,不是f f开头呢,因为24位地址最大寻址范围是16MB,我们这个芯片只有8MB,所以地址空间我们只用了一半,8MB排到最后一个字节,就是7F FF FF h,那这是整个地址空间,从000000~7F FF FF,然后在这整个空间里,我们以64kb为一个基本单元,把它划分为若干的块block,从前往后依次是块0块1块2等等,一直分到最后一块,那整块蛋糕是8MB,以64kb为一块进行划分,最后分得的快数就是8MB除以64kB,这里可以分得128块(8*1025/64=128),那块序号就是块0一直到最后一个是块127,然后观察一下块内地址值的变化规律,比如块0的起始地址是000000,结束地址是00f f f f,之后块31起始是1f0000 ,结束是1f f f f f,之后的都观察一下,可以发现在每一块内,它的地址变化范围就是最低的两个字节,每个块的起始地址是XX0000,结束是XXf f f f,这是块内地址的变化规律,到这里这一块大蛋糕我们就分好块了,64kb为一块总共128块,之后看一下左边这个示意图,我们还要再对每一块进行更细的划分,分为多个扇区sector,这里的虚线指向了右边的各个块,也就是告诉你每一块里面都是这个样子的,那在每个块里,它的起始地址是XX0000,结束地址是XXf f f f,在一块里我们再以4kb为一个单元进行切分,一块是64kb,我4kb一切总共16份,所以在每一块里都可以分为扇区0一直到扇区15,观察一下地址规律,可以发现每个扇区内的地址范围是XXX000到XXXf f f,地址划分啊到扇区就结束了,但是当我们在写入数据时啊,还会有个更细的划分,就是页Page,页是对整个存储空间划分的,当然你也可以把它看作在扇区里再进行划分都一样,那页的大小是256个字节,一个扇区是4kb,所以一个扇区里可以分为16页(4*1025/256=16),然后页的地址规律呢我们也看一下,在这里每一行就是一页,左边这里指了个箭头,写的是页地址的开始,右边这里也指了个箭头,写的是页地址的结束,在一页中,地址变化范围是XXXX00到XXXXFF,一页内的地址变化,仅限于地址的最低一个字节,这就是页的划分,那这个存储器的地址划分啊我就讲完了,我们需要记住的是一整个存储空间,首先划分为若干块,对于每一块又划分为若干扇区,然后对于整个空间会划分为很多很多页,每页256字节,这个我们需要记住。
- 控制逻辑左边就是spi的通信引脚,有wp、HOLD、CLK、CS、DI和DO,这些引脚就和我们的主控芯片相连,主控芯片通过spi协议,把指令和数据发给控制逻辑,控制逻辑就会自动去操作内部电路来完成我们想要的功能,然后去看控制逻辑上面有个状态寄存器器,这个状态寄存器是比较重要的,比如芯片是否处于忙状态,是否写使能,是否写保护,都可以在这个状态寄存器里体现,可以通过看手册分析,然后上面是写控制逻辑和外部的wp引脚相连,显然这个是配合wp引脚实现硬件写保护的,然后继续右边这里是一个高电压生成器,这个是配合flash进行编程的,因为flash是掉电不丢失的,如何实现掉电不丢失呢,比如你点亮一个led表示1,熄灭led表示0,但如果整个系统电都没有,那1和0就无从说起了,所以要想掉电不丢失,就要我们在存储器里产生一些刻骨铭心的变化,比如一个led我给他加很高的电压,那led就烧坏了,我们用烧坏的led表示1没烧坏的led表示0然后再断电,烧坏的led还是烧坏的,有电没电它都是坏的,这个烧没烧坏的状态,不受有电还是没电的影响,所以它就是掉电不丢失,那对于我们的非易失性存储器来说也是一样,我们要让它产生即使断电也不会消失的状态,一般都需要一个比较高的电压去刺激它,所以这种掉电不丢失的存储器,一般都需要一个高压源,那这里芯片内部集成了高电压发生器,所以就不需要我们在外接高电压了,比较方便哈,当然我这里只是举例简单描述一下掉电不丢失的存储原理。
- 然后继续看下面,这里是页地址锁存计数器,然后下面还有一个字节地址锁存计数器,这两个地址锁存和记数器就是用来指定地址的,我们通过spi总共发过来三个字节的地址,因为一页是256字节,所以一页内的字节地址就取决于最低一个字节,而高位的两个字节就对应的是页体质,所以在这里我们发的三个字节地址,前两个字节会进到这个页地址锁存计数器里,最后一个字节会进到这个字节地址锁存计数器里,然后页地址通过这个写保护和行解码来选择我要操作哪一页,字节地址通过这个列解码和256字节页缓存,来进行指定地址的读写操作,那就因为我们这个地址锁存,都是有个计数器的,所以这个地址指针在读写之后可以自动加1,这样就可以很容易实现从指定地址开始,连续读写多个字节的目的了,那最后右边这里有个256字节的页缓存区,它其实是一个256字节的ram存储器,这个稍微留个印象,等会儿还会提到,然后我们数据读写,就是通过这个ram缓冲区域来进行的,我们写入数据会先放到缓存区里,然后在时序结束后,芯片再将缓冲区的数据复制到对应的flash里,进行永久保存,那为啥要弄个缓冲区呢,我们直接往flash里写不好吗,那这是因为我们的spi写入的频率是非常高的,而flash的写入由于需要掉电不丢失,留下刻骨铭心的印象,他就比较慢,所以这个芯片的设计思路就是你写入的数据,我先放在缓存区里存着,因为缓存区是ram,所以它的速度非常快啊,可以跟得上spi总线的速度,但这里有个小问题,就这个缓冲区只有256字节,所以写入的时序有个限制条件,就是写入了一个时序,连续写入的数据量不能超过256字节,然后等你写完了,我芯片再慢慢的把数据从缓冲区转移到flash存储器里,那么数据从缓存区转到flash里,需要一定的时间哈,所以在写入时序结束后,芯片会进入一段忙的状态,在这里它就会有一条线哈,通往状态寄存器给状态接容器的busy位置1表示芯片当前正忙,那在忙的时候,芯片就不会响应新的读写时序了哈,就是写入的执行流程,然后我们读取数据,虽然这里画的话应该也是会通过缓冲区来读句,但是由于读取只看一下电路的状态就行了,它基本不花时间,所以读取的限制就很少了,速度也非常快。
4.Flash操作注意事项
- 第一点写入操作前必须先进行写使能,这个是一种保护措施,防止你误操作的,就像我们使用手机一样,先解锁再操作,这样可以防止手机在你裤兜里到处点点点对吧,写使能的话我们就使用spi发送一个写使能的指令,就可以完成了。
- 每个数据位只能由1改写为0,不能由0改写为1,这个意思就是说,flash并没有像ram那样的直接完全覆盖改写的能力,比如在某一个字节的存储单元里面,存储了0xAA这个数据,对应的二进制位就是10101010,如果我直接再次在这个存储单元写入一个新的数据,比如我再次写入一个0x55 ,那写完之后这个存储单元里存的是x55,实际上并不是,因为0x55的二进制是01010101,当这个01010101要覆盖原来的10101010时,就会受到这里第二条规定的限制,每个数据位只能由一改写为零,不能由零改写为一,你要问为啥会有这个限制,那只能说是成本原因或者技术原因,所以这里写入01010101之后,依次来看啊,最高位由原来的1改写为0是可以的,所以写出之后新的最高位就是零,但是第二位原来是零,现在我要改写成1,这是不行的,所以写入之后,新的第二位仍然是零,之后第三位要改写为零,可以,结果为零,第四位零改写为1,不可以,结果仍然是零,那以这个规律进行下去,0xaa在覆盖写入0x55 之后,这个存储单元最终的数据是什么啊,0x00也就是八位全为零,这就出现问题了对吧,所以为了弥补这个只能1改0,不能0改1的缺陷,我们就引出了第三条规定,就是写入数据前必须先擦除,擦除后所有数据位变为一,在这里flash是有一个擦除的概念的,擦除会有专门的擦除电路进行,我们只要给他发送擦除的指令就行了,那通过擦除电路擦除之后,所有的数据位都变成一,这样我们是不是就可以弥补第二条限制的缺陷了,当我们写出一个数据之前,无论原来存的是什么,我直接给它擦除掉,擦除之后所有的位变成1,也就是16进制的f f,这样我无论再写入什么样的数据,就都可以正确的写入了。
- 擦除必须按最小拆除单元进行,这个应该也是为了成本而做出的妥协,就是说你写入前要进行擦除,这我知道,所以如果我想在00这个地址下写入数据,那我就先把00地址擦除,再写入数据到00地址不就行了吗,但是这个方案有个问题啊,flash的擦除有最小擦除单元的限制,你不能指定某一个直接去擦除,要擦就得一大片一起擦,那在我们这个芯片里,你可以选择整个芯片擦除,也可以选择按块擦除或者按扇区擦除,然后再小就没有了,所以最小的擦除单元就是一个扇区,刚才我们看了一个扇区是4kb就是4096个字节,所以你擦除最少就得4096个字节一起擦,我只想查出某一个字节怎么办呢,这没办法你只能把那个字节所在扇区的4096个字节全都擦掉,那你又说这个扇区其他的地方我还存的有数据怎么办呢,这也没办法,要想不丢失数据,你只能先把4096个字节都读出来,再把4096个字节的扇区擦掉,改写完读出来的数据后,再把4096个字节全都写回去。
- 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据会回到页首覆盖写入,这个意思就是说你在写入的时候,一次性不能写太多了,一个写入时序最多只能写一页的数据也就是256字节,为什么有这个限制呢,这是因为在这里有一个页缓冲区,它只有256字节,为什么有缓冲区呢,这是因为flash的写入太慢了,跟不上spi的频率,所以写入的数据会先放在ram里暂存,等时序结束后,芯片再慢慢的把数据写入到flash里,所以这里会有个限制,每个时序最多写入一页的数据,你再写多缓冲区存不下了,如果你非要写,那超过页尾位置的数据会回到页首覆盖写入,另外我们这个页缓存区是和flash的页对应的,你必须得从页起始位置开始写,才能最大写入256字节,如果你从页中间的地址开始写,那写到页尾时,这个地址就会跳回到页首,这会导致地址错乱哈,所以我们在进行多字节写入时,一定要注意这个地址范围不能跨越页的边缘,否则会地址错乱。
- 然后写入操作结束后,芯片进入忙状态,不响应新的读写操作,我们的写入操作都是对缓存区进行的,等时序结束后芯片还要搬砖一段时间,所以每次写入操作后,都有一段时间的忙状态,在这个状态下不要进行新的读写操作,否则芯片是不会响应我们的,要想知道芯片什么时候结束盲状态,我们可以使用读状态寄存器的指令,看一下状态寄存器的busy位是否为1,为0时芯片就不忙了,我们再进行操作,另外注意这个写入操作,包括上面的擦除,在发出擦除指令后,芯片也会进入忙状态,我们也得等忙状态结束后才能进行后续操作。
四、软件SPI读写W25Q64
面包板接线:
MySPI.c
#include "stm32f10x.h" // Device header /*引脚配置层*/ /** * 函 数:SPI写SS引脚电平 * 参 数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1 * 返 回 值:无 * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平 */ void MySPI_W_SS(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平 } /** * 函 数:SPI写SCK引脚电平 * 参 数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1 * 返 回 值:无 * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平 */ void MySPI_W_SCK(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue,设置SCK引脚的电平 } /** * 函 数:SPI写MOSI引脚电平 * 参 数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~0xFF * 返 回 值:无 * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平 */ void MySPI_W_MOSI(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性 } /** * 函 数:I2C读MISO引脚电平 * 参 数:无 * 返 回 值:协议层需要得到的当前MISO的电平,范围0~1 * 注意事项:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1 */ uint8_t MySPI_R_MISO(void) { return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //读取MISO电平并返回 } /** * 函 数:SPI初始化 * 参 数:无 * 返 回 值:无 * 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化 */ void MySPI_Init(void) { /*开启时钟*/ 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引脚初始化为上拉输入 /*设置默认电平*/ MySPI_W_SS(1); //SS默认高电平 MySPI_W_SCK(0); //SCK默认低电平 } /*协议层*/ /** * 函 数:SPI起始 * 参 数:无 * 返 回 值:无 */ void MySPI_Start(void) { MySPI_W_SS(0); //拉低SS,开始时序 } /** * 函 数:SPI终止 * 参 数:无 * 返 回 值:无 */ void MySPI_Stop(void) { MySPI_W_SS(1); //拉高SS,终止时序 } /** * 函 数:SPI交换传输一个字节,使用SPI模式0 * 参 数:ByteSend 要发送的一个字节 * 返 回 值:接收的一个字节 */ uint8_t MySPI_SwapByte(uint8_t ByteSend) { uint8_t i, ByteReceive = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到 for (i = 0; i < 8; i ++) //循环8次,依次交换每一位数据 { MySPI_W_MOSI(ByteSend & (0x80 >> i)); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线 MySPI_W_SCK(1); //拉高SCK,上升沿移出数据 if (MySPI_R_MISO() == 1){ByteReceive |= (0x80 >> i);} //读取MISO数据,并存储到Byte变量 //当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0 MySPI_W_SCK(0); //拉低SCK,下降沿移入数据 } return ByteReceive; //返回接收到的一个字节数据 }
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" /** * 函 数:W25Q64初始化 * 参 数:无 * 返 回 值:无 */ void W25Q64_Init(void) { MySPI_Init(); //先初始化底层的SPI } /** * 函 数:MPU6050读取ID号 * 参 数:MID 工厂ID,使用输出参数的形式返回 * 参 数:DID 设备ID,使用输出参数的形式返回 * 返 回 值:无 */ void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令 *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回 *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位 *DID <<= 8; //高8位移到高位 *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位,通过输出参数返回 MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64写使能 * 参 数:无 * 返 回 值:无 */ void W25Q64_WriteEnable(void) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令 MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64等待忙 * 参 数:无 * 返 回 值:无 */ void W25Q64_WaitBusy(void) { uint32_t Timeout; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令 Timeout = 100000; //给定超时计数时间 while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //循环等待忙标志位 { Timeout --; //等待时,计数值自减 if (Timeout == 0) //自减到0后,等待超时 { /*超时的错误处理代码,可以添加到此处*/ break; //跳出等待,不等了 } } MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64页编程 * 参 数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF * 参 数:DataArray 用于写入数据的数组 * 参 数:Count 要写入数据的数量,范围:0~256 * 返 回 值:无 * 注意事项:写入的地址范围不能跨页 */ void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) { uint16_t i; W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i = 0; i < Count; i ++) //循环Count次 { MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据 } MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 } /** * 函 数:W25Q64扇区擦除(4KB) * 参 数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF * 返 回 值:无 */ void W25Q64_SectorErase(uint32_t Address) { W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 } /** * 函 数:W25Q64读取数据 * 参 数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF * 参 数:DataArray 用于接收读取数据的数组,通过输出参数返回 * 参 数:Count 要读取数据的数量,范围:0~0x800000 * 返 回 值:无 */ void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) { uint32_t i; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i = 0; i < Count; i ++) //循环Count次 { DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据 } MySPI_Stop(); //SPI终止 }
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_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
main.c
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "W25Q64.h" uint8_t MID; //定义用于存放MID号的变量 uint16_t DID; //定义用于存放DID号的变量 uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; //定义要写入数据的测试数组 uint8_t ArrayRead[4]; //定义要读取数据的测试数组 int main(void) { /*模块初始化*/ OLED_Init(); //OLED初始化 W25Q64_Init(); //W25Q64初始化 /*显示静态字符串*/ OLED_ShowString(1, 1, "MID: DID:"); OLED_ShowString(2, 1, "W:"); OLED_ShowString(3, 1, "R:"); /*显示ID号*/ W25Q64_ReadID(&MID, &DID); //获取W25Q64的ID号 OLED_ShowHexNum(1, 5, MID, 2); //显示MID OLED_ShowHexNum(1, 12, DID, 4); //显示DID /*W25Q64功能函数测试*/ W25Q64_SectorErase(0x000000); //扇区擦除 W25Q64_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q64中 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有固定引脚如下:
MySPI.c
#include "stm32f10x.h" // Device header /** * 函 数:SPI写SS引脚电平,SS仍由软件模拟 * 参 数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1 * 返 回 值:无 * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平 */ void MySPI_W_SS(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平 } /** * 函 数:SPI初始化 * 参 数:无 * 返 回 值:无 */ void MySPI_Init(void) { /*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //开启SPI1的时钟 /*GPIO初始化*/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA4引脚初始化为推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将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引脚初始化为上拉输入 /*SPI初始化*/ SPI_InitTypeDef SPI_InitStructure; //定义结构体变量 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //模式,选择为SPI主模式 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //方向,选择2线全双工 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //数据宽度,选择为8位 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //先行位,选择高位先行 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率分频,选择128分频 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //SPI极性,选择低极性 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS,选择由软件控制 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC多项式,暂时用不到,给默认值7 SPI_Init(SPI1, &SPI_InitStructure); //将结构体变量交给SPI_Init,配置SPI1 /*SPI使能*/ SPI_Cmd(SPI1, ENABLE); //使能SPI1,开始运行 /*设置默认电平*/ MySPI_W_SS(1); //SS默认高电平 } /** * 函 数:SPI起始 * 参 数:无 * 返 回 值:无 */ void MySPI_Start(void) { MySPI_W_SS(0); //拉低SS,开始时序 } /** * 函 数:SPI终止 * 参 数:无 * 返 回 值:无 */ void MySPI_Stop(void) { MySPI_W_SS(1); //拉高SS,终止时序 } /** * 函 数:SPI交换传输一个字节,使用SPI模式0 * 参 数:ByteSend 要发送的一个字节 * 返 回 值:接收的一个字节 */ uint8_t MySPI_SwapByte(uint8_t ByteSend) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); //等待发送数据寄存器空 SPI_I2S_SendData(SPI1, ByteSend); //写入数据到发送数据寄存器,开始产生时序 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); //等待接收数据寄存器非空 return SPI_I2S_ReceiveData(SPI1); //读取接收到的数据并返回 }
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" /** * 函 数:W25Q64初始化 * 参 数:无 * 返 回 值:无 */ void W25Q64_Init(void) { MySPI_Init(); //先初始化底层的SPI } /** * 函 数:MPU6050读取ID号 * 参 数:MID 工厂ID,使用输出参数的形式返回 * 参 数:DID 设备ID,使用输出参数的形式返回 * 返 回 值:无 */ void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令 *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回 *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位 *DID <<= 8; //高8位移到高位 *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位,通过输出参数返回 MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64写使能 * 参 数:无 * 返 回 值:无 */ void W25Q64_WriteEnable(void) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令 MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64等待忙 * 参 数:无 * 返 回 值:无 */ void W25Q64_WaitBusy(void) { uint32_t Timeout; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令 Timeout = 100000; //给定超时计数时间 while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //循环等待忙标志位 { Timeout --; //等待时,计数值自减 if (Timeout == 0) //自减到0后,等待超时 { /*超时的错误处理代码,可以添加到此处*/ break; //跳出等待,不等了 } } MySPI_Stop(); //SPI终止 } /** * 函 数:W25Q64页编程 * 参 数:Address 页编程的起始地址,范围:0x000000~0x7FFFFF * 参 数:DataArray 用于写入数据的数组 * 参 数:Count 要写入数据的数量,范围:0~256 * 返 回 值:无 * 注意事项:写入的地址范围不能跨页 */ void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) { uint16_t i; W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i = 0; i < Count; i ++) //循环Count次 { MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据 } MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 } /** * 函 数:W25Q64扇区擦除(4KB) * 参 数:Address 指定扇区的地址,范围:0x000000~0x7FFFFF * 返 回 值:无 */ void W25Q64_SectorErase(uint32_t Address) { W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 } /** * 函 数:W25Q64读取数据 * 参 数:Address 读取数据的起始地址,范围:0x000000~0x7FFFFF * 参 数:DataArray 用于接收读取数据的数组,通过输出参数返回 * 参 数:Count 要读取数据的数量,范围:0~0x800000 * 返 回 值:无 */ void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) { uint32_t i; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i = 0; i < Count; i ++) //循环Count次 { DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据 } MySPI_Stop(); //SPI终止 }
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_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
main.c
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "W25Q64.h" uint8_t MID; //定义用于存放MID号的变量 uint16_t DID; //定义用于存放DID号的变量 uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; //定义要写入数据的测试数组 uint8_t ArrayRead[4]; //定义要读取数据的测试数组 int main(void) { /*模块初始化*/ OLED_Init(); //OLED初始化 W25Q64_Init(); //W25Q64初始化 /*显示静态字符串*/ OLED_ShowString(1, 1, "MID: DID:"); OLED_ShowString(2, 1, "W:"); OLED_ShowString(3, 1, "R:"); /*显示ID号*/ W25Q64_ReadID(&MID, &DID); //获取W25Q64的ID号 OLED_ShowHexNum(1, 5, MID, 2); //显示MID OLED_ShowHexNum(1, 12, DID, 4); //显示DID /*W25Q64功能函数测试*/ W25Q64_SectorErase(0x000000); //扇区擦除 W25Q64_PageProgram(0x000000, ArrayWrite, 4); //将写入数据的测试数组写入到W25Q64中 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) { } }