当前位置: 代码网 > 科技>电脑产品>内存 > 【正点原子STM32】QSPI四线SPI模式(Quad-SPI存储器、间接模式、状态轮询模式、内存映射模式、命令序列、QSPI基本使用步骤、SPI FLASH基本使用步骤)

【正点原子STM32】QSPI四线SPI模式(Quad-SPI存储器、间接模式、状态轮询模式、内存映射模式、命令序列、QSPI基本使用步骤、SPI FLASH基本使用步骤)

2024年08月01日 内存 我要评论
QSPI介绍总的来说,SPI的这几个变种主要是为了提高数据传输效率,同时在一定程度上减小接口引脚数量,但也会带来一些限制,如在高数据速率下只能进行半双工通信。而在实际应用中,尤其是在与闪存设备交互时,SPI模式的选择需要根据系统的性能需求、空间占用以及功耗预算等因素综合考虑。

一、qspi介绍

二、qspi相关寄存器介绍
三、qspi相关hal库驱动介绍
四、qspi基本使用步骤
五、spi flash简介
六、spi flash基本使用步骤
七、编程实战
八、总结


在这里插入图片描述
spi(serial peripheral interface)根据其数据传输能力和硬件接口的不同,可以分为以下几个类别:

  1. standard spi(标准spi)

    • 标准spi是一种全双工通信协议,拥有四条主要通信线:
      • cs(chip select,片选):用于选择参与通信的从设备。
      • sclk(serial clock,串行时钟):主设备提供时钟信号,从设备据此进行数据采样和传输。
      • mosi(master out, slave in):主设备向从设备发送数据的线。
      • miso(master in, slave out):从设备向主设备发送数据的线。
    • 驱动spi flash时,可能还会涉及额外的控制线,如写保护(wr)和保持(hold)引脚,用于保护数据安全和暂停通信。
  2. dual spi(双线spi)

    • 双线spi是对标准spi的一种改进,它将原本的mosi和miso引脚合并成一对双向数据线,通常称为io0和io1。
    • 在一个时钟周期内,通过这两条数据线可以同时传输两位数据,这样理论上可以将数据传输速率提高一倍,但这时spi工作在半双工模式下,即在同一时刻只能进行读或写操作。
  3. quad spi(四线spi)

    • 四线spi是在双线spi的基础上进一步改进,增加了另外两条双向数据线io2和io3。
    • 这四条数据线可以在一个时钟周期内传输四位数据,极大地提升了数据传输速率。
    • 在一些实现中,原有的写保护(wr)和保持(hold)引脚可能会被复用为数据线io2和io3,以减少硬件接口的数量。

总的来说,spi的这几个变种主要是为了提高数据传输效率,同时在一定程度上减小接口引脚数量,但也会带来一些限制,如在高数据速率下只能进行半双工通信。而在实际应用中,尤其是在与闪存设备交互时,spi模式的选择需要根据系统的性能需求、空间占用以及功耗预算等因素综合考虑。
在这里插入图片描述


一、qspi介绍

在这里插入图片描述
qspi(queued serial peripheral interface)是spi接口的一种高级扩展形式,由motorola公司推出,后来在各类微控制器中广泛应用,特别是在处理高速数据传输和与外部高性能quad-spi存储器(如flash)交互时表现出色。

qspi的主要特点包括:

  • 四线通信:支持1线、2线和4线模式,其中4线模式下,通过四条数据线同时传输数据,大大提高了数据吞吐量,非常适合于大容量、高速度的spi flash存储器访问。
  • 优化操作模式:支持sdr(single data rate,单倍数据速率)和ddr(double data rate,双倍数据速率)模式,ddr模式下可以在每个时钟周期内传输两次数据,进一步提升传输速度。
  • 三种操作模式
    • 间接模式:类似于标准spi,通过qspi寄存器执行所有读写操作,适用于一般的编程和擦除操作。
    • 状态轮询模式:通过周期性读取外部flash的状态寄存器来监控操作进度,当flash状态寄存器指示操作完成时,可以通过中断告知微控制器。
    • 内存映射模式:外部quad-spi flash存储器可以直接映射到stm32等微控制器的地址空间中,如同内部flash一样进行读取操作,大大简化了访问流程,提高数据读取速度。

简单来说,qspi是一种高度优化和强化的spi接口,尤其适用于高效地驱动和管理高性能的spi flash存储器,提供更大的带宽和更低延迟的访问体验。通过先进的硬件支持和灵活的操作模式,qspi极大地提升了与spi flash等外部设备的通信效率。

1.1、qspi功能框图(双闪存模式禁止)

在这里插入图片描述
qspi(quad serial peripheral interface)在功能结构上相较于标准spi增加了更多的数据线,用于实现更高的数据传输速率。在双闪存模式禁止的情况下,其功能结构主要包括:

  1. 时钟线clk

    • 时钟线是qspi通信的同步信号,所有数据的传输都在时钟信号的上升沿或下降沿进行。
  2. 片选线bk1_ncs

    • 片选线用于选择与qspi接口相连的特定闪存设备。当bk1_ncs信号为低电平时,选定的闪存设备被激活并开始进行数据传输。
  3. 数据线bk1_io0~io3

    • 在单线spi模式下,可能只会使用到bk1_io0作为数据线。
    • 在双线spi模式下,bk1_io0和bk1_io1可作为双向数据线,一次传输两位数据。
    • 在四线spi(quad spi)模式下,所有四条数据线bk1_io0~io3都被用作双向数据通道,能够在单个时钟周期内传输四位数据,从而大大提高数据吞吐量。

根据不同模式,这些引脚的功能有所不同:

  • 单线模式:仅使用bk1_io0进行数据传输。
  • 双线模式:bk1_io0和bk1_io1用于数据传输,一次传输两个数据位。
  • 四线模式(quad spi):bk1_io0、io1、io2和io3共同工作,一次传输四个数据位。

在双闪存模式禁止时,这意味着qspi控制器不会同时处理两个独立的闪存设备,而是专注于单一的外部闪存设备。通过调整qspi控制器的配置寄存器,可以灵活地在这几种模式间切换,以适应不同的应用场景和性能需求。

时钟输入、qspi输出信号

在stm32h7系列微控制器中,qspi接口的时钟输入和输出信号说明如下:

  • 时钟输入

    • 32位ahb总线:qspi与处理器的内部总线接口使用32位ahb总线进行通信,ahb总线提供了高速的数据传输通道。
    • quadspi_ker_ck:qspi内核时钟,它是qspi模块工作的基础时钟,通常来自系统时钟或pll分频后的某个时钟源。
    • quadspi_hclk:qspi ahb时钟,它是qspi与处理器内部ahb总线通信所需的时钟信号,一般等于或小于quadspi_ker_ck。
  • qspi输出信号

    • 64位axi总线:在某些stm32h7系列中,qspi支持与64位axi总线相连,提供更高的数据吞吐量,用于内存映射模式下访问外部qspi闪存。
    • quadspi_it:qspi中断信号,当qspi完成某项操作(如读写完成)时,会触发此中断信号通知cpu。
    • quadspi_ft_trg:qspi闪存传输触发信号,用于启动或控制对qspi闪存的读写操作。
    • quadspi_tc_trg:qspi传输完成触发信号,可能用于指示qspi完成了一次完整的数据传输或操作。

请注意,以上信号名称并非官方文档中stm32h7系列的标准命名,但它们代表了qspi接口常见的时钟输入和输出信号类型。在实际使用时,请参考stm32h7系列的官方技术参考手册(trm)以获得准确的信号名称和功能描述。

1.2、qspi 时钟源

