news 2026/10/2 1:10:11

SPI协议到AXI Quad SPI实战:FPGA调试避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SPI协议到AXI Quad SPI实战:FPGA调试避坑指南

做过FPGA的朋友应该都有同感:SPI是上手最快、也最容易被"看似简单"坑到的串行协议。无论是Zynq上挂Flash、和ADC/DAC对接,还是跟STM32这类MCU联调,SPI几乎成了板级互联的默认选项。而Xilinx平台上的AXI Quad SPI IP核,则是把这套协议封装成一个可以直接拖进Block Design的标准外设,让PS或PL侧都能快速访问。

这篇文章要做的就是完整梳理一遍:从SPI协议基础,到AXI Quad SPI IP核在Vivado里的配置流程,再到裸机驱动、DMA模式的选择,以及我实际调试中踩过的各种坑。适合正在用Vivado做嵌入式的同学,也适合刚把手伸向FPGA+SPI方向的新手。内容不绕弯子,全是我这边实测过的路径。

1. 为什么用AXI Quad SPI而不是自己写SPI逻辑

1.1 SPI协议与AXI Quad SPI的定位

SPI全称Serial Peripheral Interface,四线制:SCLK、MOSI、MISO、CS(片选)。主设备产生时钟,通过MOSI发数据,通过MISO收数据,CS低有效选中从机。看起来非常直白,但工程上真正烦人的是时钟极性和相位、片选时序、以及多从机时的仲裁。很多人一开始都爱自己写一段Verilog模拟SPI时序,小项目没问题,一旦涉及高速Flash、高频DAC或者和PS核联动,自己写的逻辑往往在时序收敛上掉链子。

AXI Quad SPI这个IP核其实是Xilinx官方提供的一个完整SPI控制器,挂在AXI总线上,支持Standard SPI、Dual SPI、Quad SPI三种模式,内部带FIFO、中断控制、可配置分频和主从模式。名字里的"Quad"指的是它支持4线数据传输(用于SPI NOR Flash的Quad读),不是"四个独立SPI接口"。这个点我第一次接触时就搞混了,以为一个核能接四个外设,结果并不是。

它的核心价值在于:你不需要关心SPI时序细节,只需通过寄存器配置模式、频率、从机选择,然后把数据往FIFO里写、从FIFO里读。而且它天然适配Xilinx的驱动框架,在SDK里用库函数就能操作,再往上接DMA也顺理成章。从工程可维护性来说,比自己写逻辑高不止一个量级。

1.2 IP核的三档模式:Standard、Enhanced、Legacy

用Vivado打开AXI Quad SPI的配置界面时,第一步就会遇到一个选项:选择IP核类型(IP核Mode),里面有Standard Spartan6/7、Enhanced、Legacy Virtex-6等。很多人直接就选Easy,但实际应该结合目标芯片来选。

  • Standard模式:主要是老一代7系列、Spartan-6上用的经典实现。接口和寄存器相对简单,FIFO深度固定,不支持一些新特性,比如动态的FIFO中断阈值、独立的SPI时钟域同步等。
  • Enhanced模式:在Zynq和UltraScale+上更推荐。它支持独立时钟域(AXI时钟和SPI时钟可以不同步),FIFO能配置得更深,中断控制更灵活,还能选择是否启用FIFO半满/半空标志。
  • Legacy模式:基本只给老器件兼容用,新项目别碰。

我的建议是:如果你的芯片是Zynq-7000、UltraScale+,直接用Enhanced;如果只是Artix-7上做点轻量级控制,Standard也够用。但注意,Enhanced模式下寄存器地址布局和Standard不一样,驱动代码里如果用了硬编码地址而不是Xilinx提供的库函数,很容易踩坑。

2. Vivado中的IP核配置,每个选项背后的决策点

2.1 接口模式:Standard SPI、Dual SPI、Quad SPI怎么选

配置界面里第一个大项是"Interface Type",通常有Standard、Dual、Quad和Memory(Flash)模式。这里的核心判断依据是你接什么外设:

  • 接普通SPI传感器、LCD、ADC/DAC:选Standard,4线制,一次也就收发一字节,简单可靠。
  • 接SPI NOR Flash,但只做普通读写:Standard已经够,Flash的命令本身还是1-1-1模式。
  • 想用Flash的Quad Read(1-1-4或1-4-4)提速:开Dual或Quad,这时需要额外的IO引脚复用和FPGA引脚约束。
  • 如果只打算接一颗Flash并且用官方Flash驱动:可以选Memory模式,IP核内部会多出跟Flash指令相关的状态机,但寄存器配置也更复杂,新手不建议一上来就用。

