> 技术文档 > ZYNQ芯片,SPI驱动开发自学全解析个人笔记【FPGA】【赛灵思

ZYNQ芯片,SPI驱动开发自学全解析个人笔记【FPGA】【赛灵思

参考文献:

ZYNQ-Vitis(SDK)裸机开发之(八)PS端QSPI读写flash操作(包括SPI、Dual SPI、Qual SPI的配置使用)_sdk vivado 测试flash selftest-CSDN博客文章浏览阅读5.4k次,点赞50次,收藏81次。本文围绕ZYNQ QSPI Flash开发展开,介绍了Flash和SPI知识,包括Flash存储特性、区域划分,SPI引脚、协议等。详细说明了Vivado工程搭建,以及Vitis程序编写,涵盖操作格式、头文件和源文件内容,还编写了读写测试函数,最后给出实测结果。 https://blog.csdn.net/qq_38584212/article/details/137965232?fromshare=blogdetail&sharetype=blogdetail&sharerId=137965232&sharerefer=PC&sharesource=dsafefvf&sharefrom=from_link

ZYNQ-Linux开发之(七)国产化复旦微电子FMQL平台uboot和kernel无法识别国产SPI Flash 芯片以及分区的问题_复旦微fmql 社区-CSDN博客文章浏览阅读5.1k次,点赞8次,收藏61次。本文详细指导如何确保UBOOT和KERNEL识别新型Flash芯片SM25QH256MX,包括在UBOOT中添加驱动支持,修改源码添加芯片ID,以及在KERNEL中配置和设备树中修改相应节点。 https://blog.csdn.net/qq_38584212/article/details/132041125?fromshare=blogdetail&sharetype=blogdetail&sharerId=132041125&sharerefer=PC&sharesource=dsafefvf&sharefrom=from_link
ZYNQ中的SPI控制器使用_zynq spi-CSDN博客文章浏览阅读5.1k次,点赞43次,收藏72次。本文详细介绍了SPI控制器的工作原理,特别是Motorola提出的SPI接口,以及在XilinxZYNQ平台上的SPI0和SPI1控制器的使用方法,包括Vivado中的配置步骤、管脚映射和Vitis中的编程示例,涵盖了SPI的配置选项和轮询模式操作。 https://blog.csdn.net/2301_79521562/article/details/137013718?fromshare=blogdetail&sharetype=blogdetail&sharerId=137013718&sharerefer=PC&sharesource=dsafefvf&sharefrom=from_link

一、SPI控制器简介
SPI 接口是 Motorola 首先提出的全双工三线同步串行外围接口, 采用主从模式(MasterSlave) 架构; 支持多 slave模式应用, 一般仅支持单 Master。 时钟由 Master 控制, 在时钟移位脉冲下, 数据按位传输, 高位在前, 低位在后(MSBfirst) ; SPI 接口有 2 根单向数据线, 为全双工通信。

1.SPI 接口
共有 4 根信号线, 分别是: 设备选择线、 时钟线、 串行输出数据线、 串行输入数据线。

(1) MOSI: 主器件数据输出, 从器件数据输入

(2) MISO: 主器件数据输入, 从器件数据输出

(3) SCLK: 时钟信号, 由主器件产生

(4) /SS: 从器件使能信号, 由主器件控制

2.时钟极性和时钟相位
时钟的极性(CPOL)。决定空闲时时钟时低电平还是高电平。当时钟极性为 0 时(CPOL=0) , SCK 信号线在空闲时为低电平; 当时钟极性为 1 时(CPOL=1) , SCK 信号线在空闲时为高电平;
时钟的相位(CPHA)。当时钟相位为 1 时(CPHA=1) , 在 SCK 信号线的第二个跳变沿进行采样。为0的时候是在第一个跳变沿采样

3. ZYNQ的SPI控制器


        ZYNQ中有两个SPI控制器(SPI0和SPI1),每个控制器有三个片选信号,也可3-8译码获得八个片选信号。既可以做从机又可以做主机。他的引脚:SS/SCLK/MOSI和MISO,可以映射到MIO引脚上,也可以映射到EMIO引脚上(就可以根据需要调试)

二、在ZYNQ中使用SPI控制器的方法
1.在vivado中配置
        在vivado中程配置IP核时,勾选SPI0。同时可以选择映射到MIO还是EMIO上。这里选择EMIO,就可以把SPI控制器的线引到特定的管脚,方便调试。

        设置完IP核之后要连线,检查是否有错误,然后保存——生成OUTPUT文件——生成HDL文件。映射到EMIO的时候,要进行管脚约束,确定映射到哪一个EMIO上。设置管脚约束的方法可以在正点原子的嵌入式开发指南中的EMIO教程视频当中看到。

设置完以后要生成比特流文件。然后导出XSA文件。注意要记录下来SPI控制器的哪个脚,引射到了哪个EMIO上面。当使用了更多EMIO的时候,也要记录下来EMIO[0]对应了哪个引脚,EMIO[1]对应了哪个引脚等等……方便后面编程的时候能正确对应起来

2.在vitis中编程
将刚才生成的xsa文件导入,在vitis中编写程序

(1)添加spi控制器的相关代码
介绍轮询方式:

