嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)

Flash存储器又称闪存,它结合了ROM和RAM的长处,不仅具备电子可擦除可编程(EEPROM)的性能,还可以快速读取数据(NVRAM的优势),使数据不会因为断电而丢失。

文章目录

  • Flash
    • Flash简介
    • Flash发展历史
    • Flash用途与分类
  • W25Q64
    • W25Q64简介
    • W25Q64管脚定义及说明
  • SPI协议(针对W25Q64)
    • SPI协议概念
    • SPI协议概括
    • SPI协议4种传输模式
    • SPI协议读写数据
  • 对W25Q64进行读写操作
    • 指令表
    • 状态寄存器
    • 读状态寄存器
    • 写状态寄存器
    • 读数据
    • 页写
    • 扇区擦除
    • 全片擦除
    • 读取制造商/芯片 ID
  • 51单片机控制W25Q64(AT25F1024)
    • SPI协议代码
    • Proteus小实验

Flash

Flash简介

FLASH闪存是属于内存器件的一种,“Flash”。闪存则是一种非易失性( Non-Volatile )内存,在没有电流供应的条件下也能够长久地保持数据,其存储特性相当于硬盘,这项特性正是闪存得以成为各类便携型数字设备的存储介质的基础。

闪存是一种特殊的、以宏块抹写的EPROM。早期的闪存进行一次抹除,就会清除掉整颗芯片上的数据。

各类 DDR 、 SDRAM 或者 RDRAM 都属于挥发性内存,只要停止电流供应内存中的数据便无法保持,因此每次电脑开机都需要把数据重新载入内存。而闪存是一种非易失性存储器,即断电数据也不会丢失。因为闪存不像RAM(随机存取存储器)一样以字节为单位改写数据,因此不能取代RAM。

——百度百科

Flash发展历史

在1984年,东芝公司的发明人舛冈富士雄首先提出了快速闪存存储器(此处简称闪存)的概念。与传统电脑内存不同,闪存的特点是非易失性(也就是所存储的数据在主机掉电后不会丢失),其记录速度也非常快。

Intel是世界上第一个生产闪存并将其投放市场的公司。1988年,公司推出了一款256K bit闪存芯片。它如同鞋盒一样大小,并被内嵌于一个录音机里。後来,Intel发明的这类闪存被统称为NOR闪存。它结合EPROM(可擦除可编程只读存储器)和EEPROM(电可擦除可编程只读存储器)两项技术,并拥有一个SRAM接口。

第二种闪存称为NAND闪存。它由日立公司于1989年研制,并被认为是NOR闪存的理想替代者。NAND闪存的写周期比NOR闪存短90%,它的保存与删除处理的速度也相对较快。NAND的存储单元只有NOR的一半,在更小的存储空间中NAND获得了更好的性能。鉴于NAND出色的表现,它常常被应用于诸如CompactFlash、SmartMedia、 SD、 MMC、 xD、 and PC cards、USB sticks等存储卡上。

——百度百科

Flash用途与分类

1、IIC EEPROM------容量小,采用的是IIC通信协议;用于在掉电时,存系统配置参数,比如屏幕亮度等。常用芯片型号有 AT24C02、FM24C02、CAT24C02等,其常见的封装多为DIP8,SOP8,TSSOP8等;

2、SPI NorFlash------容量略大,采用的是SPI 通信协议;用于存放程序和数据。程序和数据可存放在同一芯片上,拥有独立的数据总线和地址总线,能快速随机读取,允许系统直接从Flash中读取代码执行;可以单字节或单字编程,但不能单字节擦除,必须以Sector为单位或对整片执行擦除操作。常见到的S25FL128、MX25L1605、W25Q64等型号都是SPI NorFlash。

3、SPI NandFlash------采用了SPI NorFlash一样的SPI的通信协议,用于存储数据;在读写的速度上没什么区别,但在存储结构上却采用了与Parallel NandFlash相同的结构,所以SPI nand相对于SPI norFlash具有擦写的次数多,擦写速度快的优势。

