第2-13讲:SPI总线的应用
-  
  
- 学习目的
 
 - 了解SPI总线的结构、特点以及4种通信模式。
 - 掌握通过SPI读、写和擦除SPI Flash W25Q128的方法以及代码编写。
 - 掌握通过SPI读、写铁电存储器FM25CL64B的方法以及代码编写。 
  
- SPI总线原理
 
 
SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速、全双工、同步的通信总线。SPI是Motorola公司推出的一种同步串行接口技术,SPI由一个主设备和一个或多个从设备组成,在一次数据传输过程中,接口上只能有一个主机和一个从机能够通信。
SPI总线的优点是操作简单、数据传输速率较高、全双工,缺点是只支持单个主机、没有指定的流控制,没有应答机制确认是否接收到数据。
-  
  
-  
    
- 接口信号定义
 
 
 -  
    
 
SPI总线接口包括以下四种信号:
- MOSI(Master Output,Slave Input):主器件数据输出,从器件数据输入。
 - MISO(Master Input, Slave Output):主器件数据输入,从器件数据输出。
 - SCK(Serial Clock):有时也称为SCLK,时钟信号,由主器件产生。
 - CS(Chip select):有时也称为SS,从器件使能信号,由主器件控制,实际使用时,经常用GPIO来代替。
 
SPI总线支持连接多个从机,如下图所示,SPI主机通过连接到从机的片选信号使能/禁止从机,并且同时只能使能一个从机,因此总线里面有多少个从机,就需要多少个片选信号。当SPI主机需要和总线中某个从机进行通信时,主机会拉低对应的CS信号使能该从机,之后发起通信,通信完成后,拉高CS信号,解除总线的占用。

图1:SPI总线结构
对于SPI总线,我们还需要能深刻理解下面几个知识点。
- 硬件片选和软件片选的区别
 
所谓硬件片选指的是SPI本身具有片选信号,当我们通过SPI发送数据时,SPI外设自动拉低CS信号使能从机,发送完成后自动拉高CS信号释放从机,这个过程是不需要软件操作的。而软件片选则是需要使用GPIO作为片选信号,SPI在发送数据之前,需要先通过软件设置作为片选信号的GPIO输出低电平,发送完成之后再设置该GPIO输出高电平。
- SPI总线是回环结构
 
SPI是一个环形总线结构,如下图所示,主设备和从设备构成一个环形。在时钟SCK的作用下,主设备发送一个位到从设备,因为是环形结构,所以从设备必定会同时传送一个位到主设备。同样,主设备向从设备发送一个字节,从设备也必定会同时传送一个字节到主设备。理解了环形结构,就很容易理解下面几点:
- SPI是全双工,同步的通信总线。
 - 主设备向从设备发送数据时,无论我们需不需要从设备返回数据,从设备都会返回数据。
 - 主设备从从设备读取一个字节数据时,为什么需要写一个字节数据到从设备:因为是环形结构,不写数据过去,对方的数据就不会被移位过来。
 

图2:SPI数据传输示意图
- SPI主机和从机之间连接时信号不需要交叉
 
SPI主机和从机连接时,MOSI和MISO信号是不需要交叉连接的,因为MOSI本身就表示了主机输出、从机输入,MISO表示主机输入、从机输出,因此不能交叉连接。

图3:MOSI和MISO不能交叉连接
-  
  
-  
    
- SPI的4种通信模式
 
 
 -  
    
 
SPI总线共有4种通信模式:模式0~模式3,这4种通信模式是由时钟相位和时钟极性确定的。
- 时钟相位CPOL(Clock polarity):SPI总线空闲时,时钟信号SCLK的电平称为时钟极性,有以下两种模式:
 
- CPOL=0:SPI总线空闲时,时钟信号为低电平。
 - CPOL=1:SPI总线空闲时,时钟信号为高电平。
 
- 时钟极性CPHA(Clock phase):SPI在时钟信号SCLK第几个边沿开始采样数据,有以下两种模式:
 
- CPHA=0:在第1个时钟边沿进行数据采样。
 - CPHA=1:在第2个时钟边沿进行数据采样。
 
时钟极性CPOL时钟相位CPHA各有2种模式,他们两两组合就形成了SPI的4种通信模式,如下表所示。
表1:SPI总线的4种工作模式
|   模式  |   描述  | 
|   模式0  |   CPOL=0,CPHA=0  | 
|   模式1  |   CPOL=0,CPHA=1  | 
|   模式2  |   CPOL=1,CPHA=0  | 
|   模式3  |   CPOL=1,CPHA=1  | 
SPI的4种模式中,最常用的是模式0和模式3。正是由于SPI有4种通信模式,因此当我们使用SPI总线时,需要去查询SPI总线中主机设备(如STC8A8K64D4)和从机设备(如SPI Flash)的数据手册,确定他们支持什么模式,从而选择适合的通信模式。
SPI的4种模式的时序图如下。
- 时钟相位CPHA=0时的时序
 

图4:CPHA=0时SPI时序
- 时钟相位CPHA=1时的时序
 

图5:CPHA=0时SPI时序
-  
  
- STC8A8K64D4的SPI应用步骤
 
 
STC8A8K64D4单片机片内集成了一个高速串行通信接口(SPI),SPI 是一种全双工的高速同步通信总线。STC8A8K64D4单片机的SPI支持主机和从机模式,通过配置寄存器,可以让SPI工作于主机模式或从机模式。
-  
  
-  
    
- SPI引脚配置
 
 
 -  
    
 
SPI有多组引脚与之对应(具体几组还取决于芯片封装引脚数),同一时刻,只能通过相关寄存器配置其中的一组使用, STC8A8K64D4单片机SPI的引脚分配如下表。
表2:STC8A8K64D4单片机SPI引脚分配
|   SPI信号  |   信号编号  |   对应的IO  | 
|   SPI时钟  |   SCLK  |   P1.5  | 
|   SCLK_2  |   P2.5  | |
|   SCLK_3  |   P7.7  | |
|   SCLK_4  |   P3.2  | |
|   SPI主出从入(MOSI)  |   MOSI  |   P1.3  | 
|   MOSI_2  |   P2.3  | |
|   MOSI_3  |   P7.5  | |
|   MOSI_4  |   P3.4  | |
|   SPI主入从出(MISO)  |   MISO  |   P1.4  | 
|   MISO_2  |   P2.4  | |
|   MISO_3  |   P7.6  | |
|   MISO_4  |   P3.3  | |
|   SPI片选  |   SS  |   P1.2  | 
|   SS_2  |   P2.2  | |
|   SS_3  |   P7.4  | |
|   SS_4  |   P3.5  | 
SPI是通过“外设端口切换控制寄存器1(P_SW1)”中的SPI_S[1:0]配置引脚的,如下图所示。
外设端口切换控制寄存器1(P_SW1):

P_SW1寄存器中的SPI_S[1:0]为 SPI 功能脚选择位,如下表所示。
表3:SPI功能脚选择位
|   SPI_S[1:0]  |   SS  |   MOSI  |   MISO  |   SCLK  | 
|   00  |   P1.2  |   P1.3  |   P1.4  |   P1.5  | 
|   01  |   P2.2  |   P2.3  |   P2.4  |   P2.5  | 
|   10  |   P7.4  |   P7.5  |   P7.6  |   P7.7  | 
|   11  |   P3.5  |   P3.4  |   P3.3  |   P3.2  | 
-  
  
-  
    
- 配置SPI工作参数
 
 
 -  
    
 
SPI工作参数通过“SPI 控制寄存器(SPCTL)”配置,配置项包括:SS引脚功能、SPI收发位序、主/从机、工作模式、速度以及SPI使能。
SPI 控制寄存器(SPCTL):

- SSIG:SS(片选)引脚功能控制位
 
- 0:SS引脚确定器件是主机还是从机。
 - 1:忽略SS引脚功能,使用 MSTR 确定器件是主机还是从机。
 
通常,我们会将“SS(片选)引脚功能控制位”设置为1,即通过配置“MSTR”位来设置SPI工作于主机或是从机。
- SPEN:SPI 使能控制位
 