首先添加文件命名为spi_ps.c文件

#include \"spi_ps.h\" XSpiPs SpiInstance; /* 发送和接收的缓冲区 */u8 *ReadBuf = (void*) 0x08000000 ;u8 *SendBuf = (void*) 0x08100000 ; /* -----------------spi初始化函数--------------------------- */ int SpiPs_Init(XSpiPs *SpiInstancePtr,u16 SpiDeviceId ){    int Status;    XSpiPs_Config *SpiConfig;     //根据器件ID查找配置信息    SpiConfig = XSpiPs_LookupConfig(SpiDeviceId);    if (NULL == SpiConfig) {        return XST_FAILURE;    }    /* 初始化函数 */    Status = XSpiPs_CfgInitialize((SpiInstancePtr), SpiConfig,                    SpiConfig->BaseAddress);    if (Status != XST_SUCCESS) {        return XST_FAILURE;    }     /* 配置SPI的工作模式     * 第一个参数是XSpiPs 实例     * 第二个参数设置为主机模式, */    Status = XSpiPs_SetOptions(SpiInstancePtr, XSPIPS_MASTER_OPTION | XSPIPS_FORCE_SSELECT_OPTION );     /* 配置SPI时钟的分频参数 */    Status = XSpiPs_SetClkPrescaler(SpiInstancePtr, XSPIPS_CLK_PRESCALE_256);     /* 配置被选择的从设备 */    XSpiPs_SetSlaveSelect(SpiInstancePtr, 0x00);     return XST_SUCCESS;} /* -------------------SPI的发送函数------------- */void SpiPs_Send(XSpiPs *SpiInstancePtr,u8 *SendBuffer, int ByteCount){     /* 原函数形式    * s32 XSpiPs_PolledTransfer(XSpiPs *InstancePtr, u8 *SendBufPtr,u8 *RecvBufPtr, u32 ByteCount)    * 其中 第二个参数 SendBufPtr 是发送的数据的指针,指向要发送数据的地址了    * 第三个参数 RecvBufPtr 是接收的数据,    * ByteCount 是之要发送的字节数    * 这个属于轮询模式    */    XSpiPs_PolledTransfer(SpiInstancePtr, SendBuffer, ReadBuf,ByteCount); } /* -------------------SPI的读取函数------------- */void SpiPs_Read(XSpiPs *SpiInstancePtr,u8 *ReadBuffer,int ByteCount){    XSpiPs_PolledTransfer(SpiInstancePtr, SendBuf, ReadBuffer,ByteCount);}

其中,这一句配置SPI的工作模式,包括主机模式、手动片选还是自动片选、手动开始传输还是自动开始传输、时钟相位和时钟极性。要配置哪一个,就把对应的写到第二个参数的位置上。

Status = XSpiPs_SetOptions(SpiInstancePtr, XSPIPS_MASTER_OPTION | XSPIPS_FORCE_SSELECT_OPTION );

 可以跳转进这个函数,能看到这个配置表,需要加哪个就把哪个复制进去。

typedef struct {    u32 Option;    u32 Mask;} OptionsMap; static OptionsMap OptionsTable[] = {    {XSPIPS_MASTER_OPTION, XSPIPS_CR_MSTREN_MASK},    {XSPIPS_CLK_ACTIVE_LOW_OPTION, XSPIPS_CR_CPOL_MASK},    {XSPIPS_CLK_PHASE_1_OPTION, XSPIPS_CR_CPHA_MASK},    {XSPIPS_DECODE_SSELECT_OPTION, XSPIPS_CR_SSDECEN_MASK},    {XSPIPS_FORCE_SSELECT_OPTION, XSPIPS_CR_SSFORCE_MASK},    {XSPIPS_MANUAL_START_OPTION, XSPIPS_CR_MANSTRTEN_MASK}};

比如,要想设置时钟极性为1,就把这个函数改写成:

Status = XSpiPs_SetOptions(SpiInstancePtr, XSPIPS_MASTER_OPTION | XSPIPS_FORCE_SSELECT_OPTION | XSPIPS_CLK_ACTIVE_LOW_OPTION);

第二个分频参数:SPI的时钟是166.66MHz,分频后的时钟就是SPI控制器做主机时的时钟频率。比如最大256分频,这时候时钟频率就是大概是651KHz左右。

然后设置片选的从设备。这个功能我没有使用,但是应该是用哪个片选就写哪个,从0到2编号

然后后面的是轮询模式下发送和接收的函数。可以直接调用。

然后再添加spi_ps.h的文件:

 #ifndef SRC_SPIPS_H_#define SRC_SPIPS_H_ #include \"xparameters.h\"    /* EDK generated parameters */#include \"xspips.h\"        /* SPI device driver */#include \"xscugic.h\"        /* Interrupt controller device driver */#include \"xil_exception.h\"#include \"xil_printf.h\"  void SpiPs_Read(XSpiPs *SpiInstancePtr,u8 *ReadBuffer,int ByteCount); void SpiPs_Send(XSpiPs *SpiInstancePtr,u8 *SendBuffer, int ByteCount); int SpiPs_Init(XSpiPs *SpiInstancePtr,u16 SpiDeviceId ); #define SPI_DEVICE_ID        XPAR_XSPIPS_0_DEVICE_ID#define MAX_DATA        100   #endif /* SRC_SPIPS_H_ */