在这里插入图片描述
在stm32f7和stm32h7系列微控制器中,qspi时钟源的选择可以根据系统设计需求进行配置。

  • stm32f7系列

    • 默认情况下,qspi时钟源可能选择的是hclk3,即系统高速时钟(system high speed clock)的一个分频版本。然而,这也依赖于具体的f7系列微控制器型号和应用需求,可以通过重新配置rcc(reset and clock control)寄存器来选择其他可用的时钟源。
  • stm32h7系列(例如stm32h7 mini pro h750开发板)

    • 在stm32h7系列中,qspi时钟源的选择更为灵活。在某些应用案例中,可以选用pll2的输出作为qspi的时钟源,这通常是为了满足更高数据传输速率的需求。pll2可以提供一个比系统主时钟更高的频率,而且常常经过适当的分频后作为qspi的工作时钟。

在实际项目开发中,你需要根据微控制器的规格书、参考手册和应用需求来配置qspi的时钟源。通过查阅stm32cubemx工具或者直接编程配置rcc寄存器,可以设置合适的时钟源。

1.3、间接模式

在这里插入图片描述
间接模式是stm32h7系列微控制器qspi接口中的一种操作模式,主要用于执行读写和擦除操作。在此模式下,qspi与外部spi flash之间的数据传输通过内部fifo(first-in-first-out)缓冲区来进行。

  • 间接写入模式

    • 开发者将待写入的数据写入qspi的fifo(quadspi_sr[13:8]位反映了fifo的状态)。
    • 数据随后通过qspi接口传输到外部spi flash。
  • 间接读取模式

    • 开发者配置好读取操作后,qspi从外部spi flash读取数据,并将数据存入内部fifo。
    • 应用程序可以从fifo中读取接收到的数据。
  • 数据阶段的控制

    • qspi 控制寄存器 quadspi_ccr 中的 fmode 字段决定操作模式,fmode=00 表示间接写入模式,fmode=01 表示间接读取模式。
    • 若 ccr 中的 dmode 设置为 00,表示不进行数据传输(适用于只发送命令和地址的情况)。
  • 读/写字节数的设置

    • 通过 quadspi_dlr 寄存器设置读写操作的数据长度。若设置为 0xffffffff,则表示将持续传输数据,直到遇到 flash 存储器的末尾。
  • 启动传输

    • 在配置好命令、地址和数据长度后,通过向相应的控制寄存器写入适当的值来启动数据传输。
  • 传输完成的标志

    • 当传输达到设定的字节数时,quadspi_sr 中的 tcf(transfer complete flag)标志位将被置1。
    • 如果启用了 tcf 中断(通过使能 tcie,transfer complete interrupt enable),那么当传输完成时,将会触发一个中断通知应用程序。

1.4、内存映射模式

在这里插入图片描述
内存映射模式是stm32h7系列微控制器中qspi接口的另一种工作模式,主要适用于以下场景:

  1. 读取操作

    • 在内存映射模式下,外部quad-spi flash存储器可以直接映射到stm32h7的内存地址空间中,处理器可以通过访问特定的内存地址来读取存储器中的数据,就像访问内部ram一样。
  2. 扩展内部存储器

    • 外部quad-spi flash存储器被当作内部存储器的一部分使用,其他主机(例如处理器内核或dma控制器)可以直接读取这些地址上的数据,无需通过qspi接口的特殊函数调用。
  3. 执行代码(xip,execute-in-place)

    • 由于quad-spi flash存储器被映射到了内存地址空间,因此可以直接从中执行代码,减少了将代码从flash复制到ram的时间开销,提高了系统的启动速度和运行效率。

在stm32h7系列中,内存映射模式下,quad-spi接口可以管理的最大地址范围是从0x9000 0000到0x9fff ffff,总计256mb的内存空间。这意味着在这个地址范围内,处理器可以直接读取外部quad-spi flash的内容,实现无缝的数据访问和代码执行。在实际应用中,需要根据具体的quad-spi flash容量和实际需求来配置映射的地址区间。

1.5、命令序列(间接模式 或 内存映射模式)

在这里插入图片描述
在stm32h7系列微控制器的qspi接口中,无论是间接模式还是内存映射模式,对spi flash进行数据读写操作时,都需要构建和发送一个命令序列。这个命令序列通常由五个可配置阶段构成:

  1. 指令阶段

    • 发送一个或多个字节的命令代码,以指示spi flash执行特定的操作,如读取、写入、擦除等。
  2. 地址阶段

    • 发送用于定位数据在spi flash中的地址信息,地址的长度可配置,取决于具体的应用需求。
  3. 交替字节阶段(optional)

    • 用于在某些特殊操作中传递额外的信息或控制字节,不是所有操作都需要这个阶段。
  4. 空周期阶段(dummy cycle phase,optional)

    • 在某些读取操作中,为了满足spi flash的时序要求,可能需要插入一定数量的空时钟周期。这个阶段的长度也可配置。
  5. 数据阶段

    • 实际的数据传输阶段,可以是向spi flash写入数据,也可以是从spi flash读取数据。数据长度根据实际传输需求配置。

在配置命令序列时,开发人员可以灵活地控制每个阶段是否启动、每个阶段的长度以及数据是在单线、双线还是四线模式下传输。这些配置通常通过qspi相关的控制寄存器(如quadspi_cr、quadspi_dcr、quadspi_ar、quadspi_abr、quadspi_ddrar等)来完成。在进行数据读写操作时,命令序列的具体构成和配置需遵循spi flash器件的数据手册。

1.6、指令、地址、交替字节、空指令周期、数据各阶段

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

1.7、qspi flash设置

在这里插入图片描述
在stm32h7系列微控制器中,配置qspi与外部spi flash通信时,需要根据所使用的flash芯片型号在quadspi_dcr(quadspi device configuration register)寄存器中设置相应的参数:

  • 外部存储器大小设置

    • dcr寄存器的fsize[4:0]字段用于指定外部spi flash的大小。例如,对于w25q128(128mbit),fsize[4:0]应设置为23,对应224(16,777,216)字节;对于w25q256(256mbit),fsize[4:0]应设置为24,对应225(33,554,432)字节。
  • 时钟模式设置

    • 根据spi flash支持的工作模式和应用需求,在dcr寄存器中设置ckmode位来决定时钟极性。当ckmode = 0时,选择spi模式0,即clk在cs(片选)为高电平期间保持低电平;当ckmode = 1时,选择spi模式3,即clk在cs为高电平期间保持高电平。
  • 片选高电平时间设置

    • 虽然你未提及具体的寄存器位,但通常微控制器会提供相关寄存器或位来设置片选(cs)信号在命令发送前或数据传输间隔期间保持高电平的时钟周期数。这对于满足某些spi flash的时序要求至关重要。

在内存映射模式下,虽然qspi可以直接访问高达256mb的外部存储器空间,但在间接模式下,如果使用32位寻址,最大可寻址空间可以达到4gb。在配置qspi接口时,请务必查阅stm32h7系列微控制器的数据手册和所使用的spi flash的数据手册,以确保正确的参数设置和兼容性。

1.8、qspi 中断类型