4、eMMC Flash------eMMC采用统一的MMC标准接口,eMMC相当于NandFlash+主控IC;自身集成MMC Controller,存储单元与NandFlash相同。常见到的KLMAG8DEDD、THGBMAG8B4JBAIM、EMMC04G-S100等型号都是eMMC Flash。

5、SD卡------它在MMC的基础上发展而来,有两个可选的通信协议:SD模式和SPI模式。

——https://www.cnblogs.com/leo0621/p/8204852.html

NOR和NAND是市场上两种主要的非易失闪存技术,当选择存储解决方案时,设计师必须权衡以下的各项因素:

  • NOR的读速度比NAND稍快一些。
  • NAND的写入速度比NOR快很多。
  • NAND的4ms擦除速度远比NOR的5ms快。
  • 大多数写入操作需要先进行擦除操作。
  • NAND的擦除单元更小,相应的擦除电路更少。

W25Q64

W25Q64简介

本文将以W25Q64为例,介绍Flash的基本知识和使用方法。

W25Q64是华邦公司推出的大容量SPI FLASH产品,其容量为64Mb(8M Bytes)。该25Q系列的器件在灵活性和性能方面远远超过普通的串行闪存器件。W25Q64将8M字节的容量分为128个块,每个块大小为64K字节,每个块又分为16个扇区,每个扇区4K个字节。W25Q64的最小擦除单位为一个扇区,也就是每次必须擦除4K个字节。所以,这需要给W25Q64开辟一个至少4K的缓存区,这样必须要求芯片有4K以上的SRAM才能有很好的操作。
W25Q64的擦写周期多达10W次,可将数据保存达20年之久,支持2.7~3.6V的电压,支持标准的SPI,还支持双输出/四输出的SPI,最大SPI时钟可达80Mhz。

——https://www.cnblogs.com/gongchuangsu/p/4850223.html

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第1张图片

W25Q64管脚定义及说明

管脚定义
嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第2张图片

管脚功能对照表

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第3张图片

功能说明
注:本文不研究双倍和四倍SPI

片选端(/CS)
SPI 片选(/CS)引脚使能和禁止芯片操作。当CS̅̅̅为高电平时,芯片未被选择,串行数据输出(DO、IO0、IO1、IO2 和 IO3)引脚为高阻态。未被选择时,芯片处于待机状态下的低功耗,除非芯片内部在擦除、编程。当/CS 变成低电平,芯片功耗将增长到正常工作,能够从芯片读写数据。上电后,在接收新的指令前,/CS 必须由高变为低电平。上电后,/CS 必须上升到 VCC,在/CS 接上拉电阻可以完成这个。

串行数据输入、输出和 IOs(DI、DO 和 IO0、IO1、IO2、IO3)
W25Q64、W25Q16 和 W25Q32 支持标准 SPI、双倍 SPI 和四倍 SPI。标准的 SPI 传输用单向的 DI(输入)引脚连续的写命令、地址或者数据在串行时钟(CLK)的上升沿时写入到芯片内。标准的SPI 用单向的 DO(输出)在 CLK 的下降沿从芯片内读出数据或状态。
双倍和四倍 SPI 指令用双向的 IO 引脚在 CLK 的上升沿来连续的写指令、地址或者数据到芯片内,在 CLK 的下降沿从芯片内读出数据或者状态。四倍 SPI 指令操作时要求在状态寄存器 2 中的四倍使能位(QE)一直是置位状态。当 QE=1 时/WP 引脚变为 IO2,/HOLD 引脚变为 IO3。

写保护(/WP)
写保护引脚(/WP)用来保护状态寄存器。和状态寄存器的块保护位(SEC、TB、BP2、BP1 和BP0)和状态寄存器保护位(SRP)对存储器进行一部分或者全部的硬件保护。/WP 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/WP 引脚(硬件写保护)的功能不可用,被用作了 IO2。

保持端(/HOLD)
当/HOLD 引脚是有效时,允许芯片暂停工作。在/CS 为低电平时,当/HOLD 变为低电平,DO 引脚将变为高阻态,在 DI 和 CLK 引脚上的信号将无效。当/HOLD 变为高电平,芯片恢复工作。/HOLD功能用在当有多个设备共享同一 SPI 总线。/HOLD 引脚低电平有效。当状态寄存器 2 的 QE 位被置位了,/ HOLD 引脚的功能不可用,被用作了 IO3。