- 0:关闭 SPI 功能。
 - 1:使能 SPI 功能。
 
- DORD:SPI 数据位发送/接收的顺序
 
- 0:先发送/接收数据的高位(MSB)。
 - 1:先发送/接收数据的低位(LSB)。
 
- MSTR:器件主/从模式选择位
 
设置主机模式:
- 若 SSIG= 0,则 SS 管脚必须为高电平且设置 MSTR 为 1。
 - 若 SSIG= 1,则只需要设置 MSTR 为 1(忽略 SS 管脚的电平)。
 
设置从机模式:
- 若 SSIG= 0,则 SS 管脚必须为低电平(与 MSTR 位无关)。
 - 若 SSIG= 1,则只需要设置 MSTR 为 0(忽略 SS 管脚的电平)。
 
- CPOL:SPI 时钟极性控制
 
- 0:SCLK 空闲时为低电平,SCLK 的前时钟沿为上升沿,后时钟沿为下降沿。
 - 1:SCLK 空闲时为高电平,SCLK 的前时钟沿为下降沿,后时钟沿为上升沿。
 
- CPHA: SPI 时钟相位控制
 
- 0:数据 SS 管脚为低电平驱动第一位数据并在 SCLK 的后时钟沿改变数据,前时钟沿采样数据(必须 SSIG= 0)。
 - 1:数据在 SCLK 的前时钟沿驱动,后时钟沿采样。
 
由“CPOL”和“CPHA”可见,STC8A8K64D4的SPI支持SPI的4种通信模式。
- SPR[1:0]:SPI时钟频率选择
 
SPR[1:0]设置的是SPI的SCLK频率,他决定了SPI的传输速率,STC8A8K64D4的SPI是快速SPI,可配置的时钟频率如下表所示。
表4:SPI时钟频率
|   SPR[1:0]的值  |   SCLK频率  | 
|   00  |   SYSclk/4  | 
|   01  |   SYSclk/8  | 
|   10  |   SYSclk/16  | 
|   11  |   SYSclk/2  | 
-  
  
-  
    
- 数据收发
 
 
 -  
    
 
SPI的数据传输只能由主机启动,因为只有主机能够产生时钟信号。主机对SPI数据寄存器 SPDAT的写操作将启动 SPI 时钟发生器和数据的传输。在数据写入 SPDAT 之后的半个到一个 SPI位时间后,数据将出现在 MOSI 脚。写入主机 SPDAT 寄存器的数据从 MOSI 脚移出发送到从机的 MOSI脚,同时从机 SPDAT 寄存器的数据从 MISO 脚移出发送到主机的 MISO 脚。
传输完一个字节后,SPI 时钟发生器停止,传输完成标志( SPIF)置位,如果SPI中断使能则会产生一个SPI中断。主机和从机 CPU的两个移位寄存器可以看作是一个16位循环移位寄存器。当数据从主机移位传送到从机的同时,数据也以相反的方向移入。这意味着在一个移位周期中,主机和从机的数据相互交换。
-  
  
- 硬件设计 
    
- 外部存储器模块接口电路
 
 
 - 硬件设计 
    
 
为了方便扩展应用,开发板上设计了一个6芯的外扩存储器接口,其电路如下图所示。该接口可以接入艾克姆科技的W25Q128存储器模块、FM25CL64B铁电存储器模块和TF卡模块。

图6:外部存储器接口
-  
  
-  
    
- W25Q128(Flash)存储器模块
 
 
 -  
    
 
W25Q128(Flash)存储器模块是一款3.3V单电源供电、存储空间为128Mbit(16M字节)的串行Flash存储器模块,使用的存储器芯片型号为W25Q128。
W25Q128是华邦公司(Winbond)推出的串行NOR Flash系列存储器中的一员,该系列还有W25Q80/16/32/64等,W25Q128名称的意义如下:
- W:华邦公司(Winbond)。
 - 25Q:SpiFlash串行NOR Flash,具有4KB扇区,双/四路I / O。
 - 128F:128M-bit。
 - V:工作电压范围为(2.7~3.6)V。
 
W25Q128JV的容量为128Mb共16MB(注意大写的B表示字节,小写的b表示位)。W25Q128将16M的容量分为256个块(Block),每个块大小为 64KB,每个块又分为16个扇区(Sector),每个扇区4K(4096)字节,每个扇区包含8个页(Page),每个页256个字节,即W25Q128由65536个可编程页面构成。
W25Q128只能按页面编程,也就是每个写操作只能写一个页面,因此,一次最多只能写入256个字节数据(从一个页面的起始到结束)。如果将数据写入多个页面,就需要执行多次写操作。
W25Q128的擦除和编程不一样,擦除时可以按扇区擦除、块擦除和全片擦除。擦除操作最小擦除单位为一个扇区而不是页面,也就是每次至少擦除一个扇区(4K字节)。
W25Q128存储器模块的接口为间距2.54mm的6PIN排针,可以直接安装到开发板的外部存储器接口J11。模块上设计有电源指示灯,用于指示模块是否正常供电。

图7:W25Q128(Flash)存储器模块
W25Q128存储器模块的参数如下图所示。

图8:W25Q128(Flash)存储器模块参数
W25Q128存储器模块引脚定义如下表所示。
表5:模块引脚定义
|   引脚序号  |   引脚名称  |   描述  | 
|   1  |   VCC  |   电源正。  | 
|   2  |   GND  |   电源地。  | 
|   3  |   CS  |   SPI片选。  | 
|   4  |   CLK  |   SPI时钟。  | 
|   5  |   MISO  |   SPI主入从出。  | 
|   6  |   MOSI  |   SPI主出从入。  | 
- 注:为了方便读者理解单片机访问W25Q128存储器模块的编程,W25Q128的SPI通信时序以及读、写、擦除操作将在软件设计部分讲解。
 
-  
  
-  
    
- FM25CL64B(FRAM)模块
 
 
 -  
    
 - FRAM铁电存储器介绍
 
FRAM(全称是Ferroelectric Random Access Memory)铁电存储器,该存储器能兼容RAM的一切功能,并且和ROM技术一样,是一种非易失性的存储器。可以说铁电存储器在这两类存储类型间搭起了一座跨越沟壑的桥梁:一种非易失性的RAM。
铁电存储器的工作原理是:当在铁电晶体材料上加入电场,晶体中的中心原子会沿着电场方向运动,达到稳定状态。晶体中的每个自由浮动的中心原子只有2个稳定状态,一个记为逻辑中的0,另一个记为1。中心原子能在常温、没有电场的情况下,停留在此状态达100年以上。铁电存储器不需要定时刷新,能在断电情况下保存数据。由于整个物理过程中没有任何原子碰撞,铁电存储器有高速读写、超低功耗和无限次写入等优点。
说到FRAM铁电存储器,就必须要介绍下美国Ramtron公司,该公司成立于1984年,是一家研究和开发铁电技术用于半导体存储器的公司。2012年9月,Ramtron公司被美国著名半导体公司赛普拉斯(Cypress)并购。2020年4月,infineon英飞凌完成了总价值90亿欧元(合人民币693亿元)对Cypress赛普拉斯半导体公司的收购。
Ramtron公司的FRAM主要包括两大类:串行FRAM和并行FRAM。其中串行FRAM又分I2C总线方式的FM24xx系列和SPI总线方式的FM25xx系列。艾克姆科技FRAM选择的存储器芯片是FM25CL64B芯片(SPI总线方式)。

- 注:Ramtron公司的商业FRAM产品全部由美国和日本的战略代工厂所制造。
 
FRAM的主要特点如下:
- 非易失性:写入的数据掉电不会丢失。
 - 高读写耐久性:FRAM保证最多10万亿次写入,远远超过EEPROM的写入次数。
 - 写入速度快:EEPROM和Flash写入数据之前都需要进行耗时的擦除操作,而FRAM不需要擦除,可以直接覆盖写入,这会节省大量的时间。另外,FRAM自身完成写操作的时间极短,程序中执行写操作时,甚至无需“判忙”。
 - 低功耗:一方面,FRAM自身功耗低,另一方面,FRAM写入速度快,相对于EEPROM,写入同样数量的数据所需的时间更少,消耗的电流也会更少。
 
