AXI Quad SPI IP核寄存器配置与仿真验证全流程

发布时间:2026/11/3 6:33:41
AXI Quad SPI IP核寄存器配置与仿真验证全流程

前后搭过三版 SPI 主机控制器,最后一版索性砍掉了自己写的状态机,全部换成 AXI Quad SPI IP 核。原因不复杂:手写的时序逻辑能跑通功能测试,但只要碰上多从机、速率切换、和 FIFO 或 DMA 的配合,问题就会像潮水一样涌出来。改一次 CPHA 得重画整张状态转移图,加一个从机要重新推敲 SS 的建立保持时间,最折磨人的是仿真波形里那些"差半个时钟周期"的现象,排查一次能耗掉一整天。后来我把 AXI4-Lite 接口转换、SCK 分频、SS 自动建立保持这些活全部交给 IP 核,自己只负责寄存器配置和上层数据搬运,项目节奏才回到正常轨道。这篇就围绕AXI Quad SPI IP 核的寄存器配置与仿真验证全流程展开,把我踩过的坑、算过的分频、写过的初始化序列和搭过的 testbench 全部摊开讲。适合刚开始接触 FPGA 外设集成的新手,也适合已经会用 IP 核但总在调试阶段反复回炉的老手,看完至少能省掉两轮上板返工。

1. 手写SPI时序到底痛在哪,IP核替你接走了什么

1.1 三个我亲身经历过的时序翻车现场

第一个场景是速率切换。当时为了适配一款响应慢的传感器,我在状态机里写死了分频计数器的上限值,结果后面换了一颗能跑 20MHz 的器件,整段代码要从头改一遍,而且改完之后 SS 到第一个 SCK 沿的延时也跟着变了,之前调好的等待时间全部作废。手写方案里,分频值、SS 前后的空拍数、采样沿的位置这三个参数是互相耦合的,改一个就得重新验证另外两个。

第二个场景是 CPHA 边沿错位。SPI 的模式 0 和模式 3 在波形上看起来几乎一样,区别只在空闲电平。我当初把采样沿和移位沿写反,导致 MOSI 上的第一个 bit 被从机吞掉,收到的数据整体左移一位,高位永远是 0。这种 bug 在低速下偶尔能蒙对,一旦时钟拉到 10MHz 以上就稳定复现,而波形上看 SCK 和数据都"很正常",非常容易误判成从机问题。

第三个场景是多从机选通。四路从机共用一组 SCK/MOSI/MISO,SS 用译码方式产生。表面逻辑没问题,但 SS 释放和最后一个 SCK 沿之间的间隔只有一个组合逻辑延迟,某些从机在这个窗口里会把最后一位采样成下一帧的起始位。后来我在 SS 释放前强行插了两个空拍才稳住,可这两个空拍又会拖慢整体吞吐。

1.2 IP核把哪些"脏活"接管了

AXI Quad SPI IP 核把这些耦合参数全部变成了可配置项,而且内部实现是经过验证的固定结构。AXI4-Lite 从机接口那一层,握手、地址译码、读写响应全部由硬件状态机处理,你不需要关心 AWVALID 和 WVALID 的先后顺序,也不需要手动维护 outstanding 计数。SCK 的生成走的是独立的时钟分频链路,分频比要么由编译期的 SCK_RATIO 参数固定,要么由运行期的 SPICR 分频字段动态调整,两种方式都不影响 SS 的时序。

SS 的建立和保持时间由硬件自动插入,典型值是半个 SCK 周期,这个数字在绝大多数从机手册的容忍范围内。FIFO 部分提供 16 深的发送和接收缓冲,配合中断状态寄存器,可以把"发一个字节等一个字节"的阻塞式流程改成批量搬运。另外 IP 核内置了回环自测位(SPICR 的 LOOP 位),把 MOSI 直接短接到 MISO,不接任何外部器件就能验证分频、相位和数据通路的正确性,这一条在早期调试阶段价值极高。

1.3 什么情况下该用IP核,什么情况下还值得手写

