1. 为什么要在FPGA里折腾AXI Quad SPI这个IP核
但凡做过FPGA嵌入式项目的人,大概率都绕不开SPI Flash这颗小芯片。无论是上电加载比特流、存储配置参数,还是跑一个轻量级文件系统,SPI Flash几乎是最经济实惠的选择。而Xilinx 7系列及之后的器件里,AXI Quad SPI IP核就是官方给出的标准答案——它把SPI控制器、AXI总线接口、可配置的FIFO深度、以及针对不同Flash厂商的时序适配逻辑全部打包好了,你只需要在Vivado里点几下,就能得到一个能读写Flash的硬件模块。
但问题恰恰出在“点几下”这三个字上。我见过太多项目卡在Quad SPI的配置环节:有人把标准SPI模式配成了Quad模式结果读出来全是0xFF,有人忘了设置FIFO深度导致大数据量传输时丢字节,还有人被LQSPI(Linear Quad SPI)模式和传统SPI模式的区别绕得晕头转向。更别提那些Flash指令集的细节了——0x03读、0x02写、0xEB快速读、0x20扇区擦除,每条指令背后都有时序要求和地址字节数的讲究。
这篇文章就是要把AXI Quad SPI IP核从架构到实战彻底讲清楚。我会先拆解IP核的内部结构和寄存器映射,然后重点讲清楚标准SPI、Dual SPI、Quad SPI三种模式在配置上的本质区别,接着用真实的Flash操作案例(读ID、扇区擦除、页编程、快速读)把指令流程走一遍。最后分享几个我在实际项目中踩过的坑和调试技巧,包括ILA抓波形时该看哪些信号、时序参数怎么算、以及为什么有时候读出来的数据会错位。
适合阅读的人群:正在用Xilinx FPGA做嵌入式存储方案的工程师、需要从零搭建SPI Flash读写逻辑的学生、以及被AXI Quad SPI配置搞得头大的开发者。前提是你对Verilog或VHDL有基本了解,知道AXI总线大概是怎么回事,至于SPI协议本身,我会在需要的地方补充说明。
2. AXI Quad SPI IP核的内部架构拆解
2.1 从AXI接口到SPI物理层的完整数据通路
AXI Quad SPI IP核的内部结构可以分成四个主要部分:AXI接口层、控制寄存器组、TX/RX FIFO、以及SPI移位引擎。当你通过AXI总线写入数据时,数据首先进入TX FIFO,然后由SPI移位引擎按照配置的时钟极性和相位逐位(或逐四位)推送到物理引脚上。接收方向则反过来,物理引脚上的数据经过移位引擎组装成字节后存入RX FIFO,等待AXI主机读取。
这里有个关键点容易被忽略:TX FIFO和RX FIFO的深度是可配置的,在Vivado的IP配置界面里可以选16、64或256字节。FIFO深度直接决定了单次传输的最大数据量,如果你要连续写入一个4KB的扇区,FIFO深度不够的话就需要分多次传输,每次都要等待FIFO有空闲空间。我在一个高速数据采集项目里就吃过这个亏——当时选了16字节的FIFO深度,结果写Flash的吞吐率只有理论值的十分之一,后来改成256字节才把性能拉上来。
SPI移位引擎是IP核的核心,它支持三种工作模式:标准SPI(单线)、Dual SPI(双线)、Quad SPI(四线)。标准模式下,MOSI和MISO各一根线,每个时钟周期传输1个bit;Dual模式下,两根线同时传输,每个周期2个bit;Quad模式下,四根线同时传输,每个周期4个bit。模式的选择不仅影响引脚分配,还直接决定了你能使用哪些Flash指令——比如0x03读指令只支持标准SPI模式,而0xEB快速读指令则支持Quad模式。
2.2 寄存器映射与关键控制位详解
AXI Quad SPI IP核的寄存器空间分为两部分:控制寄存器区和数据收发区。控制寄存器区从基地址开始,包含配置寄存器、状态寄存器、使能寄存器、从设备选择寄存器等。数据收发区则映射到基地址加0x20的偏移处,写入这个地址就是往TX FIFO压数据,读取这个地址就是从RX FIFO取数据。
配置寄存器(偏移0x00)里有几个位特别关键。Bit 0是使能位,必须置1才能启动SPI控制器;Bit 1是主从模式选择,做Flash控制器时当然设为主模式;Bit 2是LSB/MSB优先选择,绝大多数Flash都是MSB优先,所以设0;Bit 3是时钟极性(CPOL),配合Bit 4的时钟相位(CPHA)决定SPI的工作模式。Flash芯片通常支持Mode 0(CPOL=0,CPHA=0)和Mode 3(CPOL=1,CPHA=1),具体用哪个要看Flash的数据手册。
状态寄存器(偏移0x04)里的Bit 0是TX FIFO空标志,Bit 1是TX FIFO满标志,Bit 2是RX FIFO空标志,Bit 3是RX FIFO满标志。在写数据之前一定要检查TX FIFO是否满,读数据之前要检查RX FIFO是否空,否则会丢数据或读到无效值。我习惯在代码里封装两个函数:wait_tx_not_full()和wait_rx_not_empty(),每次操作前都调用一下,虽然看起来啰嗦,但能避免90%的通信异常。
使能寄存器(偏移0x08)控制着SPI控制器的全局使能,Bit 0是SPI使能,Bit 1是主模式使能,Bit 2是TX FIFO复位,Bit 3是RX FIFO复位。在初始化阶段,通常先复位两个FIFO,然后使能SPI和主模式。从设备选择寄存器(偏移0x10)用来控制片选信号,Bit 0对应SS[0],做单Flash应用时置1即可。
2.3 FIFO深度与中断机制的取舍
FIFO深度的选择是一个典型的权衡问题。深度越大,单次传输能容纳的数据越多,AXI总线上的事务开销就越小,但占用的BRAM资源也越多。以7系列FPGA为例,16字节深度的FIFO大约消耗1个BRAM块,256字节深度则要消耗4到5个。如果你的设计里BRAM资源紧张,16字节深度也能用,只是软件上要更频繁地轮询状态寄存器。
中断机制是另一个值得关注的配置项。IP核支持TX FIFO空、TX FIFO满、RX FIFO空、RX FIFO满等多种中断源。在裸机环境下,用轮询方式就够了;但在Linux系统里,用中断方式能显著降低CPU占用率。中断的使能和清除都要通过控制寄存器操作,清除中断时要注意写1清零的规则,别写成写0清零了。
注意:FIFO深度在IP核生成后就固定了,后期想改必须重新生成IP核并重新综合布局布线。所以项目初期就要估算好最大传输数据量,宁可选大一点也别选小了。
3. 标准SPI、Dual SPI、Quad SPI的配置差异与选型逻辑
3.1 三种模式的引脚占用与吞吐率对比
标准SPI模式占用4根线:SCK、MOSI、MISO、SS。每个时钟周期传输1个bit,假设SCK跑50MHz,理论吞吐率就是50Mbps,也就是6.25MB/s。Dual SPI模式把MOSI和MISO复用为IO0和IO1,每个周期传输2个bit,吞吐率翻倍到12.5MB/s。Quad SPI模式进一步把WP和HOLD引脚复用为IO2和IO3,每个周期传输4个bit,吞吐率再翻倍到25MB/s。
但实际吞吐率远达不到理论值,因为Flash指令本身有开销。比如读操作,标准SPI用0x03指令,发送指令字节加3字节地址后,数据才开始输出;Quad SPI用0xEB指令,发送指令字节加3字节地址加1字节模式位后,还需要等待若干个dummy周期,数据才出来。Dummy周期的数量取决于SCK频率和Flash的访问时间,频率越高,需要的dummy周期越多。
| 模式 | 引脚占用 | 每周期bit数 | 典型指令 | 理论吞吐率(50MHz SCK) |
|---|---|---|---|---|
| 标准SPI | SCK, MOSI, MISO, SS | 1 | 0x03 | 6.25 MB/s |
| Dual SPI | SCK, IO0, IO1, SS | 2 | 0x3B | 12.5 MB/s |
| Quad SPI | SCK, IO0-IO3, SS | 4 | 0xEB | 25 MB/s |
选型逻辑很直接:如果只是偶尔读写几个配置参数,标准SPI足够了,引脚少、时序简单、调试容易。如果要存储大量数据(比如图像、音频、日志),Quad SPI是必然选择,但要注意PCB布线时IO2和IO3要上拉,否则Flash可能进入不了Quad模式。
3.2 Vivado中IP核配置的关键参数
在Vivado里配置AXI Quad SPI IP核时,有几个参数必须根据你的Flash型号来定。首先是SPI模式,在“Mode”选项里选Standard、Dual或Quad。其次是时钟频率,这个值要填AXI总线时钟的分频系数,最终SCK频率等于AXI时钟除以分频系数。比如AXI时钟100MHz,分频系数设4,SCK就是25MHz。
FIFO深度前面说过了,根据数据量选。数据宽度通常选8位,因为Flash操作都是按字节来的。从设备数量做单Flash时选1,多Flash级联时选2或更多。地址宽度这个参数容易被忽略——它指的是AXI地址总线的宽度,不是Flash地址的宽度。Flash地址固定是24位(3字节),对应16MB空间,超过16MB的Flash需要4字节地址模式,那是另一套配置了。
还有一个隐藏很深的参数:“Enable Startup”。这个选项如果勾上,IP核会在FPGA配置完成后自动发送一条Flash读取指令,把Flash里的数据加载到FPGA的配置存储器里。做Golden Image或Fallback设计时会用到,普通应用千万别勾,否则上电后SPI总线会被IP核占用,你的软件还没跑起来Flash就被读了。
3.3 模式切换时的时序陷阱
从标准SPI切换到Quad SPI不是改个寄存器就完事了,Flash那边需要先发送一条使能Quad模式的指令(通常是0x35),然后再发写状态寄存器指令(0x01)把QE位置1。这个过程必须在标准SPI模式下完成,因为Quad模式还没使能呢。等QE位写成功后,再切换到Quad模式,后续的读写才能用4根线。
我遇到过一个问题:Flash的QE位写成功了,但IP核这边忘了改配置,结果还在用标准SPI模式发0xEB指令,Flash收到指令后把IO0-IO3都当成数据线驱动,而IP核只从MISO(也就是IO1)读数据,读出来的全是错位的。排查这种问题的方法是用ILA抓SPI总线的波形,看指令字节是否正确、地址字节是否对齐、dummy周期数是否匹配。
提示:不同厂商的Flash对QE位的定义可能不同。Winbond的W25Q系列用状态寄存器2的Bit 1,Micron的N25Q系列用状态寄存器的Bit 7。写QE位之前一定要查对应型号的数据手册,别照搬别人的代码。
4. Flash指令实战:从读ID到扇区擦除的完整流程
4.1 读JEDEC ID:验证SPI通信是否正常的第一步
拿到一块新板子,第一件事不是急着读写数据,而是读Flash的JEDEC ID。这条指令(0x9F)不需要地址,直接发送指令字节后连续读3个字节,分别代表厂商ID、存储类型、容量。比如Winbond W25Q128的ID是0xEF4018,Micron N25Q128的ID是0x20BA18。
读ID的流程很简单:拉低片选,发送0x9F,然后连续读3个字节,最后拉高片选。但在AXI Quad SPI IP核里,片选是由硬件自动控制的——你往TX FIFO写数据时,IP核会自动拉低片选;当TX FIFO空且RX FIFO有数据时,片选保持低电平;直到你读取完所有数据,片选才拉高。这意味着你不能手动控制片选时序,必须通过FIFO的读写来间接控制。
如果读出来的ID全是0xFF,说明MISO线一直是高电平,可能是Flash没供电、片选没接对、或者SPI模式配置错了。如果读出来的是0x00,说明MISO被拉低了,可能是引脚分配错了。如果读出来的ID只有第一个字节对,后面全是0xFF,那大概率是时钟相位配错了,数据在错误的边沿被采样了。
4.2 扇区擦除与页编程的时序要求
Flash的写入操作有个铁律:只能把1写成0,不能把0写成1。所以每次写入之前必须先擦除,擦除操作会把整个扇区(通常是4KB)全部置为0xFF。扇区擦除指令是0x20,后面跟3字节地址。发送完指令和地址后,Flash内部开始擦除,这个过程需要几十到几百毫秒,期间Flash不响应任何指令,只能通过读状态寄存器(0x05)的Bit 0(WIP位)来判断擦除是否完成。
页编程指令是0x02,后面跟3字节地址,然后是最多256字节的数据。注意页编程不能跨页——如果从地址0x0F0开始写200字节,写到0x100时就会回卷到页首覆盖之前的数据。正确的做法是把数据拆分成不超过页边界的小块,分别写入。
在AXI Quad SPI IP核里实现这些操作时,每次操作前都要先发送写使能指令(0x06),否则Flash会忽略擦除或编程指令。写使能指令不需要数据,只发指令字节就行。发送完之后可以读状态寄存器的Bit 1(WEL位)确认写使能是否生效。
// 扇区擦除的伪代码流程 void flash_sector_erase(uint32_t addr) { spi_write_enable(); // 发送0x06 spi_wait_wel(); // 等待WEL位置1 spi_cs_low(); // 拉低片选 spi_send_byte(0x20); // 扇区擦除指令 spi_send_byte((addr >> 16) & 0xFF); // 地址高字节 spi_send_byte((addr >> 8) & 0xFF); // 地址中字节 spi_send_byte(addr & 0xFF); // 地址低字节 spi_cs_high(); // 拉高片选 spi_wait_wip(); // 等待擦除完成 }4.3 快速读指令在Quad模式下的Dummy周期计算
Quad模式的快速读指令0xEB是吞吐率最高的读方式,但它的时序也最复杂。发送完指令字节(0xEB)和3字节地址后,还需要发送1字节的模式位(通常是0x00或0xA0),然后等待若干个dummy周期,数据才开始输出。Dummy周期的数量取决于SCK频率和Flash的访问时间。
以Winbond W25Q128为例,数据手册给出的访问时间是7ns(对应约140MHz)。如果SCK跑50MHz,周期是20ns,那么至少需要1个dummy周期;如果SCK跑100MHz,周期是10ns,就需要2个dummy周期。但IP核的配置界面里只能设一个固定的dummy周期数,所以SCK频率不能超过Flash支持的最大频率,否则读出来的数据会错位。
计算dummy周期的公式是:dummy_cycles = ceil(Flash访问时间 / SCK周期)。实际配置时建议留一个周期的余量,比如算出来需要2个,就配3个。我在一个项目里把SCK设到80MHz,dummy周期按公式算出来是2个,但实测读数据偶尔会错位,后来改成3个就稳定了。
| Flash型号 | 最大SCK频率 | 访问时间 | 50MHz下dummy周期 | 80MHz下dummy周期 |
|---|---|---|---|---|
| W25Q128 | 104MHz | 7ns | 1 | 2 |
| N25Q128 | 108MHz | 6ns | 1 | 2 |
| S25FL128 | 133MHz | 5ns | 1 | 1 |
4.4 状态寄存器轮询与超时保护
Flash的擦除和编程操作都需要时间,WIP位(Write In Progress)会在这段时间内保持1。软件上必须轮询这个位,直到它变成0才能进行下一步操作。但轮询不能无限等下去——如果Flash坏了或者引脚接触不良,WIP位可能永远不变。所以必须加超时保护,比如轮询10000次后如果WIP还是1,就报错退出。
读状态寄存器的指令是0x05,发送指令后连续读一个字节,Bit 0就是WIP位。在AXI Quad SPI IP核里,读状态寄存器的操作和读ID类似,都是发送指令后读数据。但要注意每次读状态寄存器都要重新拉低片选,不能在一次片选周期内连续读多次,因为Flash的状态寄存器是易失的,片选拉高后状态可能变化。
// 带超时保护的WIP轮询 int flash_wait_wip(uint32_t timeout) { uint8_t status; while (timeout--) { spi_cs_low(); spi_send_byte(0x05); status = spi_recv_byte(); spi_cs_high(); if ((status & 0x01) == 0) { return 0; // 操作完成 } } return -1; // 超时 }注意:擦除操作的最大时间在数据手册里通常标的是“典型值”和“最大值”。比如W25Q128的4KB扇区擦除典型时间是45ms,最大值是400ms。超时阈值要按最大值来设,别按典型值设,否则批量擦除时容易误报超时。
5. 调试实战:ILA抓波形与常见异常排查
5.1 ILA探针该接哪些信号
调试SPI通信,ILA是最有力的工具。但探针接哪些信号很有讲究——接少了看不到关键信息,接多了浪费BRAM资源。我的习惯是接这五组信号:SCK、MOSI(或IO0)、MISO(或IO1)、SS、以及IP核的TX/RX FIFO计数。SCK和SS用来判断时序,MOSI和MISO用来判断数据,FIFO计数用来判断软件是否及时读取了数据。
如果用的是Quad模式,还要接IO2和IO3。但ILA的探针数量有限,7系列FPGA的ILA最多支持1024个探针,接8根SPI线绰绰有余。采样深度建议设成4096或8192,因为一次Flash操作可能持续几百个时钟周期,深度太小抓不全。
触发条件设成SS的下降沿最方便——每次片选拉低就是一次新的SPI事务开始。触发位置设成中间偏前,这样能看到指令字节和地址字节。如果只想看数据阶段,可以把触发条件设成“MOSI等于0xEB”之类的。
5.2 读数据错位的三种典型原因
读数据错位是SPI调试中最常见的问题,表现是读出来的数据整体偏移了1个bit或1个字节。第一种原因是时钟相位配错——CPHA=0时数据在SCK的第一个边沿采样,CPHA=1时在第二个边沿采样。如果Flash要求CPHA=0但你配了CPHA=1,数据就会偏移半个周期,读出来全是乱的。
第二种原因是dummy周期数不对。前面说过,dummy周期不够的话,Flash还没把数据准备好你就开始采样了,读出来的前几个bit是无效的。用ILA看波形时,如果发现数据阶段的第一个字节和预期不符,但后面的字节都对,那基本就是dummy周期的问题。
第三种原因是FIFO读取时机不对。AXI Quad SPI IP核的RX FIFO在接收数据时,如果软件读取太慢,FIFO满了之后新数据会覆盖旧数据。用ILA看RX FIFO计数,如果发现计数经常打到满,就说明软件轮询太慢了,要么提高轮询频率,要么加大FIFO深度。
5.3 写操作失败的排查链路
写操作失败比读操作更难排查,因为写错了不会立刻报错,要等读回来才发现数据不对。我的排查链路是这样的:第一步确认写使能是否生效,读状态寄存器的WEL位,如果是0说明0x06指令没发出去或者Flash没响应。第二步确认擦除是否完成,读WIP位,如果是1说明还在擦除,不能写。第三步确认地址是否对齐,页编程不能跨页,地址要按256字节对齐。第四步确认数据是否真的写进去了,擦除后先读一遍确认全是0xFF,写入后再读一遍对比。
如果这四步都过了但数据还是不对,那就要怀疑硬件问题了——PCB上SPI线的走线太长导致信号完整性变差、上拉电阻阻值不对导致Quad模式切换失败、或者Flash的WP引脚没上拉导致写保护生效。用示波器看SCK和MOSI的上升沿,如果上升沿有明显的振铃或过冲,那就是信号完整性问题,需要在PCB上加端接电阻或降低SCK频率。
| 异常现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读ID全0xFF | Flash未供电、片选未接、模式配错 | 万用表测电压、ILA看SS信号 |
| 读ID全0x00 | MISO引脚分配错误 | 检查约束文件 |
| 读数据整体偏移 | CPHA配错、dummy周期不对 | ILA看采样边沿 |
| 写数据后读回不对 | 未擦除、未写使能、跨页写入 | 读状态寄存器、检查地址 |
| 擦除超时 | Flash损坏、WP引脚保护 | 换Flash、检查WP上拉 |
5.4 性能优化:从轮询到DMA的演进
裸机环境下用轮询方式操作SPI Flash,CPU占用率很高,因为每次读写都要等FIFO状态。如果数据量大,比如要读1MB的图片数据,轮询方式会让CPU几乎满负荷。优化方向有两个:一是用中断代替轮询,TX FIFO空或RX FIFO满时触发中断,CPU在中断里填充或读取数据;二是用DMA搬运数据,AXI Quad SPI IP核支持AXI Stream接口,可以接DMA控制器,让数据在Flash和DDR之间直接搬运,CPU完全不参与。
不过DMA方式有个坑:AXI Quad SPI的AXI Stream接口只支持单方向传输,要么全读要么全写,不能同时读写。而且DMA的传输长度要按FIFO深度对齐,否则最后几个字节会丢。我在一个视频采集项目里用DMA读Flash,一开始没对齐长度,结果每帧图像的最后16个字节都是乱的,后来把传输长度改成FIFO深度的整数倍才解决。
提示:如果项目对启动时间有要求,可以把FPGA比特流和应用程序都放在Flash里,用IP核的Startup功能实现上电自动加载。但要注意Startup模式下SPI总线被IP核独占,软件里不能再操作同一个IP核,需要另开一个SPI控制器或者用GPIO模拟SPI。
6. 几个让我印象深刻的踩坑记录
6.1 那个让我熬夜到凌晨三点的QE位
有一次做一个音频播放项目,Flash是Winbond的W25Q64,需要Quad模式读音频数据。我按照惯例发了0x35指令使能Quad模式,然后写状态寄存器2的QE位。代码写完后读ID正常,但一切换到Quad模式读数据就全是0xFF。用ILA抓波形,发现IO0-IO3上都有信号,但IP核读出来的数据就是不对。
排查了整整一个晚上,最后发现是状态寄存器2的地址写错了。W25Q64的状态寄存器2需要用0x35指令读取,但写的时候要用0x31指令,而不是0x01。0x01是写状态寄存器1的指令,我把它当成通用的写状态寄存器指令了。改成0x31之后,QE位写成功,Quad模式立刻正常。
这个坑的教训是:不同厂商、甚至同一厂商不同系列的Flash,指令集都有细微差别。写代码之前一定要把对应型号的数据手册翻出来,把指令表打印出来贴在显示器旁边。
6.2 FIFO深度不够导致的间歇性丢数据
另一个项目是做数据记录仪,每秒钟往Flash里写4KB数据。一开始用16字节深度的FIFO,大部分时候正常,但偶尔会丢几十个字节。因为丢数据是间歇性的,排查了很久都没找到规律。后来用ILA长时间抓波形,发现每次丢数据都发生在AXI总线有其它主机竞争的时候——当CPU在访问DDR时,AXI总线仲裁器会把SPI的优先级降低,导致TX FIFO空了之后软件没能及时填充,SPI移位引擎就把空FIFO里的无效数据发出去了。
解决方案是把FIFO深度从16改成256,这样即使AXI总线被占用几百个周期,FIFO里还有足够的数据供SPI引擎发送。改完之后连续跑了24小时没再丢过数据。这个经历告诉我,FIFO深度不能只按平均数据量算,要按最坏情况下的总线延迟来算。
6.3 时序约束漏配导致的偶发读写错误
最后一个坑是关于时序约束的。AXI Quad SPI IP核的SCK输出是来自AXI时钟的分频,如果AXI时钟和SCK之间没有正确的时序约束,综合工具可能会把SCK的走线延迟估错,导致建立时间或保持时间违例。表现是大部分时候读写正常,但在温度变化或电压波动时偶尔出错。
正确的做法是在XDC文件里对SCK输出加create_generated_clock约束,告诉工具SCK是由AXI时钟分频得到的。同时要对SPI的输入输出引脚加set_input_delay和set_output_delay约束,约束值参考Flash数据手册里的建立时间和保持时间。加上这些约束后,时序报告里SPI路径的裕量从负值变成了正值,偶发错误也消失了。
# SPI时序约束示例 create_generated_clock -name spi_sck -source [get_pins axi_quad_spi_0/inst/axi_clk] \ -divide_by 4 [get_ports spi_sck] set_output_delay -clock spi_sck -max 2.0 [get_ports spi_io0] set_output_delay -clock spi_sck -min 1.0 [get_ports spi_io0] set_input_delay -clock spi_sck -max 3.0 [get_ports spi_io1] set_input_delay -clock spi_sck -min 0.5 [get_ports spi_io1]这些约束值不是拍脑袋写的,2.0ns的输出延迟来自Flash的输入建立时间要求,3.0ns的输入延迟来自Flash的输出有效时间。具体数值要查你用的Flash型号的数据手册,不同型号差别很大。
6.4 软件复位后IP核不工作的诡异现象
还有一次遇到一个很诡异的问题:系统软复位后,AXI Quad SPI IP核不工作了,读什么都是0。但硬复位(重新上电)就正常。查了很久发现是软复位没有复位IP核的SPI使能位。软复位只复位了CPU和部分外设,SPI控制器的使能寄存器还是复位前的状态,但FIFO里的数据已经乱了。
解决方法是在软件初始化阶段,先往使能寄存器写0关闭SPI,然后写1复位TX和RX FIFO,最后再重新使能SPI和主模式。这个顺序不能乱——先关使能再复位FIFO,否则FIFO复位时可能有未完成的事务导致状态机卡死。
// 正确的SPI初始化顺序 void spi_init(void) { // 1. 关闭SPI使能 Xil_Out32(SPI_BASE + 0x08, 0x00); // 2. 复位TX和RX FIFO Xil_Out32(SPI_BASE + 0x08, 0x0C); // 3. 等待复位完成 while (Xil_In32(SPI_BASE + 0x08) & 0x0C); // 4. 使能SPI和主模式 Xil_Out32(SPI_BASE + 0x08, 0x03); // 5. 配置时钟分频和模式 Xil_Out32(SPI_BASE + 0x00, 0x00000000); }这个初始化顺序是我踩了无数次坑之后总结出来的,现在每次新建工程都直接复制这段代码,再也没遇到过软复位后SPI不工作的问题。
7. 从IP核到量产:一些工程化建议
如果你只是做个课程设计或者原型验证,上面这些内容够用了。但如果要量产,还有几件事必须考虑。第一是Flash的寿命管理——NOR Flash的擦写次数通常是10万次,NAND Flash是几千到几万次。如果应用需要频繁写入,必须做磨损均衡,把写操作分散到不同扇区。第二是掉电保护——写入过程中掉电会导致数据损坏,可以在Flash里留一个标志扇区,写入前先写标志,写完后清除标志,上电时检查标志判断上次写入是否完整。
第三是固件升级方案。用SPI Flash做固件存储时,通常把Flash分成三个区:Bootloader区、Golden Image区、Update Image区。Bootloader负责检查Update Image是否有效,有效则跳转执行,无效则回退到Golden Image。AXI Quad SPI IP核的Startup功能可以配合这种方案实现双镜像启动,但配置起来比较复杂,需要仔细规划Flash地址映射。
第四是温度适应性。工业级Flash的工作温度范围是-40°C到85°C,但SPI时序参数会随温度变化。高温下Flash的访问时间会变长,如果dummy周期是按常温算的,高温下可能读出错位。稳妥的做法是按最高工作温度下的访问时间来算dummy周期,或者留足够的余量。
最后说一个我个人的习惯:每次调试SPI Flash,第一件事永远是读JEDEC ID。ID对了,说明硬件连接和基本时序没问题,后面的问题都是软件配置问题;ID不对,先查硬件再查软件。这个习惯帮我节省了大量排查时间,也推荐给你。