具体要用SPI发送数据的时候,第一步先把要发的数据写到sendbuf缓冲区里面,然后调用send函数即可。比如这样:

SendBuf[0] = RegAddr;for(t=0;t<Len;t++)    {        SendBuf[t+1] = Data[t];    }SpiPs_Send(&SpiInstance,SendBuf,Len+1)

1. ZYNQ 7010平台介绍:
Xilinx的ZYNQ 7000系列处理器是集成了ARM处理器的FPGA(现场可编程门阵列)解决方案。ZYNQ 7010是这个系列中的一个型号,具有一个双核心的ARM Cortex-A9处理器和一定的FPGA逻辑资源。它为开发者提供了一个将处理器软件可编程性和FPGA硬件可编程性结合的独特机会。

2. SPI(Serial Peripheral Interface)通信协议:
SPI是一种常用的串行通信协议,用于微处理器和各种外围设备之间的数据交换。它采用主从模式,通常包括一个主设备和一个或多个从设备。SPI通信包含四根线:SCLK(时钟线)、MOSI(主设备数据输出,从设备数据输入)、MISO(主设备数据输入,从设备数据输出)和CS(片选信号)。它以其高速率和全双工通信能力而广泛应用于传感器、SD卡等设备。

3. 驱动程序的作用:
驱动程序是软件组件,它允许操作系统和计算机硬件之间的通信。在本例中,驱动程序是实现ZYNQ 7010与SPI设备通信的软件部分。驱动程序将负责初始化SPI通信,以及向SPI设备发送和接收数据。

4. SDK(Software Development Kit)驱动库:
SDK驱动库是指为了简化开发工作,将一些通用的操作封装成函数或者类库,供开发者在编写应用程序时调用。这些库通常包括了初始化硬件、配置硬件、数据传输等常用功能。使用SDK驱动库可以大大减少开发时间和复杂性。

5. 代码直接编译运行:
描述中提到的“项目代码可直接编译运行”,意味着提供的ZIP压缩文件中包含了所有必要的源代码、库文件和配置文件,且无需复杂的设置或修改即可在支持ZYNQ 7010的开发环境中编译成可执行文件。这说明了该驱动程序具有良好的兼容性和即插即用的特性。

详细知识点:

- ZYNQ 7010的编程和配置:
开发者在使用ZYNQ 7010实现SPI驱动时,需要了解如何编程和配置FPGA部分以及ARM处理器部分。这涉及到使用Xilinx提供的开发工具,如Vivado来配置FPGA,并使用SDK进行软件开发。

- SPI协议的实现细节:
在编写SPI驱动时,需要根据SPI协议规范来设计时序和通信逻辑。例如,需要编写代码来控制SCLK的频率,精确地在时钟边沿上切换MOSI和MISO的信号状态,以及确保数据的正确同步和时序控制。

- 驱动程序的架构和设计:
SPI驱动程序通常包含初始化代码、数据读写函数、中断处理等模块。驱动程序的架构应该清晰、高效,能够处理SPI通信中可能出现的各种情况,如超时、错误检测等。

- SDK的使用:
对于ZYNQ 7010这样的处理器,Xilinx提供了一套完整的软件开发工具包SDK,包括编译器、链接器、调试器等工具。开发者需要熟悉SDK的使用,才能有效地开发和调试基于ZYNQ 7010的SPI驱动程序。

- 跨平台的代码移植和兼容性:
由于ZYNQ 7010可能应用于不同的系统和设备中,因此在编写SPI驱动程序时,应考虑到代码的可移植性和兼容性。这意味着代码应尽可能地与操作系统无关,以及能够在不同硬件配置下正常工作。

- 性能优化:
SPI通信的性能优化是提高系统整体性能的关键。开发者需要在保证数据传输稳定性的前提下,优化时序、减少通信延迟、提高数据吞吐率等。

- 测试和验证:
驱动程序在开发完成后需要经过严格的测试和验证。这包括单元测试、集成测试和系统测试,确保在各种正常和异常情况下驱动程序都能正确工作。

- 文档和支持:
提供完善的开发文档和支持也是驱动开发过程中不可或缺的一部分。文档应该详细记录驱动程序的功能、API接口说明、使用示例以及常见问题解答。

通过以上知识点的详细阐述,我们可以看到开发ZYNQ 7010的SPI驱动程序是一个复杂且需要多方面考虑的工作。涉及到硬件和软件的结合、软件工程的最佳实践以及对特定硬件平台的深入理解。

项目介绍
在嵌入式系统设计中,SPI(Serial Peripheral Interface)接口是一种广泛使用的通信协议,尤其在需要高速数据传输的场景中表现出色。Zynq SoC(System on Chip)和Zynq UltraScale+ MPSoC(Multi-Processor System on Chip)作为Xilinx公司的高性能嵌入式处理器,提供了灵活且强大的SPI接口实现方案。