我实际做过一个项目,用Zynq读取GD25Q128E这颗16MB SPI Flash。一开始图方便直接选Quad,以为这样读Flash会快,结果由于Flash型号的指令集和IP核内置的指令集不完全一致,反而绕了很多弯路,最后老老实实切回Standard,速度也完全够。所以优先级应该是:先能用,再谈快。

2.2 时钟、FIFO、中断这些参数怎么定

配置界面里有几个关键参数,下面是我整理过的决策逻辑:

参数我的推荐原因
SPI时钟频率(SCLK Rate)不超过外设上限,常见10MHz-25MHz太高容易受PCB走线、终端电阻影响
Ref Clk(ext_spi_clk)设为偶数倍于SCLK,比如40MHz配20MHzIP核做分频,整数倍分频最稳
FIFO深度16够用,DMA场景用64/256深度越大逻辑资源越多,中断响应延迟也越高
Master/Slave绝大多数工程选MasterSlave模式需要外部时钟驱动,时序约束复杂
CPOL/CPHA配合从机手册选选错会出现数据错位但波形"看着正常"的情况
启用AXI4 Full接口只有要用DMA时才开这个选项会改变IP核在总线上的接口形态

这里特别提一下时钟问题。AXI Quad SPI IP核至少有两条时钟路径:一条是AXI总线时钟s_axi_aclk,另一条是SPI参考时钟ext_spi_clk。SPI主模式输出SCLK的时候,是在ext_spi_clk基础上做分频的。比如你希望SCLK输出10MHz,可以把ext_spi_clk设成40MHz或20MHz,再在驱动里配置分频比。之所以不直接用10MHz作为ext_spi_clk,是因为IP核内部有些同步逻辑需要更高的时钟余量,外设时序余量会更好。

中断配置这里也要留个心眼:如果你在Vivado里使能了中断,但没有把中断引脚接到PS的GIC或者PL的中断控制器上,驱动里开中断后系统会一直处于等待状态,看起来就像"SPI死锁"。我见过不少case,最后发现只是中断没接好。

2.3 Block Design里的接线与地址分配

在Vivado中把IP核拖进Block Design后,第一件事是确认AXI接口的连接方式。如果你用PS(比如Zynq),AXI Quad SPI会挂在M_AXI_GP0或GP1上;如果纯PL工程,则需要一个AXI Interconnect把MicroBlaze或DMA连过去。

接线方面有几个细节:

  • 时钟:s_axi_aclk要和总线时钟保持同一个频率,复位信号s_axi_aresetn也要连到同一个复位网络。
  • SPI引脚:通常在顶层模块里把SPI信号引出来,然后在XDC约束文件里分配物理引脚和IO标准。
  • 片选:IP核默认提供spi_ss信号,如果只用一个从机,可以用硬核自带的片选;如果挂多个从机,后面会单独讲为什么不建议继续用IP的片选扩展。

地址分配看起来是小事,很多新手会随便给个地址,但要注意IP核在SDK里的设备ID(XPAR_AXI_QUAD_SPI_0_DEVICE_ID)和基地址是自动生成的,如果Block Design里调整了地址空间,驱动的配置参数也会跟着变。尽量不去手动改,保持自动生成的一致性。

生成比特流之前,记得在Address Editor里确认IP的地址没有重叠,同时给MicroBlaze或者PS分配足够的地址范围。这步错了,启动后驱动初始化时访问寄存器就会返回全FF或者总线错误,问题提示很不直观。

3. 软件驱动与工程实践,从轮询到中断到DMA

3.1 Xilinx库函数的基本使用流程

在SDK或Vitis里使用AXI Quad SPI,最靠谱的方式是用Xilinx官方库。尽管代码风格有一定年代感,但稳定性没得说。

基本流程大致是:

  1. 用XQSpi_LookupConfig获取IP核配置结构体。
  2. 用XQSpi_CfgInitialize初始化实例。
  3. 设置工作模式:XSP_MASTER_OPTION | XSP_MANUAL_SSELECT_OPTION,主模式并且手动控制片选。
  4. 调用XQSpi_SetSlaveSelect指定从机。
  5. XQSpi_Start启动IP核。
  6. 发数据时用XQSpi_Transfer或者直接操作FIFO寄存器。