看到这里,读者可能会有疑惑,既然FRAM在性能上碾压EEPROM和Flash,为什么EEPROM和Flash仍在大量应用,而没有全部被FRAM取代?这是因为FRAM的制造成本高,价格比EEPROM、FLASH更加昂贵,产品设计时,不仅仅需要考虑性能,也需要考虑成本。另外FRAM的储存空间更小,如FM25CL64B的存储空间为8K字节,而W25Q128 Flash存储器的存储空间为16M字节,因此,应用中需要存储较大数据时,通常会选择Flash存储器。
- FM25CL64B铁电存储器模块
 
艾克姆科技的铁电存储器模块使用的FRAM芯片是FM25CL64B,FM25CL64B是非易失性的,并且像RAM一样执行读写,其特性如下。
- 工作电压范围:(2.7~3.65)V。
 - 待机电流典型值为3uA。
 - 1MHz时的工作电流为200 µA。
 - 100万亿(1014)次读/写。
 - 38年的数据保存时间。
 - NoDelay™写操作。
 - 频率高达20 MHz。
 - 支持SPI通信模式0和3。
 
和 W25Q128存储器模块一样,FRAM模块的接口也是间距2.54mm的6PIN排针,同样可以直接安装到开发板的外部存储器接口J11上。模块上同样设计有电源指示灯,用于指示模块是否正常供电。

图9:FM25CL64B铁电存储器模块
FM25CL64B存储器模块的参数如下图所示。

图10:FM25CL64B铁电存储器模块参数
FM25CL64B铁电存储器模块引脚定义如下表所示。
表6:模块引脚定义
|   引脚序号  |   引脚名称  |   描述  | 
|   1  |   VCC  |   电源正。  | 
|   2  |   GND  |   电源地。  | 
|   3  |   CS  |   SPI片选。  | 
|   4  |   CLK  |   SPI时钟。  | 
|   5  |   MISO  |   SPI主入从出。  | 
|   6  |   MOSI  |   SPI主出从入。  | 
- 注:为了方便读者理解单片机访问FM25CL64B铁电存储器模块的编程,FM25CL64B的SPI通信时序以及读、写操作将在软件设计部分讲解。 
  
- 软件设计 
    
- 硬件SPI读写W25Q128存储器实验
 
 
 - 软件设计 
    
 - 注:本节的实验是在“实验2-6-1:串口1数据收发实验”的基础上修改,本节对应的实验源码是:“实验2-13-1:硬件SPI读写W25Q128存储器”。 
  
-  
    
-  
      
- 实验内容
 
 
 -  
      
 
 -  
    
 
将STC8A8K64D4单片机的SPI配置为主机,通过SPI总线访问W25Q128存储器,完成以下操作。
- 读取W25Q128芯片ID:通过读芯片ID可以判断芯片类型以及判断W25Q128是否正确接入。
 - 扇区擦除:擦除一个指定的扇区。
 - 全片擦除:擦除整个芯片。
 - 页编程:向指定页面连续写入不超过页面地址范围的数据。
 - 批量编程:向指定地址连续写入指定长度数据的功能,该功能实现了跨页写入。
 - 批量读:从指定地址连续读取指定长度数据,并将读取的数据通过串口输出。
 
- 注:本节的实验需要使用艾克姆科技的W25Q128存储器模块。
 
-  
  
-  
    
-  
      
- 代码编写
 
 
 -  
      
 
 -  
    
 - 新建一个名称为“w25q128.c”的文件及其头文件“w25q128.h”并保存到工程的“Source”文件夹,并将“w25q128.c”加入到Keil工程中的“SOURCE”组。
 - 引用头文件
 
因为在“main.c”文件中使用了“w25q128.c”文件中的函数,所以需要引用下面的头文件“w25q128.h”。
代码清单:引用头文件
- //引用头文件
 - #include "w25q128.h"
 
- 初始化SPI
 
SPI初始化包含引脚配置、SPI工作模式配置、传输速率配置、SPI通信模式配置、收发数据的位序以及中断配置。
- SPI引脚配置
 
本例中,SPI连接W25Q128FV所用的引脚如下表所示。
表7:SPI连接W25Q128FV引脚分配
|   名称  |   引脚  |   说明  | 
|   SS  |   P7.4  |   SPI片选信号,连接到W25Q128模块的CS引脚。  | 
|   MOSI  |   P7.5  |   SPI时钟信号,连接到W25Q128模块的MOSI引脚。  | 
|   MISO  |   P7.6  |   SPI主入从出,连接到W25Q128模块的MISO引脚。  | 
|   SCLK  |   P7.7  |   SPI主出从入,连接到W25Q128模块的CLK引脚。  | 
- SPI模式:主机。
 - SPI传输速率:6Mbps。
 - SPI通信模式:模式0。
 - SPI收发数据的位序:高位在前(MSB),这是因为W25Q128存储器要求访问他的SPI主机发送数据时遵循MSB。
 - 中断:本例中没有使用中断,采用的是查询方式收发数据。
 
对应的初始化代码清单如下。
代码清单:SPI初始化
- /**************************************************************************************
 - * 描 述 : SPI初始化
 - * 参 数 : 无
 - * 返回值 : 无
 - **************************************************************************************/
 - void spi_init(void)
 - {
 - //设置P7.4~P7.7为准双向口,其中P7.5 P7.6 P7.7这3个引脚将作为SPI的MOSI MISO SCLK信号,P7.4将
 - //作为SPI的片选信号
 - P7M1 &= 0x0F; P7M0 &= 0x0F;
 - SPI_CS_HIGH; //拉高SPI片选引脚,不选择从机(此时,连接的从机从SPI总线上断开)
 - //设置SPI_S[1:0]的值为[1 0],使用P7.5 P7.6 P7.7作为SPI的MOSI MISO SCLK信号
 - P_SW1 &= ~0x0C;
 - P_SW1 |= 0x08;
 - /*-------------------------------------------------------------------
 - SSIG SPEN DORD MSTR CPOL CPHA SPR[1,0]
 - 1 1 0 1 0 0 00
 - SSIG=1:忽略SS引脚功能,使用 MSTR 确定器件是主机还是从机
 - SPEN=1:使能SPI
 - DORD=0:发送/接收的顺序为高位在前
 - MSTR=1:SPI为主机
 - CPOL=0,CPHA=0:SPI工作于模式0
 - SPR[1,0]=00:SPI时钟频率选择:SYSclk/4
 - -------------------------------------------------------------------*/
 - SPCTL = 0xD0;
 - //清零SPI中断标志位和写冲突标志位
 - SPSTAT = SPIF | WCOL;
 - }
 
- SPI数据传输
 
SPI是按照字节来逐个发和收数据的,当然,一次SPI操作可以发送多个字节数据。下面的代码是我们封装的SPI传输函数,用于发/收一个字节数据。注意,这个函数是给其他SPI操作函数调用的,因为函数中不能操作片选信号。
代码清单:SP数据收发函数
- /**************************************************************************************
 - * 描 述 : SPI发送一个字节数据,并返回一个字节数据
 - * 参 数 : dat[in]:待写入的数据
 - * 返回值 : SPI的MISO返回的数据
 - **************************************************************************************/
 - static u8 Spi_WriteOneByte(u8 dat)
 - {
 - SPDAT = dat; //触发SPI发送数据
 - while (!(SPSTAT & SPIF)); //等待发送完成
 - SPSTAT = SPIF | WCOL; //清除SPI状态位
 - return SPDAT; //返回SPI数据
 - }
 
- 读取W25Q128FV芯片ID
 
W25Q128芯片的ID固定为0xEF17,我们可以通过读取ID判断W25Q128是否在线,读取ID的时序如下图所示。读取ID执行的操作如下。
- 拉低片选信号CS,使能W25Q128。
 - 发送扇区擦除命令0x90。
 - 发送24位地址(读取ID时固定为0x000000),高地址在前。
 - 发送2个字节数据(0xFF)读取ID,读取的ID是接收数组的最后2个字节。
 - 拉高片选信号CS,释放W25Q128。
 

