【正点原子STM32】SPI总线协议(集成电路间通信接口标准、SPI工作原理、时钟极性(CPOL)和时钟相位(CPHA)、SPI相关寄存器和HAL库驱动、NOR FLASH基于SPI接口基本驱动步骤)

一、SPI介绍

二、SPI结构框图介绍

三、SPI工作模式介绍

四、SPI相关寄存器介绍
五、SPI相关HAL库驱动介绍
六、NOR FLASH介绍
6.1、NOR FLASH简介
6.2、NM25Q128简介
6.3、NM25Q128存储结构
6.4、NM25Q128常用指令
6.5、NM25Q128 读/擦除/写 步骤

七、NOR FLASH基本驱动步骤
八、编程实战
九、总结

一、SPI介绍

在这里插入图片描述
SPI(Serial Peripheral Interface)是一种常见的集成电路间通信接口标准,它允许一个主设备与一个或多个从设备之间进行全双工同步串行通信。SPI接口以其高速、灵活和简单易用的特性被广泛应用于嵌入式系统和众多电子设备中。

主要应用领域包括但不限于:

  1. 存储芯片:如EEPROM(电可擦除可编程只读存储器)、Flash(闪存)、SRAM(静态随机访问存储器)等。
  2. AD转换器(Analog-to-Digital Converters):用于将模拟信号转换为数字信号,常用于数据采集系统和传感器数据读取。
  3. LCD(Liquid Crystal Display)驱动器:用于控制液晶显示屏的内容显示,SPI接口可用于传输图像数据和控制命令。
  4. 数字信号处理器(DSP)和微控制器(MCU)之间、以及其他多种嵌入式系统组件之间的通信连接,如传感器、电机驱动器、通信模块等。

SPI接口通常由四条信号线组成,包括:

  • SCLK(Serial Clock):主设备提供的时钟信号,用于同步数据传输。
  • MOSI(Master Out, Slave In):主设备数据输出线,从设备数据输入线。
  • MISO(Master In, Slave Out):主设备数据输入线,从设备数据输出线。
  • SS/CS(Slave Select/Chip Select):从设备使能信号,用于选择要与之通信的从设备。

此外,某些SPI实现可能还包括额外的控制线或中断线,以便增强功能或优化通信。SPI的最大特点是它的高速同步通信能力,使其在需要高速数据传输和精确同步的场合尤为适用。

SPI 和 IIC

在这里插入图片描述
SPI和IIC(I²C)都是常见的串行通信接口协议,它们都有各自的特点、优缺点以及适合的应用场景:

SPI(Serial Peripheral Interface)

差异:
  • 通信方式:SPI是全双工通信,数据可以在两个方向上同时传输。
  • 总线结构:SPI支持多个从设备,但每个从设备需要独立的片选(CS)线,故连线数量随从设备增多而增加。
  • 时钟控制:SPI由主设备提供时钟信号,所有的从设备共享同一个时钟信号,且速率可调节。
  • 通信速率:SPI的速度通常比I²C更快,可达数Mbps到数十Mbps。
优点:
  • 高速:SPI能够实现更高的数据传输速率,适合大数据量、高速传输的应用。
  • 灵活:主设备可以单独控制每个从设备的数据传输,适合需要独立控制多个设备的场合。
缺点:
  • 线路复杂:每个从设备都需要独立的CS线,当从设备数量增加时,连线成本和布线难度增大。
  • 不具备仲裁机制:在多主设备环境下无法自行解决冲突,一般只能用于单主设备系统。
应用场景:
  • 大容量存储器(如闪存、RAM)读写。
  • 显示驱动器(如LCD、OLED)控制。
  • 高速AD/DA转换器的数据传输。
  • 实时数据采集系统。

I²C(Inter-Integrated Circuit)

差异:
  • 通信方式:I²C是半双工通信,同一时刻只能在一个方向上传输数据,但可以在数据传输间隙进行方向切换。
  • 总线结构:I²C仅需两条线(SDA和SCL),允许多个设备共享总线,采用广播式的地址寻址方式。
  • 时钟控制:I²C的时钟由主设备提供,所有设备共享同一时钟,且速率相对较慢。
  • 通信速率:I²C速率较低,典型速率从几kbps到几百kbps,现代高速I²C(Fast-mode Plus)可达到几Mbps。
优点:
  • 简单:只需要两条线路,简化了硬件设计和布线。
  • 半双工:虽然通信速率较低,但在某些应用中足以满足需求。
  • 内建仲裁:支持多主控模式,当多个主设备同时尝试通信时,可通过仲裁机制解决冲突。
缺点:
  • 速度较慢:相比于SPI,I²C的传输速率较低。
  • 总线负载能力有限:过多的设备挂在总线上会影响通信质量和可靠性。
应用场景:
  • 基本传感器(如温度、湿度、压力传感器)数据读取。
  • IC配置和控制(如EEPROM、PLL、音视频编解码器等)。
  • 低速外设控制(如键盘、触摸屏控制器)。

总结:
SPI更适合高速、实时性强、需要与多个独立从设备进行高速交互的场合,而I²C则适合低成本、低速、轻型设备互联和系统内部通信。两者在实际应用中可根据具体需求选择合适的接口标准。

SPI与I²C对比表

特性SPII²C
通信方式全双工半双工
总线结构每个从设备需要独立的片选线(CS)仅需两线(SDA和SCL),允许多设备共享总线
时钟控制主设备提供,所有从设备共享同一时钟同SPI,但速率相对较慢
通信速率较高,数Mbps到数十Mbps较低,典型几kbps到几百kbps,高速模式可达几Mbps
线路复杂度线路数量随从设备增加而增加仅需两线,简化硬件设计和布线
仲裁机制不支持多主控模式,不适合多主设备环境支持多主控模式,内建仲裁机制
应用场景大容量存储器读写、显示驱动器控制、高速AD/DA转换器传感器数据读取、IC配置与控制、低速外设控制

二、SPI结构框图介绍

在这里插入图片描述
SPI(Serial Peripheral Interface)结构框图通常包含以下几个关键组成部分:

  1. SPI相关引脚

    • MOSI(Master Output, Slave Input):主设备数据输出线,主设备向从设备发送数据。
    • MISO(Master Input, Slave Output):主设备数据输入线,从设备向主设备发送数据。
    • SCK(Serial Clock):SPI时钟信号线,由主设备产生并决定数据传输的速率和时序。
    • NSS/CS(Slave Select/Chip Select):从设备选择线,用于选择SPI总线上活动的从设备。
  2. 数据发送和接收模块

    • 缓冲区:用于暂存待发送或接收的数据。
    • 移位寄存器:在SPI通信过程中,数据通过移位寄存器逐位传输。
    • 引脚接口:将内部电路与SPI引脚连接,实现数据的物理传输。
  3. 时钟信号生成

    • SPI_CR1寄存器:在大多数嵌入式系统中,SPI的时钟频率、极性(CPOL)和相位(CPHA)等时序参数通常通过SPI控制器的控制寄存器(如SPI_CR1)进行配置。
  4. 主控制逻辑

    • SPI_CR1/2:这两个寄存器用于配置SPI的工作模式、数据大小、时钟速率、通信方向等属性。
    • SPI_SR(Status Register):SPI状态寄存器,用来监控SPI的当前工作状态,例如传输完成标志位、接收过载标志位、忙标志位等,以便于微控制器判断SPI传输是否完成或是否存在错误。

SPI的结构框图通常展示以上组件之间的连接关系和工作流程,清晰描绘了SPI通信的硬件架构和软件控制逻辑。
在这里插入图片描述
在这里插入图片描述

2.1、数据发送、数据接收

在这里插入图片描述
SPI数据发送与接收的过程可以这样描述:

数据发送过程

  1. 发送缓冲区:主机(Master)首先将待发送的数据加载到SPI控制器的发送缓冲区中。当缓冲区为空时,即TXE(Transmit Buffer Empty)标志位被置位,表明可以向缓冲区写入新数据。

  2. 移位寄存器:一旦数据被写入发送缓冲区,SPI控制器会在下一个时钟脉冲到来时将数据移位至移位寄存器中。随着SCK时钟信号的上升和下降沿,数据按位依次通过MOSI(Master Output, Slave Input)线传输给从机(Slave)。

  3. (地址&数据总线):实际上在SPI协议中,地址和数据通常是结合在一起连续传输的,不存在独立的地址总线。所有信息都通过MOSI线串行发送。

数据接收过程

  1. MISO发送DATA:从机通过其MISO(Master Input, Slave Output)线将数据串行发送回给主机。在每一个SCK时钟周期内,从机的移位寄存器中的数据位会转移到MISO线上。

  2. 移位寄存器:主机端的SPI控制器通过SCK同步接收这些数据,并将其存放在自身的移位寄存器中。

  3. 接收缓冲区:当接收到一个完整的数据字后,SPI控制器会将移位寄存器中的数据移入接收缓冲区中。当接收缓冲区中有新的数据可供读取时,RXNE(Receive Buffer Not Empty)标志位会被置位,提示主机可以从SPI接收缓冲区中读取接收到的数据。

总结来说,SPI数据的发送和接收都是通过共享的时钟信号和单向的数据线进行串行通信的,发送和接收操作可以同时进行,因此SPI具有全双工通信能力。在实际应用中,通过对SPI的状态寄存器进行监控,可以确保正确且高效地进行数据的发送和接收。

2.2、SPI 工作原理

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
SPI(Serial Peripheral Interface)工作原理基于主从模式,其核心在于主机(Master)控制通信过程,包括时钟信号的产生和数据传输的时序。以下是SPI工作原理的简要描述:

  1. 通信时序

    • SPI通信通常以字节(8位)为基本单位进行数据传输。
    • 主机通过SCK(Serial Clock)提供时钟信号,决定数据传输速率和时序,从机跟随这个时钟信号进行数据的采样和发送。
  2. 主机写操作

    • 当主机需要向从机写入数据时,主机首先通过MOSI(Master Out, Slave In)线将数据串行发送给从机。
    • 主机会先拉低从机片选线(NSS)以选中目标从机,然后通过SCK时钟信号的上升沿和下降沿驱动数据在MOSI线上逐位传输。
    • 在这种情况下,主机并不关心从机返回的数据,因为它没有通过MISO(Master In, Slave Out)线读取数据。
  3. 主机读操作

    • 若要进行读取操作,主机同样需要先拉低NSS选中从机,然后开始发送一个字节(可能是预设的空数据或指令)。
    • 即便这个字节对主机而言没有实际意义,但从机会在接收到这个字节的同时,通过MISO线回传数据给主机。
    • 主机通过监听MISO线上在SCK时钟信号驱动下的数据变化,接收从机返回的数据。