资源极度紧张的场合,比如一颗小容量器件里已经塞了三个外设,这时候 IP 核占用的 LUT 和寄存器数量可能会让你犹豫。但说实话,除非你的 SPI 速率低到 1MHz 以下、只挂一个从机、且永远不需要 FIFO,否则省下来的那点资源换不回调试时间。

真正适合手写的情况只有两类:一是协议本身就是非标准变体,比如某些器件要求 SCK 在传输中途改变频率,或者要求双向数据线在同一个时钟沿翻转;二是你需要对每一位的时序做皮秒级微调,比如做时序余量测试。除此之外,多从机、需要中断、需要和 DMA 联动、需要 XIP 直接执行 flash 代码,这些场景用 IP 核都是更划算的选择。

2. IP核配置参数逐项拆解,别在GUI上瞎点

2.1 那几个必须提前想清楚的选项

打开 IP 核的配置界面,参数看着多,真正影响架构的就那么几个。我整理成一张表,按"选错了会不会返工"排序:

参数名常见取值选错后的代价
AXI InterfaceAXI4-Lite / AXI4XIP 模式必须选 AXI4,选错无法直接寻址 flash
SPI ModeStandard / Dual / Quad / EnhancedQuad 线序和引脚数不同,后期改要重画原理图
Frequency Ratio2 / 4 / 8 / 16 / 32决定 SCK 上限,选错只能降速跑
Transaction Width8 / 16 / 32影响寄存器读写宽度和 FIFO 组织方式
Enable FIFO勾选 / 不勾选直接改变 SPISSR 和 SPIDTR 的语义,返工量最大
SPI Mode (CPOL/CPHA)0 / 1 / 2 / 3与从机不匹配则数据整体错位
Enable Master Mode主机 / 从机主从角色反了整块逻辑要重写

其中最容易翻车的是 Enable FIFO 这一项。FIFO 打开和关闭,SPISSR 寄存器的行为是两套逻辑,初始化序列也不一样。我在一个项目里因为中途把 FIFO 从关改成开,结果原来能跑通的代码突然收不到数据,查了半天才发现 SPISSR 在 FIFO 模式下需要先写选通位才会真正拉低 SS。

2.2 SCK频率到底怎么算,给你两个公式

这是被问得最多的问题,也是仿真时最容易被质疑的地方。IP 核的分频逻辑分两种通路:

FIFO 使能时,分频比由编译期参数 SCK_RATIO 决定,公式是:

SCK = AXI_CLK / (2 × SCK_RATIO)

FIFO 关闭时,分频比由 SPICR 寄存器里的分频字段在运行期决定,公式是:

SCK = AXI_CLK / (2 × (DIVIDER + 1))

举个具体例子。假设 AXI 时钟 100MHz,你要跑 6.25MHz 的 SCK:FIFO 模式下选 SCK_RATIO = 8,100 ÷ (2×8) = 6.25,正好。FIFO 关闭模式下要跑 1MHz,则 100 ÷ (2×(D+1)) = 1,解得 D = 49,也就是往分频字段写 49。

注意:不同版本的 IP 核在分频字段的解释上可能有细微差异,有的版本分频值直接等于 SCK_RATIO 而不是 2 倍关系。我的做法是先开 LOOP 回环模式,用示波器或者仿真波形实测一次 SCK 周期,确认公式对得上再往下写业务代码,这一步花五分钟,能省掉后面两小时的困惑。

还有一个隐藏约束:SCK 频率不能超过 AXI 时钟的四分之一,也就是 SCK_RATIO 最小取 2。低于这个值,内部跨时钟域逻辑会来不及采样。

2.3 Standard、Dual、Quad与Enhanced Mode的区别

Standard 模式就是最经典的四线 SPI,SCK、MOSI、MISO 加 SS。Dual 模式把 MOSI 和 MISO 变成双向的 IO0 和 IO1,理论上带宽翻倍,但需要从机支持。Quad 模式再扩展出 IO2 和 IO3,四线并行,带宽是标准模式的四倍,常见于 SPI NOR Flash 的快速读取。