在这里插入图片描述
在stm32h7系列微控制器的qspi接口中,支持多种类型的中断,这些中断在不同操作模式下有不同的触发条件:

  1. 超时中断

    • 当qspi操作超过预设的时间限制仍未完成时,超时中断被触发。这有助于及时发现和处理潜在的通信故障。
  2. 状态匹配中断(状态轮询模式下)

    • 在状态轮询模式下,qspi监视外部spi flash的状态寄存器。当flash状态寄存器中的特定位匹配预设值时,状态匹配中断发生,例如擦除或写入操作完成。
  3. fifo达到阈值中断(间接模式下)

    • 在间接模式下,qspi内部的fifo(first-in-first-out缓冲区)设有阈值。当fifo中的数据到达预设的满载或空载阈值时,会触发相应的中断,以提示cpu进行数据的读取或写入。
  4. 传输完成中断(间接模式下dlr指定字节数的数据已经发送完成)

    • 在间接模式下,当qspi完成了通过quadspi_dlr寄存器设定的字节数的数据传输后,会触发传输完成中断(tcf,transfer complete flag)。
  5. 传输错误中断(间接模式下地址越界或其他错误)

    • 当qspi在执行间接模式操作时遇到错误,例如试图访问超出外部spi flash地址范围(地址越界),或者发生其他传输错误时,会触发传输错误中断。这有助于及时捕获并处理这类异常情况,保障系统的稳定性与安全性。

这些中断可以通过配置qspi相关的中断使能寄存器和状态寄存器来管理和响应。在实际应用中,合理利用中断能够显著提高系统实时性和任务处理效率。

二、qspi相关寄存器介绍

在这里插入图片描述
以下是stm32h7系列微控制器中qspi接口相关寄存器的详细说明:

  • quadspi_cr(quadspi control register)

    • 用途:用于配置qspi的基本工作参数,包括使能或禁止qspi、设置工作模式(间接模式或内存映射模式)、选择时钟源、设置数据线数等。
  • quadspi_dcr(quadspi device configuration register)

    • 用途:主要用于配置与外部spi flash设备交互的基本参数,如spi flash的大小(fsize字段)、地址大小、等待状态周期数目等。
  • quadspi_ccr(quadspi communication configuration register)

    • 用途:配置qspi发送给spi flash的命令序列的各项属性,包括命令长度、地址长度、交替字节长度、数据长度以及数据传输模式(单线、双线、四线)等。
  • quadspi_sr(quadspi status register)

    • 用途:用于查看qspi当前的工作状态,包括读取fifo的状态、传输状态、错误标志等信息,是判断当前操作是否完成、是否有错误的重要依据。
  • quadspi_fcr(quadspi flag clear register)

    • 用途:用于清除qspi_sr中的一些状态标志位,当这些标志位被硬件置位表示某种事件发生后,可以通过写入fcr寄存器来清除它们,以便重新开始新的操作。
  • quadspi_dlr(quadspi data length register)

    • 用途:用于设置在间接模式下进行数据传输时的字节数目,当需要传输固定长度的数据时,将这一长度写入dlr寄存器。
  • quadspi_ar(quadspi address register)

    • 用途:在需要向spi flash发送地址信息的命令序列中,用于指定待访问的spi flash地址。
  • quadspi_dr(quadspi data register)

    • 用途:在间接模式下,用于向spi flash发送或接收数据,即作为数据发送和接收的缓冲区。在进行数据传输前,可以预先将待发送的数据写入此寄存器,或是从该寄存器读取接收到的数据。
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述
      在这里插入图片描述

三、qspi相关hal库驱动介绍

在这里插入图片描述
在stm32 hal库中,qspi相关的驱动函数与寄存器的关系及功能描述如下:

  • __hal_rcc_qspi_clk_enable

    • 关联寄存器:ahb3enr(advanced high-performance bus 3 enable register)
    • 功能描述:该函数用于使能qspi外设的时钟,通过设置ahb3enr寄存器中的相关位来开启qspi的时钟源。
  • hal_qspi_init

    • 关联寄存器:cr(control register)和 dcr(device configuration register)
    • 功能描述:初始化qspi外设,配置qspi的基本工作模式、时钟模式、数据线数等基本信息,同时也包括根据外部spi flash设备的特点配置dcr寄存器中的相关参数。
  • hal_qspi_mspinit

    • 功能描述:这是一个用户自定义的初始化回调函数,主要用于初始化qspi相关的gpio引脚和其他硬件资源,不属于直接与寄存器关联的函数。
  • hal_qspi_command

    • 关联寄存器:ccr(communication configuration register)、ar(address register)和 dlr(data length register)
    • 功能描述:配置和发送qspi命令序列,包括命令字、地址和可能的交替字节等,ccr用于配置命令序列的各个组成部分,ar用于设置地址信息,dlr用于设置数据长度。
  • hal_qspi_receivehal_qspi_transmit

    • 关联寄存器:ccr、dlr、ar、dr(data register)、sr(status register)和 fcr(flag clear register)
    • 功能描述:这两个函数分别用于从qspi接收数据和向qspi发送数据。在发送和接收过程中,ccr、ar、dlr用于配置传输参数,dr用于传输数据,sr用于查询当前状态,fcr则用于清除状态标志位。
  • qspi相关的结构体

    • qspi_handletypedef:包含了qspi外设的所有句柄信息,包括指向各种寄存器的指针、缓冲区指针、传输长度等。
    • qspi_inittypedef:用于配置qspi的基本工作参数,如时钟模式、数据线数等。
    • qspi_commandtypedef:用于配置和描述qspi的命令结构,包括命令字、地址、数据长度、交替字节等信息。
      在这里插入图片描述
      在这里插入图片描述

四、qspi基本使用步骤

在这里插入图片描述
qspi(quad serial peripheral interface)在stm32上的基本使用步骤可以总结为:

  1. qspi相关gpio口配置

    • 根据所用qspi闪存模式(例如单线、双线、四线模式)和bank(如果支持多bank的话)确定需要用到的gpio引脚。
    • 将这些引脚配置为复用推挽输出模式,即将它们映射到qspi功能,并设置为合适的电气特性以支持高速通信。

    示例代码片段(伪代码):

    gpio_inittypedef gpio_initstruct;
    gpio_initstruct.pin = gpio_pin_...; // 设置对应qspi引脚
    gpio_initstruct.mode = gpio_mode_af_pp; // 复用推挽输出
    gpio_initstruct.pull = gpio_nopull; // 通常不用上下拉电阻,视具体情况而定
    gpio_initstruct.speed = gpio_speed_freq_very_high; // 设置为高速模式
    gpio_initstruct.alternate = gpio_af10_quadspi; // 设置为qspi功能
    hal_gpio_init(gpiox, &gpio_initstruct); // 初始化gpio
    
  2. 设置qspi相关参数及时钟

    • 创建并填充qspi_handletypedef结构体,设置qspi的工作模式、数据线数、时钟速率等参数。
    • 调用hal_rccex_getperiphclkfreq()获取qspi时钟频率。
    • 调用hal_qspi_init()函数进行初始化,传入上述配置好的qspi_handletypedef结构体。

    示例代码片段:

    qspi_handletypedef hqspi;
    hqspi.instance = quadspi;
    hqspi.init.clockprescaler = ...; // 设置时钟预分频
    hqspi.init.fifothreshold = ...; // 设置fifo阈值
    hqspi.init.sampleshifting = ...; // 是否启用样本位移
    ...
    hal_qspi_init(&hqspi);
    
  3. 使能qspi中断及设置mpu(memory protection unit,内存保护单元)(可选)

    • 如果需要使用中断功能,可以通过设置qspi的中断标志位并调用hal_nvic_enableirq()函数来使能qspi中断。
    • 若需要使用内存映射模式,可能需要配置mpu,以确保对映射到内存空间的qspi flash进行合理的权限和访问控制。
  4. 编写qspi基本通信接口

    • 使用hal库提供的函数进行命令发送、数据接收和数据发送:
      • 发送命令:hal_qspi_command(&hqspi, ..., ...)
      • 接收数据:hal_qspi_receive(&hqspi, ..., ...)
      • 发送数据:hal_qspi_transmit(&hqspi, ..., ...)