总结起来,SPI协议允许主机在单个时钟周期内同时进行写和读操作,实现了全双工通信。虽然在主机写操作时不关注从机的返回数据,但在进行读操作时,即使主机不关心从机的数据内容,也需要发送一个“触发”信号(通常是空数据或特定指令)来促使从机开始发送数据。

三、SPI工作模式介绍

在这里插入图片描述
SPI(Serial Peripheral Interface)有四种工作模式,这些模式由两个参数决定:时钟极性(CPOL)和时钟相位(CPHA)。下面是SPI四种工作模式的详细解释:

  1. 时钟极性(CPOL)

    • CPOL=0:在没有数据传输时,SCK(Serial Clock)时钟线处于低电平(逻辑0)状态。
    • CPOL=1:在没有数据传输时,SCK时钟线处于高电平(逻辑1)状态。
  2. 时钟相位(CPHA)

    • CPHA=0:数据在SCK的第一个边沿(上升沿或下降沿,取决于CPOL设置)被采样,数据在SCK的紧接着的第二个边沿被锁存到移位寄存器中。
    • CPHA=1:数据在SCK的第二个边沿被采样,数据在SCK的紧接着的第一个边沿被锁存到移位寄存器中。

结合CPOL和CPHA的不同设置,SPI共有四种工作模式:

  • 模式0:CPOL=0,CPHA=0。数据在SCK时钟的上升沿被采样,在下降沿被锁存。
  • 模式1:CPOL=0,CPHA=1。数据在SCK时钟的下降沿被采样,在上升沿被锁存。
  • 模式2:CPOL=1,CPHA=0。数据在SCK时钟的下降沿被采样,在上升沿被锁存。
  • 模式3:CPOL=1,CPHA=1。数据在SCK时钟的上升沿被采样,在下降沿被锁存。

每种模式都会影响数据与时钟信号的相对位置,但无论哪种模式,SPI通信的基本原理都是在主设备的控制下,通过共享的时钟信号,在每个时钟周期内逐位传输数据。

时钟极性(CPOL)、时钟相位(CPHA)

时钟极性(CPOL)和时钟相位(CPHA)是SPI通信协议中用来描述数据采样和时钟信号关系的两个重要参数。下面我们将详细讲解并通过一个象限图来分析这两种参数对SPI时序的影响。

时钟极性(CPOL)

  • CPOL=0:在这种模式下,SPI的时钟信号在没有数据传输时的空闲态(idle state)为低电平(Low)。也就是说,当没有数据交换时,SCK线为低电平。
  • CPOL=1:在这种模式下,SPI的时钟信号在没有数据传输时的空闲态为高电平(High)。即SCK线在不传输数据时保持高电平。

时钟相位(CPHA)

  • CPHA=0:在这种模式下,数据在SCK时钟信号的第一个有效边沿(上升沿或下降沿,取决于CPOL设置)被采样。数据在下一个时钟边沿被锁存到接收寄存器中。
  • CPHA=1:在这种模式下,数据在SCK时钟信号的第二个有效边沿被采样。数据在前一个时钟边沿被锁存到接收寄存器中。

现在,我们可以用一个二维象限图来表示这两种参数的不同组合:

       |   CPOL=0    |   CPOL=1
-------|-------------|-------------
CPHA=0 |   Mode 0    |   Mode 3
       |  (Sample at falling edge, latch at rising edge)  |
-------|-------------|-------------
CPHA=1 |   Mode 1    |   Mode 2
       |  (Sample at rising edge, latch at falling edge) |
  1. Mode 0(CPOL=0, CPHA=0)

    • 空闲时,SCK为低电平。
    • 数据在SCK的下降沿被采样,上升沿时数据被锁存。
  2. Mode 1(CPOL=0, CPHA=1)

    • 空闲时,SCK为低电平。
    • 数据在SCK的上升沿被采样,下降沿时数据被锁存。
  3. Mode 2(CPOL=1, CPHA=0)

    • 空闲时,SCK为高电平。
    • 数据在SCK的下降沿被采样,上升沿时数据被锁存。
  4. Mode 3(CPOL=1, CPHA=1)

    • 空闲时,SCK为高电平。
    • 数据在SCK的上升沿被采样,下降沿时数据被锁存。

综上所述,通过设定不同的CPOL和CPHA组合,可以实现SPI通信的四种不同的时序模式,以适应不同设备的需求。在实际应用中,必须确保主从设备间的SPI模式匹配,才能保证正确可靠的数据传输。
在这里插入图片描述
在这里插入图片描述

四、SPI相关寄存器介绍

在这里插入图片描述
SPI(Serial Peripheral Interface)是一种常用的串行通信协议,在嵌入式系统和微控制器中广泛使用。下面是对您提到的几个关键SPI寄存器的详细说明:

  1. SPI_CR1(SPI Control Register 1)

    • 这个寄存器主要用于设置SPI的工作模式、速度、极性和相位等基本操作参数。它通常包含以下部分功能位:
      • MSTR:主/从模式选择,决定SPI设备作为主设备还是从设备。
      • BR[2:0] 或类似字段:波特率预分频设置,用于确定SPI总线的时钟频率。
      • CPOLCPHA:用来设置时钟极性和相位,对应之前所描述的四种工作模式(Mode 0至Mode 3)。
      • SSMSSI:硬件/软件管理片选信号的选择及内建的NSS信号的状态。
      • LSBFIRST:字节内的数据传输顺序选择,低位先传(LSB-first)或高位先传(MSB-first)。
      • SPE:SPI外设使能位,用于开启或关闭SPI模块。
  2. SPI_SR(SPI Status Register)

    • SPI状态寄存器用于反映SPI接口的实时状态,包括但不限于:
      • TXE:Transmit Buffer Empty,发送缓冲器空标志,当SPI准备好接受新的待发送数据时置位。
      • RXNE:Receive Buffer Not Empty,接收缓冲器非空标志,当SPI接收到有效数据并存放在接收缓冲器中时置位。
      • BSY:Busy,忙标志,SPI正在进行数据传输时置位。
      • 可能还有其他标志如OVRE(Overrun Error)、CRCERR(CRC错误)等,用于指示SPI在数据传输过程中可能出现的各种异常状况。
  3. SPI_DR(SPI Data Register)

    • SPI数据寄存器是SPI发送和接收数据的核心单元,通常具有两个独立的功能区域:
      • TXDR(Transmit Data Register):向此寄存器写入数据将会启动SPI发送过程,一旦数据发送完毕,硬件会自动清除该位置的数据准备下一次发送。
      • RXDR(Receive Data Register):读取此寄存器会获取到SPI从外部设备接收到的数据。当RXNE标志置位时,表明有新数据可读。

通过配置和监控这些寄存器,开发人员能够有效地控制SPI接口的操作和数据传输。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
对于STM32H7系列微控制器来说,SPI相关的寄存器略有不同,但基本功能相似。以下是针对H7系列的SPI寄存器介绍:

  1. SPI时钟源选择

    • 寄存器:RCC_D2CCIP1R(RCC Domain 2 Configurable Clocks Peripherals 1 Register)
    • 功能:通过设置此寄存器,可以选择为SPI模块分配合适的时钟源。
  2. 使能SPI

    • 寄存器:SPI_CR1(SPI Control Register 1)
    • 功能:通过设置SPIEN位,可以开启或关闭指定SPI外设。
  3. 配置SPI工作参数

    • 寄存器:SPI_CFG1(SPI Configuration Register 1)

    • 功能:用于配置SPI的传输速率、帧格式等基本参数。

    • 寄存器:SPI_CFG2(SPI Configuration Register 2)

    • 功能:进一步配置SPI的工作模式(例如CPOL、CPHA等)、NSS管脚的行为(硬件或软件控制)、数据传输顺序(MSB或LSB优先)等。

  4. 选择SPI模式

    • 寄存器:SPI_I2SCFGR(SPI/I2S Configuration Register)
    • 注意:在H7系列中,某些SPI配置可能与其他系列有所不同,SPI_I2SCFGR寄存器用于选择SPI模式和其他相关配置。
  5. 查看SPI传输状态

    • 寄存器:SPI_SR(SPI Status Register)
    • 功能:通过检查此寄存器中的标志位,如TXERXNE等,可以了解到SPI当前的传输状态。
  6. 发送数据寄存器

    • 寄存器:SPI_TXDR(SPI Transmit Data Register)
    • 功能:向此寄存器写入数据时,数据会被放入发送FIFO队列中等待传输。
  7. 接收数据寄存器

    • 寄存器:SPI_RXDR(SPI Receive Data Register)
    • 功能:从该寄存器读取数据时,可以从接收FIFO队列中取出已接收到的数据。

在STM32H7系列中,SPI外设可能集成了FIFO(First-In-First-Out)缓冲区,从而提高了数据传输的效率和性能。

五、SPI相关HAL库驱动介绍