Enhanced Mode 是一个组合选项,勾上之后才会出现 Quad 和 XIP 的配置项。XIP 全称是 Execute In Place,意思是处理器可以直接从 SPI Flash 里取指令执行,不需要先把代码搬到 RAM。这个功能对成本敏感的方案很有吸引力,因为它能省掉一颗并行 NOR Flash 或者大容量 SRAM。但 XIP 模式要求 AXI 接口选 AXI4 而不是 AXI4-Lite,因为需要支持突发读和更宽的地址空间。

选型建议很直接:挂传感器、EEPROM、小屏这类器件,Standard 模式足够;挂 SPI NOR Flash 且需要快速读,用 Quad;要做代码原地执行,才上 Enhanced + XIP。不要为了"看起来高级"盲目开 Quad,四根线的 PCB 走线等长要求会明显提高布线难度。

3. 寄存器地图与初始化序列,这是全流程的地基

3.1 核心寄存器一览

AXI Quad SPI 的寄存器空间是 32 位对齐的,常用的一共十来个。把这张表贴在显示器边上,写代码时基本不用翻文档:

偏移地址寄存器名作用
0x1CDGIER设备全局中断使能
0x20IPISR中断状态,写 1 清零
0x28IPIER中断使能
0x40SRR软件复位
0x60SPICR控制寄存器,模式与分频
0x64SPISR状态寄存器,FIFO 空满与错误
0x68SPIDTR发送数据寄存器
0x6CSPIDRR接收数据寄存器
0x70SPISSR从机选通寄存器
0x74TX_FIFO_OCY发送 FIFO 占用数
0x78RX_FIFO_OCY接收 FIFO 占用数

这套地址是字节偏移,如果你的指针是 32 位宽,直接按这个偏移加到基地址上就行。有一类低级错误我必须提醒:基地址是从 AXI 互联或者地址译码器分配下来的,不同工程里不一样,别把示例代码里的基地址直接抄过来。

3.2 SPICR和SPISR位域逐位解读

SPICR 是整篇文章的核心,绝大部分配置动作都落在这个寄存器上。逐位说明如下:

位名称含义与用法
0LOOP回环使能,MOSI 内部短接 MISO,自测必用
1SPESPI 系统使能,最后一步才置 1
2MASTER主机模式置 1,从机模式清 0
3CPOL时钟空闲电平,0 为低,1 为高
4CPHA采样相位,0 为第一沿采样,1 为第二沿采样
5TX_FIFO_Reset写 1 清空发送 FIFO,硬件自动清零
6RX_FIFO_Reset写 1 清空接收 FIFO,硬件自动清零
7MANUAL_SSFIFO 模式下手动控制 SS 的拉低与释放
8MASTER_INHIBIT禁止主机发起传输,用于多主竞争场景
9LSB_FIRST低位先发,默认是高位先发
23:16DIVIDERFIFO 关闭时的分频值

SPISR 主要看低八位:

位名称含义
0RX_EMPTY接收 FIFO 空,1 表示没数据可读
1RX_FULL接收 FIFO 满
2TX_EMPTY发送 FIFO 空,可继续写入
3TX_FULL发送 FIFO 满,此时写入会丢数据
4MODF模式错误,通常是从机模式下 SS 被外部拉低引起
5SLAVE_MODE_ERR从机模式错误
6CMD_ERR命令错误,多见于 Quad 模式下指令序列不合法
7LOOP_ERR回环错误,回环测试时收到数据与发送不符会置位

轮询式收发最常用的就是第 0 位和第 2 位,一个判断能不能读,一个判断能不能写。第 4 到第 7 位是错误标志,建议在初始化完成后统一读一次清零,避免历史错误干扰判断。

3.3 一套能直接抄的初始化序列

下面这段是裸机环境下的标准初始化,我把它整理成了可直接复用的函数。关键点在于顺序:先复位,再配控制位,最后才使能。

