【STC8A8K64D4开发板】第2-13讲:SPI总线的应用

news2024/11/20 20:32:43

第2-13讲:SPI总线的应用

    1. 学习目的
  1. 了解SPI总线的结构、特点以及4种通信模式。
  2. 掌握通过SPI读、写和擦除SPI Flash  W25Q128的方法以及代码编写。
  3. 掌握通过SPI读、写铁电存储器FM25CL64B的方法以及代码编写。
    1. SPI总线原理

SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速、全双工、同步的通信总线。SPI是Motorola公司推出的一种同步串行接口技术,SPI由一个主设备和一个或多个从设备组成,在一次数据传输过程中,接口上只能有一个主机和一个从机能够通信。

SPI总线的优点是操作简单、数据传输速率较高、全双工,缺点是只支持单个主机、没有指定的流控制,没有应答机制确认是否接收到数据。

      1. 接口信号定义

SPI总线接口包括以下四种信号:

  1. MOSI(Master Output,Slave Input):主器件数据输出,从器件数据输入。
  2. MISO(Master Input, Slave Output):主器件数据输入,从器件数据输出。
  3. SCK(Serial Clock):有时也称为SCLK,时钟信号,由主器件产生。
  4. CS(Chip select):有时也称为SS,从器件使能信号,由主器件控制,实际使用时,经常用GPIO来代替。

SPI总线支持连接多个从机,如下图所示,SPI主机通过连接到从机的片选信号使能/禁止从机,并且同时只能使能一个从机,因此总线里面有多少个从机,就需要多少个片选信号。当SPI主机需要和总线中某个从机进行通信时,主机会拉低对应的CS信号使能该从机,之后发起通信,通信完成后,拉高CS信号,解除总线的占用。

图1:SPI总线结构

对于SPI总线,我们还需要能深刻理解下面几个知识点。

  1. 硬件片选和软件片选的区别

所谓硬件片选指的是SPI本身具有片选信号,当我们通过SPI发送数据时,SPI外设自动拉低CS信号使能从机,发送完成后自动拉高CS信号释放从机,这个过程是不需要软件操作的。而软件片选则是需要使用GPIO作为片选信号,SPI在发送数据之前,需要先通过软件设置作为片选信号的GPIO输出低电平,发送完成之后再设置该GPIO输出高电平。

  1. SPI总线是回环结构

SPI是一个环形总线结构,如下图所示,主设备和从设备构成一个环形。在时钟SCK的作用下,主设备发送一个位到从设备,因为是环形结构,所以从设备必定会同时传送一个位到主设备。同样,主设备向从设备发送一个字节,从设备也必定会同时传送一个字节到主设备。理解了环形结构,就很容易理解下面几点:

  1. SPI是全双工,同步的通信总线。
  2. 主设备向从设备发送数据时,无论我们需不需要从设备返回数据,从设备都会返回数据。
  3. 主设备从从设备读取一个字节数据时,为什么需要写一个字节数据到从设备:因为是环形结构,不写数据过去,对方的数据就不会被移位过来。

图2:SPI数据传输示意图

  1. SPI主机和从机之间连接时信号不需要交叉

SPI主机和从机连接时,MOSI和MISO信号是不需要交叉连接的,因为MOSI本身就表示了主机输出、从机输入,MISO表示主机输入、从机输出,因此不能交叉连接。

图3:MOSI和MISO不能交叉连接

      1. SPI的4种通信模式

SPI总线共有4种通信模式:模式0~模式3,这4种通信模式是由时钟相位和时钟极性确定的。

  1. 时钟相位CPOL(Clock polarity):SPI总线空闲时,时钟信号SCLK的电平称为时钟极性,有以下两种模式:
  1. CPOL=0:SPI总线空闲时,时钟信号为低电平。
  2. CPOL=1:SPI总线空闲时,时钟信号为高电平。
  1. 时钟极性CPHA(Clock phase):SPI在时钟信号SCLK第几个边沿开始采样数据,有以下两种模式:
  1. CPHA=0:在第1个时钟边沿进行数据采样。
  2. 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种模式的时序图如下。

  1. 时钟相位CPHA=0时的时序

图4:CPHA=0时SPI时序

  1. 时钟相位CPHA=1时的时序

图5:CPHA=0时SPI时序

    1. STC8A8K64D4的SPI应用步骤

STC8A8K64D4单片机片内集成了一个高速串行通信接口(SPI),SPI 是一种全双工的高速同步通信总线。STC8A8K64D4单片机的SPI支持主机和从机模式,通过配置寄存器,可以让SPI工作于主机模式或从机模式。

      1. 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

      1. 配置SPI工作参数

SPI工作参数通过“SPI 控制寄存器(SPCTL)”配置,配置项包括:SS引脚功能、SPI收发位序、主/从机、工作模式、速度以及SPI使能。

SPI 控制寄存器(SPCTL)

  1. SSIG:SS(片选)引脚功能控制位
  1. 0:SS引脚确定器件是主机还是从机。
  2. 1:忽略SS引脚功能,使用 MSTR 确定器件是主机还是从机。

通常,我们会将“SS(片选)引脚功能控制位”设置为1,即通过配置“MSTR”位来设置SPI工作于主机或是从机。

  1. SPEN:SPI 使能控制位
  1. 0:关闭 SPI 功能。
  2. 1:使能 SPI 功能。
  1. DORD:SPI 数据位发送/接收的顺序
  1. 0:先发送/接收数据的高位(MSB)。
  2. 1:先发送/接收数据的低位(LSB)。
  1. MSTR:器件主/从模式选择位

设置主机模式:

  1. 若 SSIG= 0,则 SS 管脚必须为高电平且设置 MSTR 为 1。
  2. 若 SSIG= 1,则只需要设置 MSTR 为 1(忽略 SS 管脚的电平)。

设置从机模式:

  1. 若 SSIG= 0,则 SS 管脚必须为低电平(与 MSTR 位无关)。
  2. 若 SSIG= 1,则只需要设置 MSTR 为 0(忽略 SS 管脚的电平)。
  1. CPOL:SPI 时钟极性控制
  1. 0:SCLK 空闲时为低电平,SCLK 的前时钟沿为上升沿,后时钟沿为下降沿。
  2. 1:SCLK 空闲时为高电平,SCLK 的前时钟沿为下降沿,后时钟沿为上升沿。
  1. CPHA: SPI 时钟相位控制
  1. 0:数据 SS 管脚为低电平驱动第一位数据并在 SCLK 的后时钟沿改变数据,前时钟沿采样数据(必须 SSIG= 0)。
  2. 1:数据在 SCLK 的前时钟沿驱动,后时钟沿采样。

由“CPOL”和“CPHA”可见,STC8A8K64D4的SPI支持SPI的4种通信模式。

  1. 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

      1. 数据收发

SPI的数据传输只能由主机启动,因为只有主机能够产生时钟信号。主机对SPI数据寄存器 SPDAT的写操作将启动 SPI 时钟发生器和数据的传输。在数据写入 SPDAT 之后的半个到一个 SPI位时间后,数据将出现在 MOSI 脚。写入主机 SPDAT 寄存器的数据从 MOSI 脚移出发送到从机的 MOSI脚,同时从机 SPDAT 寄存器的数据从 MISO 脚移出发送到主机的 MISO 脚。

传输完一个字节后,SPI 时钟发生器停止,传输完成标志( SPIF)置位,如果SPI中断使能则会产生一个SPI中断。主机和从机 CPU的两个移位寄存器可以看作是一个16位循环移位寄存器。当数据从主机移位传送到从机的同时,数据也以相反的方向移入。这意味着在一个移位周期中,主机和从机的数据相互交换。

    1. 硬件设计
      1. 外部存储器模块接口电路