串行时钟(CLK)
串行时钟输入引脚为串行输入和输出操作提供时序。(见 SPI 操作),设备数据传输是从高位开 始,数据传输的格式为 8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线 clk 为高电平。

——W25Q64中文手册

SPI协议(针对W25Q64)

SPI协议概念

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议。

SPI协议概括

SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多个从设备,需要至少4根线,事实上3根也可以(单向传输时)。也是所有基于SPI的设备共有的,它们是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的数据输入和输出线独立,所以允许同时完成数据的输入和输出。芯片集成的SPI串行同步时钟极性和相位可以通过寄存器配置,IO模拟的SPI串行同步时钟需要根据从设备支持的时钟极性和相位来通讯。

最后,SPI接口的一个缺点:没有指定的流控制,没有应答机制确认是否接收到数据。

——百度百科

SPI协议4种传输模式

SPI总线传输一共有4种模式,这4种模式分别由时钟极性(CPOL,Clock Polarity)和时钟相位(CPHA,Clock Phase)来定义,其中CPOL参数规定了SCK时钟信号空闲状态的电平,CPHA规定了数据是在SCK时钟的上升沿被采样还是下降沿被采样。

图片来源网络

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第4张图片

  • 模式0CPOL= 0,CPHA=0 SCK串行时钟线空闲是为低电平,数据在SCK时钟的前边沿(上升沿)被采样,在SCK时钟的后边沿(下降沿)切换电平状态;
  • 模式1CPOL= 0,CPHA=1 SCK串行时钟线空闲是为低电平,数据在SCK时钟的后边沿(下降沿)被采样,在SCK时钟的前边沿(上升沿)切换电平状态;
  • 模式2CPOL= 1,CPHA=0 SCK串行时钟线空闲是为高电平,数据在SCK时钟的前边沿(下降沿)被采样,在SCK时钟的后边沿(上升沿)切换电平状态;
  • 模式3CPOL= 1,CPHA=1 SCK串行时钟线空闲是为高电平,数据在SCK时钟的后边沿(上升沿)被采样,在SCK时钟的前边沿(下降沿)切换电平状态。

SPI协议读写数据

图片来源:野火电子PPT

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第5张图片
根据前面SPI模式的内容,可以看出上图为模式1(CPOL=0, CPHA=1)

标号 1 处,NSS(CS)信号线由高变低,是SPI通讯的起始信号。NSS是每个从机各自独占的信号线,当从机检在自己的NSS线检测到起始信号后,就知道自己被主机选中了,开始准备与主机通讯。

在图中的标号 6 处,NSS信号由低变高,是SPI通讯的停止信号,表示本次通讯结束,从机的选中状态被取消。

SPI使用MOSI及MISO信号线来传输数据,使用SCK信号线进行数据同步。MOSI及MISO数据线在SCK的每个时钟周期传输一位数据,且数据输入输出是同时进行的。

——野火电子PPT

对W25Q64进行读写操作

支持 SPI 总线的工作模式 0(0,0)和 3(1,1)。模式 0 和模式 3 的主要区别在于常态时的 CLK信号,当 SPI 主机已准备好数据还没传输到串行 Flash 中,对于模式 0 CLK 信号常态为低。

设备数据传输是从高位开始,数据传输的格式为 8bit,数据采样从第二个时间边沿开始,空闲状态时,时钟线 CLK 为高电平。

——W25Q64中文手册

指令表

指令表1:

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第6张图片

状态寄存器

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第7张图片

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

——W25Q64中文手册

读状态寄存器

读状态寄存器 1(05h)和 状态寄存器 2(35h)
嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第8张图片

读取状态寄存器的指令是 8 位的指令。发送指令之前,先将/CS 拉低,再发送指令码“05h”或者“35h”。设备收到读取状态寄存器的指令后,将状态信息(高位)依次移位发送出去,如上图所示。