在实际应用中,还需要结合具体的应用场景和闪存芯片的数据手册进行详细配置和操作。例如,在进行读写操作前,可能需要先发送特定的读写命令,并根据需要擦除或写入扇区地址。在完成操作后,可能需要轮询状态寄存器或等待中断来确认操作完成。

五、spi flash简介

在这里插入图片描述
w25q128是一款16mb(16,777,216字节)容量的spi(serial peripheral interface)接口的nor型闪存芯片,具备高速读写性能和出色的耐用性,支持多次重复擦写且在断电后仍能保持数据完整性,数据保存期限长达20年。

在基本操作方面,w25q128支持以下操作:

  1. 擦除:w25q128的最小擦除单位是一个扇区,也就是4kb(4096字节)。这意味着用户无法单独擦除某个字节或字节组,而必须按照扇区为单位进行擦除操作。

  2. 写入:写入操作通常以页为单位进行,每个扇区包含16个页,每个页大小为256字节。不过在写入之前,所写的扇区必须先被擦除。

  3. 读取:支持随机读取任意位置的数据,不受擦除或写入操作的限制。

w25q128内部存储空间组织结构如下:

  • 整体布局:16mb的总存储空间划分为256个块(block)。
  • 块大小:每个块的大小为64kb(65,536字节)。
  • 扇区划分:每个块又被分成16个扇区,每个扇区大小为4kb(4096字节)。
  • 页结构:每个扇区内部进一步细分为16个页,每页256字节。

因此,在对w25q128进行编程或应用开发时,应按照块、扇区和页的层级结构进行数据管理,确保符合器件的擦写和读取规则,以提高数据操作效率和延长闪存寿命。
在这里插入图片描述
w25q128jv这款spi闪存芯片支持多种spi接口模式,以适应不同的应用需求和提高数据传输速率:

  • 标准spi(single spi):使用一条数据线(mosi和miso各一条),进行单线数据传输。
  • 双线spi(dual spi):使用两条数据线进行并行数据传输,有效加倍了数据传输速度。
  • 四线spi(quad spi或qspi):使用四条数据线进行并行数据传输,数据吞吐量是标准spi的四倍。

在高速模式下,w25q128jv的最高时钟频率可以达到133mhz。在双线spi模式下,由于数据线翻倍,理论上的等效传输速率将达到266mhz;在四线spi模式下,四条数据线并行工作,理论上其等效传输速率将进一步提高至532mhz。这种高速特性使得w25q128jv在处理大量数据和需要快速读取/写入的应用中表现优秀。不过,实际应用中,设备的实际工作时钟频率应根据微控制器的spi接口性能和系统稳定性综合考虑,并在器件数据手册规定的范围内进行设置。
在这里插入图片描述
spi flash(比如w25q128)的基本操作指令集:

指令(hex)名称作用
0x06写使能(write enable)在执行写入数据或擦除操作之前,必须先发送此指令,以使spi flash进入可写状态。
0x05读状态寄存器1(read status register 1)用于检测spi flash是否处于空闲状态,是否准备好接受新的擦除或写入操作。
0x03读数据(read data)常规读取spi flash中的数据,不是快速读取。
0xeb快速读取数据(fast read)用于更快地读取spi flash数据,可能需要配合地址和dummy cycles(空闲时钟周期)来提高数据传输速度。
0x32页写(page program)用于向spi flash写入数据,每次操作最多写入256字节(一页)的数据。
0x20扇区擦除(sector erase)对spi flash执行最小擦除单位操作,即擦除一个扇区(通常为4096字节)的数据。

关于状态寄存器(status register, sr)相关的额外命令:

指令(hex)名称作用
0x35读状态寄存器2(read status register 2)用于读取sr2中的内容,其中包括qe(quad enable)位,用于启用四线spi模式(quad spi)。
0x31写状态寄存器2(write status register 2)用于设置sr2中的qe位,使能四线spi模式。
0x15读状态寄存器3(read status register 3)在某些spi flash中用于判断地址模式(例如4字节地址模式)是否被启用。
0x11写状态寄存器3(write status register 3)用于在上电时设置spi flash的工作模式,例如启用4字节地址模式。
0xb7使能4字节地址模式(enter 4-byte address mode)某些spi flash需要发送特定命令来切换到4字节地址模式,以便访问更大的存储空间。

请注意,不同的spi flash芯片可能存在略微不同的指令集和功能,具体操作请参阅各自的数据手册以获取准确信息。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

六、spi flash基本使用步骤

在这里插入图片描述
spi flash w25q128的基本使用步骤可以简化描述如下:

1. qspi配置

  • 初始化qspi相关的gpio引脚为复用推挽输出模式,并配置相关寄存器以设置qspi的工作模式(单线、双线、四线模式)、时钟速率、数据位宽等参数。
  • 调用hal库的初始化函数如hal_qspi_init()对qspi外设进行初始化。

2. w25q128读取

  • 发送快速读取指令(0xeb):使用单线传输指令,然后四线传输地址(对于四线spi模式),接着四线接收数据。
  • 示例代码(伪代码):
    // 设置地址和数据长度
    uint32_t read_address = ...;
    uint32_t data_length = ...;
    
    // 构造读取命令包
    qspi_command_packet.command = 0xeb; // 快速读取指令
    qspi_command_packet.address = read_address;
    qspi_command_packet.dummy_cycles = ...; // 根据器件手册配置
    qspi_command_packet.data_length = data_length;
    
    // 发送读取命令并接收数据
    hal_qspi_command_it(&hqspi, &qspi_command_packet);
    hal_qspi_receive_it(&hqspi, receive_buffer, data_length);
    

3. w25q128扇区擦除

  • 发送扇区擦除指令(0x20):使用单线传输指令,然后单线传输地址(对于四线spi模式,此处也是单线地址),擦除操作无需传输数据。
  • 示例代码(伪代码):
    // 设置要擦除的扇区地址
    uint32_t erase_address = ...;
    
    // 构造擦除命令包
    qspi_command_packet.command = 0x20; // 扇区擦除指令
    qspi_command_packet.address = erase_address;
    qspi_command_packet.data_length = 0; // 擦除操作无数据传输
    
    // 发送擦除命令
    hal_qspi_command_it(&hqspi, &qspi_command_packet);
    // 等待擦除完成(通常通过读取状态寄存器或中断实现)
    

4. w25q128写入

  • 可选:在写入数据前,检查目标地址所在的扇区是否需要先进行擦除操作。
  • 发送页写入指令(0x02或0x32,此处使用0x32为例):使用单线传输指令,然后单线传输地址,最后四线传输数据。
  • 示例代码(伪代码):
    // 确认擦除(如果需要)
    // ...
    
    // 设置写入地址和数据
    uint32_t write_address = ...;
    uint8_t* write_data = ...;
    uint32_t write_length = ...;
    
    // 构造写入命令包
    qspi_command_packet.command = 0x32; // 页写入指令(四线模式)
    qspi_command_packet.address = write_address;
    qspi_command_packet.data_length = write_length;
    qspi_command_packet.pdata = write_data;
    
    // 发送写入命令并发送数据
    hal_qspi_command_it(&hqspi, &qspi_command_packet);
    
    // 等待写入完成(同样通过读取状态寄存器或中断实现)
    

spi flash驱动注意事项

  • 是否需要擦除:在写入新数据前,要确保目标区域已被擦除,因为spi flash只能向已擦除的区域写入新数据。
  • 写入数据:在进行写入操作时,要注意数据的写入粒度是按照页进行的,所以需要保证数据长度合适,并且地址对齐到页的边界。
  • 遵循读-改-写原则:对于修改现有数据的情况,应遵循先读取原有数据、修改部分数据、然后将整个页的数据重新写入的流程,这是因为spi flash不能直接覆盖已写入的数据。

七、编程实战

源码

qspi.c