在这里插入图片描述
SPI相关HAL库驱动函数及其功能介绍:

  1. __HAL_RCC_SPIx_CLK_ENABLE(…)

    • 关联寄存器:RCC_APB2ENR(或者RCC_APB1ENR、RCC_AHBxENR等,取决于SPI位于哪个APB或AHB总线上)
    • 功能描述:该函数用于启用指定SPI外设的时钟,确保SPI模块能够正常工作。
  2. HAL_SPI_Init(…)

    • 关联寄存器:SPI_CR1(以及其他SPI配置寄存器如SPI_CFG1、SPI_CFG2、SPI_I2SCFGR等)
    • 功能描述:初始化SPI外设,设置SPI的工作模式、速度、极性、相位、数据位宽、NSS管理模式、第一边沿采样或第二边沿采样等参数。
  3. HAL_SPI_MspInit(…)

    • 功能描述:这是一个用户自定义的回调函数,用于初始化与SPI相关的外围硬件,比如GPIO引脚配置,确保SPI的SCK、MISO、MOSI和NSS等引脚被正确配置为SPI功能。
  4. HAL_SPI_Transmit(…)

    • 关联寄存器:SPI_DR(SPI数据寄存器)和SPI_SR(SPI状态寄存器)
    • 功能描述:向SPI外设发送数据,将数据写入SPI_TXDR寄存器,并等待发送完成。通过监控SPI_SR寄存器中的TXE和BTF(Buffer Transfer Finished)标志位来判断发送状态。
  5. HAL_SPI_Receive(…)

    • 关联寄存器:SPI_DR(SPI数据寄存器)和SPI_SR(SPI状态寄存器)
    • 功能描述:从SPI外设接收数据,通过SPI_RXDR寄存器获取接收到的数据,并等待接收完成。通过监控SPI_SR寄存器中的RXNE标志位来判断接收状态。
  6. HAL_SPI_TransmitReceive(…)

    • 关联寄存器:SPI_DR(SPI数据寄存器)和SPI_SR(SPI状态寄存器)
    • 功能描述:执行SPI的全双工通信,即同时发送和接收数据。数据发送和接收均通过SPI_DR寄存器,并监控SPI_SR寄存器中的相关标志位来确保操作完成。
  7. __HAL_SPI_ENABLE(…)

    • 关联寄存器:SPI_CR1(SPE位)
    • 功能描述:使能SPI外设,设置SPI_CR1寄存器中的SPE(SPI Enable)位,激活SPI总线以便进行数据传输。
  8. __HAL_SPI_DISABLE(…)

    • 关联寄存器:SPI_CR1(SPE位)
    • 功能描述:禁用SPI外设,清除SPI_CR1寄存器中的SPE位,暂停SPI总线的数据传输。
      在这里插入图片描述

对于STM32H7系列微控制器来说,除了基本的HAL库函数外,确实需要使HAL_RCCEx_PeriphCLKConfig函数来配置SPI的时钟源。这是因为H7系列拥有更丰富的时钟树结构,SPI时钟可以源自不同的振荡器或者PLL分频输出。

  • HAL_RCCEx_PeriphCLKConfig
    此函数用于配置外设的特定时钟源,对于SPI而言,它可以设置SPI外设使用的时钟源(比如PLLI2S、PLLQ、HSI、HSE等),通过此函数可以灵活选择最合适的SPI时钟频率。

此外,STM32F7和STM32H7系列还提供了额外的SPI功能选项:

  • NSS管理
    SPI_CR2寄存器中的NSSP位可以配置NSS信号是否由硬件还是软件管理,这在SPI作为主设备时特别有用。

  • CRC功能
    SPI_CR1和SPI_CR2寄存器中的相关位可以启用或禁用SPI的CRC(循环冗余校验)功能,用于数据包的错误检测。

  • IO交换功能
    对于某些应用,可能需要在SPI通信中交换MOSI和MISO引脚的功能,H7系列的SPI提供了IO交换功能,可以通过SPI_I2SCFGR寄存器中的IOFFSET位来配置。

在使用HAL库进行SPI驱动开发时,开发者可以根据实际项目需求,利用这些附加功能来优化SPI接口的性能和灵活性。

六、NOR FLASH介绍

6.1、NOR FLASH简介

XIP技术:https://blog.csdn.net/ffdia/article/details/87437872
在这里插入图片描述
NOR Flash 和 NAND Flash 的特点和应用举例:

NOR Flash

  • 特点

    • 支持随机访问,可以直接像读取内存一样通过独立的地址线和数据线进行字节级读取,不需要额外的解码电路。
    • 提供快速的读取速度,通常比NAND Flash快,特别适合于执行代码(XIP,eXecute-In-Place)的场合。
    • 擦除和写入是以区块(sector)为单位,但也可以支持单个字节的编程。
    • 通常没有坏块管理问题,出厂时质量较为均匀。
    • 容量相对较小,成本相对较高。
  • 应用举例

    • 存储程序代码,例如微控制器的Bootloader、嵌入式系统的固件、计算机BIOS等。
    • 快速读取要求较高的小型存储应用,如无线通信模块、工业控制设备中的程序存储。

NAND Flash

  • 特点

    • 不支持随机访问,读取操作需要通过I/O口间接进行,地址和数据共用总线。
    • 读取速度略低于NOR Flash,但写入和擦除速度较快,尤其是以页(page)为单位的大批量操作。
    • 通常以较大的块(block)为擦除单位,且存在坏块管理问题,需要专门的管理机制。
    • 成本较低,容量较大,体积紧凑,适合大规模数据存储。
  • 应用举例

    • 大容量存储设备,如嵌入式系统的文件系统、消费类电子产品中的数据存储、SSD(固态硬盘)。
    • 移动设备的内置存储,如智能手机和平板电脑的eMMC(Embedded MultiMedia Card)。
    • U盘、存储卡等便携式存储设备。

6.2、NM25Q128简介

在这里插入图片描述
NM25Q128是一款128Mb(16MB)容量的NOR型串行闪存芯片,专为需要高速、可靠数据存储的应用而设计。该器件通过SPI(Serial Peripheral Interface)接口与主控设备通信,提供了一种节省引脚、易于集成到系统中的存储方案。

  • 特点概述

    • 容量:128Mb,即16,777,216比特(约2MB)。
    • 擦写次数:具有很高的耐用性,可承受至少10万次的擦写周期。
    • 数据保存期长:数据可保存长达20年以上,体现了NOR Flash优异的数据保留性能。
    • SPI接口:支持SPI模式0(CPOL=0, CPHA=0)和模式3(CPOL=1, CPHA=1),适用于多种工作环境和系统设计需求。
    • 数据格式:采用8位数据宽度传输,遵循先发高位(MSB)再发低位(LSB)的传输规则。
    • 传输速度:标准模式下支持高达104M bit/s的数据传输速率,具有高效的读写性能。
  • 接口信号说明

    • CS(Chip Select):片选信号输入,用于选择NM25Q128作为SPI总线上的活动设备。
    • HOLD:暂停通讯信号,当该信号为低电平时,暂时停止SPI通信。
    • SO(Serial Output):数据输出引脚,从NM25Q128向主控设备传送数据。
    • CLK(Clock):时钟输入引脚,为主设备与NM25Q128之间的数据传输提供同步时钟信号。
    • WP(Write Protect):写保护功能引脚,用于禁止或允许对闪存进行写入操作。
    • SI(Serial Input):数据输入引脚,用于将数据从主设备写入NM25Q128。

总的来说,NM25Q128因其高密度、高速度、易用性和耐用性等特点,被广泛应用于嵌入式系统、固件存储、微控制器的代码存储、数据记录等多种场合。

6.3、NM25Q128存储结构

在这里插入图片描述
NM25Q128的存储结构按照地址范围、页、扇区和块进行组织,其具体划分如下:

  • 地址范围:NM25Q128的地址范围是从0x00000000到0x00FFFFFF,共计16,777,216个地址,对应128Mbit(16,777,216比特)或16MByte(16,777,216字节)的存储容量。

  • 存储单元划分

    • Page:NM25Q128的一个页面(Page)大小为256字节(1byte * 256)。
    • Sector:一个扇区(Sector)由16个连续的页面组成,因此一个扇区的大小为16 * 256字节 = 4096字节。
    • Block:一个块(Block)由16个连续的扇区组成,所以一个块的大小为16 * 4096字节 = 65,536字节(即64KB)。
  • 整体结构:整个NM25Q128由256个这样的块组成,因此其总容量为256 * 65,536字节 = 16,777,216字节(即16MByte)。

这种分层结构使得NM25Q128在进行数据管理和维护时具有良好的灵活性,最小擦除单位为一个扇区,数据读写则以字节为单位。在进行擦除操作时,必须以扇区为单位进行,这样有助于提升使用寿命并简化固件升级等操作。

6.4、NM25Q128常用指令

在这里插入图片描述
NM25Q128或者其他NOR Flash常用的指令还包括但不限于以下几种:

  • 指令(十六进制):0x06
    名称:写使能(Write Enable)
    作用:在对NOR Flash进行任何写入操作(包括页写和擦除)之前,必须先发送这条指令,使得NOR Flash进入写入允许状态。

  • 指令(十六进制):0x05
    名称:读状态寄存器1(Read Status Register 1)
    作用:用于读取NOR Flash的状态寄存器1,确认NOR Flash是否处于空闲状态,是否准备好进行下一步操作,特别是擦除操作之前需要确认设备是否已完成上一次操作。

  • 指令(十六进制):0x03
    名称:读数据(Read Data)
    作用:用于从NOR Flash中读取数据,通过指定地址读取连续的数据流。

  • 指令(十六进制):0x02
    名称:页编程(Page Program)
    作用:用于向NOR Flash的指定地址开始的连续256字节区域写入数据。注意,写入数据必须按照页的边界进行,不能跨越页的边界。

  • 指令(十六进制):0x20
    名称:扇区擦除(Sector Erase)
    作用:对NOR Flash中最小擦除单位(扇区)进行擦除操作,通常NM25Q128的一个扇区大小为4096字节(4KB)。

除此之外,还有其他的指令,例如:

  • 指令(十六进制):0xD8
    名称:块擦除(Bulk Erase)
    作用:擦除整个芯片的所有数据,对于NM25Q128来说,一次性擦除所有256个块。

  • 指令(十六进制):0xB0~0xBF
    名称:读取标识符(Read Manufacturer and Device ID)
    作用:读取制造商ID和设备ID,用于验证和识别具体的NOR Flash型号。

  • 指令(十六进制):0x04
    名称:写禁止(Write Disable)
    作用:结束写入操作序列,防止意外的写入动作发生,使NOR Flash退出写入允许状态。