图11:读取ID时序
读取ID的函数代码清单如下:
代码清单:读取W25Q128的ID
- /*************************************************************************************
 - * 描 述 : 读取W25Q128芯片的ID,W25Q128的ID:0xEF17,另:W25Q16的ID:0xEF14 W25Q32的
 - ID:0xEF15 W25Q64的ID:0xEF16
 - * 参 数 : 无
 - * 返回值 : id:芯片的ID
 - ***************************************************************************************/
 - u16 W25Q_Spi_ReadID(void)
 - {
 - u16 id = 0;
 - u8 mf_id,dev_id;
 - SPI_CS_LOW; //片选拉低,使能从机
 - (void)Spi_WriteOneByte(W25_ReadID); //发送读取ID指令
 - (void)Spi_WriteOneByte(0x00); //发送24位地址,读ID时,24位地址为0x000000
 - (void)Spi_WriteOneByte(0x00);
 - (void)Spi_WriteOneByte(0x00);
 - mf_id =Spi_WriteOneByte(0xFF); //读取 MANUFACTURER ID(MF7-MF0),值应为:0xEF
 - dev_id =Spi_WriteOneByte(0xFF); //读取 Device ID(ID7-ID0),值应为:0x17
 - id = mf_id*256 + dev_id; //将读取的MANUFACTURER ID和Device ID合并为16位
 - SPI_CS_HIGH; //片选拉高,断开从机
 - return id; //返回读取的ID
 - }
 
- 擦除扇区:擦除指定的扇区
 
- Flash为什么要擦除?
 
因为Flash的编程原理都是只能将各个bit由1写为 0,而不能将0写为1,因此在Flash编程之前,为了保证写入的正确性,必须将对应的扇区擦除,擦除操作会将该扇区的内容全部恢复为0xFF,这样执行写入操作就可以正确执行了。
W25Q128支持扇区擦除、块擦除和全片擦除,W25Q128的最小擦除单位为一个扇区,也就是每次至少擦除4K字节。我们在操作Flash的时候,要特别注意Flash编程时间和擦除时间,尤其是擦除时间,因为这些操作通常用时较长,程序中如果处理不好的话,可能会导致程序运行堵塞。W25Q128编程时间和擦除时间如下表所示。
表8:W25Q128编程和擦除时间
|   描述  |   符号  |   规格  |   单位  | ||
|   最小值  |   典型值  |   最大值  | |||
|   字节编程时间 (第一个字节) (注1)  |   tBP1  |   —  |   30  |   50  |   微秒  | 
|   另外的字节编程时间 (第一个字节后) (注1)  |   tBP2  |   —  |   2.5  |   12  |   微秒  | 
|   页编程时间  |   tPP  |   —  |   0.7  |   3  |   毫秒  | 
|   块擦除时间(4KB)  |   tSE  |   —  |   100  |   400  |   毫秒  | 
|   45  | |||||
|   块擦除时间(32KB)  |   tBE1  |   —  |   120  |   1600  |   毫秒  | 
|   块擦除时间(64KB)  |   tBE2  |   —  |   150  |   2000  |   毫秒  | 
|   全片擦除时间  |   tCE  |   —  |   40  |   200  |   秒  | 
注1:同一个页面内多个字节编程的时间为:tBPN = tBP1+ tBP2 * N (max),N=编程的字节数。
W25Q128FV扇区擦除时序如下图所示,扇区擦除执行的操作如下,注意擦除扇区之前必须先发送“写使能”命令开启W25Q128FV的写使能。
- 拉低片选信号CS,使能W25Q128FV。
 - 发送扇区擦除命令0x20。
 - 发送扇区24位地址,高地址在前。
 - 拉高片选信号CS,释放W25Q128FV。
 
命令发送完成并不表示W25Q128FV已经执行完成扇区擦除操作,因此命令发送完后还需要通过查询状态寄存器的BUSY位来判断擦除操作是否完成,当BUSY位的值为0时表示操作完成,W25Q128FV就绪(扇区擦除完成)。

图12:擦除扇区时序
根据W25Q128扇区擦除时序,编写代码如下:
代码清单:扇区擦除
- /**************************************************************************************
 - * 描 述 : 擦除一个扇区,W25Q128最小的擦除单位是扇区.擦除一个扇区所需时间典型值为45ms,最大值为
 - 400ms
 - * 参 数 : SecAddr[in]:扇区地址
 - * 返回值 : 无
 - ***************************************************************************************/
 - void W25Q_Erase_Sector(u32 SecAddr)
 - {
 - while(W25Q_Spi_ReadStatus()&0x01); // 判断是否忙
 - W25Q_WriteEnable(); // 写允许
 - SPI_CS_LOW; //片选拉低,使能从机
 - Spi_WriteOneByte(W25_SectorErase); // 写之前先擦除
 - Spi_WriteOneByte((u8)(SecAddr>>16)); //先发送24位地址的高8位
 - Spi_WriteOneByte((u8)(SecAddr>>8)); //再发送24位地址的中间8位
 - Spi_WriteOneByte((u8)SecAddr); //最后发送24位地址的低8位
 - SPI_CS_HIGH; //片选拉高,断开从机
 - while(W25Q_Spi_ReadStatus()&0x01); // 等待擦除操作完成
 - }
 
- 块擦除:擦除指定的块
 
块擦除指令将指定块(64K字节)内的所有内存设置为0xFF的擦除状态。在W25Q128接受块擦除指令之前,必须先发送“写使能”命令开启W25Q128的写使能。W25Q128块擦除时序如下图所示。
- 拉低片选信号CS,使能W25Q128。
 - 发送块擦除命令0xD8。
 - 发送扇区24位地址,高地址在前。
 - 拉高片选信号CS,释放W25Q128。
 
命令发送完成并不表示W25Q128已经执行完成扇区擦除操作,因此命令发送完后还需要通过查询状态寄存器的BUSY位来判断擦除操作是否完成,当BUSY位的值为0时表示操作完成,W25Q128就绪(块擦除完成)。

图13:擦除块时序
根据W25Q128块擦除时序,编写的代码清单如下:
代码清单:块擦除
- /**************************************************************************************
 - * 描 述 : 擦除一个块(64K)
 - * 参 数 : BlockAddr[in]:块地址
 - * 返回值 : 无
 - ***************************************************************************************/
 - void W25Q_Erase_Block(u32 BlockAddr)
 - {
 - while(W25Q_Spi_ReadStatus()&0x01); //判断是否忙
 - W25Q_WriteEnable(); //写允许
 - SPI_CS_LOW; //片选拉低,使能从机
 - Spi_WriteOneByte(W25_BlockkErase); //写入扇区擦除命令
 - Spi_WriteOneByte((u8)(BlockAddr>>16)); //先发送24位地址的高8位
 - Spi_WriteOneByte((u8)(BlockAddr>>8)); //再发送24位地址的中间8位
 - Spi_WriteOneByte((u8)BlockAddr); //最后发送24位地址的低8位
 - SPI_CS_HIGH; //片选拉高,断开从机
 - while(W25Q_Spi_ReadStatus()&0x01); //等待擦除完成
 - }
 
- 全片擦除:擦除整个芯片
 
W25Q128全片擦除时序如下图所示,全片擦除执行的操作如下。注意全片擦除之前必须先发送“写使能”命令开启W25Q128的写使能。
- 拉低片选信号CS,使能W25Q128。
 - 发送扇区擦除命令0xC7。
 - 拉高片选信号CS,释放W25Q128。
 
全片擦除花费时间较长,典型时间是40秒,命令发送完后需要查询状态寄存器的BUSY位,直到BUSY位的值为0(全片擦除完成,W25Q128就绪)才可以执行其他操作。