#include "./bsp/qspi/qspi.h"


qspi_handletypedef g_qspi_handle;    /* qspi句柄 */

/**
 * @brief       等待状态标志
 * @param       flag : 需要等待的标志位
 * @param       sta  : 需要等待的状态
 * @param       wtime: 等待时间
 * @retval      0, 等待成功; 1, 等待失败.
 */
uint8_t qspi_wait_flag(uint32_t flag, uint8_t sta, uint32_t wtime)
{
    uint8_t flagsta = 0;

    while (wtime)
    {
        flagsta = (quadspi->sr & flag) ? 1 : 0;     /* 获取状态标志 */

        if (flagsta == sta)
        {
            wtime--;
        }
        break;
    }

    if (wtime)
    {
        return 0;
    }
    else
    {
        return 1;
    }
}

/**
 * @brief       初始化qspi接口
 * @param       无
 * @retval      0, 成功; 1, 失败.
 */
uint8_t qspi_init(void)
{
    g_qspi_handle.instance                  = quadspi;                          /* qspi */
    g_qspi_handle.init.clockprescaler       = 1;                                /* qspi分频比,by25q128最大频率为108m,
                                                                                   所以此处应该为2,qspi频率就为220/(1+1)=110mhz
                                                                                   稍微有点超频,可以正常就好,不行就只能降低频率 */
    g_qspi_handle.init.fifothreshold        = 4;                                /* fifo阈值为4个字节 */
    g_qspi_handle.init.sampleshifting       = qspi_sample_shifting_halfcycle;   /* 采样移位半个周期(ddr模式下,必须设置为0) */
    g_qspi_handle.init.flashsize            = 25 - 1;                           /* spi flash大小,by25q128大小为32m字节,2^25,所以取权值25 - 1 = 24 */
    g_qspi_handle.init.chipselecthightime   = qspi_cs_high_time_3_cycle;        /* 片选高电平时间为3个时钟(9.1 * 3 = 27.3ns),即手册里面的tshsl参数 */
    g_qspi_handle.init.clockmode            = qspi_clock_mode_3;                /* 模式3 */
    g_qspi_handle.init.flashid              = qspi_flash_id_1;                  /* 第一片flash */
    g_qspi_handle.init.dualflash            = qspi_dualflash_disable;           /* 禁止双闪存模式 */

    if (hal_qspi_init(&g_qspi_handle) == hal_ok)
    {
        return 0;      /* qspi初始化成功 */
    }
    else
    {
        return 1;
    }
}

/**
 * @brief       qspi底层驱动,引脚配置,时钟使能
 * @param       hqspi:qspi句柄
 * @note        此函数会被hal_qspi_init()调用
 * @retval      0, 成功; 1, 失败.
 */
void hal_qspi_mspinit(qspi_handletypedef *hqspi)
{
    gpio_inittypedef gpio_init_struct;

    __hal_rcc_qspi_clk_enable();      /* 使能qspi时钟 */
    __hal_rcc_gpiob_clk_enable();     /* gpiob时钟使能 */
    __hal_rcc_gpiod_clk_enable();     /* gpiod时钟使能 */
    __hal_rcc_gpioe_clk_enable();     /* gpioe时钟使能 */

    gpio_init_struct.pin = qspi_bk1_ncs_gpio_pin;
    gpio_init_struct.mode = gpio_mode_af_pp;                     /* 复用 */
    gpio_init_struct.pull = gpio_pullup;                         /* 上拉 */
    gpio_init_struct.speed = gpio_speed_freq_very_high;          /* 高速 */
    gpio_init_struct.alternate = gpio_af10_quadspi;              /* 复用为qspi */
    hal_gpio_init(qspi_bk1_ncs_gpio_port, &gpio_init_struct);    /* 初始化qspi_bk1_ncs引脚 */

    gpio_init_struct.pin = qspi_bk1_clk_gpio_pin;
    gpio_init_struct.mode = gpio_mode_af_pp;                     /* 复用 */
    gpio_init_struct.pull = gpio_pullup;                         /* 上拉 */
    gpio_init_struct.speed = gpio_speed_freq_very_high;          /* 高速 */
    gpio_init_struct.alternate = gpio_af9_quadspi;               /* 复用为qspi */
    hal_gpio_init(qspi_bk1_clk_gpio_port, &gpio_init_struct);    /* 初始化qspi_bk1_clk引脚 */

    gpio_init_struct.pin = qspi_bk1_io0_gpio_pin;
    hal_gpio_init(qspi_bk1_io0_gpio_port, &gpio_init_struct);    /* 初始化qspi_bk1_io0引脚 */

    gpio_init_struct.pin = qspi_bk1_io1_gpio_pin;
    hal_gpio_init(qspi_bk1_io1_gpio_port, &gpio_init_struct);    /* 初始化qspi_bk1_io1引脚 */

    gpio_init_struct.pin = qspi_bk1_io2_gpio_pin;
    hal_gpio_init(qspi_bk1_io2_gpio_port, &gpio_init_struct);    /* 初始化qspi_bk1_io2引脚 */

    gpio_init_struct.pin = qspi_bk1_io3_gpio_pin;
    hal_gpio_init(qspi_bk1_io3_gpio_port, &gpio_init_struct);    /* 初始化qspi_bk1_io3引脚 */
}

/**
 * @brief       qspi发送命令
 * @param       cmd : 要发送的指令
 * @param       addr: 发送到的目的地址
 * @param       mode: 模式,详细位定义如下:
 *   @arg       mode[1:0]: 指令模式; 00,无指令;  01,单线传输指令; 10,双线传输指令; 11,四线传输指令.
 *   @arg       mode[3:2]: 地址模式; 00,无地址;  01,单线传输地址; 10,双线传输地址; 11,四线传输地址.
 *   @arg       mode[5:4]: 地址长度; 00,8位地址; 01,16位地址;     10,24位地址;     11,32位地址.
 *   @arg       mode[7:6]: 数据模式; 00,无数据;  01,单线传输数据; 10,双线传输数据; 11,四线传输数据.
 * @param       dmcycle: 空指令周期数
 * @retval      无
 */