本项目提供了一份详细的指南——pg153-axi-quad-spi.pdf,帮助开发者在使用Zynq SoC或Zynq UltraScale+ MPSoC时,选择并实现合适的SPI接口。指南中详细介绍了两种主要的SPI实现方法:使用PS(Processing System)端的SPI控制器和在PL(Programmable Logic)端使用AXI Quad SPI(QSPI)IP模块。

项目技术分析
PS端SPI控制器
位置:位于PS端,即处理系统部分。
控制器数量:提供两个SPI控制器。
配置复杂度:较低,适合快速原型设计和简单应用。
性能:中等,适用于大多数常规应用。
资源占用:较低,对系统资源的影响较小。
灵活性:中等,适合标准SPI通信需求。
PL端AXI Quad SPI
位置:位于PL端,即可编程逻辑部分。
控制器数量:提供一个AXI Quad SPI IP模块。
配置复杂度:较高,需要更多的配置和调试工作。
性能:较高,适用于需要高性能数据传输的场景。
资源占用:较高,对系统资源的需求较大。
灵活性:较高,适合需要高度定制化的应用。
项目及技术应用场景
应用场景
工业自动化:在工业控制系统中,SPI接口常用于传感器和执行器的通信,Zynq SoC/MPSoC的高性能SPI接口可以确保数据的实时性和可靠性。
消费电子:在智能家居、可穿戴设备等消费电子产品中,SPI接口用于与各种外设(如显示屏、传感器等)的通信。
汽车电子:在汽车电子系统中,SPI接口用于与各种传感器和控制器的通信,确保系统的稳定性和安全性。
技术选择
快速原型设计:如果项目需要快速原型设计,且对性能要求不高,可以选择PS端的SPI控制器。
高性能需求:如果项目需要高性能的数据传输,且对系统资源有较高的容忍度,可以选择PL端的AXI Quad SPI IP模块。
项目特点
灵活性:项目提供了两种SPI实现方式,开发者可以根据具体需求选择最合适的方案。
高性能:PL端的AXI Quad SPI IP模块提供了更高的性能,适合需要高速数据传输的应用。
资源优化:PS端的SPI控制器占用资源较少,适合资源受限的应用场景。
详细指南:项目提供的pg153-axi-quad-spi.pdf文件详细介绍了两种SPI实现方式的配置和使用方法,帮助开发者快速上手。
通过本项目,开发者可以充分利用Zynq SoC/MPSoC的强大功能,实现高效、可靠的SPI通信,满足各种复杂应用的需求。无论是快速原型设计还是高性能数据传输,本项目都能为您提供最佳的解决方案。

例程开发环境:

SOC芯片:ZYNQ7020

开发环境:Vivado2020.2,Vitis2020.2

       Flash芯片:W25Q256,即256Mb,32MB

一、Flash知识简介
Flash存储器也叫闪存,是一种非易失性存储器,说白了就是数据掉电不丢失,所以一般用来存放运行程序或者需要掉电保存的数据,并且flash具有操作方便、读写速度快的优点。

Flash存储数据时,只能将1写为0,不能将0写为1,因此对flash进行擦除操作时,就是将flash对应区域全部置1,写数据时,就将对应bit置0即可。

       Flash内部区域划分:级别从大到小一般是:整片(chip)>块(block、bulk、bank)>扇区(sector)>页(page)

其中页为最小划分的区域单位,其内部一般包含若干字节,例如一页包含256字节等,但是目前大部分厂商常用的最小单位划分基本都是扇区,具体是啥还是要看芯片手册来确定;

       下面是我是用的W25Q256 flash芯片的区域划分情况

最小单位为扇区,每个扇区容量大小4KB,即4096字节
每个块包含16个扇区,所以每个块大小64KB
整片flash共有512个块,共32MB存储空间

二、SPI知识简介
1.SPI引脚介绍
/CS:片选引脚,低电平有效,拉低时,表明使能该芯片的操作

VCC:电源引脚

GND:接地引脚

CLK:输入时钟引脚,用于SPI通信同步

IO0(MISO):数据输入引脚

IO1(MOSI)数据输出引脚

IO2(WP):写保护引脚,低电平时,flash无法被写入数据,在Quad SPI模式下复用为数据引脚

IO3(HOLD):暂停通讯引脚,拉低时,DO为高阻态,flash暂停其余操作保持现有状态,等待HOLD拉高,再恢复之前的通讯,在Quad SPI模式下复用为数据引脚

2.SPI协议介绍
SPI为标准通信协议,不仅可以操作flash,还可以与其他类型器件进行通信,但是由于SPI标准通信协议为全双工,且速度较慢,因此实际读写flash时,一般都使用其扩展协议,即Dual flash和Quad flash

SPI接口协议:使用IO0(MISO)和IO1(MOSI)这两个进行读写,IO0输入使用,IO1输出使用,可同时进行读取操作,因此为全双工通信,但是一般读写flash时很少使用全双工模式,所以操作flash时,标准SPI协议用的很少
Dual SPI接口协议:同样使用IO0和IO1这两个进行读写,IO0和IO1只能同时向一个方向发送数据或同时读取数据,因此Dual SPI属于半双工通信,但是同一时刻可以读取2bit或写入2bit数据,是标准SPI协议速度的两倍
Quad SPI接口协议:增加两个读写flash的IO(WP和HOLD复用为数据IO),同时使用IO0- IO3四线进行读或写,同样为半双工通信,同一时刻可以传输4bit数据,是标准SPI通信速度的4倍,常用于操作flash的读写
注意:Dual SPI和Quad SPI一般只用于读写flash使用,不控其他类型器件