图14:全片擦除时序
根据W25Q128全片擦除时序,编写的代码清单如下:
代码清单:全片擦除
- /**************************************************************************************
 - * 描 述 : 全片擦除W25Q128,全片擦除所需的时间典型值为:40秒
 - * 参 数 : 无
 - * 返回值 : 无
 - ***************************************************************************************/
 - void W25Q_Erase_Chip(void)
 - {
 - while(W25Q_Spi_ReadStatus()&0x01); //判断是否忙
 - W25Q_WriteEnable(); //写允许
 - SPI_CS_LOW; //片选拉低,使能从机
 - Spi_WriteOneByte(W25_ChipErase); //写入全片擦除命令
 - SPI_CS_HIGH; //从CS=1时开始执行擦除
 - while(W25Q_Spi_ReadStatus()&0x01); //等待擦除完成
 - }
 
- 按页写:向指定页面连续写入不超过页面地址范围的数据
 
W25Q128FV扇区擦除时序如下图所示,扇区擦除执行的操作如下,注意擦除扇区之前必须先发送“写使能”命令开启W25Q128FV的写使能。
- 拉低片选信号CS,使能W25Q128FV。
 - 发送页编程命令0x02。
 - 发送24位地址,高地址在前。
 - 发送写入到Flash的数据,注意最大写入的长度不能超过该地址所处页面的剩余空间。
 - 拉高片选信号CS,释放W25Q128FV。
 
页编程时,数据传输完成并不表示W25Q128FV已经将接收的数据写入到自身的Flash内,因此数据传输完后还需要通过查询状态寄存器的BUSY位来判断编程是否完成,当BUSY位的值为0时表示编程完成,W25Q128FV就绪。

图15:页编程时序
根据W25Q128页编程时序,代码清单如下:
代码清单:页编程
- /**************************************************************************************
 - * 描 述: 向指定的地址写入数据,最大写入的长度不能超过该地址所处页面的剩余空间
 - * 参 数: *pBuffer[in]:指向待写入的数据
 - * WriteAddr[in]:写入的起始地址
 - * w_size[in]:写入的字节数
 - * 返回值: 无
 - ***************************************************************************************/
 - void W25Q_Write_Page(u8 *pBuffer, u32 WriteAddr, u32 w_size)
 - {
 - u16 i;
 - while(W25Q_Spi_ReadStatus()&0x01); //判断是否忙
 - W25Q_WriteEnable(); //写使能
 - SPI_CS_LOW; //使能器件
 - Spi_WriteOneByte(W25_PageProgram); //发送写页命令
 - Spi_WriteOneByte((u8)((WriteAddr)>>16)); //发送24bit地址
 - Spi_WriteOneByte((u8)((WriteAddr)>>8));
 - Spi_WriteOneByte((u8)WriteAddr);
 - for(i=0;i<w_size;i++) //循环写入数据
 - {
 - Spi_WriteOneByte(*pBuffer++);
 - }
 - SPI_CS_HIGH; //取消片选
 - while(W25Q_Spi_ReadStatus()&0x01); //等待W25Q128自身编程完成
 - }
 
- 批量编程:向指定地址连续写入指定长度数据的功能,该功能实现了跨页写入。
 
批量编程是在页编程的基础上,将编程的数据拆分进行多次页编程,从而实现跨页编程,也就是实现从任意地址开始写入任意长度的数据,当然,地址范围和编程的数据长度不能超过W25Q128的自身的限制。
代码清单:批量编程
- /*************************************************************************************
 - * 描 述:向指定的地址写入指定大小的数据,支持跨页写入数。注:写入数据的地址所在的页面已经执行过擦除操作
 - * 参 数:*pBuffer[in]:指向待写入的数据
 - * WriteAddr[in]:写入的起始地址
 - * w_size[in]:写入的字节数
 - * 返回值:无
 - **************************************************************************************/
 - void W25Q_Write_Bytes(u8 * pBuffer,u32 WriteAddr,u32 w_size)
 - {
 - u32 PageByteRemain = 0;
 - //计算起始地址所处页面的剩余空间
 - PageByteRemain = W25Q128_PAGE_SIZE - WriteAddr%W25Q128_PAGE_SIZE;
 - //如果编程的数据长度不大于页面的剩余空间,编程数据长度等于w_size
 - if(w_size <= PageByteRemain)
 - {
 - PageByteRemain = w_size;
 - }
 - //开始编程,直到所有的数据编程完成
 - while(1)
 - {
 - //编程PageByteRemain个字节
 - W25Q_Write_Page(pBuffer,WriteAddr,PageByteRemain);
 - //如果起始地址所处页面的剩余空间足够存放写入的数据,执行写入后,写入结束,退出循环
 - if(PageByteRemain == w_size)break;
 - else
 - {
 - //取数据的地址增加PageByteRemain
 - pBuffer += PageByteRemain;
 - //计算编程写入的地址
 - WriteAddr += PageByteRemain;
 - //剩余待编程的数据大小
 - w_size -= PageByteRemain;
 - //计算下次编程的数据长度
 - if(w_size > W25Q128_PAGE_SIZE)
 - {
 - PageByteRemain = W25Q128_PAGE_SIZE;
 - }
 - else
 - {
 - PageByteRemain = w_size;
 - }
 - }
 - }
 - }
 
- 批量读:从指定地址连续读取指定长度数据,并将读取的数据通过串口输出
 
W25Q128支持连续读任意长度数据,甚至可以通过读命令一次将整个Flash内容读取出来,但是通常单片机内存无法存储这么大的数据,因此批量读取的时候往往也是分次读取的,读取的代码清单如下。
代码清单:批量读出数据
- /**************************************************************************************
 - * 描 述: 从指定的起始地址连续读取指定长度的数据
 - * 参 数: *pBuffer[in]:指向保存读出数据的缓存的起始地址
 - * ReadAddr[in]:读取数据的起始地址
 - * r_size[in]:读取的字节数
 - * 返回值 : 无
 - ***************************************************************************************/
 - void W25Q_Read_Bytes(u8 * pBuffer,u32 ReadAddr,u32 r_size)
 - {
 - u32 i;
 - while(W25Q_Spi_ReadStatus()&0x01); //判忙
 - SPI_CS_LOW; //使能器件
 - Spi_WriteOneByte(W25X_ReadDATA8); //发送读取命令
 - Spi_WriteOneByte((u8)((ReadAddr)>>16)); //发送24bit地址
 - Spi_WriteOneByte((u8)((ReadAddr)>>8));
 - Spi_WriteOneByte((u8)ReadAddr);
 - for(i=0;i<r_size;i++) //从地址地地址开始逐字节读出指定大小的数据
 - {
 - *pBuffer++=Spi_WriteOneByte(0xFF);
 - }
 - SPI_CS_HIGH; //取消片选
 - }
 
- 主函数
 
主函数中初始化指示灯、按键和SPI等外设,接着读取W25Q128的ID,以此判断W25Q128模块是否正确安装到开发板。如果读取的ID数值为0xEF17,说明W25Q128模块安装正确,程序继续向下运行,否则,闪烁指示灯D1指示未检测到W25Q128。
程序进入主循环后,扫描按键状态,并根据按键状态执行以下动作。
- KEY1按下:按页写入,扇区0的第一页写入256个字节,之后读出数据并通过串口输出。
 - KEY2按下:向起始地址100连续写入500个字节数据,之后读出数据并通过串口输出。这里演示了跨页的批量写,写入的数据占用了2个页面。
 - KEY3按下:全片擦除W25Q128。
 