为了方便扩展应用,开发板上设计了一个6芯的外扩存储器接口,其电路如下图所示。该接口可以接入艾克姆科技的W25Q128存储器模块、FM25CL64B铁电存储器模块和TF卡模块。

图6:外部存储器接口

      1. W25Q128(Flash)存储器模块

W25Q128(Flash)存储器模块是一款3.3V单电源供电、存储空间为128Mbit(16M字节)的串行Flash存储器模块,使用的存储器芯片型号为W25Q128。

W25Q128是华邦公司(Winbond)推出的串行NOR Flash系列存储器中的一员,该系列还有W25Q80/16/32/64等,W25Q128名称的意义如下:

  1. W:华邦公司(Winbond)。
  2. 25Q:SpiFlash串行NOR Flash,具有4KB扇区,双/四路I / O。
  3. 128F:128M-bit。
  4. 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通信时序以及读、写、擦除操作将在软件设计部分讲解。

      1. FM25CL64B(FRAM)模块
  1. 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的主要特点如下:

  1. 非易失性:写入的数据掉电不会丢失。
  2. 高读写耐久性:FRAM保证最多10万亿次写入,远远超过EEPROM的写入次数。
  3. 写入速度快:EEPROM和Flash写入数据之前都需要进行耗时的擦除操作,而FRAM不需要擦除,可以直接覆盖写入,这会节省大量的时间。另外,FRAM自身完成写操作的时间极短,程序中执行写操作时,甚至无需“判忙”。
  4. 低功耗:一方面,FRAM自身功耗低,另一方面,FRAM写入速度快,相对于EEPROM,写入同样数量的数据所需的时间更少,消耗的电流也会更少。

看到这里,读者可能会有疑惑,既然FRAM在性能上碾压EEPROM和Flash,为什么EEPROM和Flash仍在大量应用,而没有全部被FRAM取代?这是因为FRAM的制造成本高,价格比EEPROM、FLASH更加昂贵,产品设计时,不仅仅需要考虑性能,也需要考虑成本。另外FRAM的储存空间更小,如FM25CL64B的存储空间为8K字节,而W25Q128 Flash存储器的存储空间为16M字节,因此,应用中需要存储较大数据时,通常会选择Flash存储器。

  1. FM25CL64B铁电存储器模块

艾克姆科技的铁电存储器模块使用的FRAM芯片是FM25CL64B,FM25CL64B是非易失性的,并且像RAM一样执行读写,其特性如下。

  1. 工作电压范围:(2.7~3.65)V。
  2. 待机电流典型值为3uA。
  3. 1MHz时的工作电流为200 µA。
  4. 100万亿(1014)次读/写。
  5. 38年的数据保存时间。
  6. NoDelay™写操作。
  7. 频率高达20 MHz。
  8. 支持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通信时序以及读、写操作将在软件设计部分讲解。
    1. 软件设计
      1. 硬件SPI读写W25Q128存储器实验
  • 注:本节的实验是在“实验2-6-1:串口1数据收发实验”的基础上修改,本节对应的实验源码是:“实验2-13-1:硬件SPI读写W25Q128存储器”。
        1. 实验内容

将STC8A8K64D4单片机的SPI配置为主机,通过SPI总线访问W25Q128存储器,完成以下操作。

  1. 读取W25Q128芯片ID:通过读芯片ID可以判断芯片类型以及判断W25Q128是否正确接入。
  2. 扇区擦除:擦除一个指定的扇区。
  3. 全片擦除:擦除整个芯片。
  4. 页编程:向指定页面连续写入不超过页面地址范围的数据。
  5. 批量编程:向指定地址连续写入指定长度数据的功能,该功能实现了跨页写入。
  6. 批量读:从指定地址连续读取指定长度数据,并将读取的数据通过串口输出。

  • 注:本节的实验需要使用艾克姆科技的W25Q128存储器模块。

        1. 代码编写
  1. 新建一个名称为“w25q128.c”的文件及其头文件“w25q128.h”并保存到工程的“Source”文件夹,并将“w25q128.c”加入到Keil工程中的“SOURCE”组。
  2. 引用头文件

因为在“main.c”文件中使用了“w25q128.c”文件中的函数,所以需要引用下面的头文件“w25q128.h”。

代码清单:引用头文件

  1. //引用头文件  
  2. #include    "w25q128.h"  
  1. 初始化SPI

SPI初始化包含引脚配置、SPI工作模式配置、传输速率配置、SPI通信模式配置、收发数据的位序以及中断配置。

  1. 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引脚。

  1. SPI模式:主机。
  2. SPI传输速率:6Mbps。
  3. SPI通信模式:模式0。
  4. SPI收发数据的位序:高位在前(MSB),这是因为W25Q128存储器要求访问他的SPI主机发送数据时遵循MSB。
  5. 中断:本例中没有使用中断,采用的是查询方式收发数据。

对应的初始化代码清单如下。