以读SPI Flash的JEDEC ID为例,核心思路是:先拉低片选,发送0x9F读ID命令,然后读回3个字节。用库函数实现的时候,很多人会踩这样一个坑:Xilinx的SPI驱动读写是同时进行的,你往发送FIFO里写一个字节,接收FIFO里就会有一个字节进来。所以你发送0x9F之后,必须紧接着继续发送0x00、0x00、0x00,直到把需要读的字节数都"顶"出来。这就是SPI的"移位寄存器"本质:读一个字节必须先写一个字节,SCLK每跳一次,收发同时发生。

刚开始学这个IP核时,以为写完命令就能立刻读数据,结果读回来的永远是上一轮的残留数据,折腾了半天才意识到问题出在这。

3.2 硬件片选和软件片选,真的不要混用

这是我在SPI调试里最想强调的一点。AXI Quad SPI的片选有两种管理方式:硬件自动片选和软件手动片选。

硬件自动片选模式下,IP核会在每次传输开始时自动拉低片选,传输结束或FIFO空时自动拉高。这种方式适合单一的、整包连续传输的场景,看起来方便,但有两个问题:一是当数据流有间隙时,片选会被拉高,很多从机就会认为事务结束,导致数据错位;二是它不支持多从机灵活切换,IP核虽然提供了多个从机选择位,但本质上同一时刻只能有一种映射。

软件手动片选,也就是在驱动里配置XSP_MANUAL_SSELECT_OPTION,让片选完全由你的代码控制。每次事务开始前先写从机选择寄存器,再拉低片选,事务结束后再拉高。虽然多几行代码,但好处非常明显:你可以精确控制片选的拉低拉高时机,适配那些对接收时序敏感的外设。

我遇到过一个传感器模组,它要求在写完配置寄存器之后必须保持片选低电平至少50us,否则不生效。这个要求在硬件自动片选模式下根本做不到,切到软件片选后一下就解决了。所以我的准则是:只要不是最简单的一次性读写,一律用软件手动片选。

3.3 什么时候该上DMA,SPI需要两个DMA吗

很多人一谈到大数据传输就想上DMA,SPI也不例外。但先想清楚:AXI Quad SPI不上DMA时,CPU每收发一个字节都要去访问寄存器,如果频率高、数据量大,CPU会被拖死。但如果只是偶尔读写几个寄存器,DMA完全是多余的,还会引入缓存一致性问题。

如果决定上DMA,注意AXI Quad SPI有两种接口形态:AXI4-Lite和AXI4 Full。只有AXI4 Full模式下,IP核才支持DMA直接往FIFO里灌数据。在Vivado里要勾选"Enable AXI4 Full Interface",否则DMA是接不进去的。

"SPI需要两个DMA吗"这个问题,答案是取决于方向。AXI DMA本身通常有MM2S(Memory to Stream)和S2MM(Stream to Memory)两条通道,对应SPI的发送方向和接收方向。如果你只做单向写,比如往Flash写数据,MM2S通道就够了;只做单向读,比如读Flash内容,S2MM通道就够了。但大多数场景下,尤其是Flash读取,你得先发命令再接收数据,这时候两条通道都得用。所以"两个DMA"通常不是两个独立DMA控制器,而是一个AXI DMA实例里的两条通道。

我第一次用DMA读Flash时,只接了MM2S发命令,结果S2MM没有使能,数据全丢。后来看了IP核配置才明白,双向通信就得把两条通道都接好,并且DMA中断也要分别注册。这里可以提一句,使用DMA后接收到的数据往往和实际的SPI数据有延迟对齐的问题,建议在数据包前后加固定的帧头帧尾来做同步,不要依赖"第几个字节就是什么"这种固定偏移。

4. 实战,用AXI Quad SPI读SPI Flash的完整套路

4.1 硬件连接与上拉电阻处理的几条经验

