当前位置:网站首页>STM32入门开发 介绍SPI总线、读写W25Q64(FLASH)(硬件+模拟时序)
STM32入门开发 介绍SPI总线、读写W25Q64(FLASH)(硬件+模拟时序)
2022-08-03 10:35:00 【华为云】
一、环境介绍
编程软件: keil5
操作系统: win10
MCU型号: STM32F103ZET6
STM32编程方式: 寄存器开发 (方便程序移植到其他单片机)
SPI总线: STM32本身支持SPI硬件时序,本文示例代码里同时采用模拟时序和硬件时序两种方式读写W25Q64。
模拟时序更加方便移植到其他单片机,更加方便学习理解SPI时序,通用性更高,不分MCU;
硬件时序效率更高,每个MCU配置方法不同,依赖MCU硬件本身支持。
存储器件: 采用华邦W25Q64 flash存储芯片。
W25Q64这类似的Flash存储芯片在单片机里、嵌入式系统里还是比较常见,可以用来存储图片数据、字库数据、音频数据、保存设备运行日志文件等。
二、华邦W25Q64介绍(FLASH存储类型)
2.1 W25Q64芯片功能介绍

W25Q64是为系统提供一个最小空间、最少引脚,最低功耗的串行Flash存储器,25Q系列比普通的串行Flash存储器更灵活,性能更优越。
W25Q64支持双倍/四倍的SPI,可以储存包括声音、文本、图片和其他数据;芯片支持的工作电压 2.7V 到 3.6V,正常工作时电流小于5mA,掉电时低于1uA,所有芯片提供标准的封装。
W25Q64的内存空间结构: 一页256字节,4K(4096 字节)为一个扇区,16个扇区为1块,容量为8M字节,共有128个块,2048 个扇区。
W25Q64每页大小由256字节组成,每页的256字节用一次页编程指令即可完成。
擦除指令分别支持: 16页(1个扇区)、128页、256页、全片擦除。
W25Q64支持标准串行外围接口(SPI),和高速的双倍/四倍输出,双倍/四倍用的引脚:串行时钟、片选端、串行数据 I/O0(DI)、I/O1(DO)、I/O2(WP)和 I/O3(HOLD)。
SPI 最高支持 80MHz,当用快读双倍/四倍指令时,相当于双倍输出时最高速率160MHz,四倍输出时最高速率 320MHz。这个传输速率比得上8位和16位的并行Flash存储器。
W25Q64支持 JEDEC 标准,具有唯一的 64 位识别序列号,方便区别芯片型号。
2.2 W25Q64芯片特性详细介绍
●SPI串行存储器系列
-W25Q64:64M 位/8M 字节
-W25Q16:16M 位/2M 字节
-W25Q32:32M 位/4M 字节
-每 256 字节可编程页
●灵活的4KB扇区结构
-统一的扇区擦除(4K 字节)
-块擦除(32K 和 64K 字节)
-一次编程 256 字节
-至少 100,000 写/擦除周期
-数据保存 20 年
●标准、双倍和四倍SPI
-标准 SPI:CLK、CS、DI、DO、WP、HOLD
-双倍 SPI:CLK、CS、IO0、IO1、WP、HOLD
-四倍 SPI:CLK、CS、IO0、IO1、IO2、IO3
●高级的安全特点
-软件和硬件写保护
-选择扇区和块保护
-一次性编程保护(1)
-每个设备具有唯一的64位ID(1)
●高性能串行Flash存储器
-比普通串行Flash性能高6倍
-80MHz时钟频率
-双倍SPI相当于160MHz
-四倍SPI相当于320MHz
-40MB/S连续传输数据
-30MB/S随机存取(每32字节)
-比得上16位并行存储器
●低功耗、宽温度范围
-单电源 2.7V-3.6V
-工作电流 4mA,掉电<1μA(典型值)
-40℃~+85℃工作
2.3 引脚介绍
下面只介绍W25Q64标准SPI接口,因为目前开发板上的封装使用的就是标准SPI接口。