代码清单:SPI初始化

  1. /************************************************************************************** 
  2.  * 描  述 : SPI初始化 
  3.  * 参  数 : 无 
  4.  * 返回值 : 无 
  5.  **************************************************************************************/  
  6. void spi_init(void)            
  7. {  
  8.    //设置P7.4~P7.7为准双向口,其中P7.5 P7.6 P7.7这3个引脚将作为SPI的MOSI MISO SCLK信号,P7.4将
  9.    //作为SPI的片选信号  
  10.    P7M1 &= 0x0F;  P7M0 &= 0x0F;       
  11.    SPI_CS_HIGH; //拉高SPI片选引脚,不选择从机(此时,连接的从机从SPI总线上断开)  
  12.    //设置SPI_S[1:0]的值为[1 0],使用P7.5 P7.6 P7.7作为SPI的MOSI MISO SCLK信号
  13.    P_SW1 &= ~0x0C;                  
  14.    P_SW1 |= 0x08;             
  15.    /*------------------------------------------------------------------- 
  16.    SSIG  SPEN  DORD  MSTR  CPOL  CPHA  SPR[1,0] 
  17.    1       1     0     1     0     0      00 
  18.    SSIG=1:忽略SS引脚功能,使用 MSTR 确定器件是主机还是从机 
  19.    SPEN=1:使能SPI 
  20.    DORD=0:发送/接收的顺序为高位在前 
  21.    MSTR=1:SPI为主机 
  22.    CPOL=0,CPHA=0:SPI工作于模式0
  23.    SPR[1,0]=00:SPI时钟频率选择:SYSclk/4 
  24.    -------------------------------------------------------------------*/  
  25.    SPCTL = 0xD0;   
  26.    //清零SPI中断标志位和写冲突标志位  
  27.    SPSTAT = SPIF | WCOL;                          
  1. SPI数据传输

SPI是按照字节来逐个发和收数据的,当然,一次SPI操作可以发送多个字节数据。下面的代码是我们封装的SPI传输函数,用于发/收一个字节数据。注意,这个函数是给其他SPI操作函数调用的,因为函数中不能操作片选信号。

代码清单:SP数据收发函数

  1. /************************************************************************************** 
  2.  * 描  述 : SPI发送一个字节数据,并返回一个字节数据 
  3.  * 参  数 : dat[in]:待写入的数据 
  4.  * 返回值 : SPI的MISO返回的数据 
  5.  **************************************************************************************/  
  6. static u8 Spi_WriteOneByte(u8 dat)  
  7. {  
  8.    SPDAT = dat;                 //触发SPI发送数据  
  9.    while (!(SPSTAT & SPIF));    //等待发送完成  
  10.    SPSTAT = SPIF | WCOL;        //清除SPI状态位  
  11.    return SPDAT;                //返回SPI数据  
  12. }  
  1. 读取W25Q128FV芯片ID

W25Q128芯片的ID固定为0xEF17,我们可以通过读取ID判断W25Q128是否在线,读取ID的时序如下图所示。读取ID执行的操作如下。

  1. 拉低片选信号CS,使能W25Q128。
  2. 发送扇区擦除命令0x90。
  3. 发送24位地址(读取ID时固定为0x000000),高地址在前。
  4. 发送2个字节数据(0xFF)读取ID,读取的ID是接收数组的最后2个字节。
  5. 拉高片选信号CS,释放W25Q128。

图11:读取ID时序

读取ID的函数代码清单如下:

代码清单:读取W25Q128的ID

  1. /************************************************************************************* 
  2.  * 描  述 : 读取W25Q128芯片的ID,W25Q128的ID:0xEF17,另:W25Q16的ID:0xEF14  W25Q32的
  3.              ID:0xEF15  W25Q64的ID:0xEF16   
  4.  * 参  数 : 无 
  5.  * 返回值 : id:芯片的ID  
  6. ***************************************************************************************/  
  7. u16 W25Q_Spi_ReadID(void)  
  8. {  
  9.    u16 id = 0;  
  10.    u8   mf_id,dev_id;  
  11.   
  12.    SPI_CS_LOW;                         //片选拉低,使能从机  
  13.    (void)Spi_WriteOneByte(W25_ReadID); //发送读取ID指令       
  14.    (void)Spi_WriteOneByte(0x00);       //发送24位地址,读ID时,24位地址为0x000000  
  15.    (void)Spi_WriteOneByte(0x00);        
  16.    (void)Spi_WriteOneByte(0x00);                   
  17.    mf_id =Spi_WriteOneByte(0xFF);      //读取 MANUFACTURER ID(MF7-MF0),值应为:0xEF  
  18.    dev_id =Spi_WriteOneByte(0xFF);     //读取 Device ID(ID7-ID0),值应为:0x17  
  19.    id = mf_id*256 + dev_id;            //将读取的MANUFACTURER ID和Device ID合并为16位  
  20.    SPI_CS_HIGH;                        //片选拉,断开从机  
  21.    return id;                          //返回读取的ID
  1. 擦除扇区:擦除指定的扇区
  1. 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的写使能。

  1. 拉低片选信号CS,使能W25Q128FV。
  2. 发送扇区擦除命令0x20。
  3. 发送扇区24位地址,高地址在前。
  4. 拉高片选信号CS,释放W25Q128FV。

命令发送完成并不表示W25Q128FV已经执行完成扇区擦除操作,因此命令发送完后还需要通过查询状态寄存器的BUSY位来判断擦除操作是否完成,当BUSY位的值为0时表示操作完成,W25Q128FV就绪(扇区擦除完成)。

图12:擦除扇区时序

根据W25Q128扇区擦除时序,编写代码如下:

代码清单:扇区擦除

  1. /************************************************************************************** 
  2.  * 描  述 : 擦除一个扇区,W25Q128最小的擦除单位是扇区.擦除一个扇区所需时间典型值为45ms,最大值为
  3.              400ms 
  4.  * 参  数 : SecAddr[in]:扇区地址 
  5.  * 返回值 : 无 
  6. ***************************************************************************************/  
  7. void W25Q_Erase_Sector(u32 SecAddr)   
  8. {  
  9.    while(W25Q_Spi_ReadStatus()&0x01);     // 判断是否忙     
  10.    W25Q_WriteEnable();                    // 写允许  
  11.    SPI_CS_LOW;                            //片选拉低,使能从机
  12.    Spi_WriteOneByte(W25_SectorErase);     // 写之前先擦除  
  13.    Spi_WriteOneByte((u8)(SecAddr>>16));   //先发送24位地址的高8位  
  14.    Spi_WriteOneByte((u8)(SecAddr>>8));    //再发送24位地址的中间8位  
  15.    Spi_WriteOneByte((u8)SecAddr);         //最后发送24位地址的低8位  
  16.    SPI_CS_HIGH;                           //片选拉,断开从机
  17.    while(W25Q_Spi_ReadStatus()&0x01);     // 等待擦除操作完成  
  18. }  
  1. 块擦除:擦除指定的块

块擦除指令将指定块(64K字节)内的所有内存设置为0xFF的擦除状态。在W25Q128接受块擦除指令之前,必须先发送“写使能”命令开启W25Q128的写使能。W25Q128块擦除时序如下图所示。

  1. 拉低片选信号CS,使能W25Q128。
  2. 发送块擦除命令0xD8。
  3. 发送扇区24位地址,高地址在前。
  4. 拉高片选信号CS,释放W25Q128。

命令发送完成并不表示W25Q128已经执行完成扇区擦除操作,因此命令发送完后还需要通过查询状态寄存器的BUSY位来判断擦除操作是否完成,当BUSY位的值为0时表示操作完成,W25Q128就绪(块擦除完成)。

图13:擦除块时序

根据W25Q128块擦除时序,编写的代码清单如下:

代码清单:块擦除

  1. /************************************************************************************** 
  2.  * 描  述 : 擦除一个块(64K) 
  3.  * 参  数 : BlockAddr[in]:块地址 
  4.  * 返回值 : 无 
  5. ***************************************************************************************/  
  6. void W25Q_Erase_Block(u32 BlockAddr)    
  7. {  
  8.    while(W25Q_Spi_ReadStatus()&0x01);       //判断是否忙     
  9.    W25Q_WriteEnable();                      //写允许  
  10.    SPI_CS_LOW;                              //片选拉低,使能从机  
  11.    Spi_WriteOneByte(W25_BlockkErase);       //写入扇区擦除命令  
  12.    Spi_WriteOneByte((u8)(BlockAddr>>16));   //先发送24位地址的高8位  
  13.    Spi_WriteOneByte((u8)(BlockAddr>>8));    //再发送24位地址的中间8位  
  14.    Spi_WriteOneByte((u8)BlockAddr);         //最后发送24位地址的低8位  
  15.    SPI_CS_HIGH;                             //片选拉高,断开从机  
  16.    while(W25Q_Spi_ReadStatus()&0x01);       //等待擦除完成  
  17. }  
  1. 全片擦除:擦除整个芯片

W25Q128全片擦除时序如下图所示,全片擦除执行的操作如下。注意全片擦除之前必须先发送“写使能”命令开启W25Q128的写使能。

  1. 拉低片选信号CS,使能W25Q128。
  2. 发送扇区擦除命令0xC7。
  3. 拉高片选信号CS,释放W25Q128。

全片擦除花费时间较长,典型时间是40秒,命令发送完后需要查询状态寄存器的BUSY位,直到BUSY位的值为0(全片擦除完成,W25Q128就绪)才可以执行其他操作。

图14:全片擦除时序

根据W25Q128全片擦除时序,编写的代码清单如下:

代码清单:全片擦除

  1. /************************************************************************************** 
  2.  * 描  述 : 全片擦除W25Q128,全片擦除所需的时间典型值为:40秒 
  3.  * 参  数 : 无 
  4.  * 返回值 : 无 
  5. ***************************************************************************************/  
  6. void W25Q_Erase_Chip(void)  
  7. {  
  8.    while(W25Q_Spi_ReadStatus()&0x01);     //判断是否忙     
  9.    W25Q_WriteEnable();                    //写允许  
  10.    SPI_CS_LOW;                               //片选拉低,使能从机
  11.    Spi_WriteOneByte(W25_ChipErase);       //写入全片擦除命令  
  12.    SPI_CS_HIGH;                           //从CS=1时开始执行擦除  
  13.    while(W25Q_Spi_ReadStatus()&0x01);     //等待擦除完成     
  14. }  
  1. 按页写:向指定页面连续写入不超过页面地址范围的数据

W25Q128FV扇区擦除时序如下图所示,扇区擦除执行的操作如下,注意擦除扇区之前必须先发送“写使能”命令开启W25Q128FV的写使能。

  1. 拉低片选信号CS,使能W25Q128FV。
  2. 发送页编程命令0x02。
  3. 发送24位地址,高地址在前。
  4. 发送写入到Flash的数据,注意最大写入的长度不能超过该地址所处页面的剩余空间。
  5. 拉高片选信号CS,释放W25Q128FV。

页编程时,数据传输完成并不表示W25Q128FV已经将接收的数据写入到自身的Flash内,因此数据传输完后还需要通过查询状态寄存器的BUSY位来判断编程是否完成,当BUSY位的值为0时表示编程完成,W25Q128FV就绪。

图15:页编程时序

根据W25Q128页编程时序,代码清单如下:

代码清单:页编程

  1. /************************************************************************************** 
  2.  * 描  述: 向指定的地址写入数据,最大写入的长度不能超过该地址所处页面的剩余空间 
  3.  * 参  数: *pBuffer[in]:指向待写入的数据 
  4.  *          WriteAddr[in]:写入的起始地址 
  5.  *          w_size[in]:写入的字节数 
  6.  * 返回值: 无  
  7. ***************************************************************************************/  
  8. void W25Q_Write_Page(u8 *pBuffer, u32 WriteAddr, u32 w_size)  
  9. {  
  10.    u16 i;  
  11.    while(W25Q_Spi_ReadStatus()&0x01);        //判断是否忙  
  12.    W25Q_WriteEnable();                       //写使能  
  13.    SPI_CS_LOW;                               //使能器件     
  14.    Spi_WriteOneByte(W25_PageProgram);        //发送写页命令  
  15.    Spi_WriteOneByte((u8)((WriteAddr)>>16));  //发送24bit地址     
  16.    Spi_WriteOneByte((u8)((WriteAddr)>>8));     
  17.    Spi_WriteOneByte((u8)WriteAddr);    
  18.    for(i=0;i<w_size;i++)                     //循环写入数据  
  19.    {  
  20.       Spi_WriteOneByte(*pBuffer++);        
  21.    }  
  22.    SPI_CS_HIGH;                              //取消片选  
  23.    while(W25Q_Spi_ReadStatus()&0x01);        //等待W25Q128自身编程完成     
  24. }  
  1. 批量编程:向指定地址连续写入指定长度数据的功能,该功能实现了跨页写入。

批量编程是在页编程的基础上,将编程的数据拆分进行多次页编程,从而实现跨页编程,也就是实现从任意地址开始写入任意长度的数据,当然,地址范围和编程的数据长度不能超过W25Q128的自身的限制。

代码清单:批量编程

  1. /************************************************************************************* 
  2.  * 描  述:向指定的地址写入指定大小的数据,支持跨页写入数。注:写入数据的地址所在的页面已经执行过擦除操作 
  3.  * 参  数:*pBuffer[in]:指向待写入的数据 
  4.  *         WriteAddr[in]:写入的起始地址 
  5.  *         w_size[in]:写入的字节数 
  6.  * 返回值:无 
  7. **************************************************************************************/  
  8. void W25Q_Write_Bytes(u8 * pBuffer,u32 WriteAddr,u32 w_size)  
  9. {  
  10.    u32 PageByteRemain = 0;  
  11.    //计算起始地址所处页面的剩余空间  
  12.    PageByteRemain = W25Q128_PAGE_SIZE - WriteAddr%W25Q128_PAGE_SIZE;  
  13.    //如果编程的数据长度不大于页面的剩余空间,编程数据长度等于w_size  
  14.    if(w_size <= PageByteRemain)  
  15.    {  
  16.       PageByteRemain = w_size;  
  17.    }  
  18.    //开始编程,直到所有的数据编程完成  
  19.    while(1)  
  20.    {  
  21.       //编程PageByteRemain个字节  
  22.       W25Q_Write_Page(pBuffer,WriteAddr,PageByteRemain);  
  23.       //如果起始地址所处页面的剩余空间足够存放写入的数据,执行写入后,写入结束,退出循环  
  24.       if(PageByteRemain == w_size)break;           
  25.       else  
  26.       {  
  27.          //取数据的地址增加PageByteRemain  
  28.          pBuffer += PageByteRemain;  
  29.          //计算编程写入的地址  
  30.          WriteAddr += PageByteRemain;  
  31.          //剩余待编程的数据大小  
  32.          w_size -= PageByteRemain;                
  33.          //计算下次编程的数据长度  
  34.          if(w_size > W25Q128_PAGE_SIZE)  
  35.          {  
  36.             PageByteRemain = W25Q128_PAGE_SIZE;  
  37.          }                   
  38.          else   
  39.          {  
  40.             PageByteRemain = w_size;           
  41.          }  
  42.       }  
  43.     }  
  1. 批量读:从指定地址连续读取指定长度数据,并将读取的数据通过串口输出

W25Q128支持连续读任意长度数据,甚至可以通过读命令一次将整个Flash内容读取出来,但是通常单片机内存无法存储这么大的数据,因此批量读取的时候往往也是分次读取的,读取的代码清单如下。

代码清单:批量读出数据

  1. /************************************************************************************** 
  2.  * 描  述: 从指定的起始地址连续读取指定长度的数据 
  3.  * 参  数: *pBuffer[in]:指向保存读出数据的缓存的起始地址 
  4.  *          ReadAddr[in]:读取数据的起始地址 
  5.  *          r_size[in]:读取的字节数 
  6.  * 返回值 : 无 
  7. ***************************************************************************************/  
  8. void W25Q_Read_Bytes(u8 * pBuffer,u32 ReadAddr,u32 r_size)     
  9. {  
  10.    u32 i;    
  11.    while(W25Q_Spi_ReadStatus()&0x01);       //判忙
  12.    SPI_CS_LOW;                              //使能器件     
  13.    Spi_WriteOneByte(W25X_ReadDATA8);        //发送读取命令     
  14.    Spi_WriteOneByte((u8)((ReadAddr)>>16));  //发送24bit地址     
  15.    Spi_WriteOneByte((u8)((ReadAddr)>>8));     
  16.    Spi_WriteOneByte((u8)ReadAddr);    
  17.    for(i=0;i<r_size;i++)                    //从地址地地址开始逐字节读出指定大小的数据  
  18.    {  
  19.       *pBuffer++=Spi_WriteOneByte(0xFF);         
  20.    }  
  21.    SPI_CS_HIGH;                             //取消片选              
  22. }   

  1. 主函数

主函数中初始化指示灯、按键和SPI等外设,接着读取W25Q128的ID,以此判断W25Q128模块是否正确安装到开发板。如果读取的ID数值为0xEF17,说明W25Q128模块安装正确,程序继续向下运行,否则,闪烁指示灯D1指示未检测到W25Q128。

程序进入主循环后,扫描按键状态,并根据按键状态执行以下动作。

  1. KEY1按下:按页写入,扇区0的第一页写入256个字节,之后读出数据并通过串口输出。
  2. KEY2按下:向起始地址100连续写入500个字节数据,之后读出数据并通过串口输出。这里演示了跨页的批量写,写入的数据占用了2个页面。
  3. KEY3按下:全片擦除W25Q128。

代码清单如下:

代码清单:主函数

  1. /************************************************************************** 
  2. 功能描述:主函数 
  3. 入口参数:无 
  4. 返 回 值:int类型 
  5. **************************************************************************/  
  6. int main(void)  
  7. {  
  8.    u8 btn_val;  
  9.    u16 i;  
  10.    u8 j = 0;  
  11.    u16 chip_id,test_len;  
  12.       
  13.    P2M1 &= 0x3F;   P2M0 &= 0x3F;     //设置P2.6~P2.7为准双向口(指示灯D1和D2)  
  14.    P7M1 &= 0xF9;   P7M0 &= 0xF9;     //设置P7.1~P7.2为准双向口(指示灯D4和D3)  
  15.    P3M1 &= 0x3F;   P3M0 &= 0x3F;     //设置P3.6~P3.7为准双向口(按键KEY2和KEY1)  
  16.    P0M1 &= 0x5F;   P0M0 &= 0x5F;     //设置P0.5,P0.7为准双向口(按键KEY4和KEY3)  
  17.    P3M1 &= 0xFE;   P3M0 &= 0xFE;     //设置P3.0为准双向口(串口1的RxD)  
  18.    P3M1 &= 0xFD;   P3M0 |= 0x02;     //设置P3.1为推挽输出(串口1的TxD)  
  19.       
  20.    uart1_init();                   //串口1初始化  
  21.    spi_init();                     //初始化SPI  
  22.    delay_ms(500);                  //初始化后适当延时  
  23.    EA = 1;                         //使能总中断  
  24.       
  25.    //通过读取w25q128芯片的ID判断w25q128模块是否正确安装到开发板,若读取ID不成功,指示灯D1持续闪烁
  26.    //提示读取ID失败  
  27.    chip_id = W25Q_Spi_ReadID();  
  28.   
  29.    while(chip_id != W25Q128_ID)  
  30.    {  
  31.       led_toggle(LED_1);  
  32.       delay_ms(1500);  
  33.    }  
  34.    printf("CHIP ID: %04hX\r\n",chip_id);    //串口打印出读取的ID  
  35.       
  36.    while(1)  
  37.    {  
  38.       btn_val=buttons_scan(0);              //获取开发板用户按键检测值,不支持连按  
  39.       //按下KEY1:测试按页写入。扇区0的第一页写入256个字节,之后读出数据并通过串口输出  
  40.       if(btn_val == BUTTON1_PRESSED)          
  41.       {  
  42.          test_len = 256;  
  43.          //写之前需要先执行擦除操作  
  44.          W25Q_Erase_Sector(0);     
  45.          for(i=0;i<test_len;i++)my_tx_buf[i] = j++;  
  46.          //写入256个字节数据,使用按页写入时,一次写入的数据长度不超过一个页面的剩余空间  
  47.          W25Q_Write_Page(my_tx_buf,0,test_len);            
  48.          //读取写入的数据  
  49.          W25Q_Read_Bytes(my_rx_buf,0,test_len);  
  50.          //串口打印读取的数据  
  51.          for(i=0;i<test_len;i++)printf("%02bX ",my_rx_buf[i]);      
  52.          //翻转指示灯D1状态,指示操作完成  
  53.          led_toggle(LED_1);                    
  54.       }  
  55.       //按下KEY2:批量写入和读出,向起始地址100连续写入500个字节数据,之后读出数据并通过串口输出  
  56.       else if(btn_val == BUTTON2_PRESSED)     
  57.       {  
  58.          test_len = 512;  
  59.          for(i=0;i<test_len;i++)my_tx_buf[i] = j++;  
  60.          //写入256个字节数据,使用按页写入时,一次写入的数据长度不超过一个页面的剩余空间  
  61.          W25Q_Write_Bytes(my_tx_buf,100,test_len);             
  62.          //读取写入的数据  
  63.          W25Q_Read_Bytes(my_rx_buf,100,test_len);  
  64.          //串口打印读取的数据  
  65.          for(i=0;i<test_len;i++)printf("%02bX ",my_rx_buf[i]);      
  66.          led_toggle(LED_2);               //控制用户指示灯D2翻转  
  67.       }   
  68.       //按下KEY3:全片擦除W25Q128,全片擦除所需的时间典型值为:40秒  
  69.       else if(btn_val == BUTTON3_PRESSED)     
  70.       {  
  71.          led_on(LED_3);               //点亮指示灯D3  
  72.          W25Q_Erase_Chip();           //全片擦除  
  73.          led_off(LED_3);              //熄灭指示灯D3  
  74.       } 
  75.     }  
        1. 硬件连接

本实验需要使用LED指示灯、按键和艾克姆科技的W25Q128存储器模块,按照下图所示安装W25Q128存储器模块和短接跳线帽。

图16:跳线帽短接

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

图17:串口接收的数据

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

图18:串口接收的数据

  1. 按下KEY3按键:全片擦除W25Q128,按下按键后指示灯D3点亮,指示灯开始全片擦除W25Q128存储器,擦除完成后指示灯D3熄灭,注意,全片擦除所需的时间较长,典型值为:40秒。
      1. 模拟SPI读写W25Q128存储器实验
  • 注:本节的实验是在“实验2-13-1:硬件SPI读写W25Q128存储器”的基础上修改,本节对应的实验源码是:“实验2-13-2:模拟SPI读写W25Q128存储器”。
        1. 实验内容

本实验实现的功能和“实验2-13-1:硬件SPI读写W25Q128存储器”完全一样,区别是本实验中不使用STC8A8K64D4单片机集成的硬件SPI外设,而是使用GPIO模拟SPI的协议时序完成SPI通信。

  • 注1:相对于硬件SPI,模拟SPI通信时,CPU需要频繁地操作IO,会占用更多的CPU资源,因此建议开发程序时优先考虑使用硬件SPI,当硬件SPI不够用时,再考虑使用模拟SPI。
  • 注2:本节的实验需要使用艾克姆科技的W25Q128存储器模块。
        1. 代码编写

模拟SPI的程序相对于硬件SPI的程序来说,主要修改点包括以下几个方面:

  1. 定义模拟SPI的信号引脚和相关的操作宏。
  2. SPI初始化函数:初始化函数无需再初始化SPI的寄存器,而是配置SPI所用的GPIO。
  3. SPI收发函数:模拟SPI时,我们习惯将发和收分开来操作,函数中通过操作GPIO来模拟SPI的时序实现SPI的发和收。

接下来,我们看一下具体的代码实现。

  1. 定义模拟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信号所用的引脚

  1. sbit    SPI_CS_PIN       = P7.4;          //定义CS    
  2. sbit    SPI_MOSI_PIN     = P7^5;          //定义MOSI  
  3. sbit    SPI_MISO_PIN     = P7^6;          //定义MISO  
  4. sbit    SPI_SCK_PIN      = P7^7;          //定义SCK
  5.   
  6. #define  SPI_CS_LOW      SPI_CS_PIN      = 0  //SPI片选引脚输出低电平  
  7. #define  SPI_CS_HIGH     SPI_CS_PIN      = 1  //SPI片选引脚输出高电平  
  8. #define  SPI_SCK_LOW     SPI_SCK_PIN     = 0  //SPI时钟信号引脚输出低电平  
  9. #define  SPI_SCK_HIGH    SPI_SCK_PIN     = 1  //SPI时钟信号引脚输出高电平  
  10. #define  SPI_MOSI_LOW    SPI_MOSI_PIN    = 0  //SPI主出从入引脚输出低电平  
  11. #define  SPI_MOSI_HIGH   SPI_MOSI_PIN    = 1  //SPI主出从入引脚输出高电平  

  1. SPI初始化函数

SPI初始化函数中将SPI的4个信号所用的GPIO全部配置为准双向口,接着拉高片选引脚,不使能SPI从机(W25Q128模块)。

对于时钟信号,要根据使用的SPI通信模式设置其状态,本例中使用的是通信模式0,时钟信号在空闲状态下为低电平,因此,需要将时钟信号对应的GPIO拉低。

代码清单:SPI初始化

  1. /************************************************************************************** 
  2.  * 描  述 : SPI初始化 
  3.  * 参  数 : 无 
  4.  * 返回值 : 无 
  5.  **************************************************************************************/  
  6. void spi_init(void)            
  7. {  
  8.    //设置P7.4~P7.7为准双向口,其中P7.5 P7.6 P7.7这3个引脚将作为SPI的MOSI MISO SCLK信号,P7.4将
  9.    //作为SPI的片选信号  
  10.    P4M1 &= 0xF0;  P4M0 &= 0xF0;       
  11.    SPI_CS_HIGH; //拉高SPI片选引脚,不选择从机(此时,连接的从机从SPI总线上断开)  
  12.    SPI_SCK_LOW; //拉低时钟线                   
  13. }  
  1. SPI收发函数

SPI收发函数中,每次发送和接收都是一个字节,收发过程中,通过操作SPI信号对应的GPIO模拟出SPI的时序,代码清单如下。

代码清单:SPI发送

  1. /************************************************************************************** 
  2.  * 描  述 : 软件模拟SPI时序发送一个字节数据,并返回一个字节数据 
  3.  * 参  数 : dat[in]:待写入的数据 
  4.  * 返回值 : 无 
  5.  **************************************************************************************/  
  6. static void Spi_WriteOneByte(u8 dat)  
  7. {  
  8.    u8 i, temp;  
  9.        
  10.    temp = dat;  
  11.    for(i=0; i<8; i++) //发送一个字节数据,循环8次   
  12.    {  
  13.       SPI_SCK_LOW;    //拉低时钟线  
  14.       spi_delay();    //延时  
  15.       if((temp&0x80) == 0x80)//最高为"1",则MOSI引脚输出高电平,否则输出低电平  
  16.       {  
  17.          SPI_MOSI_HIGH;  
  18.       }  
  19.       else  
  20.       {  
  21.          SPI_MOSI_LOW;  
  22.       }  
  23.       SPI_SCK_HIGH; //拉高时钟线  
  24.       spi_delay();  //延时  
  25.       temp <<= 1;   //第一位待发的位移到最高位  
  26.    }  
  27.    SPI_MOSI_LOW;    //拉低MOSI  
  28. }  

代码清单:SPI接收

  1. /**************************************************************************************  
  2.  * 描  述 : 软件模拟SPI时序读一个字节数据  
  3.  * 参  数 : 无  
  4.  * 返回值 : SPI读取一个字节数据  
  5.  **************************************************************************************/    
  6. static u8 Spi_ReadOneByte(void)    
  7. {    
  8.    u8 i, temp=0;    
  9.         
  10.    for(i=0; i<8; i++) //读取一个字节数据,循环8次    
  11.    {    
  12.       temp <<= 1;     //每个循环,左移一次,保存读取的位    
  13.       SPI_SCK_LOW;    //拉低时钟线    
  14.       spi_delay();      //延时     
  15.       if (SPI_MISO_PIN == 1)//若MISO为高电平,temp最低位置1    
  16.       {    
  17.          temp++;    
  18.       }    
  19.       SPI_SCK_HIGH;   //拉高时钟线    
  20.       spi_delay();      //延时            
  21.    }    
  22.    return temp;       //返回读取的数据    
  23. }    
        1. 硬件连接

同“实验2-13-1:硬件SPI读写W25Q128存储器”。

        1. 实验步骤

同“实验2-13-1:硬件SPI读写W25Q128存储器”。

      1. SPI读写铁电存储器(FRAM)实验
  • 注:本节的实验是在“实验2-13-1:硬件SPI读写W25Q128存储器”的基础上修改,本节对应的实验源码是:“实验2-13-3:硬件SPI读写铁电存储器(FM25CL64B)”。
        1. 实验内容

将STC8A8K64D4单片机的SPI配置为主机,通过SPI总线访问FM25CL64B铁电存储器模块,完成以下操作。

  1. 数据写入:向指定地址连续写入指定长度数据。
  2. 数据读取:从指定地址连续读取指定长度数据,并将读取的数据通过串口输出。

  • 注:本节的实验需要使用艾克姆科技的FM25CL64B铁电存储器模块。

        1. 代码编写
  1. 新建一个名称为“fm25cl64b.c”的文件及其头文件“fm25cl64b.h”并保存到工程的“Source”文件夹,并将“fm25cl64b.c”加入到Keil工程中的“SOURCE”组。
  2. 引用头文件

因为在“main.c”文件中使用了“fm25cl64b.c”文件中的函数,所以需要引用下面的头文件“fm25cl64b.h”。

代码清单:引用头文件

  1. //引用头文件  
  2. #include    " fm25cl64b.h"  
  1. 初始化SPI

SPI初始化配置中的引脚配置、SPI工作模式配置、传输速率配置、收发数据的位序以及中断配置和访问W25Q128的例子配置的完全一样,读者参阅“实验2-13-1:硬件SPI读写W25Q128存储器”中的SPI初始化代码即可。需要注意的是:FM25CL64B只支持SPI通信模式0和3,因此,SPI初始化代码中使用相对常用的SPI通信模式0。

  1. 写使能

FM25CL64B上电后是禁止写入的,在执行写入操作包括写入状态寄存器(WRSR)和写入存储器(WRITE)之前需要发送写使能(WREN)命令使能写入。写使能的操作步骤和时序如下。

  1. 拉低片选信号CS,使能FM25CL64B。
  2. 发送写使能命令0x06(WREN)。
  3. 拉高片选信号CS,释放FM25CL64B。

图19:写使能时序

根据写使能时序,编写的写使能函数代码清单如下。

代码清单:写使能

  1. /************************************************************************************** 
  2.  * 描  述 : 写使能 
  3.  * 参  数 : 无 
  4.  * 返回值 : 无 
  5.  **************************************************************************************/  
  6. static void Fram_WriteEnable (void)  
  7. {  
  8.    SPI_CS_LOW;                     //使能器件   
  9.    Spi_WriteOneByte(FRAM_WREN);    //发送写使能指令  
  10.    SPI_CS_HIGH;                    //取消片选  
  11. }  
  1. 写内存数据

FM25CL64B写内存数据操作的步骤和时序如下,写操作之前必须先发送“写使能”命令开启FM25CL64B的写使能。FM25CL64B写之前是无需擦除的,数据可以直接覆盖写入。

  1. 拉低片选信号CS,使能FM25CL64B。
  2. 发送写内存数据命令0x02(WRITE)。
  3. 发送13位地址,高地址在前。注意发送的地址是两个字节(16位),其中的高3位被忽略。
  4. 发送写入到FM25CL64B的数据,注意最大写入的数据长度不能超过该起始地址到内存结束地址能容纳的字节数。
  5. 拉高片选信号CS,释放FM25CL64B。

图20:写数据操作时序

根据写内存数据时序,编写的写内存数据的函数代码清单如下。

代码清单:写使能

  1. /************************************************************************************* 
  2.  * 描  述:以给定的地址为起始地址连续写入定大小的数据 
  3.  * 参  数:*pBuffer[in]:指向待写入的数据 
  4.  *         WriteAddr[in]:写入的起始地址 
  5.  *         w_size[in]:写入的字节数 
  6.  * 返回值:无 
  7. **************************************************************************************/  
  8. void Fram_Write_Bytes(u8 * pBuffer,u16 WriteAddr,u16 w_size)  
  9. {  
  10.    u16 i;  
  11.    Fram_WriteEnable();                                //写使能  
  12.    SPI_CS_LOW;  
  13.    (void)Spi_WriteOneByte(FRAM_WRITEE);               //发送写数据指令  
  14.    (void)Spi_WriteOneByte((u8)((WriteAddr&0xE0)>>8)); //发送16bit地址  
  15.    (void)Spi_WriteOneByte((u8)WriteAddr);  
  16.    for(i=0;i<w_size;i++)                              //连续写入数据   
  17.    {        
  18.       Spi_WriteOneByte(pBuffer[i]);                           
  19.    }      
  20.    SPI_CS_HIGH;       
  21. }  
  1. 写存储器数据

FM25CL64B读存储器数据操作的步骤和时序如下。

  1. 拉低片选信号CS,使能FM25CL64B。
  2. 发送读存储器数据命令0x03(READ)。
  3. 发送13位地址,高地址在前。注意发送的地址是两个字节(16位),其中的高3位被忽略。
  4. SPI通过“虚写”读出数据,每读出一个字节数据,只要主机继续发出时钟和片选为低,地址就在FM25CL64B内部自动递增,若地址达到最后一个地址0x01FFF,则自动返回0x 0000。
  5. 拉高片选信号CS,释放FM25CL64B。

图21:读存储器时序

根据读存储器数据时序,编写的读存储器数据的函数代码清单如下。

代码清单:读取数据

  1. /************************************************************************************** 
  2.  * 描  述: 从指定的起始地址开始连续读取指定长度的数据 
  3.  * 参  数: *pBuffer[in]:指向保存读出数据的缓存的起始地址 
  4.  *          ReadAddr[in]:读取数据的起始地址 
  5.  *          r_size[in]:读取的字节数 
  6.  * 返回值 : 无 
  7. ***************************************************************************************/  
  8. void Fram_Read_Bytes(u8 * pBuffer,u16 ReadAddr,u16 r_size)     
  9. {  
  10.    u32 i;    
  11.                                                       
  12.    SPI_CS_LOW;                                 //使能器件     
  13.    Spi_WriteOneByte(FRAM_READD);               //发送读取命令      
  14.    Spi_WriteOneByte((u8)((ReadAddr&0xE0)>>8)); //发送16bit地址  
  15.    Spi_WriteOneByte((u8)ReadAddr);    
  16.    for(i=0;i<r_size;i++)                        //从地址地地址开始逐字节读出指定大小的数据  
  17.    {  
  18.       *pBuffer++=Spi_WriteOneByte(0xFF);         
  19.    }  
  20.     SPI_CS_HIGH;                                //取消片选              
  21. }  
  1. 主函数

主函数中初始化指示灯、按键和SPI等外设,之后在主循环中扫描按键状态,并根据按键状态执行以下动作。

  1. KEY1按下:从地址0x0000开始连续写入256个字节数据。
  2. KEY2按下:从地址0x0000开始连续读出256个字节数据。
  3. KEY3按下:全片擦除FM25CL64B(FRAM是没有擦除操作的,这里所谓的擦除即向FRAM全片写入0x00)。

代码清单如下:

代码清单:主函数

  1. /************************************************************************** 
  2. 功能描述:主函数 
  3. 入口参数:无 
  4. 返 回 值:int类型 
  5. **************************************************************************/  
  6. int main(void)  
  7. {  
  8.    u8 btn_val;  
  9.    u16 i;  
  10.    u8 j = 0;  
  11.       
  12.    P2M1 &= 0x3F;   P2M0 &= 0x3F;     //设置P2.6~P2.7为准双向口(指示灯D1和D2)  
  13.    P7M1 &= 0xF9;   P7M0 &= 0xF9;     //设置P7.1~P7.2为准双向口(指示灯D4和D3)  
  14.    P3M1 &= 0x3F;   P3M0 &= 0x3F;     //设置P3.6~P3.7为准双向口(按键KEY2和KEY1)  
  15.    P0M1 &= 0x5F;   P0M0 &= 0x5F;     //设置P0.5,P0.7为准双向口(按键KEY4和KEY3)  
  16.    P3M1 &= 0xFE;   P3M0 &= 0xFE;     //设置P3.0为准双向口(串口1的RxD)  
  17.    P3M1 &= 0xFD;   P3M0 |= 0x02;     //设置P3.1为推挽输出(串口1的TxD)   
  18.       
  19.    uart1_init();                   //串口1初始化  
  20.    spi_init();                     //初始化SPI  
  21.    delay_ms(10);                   //初始化后适当延时  
  22.    EA = 1;                         //使能总中断  
  23.       
  24.    while(1)  
  25.    {  
  26.       btn_val=buttons_scan(0);      //获取开发板用户按键检测值,不支持连按  
  27.       //按下KEY1:从地址0x0000开始连续写入256个字节数据  
  28.       if(btn_val == BUTTON1_PRESSED)          
  29.       {  
  30.          j = 0;  
  31.          for(i=0;i<256;i++)my_tx_buf[i] = j++;  
  32.          //写入256个字节数据  
  33.          Fram_Write_Bytes(my_tx_buf,0,256);            
  34.          printf("Write data to fram!\r\n");  
  35.          //指示灯D1状态翻转,指示操作完成  
  36.          led_toggle(LED_1);                           
  37.       }  
  38.       //按下KEY2:从地址0x0000开始连续读出256个字节数据  
  39.       else if(btn_val == BUTTON2_PRESSED)     
  40.       {  
  41.          printf("Read data from fram!\r\n");  
  42.          //读取写入的数据  
  43.          Fram_Read_Bytes(my_rx_buf,0,256);  
  44.          //串口打印读取的数据  
  45.          for(i=0;i<256;i++)printf("%02bX ",my_rx_buf[i]); 
  46.          printf("\r\n");   
  47.          led_toggle(LED_2);               //指示灯D2状态翻转,指示操作完成  
  48.       }   
  49.       //按下KEY3:全片擦除FRAM,即向FRAM全片写入0x00  
  50.       else if(btn_val == BUTTON3_PRESSED)     
  51.       {  
  52.          led_on(LED_3);               //点亮指示灯D3  
  53.          printf("Clear fram!\r\n");  
  54.          Fram_Clear_Bytes(0,256);  
  55.          led_off(LED_3);              //熄灭指示灯D3  
  56.       }               
  57.    }  
        1. 硬件连接

本实验需要使用LED指示灯、按键和艾克姆科技的铁电存储器(FM25CL64B)模块,按照下图所示安装铁电存储器模块和短接跳线帽。

图22:硬件连接

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

图23:串口接收的数据

  1. 按下KEY3按键,擦除FRAM(向FRAM全片写入0x00),之后再按下KEY2按键读取数据,可以看到读出的数据均为0x00。

图24:串口接收的数据

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1466087.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI 视频 | Stable Video 开放公测了,免部署,免费使用!谁说 4 秒的 AI 视频不香?!

谁说 4 秒的视频不香&#xff1f;2.21 日&#xff0c;Stable Video 开放公测了&#xff0c;不需要自己部署了&#xff0c;直接在网页上就可以生成视频了。 下面这些视频&#xff0c;都是通过 Stable Video Diffusion 生成的&#xff0c;可以先来感受一下&#xff1a; Stable V…

JavaScript的内存管理与垃圾回收

前言 JavaScript提供了高效的内存管理机制&#xff0c;它的垃圾回收功能是自动的。在我们创建新对象、函数、原始类型和变量时&#xff0c;所有这些编程元素都会占用内存。那么JavaScript是如何管理这些元素并在它们不再使用时清理它们的呢&#xff1f; 在本节中&#xff0c;…

LED景观照明灯驱动电路串联、并联和恒流3款方案

LED景观照明灯是现代城市照明中常见的一种灯具。为了保证LED景观照明灯的正常工作&#xff0c;需要设计合适的驱动电路。LED景观照明灯的驱动电路可以采用串联、并联或恒流的方式来设计。 首先&#xff0c;串联驱动电路是指将多个LED灯串联在一起&#xff0c;然后接入电源进行…

不做内容引流,你凭什么在互联网上赚钱?

孩子们放寒假了&#xff0c;待在家里不是看电视&#xff0c;就是拿着手机刷视频&#xff0c;脸上是各种欢快和满足。只是一切换到写作业模式&#xff0c;孩子是各种痛苦表情包&#xff0c;家长则是使出浑身解数&#xff0c;上演亲子大战。可见娱乐常常让人愉悦&#xff0c;而学…

MyBatis-Plus框架(千峰学习笔记)

简介 MyBatis-Plus是一个 MyBatis的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;主要作用 为简化开发、提高效率。 我们的愿景是成为 MyBatis 最好的搭档&#xff0c;就像 魂斗罗 中的 1P、2P&#xff0c;基友搭配&#xff0c;效率翻倍。 特性 ● …

XFF伪造 [MRCTF2020]PYWebsite1

打开题目 直接查看源码 看到一个./flag.php 访问一下 购买者的ip已经被记录&#xff0c;本地可以看到flag&#xff0c;那么使用xff或者client-ip伪造一下ip试试 bp抓包 加一个X-Forwarded-For头 得到flag

无线局域网(WLAN)简单概述

无线局域网 无线局域网概述 无限局域网&#xff08;Wireless Local Area Network&#xff0c;WLAN&#xff09;是一种短距离无线通信组网技术&#xff0c;它是以无线信道为传输媒质构成的计算机网络&#xff0c;通过无线电传播技术来实现在空间传输数据。 WLAN是传输范围在1…

2、Web攻防-SQL注入-联合查询注入

用途&#xff1a;个人学习笔记&#xff0c;有所借鉴&#xff0c;欢迎指正&#xff01; 声明&#xff1a;只用于学习交流&#xff0c;点到为止&#xff0c;请勿非法测试。 概念&#xff1a; 联合查询注入&#xff1a;联合注入是回显注入的一种&#xff0c;也就是说联合注入的前…

关于Android下gralloc,hwcompoer以及surface模块的重新认识

关于Android下gralloc&#xff0c;hwcompoer以及surface模块的重新认识 引言 欠债还钱天经地义&#xff0c;知识的债也是如此&#xff01;这不必须得将我前面欠下来的债给补上&#xff01;对于任何复杂的知识点&#xff0c;我们都可以采用庖丁解牛的学习方式&#xff0c;一步步…

DSL Query基本语法

DSL Query基本语法 查询的基本语法如下&#xff1a; GET /indexName/_search {"query":{"查询类型":{"查询条件":"条件值"}} }查询所有 GET /indexName/_search {"query":{"match_all":{}} }match查询&#xf…

5分钟让你搞懂什么是Http协议

计算机网络基础课程是计算机专业方向非常重要的一门功课。 所有的互联网都通过网络协议来建立通信连接。 而http协议又是一种无状态的协议&#xff0c;也是工作中最常用的一种基于Web浏览器的网络通信协议。 如何学习http协议&#xff1f;提供三种方法供参考&#xff1a; 第…

分类预测 | Matlab实现KPCA-ISSA-LSSVM基于核主成分分析和改进的麻雀搜索算法优化最小二乘支持向量机故障诊断分类预测

分类预测 | Matlab实现KPCA-ISSA-LSSVM基于核主成分分析和改进的麻雀搜索算法优化最小二乘支持向量机故障诊断分类预测 目录 分类预测 | Matlab实现KPCA-ISSA-LSSVM基于核主成分分析和改进的麻雀搜索算法优化最小二乘支持向量机故障诊断分类预测分类效果基本描述程序设计参考资…

4.网络游戏逆向分析与漏洞攻防-游戏启动流程漏洞-模拟游戏登陆器启动游戏并且完成注入

内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;游戏启动流程的分析 码云地址&#xff08;master 分支&#xff09;&#xff1a;https://gitee.com/dye_your_fingers/titan 码云版本号&#xff1a;bcf7559184863febdcad819e48aaacad9f25d633 代码下…

第三百六十二回

文章目录 1. 概念介绍2. 使用方法3. 代码与效果3.1 示例代码3.2 运行效果 4. 内容总结 我们在上一章回中介绍了"如何创建垂直方向的Switch"相关的内容&#xff0c;本章回中将介绍SlideSwitch组件.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在…

在ubuntu20.04 上配置 qemu/kvm linux kernel调试环境

一&#xff1a;安装qemu/kvm 和 virsh qemu/kvm 是虚拟机软件&#xff0c;virsh是管理虚拟机的命令行工具&#xff0c;可以使用virsh创建&#xff0c;编辑&#xff0c;启动&#xff0c;停止&#xff0c;删除虚拟机。 &#xff08;1&#xff09;&#xff1a;安装之前&#xff0c…

九州金榜|家庭教育小技巧,孩子好习惯养成记

家庭教育对于孩子的发展至关重要&#xff0c;家长一定要重视孩子在家里的举动&#xff0c;要及时纠正孩子的不足&#xff0c;发展孩子的优良品德和教孩子养成勤俭朴素的的好习惯。九州金榜家庭教育将从以下方面说一下家庭教育中的方法技巧。 一、家长以身作则 家长教育孩子&a…

js设计模式:策略模式

作用: 根据不同的条件去进行相应的业务逻辑处理 就好比针对每种情况都制定对应的方案,触发条件就启动某项方案策略 示例: //策略对象const arrangeFun {model1:(value1,value2,value3,value4)>{return ${value1}${value2}${value3}:${value4}},model2:(value1,value2,va…

【Postgres】11、PROCEDURE 存储过程、FUNCTION 函数、使用方式和区别

文章目录 一、PROCEDURE1.1 语法1.2 描述1.3 参数1.4 示例 二、FUNCTION2.1 语法2.2 重载2.3 示例2.4 兼容性2.5 示例2.5.1 declare variable 定义变量2.5.2 declare、ARRAY、ANY2.5.2.1 ARRAY 和 ANY 三、其他3.1 PL/pgSQL 在PostgreSQL中&#xff0c;存储过程&#xff08;Pro…

Nginx跳转模块之rewrite

一.location与rewrite模块的区别 rewrite&#xff1a;对访问的域名或者域名内的URL路径地址重写 location&#xff1a;对访问的路径做访问控制或者代理转发 二.rewrite模块基本内容 1.功能 通过正则表达式的匹配来改变URI&#xff0c;可以同时存在一个或多个指令&#xff0c…

前端工程Bem架构及其封装

文章目录 简介语法在vue3项目中引用sass创建bem.scss文件修改vite.config.tsvue文件中使用结果 这是我学习记录的笔记&#xff0c;如有不正&#xff0c;欢迎补充 简介 首先认识一下什么是bem架构&#xff1f;BEM的意思就是块&#xff08;block&#xff09;、元素&#xff08;e…