3.ZYNQ Quad SPI说明
       ZYNQ QSPI Flash控制器通过MIO与外部 Flash 器件连接,支持三种模式:单个从

器件模式、双从器件并行模式和双从器件堆模式:

(1)单个从器件模式:即外接单个 flash,通过 4bit I/O(即 quad 、dual 或单线)与 flash 进行通信。

(2)双从器件并行模式:把每个 flash 的 IO 进行了单独的连接,扩展成 8bit 用于同时访问两块 flash,实现扩展 QSPI Flash 容量。

(3)双从器件堆叠模式:使用片选 SS 信号进行区分 flash的使能。对 flash 仍然是 4bit,即同一时间只能操作一块 flash。通过使用双从器件模式可以扩展 QSPI Flash

的存储容量

下图是ZYNQ QSPI Flash三种使用模式的框图以及block design中的配置使用方法

三、Vivado工程搭建
ZYNQ QSPI Flash为PS核内置功能,属于硬核,直接对PS端进行配置即可使用,不需要增加PL端的任何IP核;因此本项目工程是在ZYNQ-Vitis(SDK)裸机开发之(一)串口实验工程基础上开发的,一些block design的设计方法,Vitis工程的建立方法等,均在该篇文章中进行了详细的讲解,大家可以去参考:

ZYNQ-Vitis(SDK)裸机开发之(一)串口收发使用:PS串口+PL串口、多个串口使用方法

       PS核需要勾选上QSPI,我的只有一片flash,因此选的Signal SS 4-bit IO选项,具体引脚约束根据自己项目原理图确定

四、编写Vitis程序

1.ZYNQ QSPI Flash操作的格式:

(1)在向flash中写数据时,传入的buffer中结构应按照如下放置数据:

第0个字节:放要下发的指令

第1-3个字节:放要操作数据的起始地址,当然如果某些指令不需要操作数据,例如读取flash ID,这种的话1-3字节就不需要填写数据,后者随便填就行,作为空闲字节使用

从第4个字节开始:为纯数据区,即需要写入flash内部的数据

(2)在从flash中读数据时,读取的buffer中的数据结构如下所示:

使用普通读指令READ_CMD读取数据时,返回的数据结构与写入时的数据结构一致,提取数据时从第4个字节开始提取,可见下图所示:      

       使用Fast、Dual、Quad这三种模式读取的时候,读取回来的数据结构中,多出一个空闲字节Dummy,在数据区的前面,因此此时提取数据时,应该从第5个字节开始提取

2.头文件:qspi_hdl.h

(1)定义QSPI器件ID号

(2)定义flash芯片操作指令,这个需要根据自己使用芯片的手册进行修改

(3)定义flash操作指令、起始地址、空闲字节、数据等的偏移地址

(4)定义空闲字节Dummy、读ID、擦除指令、buffer头部所占字节数量

(5)定义flash芯片的容量参数,包括页数、页字节、扇区数、扇区字节等

(6)定义要操作的flash部分区域,包括读写起始地址,读写范围、读写的字节数量等等

(7)声明QSPI Flash操作相关的函数,QSPI初始化、读写操作、擦除操作、读flash ID、使能Quad SPI模式等