读出的状态信息,最低位为 1 代表忙,最低位为 0 代表可以操作,状态信息读取完毕,将片选线拉高。

读状态寄存器指令可以使用在任何时候,即使程序在擦除的过程中或者写状态寄存器周期正在进行中。这可以检测忙碌状态来确定周期是否完成,以确定设备是否可以接受另一个指令。

——W25Q64中文手册

写状态寄存器

读数据

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第9张图片

读取数据指令允许按顺序读取一个字节的内存数据。当片选 CS/拉低之后,紧随其后是一个 24 位的地址(A23-A0)(需要发送 3 次,每次 8 个字节,先发高位)。芯片收到地址后,将要读的数据按字节大小转移出去,数据是先转移高位,对于单片机,时钟下降沿发送数据,上升沿接收数据。读数据时,地址会自动增加,允许连续的读取数据。这意味着读取整个内存的数据,只要用一个指令就可以读完。数据读取完成之后,片选信号/ CS 拉高。

读取数据的指令序列,如上图所示。如果一个读数据指令而发出的时候,设备正在擦除扇区,或者(BUSY = 1),该读指令将被忽略,也不会对当前周期有什么影响。

——W25Q64中文手册

页写

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第10张图片

页编程指令允许从一个字节到 256 字节的数据编程(一页)(编程之前必须保证内存空间是 0XFF)。允许写入指令之前,必须先发送设备写使能指令。写使能开启后,设备才能接收编程指令。开启页编程先拉底/ CS,然后发送指令代码“02h”,接着发送一个 24 位地址(A23-A0)(发送 3 次,每次 8 位) 和至少一个数据字节(数据字节不能超过 256字节)。数据字节发送完毕,需要拉高片选线 /CS,并判断状态位,等待写入结束。

进行页编程时,如果数据字节数超过了 256 字节,地址将自动回到页的起始地址,覆盖掉之前的数据。在某些情况下,数据字节小于 256 字节(同一页内),也可以正常对其他字节存放,不会有任何影响。如果存放超过 256 字节的数据,需要分次编程存放。

——W25Q64中文手册

扇区擦除

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第11张图片

扇区擦除指令可以擦除指定一个扇区(4 k 字节)内所有数据,将内存空间恢复到 0xFF 状态。写入扇区擦除指令之前必须执行设备写使能(发送设备写使能指令 0x06),并判断状态寄存器(状态寄存器位最低位必须等于 0 才能操作)。发送的扇区擦除指令前,先拉低/ CS,接着发送扇区擦除指令码”20h”,和 24 位地址(A23-A0),地址发送完毕后,拉高片选线 /CS,并判断状态位,等待擦除结束。擦除一个扇区的最少需要 150ms 时间。

——W25Q64中文手册

全片擦除

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第12张图片

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

——W25Q64中文手册

读取制造商/芯片 ID

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第13张图片

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

——W25Q64中文手册

51单片机控制W25Q64(AT25F1024)

SPI协议代码

以下代码仅供参考,属于我练手的代码,并不能保证没有Bug

大部分51单片机没有自带的SPI接口,所以这里使用纯软件代码实现SPI协议。

51单片机型号——AT89C52RC

由于我没有现成的W25Q64芯片,所以只能用Proteus进行仿真,但是Proteus里只有AT25FX型号的SPI Flash,好在它们的读写操作指令基本相同,只有部分功能(读ID、擦写等)存在差异。
嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第14张图片

SPI读写字节
关键函数,同时进行SPI的写和读,采用SPI模式3(CPOL=1,CPHA=1)。

/******************************************************************************
 * @ 函数名  : Spi_Write_Read_One_Byte
 * @ 功  能  : SPI读写一个字节
 * @ 参  数  : tx_data 要写入的字节
 * @ 返回值  : 读出的字节
 * @ 备  注  : 核心函数
 ******************************************************************************/