| 引脚编号 | 引脚名称 | I/O | 功能 |
|---|---|---|---|
| 1 | /CS | I | 片选端输入 |
| 2 | DO(IO1) | I/O | 数据输出(数据输入输出 1)*1 |
| 3 | /WP(IO2) | I/O | 写保护输入(数据输入输出 2)*2 |
| 4 | GND | 地 | |
| 5 | DI(IO0) | I/O | 数据输入(数据输入输出 0)*1 |
| 6 | CLK | I | 串行时钟输入 |
|---|---|---|---|
| 7 | /HOLD(IO3) | I/O | 保持端输入(数据输入输出 3)*2 |
| 8 | VCC | 电源 |
2.2.1 SPI片选(/CS)引脚用于使能和禁止芯片操作
CS引脚是W25Q64的片选引脚,用于选中芯片;当CS为高电平时,芯片未被选择,串行数据输出(DO、IO0、IO1、IO2 和 IO3)引脚为高阻态。未被选择时,芯片处于待机状态下的低功耗,除非芯片内部在擦除、编程。当/CS 变成低电平,芯片功耗将增长到正常工作,能够从芯片读写数据。上电后, 在接收新的指令前,/CS 必须由高变为低电平。上电后,/CS 必须上升到 VCC,在/CS 接上拉电阻可以完成这个操作。
2.2.2 串行数据输入、输出和 IOs(DI、DO 和 IO0、IO1、IO2、IO3)
W25Q64、W25Q16 和 W25Q32 支持标准 SPI、双倍 SPI 和四倍 SPI。
标准的 SPI 传输用单向的 DI(输入)引脚连续的写命令、地址或者数据在串行时钟(CLK)的上升沿时写入到芯片内。
标准的SPI 用单向的 DO(输出)在 CLK 的下降沿从芯片内读出数据或状态。
2.2.3 写保护(/WP)
写保护引脚(/WP)用来保护状态寄存器。和状态寄存器的块保护位(SEC、TB、BP2、BP1 和BP0)和状态寄存器保护位(SRP)对存储器进行一部分或者全部的硬件保护。/WP 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/WP 引脚(硬件写保护)的功能不可用。
2.2.4 保持端(/HOLD)
当/HOLD 引脚是有效时,允许芯片暂停工作。在/CS 为低电平时,当/HOLD 变为低电平,DO 引脚将变为高阻态,在 DI 和 CLK 引脚上的信号将无效。当/HOLD 变为高电平,芯片恢复工作。/HOLD 功能用在当有多个设备共享同一 SPI 总线时。/HOLD 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/ HOLD 引脚的功能不可用。
2.2.5 串行时钟(CLK)
串行时钟输入引脚为串行输入和输出操作提供时序。(见 SPI 操作)。
设备数据传输是从高位开始,数据传输的格式为 8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线 clk 为高电平。
2.3 内部结构框架图

2.4 W25Q64的标准SPI操作流程
W25Q64标准SPI总线接口包含四个信号: 串行时钟(CLK)、片选端(/CS)、串行数据输入(DI)和串行数据输出(DO)。
DI输入引脚在CLK的上升沿连续写命令、地址或数据到芯片内。
DO输出引脚在CLK的下降沿从芯片内读出数据或状态。
W25Q64分别支持SPI总线工作模式0和工作模式3。模式0和模式3的主要区别在于常态时的CLK信号不同;对于模式0来说,当SPI主机已准备好数据还没传输到串行Flash中时,CLK信号常态为低;
设备数据传输是从高位开始,数据传输的格式为8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线clk为高电平。
2.5 部分控制和状态寄存器介绍
2.5.1 W25Q64的指令表
| 指令名称 | 字节 1 (代码) | 字节 2 | 字节 3 | 字节 4 | 字节 5 | 字节 6 |
|---|---|---|---|---|---|---|
| 写使能 | 06h | write_enabled | ||||
| 禁止写 | 04h | |||||
| 读状态寄存器 1 | 05h | (S7-S0)(2) | ||||
| 读状态寄存器 2 | 35h | (S15-S8)(2) | ||||
| 写状态寄存器 | 01h | (S7-S0) | (S15-S8) | |||
| 页编程 | 02h | A23-A16 | A15-A8 | A7-A0 | (D7-D0) | |
| 四倍页编程 | 32h | A23-A16 | A15-A8 | A7-A0 | (D7-D0,…)(3) | |
| 块擦除(64KB) | D8h | A23-A16 | A15-A8 | A7-A0 | ||
| 块擦除(32KB) | 52h | A23-A16 | A15-A8 | A7-A0 | ||
| 扇区擦除(4KB) | 20h | A23-A16 | A15-A8 | A7-A0 | ||
| 全片擦除 | C7h/60h | |||||
| 暂停擦除 | 75h | |||||
| 恢复擦除 | 7Ah | |||||
| 掉电模式 | B9h | |||||
| 高性能模式 | A3h |
2.5.2 读状态寄存器1
状态寄存器1的内部结构如下:

状态寄存器1的S0位是当前W25Q64的忙状态;为1的时候表示设备正在执行程序(可能是在擦除芯片)或写状态寄存器指令,这个时候设备将忽略传来的指令, 除了读状态寄存器和擦除暂停指令外,其他写指令或写状态指令都无效, 当 S0 为 0 状态时指示设备已经执行完毕,可以进行下一步操作。
读状态寄存器1的时序如下:

读取状态寄存器的指令是 8 位的指令。发送指令之前,先将/CS 拉低,再发送指令码“05 h” 或者“35h”。设备收到读取状态寄存器的指令后,将状态信息(高位)依次移位发送出去,读出的状态信息,最低位为 1 代表忙,最低位为 0 代表可以操作,状态信息读取完毕,将片选线拉高。
读状态寄存器指令可以使用在任何时候,即使程序在擦除的过程中或者写状态寄存器周期正在进行中。这可以检测忙碌状态来确定周期是否完成,以确定设备是否可以接受另一个指令。
2.5.3 读制造商ID和芯片ID
时序图如下:

读取制造商/设备 ID 指令可以读取制造商 ID 和特定的设备 ID。读取之前,拉低 CS 片选信号,接着发送指令代码“90h” ,紧随其后的是一个 24 位地址(A23-A0)000000h。 设备收到指令之后,会发出华邦电子制造商 ID(EFh) 和设备ID(w25q64 为 16h)。如果 24 位地址设置为 000001h ,设备 ID 会先发出,然后跟着制造商 ID。制造商和设备ID可以连续读取。完成指令后,片选信号/ CS 拉高。
2.5.4 全片擦除(C7h/60h)

全芯片擦除指令,可以将整个芯片的所有内存数据擦除,恢复到 0XFF 状态。写入全芯片擦除指令之前必须执行设备写使能(发送设备写使能指令 0x06),并判断状态寄存器(状态寄存器位最低位必须等于 0 才能操作)。发送全芯片擦除指令前,先拉低/ CS,接着发送擦除指令码”C7h”或者是”60h”, 指令码发送完毕后,拉高片选线 CS/,,并判断状态位,等待擦除结束。全片擦除指令尽量少用,擦除会缩短设备的寿命。
2.5.5 读数据(03h)