#define SPI_BASE 0x40000000u #define SPI_SRR (SPI_BASE + 0x40) #define SPI_SPICR (SPI_BASE + 0x60) #define SPI_SPISR (SPI_BASE + 0x64) #define SPI_SPIDTR (SPI_BASE + 0x68) #define SPI_SPIDRR (SPI_BASE + 0x6C) #define SPI_SPISSR (SPI_BASE + 0x70) #define SPICR_LOOP (1u << 0) #define SPICR_SPE (1u << 1) #define SPICR_MASTER (1u << 2) #define SPICR_CPOL (1u << 3) #define SPICR_CPHA (1u << 4) #define SPICR_TX_RESET (1u << 5) #define SPICR_RX_RESET (1u << 6) #define SPICR_MANUAL_SS (1u << 7) static void spi_init(uint32_t divider) { /* 第一步:软件复位,写 0xA 触发 */ REG_WRITE(SPI_SRR, 0x0000000Au); /* 第二步:等待复位完成,读回 SPISR 确认非全 1 */ while (REG_READ(SPI_SPISR) == 0xFFFFFFFFu) { /* 空循环等待 */ } /* 第三步:清 FIFO,配置主模式、时钟极性相位、分频 */ uint32_t ctrl = SPICR_MASTER | SPICR_TX_RESET | SPICR_RX_RESET | ((divider & 0xFFu) << 16); /* 模式 0:CPOL = 0,CPHA = 0 */ ctrl &= ~(SPICR_CPOL | SPICR_CPHA); REG_WRITE(SPI_SPICR, ctrl); /* 第四步:使能 SPI 系统,注意 SPE 单独置位 */ REG_WRITE(SPI_SPICR, REG_READ(SPI_SPICR) | SPICR_SPE); /* 第五步:FIFO 模式下选通从机 0 */ REG_WRITE(SPI_SPISSR, 0xFFFFFFFEu); }

关于 SRR 写 0xA 这个细节,一开始我也觉得奇怪,为什么不是写 1。原因是这个寄存器的高四位和低四位有特定的解锁组合,0xA 对应二进制 1010,能同时触发发送和接收通路的复位。不同版本的 IP 核在该寄存器的实现上可能有差异,如果你的版本写 0xA 没反应,可以先读一次 PG 文档确认。

注意:软件复位之后,SPICR 里的所有控制位都会被清空,包括你之前配好的分频和模式。所以任何一次复位之后都必须重写 SPICR,不能只写一次就以为一劳永逸。我在一个低功耗项目里就是因为休眠唤醒后忘了重配,SPI 一直发不出时钟,查了两天才定位到。

4. 收发数据通路:轮询、中断和FIFO怎么选

4.1 FIFO打开与关闭是两套完全不同的逻辑

这是我觉得 AXI Quad SPI 最反直觉的设计。FIFO 关闭的时候,你往 SPIDTR 写一个字节,硬件立刻启动一次传输,SS 自动拉低、发完自动拉高,整个过程不需要你干预从机选通。而 FIFO 打开之后,写 SPIDTR 只是把数据塞进缓冲区,什么时候发、发给谁,取决于 SPISSR 的选通状态和 MANUAL_SS 位。

对比一下两种通路的差异:

维度FIFO 关闭FIFO 使能
数据触发写 SPIDTR 立即发送写入缓冲,按 FIFO 顺序发
SS 控制硬件自动拉低拉高需先写 SPISSR,可用 MANUAL_SS 手动控制
分频来源SPICR 的分频字段编译期 SCK_RATIO 参数
吞吐能力单字节阻塞支持批量连续传输
适用场景简单传感器、调试阶段高速 Flash、DMA 联动

我一般的做法是:早期功能验证阶段把 FIFO 关掉,代码最简单,出问题也容易定位;功能和时序都确认无误之后,再把 FIFO 打开做性能优化。这样能把"协议问题"和"缓冲问题"分开排查,不会一上来就面对十几个变量。

4.2 轮询方式的完整收发实现

轮询是最朴素也最可靠的方式,适合数据量不大、对实时性要求不极端的场景。核心逻辑就两步:等发送 FIFO 有空位,塞数据;等接收 FIFO 有数据,取出来。