/*!    \\file    qspi_hdl.h    \\brief   firmware functions to manage qspi    \\version 2024-04-15, V1.0.0    \\author  tbj*/ #ifndef QSPI_HDL_H#define QSPI_HDL_H #include \"xqspips.h\" //QSPI器件ID#define QSPI_DEVICE_ID        XPAR_XQSPIPS_0_DEVICE_ID //flash操作指令#define READ_CMD            0x03    //读指令#define WRITE_CMD            0x02    //写指令#define READ_STATUS_CMD        0x05    //读状态指令#define WRITE_STATUS_CMD    0x01    //写状态指令#define WRITE_ENABLE_CMD    0x06    //写使能指令#define WRITE_DISABLE_CMD    0x04    //禁止写使能指令#define FAST_READ_CMD        0x0B    //单通道读取#define DUAL_READ_CMD        0x3B    //双通道读取-半双工#define QUAD_READ_CMD        0x6B    //四通道读取-半双工#define BULK_ERASE_CMD        0xC7    //擦除整片flash-全部写1#define    SEC_ERASE_CMD        0xD8    //擦除一个扇区-全部写1#define READ_ID                0x9F    //读取flash ID指令 //定义flash操作指令、地址、数据等在读写buffer中的位置#define COMMAND_OFFSET        0 //flash操作指令在写buffer中的位置(第0个字节)#define ADDRESS_1_OFFSET    1 //操作flash数据起始地址的高字节在写buffer中的位置(第1个字节)#define ADDRESS_2_OFFSET    2 //操作flash数据起始地址的中字节在写buffer中的位置(第2个字节)#define ADDRESS_3_OFFSET    3 //操作flash数据起始地址的低字节在写buffer中的位置(第3个字节)#define DATA_OFFSET            4 //操作flash的数据,读取或写入的数据,在写buffer中的位置(第4个字节开始是纯数据区)#define DUMMY_OFFSET        4 //空闲字节的位置,当使用快速、双线、四线模式读取数据时,空闲字节占读buffer的第4个字节,纯数据区从第5个字节开始 //定义各种操作所需字节长度#define DUMMY_SIZE            1 //空闲字节大小占1个字节(当使用快速、双线、四线模式读取数据时存在dummy byte)#define RD_ID_SIZE            4 //读取flash ID占字节数,其中第0个字节为读取ID指令号,后3个字节为读取到的flash ID号#define BULK_ERASE_SIZE        1 //清空整片flash指令占字节数,只需要一个清空整片flash的指令号#define SEC_ERASE_SIZE        4 //按扇区清空flash指令占字节数,其中第0字节为按扇区清空flash的指令号,后3个字节是起始清空的flash地址#define OVERHEAD_SIZE        4 //定义读写buffer头部数据长度,包括指令号1字节和操作地址3字节 //定义flash的参数数据#define SECTOR_SIZE 0x10000      //定义单个扇区大小-64KB(根据自己flash芯片手册确定)#define NUM_SECTORS 0x200      //定义扇区数量-256个(根据自己flash芯片手册确定)#define NUM_PAGES 0x20000      //定义页数量-65536个(根据自己flash芯片手册确定)#define PAGE_SIZE 256          //定义每页字节数-256个字节(根据自己flash芯片手册确定) //定义实际读写操作的范围和数据大小#define PAGE_COUNT 16         //定义需要操作读写的页数#define TEST_ADDRESS 0x01FF0000//0x00055000        //定义读写操作的起始地址#define UNIQUE_VALUE 0x05            //定义读写操作的起始值#define MAX_DATA (PAGE_COUNT * PAGE_SIZE)    //定义读写操作的最大数据量 //定义QSPI操作结构体对象XQspiPs QspiInstance; #ifdef __cplusplus extern \"C\" {#endif  //初始化QSPI控制器 int Qspi_Init(XQspiPs *QspiInstancePtr); //通过QSPI将数据写入flash中 void FlashWrite(XQspiPs *QspiPtr, u32 Address, u8 *WriteBuf, u32 ByteCount, u8 Command); //通过QSPI读取flash中的数据 void FlashRead(XQspiPs *QspiPtr, u32 Address, u8 *ReadBuf, u32 ByteCount, u8 Command); //擦除flash void FlashErase(XQspiPs *QspiPtr, u32 Address, u32 ByteCount); //读取flash ID int FlashReadID(void); //使能四线模式 void FlashQuadEnable(XQspiPs *QspiPtr); #ifdef __cplusplus}#endif #endif /* QSPI_HDL_H */

3.源文件:qspi_hdl.c

(1)对头文件总QSPI初始化、读写操作、擦除操作、读flash ID、使能Quad SPI模式等函数进行实现