static unsigned char Spi_Write_Read_One_Byte(unsigned char tx_data)
{
	unsigned char i = 0;
	unsigned char rx_data = 0;
	for(i = 0; i < 8; i++)
	{
		SCK = 0;          // 时钟电平转换
		if(tx_data & 0x80)
			SO = 1;       // 输出高电平
		else
			SO = 0;
		rx_data <<= 1;
		SCK = 1;          // 时钟电平转换
		if(SI)
			rx_data++;    // 最低位设为1
		tx_data <<= 1;
	}
	return rx_data;       // 返回读到字节
}

写使能

/******************************************************************************
 * @ 函数名  : Spi_Write_Enable
 * @ 功  能  : 写使能
 * @ 参  数  : 无
 * @ 返回值  : 无
 ******************************************************************************/
static void Spi_Write_Enable(void)
{
	CS = 0;  //选中芯片
	Spi_Write_Read_One_Byte(WRITE_ENABLE); //写使能
	CS = 1;  //释放芯片
}

等待Flash写入或擦除完成

/******************************************************************************
 * @ 函数名  : Flash_Wait_For_Written
 * @ 功  能  : 等待FLASH写完
 * @ 参  数  : 无
 * @ 返回值  : 无
 ******************************************************************************/
static void Flash_Wait_For_Written(void)
{
	unsigned char rx_data = 0;
	
	CS = 0;  //选中芯片
	Spi_Write_Read_One_Byte(READ_STATUS_REG); //读取状态寄存器
	do
	{
		rx_data = Spi_Write_Read_One_Byte(0);
	}while((rx_data & 0x01) == 1);
	CS = 1;  //释放芯片
}

Flash读数据

/******************************************************************************
 * @ 函数名  : Flash_Read_Data
 * @ 功  能  : 读取存储器数据
 * @ 参  数  : 
                buff 读出的数据
                addr 开始读取的地址
                len  要读取的长度
 * @ 返回值  : 无
 ******************************************************************************/
void Flash_Read_Data(unsigned char *buff, unsigned long addr, unsigned int len)
{
	CS = 0;  //选中芯片
	Spi_Write_Read_One_Byte(READ_DATA);  //发送读取命令
	Spi_Write_Read_One_Byte((addr >> 16) & 0xff);
	Spi_Write_Read_One_Byte((addr >> 8) & 0xff);
	Spi_Write_Read_One_Byte(addr & 0xff);
	
	while(len--)
	{
		*buff++ = Spi_Write_Read_One_Byte(0);
	}
	CS = 1;  //释放芯片
}

Flash页写

/******************************************************************************
 * @ 函数名  : Flash_Write_Data
 * @ 功  能  : 向存储器写入数据(最大写一页)
 * @ 参  数  : 
                buff 读出的数据
                addr 开始读取的地址
                len  要读取的长度
 * @ 返回值  : 无
 ******************************************************************************/
void Flash_Write_Data(unsigned char *buff, unsigned long addr, unsigned int len)
{
	Spi_Write_Enable();  //写使能
	CS = 0;  //选中芯片
	Spi_Write_Read_One_Byte(WRITE_DATA);  //发送页写命令
	Spi_Write_Read_One_Byte((addr >> 16) & 0xff);
	Spi_Write_Read_One_Byte((addr >> 8) & 0xff);
	Spi_Write_Read_One_Byte(addr & 0xff);
	
	while(len--)
	{
		Spi_Write_Read_One_Byte(*buff++);
	}
	CS = 1;  //释放芯片
	Flash_Wait_For_Written();  //等待写完成
}

扇区擦除
只有Flash存储空间数据为0xFF时才能修改Flash的数据,所以每次写入前需要进行擦除。

/******************************************************************************
 * @ 函数名  : Flash_Erase_Sector
 * @ 功  能  : 擦除FLASH指定扇区
 * @ 参  数  : addr 开始读取的地址
 * @ 返回值  : 无
 ******************************************************************************/
void Flash_Erase_Sector(unsigned long addr)
{
	Spi_Write_Enable();  //写使能
	CS = 0;  //选中芯片
	Spi_Write_Read_One_Byte(ERASE_SECTOR); //发送扇区擦除命令
	Spi_Write_Read_One_Byte((addr >> 16) & 0xff);
	Spi_Write_Read_One_Byte((addr >> 8) & 0xff);
	Spi_Write_Read_One_Byte(addr & 0xff);
	CS = 1;  //释放芯片
	Flash_Wait_For_Written();  //等待擦写完成
}