void qspi_send_cmd(uint8_t cmd, uint32_t addr, uint8_t mode, uint8_t dmcycle)
{
    qspi_commandtypedef qspi_command_init;
    
    qspi_command_init.sioomode            = qspi_sioo_inst_every_cmd;     /* 每次都发送指令 */
    qspi_command_init.ddrmode             = qspi_ddr_mode_disable;        /* 关闭ddr模式,使用sdr模式 */
    qspi_command_init.ddrholdhalfcycle    = qspi_ddr_hhc_analog_delay;    /* ddr模式下,用于设置延迟半个时钟周期再数据输出 */

    /* 指令阶段 */
    qspi_command_init.instruction         = cmd;                          /* 要发送的指令 */
    /* 设置指令阶段需要几线模式 */
    if (((mode >> 0) & 0x03) == 0)
        qspi_command_init.instructionmode = qspi_instruction_none;        /* 不需要指令阶段 */
    if (((mode >> 0) & 0x03) == 1)
        qspi_command_init.instructionmode = qspi_instruction_1_line;      /* 单线模式 */
    if (((mode >> 0) & 0x03) == 2)
        qspi_command_init.instructionmode = qspi_instruction_2_lines;     /* 双线模式 */
    if (((mode >> 0) & 0x03) == 3)
        qspi_command_init.instructionmode = qspi_instruction_4_lines;     /* 四线模式 */
        
    /* 地址阶段 */
    qspi_command_init.address             = addr;                         /* 要发送的地址 */
    /* 设置地址长度 */
    if (((mode >> 4) & 0x03) == 0)
        qspi_command_init.addresssize     = qspi_address_8_bits;          /* 8位地址 */
    if (((mode >> 4) & 0x03) == 1)
        qspi_command_init.addresssize     = qspi_address_16_bits;         /* 16位地址 */
    if (((mode >> 4) & 0x03) == 2)
        qspi_command_init.addresssize     = qspi_address_24_bits;         /* 24位地址 */
    if (((mode >> 4) & 0x03) == 3)
        qspi_command_init.addresssize     = qspi_address_32_bits;         /* 32位地址 */
    /* 设置地址阶段需要几线模式 */
    if (((mode >> 2) & 0x03) == 0)
        qspi_command_init.addressmode     = qspi_address_none;            /* 不需要地址阶段 */
    if (((mode >> 2) & 0x03) == 1)
        qspi_command_init.addressmode     = qspi_address_1_line;          /* 单线模式 */
    if (((mode >> 2) & 0x03) == 2)
        qspi_command_init.addressmode     = qspi_address_2_lines;         /* 双线模式 */
    if (((mode >> 2) & 0x03) == 3)
        qspi_command_init.addressmode     = qspi_address_4_lines;         /* 四线模式 */
    
    /* 交替字节阶段 */
    qspi_command_init.alternatebytes      = 0;                            /* 交替字节内容 */
    qspi_command_init.alternatebytessize  = qspi_alternate_bytes_8_bits;  /* 交替字节长度 */
    qspi_command_init.alternatebytemode   = qspi_alternate_bytes_none;    /* 交替字节阶段需要几线模式 */
    
    /* 空指令周期阶段 */
    qspi_command_init.dummycycles         = dmcycle;                      /* 空指令周期数 */
    
    /* 数据阶段 */
    /* 不设置nbdata成员,在qspi_transmit/receive函数中指定 */
//    qspi_command_init.nbdata              = ;                           /* 数据长度 */
    /* 设置数据阶段需要几线模式 */
    if (((mode >> 6) & 0x03) == 0)
        qspi_command_init.datamode        = qspi_data_none;               /* 不需要数据阶段 */
    if (((mode >> 6) & 0x03) == 1)
        qspi_command_init.datamode        = qspi_data_1_line;             /* 单线模式 */
    if (((mode >> 6) & 0x03) == 2)
        qspi_command_init.datamode        = qspi_data_2_lines;            /* 双线模式 */
    if (((mode >> 6) & 0x03) == 3)
        qspi_command_init.datamode        = qspi_data_4_lines;            /* 四线模式 */
        
    hal_qspi_command(&g_qspi_handle, &qspi_command_init, 5000);           /* 用于向qspi flash发送命令 */
}

/**
 * @brief       qspi发送指定长度的数据
 * @param       buf     : 发送数据缓冲区首地址
 * @param       datalen : 要传输的数据长度
 * @retval      0, 成功; 其他, 错误代码
 */
uint8_t qspi_transmit(uint8_t *buf, uint32_t datalen)
{
    g_qspi_handle.instance->dlr = datalen - 1;  /* 直接使用寄存器赋值的方式设置要发送的数据字节数 */
    
    if (hal_qspi_transmit(&g_qspi_handle, buf, 5000) == hal_ok)
    {
        return 0;
    }
    else
    {
        return 1;
    }
}


/**
 * @brief       qspi接收指定长度的数据
 * @param       buf     : 接收数据缓冲区首地址
 * @param       datalen : 要传输的数据长度
 * @retval      0, 成功; 其他, 错误代码.
 */
uint8_t qspi_receive(uint8_t *buf, uint32_t datalen)
{
    g_qspi_handle.instance->dlr = datalen - 1;  /* 直接使用寄存器赋值的方式设置要发送的数据字节数 */

    if (hal_qspi_receive(&g_qspi_handle, buf, 5000) == hal_ok)
    {
        return 0;
    }
    else
    {
        return 1;
    }
}

qspi.h

#ifndef __qspi_h
#define __qspi_h

#include "./system/sys/sys.h"


/******************************************************************************************/
/* qspi 相关 引脚 定义 */

#define qspi_bk1_clk_gpio_port          gpiob
#define qspi_bk1_clk_gpio_pin           gpio_pin_2
#define qspi_bk1_clk_gpio_af            gpio_af9_quadspi
#define qspi_bk1_clk_gpio_clk_enable()  do{ __hal_rcc_gpiob_clk_enable; }while(0)   /* pb口时钟使能 */

#define qspi_bk1_ncs_gpio_port          gpiob
#define qspi_bk1_ncs_gpio_pin           gpio_pin_6
#define qspi_bk1_ncs_gpio_af            gpio_af10_quadspi
#define qspi_bk1_ncs_gpio_clk_enable()  do{ __hal_rcc_gpiob_clk_enable; }while(0)   /* pb口时钟使能 */

#define qspi_bk1_io0_gpio_port          gpiod
#define qspi_bk1_io0_gpio_pin           gpio_pin_11
#define qspi_bk1_io0_gpio_af            gpio_af9_quadspi
#define qspi_bk1_io0_gpio_clk_enable()  do{ __hal_rcc_gpiod_clk_enable; }while(0)   /* pd口时钟使能 */

#define qspi_bk1_io1_gpio_port          gpiod
#define qspi_bk1_io1_gpio_pin           gpio_pin_12
#define qspi_bk1_io1_gpio_af            gpio_af9_quadspi
#define qspi_bk1_io1_gpio_clk_enable()  do{ __hal_rcc_gpiod_clk_enable; }while(0)   /* pd口时钟使能 */

#define qspi_bk1_io2_gpio_port          gpiod
#define qspi_bk1_io2_gpio_pin           gpio_pin_13
#define qspi_bk1_io2_gpio_af            gpio_af9_quadspi
#define qspi_bk1_io2_gpio_clk_enable()  do{ __hal_rcc_gpiod_clk_enable; }while(0)   /* pd口时钟使能 */

#define qspi_bk1_io3_gpio_port          gpioe
#define qspi_bk1_io3_gpio_pin           gpio_pin_2
#define qspi_bk1_io3_gpio_af            gpio_af9_quadspi
#define qspi_bk1_io3_gpio_clk_enable()  do{ __hal_rcc_gpioe_clk_enable; }while(0)   /* pe口时钟使能 */

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


uint8_t qspi_wait_flag(uint32_t flag, uint8_t sta, uint32_t wtime); /* qspi等待某个状态 */
uint8_t qspi_init(void);    /* 初始化qspi */
void qspi_send_cmd(uint8_t cmd, uint32_t addr, uint8_t mode, uint8_t dmcycle);  /* qspi发送命令 */
uint8_t qspi_receive(uint8_t *buf, uint32_t datalen);   /* qspi接收数据 */
uint8_t qspi_transmit(uint8_t *buf, uint32_t datalen);  /* qspi发送数据 */

#endif

norflash.c

#include "./bsp/qspi/qspi.h"
#include "./system/delay/delay.h"
#include "./system/usart/usart.h"
#include "./bsp/norflash/norflash.h"


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

/* spi flash 地址位宽 */
volatile uint8_t g_norflash_addrw = 2;  /* spi flash地址位宽, 在norflash_read_id函数里面被修改
                                         * 2, 表示24bit地址宽度
                                         * 3, 表示32bit地址宽度
                                         */

/**
 * @brief       初始化nor flash
 * @param       无
 * @retval      无
 */