6.5、NM25Q128 读/擦除/写 步骤

NM25Q128的读、擦除、写操作时序如下:

  1. 写使能(Write Enable)

    • 发送指令:0x06H
    • 作用:在执行写操作(如页写或擦除)之前,必须先发送该指令,使NOR Flash进入可写状态。
  2. 读状态寄存器1(Read Status Register 1)

    • 发送指令:0x05H
    • 作用:读取NOR Flash的状态寄存器1,检查设备是否处于就绪状态,以便进行下一步操作。
  3. 读数据时序(Read Data Bytes)

    • 发送指令:0x03H
    • 时序步骤:
      • 发送读指令(0x03H);
      • 发送要读取数据的起始地址;
      • 从数据线(SO)接收数据;
      • 循环上述步骤直到读取所需的所有数据。
  4. 页写时序(Page Program)

    • 发送指令:0x02H
    • 时序步骤:
      • 发送写使能指令(0x06H);
      • 发送页写指令(0x02H);
      • 发送要写入数据的目标地址;
      • 将最多256字节的数据通过数据线(SI)发送到NOR Flash;
      • 等待写操作完成,可通过读状态寄存器来确认。
  5. 扇区擦除时序(Sector Erase)

    • 发送指令:0x20H
    • 时序步骤:
      • 发送写使能指令(0x06H);
      • 发送扇区擦除指令(0x20H);
      • 发送要擦除扇区的起始地址(地址应对齐到扇区边界);
      • 等待擦除操作完成,可通过读状态寄存器来确认。

在实际操作过程中,还需注意地址和数据的传输格式以及相关的时钟同步,确保严格按照器件数据手册的要求进行操作。此外,某些操作完成后,建议发送写禁止指令(0x04H)以防止意外的写操作。
在这里插入图片描述
在执行诸如Page Program(页写)、Sector Erase(扇区擦除)、Block Erase(块擦除)和Chip Erase(整片擦除)以及Write Status Register(写状态寄存器)等操作之前,必须先向NOR Flash发送Write Enable(写使能)指令(0x06H)。这个指令的作用是让NOR Flash进入可写状态,只有在写使能之后,才能进行后续的写入或擦除操作。

当写使能指令发出后,NOR Flash内部的相关控制位会被设置,允许接下来的写入或擦除指令被执行。完成相应的写入或擦除操作后,通常需要发送Write Disable(写禁止)指令以取消写使能状态,防止在不需要写入的时候误操作导致数据损坏。写禁止指令通常为0x04H。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
NM25Q128的读操作步骤可以细化如下:

  1. 发送读命令

    • 向SPI总线的MOSI(数据输入)线上发送0x03H(读数据指令)。
  2. 发送地址

    • NM25Q128地址范围是0x000000到0xFFFFFF,总共24位。
    • 地址需要按照小端模式(Least Significant Bit First,LSB)分三次发送,每次8位。例如,要读取地址0x123456,则应按以下顺序发送地址字节:
      • 第一次发送最低8位(0x56)。
      • 第二次发送中间8位(0x34)。
      • 第三次发送最高8位(0x12)。
  3. 读取数据

    • 在地址发送完毕后,发送一个无效数据字节(通常是0xFF)到SPI总线的MOSI线上,此时NM25Q128开始在MISO(数据输出)线上输出请求地址处的数据。
    • 接收从NM25Q128返回的第一个数据字节,并且只要保持SPI时钟(SCK)的持续脉冲,就可以连续读取数据,直至所需数据全部读取完毕。

总之,NM25Q128的读操作流程是先发送读指令,接着发送目标地址,然后通过发送无效数据字节触发并连续读取存储器中的数据。在整个读取过程中,务必确保时钟信号的正确同步,以保证数据的准确传输。
在这里插入图片描述
NM25Q128擦除扇区的具体步骤如下:

  1. 发送写使能命令

    • 向SPI总线的MOSI(数据输入)线上发送0x06H(写使能命令),使NOR Flash进入可写状态。
  2. 等待空闲

    • 读取并检查状态寄存器(通常通过发送0x05H指令读取),确保NOR Flash已经完成之前的任何操作,并且处于空闲状态。只有在空闲状态下,才能进行下一步的擦除操作。
  3. 发送擦除扇区命令

    • 向SPI总线的MOSI线上发送0x20H(扇区擦除命令),通知NOR Flash即将进行扇区擦除操作。
  4. 发送地址

    • 根据要擦除的扇区地址,将24位地址按照小端模式(LSB优先)分三次发送到SPI总线的MOSI线上。地址应对齐到扇区边界,即扇区的起始地址。
  5. 等待空闲

    • 在发送完擦除命令和地址后,需要再次等待NOR Flash完成擦除操作并回到空闲状态。通常通过轮询读取状态寄存器来判断是否完成,状态寄存器中的相应位变为适当状态时,表示擦除操作完成。

需要注意的是,整个擦除操作期间需要确保时钟信号SCK的正确同步,并且在擦除操作结束后,最好发送写禁止命令(0x04H)以避免意外的写操作。此外,在实际应用中,请参照NM25Q128的具体数据手册,遵循正确的时序和操作规范。
在这里插入图片描述
NM25Q128写操作步骤(此处假设目标区域已经经过擦除):

  1. 擦除扇区(20H)

    • 在写入数据前,需要确保目标地址所在的扇区已经被擦除。如果目标地址的数据尚未擦除,可参照之前提供的擦除扇区步骤进行操作。
  2. 发送写使能命令

    • 向SPI总线的MOSI(数据输入)线上发送0x06H(写使能命令),使NOR Flash进入可写状态。
  3. 发送页写命令

    • 向SPI总线的MOSI线上发送0x02H(页写命令),通知NOR Flash即将进行页写操作。
  4. 发送地址

    • 根据要写入数据的目标地址,将24位地址按照小端模式(LSB优先)分三次发送到SPI总线的MOSI线上。
  5. 发送数据

    • 在发送完地址后,立即通过SPI总线的MOSI线将最多256字节的数据写入到NOR Flash中。注意数据应按照字节顺序逐个发送,且必须连续写入整个一页。
  6. 等待空闲

    • 在发送完所有数据后,需要等待NOR Flash完成写入操作并返回到空闲状态。可以通过轮询读取状态寄存器来判断写入是否完成,当状态寄存器中的相应位变为适当状态时,表示写入操作完成。

在实际应用中,一定要确保写入数据前目标区域已被擦除,并且在写入操作结束后,推荐发送写禁止命令(0x04H)以避免不必要的写操作。同时,严格遵守NM25Q128的数据手册中规定的时序要求。

七、NOR FLASH基本驱动步骤

在这里插入图片描述
NOR FLASH基于SPI接口的基本驱动步骤可以概括为以下几点:

  1. SPI工作参数配置初始化

    • 使用HAL_SPI_Init()函数对SPI外设进行初始化,设置工作模式(CPOL和CPHA)、时钟速度、数据帧格式(8位、16位等)、NSS管理方式等参数。
  2. 使能SPI时钟和初始化相关引脚

    • 首先,通过__HAL_RCC_SPIx_CLK_ENABLE()函数启用SPI外设的时钟。
    • 其次,对于STM32等微控制器,需要配置SPI接口相关的GPIO引脚为复用推挽输出模式或其他合适的模式,这部分操作通常不在HAL_SPI_MspInit()函数内完成,而是在初始化SPI之前手动配置GPIO的AF(Alternate Function)寄存器。
    // 示例代码(伪代码)
    GPIO_InitStruct.Pin = GPIO_PIN_...; // 设置SPI对应引脚
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 设置为复用推挽输出模式
    GPIO_InitStruct.Pull = GPIO_NOPULL; // 如果不需要上下拉电阻,设置为无上拉下拉
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; // 设置GPIO速度
    GPIO_InitStruct.Alternate = GPIO_AF_SPIx; // 设置对应SPI外设的复用功能
    HAL_GPIO_Init(GPIOx, &GPIO_InitStruct); // 初始化GPIO引脚
    
  3. 使能SPI

    • 使用__HAL_SPI_ENABLE(&hspi)函数使能SPI外设,这里的hspi是指向SPI_HandleTypeDef结构体的指针。
  4. SPI传输数据

    • 发送数据:使用HAL_SPI_Transmit(&hspi, tx_buffer, size, timeout)函数发送数据,tx_buffer是发送缓冲区,size是发送数据的长度,timeout是超时时间。
    • 接收数据:使用HAL_SPI_Receive(&hspi, rx_buffer, size, timeout)函数接收数据,rx_buffer是接收缓冲区。
    • 同时发送和接收:使用HAL_SPI_TransmitReceive(&hspi, tx_buffer, rx_buffer, size, timeout)函数执行全双工传输。
  5. 设置SPI传输速度

    • 根据系统要求,操作SPI_CR1寄存器(或相关的时钟配置寄存器)中的波特率控制位来设置SPI通信的时钟频率。在HAL库中,通常在HAL_SPI_Init()函数里通过配置SPI_InitTypeDef结构体中的BaudRatePrescaler字段来间接设置SPI时钟速度。

