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

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

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

——百度百科

2.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等存储卡上。

——百度百科

3.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是市场上两种主要的非易失闪存技术,当选择存储解决方案时,设计师必须权衡以下的各项因素:

A.NOR的读速度比NAND稍快一些。
B.NAND的写入速度比NOR快很多。
C.NAND的4ms擦除速度远比NOR的5ms快。
D.大多数写入操作需要先进行擦除操作。
E.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

 

W25Q64管脚定义及说明
管脚定义


管脚功能对照表

 

 

功能说明
注:本文不研究双倍和四倍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时钟的上升沿被采样还是下降沿被采样。

图片来源网络


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

SPI协议读写数据
图片来源:野火电子PPT

 

 根据前面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:

 状态寄存器


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

 

——W25Q64中文手册

读状态寄存器
读状态寄存器 1(05h)和 状态寄存器 2(35h)


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

——W25Q64中文手册

写状态寄存器
读数据


读取数据指令允许按顺序读取一个字节的内存数据。当片选 CS/拉低之后,紧随其后是一个 24 位的地址(A23-A0)(需要发送 3 次,每次 8 个字节,先发高位)。芯片收到地址后,将要读的数据按字节大小转移出去,数据是先转移高位,对于单片机,时钟下降沿发送数据,上升沿接收数据。读数据时,地址会自动增加,允许连续的读取数据。这意味着读取整个内存的数据,只要用一个指令就可以读完。数据读取完成之后,片选信号/ CS 拉高。读取数据的指令序列,如上图所示。如果一个读数据指令而发出的时候,设备正在擦除扇区,或者(BUSY = 1),该读指令将被忽略,也不会对当前周期有什么影响。

——W25Q64中文手册

页写


页编程指令允许从一个字节到 256 字节的数据编程(一页)(编程之前必须保证内存空间是 0XFF)。允许写入指令之前,必须先发送设备写使能指令。写使能开启后,设备才能接收编程指令。开启页编程先拉底/ CS,然后发送指令代码“02h”,接着发送一个 24 位地址(A23-A0)(发送 3 次,每次 8 位) 和至少一个数据字节(数据字节不能超过 256字节)。数据字节发送完毕,需要拉高片选线 /CS,并判断状态位,等待写入结束。进行页编程时,如果数据字节数超过了 256 字节,地址将自动回到页的起始地址,覆盖掉之前的数据。在某些情况下,数据字节小于 256 字节(同一页内),也可以正常对其他字节存放,不会有任何影响。如果存放超过 256 字节的数据,需要分次编程存放。

——W25Q64中文手册

扇区擦除


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

 

——W25Q64中文手册

全片擦除


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

——W25Q64中文手册

读取制造商/芯片 ID


读取制造商/设备 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、擦写等)存在差异。


1.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;       // 返回读到字节
}


2.写使能,代码:

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


3.等待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;  //释放芯片
}


4.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;  //释放芯片
}


5.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();  //等待写完成
}


.6.扇区擦除,代码:
只有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,可能是给虚拟机的内存不够,导致运行速度受限。

 实验现象:

 

完整代码:

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

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);         
        }
    }
}

  • 1
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
首先,需要明确的是,STM32和W25Q64是完全不同的产品,它们分别是微控制器和闪存存储器。而FATFS是一个用于嵌入式系统的文件系统。因此,题目中的"STM32 W25Q64 FATFS速度慢"可能涉及到两个方面的问题,即STM32的操作速度与W25Q64芯片的读写速度,以及FATFS文件系统的读写性能。以下我将就这两个方面进行回答。 首先,STM32的速度问题可能与控制器的硬件和软件设计有关。可能是使用了低速的系统时钟频率或者是编写的代码存在效率上的问题。解决这个问题的方法是重新评估系统的时钟配置,以获得更高的处理速度,并且可以通过优化代码来提高程序的执行效率。 其次,W25Q64芯片的读写速度取决于其本身的性能和连接方式。要确保读写速度最大化,首先要确保正确选择了合适的SPI总线时钟,并进行合适的SPI通信设置。其次,在读写数据时需要考虑到W25Q64芯片的数据传输速率以及是否使用了缓存等功能。最后,检查硬件电路的设计与布线是否符合要求,并根据需要进行优化。 最后,FATFS文件系统的读写性能可能与文件系统的配置有关。在初始化FATFS时,需要选择合适的参数来优化文件系统的性能。例如,可以选择合适的簇大小和对齐方式,以减少磁盘片段和提高读写速度。此外,可以根据实际需求进行文件缓存的大小调整,以提高磁盘访问效率。 总结而言,如果STM32的速度慢、W25Q64芯片的读写速度慢或者FATFS的读写性能慢,需要综合考虑硬件和软件层面的因素,并根据具体问题进行适当的优化和调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值