/*!    \\file    qspi_hdl.c    \\brief   firmware functions to manage qspi    \\version 2024-04-15, V1.0.0    \\author  tbj*/ #include \"qspi_hdl.h\" //QSPI读写flash使用的buffer,内部使用static u8 FlashReadBuffer[MAX_DATA + DATA_OFFSET + DUMMY_SIZE];static u8 FlashWriteBuffer[PAGE_SIZE + DATA_OFFSET]; /* 功能:初始化QSPI控制器 * 入参1:QSPI控制器实例化对象指针 */int Qspi_Init(XQspiPs *QspiInstancePtr){     int Status;    XQspiPs_Config *QspiConfig;     //初始化QSPI控制器    QspiConfig = XQspiPs_LookupConfig(QSPI_DEVICE_ID);    if (QspiConfig == NULL) {        return XST_FAILURE;    }     Status = XQspiPs_CfgInitialize(QspiInstancePtr, QspiConfig,                    QspiConfig->BaseAddress);    if (Status != XST_SUCCESS) {        return XST_FAILURE;    }     //QSPI控制器自检,保证初始化成功    Status = XQspiPs_SelfTest(QspiInstancePtr);    if (Status != XST_SUCCESS) {        return XST_FAILURE;    }     //清空读写操作的buffer    memset(FlashWriteBuffer, 0x00, sizeof(FlashWriteBuffer));    memset(FlashReadBuffer, 0x00, sizeof(FlashReadBuffer));     //将flash配置为手动启动、手动片选模式,将hold(reset)引脚配置为高电平,hold低电平,暂停收发数据,高电平恢复收发数据    Status |= XQspiPs_SetOptions(QspiInstancePtr, XQSPIPS_MANUAL_START_OPTION |            XQSPIPS_FORCE_SSELECT_OPTION |            XQSPIPS_HOLD_B_DRIVE_OPTION);     //设置QSPI预分频系数    Status |= XQspiPs_SetClkPrescaler(QspiInstancePtr, XQSPIPS_CLK_PRESCALE_8);     //将片选信号置为有效    Status |= XQspiPs_SetSlaveSelect(QspiInstancePtr);     //读取flash ID    Status |= FlashReadID();     //使能QSPI Quad模式    FlashQuadEnable(QspiInstancePtr);     return Status;}  /*** @brief 通过QSPI将数据写入flash中* @param QSPI结构体指针* @param 要写入数据的起始地址* @param 要写入数据的数量-按字节* @param 写数据指令* @return 无* @note 无* */void FlashWrite(XQspiPs *QspiPtr, u32 Address, u8 *WriteBuf, u32 ByteCount, u8 Command){    u8 WriteEnableCmd = { WRITE_ENABLE_CMD };    u8 ReadStatusCmd[] = { READ_STATUS_CMD, 0 };  /* must send 2 bytes */    u8 FlashStatus[2];     //发送写使能指令    XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL,                sizeof(WriteEnableCmd));     //将写操作指令以及数据地址写入对应待发送buffer的前4个字节,第五个字节开始才是写入的数据    FlashWriteBuffer[COMMAND_OFFSET]   = Command;    FlashWriteBuffer[ADDRESS_1_OFFSET] = (u8)((Address & 0xFF0000) >> 16);    FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)((Address & 0xFF00) >> 8);    FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF);     //将要写入的纯数据,写入到flash写操作buffer中(flash写操作buffer包括写指令、地址、数据等内容)    memcpy(FlashWriteBuffer + 4, WriteBuf, ByteCount);     //将写指令、写起始地址信息、写入数据内容,写到flash中    XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, NULL,                ByteCount + OVERHEAD_SIZE);    //等待数据写入完毕    while (1) {         //通过发送读状态指令,获取是否已经将数据写完,如果为可读状态,则数据写入完毕        XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd, FlashStatus,                    sizeof(ReadStatusCmd));         //如果读取的状态值是0xff,则证明数据还未写完        FlashStatus[1] |= FlashStatus[0];        if ((FlashStatus[1] & 0x01) == 0) {            break;        }    }} /*** @brief 通过QSPI读取flash中的数据* @param QSPI结构体指针* @param 读取数据的起始地址* @param 读取数据的数量-按字节* @param 读取数据指令-普通读取、快速、双线、四线读取等* @return 无* @note 无* */void FlashRead(XQspiPs *QspiPtr, u32 Address, u8 *ReadBuf, u32 ByteCount, u8 Command){    //将读指令和读数据首地址写入到要发送的buffer中,它们占4个字节    FlashWriteBuffer[COMMAND_OFFSET]   = Command;    FlashWriteBuffer[ADDRESS_1_OFFSET] = (u8)((Address & 0xFF0000) >> 16);    FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)((Address & 0xFF00) >> 8);    FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF);     //如果是快速读取、双线读取和四线读取,则需要增加一个空闲字节的长度DUMMY_SIZE    if ((Command == FAST_READ_CMD) || (Command == DUAL_READ_CMD) ||        (Command == QUAD_READ_CMD)) {        ByteCount += DUMMY_SIZE;    }     //将读指令和读地址发送到通过QSPI发送到flash,等待数据读取至ReadBuffer中    XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, FlashReadBuffer,                ByteCount + OVERHEAD_SIZE);      //如果是快速读取、双线读取和四线读取,则需要增加一个虚拟字节的长度DUMMY_SIZE    if ((Command == FAST_READ_CMD) || (Command == DUAL_READ_CMD) ||        (Command == QUAD_READ_CMD)) {        //非普通模式读取,有空闲字节,从第5个字节开始是纯数据        memcpy(ReadBuf, FlashReadBuffer + 5, ByteCount - 1);    }else{        //普通模式读取,无空闲字节,从第4个字节开始是纯数据        memcpy(ReadBuf, FlashReadBuffer + 4, ByteCount);    } } /*** @brief 擦除flash* @param QSPI结构体指针* @param 擦除的起始地址* @param 擦除数据的数量-按字节* @return 无* @note 无* */void FlashErase(XQspiPs *QspiPtr, u32 Address, u32 ByteCount){    u8 WriteEnableCmd = { WRITE_ENABLE_CMD };    u8 ReadStatusCmd[] = { READ_STATUS_CMD, 0 };  /* must send 2 bytes */    u8 FlashStatus[2];    int Sector;     //如果是擦除整片flash,则使用整片擦除指令chip erase    if (ByteCount == (NUM_SECTORS * SECTOR_SIZE)) {        //发送写使能指令        XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL,                  sizeof(WriteEnableCmd));         //将整片擦除指令写入到发送buffer的首个字节的位置        FlashWriteBuffer[COMMAND_OFFSET]   = BULK_ERASE_CMD;        //将整片擦除指令发送到flash        XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, NULL,                    BULK_ERASE_SIZE);         //等待擦除完成        while (1) {            //通过发送读状态指令,获取是否已经将数据写完,如果为可读状态,则数据写入完毕            XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd,                        FlashStatus,                        sizeof(ReadStatusCmd));             //如果读取的状态值是0xff,则证明数据还未写完            FlashStatus[1] |= FlashStatus[0];            if ((FlashStatus[1] & 0x01) == 0) {                break;            }        }         return;    }     //如果是部分擦除,则使用扇区sector擦除的指令进行擦除操作    for (Sector = 0; Sector > 16);        FlashWriteBuffer[ADDRESS_2_OFFSET] = (u8)(Address >> 8);        FlashWriteBuffer[ADDRESS_3_OFFSET] = (u8)(Address & 0xFF);         //将扇区擦除指令,以及开始擦除首地址发送到flash        XQspiPs_PolledTransfer(QspiPtr, FlashWriteBuffer, NULL,                    SEC_ERASE_SIZE);         //等待擦除完成        while (1) {            //通过发送读状态指令,获取是否已经将数据写完,如果为可读状态,则数据写入完毕            XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd,                        FlashStatus,                        sizeof(ReadStatusCmd));             //如果读取的状态值是0xff,则证明数据还未写完            FlashStatus[1] |= FlashStatus[0];            if ((FlashStatus[1] & 0x01) == 0) {                break;            }        }         Address += SECTOR_SIZE;    }} /*** @brief 读取flash ID* @param 无* @return 无* @note 无* */int FlashReadID(void){    int Status;     //读取ID指令,后三个字节是空闲字节,填不填都行,填什么也无所谓    FlashWriteBuffer[COMMAND_OFFSET]   = READ_ID;    FlashWriteBuffer[ADDRESS_1_OFFSET] = 0x23;    FlashWriteBuffer[ADDRESS_2_OFFSET] = 0x08;    FlashWriteBuffer[ADDRESS_3_OFFSET] = 0x09;     //将读ID指令发送到flash    Status = XQspiPs_PolledTransfer(&QspiInstance, FlashWriteBuffer, FlashReadBuffer,                RD_ID_SIZE);    if (Status != XST_SUCCESS) {        return XST_FAILURE;    }     xil_printf(\"FlashID=0x%x 0x%x 0x%x\\n\\r\", FlashReadBuffer[1], FlashReadBuffer[2],            FlashReadBuffer[3]);     return XST_SUCCESS;} /*** @brief 使能四线模式* @param QSPI结构体指针* @return 无* @note 无* */void FlashQuadEnable(XQspiPs *QspiPtr){    u8 WriteEnableCmd = {WRITE_ENABLE_CMD};    u8 ReadStatusCmd[] = {READ_STATUS_CMD, 0};    u8 QuadEnableCmd[] = {WRITE_STATUS_CMD, 0};    u8 FlashStatus[2];     //判断读取的flash ID是否正确,不加这个判断也行    if (FlashReadBuffer[1] == 0xEF) {        //获取读状态        XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd,                    FlashStatus,                    sizeof(ReadStatusCmd));         QuadEnableCmd[1] = FlashStatus[1] | 1 << 6;        //发送写使能指令        XQspiPs_PolledTransfer(QspiPtr, &WriteEnableCmd, NULL,                  sizeof(WriteEnableCmd));        //发送Quad配置指令        XQspiPs_PolledTransfer(QspiPtr, QuadEnableCmd, NULL,                    sizeof(QuadEnableCmd));        while (1) {            //获取读状态,等待指令写入完毕            XQspiPs_PolledTransfer(QspiPtr, ReadStatusCmd, FlashStatus,                    sizeof(ReadStatusCmd));            /*             * 第6it置1,第0bit置0,则Quad模式设置成功、且设备状态准备就绪             */            if ((FlashStatus[0] == 0x40) && (FlashStatus[1] == 0x40)) {                break;            }        }    }}