请注意,以上步骤仅为基本流程指导,具体实现细节会因微控制器系列和HAL库版本的不同而有所差异,务必查阅对应MCU的手册和HAL库API文档。
在这里插入图片描述
NM25Q128 NOR Flash的驱动步骤可以归纳为以下详细步骤:

  1. 初始化片选引脚与SPI接口

    • 配置NM25Q128对应的片选引脚为GPIO输出模式,并将其设置为高电平(通常意味着SPI处于非选通状态)。
    • 初始化SPI接口:
      • 配置SPI的工作模式(根据NM25Q128支持的SPI模式0或模式3)。
      • 设置数据传输位数(通常是8位)。
      • 分配SPI时钟源并设置分频系数以得到合适的SPI时钟频率。
      • 根据需求设置数据传输顺序(MSB或LSB优先)。
      • 使用HAL库函数如HAL_SPI_Init()进行初始化。
  2. NM25Q128 读取

    • 发送读取指令(0x03)。
    • 发送24位地址(地址对齐到读取粒度)。
    • 通过SPI接口读取数据,可以循环读取直到需要的数据量接收完毕。
  3. NM25Q128 扇区擦除

    • 发送写使能指令(0x06)。
    • 确保设备处于空闲状态,可以通过读取状态寄存器来判断。
    • 发送扇区擦除指令(0x20)。
    • 发送24位地址(地址对齐到扇区边界)。
    • 等待设备完成擦除操作并返回空闲状态。
  4. NM25Q128 写入

    • 可选:如果目标地址所在的扇区尚未擦除,则先执行扇区擦除。
    • 发送写使能指令(0x06)。
    • 发送页写指令(0x02)。
    • 发送24位地址(地址对齐到页边界)。
    • 发送要写入的数据,每次最多256字节(一个页)。
    • 确保所有数据发送完毕,并等待设备返回空闲状态。

NOR FLASH驱动注意事项:

  1. 是否需要擦除:在向NOR Flash写入数据前,必须确保目标地址所在的区域已经被擦除,因为NOR Flash只能向已擦除的区域写入数据,且是以块(扇区/页)为单位进行擦除操作。
  2. 写入数据
    • 换页:如果需要写入的数据跨越了页的边界,必须分开多次写入操作,每次写入不超过一个页的数据量。
    • 换扇区:同样的,如果跨越了扇区边界,也需确保对相邻扇区做相应的擦除操作。
  3. 遵循读、改、写的原则:在修改已存在的数据时,通常需要先读取原始数据,更改需要改变的部分,然后覆盖写入整个块(或页)。这是因为在NOR Flash中不能直接修改已写入的数据,只能通过擦除后再写入的方式更新数据。

在实际应用中,应根据NM25Q128的数据手册和所使用的HAL库函数来精确实施上述步骤。

八、编程实战

在这里插入图片描述
spi.c

#include "./BSP/SPI/spi.h"

SPI_HandleTypeDef g_spi2_handler; /* SPI2句柄 */

void spi2_init(void)
{
    g_spi2_handler.Instance                 = SPI2_SPI;                     /* SPI2 */
    g_spi2_handler.Init.Mode                = SPI_MODE_MASTER;              /* 设置SPI模式(主机模式) */
    g_spi2_handler.Init.Direction           = SPI_DIRECTION_2LINES;         /* 设置SPI工作方式(全双工) */
    g_spi2_handler.Init.DataSize            = SPI_DATASIZE_8BIT;            /* 设置数据格式(8bit长度) */
    g_spi2_handler.Init.CLKPolarity         = SPI_POLARITY_HIGH;            /* 设置时钟极性(CPOL = 1) */
    g_spi2_handler.Init.CLKPhase            = SPI_PHASE_2EDGE;              /* 设置时钟相位(CPHA = 1) */
    g_spi2_handler.Init.NSS                 = SPI_NSS_SOFT;                 /* 设置片选方式(软件片选,自定义GPIO) */
    g_spi2_handler.Init.BaudRatePrescaler   = SPI_BAUDRATEPRESCALER_256;    /* 设置SPI时钟波特率分频(256分频) */
    g_spi2_handler.Init.FirstBit            = SPI_FIRSTBIT_MSB;             /* 设置大小端模式(MSB高位在前) */
    g_spi2_handler.Init.TIMode              = SPI_TIMODE_DISABLE;           /* 设置帧格式(关闭TI模式) */
    g_spi2_handler.Init.CRCCalculation      = SPI_CRCCALCULATION_DISABLE;   /* 设置CRC校验(关闭CRC校验) */
    g_spi2_handler.Init.CRCPolynomial       = 7;                            /* 设置CRC校验多项式(范围:1~65535) */

    HAL_SPI_Init(&g_spi2_handler);
}

/**
 * @brief       SPI2底层驱动,时钟使能,引脚配置
 *   @note      此函数会被HAL_SPI_Init()调用
 * @param       hspi:SPI句柄
 * @retval      无
 */
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
    SPI2_SPI_CLK_ENABLE();
    
    GPIO_InitTypeDef gpio_init_struct;

    if (hspi->Instance == SPI2_SPI)
    {
        SPI2_SCK_GPIO_CLK_ENABLE();     /* SPI2_SCK脚时钟使能 */
        SPI2_MISO_GPIO_CLK_ENABLE();    /* SPI2_MISO脚时钟使能 */
        SPI2_MOSI_GPIO_CLK_ENABLE();    /* SPI2_MOSI脚时钟使能 */

        /* SCK引脚模式设置(复用输出) */
        gpio_init_struct.Pin = SPI2_SCK_GPIO_PIN;
        gpio_init_struct.Mode = GPIO_MODE_AF_PP;
        gpio_init_struct.Pull = GPIO_PULLUP;
        gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(SPI2_SCK_GPIO_PORT, &gpio_init_struct);

        /* MISO引脚模式设置(复用输出) */
        gpio_init_struct.Pin = SPI2_MISO_GPIO_PIN;
        HAL_GPIO_Init(SPI2_MISO_GPIO_PORT, &gpio_init_struct);

        /* MOSI引脚模式设置(复用输出) */
        gpio_init_struct.Pin = SPI2_MOSI_GPIO_PIN;
        HAL_GPIO_Init(SPI2_MOSI_GPIO_PORT, &gpio_init_struct);
    }
}

uint8_t spi2_read_write_byte(uint8_t data)
{
    uint8_t rec_data = 0;
    
    HAL_SPI_TransmitReceive(&g_spi2_handler, &data, &rec_data, 1, 1000);
    
    return rec_data;
}

spi.h

#ifndef __SPI_H
#define __SPI_H

#include "./SYSTEM/sys/sys.h"


/******************************************************************************************/
/* SPI2 引脚 定义 */

#define SPI2_SCK_GPIO_PORT              GPIOB
#define SPI2_SCK_GPIO_PIN               GPIO_PIN_13
#define SPI2_SCK_GPIO_CLK_ENABLE()      do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

#define SPI2_MISO_GPIO_PORT             GPIOB
#define SPI2_MISO_GPIO_PIN              GPIO_PIN_14
#define SPI2_MISO_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

#define SPI2_MOSI_GPIO_PORT             GPIOB
#define SPI2_MOSI_GPIO_PIN              GPIO_PIN_15
#define SPI2_MOSI_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

/* SPI2相关定义 */
#define SPI2_SPI                        SPI2
#define SPI2_SPI_CLK_ENABLE()           do{ __HAL_RCC_SPI2_CLK_ENABLE(); }while(0)    /* SPI2时钟使能 */

/******************************************************************************************/

void spi2_init(void);
uint8_t spi2_read_write_byte(uint8_t data);

#endif

norflash.c

#include "./BSP/SPI/spi.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/NORFLASH/norflash.h"


void norflash_init(void)
{
    NORFLASH_CS_GPIO_CLK_ENABLE();      /* NORFLASH CS脚 时钟使能 */

    GPIO_InitTypeDef gpio_init_struct;
    gpio_init_struct.Pin = NORFLASH_CS_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_init_struct.Pull = GPIO_PULLUP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(NORFLASH_CS_GPIO_PORT, &gpio_init_struct);    /* SCK引脚模式设置(复用输出) */

    spi2_init();
    spi2_read_write_byte(0xFF); /* 清除DR的作用 */
    
    NORFLASH_CS(1);
}

uint8_t norflash_rd_sr1(void)
{
    uint8_t rec_data = 0;
    
    NORFLASH_CS(0);
    spi2_read_write_byte(0x05);     /* 读状态寄存器1 */
    rec_data = spi2_read_write_byte(0xFF);
    NORFLASH_CS(1);
    
    return rec_data;
}

uint8_t norflash_read_data(uint32_t addr)
{
    uint8_t rec_data = 0;
    
    NORFLASH_CS(0);
    
    /* 1 发送读命令 */
    spi2_read_write_byte(0x03);
    
    /* 2 发送地址 */
    spi2_read_write_byte(addr >> 16);
    spi2_read_write_byte(addr >> 8);
    spi2_read_write_byte(addr);
       
    /* 3 读取数据 */
    rec_data = spi2_read_write_byte(0xFF);
    
    NORFLASH_CS(1);
    return rec_data;
}

void norflash_erase_sector(uint32_t addr)
{
    /* 1 写使能 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x06);
    NORFLASH_CS(1);
    
    /* 2 等待空闲 */
    while(norflash_rd_sr1()&0x01);
    
    /* 3 发送扇区擦除指令 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x20);
    
    /* 4 发送地址 */
    spi2_read_write_byte(addr >> 16);
    spi2_read_write_byte(addr >> 8);
    spi2_read_write_byte(addr);
    NORFLASH_CS(1);
    
    /* 5 等待空闲 */
    while(norflash_rd_sr1()&0x01);
}

void norflash_write_page(uint8_t data, uint32_t addr)
{
    /* 1 擦除扇区 */
    norflash_erase_sector(addr);
    
    /* 2 写使能 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x06);
    NORFLASH_CS(1);
    
    /* 3 发送页写指令 */
    NORFLASH_CS(0);
    spi2_read_write_byte(0x02);
    
    /* 4 发送地址 */
    spi2_read_write_byte(addr >> 16);
    spi2_read_write_byte(addr >> 8);
    spi2_read_write_byte(addr);
    
    /* 5 要写入的数据 */
    spi2_read_write_byte(data);
    NORFLASH_CS(1);
    
    /* 6 等待写入完成(等待空闲) */
    while(norflash_rd_sr1()&0x01);
}

norflash.h

#ifndef __norflash_H
#define __norflash_H

#include "./SYSTEM/sys/sys.h"

/******************************************************************************************/
/* NORFLASH 片选 引脚 定义 */
#define NORFLASH_CS_GPIO_PORT           GPIOB
#define NORFLASH_CS_GPIO_PIN            GPIO_PIN_12
#define NORFLASH_CS_GPIO_CLK_ENABLE()   do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

