FPGA通过STARTUPE3原语直接读写配置Flash的实战指南

发布时间:2026/10/6 6:06:57
FPGA通过STARTUPE3原语直接读写配置Flash的实战指南 1. 为什么要在FPGA里直接读写配置Flash做过FPGA项目的朋友大概率都遇到过这样一个场景板子焊好之后配置Flash里是空的或者需要在线升级固件但手头没有专用的下载器或者产品已经装在设备内部根本没法插JTAG。这时候如果FPGA逻辑本身就能读写那颗配置Flash事情就简单多了——上电后FPGA先从Flash加载配置运行起来之后逻辑里再通过SPI接口去擦写Flash的其它区域实现远程升级或者参数存储。这个思路听起来很美好但真正动手的时候第一个拦路虎就是FPGA的配置引脚在配置完成后通常被内部逻辑接管了你没法直接拿普通IO去驱动那些专用的配置引脚。Xilinx的7系列和UltraScale系列FPGA提供了一个叫STARTUPE3的原语专门用来在配置完成后继续访问配置相关的专用引脚其中就包括SPI Flash的时钟、片选和数据线。Intel这边对应的方案是Remote System Update和ASMI Parallel IP思路类似但实现方式不同。我这次的项目背景是一块Kintex-7的板子配置Flash用的是Micron的N25Q128128Mbit的NOR Flash标准SPI接口。需求是在FPGA正常运行期间能够读取Flash的ID、擦除指定扇区、写入数据、再读回来校验。整个链路走的是STARTUPE3原语加自己写的SPI Master控制器没有用MicroBlaze软核也没有用Vivado自带的Flash编程IP纯RTL实现资源占用小时序可控。这篇文章我会把整个实战过程拆开来讲包括STARTUPE3原语到底怎么用、SPI时序怎么对齐、Flash的扇区擦除和页写入有什么坑、Vivado里怎么约束这些专用引脚、以及调试过程中遇到的各种报错和排查思路。如果你也在做类似的事情或者正在被配置完成后无法访问Flash这个问题困扰下面的内容应该能帮你省下不少时间。2. STARTUPE3原语的核心机制与使用要点2.1 这个原语到底解决了什么问题FPGA的配置过程是这样的上电后FPGA处于配置模式内部的配置控制器接管了专用配置引脚比如SPI Flash的CS、DCLK、DIN、DOUT从Flash里读取比特流。配置完成后FPGA进入用户模式这些专用引脚按理说就功成身退了普通逻辑没法直接驱动它们。但实际项目中我们往往需要在用户模式下继续访问配置Flash。比如远程升级时新固件先写到Flash的备用区域然后触发一个IPROG命令让FPGA重新配置又比如把Flash的剩余空间当作非易失存储存一些校准参数、设备序列号之类的数据。STARTUPE3就是Xilinx给7系列及更新架构FPGA提供的一个原语它把配置专用引脚借给用户逻辑使用。具体来说它提供了以下几个关键信号USRCCLKO用户时钟输出连接到配置时钟引脚CCLKUSRDONEO用户DONE信号输出USRDONETSDONE引脚的三态控制DI配置数据输入从Flash到FPGADO配置数据输出从FPGA到FlashDTS数据线三态控制FCSBO片选输出FCSBTS片选三态控制对于SPI Flash的读写我们主要用到的是USRCCLKO、DO、DI、FCSBO以及对应的三态控制信号。这里有个关键点STARTUPE3原语必须例化在顶层模块中而且一个设计里只能例化一次。如果你在多个子模块里都例化综合会直接报错。2.2 原语例化的正确姿势先看一段实际的例化代码这是我在项目里用的STARTUPE3 #( .PROG_USR(FALSE), .SIM_CCLK_FREQ(10.0) ) STARTUPE3_inst ( .CFGCLK(), .CFGMCLK(), .DI(di_from_flash), .DO(do_to_flash), .DTS(dts_ctrl), .FCSBO(fcsbo_out), .FCSBTS(fcsbts_ctrl), .GSR(), .GTS(), .KEYCLEARB(), .PACK(), .USRCCLKO(usr_cclk), .USRCCLKTS(usr_cclk_ts), .USRDONEO(), .USRDONETS() );几个参数和信号需要重点说明PROG_USR参数设为FALSE表示不使用用户触发的重配置功能。如果你需要做IPROG远程升级这个参数要设为TRUE并且还要配合ICAP原语一起使用。SIM_CCLK_FREQ只在仿真时起作用设置CCLK的频率单位是MHz。实际硬件上CCLK的频率由STARTUPE3内部逻辑和你的时钟源决定。USRCCLKTS是CCLK的三态控制低电平有效。也就是说当usr_cclk_ts为0时CCLK输出使能为1时CCLK为高阻。这个信号必须由你的逻辑正确控制否则CCLK一直输出或者一直高阻都会导致Flash通信失败。FCSBTS同理控制片选的三态。当fcsbts_ctrl为0时FCSBO输出使能。DTS控制数据线DO的三态。当dts_ctrl为0时DO输出使能为1时DO高阻此时DI可以接收Flash返回的数据。这里有个很容易踩的坑DI和DO的方向。在SPI模式下FPGA的DO连到Flash的SI或DIFPGA的DI连到Flash的SO或DO。命名上容易搞混建议对着原理图仔细核对。我第一版就把DI和DO接反了结果读出来的ID全是0xFF。2.3 时钟域的处理STARTUPE3的USRCCLKO需要你提供一个时钟信号。这个时钟的频率决定了SPI的SCK频率。对于N25Q128这类NOR FlashSPI模式0CPOL0CPHA0下最高支持到108MHz但实际用的时候要考虑板级信号完整性和Flash的访问时间。我一开始用的是50MHz的系统时钟直接分频到12.5MHz作为SCK实测读写都正常。后来为了加快升级速度提到了25MHz也没问题。但再往上提到50MHz的时候读ID正常读数据偶尔出错。用示波器看波形发现SCK的上升沿有振铃Flash端接收到的时钟质量下降。后来在SCK线上串了一个22欧姆的电阻问题解决。所以这里给个经验值SCK频率在25MHz以下时一般不需要额外处理超过25MHz建议在PCB上预留串阻位置或者降低频率换取稳定性。远程升级场景下25MHz已经足够快了128Mbit的Flash全片擦写也就几十秒的事。另外USRCCLKO的时钟源最好来自一个全局时钟缓冲BUFG不要直接用普通IO或者局部时钟否则时钟偏斜会很大SPI时序容易出问题。3. SPI Master控制器的RTL设计与时序对齐3.1 状态机设计思路SPI Master控制器我采用的是经典的四状态机结构IDLE、CMD、DATA、DONE。但Flash操作和普通SPI设备不太一样Flash的命令有单字节命令如读ID、写使能、带地址的命令如读数据、页写入、扇区擦除还有带Dummy周期的快速读命令。所以状态机需要能灵活配置每个阶段的字节数和Dummy周期数。我的做法是定义一个命令配置寄存器组包含以下字段字段位宽说明cmd_byte8命令码addr_bytes2地址字节数0/3/4dummy_cycles4Dummy周期数以SCK周期计data_bytes16数据阶段字节数direction10写1读这样设计的好处是上层模块只需要配置这个寄存器组然后启动传输状态机自动完成整个SPI帧的发送和接收。对于不同的Flash命令只需要改配置寄存器的值不需要改状态机逻辑。比如读ID命令0x9F配置为cmd_byte0x9Faddr_bytes0dummy_cycles0data_bytes3读3字节IDdirection1。扇区擦除命令0xD8配置为cmd_byte0xD8addr_bytes3dummy_cycles0data_bytes0direction0。3.2 SCK相位与数据采样的对齐SPI模式0下数据在SCK的上升沿被采样在下降沿变化。这意味着发送数据时在SCK下降沿更新DO这样接收端在下一个上升沿能采到稳定的数据。接收数据时在SCK上升沿采样DI。我的实现方式是用一个2倍频的时钟来生成SCK和数据。比如系统时钟50MHzSCK目标25MHz那么用一个计数器产生一个相位标志计数器为0时SCK为低计数器为1时SCK为高。在计数器为0的时刻更新DO在计数器为1的时刻采样DI。具体代码片段always (posedge clk) begin if (spi_en) begin case (sck_cnt) 1b0: begin sck_r 1b0; if (bit_cnt total_bits) begin do_r tx_shift[total_bits-1-bit_cnt]; end end 1b1: begin sck_r 1b1; rx_shift[total_bits-1-bit_cnt] di_sync; bit_cnt bit_cnt 1; end endcase sck_cnt ~sck_cnt; end end这里有个细节di_sync需要先做两级同步因为DI来自Flash相对于FPGA内部时钟是异步信号。不做同步的话亚稳态会导致偶发的位错误。我一开始没加同步器读出来的数据偶尔有几位翻转加了之后就没再出现过。3.3 片选和时序间隔Flash的片选CS在每条命令之间必须拉高至少一个时钟周期这是Flash内部逻辑复位所需要的。我在状态机的DONE状态之后加了一个IDLE_DELAY状态强制CS拉高至少4个SCK周期然后再允许下一条命令。另外扇区擦除和页写入之后Flash需要内部写周期时间。扇区擦除典型值是0.7秒最大4秒页写入典型值是0.7毫秒最大5毫秒。在这段时间内Flash不响应任何命令除了读状态寄存器。所以我的控制器里加了一个轮询状态寄存器的逻辑发送写命令后不断读状态寄存器的bit0WIP位直到WIP变为0才认为操作完成。这个轮询逻辑很重要如果不等WIP清零就发下一条命令Flash会忽略你的命令导致数据丢失。我踩过这个坑连续写两个页第一页写完后没等WIP清零就发第二页的写命令结果第二页数据没写进去读回来全是0xFF。4. Flash擦写操作的完整流程与参数计算4.1 读ID和读状态寄存器读ID是最基础的操作用来确认SPI通信链路是否正常。N25Q128的读ID命令是0x9F返回3字节制造商ID0x20、存储类型0xBA、容量0x18。如果读回来是0xFFFFFF或者0x000000说明通信有问题。读状态寄存器命令是0x05返回1字节。bit0是WIPWrite In Progressbit1是WELWrite Enable Latchbit5是SRWDbit7是BUSY。擦写操作前需要先发写使能命令0x06把WEL置1然后才能发擦除或写入命令。这里有个容易忽略的点写使能命令之后CS必须拉高WEL才会生效。如果你在发完0x06之后不拉高CS就直接发擦除命令WEL不会被置位擦除命令会被忽略。我一开始就是把写使能和擦除命令连在一起发中间没有拉高CS结果擦除一直不成功查了好久才发现这个问题。4.2 扇区擦除的地址对齐N25Q128的扇区大小是64KB共2048个扇区。擦除命令0xD8需要3字节地址地址必须是扇区起始地址也就是低16位为0。如果你给的地址不是扇区对齐的Flash会忽略这个命令或者擦除错误的扇区。我的做法是在上层模块里做一个地址对齐检查如果传入的地址低16位不为0就自动向下对齐到扇区起始地址。同时擦除长度也要按扇区对齐不足一个扇区的按一个扇区算。擦除时间方面N25Q128的64KB扇区擦除典型值是0.7秒最大4秒。实际测试下来室温下大约0.8到1.2秒。所以轮询WIP的时候超时计数器要设得足够大。我用25MHz的SCK读状态寄存器一次大约需要几十个时钟周期轮询间隔设为1毫秒超时设为5秒足够覆盖最坏情况。4.3 页写入的数据组织N25Q128的页大小是256字节。页写入命令0x02每次最多写256字节而且不能跨页。如果你要写的数据超过256字节或者起始地址不是页对齐的就需要拆分成多次页写入。拆分逻辑是这样的假设起始地址是addr长度是len那么计算当前页剩余空间page_remain 256 - (addr % 256)本次写入长度 min(len, page_remain)写入后addr 本次长度len - 本次长度重复直到len为0每次页写入之前都要发写使能命令写入之后要轮询WIP。页写入时间典型值0.7毫秒最大5毫秒。实际测试下来256字节写入大约0.5到1毫秒。这里有个性能优化的点不要每写一页就轮询一次WIP。可以连续发多页写入命令每页之间只发写使能不轮询WIP等所有页都发完之后再统一轮询。但这样做的前提是Flash内部有足够的缓冲而且你的SPI控制器能连续发送。N25Q128支持这种操作但为了简单可靠我还是每页都轮询了。实际升级128KB的固件也就多花几百毫秒可以接受。4.4 读数据的Dummy周期普通读命令0x03支持最高50MHz的SCK不需要Dummy周期。但快速读命令0x0B支持更高的频率需要在地址之后插入8个Dummy周期。我用的是0x0325MHz下稳定工作就没折腾快速读。读数据的时候DI上的数据在SCK上升沿被采样。我的控制器里rx_shift寄存器在bit_cnt达到地址和命令的总位数之后开始移位。这里要注意读命令的data_bytes要设对否则状态机会提前结束或者多读。我一般是在上层模块里根据要读的长度动态配置data_bytes。5. Vivado工程配置与引脚约束实战5.1 原语的综合与实现设置STARTUPE3原语在Vivado里是直接支持的不需要额外的IP核。但有几个综合选项需要注意-flatten_hierarchy建议设为none或者rebuilt不要用full。因为STARTUPE3必须例化在顶层如果综合工具把层次打平了可能会出问题。-keep_equivalent_registers建议设为true防止工具把三态控制相关的寄存器优化掉。-resource_sharing建议设为off避免工具把SPI控制器的资源和其他逻辑共享。实现阶段STARTUPE3相关的路径会被自动识别为专用配置路径不需要额外的时序约束。但USRCCLKO的输出时钟需要约束我加了一条create_generated_clock -name usr_cclk -source [get_pins STARTUPE3_inst/USRCCLKO] -divide_by 1 [get_pins STARTUPE3_inst/USRCCLKO]这条约束告诉工具USRCCLKO是一个生成时钟频率和源时钟一致。如果不加工具可能会报时序违例因为默认情况下它不知道这个时钟的频率。5.2 引脚约束的写法配置专用引脚在Vivado里是通过CFGBVS和CONFIG_MODE属性来约束的不需要在XDC里手动指定管脚位置。但如果你用了STARTUPE3需要在XDC里加上set_property CFGBVS VCCO [current_design] set_property CONFIG_VOLTAGE 3.3 [current_design] set_property BITSTREAM.CONFIG.SPI_BUSWIDTH 1 [current_design] set_property BITSTREAM.CONFIG.CONFIGRATE 25 [current_design]CFGBVS设为VCCO表示配置bank的电压跟随VCCO。CONFIG_VOLTAGE设为3.3因为我的Flash是3.3V供电。SPI_BUSWIDTH设为1表示用标准SPI模式不是Quad SPI。CONFIGRATE设为25表示配置时钟25MHz。这里有个坑如果你在用户模式下用STARTUPE3访问Flash配置比特流的时候也要用相同的SPI模式。也就是说如果用户逻辑用标准SPI比特流也必须用标准SPI。如果比特流用了Quad SPI用户逻辑再用标准SPI可能会因为Flash的Quad使能位被置位而通信失败。5.3 比特流生成与固化生成比特流之后需要把它固化到Flash里。Vivado里可以用Add Configuration Memory Device来添加Flash型号然后Program Configuration Memory Device来烧录。但这个过程需要JTAG连接而且烧录速度受JTAG时钟限制。如果板子已经装到设备里没法插JTAG那就需要用到前面说的远程升级方案FPGA逻辑里通过STARTUPE3读写Flash把新比特流写到Flash的备用区域然后触发IPROG重配置。这个方案需要ICAP原语配合我这次项目没用到但思路是通的。固化的时候有个细节比特流的起始地址和Flash的扇区边界要对齐。N25Q128的比特流通常从0x000000开始占用前几个扇区。用户数据要放在比特流之后的区域比如从0x100000开始。这样升级的时候不会覆盖配置比特流即使升级失败FPGA还能从原始比特流启动。6. 调试过程中遇到的典型问题与排查方法6.1 读ID返回全0xFF或全0x00这是最常见的问题说明SPI通信根本没建立起来。排查顺序如下检查DI和DO是否接反。这是最高频的错误。用示波器看DO上有没有波形如果没有说明DO被约束成了输入或者三态控制反了。检查CS是否拉低。用示波器看FCSBO引脚发命令的时候应该拉低。如果一直高说明FCSBTS控制反了。检查SCK是否有输出。看USRCCLKO引脚应该有25MHz的方波。如果没有检查USRCCLKTS是否拉低以及时钟源是否正常。检查Flash的供电和复位。有些Flash有复位引脚如果复位没释放Flash不工作。我遇到过一次读ID全0xFF查了半天发现是FCSBTS的极性搞反了。STARTUPE3的FCSBTS是低电平有效也就是说fcsbts_ctrl0时CS输出使能。我一开始以为高电平有效结果CS一直高阻Flash根本没被选中。6.2 擦除成功但写入后读回数据错误这种情况通常是时序或者WIP轮询的问题。排查思路确认写使能命令之后CS拉高了。前面说过WEL只有在CS拉高之后才生效。确认WIP轮询正确。读状态寄存器的时候DI上的数据要在SCK上升沿采样。如果采样相位错了读到的WIP永远是0就会在Flash还没写完的时候发下一条命令。确认页写入没有跨页。跨页写入会导致Flash内部地址回绕数据写到错误的位置。确认数据在DO上的建立时间。如果SCK频率太高DO上的数据还没稳定就被Flash采样了写进去的数据就是错的。降低SCK频率试试。我遇到过一次写入后读回数据错位查下来是页写入的地址计算错了。我的地址是字节地址但Flash的页写入命令需要的是字节地址不是字地址。我一开始把字节地址右移了2位当成字地址传进去结果写到了错误的位置。6.3 Vivado实现报错DRC RTSTAT-2这个报错通常和STARTUPE3的时钟约束有关。RTSTAT-2的意思是未约束的时钟资源工具发现USRCCLKO这个时钟没有约束就报错了。解决方法就是前面说的加一条create_generated_clock约束。还有一种情况是STARTUPE3例化了多次。前面强调过一个设计里只能例化一次。如果你在多个模块里都例化了综合会报错STARTUPE3 is already instantiated。检查一下代码确保只在顶层例化一次。6.4 比特流固化后FPGA不启动这个问题比较严重通常是比特流配置和Flash实际状态不匹配。排查确认Flash的Quad使能位没有被置位。如果你之前用过Quad SPI模式Flash的Quad使能位可能是1这时候再用标准SPI配置FPGA读不到正确的数据。解决方法是通过SPI发命令0xFFReset Enable和0x66Reset或者用0x01写状态寄存器把Quad位清零。确认比特流的起始地址正确。有些Flash的0x000000地址不是从扇区0开始的或者有保护区域。查一下Flash的数据手册确认配置区域没有被写保护。确认CONFIGRATE和Flash的访问时间匹配。如果CONFIGRATE设得太高Flash来不及返回数据FPGA配置就会失败。N25Q128在标准SPI下最高支持50MHz但实际用25MHz更稳。7. 实操心得与性能优化建议7.1 关于STARTUPE3的三态控制STARTUPE3的三态控制信号USRCCLKTS、FCSBTS、DTS在配置完成后默认是什么状态这个在文档里写得不是很清楚。实测下来配置完成后这些信号默认是高阻的也就是说CCLK、CS、DO都是高阻。所以你的逻辑必须在开始SPI通信之前主动把这些三态控制拉低否则通信无法建立。我的做法是在复位释放之后先等一段时间比如1毫秒确保配置完成然后把三态控制信号拉低再开始SPI操作。这个等待时间不能省否则可能在配置还没完全结束的时候就抢占了配置引脚导致配置失败。7.2 关于SPI时钟的频率选择前面提到25MHz是个比较稳妥的选择。但实际项目中如果你的板子信号完整性做得好Flash也是高速型号可以尝试50MHz甚至更高。不过要注意SCK频率越高Flash的读访问时间tV就越关键。N25Q128在50MHz下的tV是6纳秒也就是说SCK上升沿之后6纳秒DI上的数据才有效。如果你的采样点太靠前就会采到错误的数据。我的建议是先用低速比如10MHz调通功能再逐步提高频率同时用示波器观察DI和SCK的相位关系。如果发现数据眼图很小就降低频率或者调整采样相位。7.3 关于Flash的寿命和磨损均衡NOR Flash的擦写寿命通常是10万次。如果你把Flash当参数存储用频繁擦写同一个扇区很快就会坏。所以如果要做参数存储建议加一个简单的磨损均衡算法把参数分成多个副本轮流写入不同的扇区读的时候取最新的那个。我这次项目里Flash主要用来存固件擦写频率很低所以没做磨损均衡。但如果你的应用场景是频繁记录数据这个一定要考虑。7.4 关于远程升级的可靠性远程升级最怕的是升级过程中断电导致Flash里的固件不完整FPGA再也启动不了。所以升级方案必须要有回滚机制。常见的做法是Flash里存两份固件一份是当前运行的一份是升级用的。升级时先擦除备用区域写入新固件校验通过后更新一个标志位然后触发IPROG重配置。如果新固件启动失败看门狗超时自动回滚到旧固件。这个方案需要ICAP原语和看门狗配合实现起来稍微复杂一些但可靠性高很多。我这次项目没做这么复杂但如果你要做产品级的远程升级强烈建议加上。7.5 一个容易被忽略的细节DI上的上拉电阻有些板子在DI线上没有上拉电阻Flash在CS拉高的时候DO输出高阻DI线上的电平是不确定的。如果这时候FPGA的DI输入没有上拉可能会采到随机数据导致状态机误判。我的做法是在FPGA内部给DI加一个弱上拉用PULLUP属性或者在PCB上加一个10K的上拉电阻。这个细节很小但能避免很多偶发问题。8. 常见问题速查表现象可能原因排查方法解决方法读ID返回0xFFDI/DO接反示波器看DO波形交换DI/DO连接读ID返回0xFFCS未拉低示波器看FCSBO检查FCSBTS极性读ID返回0xFFSCK无输出示波器看USRCCLKO检查USRCCLKTS和时钟源擦除不成功写使能后CS未拉高逻辑分析仪看CS写使能后强制拉高CS至少1周期写入后读回错误WIP轮询错误读状态寄存器看WIP检查DI采样相位写入后读回错误页写入跨页检查地址计算拆分写入不跨256字节边界写入后读回错误SCK频率过高降低SCK频率降到25MHz以下Vivado报DRC RTSTAT-2USRCCLKO未约束检查XDC加create_generated_clock综合报STARTUPE3重复例化多处例化搜索代码只在顶层例化一次固化后FPGA不启动Quad使能位被置位读状态寄存器发0xFF和0x66复位固化后FPGA不启动CONFIGRATE过高检查比特流设置降到25MHz偶发数据位翻转DI未同步检查代码加两级同步器偶发数据位翻转DI无上拉检查PCB加上拉电阻或内部PULLUP9. 写在最后的一些个人体会这个项目做下来最大的感受是STARTUPE3原语本身不复杂复杂的是对Flash时序的理解和对三态控制的把握。很多问题不是逻辑写错了而是对Flash的行为理解不到位。比如写使能之后必须拉高CS、擦除必须扇区对齐、页写入不能跨页这些细节在Flash的数据手册里都有但如果不仔细看很容易踩坑。另外调试工具很重要。我这次用了一台带SPI解码功能的逻辑分析仪抓SPI波形的时候直接解码出命令、地址、数据省了很多时间。如果没有逻辑分析仪用示波器看波形也能凑合但效率低很多。建议做SPI相关项目的朋友手头备一个逻辑分析仪几百块钱的就行比示波器好用。最后关于代码的复用性。我把SPI Master控制器做成了一个独立的模块接口是标准的AXI-Lite或者简单的寄存器接口这样在不同的项目里可以直接复用。Flash的命令配置也做成了参数化的换不同型号的Flash只需要改配置参数不需要改逻辑。这个思路在FPGA开发里很实用值得推广。如果你也在做类似的项目或者遇到了什么奇怪的问题欢迎一起交流。FPGA这个领域坑多但踩多了也就成了路。