Flash还有其他高级操作,比如跨页写,这里就不进行讲述,只实现简单的读写功能。

Proteus小实验

这里用上面的代码仿真一个小实验,完整工程代码见文末

实现功能:先将 AT25F1024(1Mb)的第一个扇区进行擦除,再向 0 号地址空间写入 8 个16进制数据,最后从 AT25F1024 的 0 号地址空间写读出 8 个字节数据,同时显示在数码管上。

Proteus仿真

第一步擦除实际用时40多秒,但Proteus中的时序分析显示用时1s,可能是给虚拟机的内存不够,导致运行速度受限。

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第15张图片

实验现象:

嵌入式硬件入门——Flash Memory(W25Q64+SPI协议)_第16张图片

完整代码

本人水平有限,仅供参考。
为方便展示,所有代码写在同一个文件

#include    //此文件中定义了单片机的一些特殊功能寄存器


unsigned char code coding[16]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,
					0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71};//显示0-F的值

					
#define LED_TUBE  P3 //P3的8个IO端口对应数码管的8个信号引脚

					
#define WRITE_ENABLE  0x06      //写芯片使能,在指令或数据前先要使能
#define WRITE_DISABLE 0x04      //禁止写芯片
#define READ_STATUS_REG 0x05    //读状态寄存器
#define READ_DATA      0x03     //读取存储器数据
#define WRITE_DATA    0x02      //页编程
#define ERASE_SECTOR  0x52      //扇区擦除   w25qxx为0x20
#define CHIP_ERASE    0x62      //全片擦除   w25qxx为0xc7/0x60
#define READ_ID       0x15      //读取芯片ID w25qxx为0x90

sbit SCK = P1^0;       //将SCK位定义为P1.0引脚
sbit SO = P1^1;        //将SI位定义为P1.1引脚
sbit SI = P1^2;        //将SO位定义为P1.2引脚
sbit CS = P1^3;        //将CS定义为P1.3引脚
						

/******************************************************************************
 * @ 函数名  : Spi_Write_Read_One_Byte
 * @ 功  能  : SPI读写一个字节
 * @ 参  数  : tx_data 要写入的字节
 * @ 返回值  : 读出的字节
 * @ 备  注  : 核心函数
 ******************************************************************************/
static unsigned char Spi_Write_Read_One_Byte(unsigned char tx_data)
{
	unsigned char i = 0;
	unsigned char rx_data = 0;
	for(i = 0; i < 8; i++)
	{
		SCK = 0;          // 时钟电平转换
		if(tx_data & 0x80)
			SO = 1;       // 输出高电平
		else
			SO = 0;
		rx_data <<= 1;
		SCK = 1;          // 时钟电平转换
		if(SI)
			rx_data++;    // 最低位设为1
		tx_data <<= 1;
	}
	return rx_data;       // 返回读到字节
}



/******************************************************************************
 * @ 函数名  : Spi_Write_Enable
 * @ 功  能  : 写使能 
 * @ 参  数  : 无
 * @ 返回值  : 无
 ******************************************************************************/
static void Spi_Write_Enable(void)
{
	CS = 0;  //选中芯片
	
	Spi_Write_Read_One_Byte(WRITE_ENABLE); //写使能
	
	CS = 1;  //释放芯片
}


/******************************************************************************
 * @ 函数名  : Flash_Wait_For_Written
 * @ 功  能  : 等待FLASH写完
 * @ 参  数  : 无
 * @ 返回值  : 无
 ******************************************************************************/
static void Flash_Wait_For_Written(void)
{
	unsigned char rx_data = 0;
	
	CS = 0;  //选中芯片
	Spi_Write_Read_One_Byte(READ_STATUS_REG); //读取状态寄存器
	do
	{
		rx_data = Spi_Write_Read_One_Byte(0);
	}while((rx_data & 0x01) == 1);
	CS = 1;  //释放芯片
}