static int spi_transfer(const uint8_t *tx, uint8_t *rx, uint32_t len) { uint32_t sent = 0, recv = 0; uint32_t timeout = 1000000u; /* 选通从机 0,FIFO 模式下必须先写 SPISSR */ REG_WRITE(SPI_SPISSR, 0xFFFFFFFEu); while (recv < len) { /* 发送侧:TX FIFO 未满就继续塞 */ if (sent < len && !(REG_READ(SPI_SPISR) & (1u << 3))) { REG_WRITE(SPI_SPIDTR, tx[sent]); sent++; } /* 接收侧:RX FIFO 非空就读 */ if (!(REG_READ(SPI_SPISR) & (1u << 0))) { rx[recv] = (uint8_t)(REG_READ(SPI_SPIDRR) & 0xFFu); recv++; timeout = 1000000u; } else { if (--timeout == 0) { return -1; /* 超时,返回错误 */ } } } /* 传输结束,释放从机选通 */ REG_WRITE(SPI_SPISSR, 0xFFFFFFFFu); return 0; }

这段代码里有几个点值得单独说。第一,超时计数器是必须的,没有它一旦从机不响应,整个系统就死循环在那里,看门狗都救不回来。第二,读 SPIDRR 时只取低 8 位,因为 Transaction Width 如果配成 32 位,高 24 位是无效填充。第三,最后一定要把 SPISSR 写回全 1 释放从机,否则下一次传输开始前 SS 还是低电平,某些从机会认为上一帧没结束。

4.3 中断方式的关键配置步骤

数据量大或者不想占用 CPU 的时候就得走中断。配置顺序有讲究,乱写容易出现中断丢失或者反复进中断出不来。

第一步配 IPIER,把需要的中断源打开。SPI 相关的中断位包括发送 FIFO 空、接收 FIFO 非空、发送 FIFO 满、接收 FIFO 满,以及各类错误。新手最容易犯的错是把错误中断也打开了,结果一个模式错误导致中断风暴,CPU 一直在处理错误中断,正常业务跑不动。

第二步清 IPISR。这个寄存器是写 1 清零,上电时可能残留历史状态,不清干净会立刻触发一次假中断。

第三步写 DGIER 的最低位,打开全局中断使能。这一步必须是最后做的,因为它一开,前面配好的所有中断源立刻生效。如果先开全局再配具体中断源,中间那个窗口期可能会漏掉事件。

第四步在中断服务函数里,先读 IPISR 判断中断源,处理完之后再写回对应位清零。切记不要在服务函数开头就把所有位清零,那样会丢失同时到达的其他中断源。

5. 仿真验证全流程,波形比示波器更好用

5.1 验证环境的三种搭建方案

第一种最省事,直接用 Vivado 的 Block Design 把 IP 核例化好,右键生成仿真文件,工具会自动帮你把 AXI 接口的激励骨架搭起来。缺点是自动生成的 testbench 比较模板化,SPI 侧通常只挂了悬空线,需要自己补从机模型。

第二种是手写 AXI4-Lite 主设备激励。自己控制 AWVALID、AWREADY、WVALID、WREADY 这些信号,能做到精确到时钟周期的控制。这种方式适合排查那些"只在特定握手组合下出现"的怪问题。

第三种是挂一个 SPI 从机行为模型。这个模型不需要多复杂,能按照 CPOL/CPHA 采样数据、能返回预设内容就够。我通常用状态机实现,核心就四个状态:等待 SS 拉低、移位接收、移位发送、等待 SS 释放。

实际项目中我会把第二种和第三种结合起来用,主机侧精确控制寄存器写入,从机侧验证 SPI 时序,两端一对账,问题无处可藏。

5.2 AXI4-Lite写任务的写法

下面这个 task 是我反复用过的版本,支持单次写和单次读,超时保护也加了。