void norflash_init(void)
{
    uint8_t temp;
    
    qspi_init();                            /* 初始化qspi */
    norflash_qspi_disable();                /* 退出qpi模式(避免芯片之前进入这个模式,导致下载失败) */
    norflash_qe_enable();                   /* 使能qe位 */
    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_write_enable();        /* 写使能 */
            
            /* spi, 使能4字节地址指令, 地址为0, 无数据_8位地址_无地址_单线传输指令, 无空指令周期 */
            qspi_send_cmd(flash_enable4byteaddr, 0, (0 << 6) | (0 << 4) | (0 << 2) | (1 << 0), 0); 
        }
    }

    //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       退出qspi模式
 * @param       无
 * @retval      无
 */
static void norflash_qspi_disable(void)
{
    /* 退出qpi模式指令, 地址为0, 无数据_8位地址_无地址_4线传输指令, 无空周期 */
    qspi_send_cmd(flash_exitqpimode, 0, (0 << 6) | (0 << 4) | (0 << 2) | (3 << 0), 0);
}

/**
 * @brief       使能flash qe位,使能io2/io3
 * @param       无
 * @retval      无
 */
static void norflash_qe_enable(void)
{
    uint8_t stareg2 = 0;
    
    stareg2 = norflash_read_sr(2);      /* 先读出状态寄存器2的原始值 */

    //printf("stareg2:%x\r\n", stareg2);
    if ((stareg2 & 0x02) == 0)          /* qe位未使能 */
    {
        norflash_write_enable();        /* 写使能 */
        stareg2 |= 1 << 1;              /* 使能qe位 */
        norflash_write_sr(2, stareg2);  /* 写状态寄存器2 */
    }
}

/**
 * @brief       25qxx写使能
 *   @note      将sr1寄存器的wel置位
 * @param       无
 * @retval      无
 */
void norflash_write_enable(void)
{
    /* spi, 写使能指令, 地址为0, 无数据_8位地址_无地址_单线传输指令, 无空周期 */
    qspi_send_cmd(flash_writeenable, 0, (0 << 6) | (0 << 4) | (0 << 2) | (1 << 0), 0);
}

/**
 * @brief       25qxx写禁止
 *   @note      将s1寄存器的wel清零
 * @param       无
 * @retval      无
 */
void norflash_write_disable(void)
{
    /* spi, 写禁止指令, 地址为0, 无数据_8位地址_无地址_单线传输指令, 无空周期 */
    qspi_send_cmd(flash_writedisable, 0, (0 << 6) | (0 << 4) | (0 << 2) | (1 << 0), 0);
}

/**
 * @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;
    }

    /* spi, 发送command指令, 地址为0, 单线传输数据_8位地址_无地址_单线传输指令,无空周期 */
    qspi_send_cmd(command, 0, (1 << 6) | (0 << 4) | (0 << 2) | (1 << 0), 0);
    qspi_receive(&byte, 1);     /* 读状态寄存器指令会返回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;
    }

    /* spi, 发送command指令, 地址为0, 单线传输数据_8位地址_无地址_单线传输指令,无空周期,1个字节数据 */
    qspi_send_cmd(command, 0, (1 << 6) | (0 << 4) | (0 << 2) | (1 << 0), 0);
    qspi_transmit(&sr, 1);      /* 写状态寄存器指令需要写入1个字节数据 */
}

/**
 * @brief       读取芯片id
 * @param       无
 * @retval      flash芯片id
 *   @note      芯片id列表见: norflash.h, 芯片列表部分
 */
uint16_t norflash_read_id(void)
{
    uint8_t temp[2];
    uint16_t deviceid;
    
    qspi_init();    /* 进行库函数调用前要先初始化 */
    /* spi, 读id指令, 地址为0, 单线传输数据_24位地址_单线传输地址_单线传输指令, 无空周期 */
    qspi_send_cmd(flash_manufactdeviceid, 0, (1 << 6) | (2 << 4) | (1 << 2) | (1 << 0), 0);
    qspi_receive(temp, 2);     /* 读状态寄存器指令会返回2个字节数据 */
    
    deviceid = (temp[0] << 8) | temp[1];

    if (deviceid == w25q256)
    {
        g_norflash_addrw = 3;   /* 如果是w25q256, 标记32bit地址宽度 */
    }

    return deviceid;
}

/**
 * @brief       读取spi flash,仅支持qspi模式
 *   @note      在指定地址开始读取指定长度的数据
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始读取的地址(最大32bit)
 * @param       datalen : 要读取的字节数(最大65535)
 * @retval      无
 */
void norflash_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    /* qspi, 快速读数据指令, 地址为addr, 4线传输数据_24/32位地址_4线传输地址_1线传输指令, 6个空指令周期 */
    qspi_send_cmd(flash_fastreadquad, addr, (3 << 6) | (g_norflash_addrw << 4) | (3 << 2) | (1 << 0), 6);
    qspi_receive(pbuf, datalen);    /* 快速读数据指令会返回设置的datalen个字节数据 */
}

/**
 * @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)
{
    norflash_write_enable();        /* 写使能 */

    /* qspi, 页写指令, 地址为addr, 4线传输数据_24/32位地址_1线传输地址_1线传输指令, 无空周期 */
    qspi_send_cmd(flash_pageprogramquad, addr, (3 << 6) | (g_norflash_addrw << 4) | (1 << 2) | (1 << 0), 0);
    qspi_transmit(pbuf, datalen);   /* 页写指令会需要发送设置的datalen个字节数据 */

    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();       /* 等待空闲 */
    /* spi, 写全片擦除指令, 地址为0, 无数据_8位地址_无地址_1线传输指令, 无空周期 */
    qspi_send_cmd(flash_chiperase, 0, (0 << 6) | (0 << 4) | (0 << 2) | (1 << 0), 0);
    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();           /* 等待空闲 */

    /* spi, 写扇区擦除指令, 地址为0, 无数据_24/32位地址_1线传输地址_1线传输指令, 无空周期 */
    qspi_send_cmd(flash_sectorerase, saddr, (0 << 6) | (g_norflash_addrw << 4) | (1 << 2) | (1 << 0), 0);

    norflash_wait_busy();           /* 等待擦除完成 */
}

norflash.h

#ifndef __norflash_h
#define __norflash_h

#include "./system/sys/sys.h"