/******************************************************************************
 * @ 函数名  : Flash_Read_Data
 * @ 功  能  : 读取存储器数据
 * @ 参  数  : 
                buff 读出的数据
                addr 开始读取的地址
                len  要读取的长度
 * @ 返回值  : 无
 ******************************************************************************/
void Flash_Read_Data(unsigned char *buff, unsigned long addr, unsigned int len)
{
	CS = 0;  //选中芯片
	Spi_Write_Read_One_Byte(READ_DATA);  //发送读取命令
	Spi_Write_Read_One_Byte((addr >> 16) & 0xff);
	Spi_Write_Read_One_Byte((addr >> 8) & 0xff);
	Spi_Write_Read_One_Byte(addr & 0xff);
	
	while(len--)
	{
		*buff++ = Spi_Write_Read_One_Byte(0);
	}
	CS = 1;  //释放芯片
}

/******************************************************************************
 * @ 函数名  : Flash_Write_Data
 * @ 功  能  : 向存储器写入数据(最大写一页)
 * @ 参  数  : 
                buff 读出的数据
                addr 开始读取的地址
                len  要读取的长度
 * @ 返回值  : 无
 ******************************************************************************/
void Flash_Write_Data(unsigned char *buff, unsigned long addr, unsigned int len)
{
	Spi_Write_Enable();  //写使能
	CS = 0;  //选中芯片
	Spi_Write_Read_One_Byte(WRITE_DATA);  //发送页写命令
	Spi_Write_Read_One_Byte((addr >> 16) & 0xff);
	Spi_Write_Read_One_Byte((addr >> 8) & 0xff);
	Spi_Write_Read_One_Byte(addr & 0xff);
	
	while(len--)
	{
		Spi_Write_Read_One_Byte(*buff++);
	}
	CS = 1;  //释放芯片
	Flash_Wait_For_Written();  //等待写完成
}


/******************************************************************************
 * @ 函数名  : Flash_Erase_Sector
 * @ 功  能  : 擦除FLASH指定扇区
 * @ 参  数  : addr 开始读取的地址
 * @ 返回值  : 无
 ******************************************************************************/
void Flash_Erase_Sector(unsigned long addr)
{
	Spi_Write_Enable();  //写使能
	CS = 0;  //选中芯片
	Spi_Write_Read_One_Byte(ERASE_SECTOR); //发送扇区擦除命令
	Spi_Write_Read_One_Byte((addr >> 16) & 0xff);
	Spi_Write_Read_One_Byte((addr >> 8) & 0xff);
	Spi_Write_Read_One_Byte(addr & 0xff);
	CS = 1;  //释放芯片
	Flash_Wait_For_Written();  //等待擦写完成
}


/******************************************************************************
 * @ 函数名  : Delay_10us
 * @ 功  能  : 10us粗略延时
 * @ 参  数  : 延时时间--单位10us
 * @ 返回值  : 无
 ******************************************************************************/
void Delay_10us(unsigned int time)
{
	while(time--);
}


/******************************************************************************
 * @ 函数名  : main
 * @ 功  能  : 主函数
 * @ 参  数  : 无
 * @ 返回值  : 无
 ******************************************************************************/

int main()
{	
	unsigned char i = 0;
	unsigned char tx_buff[20] = {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07};
	unsigned char rx_buff[20] = {0};
	
	SCK = 1;  //时钟空闲电平为高
	CS = 1;   //释放芯片
	
	Flash_Erase_Sector(0);   //擦除第一个扇区
	Flash_Write_Data(tx_buff, 0, 8); //向0号地址写入8个字节
	Flash_Read_Data(rx_buff, 0, 8);  //从0号地址读出8个字节
	while(1)
	{
		
		for(i = 0; i < 8; i++)
		{
			//将从Flash读出的数据显示在数码管上
			LED_TUBE = coding[rx_buff[i]];
			//粗略延时500ms
			Delay_10us(50000);	 	
		}
	}
}

你可能感兴趣的:(#,硬件知识,#,51单片机入门,嵌入式硬件,SPI,单片机)