4.编写QSPI Flash读写测试函数

//读写buffer数据长度#define test_buf_len 255//QSPI读写flash测试void QSPI_Flash_Opt(){     u8 nRet = XST_SUCCESS;    u8 write_buf[test_buf_len] = {0};    u8 read_buf[test_buf_len] = {0};     //初始化QSPI控制器    Qspi_Init(&QspiInstance);     //write buffer填写数据    for(int i = 0; i < test_buf_len; i++){        write_buf[i] = i + 1;    }     //清除要写入的flash区域    FlashErase(&QspiInstance, TEST_ADDRESS, test_buf_len);    //将write buffer数据写入flash中    FlashWrite(&QspiInstance, TEST_ADDRESS, write_buf, test_buf_len, WRITE_CMD);    //将写入flash中的数据再进行读取//    FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, READ_CMD);//    FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, FAST_READ_CMD);//    FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, DUAL_READ_CMD);    FlashRead(&QspiInstance, TEST_ADDRESS, read_buf, test_buf_len, QUAD_READ_CMD);     //打印写buffer区数据    for(int i = 0; i < test_buf_len; i++){        printf(\"%d \", write_buf[i]);        if(i == test_buf_len - 1)            printf(\"\\n\");    }     //打印读buffer区数据    for(int i = 0; i < test_buf_len; i++){        printf(\"%d \", read_buf[i]);        if(i == test_buf_len - 1)            printf(\"\\n\");    }     //对比写入和读出的数据是否一致    for(int i = 0; i < test_buf_len; i++){        if(read_buf[i] != write_buf[i]){            nRet = XST_FAILURE;        }    }     if(nRet == XST_SUCCESS){        printf(\"QSPI Operate flash successful!\\n\");    }else{        printf(\"QSPI Operate flash failed!\\n\");    }}

5.main函数调用

五、实测结果