以GD25Q128E为例,这颗Flash是常见的SPI NOR Flash,支持Standard SPI、Dual和Quad。硬件连接上,SCLK、MOSI、MISO、CS四根线直接连到FPGA引脚,麻烦的地方在于引脚分配和上下拉。

  • /CS、SCLK、MOSI建议在FPGA侧加10k-47k上拉电阻到Flash的供电电压,确保在FPGA配置完成前,这些引脚不会出现低电平毛刺。FPGA在配置过程中IO处于高阻态,如果外部没有上拉,CS可能被干扰拉低,Flash就会误以为有命令进来,甚至误写状态寄存器。

  • MISO这根线需要区分从机类型。像GD25Q128E这种标准SPI Flash,MISO是推挽输出,不一定要上拉;但如果你接SD卡(SPI模式),它的MISO是开漏输出,必须加上拉电阻否则读不到高电平。热词里问"TF卡SPI需要上拉吗",答案就在这里:需要,而且不只是MISO,CMD也建议加。

  • 电压域也要注意。如果FPGA是1.8V IO,而Flash是3.3V供电,电平不匹配会导致通信时好时坏。最好查清楚FPGA bank电压,或者用带电平转换的Flash模块。

4.2 从ID读回验证通信,代码与ILA抓波

硬件和IP核配置完成后,不要急着读整个Flash,先把JEDEC ID读回来验证链路。以SDK裸机为例,简化的代码逻辑像下面这样:

#include "xspi.h" #include "xparameters.h" #define SPI_DEVICE_ID XPAR_AXI_QUAD_SPI_0_DEVICE_ID #define SPI_SELECT XPAR_AXI_QUAD_SPI_0_NUM_TRANSFER_BITS static XSpi Spi; // JEDEC ID命令:0x9F,之后顶出3个字节 u8 WriteBuffer[4] = {0x9F, 0x00, 0x00, 0x00}; u8 ReadBuffer[4] = {0}; int SpiFlashReadId(void) { XSpi_Config *Config; int Status; Config = XSpi_LookupConfig(SPI_DEVICE_ID); Status = XSpi_CfgInitialize(&Spi, Config, Config->BaseAddress); if (Status != XST_SUCCESS) { return XST_FAILURE; } // 主模式 + 手动片选 XSpi_SetOptions(&Spi, XSP_MASTER_OPTION | XSP_MANUAL_SSELECT_OPTION); XSpi_SetSlaveSelect(&Spi, 0x01); XSpi_Start(&Spi); // 等待IP核就绪 while (XSpi_IsBusy(&Spi)); // 拉低片选,发送命令,再拉高片选 XSpi_IntrGlobalDisable(&Spi); XSpi_Transfer(&Spi, WriteBuffer, ReadBuffer, 4); // 检查返回,正常情况下ReadBuffer[1]应该是0xC8(GD25Q128E的Manufacturer ID) return XST_SUCCESS; }

这里很多细节容易出错,我在实际调试中反复踩过。比如XSpi_Transfer里面,接收缓冲区的首字节其实对应的是"命令发送期间移位寄存器收进来的残留数据",所以真正的ID从ReadBuffer[1]开始读。如果你把ReadBuffer[0]当成第一个ID字节,怎么读都对不上。

验证完ID后再用ILA(集成逻辑分析仪)抓波形。ILA核都挂在AXI Quad SPI的引脚上,比如io0_io1_i这类信号。ILA的采样时钟最好用ext_spi_clk,因为SPI引脚信号跟它是同一个时钟域,能看到干净的边沿。触发条件设成片选下降沿,然后看SCLK、MOSI、MISO的波形。

我第一次抓波形的时候,看到SCLK和MOSI都有正常的脉冲,但MISO一直是高——这其实说明Flash根本没响应。排查完后发现,是我在XDC里把MISO引脚约束错了,FPGA内部看到了引脚但实际PCB上没连到Flash。波形会说话,比在软件里反复打印状态寄存器高效得多。

5. 排查手册,我踩过的那些SPI大坑

5.1 回读全FF和全0的常见原因

回读数据全是FF(0xFF),这是SPI调试里最经典的现象。按我的排查顺序来:

  • 检查MISO引脚。默认状态下MISO引脚可能是高阻,如果有上拉就会读出0xFF。用万用表量一下FPGA引脚到Flash引脚之间是否连通。
  • 检查Flash供电。Flash核心电压和IO电压没起来时,数据输出引脚通常是高阻或弱上拉,读出来自然全是FF。
  • 检查CS片选是否真的拉低。有些调试场景下,片选逻辑反了,FPGA拉低的时候外部电路因为是光耦隔离,实际电平不对。
  • 检查命令字是否正确。不同Flash的命令集大体一致,但个别型号的读ID命令可能是0x9F,而读状态寄存器是0x05,写错命令也可能导致后续读回异常。