/******************************************************************************************/
/* NORFLASH 片选信号 */
#define NORFLASH_CS(x)      do{ x ? \
                                  HAL_GPIO_WritePin(NORFLASH_CS_GPIO_PORT, NORFLASH_CS_GPIO_PIN, GPIO_PIN_SET) : \
                                  HAL_GPIO_WritePin(NORFLASH_CS_GPIO_PORT, NORFLASH_CS_GPIO_PIN, GPIO_PIN_RESET); \
                            }while(0)


void norflash_init(void);
uint8_t norflash_rd_sr1(void);
uint8_t norflash_read_data(uint32_t addr);
void norflash_erase_sector(uint32_t addr);
void norflash_write_page(uint8_t data, uint32_t addr);
                            
#endif

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./USMART/usmart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/NORFLASH/norflash.h"

int main(void)
{
    uint8_t key;
    uint16_t i = 0;
    uint8_t rec_data = 0;
    
    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    usart_init(115200);                 /* 串口初始化为115200 */
    usmart_dev.init(72);                /* 初始化USMART */
    led_init();                         /* 初始化LED */
    lcd_init();                         /* 初始化LCD */
    key_init();                         /* 初始化按键 */
    norflash_init();
    
    while (1)
    {
        key = key_scan(0);

        if (key == KEY1_PRES) /* KEY1按下,写入 */
        {
            norflash_write_page('5', 0x123457); /* 地址范围0~0xFFFFFF */
            printf("write finish \r\n");
        }

        if (key == KEY0_PRES) /* KEY0按下,读取数据 */
        {
            rec_data = norflash_read_data(0x123456);
            printf("read data : %c \r\n", rec_data);
        }

        i++;

        if (i == 20)
        {
            LED0_TOGGLE(); /* LED0闪烁 */
            i = 0;
        }

        delay_ms(10);
    }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

F1 SPI源码

在这里插入图片描述

spi.c

#include "./BSP/SPI/spi.h"

SPI_HandleTypeDef g_spi2_handler; /* SPI2句柄 */

/**
 * @brief       SPI初始化代码
 *   @note      主机模式,8位数据,禁止硬件片选
 * @param       无
 * @retval      无
 */
void spi2_init(void)
{
    SPI2_SPI_CLK_ENABLE(); /* SPI2时钟使能 */

    g_spi2_handler.Instance = SPI2_SPI;                                /* SPI2 */
    g_spi2_handler.Init.Mode = SPI_MODE_MASTER;                        /* 设置SPI工作模式,设置为主模式 */
    g_spi2_handler.Init.Direction = SPI_DIRECTION_2LINES;              /* 设置SPI单向或者双向的数据模式:SPI设置为双线模式 */
    g_spi2_handler.Init.DataSize = SPI_DATASIZE_8BIT;                  /* 设置SPI的数据大小:SPI发送接收8位帧结构 */
    g_spi2_handler.Init.CLKPolarity = SPI_POLARITY_HIGH;               /* 串行同步时钟的空闲状态为高电平 */
    g_spi2_handler.Init.CLKPhase = SPI_PHASE_2EDGE;                    /* 串行同步时钟的第二个跳变沿(上升或下降)数据被采样 */
    g_spi2_handler.Init.NSS = SPI_NSS_SOFT;                            /* NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制 */
    g_spi2_handler.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; /* 定义波特率预分频的值:波特率预分频值为256 */
    g_spi2_handler.Init.FirstBit = SPI_FIRSTBIT_MSB;                   /* 指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始 */
    g_spi2_handler.Init.TIMode = SPI_TIMODE_DISABLE;                   /* 关闭TI模式 */
    g_spi2_handler.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;   /* 关闭硬件CRC校验 */
    g_spi2_handler.Init.CRCPolynomial = 7;                             /* CRC值计算的多项式 */
    HAL_SPI_Init(&g_spi2_handler);                                     /* 初始化 */

    __HAL_SPI_ENABLE(&g_spi2_handler); /* 使能SPI2 */

    spi2_read_write_byte(0Xff); /* 启动传输, 实际上就是产生8个时钟脉冲, 达到清空DR的作用, 非必需 */
}

/**
 * @brief       SPI2底层驱动,时钟使能,引脚配置
 *   @note      此函数会被HAL_SPI_Init()调用
 * @param       hspi:SPI句柄
 * @retval      无
 */
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
    GPIO_InitTypeDef GPIO_Initure;
    if (hspi->Instance == SPI2_SPI)
    {
        SPI2_SCK_GPIO_CLK_ENABLE();  /* SPI2_SCK脚时钟使能 */
        SPI2_MISO_GPIO_CLK_ENABLE(); /* SPI2_MISO脚时钟使能 */
        SPI2_MOSI_GPIO_CLK_ENABLE(); /* SPI2_MOSI脚时钟使能 */

        /* SCK引脚模式设置(复用输出) */
        GPIO_Initure.Pin = SPI2_SCK_GPIO_PIN;
        GPIO_Initure.Mode = GPIO_MODE_AF_PP;
        GPIO_Initure.Pull = GPIO_PULLUP;
        GPIO_Initure.Speed = GPIO_SPEED_FREQ_HIGH;
        HAL_GPIO_Init(SPI2_SCK_GPIO_PORT, &GPIO_Initure);

        /* MISO引脚模式设置(复用输出) */
        GPIO_Initure.Pin = SPI2_MISO_GPIO_PIN;
        HAL_GPIO_Init(SPI2_MISO_GPIO_PORT, &GPIO_Initure);

        /* MOSI引脚模式设置(复用输出) */
        GPIO_Initure.Pin = SPI2_MOSI_GPIO_PIN;
        HAL_GPIO_Init(SPI2_MOSI_GPIO_PORT, &GPIO_Initure);
    }
}

/**
 * @brief       SPI2速度设置函数
 *   @note      SPI2时钟选择来自APB1, 即PCLK1, 为36Mhz
 *              SPI速度 = PCLK1 / 2^(speed + 1)
 * @param       speed   : SPI2时钟分频系数
                        取值为SPI_BAUDRATEPRESCALER_2~SPI_BAUDRATEPRESCALER_2 256
 * @retval      无
 */
void spi2_set_speed(uint8_t speed)
{
    assert_param(IS_SPI_BAUDRATE_PRESCALER(speed)); /* 判断有效性 */
    __HAL_SPI_DISABLE(&g_spi2_handler);             /* 关闭SPI */
    g_spi2_handler.Instance->CR1 &= 0XFFC7;         /* 位3-5清零,用来设置波特率 */
    g_spi2_handler.Instance->CR1 |= speed << 3;     /* 设置SPI速度 */
    __HAL_SPI_ENABLE(&g_spi2_handler);              /* 使能SPI */
}

/**
 * @brief       SPI2读写一个字节数据
 * @param       txdata  : 要发送的数据(1字节)
 * @retval      接收到的数据(1字节)
 */
uint8_t spi2_read_write_byte(uint8_t txdata)
{
    uint8_t rxdata;
    HAL_SPI_TransmitReceive(&g_spi2_handler, &txdata, &rxdata, 1, 1000);
    return rxdata; /* 返回收到的数据 */
}

spi.h

#ifndef __SPI_H
#define __SPI_H

#include "./SYSTEM/sys/sys.h"


/******************************************************************************************/
/* SPI2 引脚 定义 */

#define SPI2_SCK_GPIO_PORT              GPIOB
#define SPI2_SCK_GPIO_PIN               GPIO_PIN_13
#define SPI2_SCK_GPIO_CLK_ENABLE()      do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

#define SPI2_MISO_GPIO_PORT             GPIOB
#define SPI2_MISO_GPIO_PIN              GPIO_PIN_14
#define SPI2_MISO_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

#define SPI2_MOSI_GPIO_PORT             GPIOB
#define SPI2_MOSI_GPIO_PIN              GPIO_PIN_15
#define SPI2_MOSI_GPIO_CLK_ENABLE()     do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

/* SPI2相关定义 */
#define SPI2_SPI                        SPI2
#define SPI2_SPI_CLK_ENABLE()           do{ __HAL_RCC_SPI2_CLK_ENABLE(); }while(0)    /* SPI2时钟使能 */

/******************************************************************************************/


/* SPI总线速度设置 */
#define SPI_SPEED_2         0
#define SPI_SPEED_4         1
#define SPI_SPEED_8         2
#define SPI_SPEED_16        3
#define SPI_SPEED_32        4
#define SPI_SPEED_64        5
#define SPI_SPEED_128       6
#define SPI_SPEED_256       7


void spi2_init(void);
void spi2_set_speed(uint8_t speed);
uint8_t spi2_read_write_byte(uint8_t txdata);

#endif

norflash.c

#include "./BSP/SPI/spi.h"
#include "./SYSTEM/delay/delay.h"
#include "./SYSTEM/usart/usart.h"
#include "./BSP/NORFLASH/norflash.h"


uint16_t g_norflash_type = NM25Q128;     /* 默认是NM25Q128 */

/**
 * @brief       初始化SPI NOR FLASH
 * @param       无
 * @retval      无
 */
void norflash_init(void)
{
    uint8_t temp;

    NORFLASH_CS_GPIO_CLK_ENABLE();      /* NORFLASH CS脚 时钟使能 */

    GPIO_InitTypeDef gpio_init_struct;
    gpio_init_struct.Pin = NORFLASH_CS_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;
    gpio_init_struct.Pull = GPIO_PULLUP;
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(NORFLASH_CS_GPIO_PORT, &gpio_init_struct); /* CS引脚模式设置(复用输出) */

    NORFLASH_CS(1);                     /* 取消片选 */

    spi2_init();                        /* 初始化SPI2 */
    spi2_set_speed(SPI_SPEED_2);        /* SPI2 切换到高速状态 18Mhz */
    
    g_norflash_type = norflash_read_id();   /* 读取FLASH ID. */
    
    if (g_norflash_type == W25Q256)     /* SPI FLASH为W25Q256, 必须使能4字节地址模式 */
    {
        temp = norflash_read_sr(3);     /* 读取状态寄存器3,判断地址模式 */

        if ((temp & 0X01) == 0)         /* 如果不是4字节地址模式,则进入4字节地址模式 */
        {
            norflash_write_enable();    /* 写使能 */
            temp |= 1 << 1;             /* ADP=1, 上电4字节地址模式 */
            norflash_write_sr(3, temp); /* 写SR3 */
            
            NORFLASH_CS(0);
            spi2_read_write_byte(FLASH_Enable4ByteAddr);    /* 使能4字节地址指令 */
            NORFLASH_CS(1);
        }
    }

    //printf("ID:%x\r\n", g_norflash_type);
}