/* 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_qe_enable(void);       /* 使能qe位 */
static void norflash_qspi_disable(void);    /* 退出qpi模式 */
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);           /* 写使能 */
void norflash_write_disable(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

norflash_ex.c

#include "./bsp/qspi/qspi.h"
#include "./bsp/norflash/norflash.h"
#include "./bsp/norflash/norflash_ex.h"


extern uint8_t g_norflash_addrw;    /* 表示当前是24bit/32bit数据位宽, 在norflash.c里面定义 */

/**
 * @brief       qspi接口进入内存映射模式
 *   @note      调用该函数之前务必已经初始化了qspi接口
 *              sys_qspi_enable_memmapmode or norflash_init
 * @param       无
 * @retval      无
 */
static void norflash_ex_enter_mmap(void)
{
    uint32_t tempreg = 0;

    /* by/w25qxx 写使能(0x06指令) */
    while (quadspi->sr & (1 << 5)); /* 等待busy位清零 */

    quadspi->ccr = 0x00000106;      /* 发送0x06指令,by/w25qxx写使能 */

    while ((quadspi->sr & (1 << 1)) == 0);  /* 等待指令发送完成 */

    quadspi->fcr |= 1 << 1;

    if (qspi_wait_flag(1 << 5, 0, 0xffff) == 0) /* 等待busy空闲 */
    {
        tempreg = 0xeb;         /* instruction[7:0]=0xeb,发送0xeb指令(fast read quad i/o) */
        tempreg |= 1 << 8;      /* imode[1:0]=1,单线传输指令 */
        tempreg |= 3 << 10;     /* address[1:0]=3,四线传输地址 */ 
        tempreg |= (uint32_t)g_norflash_addrw << 12;    /* adsize[1:0]=2,24/32位地址长度 */
        tempreg |= 3 << 14;     /* abmode[1:0]=3,四线传输交替字节 */
        tempreg |= 0 << 16;     /* absize[1:0]=0,8位交替字节(m0~m7) */
        tempreg |= 4 << 18;     /* dcyc[4:0]=4,4个dummy周期 */
        tempreg |= 3 << 24;     /* dmode[1:0]=3,四线传输数据 */
        tempreg |= 3 << 26;     /* fmode[1:0]=3,内存映射模式 */
        quadspi->ccr = tempreg; /* 设置ccr寄存器 */
    }

    sys_intx_enable();          /* 开启中断 */
}

/**
 * @brief       qspi接口退出内存映射模式
 *   @note      调用该函数之前务必已经初始化了qspi接口
 *              sys_qspi_enable_memmapmode or norflash_init
 * @param       无
 * @retval      0, ok;  其他, 错误代码
 */
static uint8_t norflash_ex_exit_mmap(void)
{
    uint8_t res = 0;

    sys_intx_disable();         /* 关闭中断 */
    scb_invalidateicache();     /* 清空i cache */
    scb_invalidatedcache();     /* 清空d cache */
    quadspi->cr &= ~(1 << 0);   /* 关闭 qspi 接口 */
    quadspi->cr |= 1 << 1;      /* 退出memmaped模式 */
    res = qspi_wait_flag(1 << 5, 0, 0xffff);    /* 等待busy空闲 */

    if (res == 0)
    {
        quadspi->ccr = 0;       /* ccr寄存器清零 */
        quadspi->cr |= 1 << 0;  /* 使能 qspi 接口 */
    }

    return res;
}

/**
 * @brief       往 qspi flash写入数据
 *   @note      在指定地址开始写入指定长度的数据
 *              该函数带擦除操作!
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始写入的地址(最大32bit)
 * @param       datalen : 要写入的字节数(最大65535)
 * @retval      0, ok;  其他, 错误代码
 */
uint8_t norflash_ex_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint8_t res = 0;
    res = norflash_ex_exit_mmap();  /* 退出内存映射模式 */

    if (res == 0)
    {
        norflash_write(pbuf, addr, datalen);
    }

    norflash_ex_enter_mmap();       /* 进入内存映射模式 */
    return res;
}

/**
 * @brief       从 qspi flash 读取数据
 *   @note      在指定地址开始读取指定长度的数据(必须处于内存映射模式下,才可以执行)
 *
 * @param       pbuf    : 数据存储区
 * @param       addr    : 开始读取的地址(最大32bit)
 * @param       datalen : 要读取的字节数(最大65535)
 * @retval      0, ok;  其他, 错误代码
 */
void norflash_ex_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i = 0;
    addr += 0x90000000;     /* 使用内存映射模式读取,qspi的基址是0x90000000,所以这里要加上基址 */
    sys_intx_disable();     /* 关闭中断 */

    for (i = 0; i < datalen; i++)
    {
        pbuf[i] = *(volatile uint8_t *)(addr + i);
    }

    sys_intx_enable();      /* 开启中断 */
}

/**
 * @brief       读取qspi flash的id
 * @param       无
 * @retval      nor flash id
 */
uint16_t norflash_ex_read_id(void)
{
    uint8_t res = 0;
    uint16_t id = 0; 
    res = norflash_ex_exit_mmap();  /* 退出内存映射模式 */

    if (res == 0)
    {
        id = norflash_read_id();
    }

    norflash_ex_enter_mmap();       /* 进入内存映射模式 */
    return id;
}

/**
 * @brief       擦除qspi flash的某个扇区
 *   @note      注意,这里是扇区地址,不是字节地址!!
 *              擦除一个扇区的最少时间:150ms
 *
 * @param       saddr: 扇区地址
 * @retval      无
 */
void norflash_ex_erase_sector(uint32_t addr)
{
    uint8_t res = 0;
    res = norflash_ex_exit_mmap();  /* 退出内存映射模式 */

    if (res == 0)
    {
        norflash_erase_sector(addr);
    }

    norflash_ex_enter_mmap();       /* 进入内存映射模式 */
}

/**
 * @brief       擦除qspi flash整个芯片
 *   @note      等待时间超长...
 *
 * @param       无
 * @retval      无
 */
void norflash_ex_erase_chip(void)
{
    uint8_t res = 0;
    res = norflash_ex_exit_mmap();  /* 退出内存映射模式 */

    if (res == 0)
    {
        norflash_erase_chip();
    }

    norflash_ex_enter_mmap();       /* 进入内存映射模式 */
}

norflash_ex.h

#ifndef __norflash_ex_h
#define __norflash_ex_h

#include "./system/sys/sys.h"


void norflash_ex_erase_chip(void);              /* nor flash 全片擦除 */
uint16_t norflash_ex_read_id(void);             /* nor flash读取id */
void norflash_ex_erase_sector(uint32_t addr);   /* nor flash 擦除扇区 */
uint8_t norflash_ex_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen);  /* nor flash写入数据 */
void norflash_ex_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen);      /* nor 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/mpu/mpu.h"
#include "./bsp/led/led.h"
#include "./bsp/lcd/lcd.h"
#include "./bsp/key/key.h"
#include "./bsp/qspi/qspi.h"
#include "./bsp/norflash/norflash.h"
#include "./bsp/norflash/norflash_ex.h"
#include "string.h"

/* 要写入到flash的字符串数组 */
const uint8_t g_text_buf[] = {"minipro h7 qspi test"};

#define text_size       sizeof(g_text_buf)  /* text字符串长度 */

int main(void)
{
    uint8_t key;
    uint16_t i = 0;
    uint8_t datatemp[text_size + 2];
    uint8_t rectemp[text_size + 2];
    uint32_t flashsize;
    uint16_t id = 0;

    sys_cache_enable();                     /* 打开l1-cache */
    hal_init();                             /* 初始化hal库 */
    sys_stm32_clock_init(240, 2, 2, 4);     /* 设置时钟, 480mhz */
    delay_init(480);                        /* 延时初始化 */
    usart_init(115200);                     /* 串口初始化为115200 */
    usmart_dev.init(240);                   /* 初始化usmart */
    mpu_memory_protection();                /* 保护相关存储区域 */
    led_init();                             /* 初始化led */
    lcd_init();                             /* 初始化lcd */
    key_init();                             /* 初始化按键 */
    /* 
     * 不需要调用norflash_init函数了,因为sys.c里面的sys_qspi_enable_memmapmode函数已
     * 经初始化了qspi接口,如果再调用,则内存映射模式的设置被破坏,导致qspi代码执行异常!
     * 除非不用分散加载,所有代码放内部flash,才可以调用该函数!否则将导致异常!
     */
    //norflash_init();
    
    lcd_show_string(30, 50, 200, 16, 16, "stm32", red);
    lcd_show_string(30, 70, 200, 16, 16, "qspi 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_ex_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, "qspi 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_ex_write((uint8_t *)datatemp, flashsize - 100, text_size + 2); /* 从倒数第100个地址处开始,写入text_size + 2长度的数据 */
            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_ex_read(rectemp, flashsize - 100, text_size + 2);              /* 从倒数第100个地址处开始,读出text_size + 2个字节 */
            lcd_show_string(30, 150, 200, 16, 16, "the data readed is:   ", blue);  /* 提示传送完成 */
            lcd_show_string(30, 170, 200, 16, 16, (char *)rectemp, blue);           /* 显示读到的字符串 */
        }

        i++;

        if (i == 20)
        {
            led0_toggle();      /* led0闪烁 */
            i = 0;
        }
        
        delay_ms(10);
    }
}

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

八、总结

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

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com