
很多第一次在 Xilinx FPGA 上跑 SPI 的同学第一反应都是打开 Verilog 手写一个 SPI master 模块。想想也不复杂SCLK 高低翻转、MOSI 按位输出、MISO 按位采样一个状态机加两个计数器就搞定了。这个思路本身没问题但你很快就会撞上几堵墙多从机片选怎么灵活切换一轮传输过程中要不要保证 CS 不被释放连续大数据量怎么搬运中断和 DMA 怎么配合这时候回头看一眼 AXI Quad SPI IP 核会发现 Xilinx 其实已经把从机选择、收发 FIFO、中断、DMA 接口这些零件整整齐齐摆在货架上了。这篇文章就是把我从零开始配置 AXI Quad SPI 的完整过程、踩坑记录和排错思路摊开来讲内容包括 Vivado 里的 IP 配置、寄存器模型、仿真验证、裸机和 Linux 下的软件适配覆盖 Zynq 和纯 PL 两种常见场景。适合两种人一种是想在 FPGA 上快速出 SPI 接口但不想从 RTL 造轮子的另一种是已经在用但被片选时序、数据错位这类问题折磨过的。1. 先说清楚AXI Quad SPI 到底是干什么的1.1 SPI 协议本身没那么玄但工程化有成本SPI 是真正意义上的“四根线走天下”SCLK 提供时钟MOSI 从主机发往从机MISO 从从机送回主机CS 负责选人。它不像 I2C 那样有地址和 ACK主机想跟谁说话就把谁的片选拉低收发同时进行逻辑上非常直白。用逻辑分析仪看波形也就是 MISO/MOSI 在 SCLK 边沿前后的数据翻转。但工程上的麻烦从来不在于协议本身而在于外围你要处理不同从机的极性和相位有的器件要求 CPOL0、CPHA1有的要求 CPOL1、CPHA0你要决定多从机时用独立片选还是菊花链传输过程中片选要不要一直保持低电平还是每发完一个字节就松开高速场景下 CPU 不可能一个字节一个字节去搬数据这时候 FIFO、中断、DMA 就必须登场。STM32 用户对这套很熟因为 CubeMX 里 SPI 外设已经是现成的但 FPGA 上这些能力不会平白出现要么自己写 RTL要么选现成的 IP。1.2 AXI Quad SPI 这个 IP 核帮你省掉了什么AXI Quad SPI 是 Xilinx 提供的 SPI 控制器 IP挂在 AXI4-Lite 总线上通过一组寄存器来控制时序、片选、收发和中断。它帮你省掉的东西我总结了四块时序生成CPOL/CPHA 可配时钟分频可配LSB/MSB 优先可配不需要你关心 SCLK 什么时候翻转。收发缓冲内部有 TX/RX FIFO深度可配置读写都有状态位避免一个字节一次中断的尴尬。从机选择支持多个片选自动和手动两种模式能应对大多数外部器件对片选时序的要求。系统集成自带 AXI4-Lite 接口和 Zynq PS、MicroBlaze 甚至自己写的 AXI 主机对接都方便Xilinx 还提供了裸机库函数和 Linux 驱动。光“自带驱动”这一条就值回票价。你自己写的 RTL后面每个软件工程师接手时都得先啃一遍你的状态机换成 IP 核大家看的是同一份寄存器手册沟通成本低得多。1.3 什么时候别用这个 IP直接写 RTL不是所有场景都适合上 IP 核。如果你只是控制一颗单从机的低速传感器时钟几百 kHz一次就读三个字节那手写 RTL 可能两百行就完事了IP 核的配置反而显得笨重。反过来如果项目里要接 SPI Flash、要多从机切换、要长时间连续采数那 AXI Quad SPI 的 FIFO、中断、DMA 这些能力就是你需要的。我个人的判断标准是SPI 时钟超过 10 MHz或者从机数量大于 1或者有 DMA 需求这三条占任何一条就老实上 IP 核。2. Vivado 里添加和配置 AXI Quad SPI 核的完整流程2.1 在 Block Design 里把 IP 拖进来流程很简单Vivado 打开或新建 Block Design 之后在 IP Catalog 面板搜 “AXI Quad SPI”双击添加。新版本 Vivado 里可能会出现名字带版本号的条目选最新的稳定版就行。添加完成后IP 的 AXI4-Lite 从机接口会自动暴露出来如果你用的是 Zynq把它连到M_AXI_GP0或M_AXI_GP1如果是 MicroBlaze连到M_AXI_DC或M_AXI_IC通常连数据总线如果是纯 PL 项目后续就得自己写或配一个 AXI 主机来访问它。这里有个容易忽略的细节SPI 的时钟spi_clk在 IP 里是内部生成的来源是 AXI 总线时钟s_axi_aclk的分频不是独立的外部时钟输入。这意味着你不用额外分配一个时钟引脚但也要意识到SPI 的最高频率受限于 AXI 总线时钟例如总线时钟 100 MHz、最小分频系数 2那么 SCLK 最高就是 50 MHz。2.2 Configuration 界面里每个选项的含义双击 IP 打开配置界面第一眼看到的就是一堆下拉框。我把关键选项和我的推荐值放在一起配置项我常用的值说明ModeStandard标准 SPI 模式四线制。Quad/Dual 是给 SPI Flash 高速读写用的后面单独讲FIFO Depth16默认 16够大多数场景用。需要连续大数据量时再往上调Number of Slave Selects视从机数量一个从机就选 1多个就选对应数量Enable Slave Mode不勾默认只做主机省资源。确实需要从机模式再说Enable Manual Slave Select推荐勾上有了它才能在软件里手动控制片选保持避免自动模式下 CS 乱跳Frequency Ratio配置中会自动给出实际是通过寄存器控制的分频系数第二页还有其他的Enable Interrupt需要中断通知收发完成时勾上。Enable FIFO Debug调试用正常工程不用开。Enable Quad Mode跟前一页的 Mode 联动选了 Quad 才会出现 IO2/IO3 引脚。2.3 从机选择自动与手动这一步决定了后面会不会被坑这是整个配置里最值得停下来理解的选项。IP 核的从机选择有两种工作方式自动模式每次把数据写进 TX FIFOIP 会自动拉低对应片选开始传输等 TX FIFO 里的数据全部发完它会自动拉高片选。对大多数只有一次读或写操作的场合够用。手动模式片选由软件通过寄存器控制拉低之后一直保持直到软件明确改寄存器才释放。适合外部器件要求“一次操作期间片选必须连续拉低多个字节”的场景比如 SPI Flash 的 Read Data 命令主机先发命令码和地址然后连续读一串数据整个过程 CS 要持续低电平。我的建议是只要不是特别简单的单字节读写一律用手动模式。自动模式在遇到“先发命令再读数据”这种时序时经常会因为 TX FIFO 空而提前松开 CS外设就会误以为本次操作结束返回值全是 0xFF。这个问题我见太多了后面排错章节会再详细展开。2.4 Quad 模式、Dual 模式和标准 SPI 怎么选这个命名特别容易让新手误会。AXI Quad SPI 里的 Quad 指的是“该 IP 支持 Quad SPI Flash 的四线读写模式”并不是说你有四个从机。配置界面的 Mode 下拉框里Standard标准 SPISCLK MOSI MISO CS最通用。Dual在读数据阶段用两根数据线通常用于 Dual Output SPI Flash。Quad在命令阶段用单线发命令在数据阶段用四根 IO 线传数据用于 Quad Output / Quad IO SPI Flash。这时 IP 会多出io0_io1_io2_io3引脚对应 Flash 的 IO0~IO3。如果你只接普通 SPI 传感器选 Standard 就行。接 SPI NOR Flash且想充分发挥读写速度可以考虑 Quad。但要注意Quad 模式需要你的外部器件芯片本身支持而且 Quad 模式下仍然可以发标准 SPI 的命令所以很多人会先把 Flash 配置成 Standard 模式做兼容再进入 Quad 模式提速。我实际项目里接得最多的就是 GD25Q128 这类 SPI NOR Flash用的是 Dual 模式读命令阶段还是标准 SPI数据阶段两根线并行速率比标准模式高一倍而且不像 Quad 那样占用额外引脚性价比很合适。3. 寄存器模型把 IP 的“神经中枢”摸清楚3.1 核心寄存器一览硬件配好了接下来是软件。不管后面用不用官方库函数我建议都先把寄存器模型过一遍因为库函数封装再好出问题时最终还是要落到寄存器层面。AXI Quad SPI 的寄存器基地址就是你在 Block Design 里分配的地址例如 Zynq 上可能被分到0x40000000或0x80000000。各个寄存器的偏移如下偏移名称作用0x00SRR软件复位寄存器0x04SPICR主控制寄存器核心中的核心0x08SPISR状态寄存器查询 FIFO 空满状态0x0CSPIDTR数据发送寄存器写数据进 TX FIFO0x10SPIDRR数据接收寄存器从 RX FIFO 读数据0x14SPISSR从机选择寄存器注意是低有效0x1CDGIER全局中断使能0x20IPISR中断状态寄存器0x28IPIER中断使能寄存器SPICR里有几个位是我每次配置都要确认的CPOL和CPHA决定极性和相位对应四种 SPI ModeMASTER决定主从模式SPE是 SPI 总开关LSB FIRST决定位序MANUAL SS ASSERT结合外部引脚选择手动片选还有TX FIFO RESET和RX FIFO RESET出问题时先用它们。读取这些寄存器时建议用 Vivado 里的 Hardware Manager 的 register view或者直接在代码里打印出来排查效率会高很多。3.2 一次读写的完整寄存器时序SPI 是全双工写一个字节的同时会收到一个字节所以 Xilinx 把发送和接收寄存器拆成两个但传输过程是同时进行的。手动模式下一次完整操作的寄存器流程是这样写SPICR确保SPE1MASTER1设置好CPOL/CPHA打开MANUAL SS ASSERT。写SPISSR选择从机。注意这个寄存器低有效需要选中从机 0 时写 0x01 到对应位域。具体位宽取决于配置的从机数量多数情况下写0x01就是 CS0 拉低。将要发送的数据写入SPIDTR一次可以写多个字节IP 内部 FIFO 会把它们按顺序发出去。轮询SPISR的TX FIFO EMPTY等所有数据发完。从SPIDRR读回接收数据。发送几个字节就能读回几个字节即使你只需要从机的返回数据也必须先发出对应数量的字节SPI 没有“只读不发”这种操作。这里有个初学者容易绕晕的点读数据之前为什么要先写数据因为 SPI 是主机产生时钟从机只有在主机发时钟的时候才能把数据放到 MISO 上所以哪怕你要读 4 个字节也得先写 4 个字节内容通常无所谓填 0x00 或 0xFF 都行来产生 4 个字节的时钟窗口。3.3 中断和 DMA 的底层逻辑当传输的数据量上来之后轮询状态寄存器就有点浪费 CPU 了。AXI Quad SPI 支持两种提效手段中断DGIER打开全局中断IPISR里会置位 TX FIFO 空或 RX FIFO 非空等状态再通过IPIER使能对应中断事件。Zynq 上还要把 IP 的中断输出接到 PS 的 GICMicroBlaze 则接到中断控制器。DMAFIFO 深度配置超过 16 时IP 会额外暴露 AXI4-Stream 接口这时可以接 AXI DMA数据搬运就不经过 CPU 了。这个组合适合从 SPI ADC 连续采数或者高速读写 Flash 的场景。中断的好处是省 CPU坏处是每次传输都有中断开销。DMA 适合大于等于一片数据、需要连续搬移的场景。如果只是几十个字节的寄存器读写中断加轮询其实没多大差别。4. 波形验证在仿真和 ILA 里把问题提前拦下4.1 最快的验证方式是用 IP 自带的 Example Design配完 IP 后很多人直接生成比特流上板然后对着逻辑分析仪找问题。我的习惯是先做仿真。不是每个项目都要写完整的 TestbenchAXI Quad SPI 在 Vivado 里右键点开有一个 “Open IP Example Design”会生成一个带 AXI 主机的示例工程里面包含对 SPI Flash 的仿真模型。跑一遍行为仿真你立刻能看到 SCLK、MOSI、CS 的实际波形。如果你自己建 Testbench要模拟一个 AXI master 发读写命令可以参照下面这个简化思路用 task 封装 AXI4-Lite 写操作task axi_write(input [31:0] addr, input [31:0] data); begin (posedge S_AXI_ACLK); S_AXI_AWADDR addr; S_AXI_AWVALID 1b1; S_AXI_WDATA data; S_AXI_WVALID 1b1; wait (S_AXI_AWREADY 1b1 S_AXI_WREADY 1b1); (posedge S_AXI_ACLK); S_AXI_AWVALID 1b0; S_AXI_WVALID 1b0; S_AXI_BREADY 1b1; wait (S_AXI_BVALID 1b1); (posedge S_AXI_ACLK); S_AXI_BREADY 1b0; end endtask读操作类似只是把方向反过来。写 AXI 主机逻辑本身也是一个不小的工作量这也是为什么我推荐先跑 Example Design而不是从零搭 Testbench。4.2 波形图怎么看哪些点是关键仿真跑起来后不要急着看 SCLK按照这个顺序检查看总复位和 AXI 时钟所有寄存器访问都要在aresetn释放之后进行。看spi_cs是否拉低如果总是高说明SPISSR没写对或者从机选择数量配置超了。看spi_sclk有没有波形SCLK 没反应多半是SPE没置位或者TX FIFO里没数据。看 MOSI 数据位的顺序对比SPICR里的LSB FIRST位确认跟从机要求一致。看 MISO 采样点在 SCLK 的哪个边沿采数据是CPOL/CPHA决定的波形不对就去查这两个位。仿真阶段解决问题成本是最低的。等到上了板才发现 MISO 全 0xFF排查起来要多花好几倍时间。4.3 上板后用 ILA 核直接抓波形仿真过了不代表硬件就对电平、接线、晶振频率这些仿真看不到。上板调试我一般会在 Block Design 里加一个 ILA 核把 SCLK、MOSI、MISO、CS 这四根线牵进去。Vivado 会把 ILA 自动接到 JTAG 链路上Debug 窗口里实时看波形。ILA 使用时有两个习惯值得养成一是触发条件设为 CS 下降沿这样每次传输开始都能抓到二是采样深度设深一点把一次完整的命令数据过程都包含进去别只抓前两个字节。5. 软件侧裸机库函数和 Linux 设备树两种玩法5.1 裸机环境直接调 Xilinx 的 XSpi 库如果跑的是 Zynq 裸机或者 MicroBlaze BSPXilinx 已经提供了完整的裸机驱动。开发流程是生成 BSP 时勾选xilspi库代码里包含头文件xspi.h首先查找设备配置#include xspi.h #include xparameters.h XSpi Spi; XSpi_Config *SpiCfg; SpiCfg XSpi_LookupConfig(XPAR_AXI_QUAD_SPI_0_DEVICE_ID); XSpi_CfgInitialize(Spi, SpiCfg, SpiCfg-BaseAddress);然后设置主机模式、手动片选XSpi_SetOptions(Spi, XSP_MASTER_OPTION | XSP_MANUAL_SSELECT_OPTION); XSpi_Start(Spi); XSpi_IntrGlobalDisable(Spi);发送数据时片选寄存器要手动操作XSpi_SetSlaveSelect(Spi, 0x01); // 选中 CS0注意是低有效掩码然后用XSpi_Transfer一次性完成收发u8 TxBuf[8] {0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; // Read Data 命令 u8 RxBuf[8] {0}; XSpi_Transfer(Spi, TxBuf, RxBuf, 8);XSpi_Transfer本质上还是填 FIFO 再轮询状态库里把这些细节藏起来了。要注意它默认是阻塞式的里面会等传输完成别在中断处理函数里调用它否则容易死锁。5.2 Linux 环境下走设备树和 spidevZynq 上跑 Linux 的话AXI Quad SPI 有现成的内核驱动支持。老一点的器件对应spi-xilinx驱动Zynq 系 PS 自带的 SPI 控制器的驱动是spi-zynqmp-gqspi而 AXI Quad SPI 通常走spi-xilinx或通用spi-axi具体要看内核版本。设备树里要把 IP 的地址和中断都配好然后在子节点里声明你接的从机设备。最常用的调试方式是把从机节点配成spidev这样用户态可以直接通过/dev/spidevX.X访问axi_quad_spi_0 { status okay; #address-cells 1; #size-cells 0; num-cs 1; /* 片选数量 */ spidev0 { compatible rohm,dh2228fv; /* 有些内核要求一个具体器件名 */ reg 0x0; /* 对应 CS0 */ spi-max-frequency 1000000; /* 初始允许的最大频率 */ }; };关于compatible有个坑较新的内核不允许直接用spidev作为 compatible必须提供一个真实的器件名或者用rohm,dh2228fv这类已经被内核接受的字符串否则驱动 probe 会失败。遇到spidev注册不上的情况先检查内核版本和 compatible 是否合法。配好之后利用 Linux 自带的spidev_test工具验证# 读一个字节同时发一个字节 spidev_test -D /dev/spidev0.0 -s 1000000 -p \x01\x02\x03-s指定速率-p指定发送数据输出会显示接收到的数据。这个工具能快速确认硬件链路是否通的。5.3 硬件连接里容易忽略的坑软件调完发现数据还是不对回头检查硬件常见的坑有三个电平不匹配FPGA 的 IO 电压是 1.8V 还是 3.3V外部器件工作电压是多少不匹配必须加电平转换。有些 SPI Flash 支持宽电压但很多传感器只支持单一电压。MISO 信号共地问题FPGA 和外设之间必须共地否则电平参考不一样数据会随机错乱。上拉电阻SPI Flash 的 CS、SCLK、MOSI、MISO 在空闲时是高阻还是上拉取决于器件。GD25Q128 这类 Flash 的 MISO 在 CS 拉高后会释放为高阻如果不加上拉MISO 会悬空读回来就全 0xFF。TF 卡这类器件则明确要求 CS、MOSI、SCLK 接上拉。硬件上我习惯在 MISO 上加一个 10 kΩ 上拉到 VCCIOCS/SCLK/MOSI 根据器件手册决定要不要上拉。这个习惯帮我省了不少调板时间。6. 实测中常踩的坑与排错路径6.1 读出来的数据全 0xFF先别急着改软件这是 SPI 调试里的头号问题。碰到读回全是 0xFF我的排查顺序是用 ILA 抓 MISO 波形看 CS 拉低期间 MISO 是持续高电平还是跟随 SCLK 有数据变化。如果 MISO 一直高硬件可能性大。查 MISO 连线、共地、上拉如果 CS 没真正拉到从机从机压根没被激活MISO 会保持高阻上拉后的高电平。如果 MISO 有变化但结果错误查 CPOL/CPHA 是否匹配再查字节序MSB/LSB。如果 MISO 有变化但整体错位一位多半是采样边沿不对把 CPHA 反过来试试。软件里最常犯的错误是XSpi_SetSlaveSelect的参数写反。注意到这个函数的参数是低有效掩码选中 CS0 时传0x01、选中 CS1 时传0x02不是传0xFFFFFFFE。传反了从机永远不会被选中。6.2 SCLK 频率和分频设置不一致AXI Quad SPI 的 SCLK 由 AXI 总线时钟分频而来这个分频系数要结合外部器件的最高时钟能力来定。比如 GD25Q128 的标准 SPI 模式最高支持 120 MHz但你的 AXI 总线时钟是 100 MHz直接最小分频成 50 MHz 也没问题关键是不要超过器件上限。如果发现 SCLK 频率和预期不符别急着怀疑 PLL去寄存器里面看当前分频系数再看看SPICR里有没有被别的地方改掉。有些驱动库里会自动去改分频跟你的手动设置打架。6.3 自动片选导致 CS 提前释放这是最大的坑我在 2.3 节就说过这个问题这里用实际场景再走一遍。假设你给 SPI Flash 发0x03Read Data命令加 3 字节地址然后想连续读 256 字节数据。如果你用的是自动从机选择模式写进 TX FIFO 的只有 4 个字节命令地址IP 发完这 4 个字节后就会认为本轮传输结束自动拉高 CS。可这时候 Flash 还没把数据准备好后续 256 字节根本没法读最终得到的就是一片 0xFF。解决办法就是手动从机选择模式把所有字节写进 FIFO 后再读或者干脆一次性构建一个大缓冲区把所有命令、地址和 dummy 字节一起写进去再用XSpi_Transfer一次完成。这样 CS 在整个传输过程中一直保持低电平。判断是不是这个坑用 ILA 抓 CS 波形如果发现 CS 在数据还没读完就提前拉高那就是自动片选干的。6.4 一个类比把 AXI Quad SPI 看成“可编程的 SPI 翻译官”每次有人问我这 IP 到底怎么理解我都说把它想象成一个翻译官。你不需要懂从机的每一个时序细节只需要通过寄存器告诉他“用什么语速分频、什么口音CPOL/CPHA、叫什么人片选、说什么内容TX FIFO”他就能替你把话传出去再把对方的回答递回来RX FIFO。理解了这个角色分配后面看一些看似“玄学”的现象就容易多了数据不对不是翻译官乱说话就是你没把话说清楚。8. 一点真实的工程体验其实这篇文章写到这最想传达的不是某个具体的寄存器地址而是“选型决定排错成本”这件事。我最早做 FPGA 接传感器时也手写过 SPI master后来项目要接四颗 SPI 器件还要 DMA才转到 AXI Quad SPI 上。回头看如果一开始就直接用 IP 核至少能省掉一版 RTL 的联调时间。Xilinx 的 IP 生态本来就是这么设计的——先用现成的轮子验证整条链路再把精力花在你的核心逻辑上。刚开始接触这套流程会觉得配置项很多但多配几次就会发现真正需要关心的核心就那几个模式、片选方式、分频、CPOL/CPHA。把这几个搞清楚AXI Quad SPI 基本就不会给你找麻烦了。