尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

FPGA BRAM三种读写模式详解:Write First、Read First与No Change

发布时间:2026/9/29 15:59:41

资讯中心
01
ARTICLE

FPGA BRAM三种读写模式详解:Write First、Read First与No Change

FPGA BRAM三种读写模式详解:Write First、Read First与No Change
做FPGA的人十有八九都被BRAM的读写时序坑过。我印象最深的一次是在调一个图像行缓存模块仿真波形明明看着数据写进去了可下游模块就是读到错误值。排查了整整两天最后发现根因不在逻辑代码而是IP核里默认的读写模式设置跟我预想的不一样。从那时候起我意识到BRAM的三种读写模式——Write First、Read First、No Change——不是配置界面上一行不起眼的下拉选项而是直接决定数据在某个时钟沿到底能不能被正确读出的核心机制。这篇就把三种模式彻底讲透配合时序分析帮你在Vivado里少走弯路。1. 三种读写模式的底层逻辑为什么BRAM要分写优先、读优先、不变很多初学者第一次打开Vivado的Block Memory Generator IP核配置界面时看到Write Mode下面有三个选项心里其实没什么概念随手选一个默认值就往下走了。等仿真波形出来发现读数据跟自己想的不一样才开始回头研究这个选项。这种事我见得太多了甚至包括一些工作两年以上的工程师对这三种模式的认知也只停留在知道有这回事的层面。1.1 模式选择在现代FPGA工程中的真实影响先给结论BRAM的读写模式决定了当同一个时钟周期内对同一地址同时发生读和写操作时输出端口的数据到底是什么。在FPGA设计中BRAM作为最常用的存储资源几乎每个工程都离不开它——FIFO、帧缓存、查找表、系数存储、配置寄存器组、乒乓缓存全是BRAM在扛。而工程规模越大读写冲突的概率就越高模式选择的影响也就越明显。有一个非常实际的场景图像处理里的行缓存。行数据从摄像头接口进来写到BRAM的地址A同时上一个时钟周期读地址A的数据送给后续的滤波模块。如果你的模式选错了读出来的可能是刚写入的新数据也可能是写入前的旧数据还可能是完全不确定的值。这个错误不会每次都触发一旦触发图像上就是一行花屏或者杂点而且复现条件苛刻很难定位。1.2 BRAM的物理结构决定了原子操作的含义理解三种模式之前得先知道BRAM里面到底长什么样。Xilinx 7系列之后的FPGABRAM的基本单元是36Kb大小的块可以拆成两个18Kb独立使用。每个块有独立的地址寄存器、写数据寄存器和读数据寄存器写操作和读操作都发生在时钟上升沿本质上是同一个时钟沿同时采样地址、数据和控制信号。关键点在于当写使能WE有效而且写地址等于读地址时硬件上必须决定一个优先级——是先让写入的数据覆盖存储单元还是先把存储单元里原来的数据读出来。这个决定就是写模式。之所以有三种模式是因为不同的场景对读出来的应该是什么有不同要求而硬件只能选择其中一种行为固化下来。注意这里的同时发生是站在时钟沿的角度说的不是软件思维里的先后顺序这是很多从软件转FPGA的人最容易卡住的地方。1.3 三种模式在数据手册上的标准定义从Xilinx官方文档UC162和UG473里的定义来看三种模式的行为可以这样概括模式同一地址读写冲突时输出行为硬件实现思路Write First写优先输出端变为新写入的数据写数据旁路到输出存储单元更新为新值Read First读优先输出端保持写入前的旧数据先读存储单元再写入输出是旧值No Change不变输出端保持不变不更新读写操作被隔离开输出锁存不响应刷新后面几个章节会逐个拆开讲重点放在时序行为、硬件实现、适用场景和坑上。这部分的深度会直接决定你后面调试 BRAM 相关问题的效率。2. 写优先Write First模式的使用边界与深层行为2.1 时序特征与硬件实现Write First模式从名字就知道写操作拥有最高优先级。当WE有效且读写地址相同时在时钟上升沿到来后数据总线DIN上的值会被写入存储单元同时直接出现在输出总线DOUT上。也就是说这个周期你写的是什么DOUT在这个周期就能读到什么不需要等下一个时钟周期。这个行为在硬件上是怎么做到的实际上BRAM内部不是简单地把存储单元的输出通过读地址选择器送到输出端而是额外做了一条写数据旁路通路。写数据DIN在进入存储单元阵列的同时还会送进一个旁路多路选择器MUX当检测到写操作且地址匹配时MUX会把DIN直接引导到输出寄存器前端。这样输出端拿到的就是热乎的新数据用不着等存储单元的写入延迟稳定下来。Xilinx BRAM的输出端通常还有一个可选的输出寄存器DOUTB_REG或DOUT_REG如果使能了这个寄存器写优先模式下DOUT也会因为寄存器的流水延迟而晚一个周期出现。换句话说写优先并不是无条件立即输出新值而是在输出端没有额外流水寄存器的前提下当前周期输出新值。2.2 最容易踩的坑新数据抢占旧数据这个模式最大的坑在于你以为自己在读一个稳定存储的数据结果数据在同一个时钟周期被写操作悄悄替换了。我见过一个典型的错误用法——用BRAM做双缓冲的帧指针切换。代码逻辑是这样if (frame_start) begin mem[rd_addr] rd_data; // 读之前先更新当前帧指针 curr_frame mem[rd_addr]; // 期望读到更新前的旧指针 end这种写法默认了BRAM是Read First行为也就是先读旧值再写新值。但如果IP核配置的是Write First第二行读到的就是刚写进去的新帧指针整个双缓冲机制形同虚设下游模块直接拿到还没准备好数据的地址去取数花屏、错位、撕裂全来了。所以这里要记住一条铁律如果你的代码逻辑依赖先读旧值再写新值必须明确选择Read First模式反过来如果依赖写后立即读出新值必须选Write First。不能靠仿真碰运气仿真有时候因为testbench写得不准根本暴露不了这种问题。2.3 适用的场景推荐Write First模式最典型的应用场景是寄存器直通逻辑。举个例子你在做AXI-Lite寄存器组时软件通过总线写了一个控制寄存器紧接着硬件逻辑需要立刻使用这个寄存器的新值来切换多路选择器。如果走Read First你必须等一个周期才能拿到新值时序上多一拍在一些时序紧张的设计里这一拍可能就会让建立时间违例。此时Write First模式就可以让写入和读取新值发生在同一个周期省掉一拍。另一个适合的场景是状态机里做配置覆盖。某些配置项允许软件在后台上报的同时强行修改你希望修改立刻生效——比如动态切换滤波器的系数。Write First让新系数在写入周期的输出端就可见状态机下个状态直接就能用逻辑清晰且时序紧凑。不过Write First模式也有代价。因为写数据和输出之间存在旁路这个旁路会引入组合逻辑延迟在某些高频率设计里BRAM的最大工作频率可能会因为这个旁路而略微下降。如果你的设计跑在600MHz以上的超高速接口上使用Write First时需要特别关注时序报告不要默认它和No Change模式跑一样快。3. 读优先Read First模式解决数据搬移中的旧值依赖3.1 时序特征与硬件实现Read First模式——也常被称为读前写后或者先读后写——的行为是当WE有效且读写地址相同时时钟上升沿先触发读操作把存储单元里现有的旧数据送到输出端然后再执行写操作把新数据覆盖进去。所以这个周期DOUT上读到的是写入前的旧数据新数据要等到下一个读周期才能被读到。硬件上BRAM内部在读数据路径上有专门的锁存逻辑。时钟沿到来后读地址选择器先把存储单元里地址对应的数据捕获到读数据锁存器里与此同时写操作启动写入的数据通过写入驱动电路进入存储单元阵列。因为读数据锁存器的捕获发生在写入完成之前所以锁存器里保留的是旧值。这个过程在物理上是非常短暂的但在逻辑上保证了读出来的数据一定是写入之前的状态。Vivado的BRAM IP核文档里明确提到Read First模式下写入后的新数据不会出现在当前周期的输出端而是要在写入完成之后的下一次读操作才能读到。这个延迟特性在处理数据搬移类算法时非常值钱。3.2 读优先模式中的数据新旧之争Read First模式容易让人困惑的地方在于这个模式读出来的是旧数据但很多工程师误以为Read First就等于先读后写那写操作被延迟了新数据要更晚才能写入。实际上不是。写入仍然是当前周期完成的只是输出端展示的是写入前的快照。存储单元的更新并不会被延迟延迟的只是读数据路径的显示。举个例子你用BRAM做矩阵转置把数据按行写入按列读出。转置过程中行末元素和列首元素在地址空间上往往存在重叠。如果使用Read First模式当读地址和写地址碰撞时读出的数据是写入前的旧值这样你就不会用刚写入的行数据污染正在读的列数据。这个特性特别适合数据流型的算法模块因为数据在流式处理中对旧值和新值的边界极其敏感。还有一点需要注意Read First模式下仿真时可以通过后门backdoor访问或者层次化引用来观察BRAM内部存储值来确认写入是否真的生效。Vivado仿真库里BRAM的行为模型是精确模拟这个模式的所以仿真中看到输出还是旧值不代表写失败只是让你看到读优先的时序罢了。这个误解非常常见尤其在查看波形时很多人看到写入后输出没变第一反应是代码bug其实不是。3.3 适用的场景推荐Read First模式最常见的用途是数据搬移和缓冲管理。比如你维护一个环形缓冲区写指针和读指针在极端情况下可能相等缓冲区将满或刚空此时如果恰好同一周期对同一个地址做读写你希望读操作拿到的是缓冲区里原来的数据而不是刚覆盖的新数据否则就会把还没处理完的数据覆盖掉。Read First模式天然保证这一点。另一个场景是查找表LUT的在线更新。比如你在做图像Gamma校正Gamma表是存放在BRAM里的查找表。系统运行中允许上位机更新Gamma曲线更新时写入新系数同时正在显示的这一帧还要继续查表。如果发生地址碰撞Read First保证当前帧仍然用旧Gamma值完成这帧显示新Gamma从下一帧开始生效。这就是一个平滑切换的需求Read First完美契合。在双口BRAM中如果在配置时一个端口设为读优先另一个端口设为写优先可以实现一些很有意思的同步逻辑。比如A口读旧值保证数据不丢B口写新值保证数据更新两边各取所需。这种情况下模式选择就不是简单的选哪个好而是每个端口谁承载什么职责的架构问题了。4. 不变No Change模式被多数人低估的功耗与稳定性设计4.1 时序特征与硬件实现No Change模式从行为上看最简单当读写冲突发生时DOUT输出端保持之前的值既不更新为新写入的数据也不显示存储单元的旧数据。输出端就像被冻住了一样直到后续某个时钟周期读地址变化且没有写冲突时DOUT才会输出新的读数据。硬件实现上No Change模式实际上把读数据输出寄存器的使能信号控制起来了。当检测到同一周期读写同一地址时硬件会关闭输出寄存器的更新使能让寄存器保持原值。这样做的直接好处是DOUT端口的开关活动被抑制了不会因为数据切换产生多余的反转功耗。在BRAM功耗中输出数据总线翻转消耗的功耗占相当比例No Change模式能让这部分功耗降为接近零。4.2 输出锁存的价值不止是省电很多人以为No Change只是为了省电其实它的价值远不止如此。在数据路径上如果DOUT在某个时刻跳变可能会触发下游组合逻辑的毛刺传递。尤其当BRAM输出直接连到异步FIFO的写数据端口或者跨时钟域逻辑的输入端时输出端不必要的跳变会造成亚稳态风险上升、毛刺滤波困难等连锁问题。No Change模式让输出在冲突周期保持稳定相当于给下游逻辑制造了一个安静窗口。还有一个很实际的好处方便调试。当你用ILA集成逻辑分析仪抓内部信号时如果BRAM输出端不停翻转波形很难看清具体在哪一拍数据有效。No Change模式下冲突周期输出稳定信号变化点很干净抓波形时观察点的逻辑判断会容易很多。这对硬件调试来说是真真切切的便利。4.3 适用的场景推荐No Change模式最典型的应用场景是配置寄存器类的BRAM。比如存储设备配置参数的寄存器组软件通过总线一次性写入所有参数硬件逻辑在特定时刻比如帧同步信号到来时才并行读取这些参数。在参数写入周期你完全不需要输出端跟着更新反而希望输出保持稳定避免在参数还没写全的中间状态被下游采到错误配置。No Change模式让输出端在整个写入窗口保持稳定下游逻辑只会在你允许的时刻采样大大增强了配置时序的安全性。另一个场景是乒乓缓存控制。乒乓结构里一个BRAM在被写端填充时另一个BRAM在被读端消耗。如果你在BRAM的写端口上配置No Change模式那么在写入过程中输出端不会乱跳当控制逻辑切换读端时读到的数据是完整的、稳定的数据包不会出现半个包的数据被读走的情况。频率方面No Change模式因为没有写数据旁路的组合逻辑通常可以跑出比Write First更高的时钟频率。在做高速接口的存储桥接时如果不需要旁路特性选No Change往往能更容易满足时序收敛。5. 时序图对比手把手教你读懂读出的究竟是新数据还是旧数据5.1 三种模式同地址读写时的波形差异这一节是重点。我们用同一个场景对比三种模式的波形行为。假设BRAM的时钟是CLK地址总线上从T0周期开始出现地址A写使能WE在T0周期拉高写数据DIN在T0周期是D_new存储单元中地址A原来的值是D_old。在T0时钟上升沿到来前所有输入信号已经稳定。T0上升沿是一个关键动作点三种模式在这一点之后的DOUT表现完全不同Write First模式T0上升沿后DOUT立刻更新为D_new。也就是当前周期你能看到新数据。DOUT变到D_new的时间点相对于时钟上升沿有一个极短的时钟到输出延迟Tco但不需要等下一个时钟周期。Read First模式T0上升沿后DOUT输出的是D_old。这个D_old是T0上升沿从存储单元捕获到的旧值。等到T1上升沿如果T1周期没有新的写操作或者地址不同DOUT才会输出D_new。No Change模式T0上升沿后DOUT保持它之前的值这个值可能是更早某个周期读到的数据跟D_old和D_new都没有关系。当后续某个周期读地址变化且没有写冲突时DOUT才会更新。如果用一个时钟周期为单位的表格来表达会非常清晰周期输入操作Write First DOUTRead First DOUTNo Change DOUTT0写地址ADIND_newD_newD_old保持不变T1无写或写其他地址D_old或D_new取决于T1读地址D_new若T1读地址为A可能保持不变或输出对应地址数据注意T1行的理解Read First模式下D_new在T1周期才重见天日因为T1周期如果读地址仍然为A存储单元里已经是D_new了所以DOUT输出D_new。Write First模式下T1周期如果读地址切到其他地址DOUT则是那个地址的数据。5.2 按操作顺序拆分写后读与读后写的模式表现实际工程里读写冲突不一定发生在同一个时钟周期。更常见的是上一周期写A这一周期读A的顺序操作。这种情况下三种模式的表现是一致的都能读到新值因为写入在上一周期已经完成。真正的差异只发生在同一周期同地址读写的碰撞窗口。为了在仿真里观察这个碰撞窗口testbench需要刻意构造写使能和读使能在同一周期有效且地址相同的激励。有些工程师写testbench时写使能拉高一个周期之后再拉高读使能这实际上是背靠背写读而不是同周期读写碰撞根本测不出模式的差异。如果你想要验证BRAM模式配置是否符合预期一定要让写使能和读使能在同一个时钟沿之前同时有效并且读地址总线和写地址总线接同一个地址源。我自己的验证习惯是在testbench里设置一个计数器每8个周期产生一次同时读写操作连续运行几千个周期对比三种模式下的DOUT输出写一个自动比对逻辑来检查行为是否符合预期。这样能把模式差异在仿真阶段就牢牢锁死不用等到上板抓波形。5.3 Vivado仿真与综合后的行为差异用Vivado仿真BRAM时行为级模型behavioral model会精确模拟三种模式。但有一个细节如果BRAM的输出寄存器被例化输出加了一级寄存器那么无论哪种模式DOUT都会比前面描述晚一个周期出现。这个输出寄存器使能选项在Block Memory Generator里对应Output Register Options默认是不使能的但很多模板代码会把它加上以改善时序。一旦加了模式差异还是会体现在内部存储节点上但外部看到的DOUT会统一多流水一拍。综合之后如果BRAM被推断为分布式RAMDRAM基于LUT而不是BRAM行为可能又有细微差别。比如分布式RAM天然以读优先为默认行为写优先模式需要额外引入旁路LUT这会导致资源占用上升。所以如果代码里写了综合属性ram_style block但没有正确指定可能实际综合出来的不是BRAM而是DRAM模式行为跟着变化排查起来非常隐蔽。建议在综合后的原理图里确认BRAM原语类型以及在配置界面核对模式选项。另外提醒一句Vivado的IP核配置界面里Write Mode下拉选中的值会写入生成的例化模板中以原语属性或IP核参数形式存在。如果你后续手动修改了生成文件或者在不同版本之间迁移工程记得重新检查这个设置版本升级后默认行为有变化的情况我也遇到过。6. 工程选型建议与排查经验6.1 单口/真双口/简单双口下的模式选择BRAM按端口类型可以分为单口Single Port、简单双口Simple Dual Port和真双口True Dual Port。这三种结构下读写模式的选择逻辑是不同的。单口BRAM只有一个时钟域下的一个端口读写共用地址总线本身就难以在同一周期同时进行读和写。所以严格来说单口BRAM通常在功能上也不存在同地址同时读写的问题模式的影响更多体现在读地址等于写地址且WE有效时DOUT的表现上。对于单口我一般直接选No Change让输出最稳定。真双口BRAM两个端口各自有独立的时钟、地址和读写使能。这种结构下两个端口对同一地址同时操作是完全可能的模式选择必须分别针对两个端口独立设置。两个端口可以一个选Read First一个选No Change完全按逻辑需求来。这里要特别注意如果两个端口一个写、一个读且频率不同那么即使两边模式都设对了跨时钟域的读写碰撞也无法完全避免。Xilinx手册里对这种情况的建议是如果两个端口时钟不同且存在同一地址的读写在非常接近的窗口内发生BRAM内部仲裁会采用一种写后读或读后写的物理实现但行为无法保证和配置的模式完全一致。所以跨时钟域双口BRAM本质上还是要靠外部握手机制来避免真正的碰撞。简单双口BRAM一个端口只负责写另一个端口只负责读。这种结构下两个端口对同一地址的同时读写操作行为通常由写端口优先或读端口优先来决定Vivado提供的模式选项在这里有效。做异步FIFO时内部就是简单双口BRAM写时钟和读时钟不同此时一般建议把写端口设为Write First或No Change看具体需求。6.2 FPGA与ASIC的差异提醒不要过度依赖仿真行为如果你有ASIC背景或者未来打算把FPGA验证过的逻辑移植到ASIC需要知道一个差异ASIC SRAM编译器提供的写模式行为和FPGA BRAM并不完全一致。ASIC SRAM通常只保证写后读出新数据或者输出保持这类行为但读优先输出旧值在很多标准单元SRAM里很难实现或者代价很高。FPGA里的Read First模式实现相对容易因为读写数据通路本来就是独立构建的。所以在FPGA上调试好的Read First依赖逻辑在移植到ASIC时需要额外确认SRAM行为或者改造成先寄存旧值下一拍再写入的纯数字逻辑等价结构。很多芯片回来之后出现偶发数据错误追根溯源都是这种FPGA能跑ASIC跑不了的行为差异。提前知道这个问题能省掉后面大把的联调时间。6.3 异步读写时钟下的模式修正异步时钟域的场景不能把模式选择当作解决一切的挡箭牌。两个时钟之间存在相位漂移写操作和读操作的碰撞是概率性的不会每次都发生精准的同周期冲突。正因为这种概率性问题最难复现也最容易在量产设备上随机爆发。我处理异步双口BRAM时的经验是不要指望模式选择来保证正确性而是用握手、FIFO指针、格雷码同步这些结构从架构层面消灭冲突窗口。在确保不会发生碰撞的前提下No Change模式往往是最好的选择因为它功耗最低输出最稳定。如果你确实无法完全消除碰撞窗口那就必须接受一个事实读到的数据在冲突时刻是不确定的能不能接受取决于你的业务逻辑。6.4 排查BRAM数据异常的实用链路最后分享一套我排查BRAM相关数据问题的实操流程。遇到读数据不对不要急着翻代码逻辑先按这个顺序排查打开IP核配置界面确认Write Mode选的是什么。八成问题出在这里。检查综合后的原理图或ELABORATED设计视图确认BRAM原语映射的是RAMB36/RAMB18还是分布式RAM。如果是分布式RAM查看其写模式行为和你预期是否一致。查看输出寄存器选项。如果使能了输出寄存器观察外部数据会比内部存储数据晚一个周期这是正常现象不要误判。在testbench里构造真实的同周期读写碰撞激励仿真观察DOUT行为是否符合该模式的预期。如果不符合检查IP核版本和仿真模型。如果仿真对但上板错用ILA抓BRAM的输入和输出信号重点看WE、地址和DIN的时间关系确认是不是因为时序收敛问题导致实际采样点偏移了。这套流程帮我定位过不少隐蔽问题其中一大半都是模式选错加输出寄存器打开两个因素叠加在一起造成的数据错拍。另一个容易忽略的点是复位。BRAM的输出寄存器有单独的复位信号RST如果你使用的复位不是同步复位而设计里又对BRAM输出寄存器做了异步复位操作复位释放时可能和时钟沿产生竞争导致输出端出现不确定值。这一点在Xilinx FPGA上尤其需要注意建议BRAM输出端使用同步复位或者在复位释放时保证时钟稳定。这个问题在热词列表里有fpga复位信号亚稳态的搜索说明很多人都在这里卡过。如果你正准备在Vivado里配置BRAM我的建议是新建IP核时把三种模式都仿真一遍花一个小时把这个基础功练扎实后面做任何存储类模块都能省下数倍的时间。我在实际项目中会把这套BRAM模式验证做成一个通用testbench模板每换一个FPGA型号或者Vivado版本就重新跑一遍确保行为没有变化。这步看起来不起眼但能避免很多莫名其妙的灵异问题。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。