代码清单如下:
代码清单:主函数
- /**************************************************************************
 - 功能描述:主函数
 - 入口参数:无
 - 返 回 值:int类型
 - **************************************************************************/
 - int main(void)
 - {
 - u8 btn_val;
 - u16 i;
 - u8 j = 0;
 - u16 chip_id,test_len;
 - P2M1 &= 0x3F; P2M0 &= 0x3F; //设置P2.6~P2.7为准双向口(指示灯D1和D2)
 - P7M1 &= 0xF9; P7M0 &= 0xF9; //设置P7.1~P7.2为准双向口(指示灯D4和D3)
 - P3M1 &= 0x3F; P3M0 &= 0x3F; //设置P3.6~P3.7为准双向口(按键KEY2和KEY1)
 - P0M1 &= 0x5F; P0M0 &= 0x5F; //设置P0.5,P0.7为准双向口(按键KEY4和KEY3)
 - P3M1 &= 0xFE; P3M0 &= 0xFE; //设置P3.0为准双向口(串口1的RxD)
 - P3M1 &= 0xFD; P3M0 |= 0x02; //设置P3.1为推挽输出(串口1的TxD)
 - uart1_init(); //串口1初始化
 - spi_init(); //初始化SPI
 - delay_ms(500); //初始化后适当延时
 - EA = 1; //使能总中断
 - //通过读取w25q128芯片的ID判断w25q128模块是否正确安装到开发板,若读取ID不成功,指示灯D1持续闪烁
 - //提示读取ID失败
 - chip_id = W25Q_Spi_ReadID();
 - while(chip_id != W25Q128_ID)
 - {
 - led_toggle(LED_1);
 - delay_ms(1500);
 - }
 - printf("CHIP ID: %04hX\r\n",chip_id); //串口打印出读取的ID
 - while(1)
 - {
 - btn_val=buttons_scan(0); //获取开发板用户按键检测值,不支持连按
 - //按下KEY1:测试按页写入。扇区0的第一页写入256个字节,之后读出数据并通过串口输出
 - if(btn_val == BUTTON1_PRESSED)
 - {
 - test_len = 256;
 - //写之前需要先执行擦除操作
 - W25Q_Erase_Sector(0);
 - for(i=0;i<test_len;i++)my_tx_buf[i] = j++;
 - //写入256个字节数据,使用按页写入时,一次写入的数据长度不超过一个页面的剩余空间
 - W25Q_Write_Page(my_tx_buf,0,test_len);
 - //读取写入的数据
 - W25Q_Read_Bytes(my_rx_buf,0,test_len);
 - //串口打印读取的数据
 - for(i=0;i<test_len;i++)printf("%02bX ",my_rx_buf[i]);
 - //翻转指示灯D1状态,指示操作完成
 - led_toggle(LED_1);
 - }
 - //按下KEY2:批量写入和读出,向起始地址100连续写入500个字节数据,之后读出数据并通过串口输出
 - else if(btn_val == BUTTON2_PRESSED)
 - {
 - test_len = 512;
 - for(i=0;i<test_len;i++)my_tx_buf[i] = j++;
 - //写入256个字节数据,使用按页写入时,一次写入的数据长度不超过一个页面的剩余空间
 - W25Q_Write_Bytes(my_tx_buf,100,test_len);
 - //读取写入的数据
 - W25Q_Read_Bytes(my_rx_buf,100,test_len);
 - //串口打印读取的数据
 - for(i=0;i<test_len;i++)printf("%02bX ",my_rx_buf[i]);
 - led_toggle(LED_2); //控制用户指示灯D2翻转
 - }
 - //按下KEY3:全片擦除W25Q128,全片擦除所需的时间典型值为:40秒
 - else if(btn_val == BUTTON3_PRESSED)
 - {
 - led_on(LED_3); //点亮指示灯D3
 - W25Q_Erase_Chip(); //全片擦除
 - led_off(LED_3); //熄灭指示灯D3
 - }
 - }
 - }  
  
-  
    
-  
      
- 硬件连接
 
 
 -  
      
 
 -  
    
 
本实验需要使用LED指示灯、按键和艾克姆科技的W25Q128存储器模块,按照下图所示安装W25Q128存储器模块和短接跳线帽。

图16:跳线帽短接
-  
  
-  
    
-  
      
- 实验步骤
 
 
 -  
      
 
 -  
    
 - 解压“…\第3部分:配套例程源码”目录下的压缩文件“实验2-13-1:硬件SPI读写W25Q128存储器”,将解压后得到的文件夹拷贝到合适的目录,如“D\STC8”(这样做的目的是为了防止中文路径或者工程存放的路径过深导致打开工程出现问题)。
 - 双击“…\spi_w25q128\project”目录下的工程文件“spi_w25q128.uvproj”。
 - 点击编译按钮编译工程,编译成功后生成的HEX文件“spi_w25q128.hex”位于工程的“…\spi_w25q128\Project\Object”目录下。
 - 打开STC-ISP软件下载程序,下载使用内部IRC时钟,IRC频率选择:24MHz。
 - 电脑上打开串口调试助手,选择开发板对应的串口号,将波特率设置为9600bps。程序运行后,分别按下KEY1、KEY2和KEY3按键执行读写和擦除W25Q128。
 
- 按下KEY1按键:按页编程,先擦除扇区0,接着向扇区0的第一页写入256个字节,之后读出数据并通过串口输出。
 

图17:串口接收的数据
- 按下KEY2按键:批量写入和读出,向起始地址100连续写入500个字节数据,之后读出数据并通过串口输出。这里演示了跨页的批量写,写入的数据占用了2个页面。需要注意的是,批量写函数没有对所要写入的扇区进行擦除,读者在调用该函数前,要确定所写的内容占用的扇区是擦除过的。
 

图18:串口接收的数据
- 按下KEY3按键:全片擦除W25Q128,按下按键后指示灯D3点亮,指示灯开始全片擦除W25Q128存储器,擦除完成后指示灯D3熄灭,注意,全片擦除所需的时间较长,典型值为:40秒。 
  
-  
    
- 模拟SPI读写W25Q128存储器实验
 
 
 -  
    
 
- 注:本节的实验是在“实验2-13-1:硬件SPI读写W25Q128存储器”的基础上修改,本节对应的实验源码是:“实验2-13-2:模拟SPI读写W25Q128存储器”。 
  
-  
    
-  
      
- 实验内容
 
 
 -  
      
 
 -  
    
 
本实验实现的功能和“实验2-13-1:硬件SPI读写W25Q128存储器”完全一样,区别是本实验中不使用STC8A8K64D4单片机集成的硬件SPI外设,而是使用GPIO模拟SPI的协议时序完成SPI通信。
- 注1:相对于硬件SPI,模拟SPI通信时,CPU需要频繁地操作IO,会占用更多的CPU资源,因此建议开发程序时优先考虑使用硬件SPI,当硬件SPI不够用时,再考虑使用模拟SPI。
 - 注2:本节的实验需要使用艾克姆科技的W25Q128存储器模块。 
  
-  
    
-  
      
- 代码编写
 
 
 -  
      
 
 -  
    
 
模拟SPI的程序相对于硬件SPI的程序来说,主要修改点包括以下几个方面:
- 定义模拟SPI的信号引脚和相关的操作宏。
 - SPI初始化函数:初始化函数无需再初始化SPI的寄存器,而是配置SPI所用的GPIO。
 - SPI收发函数:模拟SPI时,我们习惯将发和收分开来操作,函数中通过操作GPIO来模拟SPI的时序实现SPI的发和收。
 
接下来,我们看一下具体的代码实现。
- 定义模拟SPI的信号引脚和相关的操作宏
 
本例中,我们同样使用P7.4、P7.5、P7.6和P7.7分别做为SPI的SS、MOSI、MISO和SCLK信号引脚,如下表所示。模拟SPI配置引脚时需要注意引脚的方向,STC8A8K64D4单片机的GPIO支持配置为“准双向口”,所以我们在后面的SPI初始化函数中我们统一将4个引脚全部配置为准双向即可。
表9:模拟SPI引脚
|   名称  |   引脚  |   方向  | 
|   SS  |   P7.4  |   输出。  | 
|   MOSI  |   P7.5  |   输出。  | 
|   MISO  |   P7.6  |   输入。  | 
|   SCLK  |   P7.7  |   输出。  | 
SPI信号引脚定义的代码清单如下,另外,为了程序操作方便,我们还定义了片选、时钟信号和MOSI引脚输出高电平和低电平的宏。
代码清单:定义SPI信号所用的引脚
- sbit SPI_CS_PIN = P7.4; //定义CS
 - sbit SPI_MOSI_PIN = P7^5; //定义MOSI
 - sbit SPI_MISO_PIN = P7^6; //定义MISO
 - sbit SPI_SCK_PIN = P7^7; //定义SCK
 - #define SPI_CS_LOW SPI_CS_PIN = 0 //SPI片选引脚输出低电平
 - #define SPI_CS_HIGH SPI_CS_PIN = 1 //SPI片选引脚输出高电平
 - #define SPI_SCK_LOW SPI_SCK_PIN = 0 //SPI时钟信号引脚输出低电平
 - #define SPI_SCK_HIGH SPI_SCK_PIN = 1 //SPI时钟信号引脚输出高电平
 - #define SPI_MOSI_LOW SPI_MOSI_PIN = 0 //SPI主出从入引脚输出低电平
 - #define SPI_MOSI_HIGH SPI_MOSI_PIN = 1 //SPI主出从入引脚输出高电平
 