task automatic axi_write(input [31:0] addr, input [31:0] data); begin awaddr = addr; awvalid = 1'b1; wdata = data; wstrb = 4'hF; wvalid = 1'b1; bready = 1'b1; fork begin : AW_HANDSHAKE @(posedge aclk); while (!awready) @(posedge aclk); awvalid = 1'b0; end begin : W_HANDSHAKE @(posedge aclk); while (!wready) @(posedge aclk); wvalid = 1'b0; end join @(posedge aclk); while (!bvalid) @(posedge aclk); bready = 1'b0; end endtask task automatic axi_read(input [31:0] addr, output [31:0] data); begin araddr = addr; arvalid = 1'b1; rready = 1'b1; @(posedge aclk); while (!arready) @(posedge aclk); arvalid = 1'b0; @(posedge aclk); while (!rvalid) @(posedge aclk); data = rdata; rready = 1'b0; end endtask

这里用 fork-join 并行处理地址通道和数据通道的握手,是因为 AXI4-Lite 协议本身就允许这两个通道独立推进。如果写成串行等待,虽然大多数情况下也能跑通,但一旦 IP 核内部实现有特定的握手顺序要求,就会卡死。加超时保护同样重要,仿真里死循环比硬件里更隐蔽,波形跑了几十万纳秒你却以为是在正常工作。

5.3 波形上必须盯住的五个观察点

第一看 SS 到第一个 SCK 沿的间隔。这个间隔通常应该是半个 SCK 周期,如果实测明显偏小,说明分频配置有问题,从机可能来不及准备。

第二看最后一个 SCK 沿到 SS 释放的间隔。这个和上一条对称,同样应该是半个周期。间隔太短会导致从机截断最后一位,这是我在一个高速 Flash 项目里踩过的坑,读回来的数据末尾永远是 FF。

第三看 CPHA 对应关系。模式 0 下,数据必须在第一个上升沿之前稳定,在上升沿被采样。如果你看到数据在上升沿才刚开始变化,那就是 CPHA 配反了,改一位寄存器就能解决。

第四看 FIFO 占用数的变化。抓 TX_FIFO_OCY 和 RX_FIFO_OCY 这两个信号,正常传输时应该是锯齿状上升下降。如果 TX 一直满着不动,说明从机没在消费数据;如果 RX 一直是 0,说明数据根本没进来。

第五看中断触发时机。把 IPISR 的接收非空位拉出来观察,确认它在 RX FIFO 从空变非空的瞬间置位,而不是延迟几个周期。延迟过大的话,高吞吐场景下会来不及取数据导致 FIFO 溢出。

5.4 回环自测是最快的验证手段

如果从机模型还没写好,先用 LOOP 位自测一轮。把 SPICR 的第 0 位置 1,MOSI 内部短接到 MISO,发送什么就该收到什么。这个测试能一次性覆盖分频、相位、数据通路和 FIFO 逻辑,是排查"到底是我配错了还是从机有问题"的最快手段。

自测流程非常短:复位、配 SPICR(带 LOOP 位)、发一串递增数据、读回来比对。对不上就查 SPISR 的 LOOP_ERR 位,它专门标记回环错误。我一般会跑一个 256 字节的全字节扫描,从 0x00 到 0xFF 全发一遍,能覆盖大部分位翻转问题。

6. 常见问题与排查速查表

6.1 配置类问题:寄存器读回全是0或者写不进去

这种症状十有八九是地址错了。先确认基地址是不是从地址分配器里拿的,再确认偏移有没有算错。有一个很隐蔽的坑:某些工程里 AXI 互联把地址做了重映射,你按数据手册写的偏移在实际总线上可能是另一个值。

还有一种情况是 AXI 时钟没起来。IP 核的 AXI 接口依赖时钟和复位,如果复位信号一直拉低,所有寄存器读写都会被忽略,读回来自然全是 0。检查一下复位极性,是高有效还是低有效,这个在 IP 核配置里可以选。

再有一种是位宽不匹配。Transaction Width 配成 8 位,但你的 AXI 数据总线是 32 位,那读回来的高 24 位是无效的。这时候如果直接拿整个 32 位去判断,会得到永远不对的结果。