/**
 * @brief       等待空闲
 * @param       无
 * @retval      无
 */
static void norflash_wait_busy(void)
{
    while ((norflash_read_sr(1) & 0x01) == 0x01);   /*  等待BUSY位清空 */
}

/**
 * @brief       25QXX写使能
 *   @note      将S1寄存器的WEL置位
 * @param       无
 * @retval      无
 */
void norflash_write_enable(void)
{
    NORFLASH_CS(0);
    spi2_read_write_byte(FLASH_WriteEnable);   /* 发送写使能 */
    NORFLASH_CS(1);
}

/**
 * @brief       25QXX发送地址
 *   @note      根据芯片型号的不同, 发送24ibt / 32bit地址
 * @param       address : 要发送的地址
 * @retval      无
 */
static void norflash_send_address(uint32_t address)
{
    if (g_norflash_type == W25Q256) /*  只有W25Q256支持4字节地址模式 */
    {
        spi2_read_write_byte((uint8_t)((address)>>24)); /* 发送 bit31 ~ bit24 地址 */
    } 
    spi2_read_write_byte((uint8_t)((address)>>16));     /* 发送 bit23 ~ bit16 地址 */
    spi2_read_write_byte((uint8_t)((address)>>8));      /* 发送 bit15 ~ bit8  地址 */
    spi2_read_write_byte((uint8_t)address);             /* 发送 bit7  ~ bit0  地址 */
}

/**
 * @brief       读取25QXX的状态寄存器,25QXX一共有3个状态寄存器
 *   @note      状态寄存器1:
 *              BIT7  6   5   4   3   2   1   0
 *              SPR   RV  TB BP2 BP1 BP0 WEL BUSY
 *              SPR:默认0,状态寄存器保护位,配合WP使用
 *              TB,BP2,BP1,BP0:FLASH区域写保护设置
 *              WEL:写使能锁定
 *              BUSY:忙标记位(1,忙;0,空闲)
 *              默认:0x00
 *
 *              状态寄存器2:
 *              BIT7  6   5   4   3   2   1   0
 *              SUS   CMP LB3 LB2 LB1 (R) QE  SRP1
 *
 *              状态寄存器3:
 *              BIT7      6    5    4   3   2   1   0
 *              HOLD/RST  DRV1 DRV0 (R) (R) WPS ADP ADS
 *
 * @param       regno: 状态寄存器号,范:1~3
 * @retval      状态寄存器值
 */
uint8_t norflash_read_sr(uint8_t regno)
{
    uint8_t byte = 0, command = 0;

    switch (regno)
    {
        case 1:
            command = FLASH_ReadStatusReg1;  /* 读状态寄存器1指令 */
            break;

        case 2:
            command = FLASH_ReadStatusReg2;  /* 读状态寄存器2指令 */
            break;

        case 3:
            command = FLASH_ReadStatusReg3;  /* 读状态寄存器3指令 */
            break;

        default:
            command = FLASH_ReadStatusReg1;
            break;
    }

    NORFLASH_CS(0);
    spi2_read_write_byte(command);      /* 发送读寄存器命令 */
    byte = spi2_read_write_byte(0Xff);  /* 读取一个字节 */
    NORFLASH_CS(1);
    
    return byte;
}

/**
 * @brief       写25QXX状态寄存器
 *   @note      寄存器说明见norflash_read_sr函数说明
 * @param       regno: 状态寄存器号,范:1~3
 * @param       sr   : 要写入状态寄存器的值
 * @retval      无
 */
void norflash_write_sr(uint8_t regno, uint8_t sr)
{
    uint8_t command = 0;

    switch (regno)
    {
        case 1:
            command = FLASH_WriteStatusReg1;  /* 写状态寄存器1指令 */
            break;

        case 2:
            command = FLASH_WriteStatusReg2;  /* 写状态寄存器2指令 */
            break;

        case 3:
            command = FLASH_WriteStatusReg3;  /* 写状态寄存器3指令 */
            break;

        default:
            command = FLASH_WriteStatusReg1;
            break;
    }

    NORFLASH_CS(0);
    spi2_read_write_byte(command);  /* 发送读寄存器命令 */
    spi2_read_write_byte(sr);       /* 写入一个字节 */
    NORFLASH_CS(1);
}

/**
 * @brief       读取芯片ID
 * @param       无
 * @retval      FLASH芯片ID
 *   @note      芯片ID列表见: norflash.h, 芯片列表部分
 */
uint16_t norflash_read_id(void)
{
    uint16_t deviceid;

    NORFLASH_CS(0);
    spi2_read_write_byte(FLASH_ManufactDeviceID);   /* 发送读 ID 命令 */
    spi2_read_write_byte(0);    /* 写入一个字节 */
    spi2_read_write_byte(0);
    spi2_read_write_byte(0);
    deviceid = spi2_read_write_byte(0xFF) << 8;     /* 读取高8位字节 */
    deviceid |= spi2_read_write_byte(0xFF);         /* 读取低8位字节 */
    NORFLASH_CS(1);

    return deviceid;
}

/**
 * @brief       读取SPI FLASH
 *   @note      在指定地址开始读取指定长度的数据
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始读取的地址(最大32bit)
 * @param       datalen : 要读取的字节数(最大65535)
 * @retval      无
 */
void norflash_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;

    NORFLASH_CS(0);
    spi2_read_write_byte(FLASH_ReadData);       /* 发送读取命令 */
    norflash_send_address(addr);                /* 发送地址 */
    
    for(i = 0; i < datalen; i++)
    {
        pbuf[i] = spi2_read_write_byte(0XFF);   /* 循环读取 */
    }
    
    NORFLASH_CS(1);
}

/**
 * @brief       SPI在一页(0~65535)内写入少于256个字节的数据
 *   @note      在指定地址开始写入最大256字节的数据
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始写入的地址(最大32bit)
 * @param       datalen : 要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!
 * @retval      无
 */
static void norflash_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;

    norflash_write_enable();    /* 写使能 */

    NORFLASH_CS(0);
    spi2_read_write_byte(FLASH_PageProgram);    /* 发送写页命令 */
    norflash_send_address(addr);                /* 发送地址 */

    for(i = 0; i < datalen; i++)
    {
        spi2_read_write_byte(pbuf[i]);          /* 循环写入数据 */
    }
    
    NORFLASH_CS(1);
    norflash_wait_busy();       /* 等待写入结束 */
}

/**
 * @brief       无检验写SPI FLASH
 *   @note      必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!
 *              具有自动换页功能
 *              在指定地址开始写入指定长度的数据,但是要确保地址不越界!
 *
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始写入的地址(最大32bit)
 * @param       datalen : 要写入的字节数(最大65535)
 * @retval      无
 */
static void norflash_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t pageremain;
    pageremain = 256 - addr % 256;  /* 单页剩余的字节数 */

    if (datalen <= pageremain)      /* 不大于256个字节 */
    {
        pageremain = datalen;
    }

    while (1)
    {
        /* 当写入字节比页内剩余地址还少的时候, 一次性写完
         * 当写入直接比页内剩余地址还多的时候, 先写完整个页内剩余地址, 然后根据剩余长度进行不同处理
         */
        norflash_write_page(pbuf, addr, pageremain);

        if (datalen == pageremain)   /* 写入结束了 */
        {
            break;
        }
        else     /* datalen > pageremain */
        {
            pbuf += pageremain;         /* pbuf指针地址偏移,前面已经写了pageremain字节 */
            addr += pageremain;         /* 写地址偏移,前面已经写了pageremain字节 */
            datalen -= pageremain;      /* 写入总长度减去已经写入了的字节数 */

            if (datalen > 256)          /* 剩余数据还大于一页,可以一次写一页 */
            {
                pageremain = 256;       /* 一次可以写入256个字节 */
            }
            else     /* 剩余数据小于一页,可以一次写完 */
            {
                pageremain = datalen;   /* 不够256个字节了 */
            }
        }
    }
}

/**
 * @brief       写SPI FLASH
 *   @note      在指定地址开始写入指定长度的数据 , 该函数带擦除操作!
 *              SPI FLASH 一般是: 256个字节为一个Page, 4Kbytes为一个Sector, 16个扇区为1个Block
 *              擦除的最小单位为Sector.
 *
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始写入的地址(最大32bit)
 * @param       datalen : 要写入的字节数(最大65535)
 * @retval      无
 */
uint8_t g_norflash_buf[4096];   /* 扇区缓存 */