- SPI初始化函数
 
SPI初始化函数中将SPI的4个信号所用的GPIO全部配置为准双向口,接着拉高片选引脚,不使能SPI从机(W25Q128模块)。
对于时钟信号,要根据使用的SPI通信模式设置其状态,本例中使用的是通信模式0,时钟信号在空闲状态下为低电平,因此,需要将时钟信号对应的GPIO拉低。
代码清单:SPI初始化
- /**************************************************************************************
 - * 描 述 : SPI初始化
 - * 参 数 : 无
 - * 返回值 : 无
 - **************************************************************************************/
 - void spi_init(void)
 - {
 - //设置P7.4~P7.7为准双向口,其中P7.5 P7.6 P7.7这3个引脚将作为SPI的MOSI MISO SCLK信号,P7.4将
 - //作为SPI的片选信号
 - P4M1 &= 0xF0; P4M0 &= 0xF0;
 - SPI_CS_HIGH; //拉高SPI片选引脚,不选择从机(此时,连接的从机从SPI总线上断开)
 - SPI_SCK_LOW; //拉低时钟线
 - }
 
- SPI收发函数
 
SPI收发函数中,每次发送和接收都是一个字节,收发过程中,通过操作SPI信号对应的GPIO模拟出SPI的时序,代码清单如下。
代码清单:SPI发送
- /**************************************************************************************
 - * 描 述 : 软件模拟SPI时序发送一个字节数据,并返回一个字节数据
 - * 参 数 : dat[in]:待写入的数据
 - * 返回值 : 无
 - **************************************************************************************/
 - static void Spi_WriteOneByte(u8 dat)
 - {
 - u8 i, temp;
 - temp = dat;
 - for(i=0; i<8; i++) //发送一个字节数据,循环8次
 - {
 - SPI_SCK_LOW; //拉低时钟线
 - spi_delay(); //延时
 - if((temp&0x80) == 0x80)//最高为"1",则MOSI引脚输出高电平,否则输出低电平
 - {
 - SPI_MOSI_HIGH;
 - }
 - else
 - {
 - SPI_MOSI_LOW;
 - }
 - SPI_SCK_HIGH; //拉高时钟线
 - spi_delay(); //延时
 - temp <<= 1; //第一位待发的位移到最高位
 - }
 - SPI_MOSI_LOW; //拉低MOSI
 - }
 
代码清单:SPI接收
- /**************************************************************************************
 - * 描 述 : 软件模拟SPI时序读一个字节数据
 - * 参 数 : 无
 - * 返回值 : SPI读取一个字节数据
 - **************************************************************************************/
 - static u8 Spi_ReadOneByte(void)
 - {
 - u8 i, temp=0;
 - for(i=0; i<8; i++) //读取一个字节数据,循环8次
 - {
 - temp <<= 1; //每个循环,左移一次,保存读取的位
 - SPI_SCK_LOW; //拉低时钟线
 - spi_delay(); //延时
 - if (SPI_MISO_PIN == 1)//若MISO为高电平,temp最低位置1
 - {
 - temp++;
 - }
 - SPI_SCK_HIGH; //拉高时钟线
 - spi_delay(); //延时
 - }
 - return temp; //返回读取的数据
 - }     
  
-  
    
-  
      
- 硬件连接
 
 
 -  
      
 
 -  
    
 
同“实验2-13-1:硬件SPI读写W25Q128存储器”。
-  
  
-  
    
-  
      
- 实验步骤
 
 
 -  
      
 
 -  
    
 
同“实验2-13-1:硬件SPI读写W25Q128存储器”。
-  
  
-  
    
- SPI读写铁电存储器(FRAM)实验
 
 
 -  
    
 - 注:本节的实验是在“实验2-13-1:硬件SPI读写W25Q128存储器”的基础上修改,本节对应的实验源码是:“实验2-13-3:硬件SPI读写铁电存储器(FM25CL64B)”。 
  
-  
    
-  
      
- 实验内容
 
 
 -  
      
 
 -  
    
 
将STC8A8K64D4单片机的SPI配置为主机,通过SPI总线访问FM25CL64B铁电存储器模块,完成以下操作。
- 数据写入:向指定地址连续写入指定长度数据。
 - 数据读取:从指定地址连续读取指定长度数据,并将读取的数据通过串口输出。
 
- 注:本节的实验需要使用艾克姆科技的FM25CL64B铁电存储器模块。
 
-  
  
-  
    
-  
      
- 代码编写
 
 
 -  
      
 
 -  
    
 - 新建一个名称为“fm25cl64b.c”的文件及其头文件“fm25cl64b.h”并保存到工程的“Source”文件夹,并将“fm25cl64b.c”加入到Keil工程中的“SOURCE”组。
 - 引用头文件
 
因为在“main.c”文件中使用了“fm25cl64b.c”文件中的函数,所以需要引用下面的头文件“fm25cl64b.h”。
代码清单:引用头文件
- //引用头文件
 - #include " fm25cl64b.h"
 
- 初始化SPI
 
SPI初始化配置中的引脚配置、SPI工作模式配置、传输速率配置、收发数据的位序以及中断配置和访问W25Q128的例子配置的完全一样,读者参阅“实验2-13-1:硬件SPI读写W25Q128存储器”中的SPI初始化代码即可。需要注意的是:FM25CL64B只支持SPI通信模式0和3,因此,SPI初始化代码中使用相对常用的SPI通信模式0。
- 写使能
 
FM25CL64B上电后是禁止写入的,在执行写入操作包括写入状态寄存器(WRSR)和写入存储器(WRITE)之前需要发送写使能(WREN)命令使能写入。写使能的操作步骤和时序如下。
- 拉低片选信号CS,使能FM25CL64B。
 - 发送写使能命令0x06(WREN)。
 - 拉高片选信号CS,释放FM25CL64B。
 

图19:写使能时序
根据写使能时序,编写的写使能函数代码清单如下。
代码清单:写使能
- /**************************************************************************************
 - * 描 述 : 写使能
 - * 参 数 : 无
 - * 返回值 : 无
 - **************************************************************************************/
 - static void Fram_WriteEnable (void)
 - {
 - SPI_CS_LOW; //使能器件
 - Spi_WriteOneByte(FRAM_WREN); //发送写使能指令
 - SPI_CS_HIGH; //取消片选
 - }
 
- 写内存数据
 
FM25CL64B写内存数据操作的步骤和时序如下,写操作之前必须先发送“写使能”命令开启FM25CL64B的写使能。FM25CL64B写之前是无需擦除的,数据可以直接覆盖写入。
- 拉低片选信号CS,使能FM25CL64B。
 - 发送写内存数据命令0x02(WRITE)。
 - 发送13位地址,高地址在前。注意发送的地址是两个字节(16位),其中的高3位被忽略。
 - 发送写入到FM25CL64B的数据,注意最大写入的数据长度不能超过该起始地址到内存结束地址能容纳的字节数。
 - 拉高片选信号CS,释放FM25CL64B。
 