6.2 时序类问题:SCK频率对不上或者数据整体错位

SCK 频率对不上,先回去看第 2.2 节的公式,确认 FIFO 是开还是关,两条公式不一样。然后确认 AXI 时钟的实际频率,仿真里你可以直接看时钟周期,硬件上要用示波器量,别信约束文件里写的数字。

数据整体错位,九成是 CPOL/CPHA 和从机不匹配。查从机手册的时序图,看它在哪个沿采样、哪个沿输出,对照模式表选 0 到 3。如果从机手册只写了"上升沿采样",那大概率是模式 0,但也要确认空闲电平。

数据只在高速下错,低速正常,这种通常是建立保持时间不够。检查 SCK 到数据的 skew,如果 PCB 走线太长,把 SCK 降一档先跑通,再考虑加串联电阻或者调整走线。

6.3 数据类问题:首字节丢失或者FIFO溢出

首字节丢失最常见的原因是 SS 拉低和第一次写数据之间的顺序问题。FIFO 模式下,必须先把 SPISSR 写对,再写 SPIDTR。反过来写的话,第一个字节可能在 SS 还没拉低的时候就被移出去了,从机根本没接收到。

FIFO 溢出看 SPISR 的第 1 位和第 3 位。RX_FULL 置位说明接收缓冲满了没人取,这时候后续数据会被丢弃。解决办法要么提高轮询频率,要么改用中断,要么降低 SCK 速率给 CPU 留出反应时间。

读回数据高位恒为 0 或者恒为 F,先看 Transaction Width 和实际数据宽度的匹配关系。8 位数据放在 32 位寄存器里,如果从机返回时高位没处理好,读出来就是脏数据。取数据时用掩码截取有效位,这个习惯能避免大量莫名其妙的错误。

6.4 上板类问题:仿真通过但硬件不工作

仿真通过硬件不工作,第一件事查管脚约束。SPI 的四根线有没有分配到正确的 IO bank,电平标准是不是匹配从机。我见过因为 IO 电平配成 3.3V 但从机是 1.8V,导致数据线上读回来全是高电平的案例。

第二件事查上电时序。有些从机需要在上电后等待一段时间才能接受第一条命令,而 FPGA 配置完成后立刻就发数据,从机还没准备好。解决办法是在初始化函数里加一段延时,或者在发第一条命令前先读一次从机的状态寄存器做探测。

第三件事查 STARTUP 原语。某些器件系列在使用 Quad SPI 时需要例化 STARTUP 原语来配置专用的配置管脚,如果漏了这一步,Quad 模式下 IO2 和 IO3 可能被其他功能占用。

提示:上板调试阶段,最有效的工具不是逻辑分析仪,而是把 SPISR 的值通过串口打印出来。FIFO 空满状态、错误标志位,这几个数字能覆盖八成以上的问题定位,比盯着波形猜要快得多。

6.5 一张速查表收尾

现象最可能的原因优先检查项
寄存器读回全 0基地址错或复位未释放地址映射、复位极性
SCK 无输出SPE 位未置 1SPICR 第 1 位
SCK 频率偏差一倍分频公式用错FIFO 开关状态
数据整体移位CPHA 配置错误SPICR 第 4 位
首字节丢失SS 与数据写入顺序先写 SPISSR 再写 SPIDTR
接收 FIFO 溢出取数不及时提高轮询频率或改中断
只在高速下出错建立保持时间不足降速验证、查走线
硬件不工作仿真正常管脚或电平配置IO 约束、电平标准

我在实际项目里养成的一个习惯是:每换一颗新的从机器件,先用 LOOP 回环把 IP 核自身的配置跑通,再接真实器件。这样一旦出问题,责任范围立刻缩小一半——要么是 IP 核配置,要么是从机协议,不会两头同时怀疑。这个做法看起来多花十分钟,实际上每次都能省下至少半天的排查时间,尤其是在项目后期时间紧张的时候,这十分钟就是救命的那十分钟。