void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint32_t secpos;
    uint16_t secoff;
    uint16_t secremain;
    uint16_t i;
    uint8_t *norflash_buf;

    norflash_buf = g_norflash_buf;
    secpos = addr / 4096;       /* 扇区地址 */
    secoff = addr % 4096;       /* 在扇区内的偏移 */
    secremain = 4096 - secoff;  /* 扇区剩余空间大小 */

    //printf("ad:%X,nb:%X\r\n", addr, datalen); /* 测试用 */
    if (datalen <= secremain)
    {
        secremain = datalen;    /* 不大于4096个字节 */
    }

    while (1)
    {
        norflash_read(norflash_buf, secpos * 4096, 4096);   /* 读出整个扇区的内容 */

        for (i = 0; i < secremain; i++)   /* 校验数据 */
        {
            if (norflash_buf[secoff + i] != 0XFF)
            {
                break;      /* 需要擦除, 直接退出for循环 */
            }
        }

        if (i < secremain)   /* 需要擦除 */
        {
            norflash_erase_sector(secpos);  /* 擦除这个扇区 */

            for (i = 0; i < secremain; i++)   /* 复制 */
            {
                norflash_buf[i + secoff] = pbuf[i];
            }

            norflash_write_nocheck(norflash_buf, secpos * 4096, 4096);  /* 写入整个扇区 */
        }
        else        /* 写已经擦除了的,直接写入扇区剩余区间. */
        {
            norflash_write_nocheck(pbuf, addr, secremain);  /* 直接写扇区 */
        }

        if (datalen == secremain)
        {
            break;  /* 写入结束了 */
        }
        else        /* 写入未结束 */
        {
            secpos++;               /* 扇区地址增1 */
            secoff = 0;             /* 偏移位置为0 */

            pbuf += secremain;      /* 指针偏移 */
            addr += secremain;      /* 写地址偏移 */
            datalen -= secremain;   /* 字节数递减 */

            if (datalen > 4096)
            {
                secremain = 4096;   /* 下一个扇区还是写不完 */
            }
            else
            {
                secremain = datalen;/* 下一个扇区可以写完了 */
            }
        }
    }
}

/**
 * @brief       擦除整个芯片
 *   @note      等待时间超长...
 * @param       无
 * @retval      无
 */
void norflash_erase_chip(void)
{
    norflash_write_enable();    /* 写使能 */
    norflash_wait_busy();       /* 等待空闲 */
    NORFLASH_CS(0);
    spi2_read_write_byte(FLASH_ChipErase);  /* 发送读寄存器命令 */ 
    NORFLASH_CS(1);
    norflash_wait_busy();       /* 等待芯片擦除结束 */
}

/**
 * @brief       擦除一个扇区
 *   @note      注意,这里是扇区地址,不是字节地址!!
 *              擦除一个扇区的最少时间:150ms
 *
 * @param       saddr : 扇区地址 根据实际容量设置
 * @retval      无
 */
void norflash_erase_sector(uint32_t saddr)
{
    //printf("fe:%x\r\n", saddr);   /* 监视falsh擦除情况,测试用 */
    saddr *= 4096;
    norflash_write_enable();        /* 写使能 */
    norflash_wait_busy();           /* 等待空闲 */

    NORFLASH_CS(0);
    spi2_read_write_byte(FLASH_SectorErase);    /* 发送写页命令 */
    norflash_send_address(saddr);   /* 发送地址 */
    NORFLASH_CS(1);
    norflash_wait_busy();           /* 等待扇区擦除完成 */
}

norflash.h

#ifndef __norflash_H
#define __norflash_H

#include "./SYSTEM/sys/sys.h"


/******************************************************************************************/
/* NORFLASH 片选 引脚 定义 */

#define NORFLASH_CS_GPIO_PORT           GPIOB
#define NORFLASH_CS_GPIO_PIN            GPIO_PIN_12
#define NORFLASH_CS_GPIO_CLK_ENABLE()   do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

/******************************************************************************************/

/* NORFLASH 片选信号 */
#define NORFLASH_CS(x)      do{ x ? \
                                  HAL_GPIO_WritePin(NORFLASH_CS_GPIO_PORT, NORFLASH_CS_GPIO_PIN, GPIO_PIN_SET) : \
                                  HAL_GPIO_WritePin(NORFLASH_CS_GPIO_PORT, NORFLASH_CS_GPIO_PIN, GPIO_PIN_RESET); \
                            }while(0)

/* FLASH芯片列表 */
#define W25Q80      0XEF13          /* W25Q80   芯片ID */
#define W25Q16      0XEF14          /* W25Q16   芯片ID */
#define W25Q32      0XEF15          /* W25Q32   芯片ID */
#define W25Q64      0XEF16          /* W25Q64   芯片ID */
#define W25Q128     0XEF17          /* W25Q128  芯片ID */
#define W25Q256     0XEF18          /* W25Q256  芯片ID */
#define BY25Q64     0X6816          /* BY25Q64  芯片ID */
#define BY25Q128    0X6817          /* BY25Q128 芯片ID */
#define NM25Q64     0X5216          /* NM25Q64  芯片ID */
#define NM25Q128    0X5217          /* NM25Q128 芯片ID */

extern uint16_t norflash_TYPE;      /* 定义FLASH芯片型号 */
 
/* 指令表 */
#define FLASH_WriteEnable           0x06 
#define FLASH_WriteDisable          0x04 
#define FLASH_ReadStatusReg1        0x05 
#define FLASH_ReadStatusReg2        0x35 
#define FLASH_ReadStatusReg3        0x15 
#define FLASH_WriteStatusReg1       0x01 
#define FLASH_WriteStatusReg2       0x31 
#define FLASH_WriteStatusReg3       0x11 
#define FLASH_ReadData              0x03 
#define FLASH_FastReadData          0x0B 
#define FLASH_FastReadDual          0x3B 
#define FLASH_FastReadQuad          0xEB  
#define FLASH_PageProgram           0x02 
#define FLASH_PageProgramQuad       0x32 
#define FLASH_BlockErase            0xD8 
#define FLASH_SectorErase           0x20 
#define FLASH_ChipErase             0xC7 
#define FLASH_PowerDown             0xB9 
#define FLASH_ReleasePowerDown      0xAB 
#define FLASH_DeviceID              0xAB 
#define FLASH_ManufactDeviceID      0x90 
#define FLASH_JedecDeviceID         0x9F 
#define FLASH_Enable4ByteAddr       0xB7
#define FLASH_Exit4ByteAddr         0xE9
#define FLASH_SetReadParam          0xC0 
#define FLASH_EnterQPIMode          0x38
#define FLASH_ExitQPIMode           0xFF

/* 静态函数 */
static void norflash_wait_busy(void);               /* 等待空闲 */
static void norflash_send_address(uint32_t address);/* 发送地址 */
static void norflash_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen);    /* 写入page */
static void norflash_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen); /* 写flash,不带擦除 */

/* 普通函数 */
void norflash_init(void);                   /* 初始化25QXX */
uint16_t norflash_read_id(void);            /* 读取FLASH ID */
void norflash_write_enable(void);           /* 写使能 */
uint8_t norflash_read_sr(uint8_t regno);    /* 读取状态寄存器 */
void norflash_write_sr(uint8_t regno,uint8_t sr);   /* 写状态寄存器 */

void norflash_erase_chip(void);             /* 整片擦除 */
void norflash_erase_sector(uint32_t saddr); /* 扇区擦除 */
void norflash_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen);     /* 读取flash */
void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen);    /* 写入flash */

#endif

main.c

#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./USMART/usmart.h"
#include "./BSP/LED/led.h"
#include "./BSP/LCD/lcd.h"
#include "./BSP/KEY/key.h"
#include "./BSP/NORFLASH/norflash.h"

/* 要写入到FLASH的字符串数组 */
const uint8_t g_text_buf[] = {"STM32 SPI TEST"};

#define TEXT_SIZE sizeof(g_text_buf) /* TEXT字符串长度 */

int main(void)
{
    uint8_t key;
    uint16_t i = 0;
    uint8_t datatemp[TEXT_SIZE];
    uint32_t flashsize;
    uint16_t id = 0;

    HAL_Init();                         /* 初始化HAL库 */
    sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
    delay_init(72);                     /* 延时初始化 */
    usart_init(115200);                 /* 串口初始化为115200 */
    usmart_dev.init(72);                /* 初始化USMART */
    led_init();                         /* 初始化LED */
    lcd_init();                         /* 初始化LCD */
    key_init();                         /* 初始化按键 */
    norflash_init();                    /* 初始化NORFLASH */

    lcd_show_string(30,  50, 200, 16, 16, "STM32", RED);
    lcd_show_string(30,  70, 200, 16, 16, "SPI TEST", RED);
    lcd_show_string(30,  90, 200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "KEY1:Write  KEY0:Read", RED); /* 显示提示信息 */

    id = norflash_read_id(); /* 读取FLASH ID */

    while ((id == 0) || (id == 0XFFFF)) /* 检测不到FLASH芯片 */
    {
        lcd_show_string(30, 130, 200, 16, 16, "FLASH Check Failed!", RED);
        delay_ms(500);
        lcd_show_string(30, 130, 200, 16, 16, "Please Check!      ", RED);
        delay_ms(500);
        LED0_TOGGLE(); /* LED0闪烁 */
    }

    lcd_show_string(30, 130, 200, 16, 16, "SPI FLASH Ready!", BLUE);
    flashsize = 16 * 1024 * 1024; /* FLASH 大小为16M字节 */

    while (1)
    {
        key = key_scan(0);

        if (key == KEY1_PRES) /* KEY1按下,写入 */
        {
            lcd_fill(0, 150, 239, 319, WHITE); /* 清除半屏 */
            lcd_show_string(30, 150, 200, 16, 16, "Start Write FLASH....", BLUE);
            sprintf((char *)datatemp, "%s%d", (char *)g_text_buf, i);
            norflash_write((uint8_t *)datatemp, flashsize - 100, TEXT_SIZE);      /* 从倒数第100个地址处开始,写入SIZE长度的数据 */
            lcd_show_string(30, 150, 200, 16, 16, "FLASH Write Finished!", BLUE); /* 提示传送完成 */
        }

        if (key == KEY0_PRES) /* KEY0按下,读取字符串并显示 */
        {
            lcd_show_string(30, 150, 200, 16, 16, "Start Read FLASH... . ", BLUE);
            norflash_read(datatemp, flashsize - 100, TEXT_SIZE);                   /* 从倒数第100个地址处开始,读出SIZE个字节 */
            lcd_show_string(30, 150, 200, 16, 16, "The Data Readed Is:   ", BLUE); /* 提示传送完成 */
            lcd_show_string(30, 170, 200, 16, 16, (char *)datatemp, BLUE);         /* 显示读到的字符串 */
        }

        i++;

        if (i == 20)
        {
            LED0_TOGGLE(); /* LED0闪烁 */
            i = 0;
        }

        delay_ms(10);
    }
}

在这里插入图片描述

九、总结

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

咖喱年糕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值