图20:写数据操作时序
根据写内存数据时序,编写的写内存数据的函数代码清单如下。
代码清单:写使能
- /*************************************************************************************
 - * 描 述:以给定的地址为起始地址连续写入给定大小的数据
 - * 参 数:*pBuffer[in]:指向待写入的数据
 - * WriteAddr[in]:写入的起始地址
 - * w_size[in]:写入的字节数
 - * 返回值:无
 - **************************************************************************************/
 - void Fram_Write_Bytes(u8 * pBuffer,u16 WriteAddr,u16 w_size)
 - {
 - u16 i;
 - Fram_WriteEnable(); //写使能
 - SPI_CS_LOW;
 - (void)Spi_WriteOneByte(FRAM_WRITEE); //发送写数据指令
 - (void)Spi_WriteOneByte((u8)((WriteAddr&0xE0)>>8)); //发送16bit地址
 - (void)Spi_WriteOneByte((u8)WriteAddr);
 - for(i=0;i<w_size;i++) //连续写入数据
 - {
 - Spi_WriteOneByte(pBuffer[i]);
 - }
 - SPI_CS_HIGH;
 - }
 
- 写存储器数据
 
FM25CL64B读存储器数据操作的步骤和时序如下。
- 拉低片选信号CS,使能FM25CL64B。
 - 发送读存储器数据命令0x03(READ)。
 - 发送13位地址,高地址在前。注意发送的地址是两个字节(16位),其中的高3位被忽略。
 - SPI通过“虚写”读出数据,每读出一个字节数据,只要主机继续发出时钟和片选为低,地址就在FM25CL64B内部自动递增,若地址达到最后一个地址0x01FFF,则自动返回0x 0000。
 - 拉高片选信号CS,释放FM25CL64B。
 

图21:读存储器时序
根据读存储器数据时序,编写的读存储器数据的函数代码清单如下。
代码清单:读取数据
- /**************************************************************************************
 - * 描 述: 从指定的起始地址开始连续读取指定长度的数据
 - * 参 数: *pBuffer[in]:指向保存读出数据的缓存的起始地址
 - * ReadAddr[in]:读取数据的起始地址
 - * r_size[in]:读取的字节数
 - * 返回值 : 无
 - ***************************************************************************************/
 - void Fram_Read_Bytes(u8 * pBuffer,u16 ReadAddr,u16 r_size)
 - {
 - u32 i;
 - SPI_CS_LOW; //使能器件
 - Spi_WriteOneByte(FRAM_READD); //发送读取命令
 - Spi_WriteOneByte((u8)((ReadAddr&0xE0)>>8)); //发送16bit地址
 - Spi_WriteOneByte((u8)ReadAddr);
 - for(i=0;i<r_size;i++) //从地址地地址开始逐字节读出指定大小的数据
 - {
 - *pBuffer++=Spi_WriteOneByte(0xFF);
 - }
 - SPI_CS_HIGH; //取消片选
 - }
 
- 主函数
 
主函数中初始化指示灯、按键和SPI等外设,之后在主循环中扫描按键状态,并根据按键状态执行以下动作。
- KEY1按下:从地址0x0000开始连续写入256个字节数据。
 - KEY2按下:从地址0x0000开始连续读出256个字节数据。
 - KEY3按下:全片擦除FM25CL64B(FRAM是没有擦除操作的,这里所谓的擦除即向FRAM全片写入0x00)。
 
代码清单如下:
代码清单:主函数
- /**************************************************************************
 - 功能描述:主函数
 - 入口参数:无
 - 返 回 值:int类型
 - **************************************************************************/
 - int main(void)
 - {
 - u8 btn_val;
 - u16 i;
 - u8 j = 0;
 - P2M1 &= 0x3F; P2M0 &= 0x3F; //设置P2.6~P2.7为准双向口(指示灯D1和D2)
 - P7M1 &= 0xF9; P7M0 &= 0xF9; //设置P7.1~P7.2为准双向口(指示灯D4和D3)
 - P3M1 &= 0x3F; P3M0 &= 0x3F; //设置P3.6~P3.7为准双向口(按键KEY2和KEY1)
 - P0M1 &= 0x5F; P0M0 &= 0x5F; //设置P0.5,P0.7为准双向口(按键KEY4和KEY3)
 - P3M1 &= 0xFE; P3M0 &= 0xFE; //设置P3.0为准双向口(串口1的RxD)
 - P3M1 &= 0xFD; P3M0 |= 0x02; //设置P3.1为推挽输出(串口1的TxD)
 - uart1_init(); //串口1初始化
 - spi_init(); //初始化SPI
 - delay_ms(10); //初始化后适当延时
 - EA = 1; //使能总中断
 - while(1)
 - {
 - btn_val=buttons_scan(0); //获取开发板用户按键检测值,不支持连按
 - //按下KEY1:从地址0x0000开始连续写入256个字节数据
 - if(btn_val == BUTTON1_PRESSED)
 - {
 - j = 0;
 - for(i=0;i<256;i++)my_tx_buf[i] = j++;
 - //写入256个字节数据
 - Fram_Write_Bytes(my_tx_buf,0,256);
 - printf("Write data to fram!\r\n");
 - //指示灯D1状态翻转,指示操作完成
 - led_toggle(LED_1);
 - }
 - //按下KEY2:从地址0x0000开始连续读出256个字节数据
 - else if(btn_val == BUTTON2_PRESSED)
 - {
 - printf("Read data from fram!\r\n");
 - //读取写入的数据
 - Fram_Read_Bytes(my_rx_buf,0,256);
 - //串口打印读取的数据
 - for(i=0;i<256;i++)printf("%02bX ",my_rx_buf[i]);
 - printf("\r\n");
 - led_toggle(LED_2); //指示灯D2状态翻转,指示操作完成
 - }
 - //按下KEY3:全片擦除FRAM,即向FRAM全片写入0x00
 - else if(btn_val == BUTTON3_PRESSED)
 - {
 - led_on(LED_3); //点亮指示灯D3
 - printf("Clear fram!\r\n");
 - Fram_Clear_Bytes(0,256);
 - led_off(LED_3); //熄灭指示灯D3
 - }
 - }
 - }  
  
-  
    
-  
      
- 硬件连接
 
 
 -  
      
 
 -  
    
 
本实验需要使用LED指示灯、按键和艾克姆科技的铁电存储器(FM25CL64B)模块,按照下图所示安装铁电存储器模块和短接跳线帽。

图22:硬件连接
-  
  
-  
    
-  
      
- 实验步骤
 
 
 -  
      
 
 -  
    
 - 解压“…\第3部分:配套例程源码”目录下的压缩文件“实验2-13-3:硬件SPI读写铁电存储器(FM25CL64B)”,将解压后得到的文件夹拷贝到合适的目录,如“D\STC8”(这样做的目的是为了防止中文路径或者工程存放的路径过深导致打开工程出现问题)。
 - 双击“…\spi_fm25cl64b\project”目录下的工程文件“spi_fm25cl64b.uvproj”。
 - 点击编译按钮编译工程,编译成功后生成的HEX文件“spi_w25q128.hex”位于工程的“…\spi_fm25cl64b\Project\Object”目录下。
 - 打开STC-ISP软件下载程序,下载使用内部IRC时钟,IRC频率选择:24MHz。
 - 电脑上打开串口调试助手,选择开发板对应的串口号,将波特率设置为9600bps。
 
- 程序运行后,先按下KEY1按键,向FRAM写入数据:从地址0x0000开始连续写入256个字节数据。再按下KEY2按键,读出刚写入的数据并通过串口输出,如下图所示。
 

图23:串口接收的数据
- 按下KEY3按键,擦除FRAM(向FRAM全片写入0x00),之后再按下KEY2按键读取数据,可以看到读出的数据均为0x00。
 

图24:串口接收的数据
- 说明:模拟SPI读写FRAM存储器和模拟SPI读写W25Q128存储器的修改方法一样,这里不再赘述。我们也编写好了SPI读写FRAM存储器的例子,该例子在资料的“…\第3部分:配套例程源码”目录下,名称为“实验2-13-4:模拟SPI读写铁电存储器(FM25CL64B)”,读者在编写的过程中可以参考一下。
 





![XFF伪造 [MRCTF2020]PYWebsite1](https://img-blog.csdnimg.cn/direct/7cc699f2086e48e58ba1be392ee5669f.png)