回读全是0则相对少见,通常意味着MISO引脚被下拉到了地,或者FPGA的IO配置成了内部下拉。另外还有一种情况是DMA接收通道没有真正使能,接收缓冲区里的数据一直是初值0,而不是"SPI读回"的数据。

5.2 数据错位、多读一拍、丢帧的对症方案

出现错位或者"每帧数据多一拍",首先要检查CPOL和CPHA。SPI的四种模式,最简单的判断方法是看数据手册里的时序图:SCLK空闲电平是低还是高,数据采样是在第一个边沿还是第二个边沿。选错模式时,从机可能把相邻两bit的数据当成一个bit来解析,表现出来就是整体错一位,或者首尾各多出一个假bit。

接下来要看SCLK的频率。某些Flash或者传感器在高速SCLK下传输距离稍远,时序余量不够,也会出现偶发的数据错位。与其在软件里反复重试,不如先把SCLK降到5MHz试试,确认是频率问题还是逻辑问题,再逐步升频。

再就是FIFO读空/写满的判断。AXI Quad SPI的FIFO如果读空,你再取数据,取出来的可能就是垃圾数据。好在IP核有状态寄存器可以查询FIFO是否为空、是否半满等标志,驱动代码里最好在每次接收前查询一下状态寄存器,别闷头读。

丢帧问题往往和中断响应的及时性有关。中断优先级低、触发频繁时,CPU来不及搬走FIFO里的数据,就会丢帧。这时候要么加深FIFO,要么直接DMA。我实测过,数据量超过1KB之后,再靠中断逐字节处理,在Zynq上很容易出现丢帧,DMA才是稳定方案。

5.3 与STM32和单片机的SPI对照调试技巧

如果你手头有STM32这类MCU,恰好它也有SPI,那可以做一件很有价值的事:先用STM32的硬件SPI去读同一个Flash,排除外设本身的问题。

STM32上配置SPI用的是CubeMX,你配置好SPI引脚、时钟极性和相位,然后同样发送0x9F命令读JEDEC ID。如果STM32能正确读回0xC8,说明Flash没问题,问题一定在你的FPGA侧;如果STM32也读不回来,那大概率是Flash接线、供电或焊接的问题。

调试上还有一个很实用的方法:两边SPI时钟频率保持接近,比如都是10MHz。然后比较两者的SCLK波形,看占空比、上升沿位置是否有明显差异。SPI这种同步协议,其实对时钟占空比的要求没那么严格,只要满足建立保持时间就行,但波形差异能帮你快速判断是不是引脚约束、IO驱动能力配置的问题。

我在Xilinx平台上调试SPI时,还遇到过一个独特的问题:Xilinx的IOBUF、延迟链和bank的IO标准配置会影响信号边沿斜率,如果IO配置成了慢速摆率,高频SPI脉冲会变圆,导致协议错误。这时候在XDC里加上SLEW FAST约束通常能解决问题。这个细节在STM32上几乎不需要关注,但在FPGA里真的会让人抓狂。

结尾想说的话

做FPGA这些年,SPI是我用得最多的串行协议之一。刚开始我也沉迷过自己写SPI时序逻辑,觉得能掌控每一个时钟边沿很酷。后来发现,工程上稳定压倒一切,AXI Quad SPI IP核虽然配置起来有些细节,但一旦吃透,节省的时间和精力非常可观。

最后分享一个自己的习惯:每次新建SPI相关工程,我都会先做一个"点亮链路"的最小测试——只读JEDEC ID或者只写一个寄存器再读回来,确认物理链路和IP配置没问题之后,才会继续往里面加功能。这个习惯帮我避开了很多"表面上看是SPI问题,实际上是模块间配合问题"的坑。希望这篇内容能帮你少走几步弯路,尤其是那些我踩得最深的地方。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 1:09:56

AC63蓝牙名修改导致iOS不可见的广播包溢出原理与解决方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:09:27

Samba框架:面向显著性检测的状态空间模型架构

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:09:07

开源硬件项目查找指南:从入门到量产的四阶学习路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:08:17

包图:UML中最被低估的架构图,如何理清系统边界与依赖

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:07:45

双网卡同时上内外网?Windows路由表配置与排障全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/2 1:07:45

JavaWeb故障排查地图:Servlet容器、HTTP协议与Maven协同原理

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华