读取数据指令允许按顺序读取一个字节的内存数据。当片选 CS/拉低之后,紧随其后是一个 24 位的地址(A23-A0)(需要发送 3 次,每次 8 个字节,先发高位)。芯片收到地址后,将要读的数据按字节大小转移出去,数据是先转移高位,对于单片机,时钟下降沿发送数据,上升沿接收数据。读数据时,地址会自动增加,允许连续的读取数据。这意味着读取整个内存的数据,只要用一个指令就可以读完。数据读取完成之后,片选信号/ CS 拉高。
读取数据的指令序列,如上图所示。如果一个读数据指令而发出的时候,设备正在擦除扇区,或者(忙= 1),该读指令将被忽略,也不会对当前周期有什么影响。
三、SPI时序介绍
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间。
SPI是一种高速、高效率的串行接口技术,一共有4根线。通常由一个主模块和一个或多个从模块组成,主模块选择一个从模块进行同步通信,从而完成数据的交换。SPI是一个环形结构,通信时需要至少4根线(在单向传输时3根线也可以)。分别是MISO(主设备数据输入)、MOSI(主设备数据输出)、SCLK(时钟)、CS(片选)。
(1)MISO– Master Input Slave Output,主设备数据输入,从设备数据输出;
(2)MOSI– Master Output Slave Input,主设备数据输出,从设备数据输入;
(3)SCLK – Serial Clock,时钟信号,由主设备产生;
(4)CS – Chip Select,从设备使能信号,由主设备控制。
其中,CS是从芯片是否被主芯片选中的控制信号,也就是说只有片选信号为预先规定的使能信号时(高电位或低电位),主芯片对此从芯片的操作才有效。这就使在同一条总线上连接多个SPI设备成为可能。接下来就负责通讯的3根线了。通讯是通过数据交换完成的,这里先要知道SPI是串行通讯协议,也就是说数据是一位一位的传输的。这就是SCLK时钟线存在的原因,由SCLK提供时钟脉冲,SDI,SDO则基于此脉冲完成数据传输。数据输出通过 SDO线,数据在时钟上升沿或下降沿时改变,在紧接着的下降沿或上升沿被读取。完成一位数据传输,输入也使用同样原理。因此,至少需要8次时钟信号的改变(上沿和下沿为一次),才能完成8位数据的传输。
时钟信号线SCLK只能由主设备控制,从设备不能控制。这样的传输方式有一个优点,在数据位的传输过程中可以暂停,也就是时钟的周期可以为不等宽,因为时钟线由主设备控制,当没有时钟跳变时,从设备不采集或传送数据。SPI还是一个数据交换协议:因为SPI的数据输入和输出线独立,所以允许同时完成数据的输入和输出。芯片集成的SPI串行同步时钟极性和相位可以通过寄存器配置,IO模拟的SPI串行同步时钟需要根据从设备支持的时钟极性和相位来通讯。SPI通信原理比I2C要简单,IIC有应答机制,可以确保数据都全部发送成。SPI接口没有指定的流控制,没有应答机制确认是否接收到数据,速度上更加快。
SPI总线通过时钟极性和相位可以配置成4种时序:
STM32F103参考手册,SPI章节介绍的时序图:

SPI时序比较简单,CPU如果没有硬件支持,可以直接写代码采用IO口模拟,下面是模拟时序的示例的代码:
SPI的模式1:u8 SPI_ReadWriteOneByte(u8 tx_data){ u8 i,rx_data=0; SCK=0; //空闲电平(默认初始化情况) for(i=0;i<8;i++) { /*1. 主机发送一位数据*/ SCK=0;//告诉从机,主机将要发送数据 if(tx_data&0x80)MOSI=1; //发送数据 else MOSI=0; SCK=1; //告诉从机,主机数据发送完毕 tx_data<<=1; //继续发送下一位 /*2. 主机接收一位数据*/ rx_data<<=1; //默认认为接收到0 if(MISO)rx_data|=0x01; } SCK=0; //恢复空闲电平 return rx_data;}SPI的模式2:u8 SPI_ReadWriteOneByte(u8 tx_data){ u8 i,rx_data=0; SCK=0; //空闲电平(默认初始化情况) for(i=0;i<8;i++) { /*1. 主机发送一位数据*/ SCK=1;//告诉从机,主机将要发送数据 if(tx_data&0x80)MOSI=1; //发送数据 else MOSI=0; SCK=0; //告诉从机,主机数据发送完毕 tx_data<<=1; //继续发送下一位 /*2. 主机接收一位数据*/ rx_data<<=1; //默认认为接收到0 if(MISO)rx_data|=0x01; } SCK=0; //恢复空闲电平 return rx_data;}SPI的模式3:u8 SPI_ReadWriteOneByte(u8 tx_data){ u8 i,rx_data=0; SCK=1; //空闲电平(默认初始化情况) for(i=0;i<8;i++) { /*1. 主机发送一位数据*/ SCK=1;//告诉从机,主机将要发送数据 if(tx_data&0x80)MOSI=1; //发送数据 else MOSI=0; SCK=0; //告诉从机,主机数据发送完毕 tx_data<<=1; //继续发送下一位 /*2. 主机接收一位数据*/ rx_data<<=1; //默认认为接收到0 if(MISO)rx_data|=0x01; } SCK=1; //恢复空闲电平 return rx_data;}SPI的模式4:u8 SPI_ReadWriteOneByte(u8 tx_data){ u8 i,rx_data=0; SCK=1; //空闲电平(默认初始化情况) for(i=0;i<8;i++) { /*1. 主机发送一位数据*/ SCK=0;//告诉从机,主机将要发送数据 if(tx_data&0x80)MOSI=1; //发送数据 else MOSI=0; SCK=1; //告诉从机,主机数据发送完毕 tx_data<<=1; //继续发送下一位 /*2. 主机接收一位数据*/ rx_data<<=1; //默认认为接收到0 if(MISO)rx_data|=0x01; } SCK=1; //恢复空闲电平 return rx_data;}四、W25Q64的示例代码
4.1 STM32采用硬件SPI读写W25Q64示例代码
/*函数功能:SPI初始化(模拟SPI)硬件连接:MISO--->PB14MOSI--->PB15SCLK--->PB13*/void SPI_Init(void){ /*开启时钟*/ RCC->APB1ENR|=1<<14; //开启SPI2时钟 RCC->APB2ENR|=1<<3; //PB GPIOB->CRH&=0X000FFFFF; //清除寄存器 GPIOB->CRH|=0XB8B00000; GPIOB->ODR|=0X7<<13; //PB13/14/15上拉--输出高电平 /*SPI2基本配置*/ SPI2->CR1=0X0; //清空寄存器 SPI2->CR1|=0<<15; //选择“双线双向”模式 SPI2->CR1|=0<<11; //使用8位数据帧格式进行发送/接收; SPI2->CR1|=0<<10; //全双工(发送和接收); SPI2->CR1|=1<<9; //启用软件从设备管理 SPI2->CR1|=1<<8; //NSS SPI2->CR1|=0<<7; //帧格式,先发送高位 SPI2->CR1|=0x0<<3;//当总线频率为36MHZ时,SPI速度为18MHZ,高速。 SPI2->CR1|=1<<2; //配置为主设备 SPI2->CR1|=1<<1; //空闲状态时, SCK保持高电平。 SPI2->CR1|=1<<0; //数据采样从第二个时钟边沿开始。 SPI2->CR1|=1<<6; //开启SPI设备。}/*函数功能:SPI读写一个字节*/u8 SPI_ReadWriteOneByte(u8 data_tx){ u16 cnt=0; while((SPI2->SR&1<<1)==0) //等待发送区空--等待发送缓冲为空 { cnt++; if(cnt>=65530)return 0; //超时退出 u16=2个字节 } SPI2->DR=data_tx; //发送一个byte cnt=0; while((SPI2->SR&1<<0)==0) //等待接收完一个byte { cnt++; if(cnt>=65530)return 0; //超时退出 } return SPI2->DR; //返回收到的数据 }/*函数功能:W25Q64初始化硬件连接:MOSI--->PB15MISO--->PB14SCLK--->PB13CS----->PB12*/void W25Q64_Init(void){ /*1. 开时钟*/ RCC->APB2ENR|=1<<3; //PB /*2. 配置GPIO口模式*/ GPIOB->CRH&=0xFFF0FFFF; GPIOB->CRH|=0x00030000; W25Q64_CS=1; //未选中芯片 SPI_Init(); //SPI初始化}/*函数功能:读取芯片的ID号*/u16 W25Q64_ReadID(void){ u16 id; /*1. 拉低片选*/ W25Q64_CS=0; /*2. 发送读取ID的指令*/ SPI_ReadWriteOneByte(0x90); /*3. 发送24位的地址-0*/ SPI_ReadWriteOneByte(0); SPI_ReadWriteOneByte(0); SPI_ReadWriteOneByte(0); /*4. 读取芯片的ID*/ id=SPI_ReadWriteOneByte(0xFF)<<8; id|=SPI_ReadWriteOneByte(0xFF); /*5. 拉高片选*/ W25Q64_CS=1; return id;}/*函数功能:检测W25Q64状态*/void W25Q64_CheckStat(void){ u8 stat=1; while(stat&1<<0) { W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x05); //发送读状态寄存器1指令 stat=SPI_ReadWriteOneByte(0xFF); //读取状态 W25Q64_CS=1; //取消选中芯片 }}/*函数功能:页编程说 明:一页最多写256个字节。 写数据之前,必须保证空间是0xFF函数参数:u32 addr:页编程起始地址u8 *buff:写入的数据缓冲区u16 len :写入的字节长度*/void W25Q64_PageWrite(u32 addr,u8 *buff,u16 len){ u16 i; W25Q64_Enabled(); //写使能 W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x02); //页编程指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 for(i=0;i<len;i++) { SPI_ReadWriteOneByte(buff[i]); //8~0地址 } W25Q64_CS=1; //取消选中芯片 W25Q64_CheckStat(); //检测芯片忙状态}/*函数功能:连续读数据函数参数:u32 addr:读取数据的起始地址u8 *buff:读取数据存放的缓冲区u32 len :读取字节的长度*/void W25Q64_ReadByteData(u32 addr,u8 *buff,u32 len){ u32 i; W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x03); //读数据指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 for(i=0;i<len;i++)buff[i]=SPI_ReadWriteOneByte(0xFF); W25Q64_CS=1; //取消选中芯片}/*函数功能:擦除一个扇区函数参数:u32 addr:擦除扇区的地址范围*/void W25Q64_ClearSector(u32 addr){ W25Q64_Enabled(); //写使能 W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x20); //扇区擦除指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 W25Q64_CS=1; //取消选中芯片 W25Q64_CheckStat(); //检测芯片忙状态}/*函数功能:写使能*/void W25Q64_Enabled(void){ W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x06); //写使能 W25Q64_CS=1; //取消选中芯片}/*函数功能:指定位置写入指定个数的数据,不考虑擦除问题注意事项:W25Q64只能将1写为,不能将0写为1。函数参数:u32 addr---写入数据的起始地址u8 *buff---写入的数据u32 len---长度*/void W25Q64_WriteByteDataNoCheck(u32 addr,u8 *buff,u32 len){ u32 page_remain=256-addr%256; //计算当前页还可以写下多少数据 if(len<=page_remain) //如果当前写入的字节长度小于剩余的长度 { page_remain=len; } while(1) { W25Q64_PageWrite(addr,buff,page_remain); if(page_remain==len)break; //表明数据已经写入完毕 buff+=page_remain; //buff向后偏移地址 addr+=page_remain; //起始地址向后偏移 len-=page_remain; //减去已经写入的字节数 if(len>256)page_remain=256; //如果大于一页,每次就直接写256字节 else page_remain=len; }}/*函数功能:指定位置写入指定个数的数据,考虑擦除问题,完善代码函数参数:u32 addr---写入数据的起始地址u8 *buff---写入的数据u32 len---长度说明:擦除的最小单位扇区,4096字节*/static u8 W25Q64_READ_WRITE_CHECK_BUFF[4096];void W25Q64_WriteByteData(u32 addr,u8 *buff,u32 len){ u32 i; u32 len_w; u32 sector_addr; //存放扇区的地址 u32 sector_move; //扇区向后偏移的地址 u32 sector_size; //扇区大小。(剩余的空间大小) u8 *p=W25Q64_READ_WRITE_CHECK_BUFF;//存放指针 sector_addr=addr/4096; //传入的地址是处于第几个扇区 sector_move=addr%4096; //计算传入的地址存于当前的扇区的偏移量位置 sector_size=4096-sector_move; //得到当前扇区剩余的空间 if(len<=sector_size) { sector_size=len; //判断第一种可能性、一次可以写完 } while(1) { W25Q64_ReadByteData(addr,p,sector_size); //读取剩余扇区里的数据 for(i=0;i<sector_size;i++) { if(p[i]!=0xFF)break; } if(i!=sector_size) //判断是否需要擦除 { W25Q64_ClearSector(sector_addr*4096); }// for(i=0;i<len;i++)// {// W25Q64_READ_WRITE_CHECK_BUFF[i]=buff[len_w++]; //// }// W25Q64_WriteByteDataNoCheck(addr,W25Q64_READ_WRITE_CHECK_BUFF,sector_size); W25Q64_WriteByteDataNoCheck(addr,buff,sector_size); if(sector_size==len)break; addr+=sector_size; //向后偏移地址 buff+=sector_size ;//向后偏移 len-=sector_size; //减去已经写入的数据 sector_addr++; //校验第下个扇区 if(len>4096) //表明还可以写一个扇区 { sector_size=4096;//继续写一个扇区 } else { sector_size=len; //剩余的空间可以写完 } }}4.2 STM32采用硬件SPI读写W25Q64示例代码
#include "spi.h"/*函数功能:SPI初始化(模拟SPI)硬件连接:MISO--->PB14MOSI--->PB15SCLK--->PB13*/void SPI_Init(void){ /*1. 开时钟*/ RCC->APB2ENR|=1<<3; //PB /*2. 配置GPIO口模式*/ GPIOB->CRH&=0x000FFFFF; GPIOB->CRH|=0x38300000; /*3. 上拉*/ SPI_MOSI=1; SPI_MISO=1; SPI_SCLK=1;}/*函数功能:SPI读写一个字节*/u8 SPI_ReadWriteOneByte(u8 data_tx){ u8 data_rx=0; //存放读取的数据 u8 i; for(i=0;i<8;i++) { SPI_SCLK=0; //准备发送数据 if(data_tx&0x80)SPI_MOSI=1; else SPI_MOSI=0; data_tx<<=1; //依次发送最高位 SPI_SCLK=1; //表示主机数据发送完成,表示从机发送完毕 data_rx<<=1; //表示默认接收的是0 if(SPI_MISO)data_rx|=0x01; } return data_rx;}#include "W25Q64.h"/*函数功能:W25Q64初始化硬件连接:MOSI--->PB15MISO--->PB14SCLK--->PB13CS----->PB12*/void W25Q64_Init(void){ /*1. 开时钟*/ RCC->APB2ENR|=1<<3; //PB /*2. 配置GPIO口模式*/ GPIOB->CRH&=0xFFF0FFFF; GPIOB->CRH|=0x00030000; W25Q64_CS=1; //未选中芯片 SPI_Init(); //SPI初始化}/*函数功能:读取芯片的ID号*/u16 W25Q64_ReadID(void){ u16 id; /*1. 拉低片选*/ W25Q64_CS=0; /*2. 发送读取ID的指令*/ SPI_ReadWriteOneByte(0x90); /*3. 发送24位的地址-0*/ SPI_ReadWriteOneByte(0); SPI_ReadWriteOneByte(0); SPI_ReadWriteOneByte(0); /*4. 读取芯片的ID*/ id=SPI_ReadWriteOneByte(0xFF)<<8; id|=SPI_ReadWriteOneByte(0xFF); /*5. 拉高片选*/ W25Q64_CS=1; return id;}/*函数功能:检测W25Q64状态*/void W25Q64_CheckStat(void){ u8 stat=1; while(stat&1<<0) { W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x05); //发送读状态寄存器1指令 stat=SPI_ReadWriteOneByte(0xFF); //读取状态 W25Q64_CS=1; //取消选中芯片 }}/*函数功能:页编程说 明:一页最多写256个字节。 写数据之前,必须保证空间是0xFF函数参数:u32 addr:页编程起始地址u8 *buff:写入的数据缓冲区u16 len :写入的字节长度*/void W25Q64_PageWrite(u32 addr,u8 *buff,u16 len){ u16 i; W25Q64_Enabled(); //写使能 W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x02); //页编程指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 for(i=0;i<len;i++) { SPI_ReadWriteOneByte(buff[i]); //8~0地址 } W25Q64_CS=1; //取消选中芯片 W25Q64_CheckStat(); //检测芯片忙状态}/*函数功能:连续读数据函数参数:u32 addr:读取数据的起始地址u8 *buff:读取数据存放的缓冲区u32 len :读取字节的长度*/void W25Q64_ReadByteData(u32 addr,u8 *buff,u32 len){ u32 i; W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x03); //读数据指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 for(i=0;i<len;i++)buff[i]=SPI_ReadWriteOneByte(0xFF); W25Q64_CS=1; //取消选中芯片}/*函数功能:擦除一个扇区函数参数: u32 addr:擦除扇区的地址范围*/void W25Q64_ClearSector(u32 addr){ W25Q64_Enabled(); //写使能 W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x20); //扇区擦除指令 SPI_ReadWriteOneByte(addr>>16); //24~16地址 SPI_ReadWriteOneByte(addr>>8); //16~8地址 SPI_ReadWriteOneByte(addr); //8~0地址 W25Q64_CS=1; //取消选中芯片 W25Q64_CheckStat(); //检测芯片忙状态}/*函数功能:写使能*/void W25Q64_Enabled(void){ W25Q64_CS=0; //选中芯片 SPI_ReadWriteOneByte(0x06); //写使能 W25Q64_CS=1; //取消选中芯片}/*函数功能:指定位置写入指定个数的数据,不考虑擦除问题注意事项:W25Q64只能将1写为,不能将0写为1。函数参数:u32 addr---写入数据的起始地址u8 *buff---写入的数据u32 len---长度*/void W25Q64_WriteByteDataNoCheck(u32 addr,u8 *buff,u32 len){ u32 page_remain=256-addr%256; //计算当前页还可以写下多少数据 if(len<=page_remain) //如果当前写入的字节长度小于剩余的长度 { page_remain=len; } while(1) { W25Q64_PageWrite(addr,buff,page_remain); if(page_remain==len)break; //表明数据已经写入完毕 buff+=page_remain; //buff向后偏移地址 addr+=page_remain; //起始地址向后偏移 len-=page_remain; //减去已经写入的字节数 if(len>256)page_remain=256; //如果大于一页,每次就直接写256字节 else page_remain=len; }}/*函数功能:指定位置写入指定个数的数据,考虑擦除问题,完善代码函数参数:u32 addr---写入数据的起始地址u8 *buff---写入的数据u32 len---长度说明:擦除的最小单位扇区,4096字节*/static u8 W25Q64_READ_WRITE_CHECK_BUFF[4096];void W25Q64_WriteByteData(u32 addr,u8 *buff,u32 len){ u32 i; u32 sector_addr; //存放扇区的地址 u32 sector_move; //扇区向后偏移的地址 u32 sector_size; //扇区大小。(剩余的空间大小) u8 *p=W25Q64_READ_WRITE_CHECK_BUFF;//存放指针 sector_addr=addr/4096; //传入的地址是处于第几个扇区 sector_move=addr%4096; //计算传入的地址存于当前的扇区的偏移量位置 sector_size=4096-sector_move; //得到当前扇区剩余的空间 if(len<=sector_size) { sector_size=len; //判断第一种可能性、一次可以写完 } while(1) { W25Q64_ReadByteData(addr,p,sector_size); //读取剩余扇区里的数据 for(i=0;i<sector_size;i++) { if(p[i]!=0xFF)break; } if(i!=sector_size) //判断是否需要擦除 { W25Q64_ClearSector(sector_addr*4096); } W25Q64_WriteByteDataNoCheck(addr,buff,sector_size); if(sector_size==len)break; addr+=sector_size; //向后偏移地址 buff+=sector_size ;//向后偏移 len-=sector_size; //减去已经写入的数据 sector_addr++; //校验第下个扇区 if(len>4096) //表明还可以写一个扇区 { sector_size=4096;//继续写一个扇区 } else { sector_size=len; //剩余的空间可以写完 } }}边栏推荐
猜你喜欢

Mysql OCP 29题

SQL教程之递归 CTE Common Table Expression

孙宇晨式“溢价逻辑”:不局限眼前,为全人类的“星辰大海”大胆下注

4 g acquisition ModbusTCP turn JSON MQTT cloud platform

2022T电梯修理考试题及答案

Promise 1: Basic Questions

This article understands the process from RS485 sensor to IoT gateway to cloud platform

Mysql OCP 27题

2022年山东省安全员C证复习题模拟考试平台操作

numpy
随机推荐
Interview Blitz 71: What's the difference between GET and POST?
成对连接点云分割
媒体查询代码
分布式事务七种解决方案
如何将Oracle/MySQL中的数据迁移到GBase 8c中?
如何改变sys_guid() 返回值类型
自定义实现乘风破浪的小船
关于OPENSSL的问题
【学习笔记之菜Dog学C】通讯录
What is the IDE?Novice with which the IDE is better?
通过GBase 8c Platform安装数据库集群时报错
rpm文件解包提取 cpio
罕见的数学天才,靠“假结婚”才得到追求事业的机会
从餐桌到太空,孙宇晨的“星辰大海”
oracle计算同、环比
Matplotlib
Mysql OCP 75 questions
select statement in go
安全研究员:大量Solana钱包被盗
DOM0、DOM2、DOM3 事件