ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

FPGA存储设计必读:Xilinx Block Memory Generator v8.4配置与上板调试全攻略

FPGA存储设计必读:Xilinx Block Memory Generator v8.4配置与上板调试全攻略 做FPGA的同学几乎都绕不开存储而说到存储Xilinx的Block Memory GeneratorBMGIP核绝对是最常用的那一个。不管是做简单数据缓存、查表ROM还是作为跨时钟域缓冲的中间层BMG v8.4都是Vivado里默认推荐的选择。这篇文章就把BMG v8.4从配置到上板调试的关键细节从头到尾捋一遍适合刚开始碰BRAM的新手也能给做了几年逻辑的老手提个醒——有些坑配置界面上一眼看不出来。先说清楚它能干什么。BMG这个IP核说白了就是帮你在FPGA内部生成一块或一堆块RAM/ROM你不用自己手写BRAM原语也不用担心位宽扩展、地址拼接、时序约束这些琐碎事。你只需要在图形界面里填几个参数它就会自动生成对应的RTL代码和约束甚至帮你算好占用多少块36Kb BRAM。它支持单端口RAM、简单双端口RAM、真正双端口RAM也支持ROM还额外提供ECC校验、字节写使能、复位同步这些高级功能。v8.4这个版本在Vivado 2018.x和后续版本里最为常见基本涵盖了7系列、UltraScale及UltraScale器件。这篇博文我会从实际工程的角度出发把BMG v8.4的配置流程、时序行为、初始化文件、例化代码、常见问题这几个方面全部拆开讲保证你跟着走完一遍自己能独立把这个IP核用起来。1. Block Memory Generator是干什么的为什么绕不开它1.1 所谓“块RAM”到底是个什么东西Xilinx FPGA内部的存储资源大致分三类分布式RAMDistributed RAM由LUT拼出来的存储、块RAMBlock RAM即BRAM和UltraRAM只在部分UltraScale器件上有。其中BRAM是真正意义上的硬件存储单元一个BRAM在7系列和UltraScale上是36Kb可以配置成各种宽度和深度。比如一个36Kb BRAM可以做成32Kx1、16Kx2、8Kx4、4Kx9、2Kx18、1Kx36这样的组合再通过BMG做拼接扩展就能实现更大的位宽或深度。BMG这个IP核就是用来管理这些BRAM资源的。它帮你把物理BRAM的输入输出时序、写使能逻辑、读地址变化检测、复位同步、以及两块BRAM之间的拼接全部封装好。你不需要关心具体用了多少个BRAM原语也不用关心地址线要不要错位、数据线要不要拼接这些BMG全自动处理。1.2 什么时候该用BMG什么时候不该用它很多初学者容易走进一个误区只要想用RAM就搜BMG去生成一个。但有些场景下其实有更合适的选择。如果你需要一个带空满标志、跨时钟域传输的FIFO那应该用FIFO Generator而不是BMG。FIFO Generator在实现上内部确实也用到了BRAM但它在BRAM外面封装了读写指针、空满判断、格雷码同步这些逻辑直接用BMG自己搭FIFO不仅麻烦还容易在某些极端情况出现指针同步问题。只要你的需求里有“先进先出”“跨时钟域”“空满标志”这三个关键词中的任意一个就去FIFO Generator。如果你只是想存几十个深度的数据而且数据宽度不大可以考虑用分布式RAM也就是Distributed Memory Generator。LUT的资源往往比较充裕用LUT做分布式RAM可以减少BRAM占用把宝贵的BRAM留给更需要的场景。BMG的配置界面里也能选Auto或者Block RAM如果你选了AutoVivado会根据你填的深度和位宽自动判断该用BRAM还是分布式RAM。反过来如果你的存储深度超过几百或者位宽很大或者你需要真正双端口同时读写那BMG就是最顺手的方案。它支持Native接口也支持AXI4/AXI4-Lite接口挂CPU总线或者DMA都很方便。总的来说判断依据就一条你的需求是“一块随机读写的存储区”不是FIFO也不是寄存器组。2. Vivado里把BMG配起来几个绕不开的页面2.1 Basic页先把“单双口”这事定了创建BMG IP核的入口在Vivado左侧IP Catalog里搜“Block Memory Generator”就行。双击打开配置界面首先看到的就是Basic页。Basic页里最关键的第一个选项是Interface Type默认是Native另一个选项是AXI4。如果你以后打算把这块RAM挂到AXI总线上比如接到MicroBlaze或Zynq的PL侧总线那就选AXI4。否则只是逻辑内部自己访问就保持Native。选成AXI4之后IP核外面会多出一堆AXI接口信号比如AWADDR、WDATA、BRESP这些配置复杂度会上一个台阶。接下来是Memory Type这个选项决定了RAM的端口结构一共有5个选项Single Port RAM只有一个端口读写共用一组地址线、一组数据线。适合只有一个模块需要访问存储器的情况。Simple Dual Port RAM一个端口只写另一个端口只读。读写地址和时钟可以不同适合做跨时钟域数据缓存。True Dual Port RAM两个端口都可以读也可以写端口A和端口B完全独立。适合两个模块并发读写同一块存储。Single Port ROM只读存储通常用COE文件初始化好内容。Dual Port ROM两个独立的读端口适合查表比如正弦表、系数表。我自己的习惯是如果只是需要一个缓存区而且读写两侧逻辑相对独立优先选Simple Dual Port RAM。因为Simple Dual Port的结构最简单综合和布线资源利用率最高时序也更好收敛。True Dual Port虽然更灵活但如果两侧逻辑其实不会同时写就浪费了端口资源。Basic页里还有一个ECC选项如果选成ECCP那么RAM内部会额外生成汉明码校验逻辑可以实现单比特纠错、双比特检错SEC-DED。这个功能在通信或者可靠性要求高的场景里很有用但代价是数据总线宽度要扩展校验位。普通工程用不到的话保持No ECC就行。最后是Memory Implementation有Auto、Block RAM和Distributed Memory三个选项。前面提过深度小时用Distributed更省BRAM但如果你明确知道这块RAM要跑很高频率、希望时序稳定建议直接选Block RAM。Auto模式在有些边界情况下的选择不一定符合你的预期而且你没法控制它到底用了BRAM还是LUT后期资源出问题时改起来更麻烦。2.2 Port A Options页宽度、深度和输出寄存器的讲究选定Memory Type后接下来就是Port A Options页。这里要配置端口A的写数据宽度、读数据宽度、写深度、读深度。注意这里有一个细节Xilinx BMG允许写宽度和读宽度不同。比如你可以写32位、读8位这样在内部它会通过数据宽度转换逻辑把32位的字拆成4个8位的字按地址顺序输出。这在某些需要串行化数据的场合很实用但我不建议普通用户轻易开启这个功能因为宽度转换会在时序上引入额外延迟调试起来也比较抽象。默认让写读宽度保持一致省心。勾选Enable Write Enable后端口A会多出一根WEA写使能信号。如果勾选Enable Byte Write Enable还会多出一根WEBWE或WEA字节使能信号用于按字节控制写操作。对于数据总线来说字节使能的位宽等于写入数据宽度除以8这个后面在第五节里详细讲。然后是最容易踩坑的一项Operating Mode。它有三个值WRITE_FIRST、READ_FIRST、NO_CHANGE。这个选项定义了“同一端口在同一个时钟周期内同时发生写和读操作时”的行为。如果你不做特殊用途我建议选NO_CHANGE因为它的行为最可预测写数据时输出端保持原值不变不会出现读数据与写数据交织的毛刺。WRITE_FIRST模式下写入的数据会同时出现在输出端适合想立刻观察到写入值的场景READ_FIRST模式下输出读到的是当前地址的旧数据同时写入操作也生效。选哪个取决于你的业务逻辑但在板上调试时如果发现RAM输出数据不定先查一下是不是Operating Mode选得不合适。再往下是Primitives Output Register这个选项非常关键。勾选后RAM的输出端会加一级寄存器把从BRAM单元读出来的数据再锁存一拍。它的作用是缩短组合逻辑路径提升时序性能但代价是读延迟多一个时钟周期。这里要记住一个核心结论勾选Primitives Output Register后从地址信号出现到数据稳定输出至少需要2个时钟周期某些配置下是1个或更多不勾选则只需要1个时钟周期。很多调试时数据错位的Bug根源就在这个选项上。还有一个Reset Options区域。对于RAM来说勾选Enable Reset Synchronization可以选择复位类型一般默认异步复位即可。需要特别注意如果你选的是ROM类型端口A默认没有复位复位因为读ROM数据不需要复位强行加复位反而会导致初始化内容被清掉。选RAM类型时RSTA信号复位的是输出寄存器不是RAM存储单元的内容。这一点很多人会搞混以为复位能把RAM里的数据清零实际上不会。2.3 Other Options页Byte Write、ECC这类高级选项别乱勾Port A Options页配置完成后还有Port B Options页取决于Memory Type和Other Options页。Port B的配置项和Port A基本一致不再重复。Other Options页里的内容比较复杂建议只在确定需要时才去动它。第一个是Pipeline Stages within Mux。这个选项只有在Simple Dual Port RAM且B口读、A口写时才出现值为1或2。简单理解就是在读出数据的MUX级联上插入流水线寄存器。选择2会增加一拍延迟但能让BRAM跑到更高的时钟频率。如果你发现BRAM成了时序关键路径可以试一下改成2然后再看是否还满足约束。接下来是Memory Initialization区域。这里可以指定一个COE或MIF文件作为RAM/ROM的初始内容。对于ROM这块是必填的对于RAM如果业务需要在复位后先读取一段初始值也可以填入。COE文件是文本格式里面写着十进制或十六进制的初始值后面详细展开。Other Options页底部还有一个Fill Remaining Memory Locations选项。如果COE文件只填了一部分地址勾选这个选项就能把剩余地址都填成指定的值。我建议始终勾选它并填一个确定的值通常是0否则没有初始化的RAM单元在仿真里会显示为X不定态上板后读到的内容也是随机的。虽然有些场景确实故意不初始化RAM但对于刚开始使用BMG的同学填0能减少很多仿真和调试时的干扰。3. 时序行为是最大陷阱你到底该等几个时钟周期3.1 读操作那个“Primitives Output Register”决定了你要等多久BMG的读时序是使用中最大的坑没有之一。很多人照着别人的代码例化好一个RAM上板后读数据发现错位一个周期甚至错两个周期排查半天才发现是输出寄存器配置的问题。先看不勾选Primitives Output Register的情况BRAM内部地址信号在时钟上升沿被锁存到地址寄存器然后存储单元的读数据会通过MUX直接送到输出引脚。这个过程里数据输出只经过组合逻辑理论上地址在时钟沿稳定后数据在同个时钟周期内就可以呈现。换句话说从地址有效到数据有效延迟不到一个时钟周期。但是这种输出方式有一个问题因为数据是组合逻辑直出的在多路选择器切换地址时输出数据在时钟沿附近会有一段不稳定的“毛刺”窗口。如果你的下游逻辑没有在正确的时刻采样很容易采到错误数据。所以很多经验丰富的工程师习惯在RAM输出后面自己再补一级寄存器。再看勾选Primitives Output Register的情况地址在T0时刻打入地址寄存器T0到T1之间BRAM内部把数据读出来并送到片内总线在T1时刻这个数据被锁存到输出寄存器从T1到T2数据稳定在输出引脚上。所以从地址信号在T0沿有效到数据在输出引脚上稳定实际等待了大约一个完整的时钟周期。在仿真中你会看到T0沿给入地址T1沿数据还是不定态直到T2沿之后数据才稳定有效。这就是常说的一拍延迟。更需要注意一种情况如果使用True Dual Port RAMA口和B口都勾选了输出寄存器两个端口的读数据延迟都增加了一拍。如果你的读写逻辑没有对齐这个延迟那么当你写入地址0x10后立刻去读地址0x10会读到旧数据。这类问题在调试跨时钟域处理器和DMA交互时频繁出现。我的建议是在一个工程里统一规则。比如所有BRAM都勾选Primitives Output Register然后在RTL代码里统一按“地址发出后数据在第二个时钟沿有效”来设计读数据流。或者所有BRAM都不勾选靠外部逻辑自行打拍。最怕的就是不同模块里有的勾选有的不勾选这样一旦做数据对齐整个工程的延迟计算就像一团乱麻。3.2 写操作READ_FIRST / WRITE_FIRST / NO_CHANGE 各自有什么用读时序说完写时序这里同样有讲究关键就是前面提过的Operating Mode。给个具体场景方便理解。假设你写了一块Simple Dual Port RAMA口写、B口读。那么A口的Operating Mode对写操作几乎没有影响真正影响的是B口的Read Function。但在Single Port RAM和True Dual Port RAM中同一个端口上读写同时发生时Operating Mode会决定输出端的行为。WRITE_FIRST写数据会被直接送到输出端相当于“写优先”。假如你在某个时钟沿向某个地址写入数据同时读这个地址读到的值就是刚写入的新值。这个模式适合做寄存器更新后立刻校验的场景。READ_FIRST读写同时发生时输出端给出的是存储单元中原来保存的数据然后写操作在内部生效。简单说就是“先读旧值再写新值”。这个模式适合做状态保存不希望读操作被写操作打断。NO_CHANGE写操作进行期间输出端完全保持原值不变。这个模式行为最干净芯片内部也不需要额外考虑“先读后写”或“先写后读”的数据旁路逻辑时序上压力最小。我个人的工程经验是如果只是普通数据缓存选NO_CHANGE如果要在写入后紧接着观察写入内容选WRITE_FIRST如果要做读改写之类的原子操作选READ_FIRST。但是在你没有充分理解某个模式对时序的影响前不要频繁切换这个选项它在不同端口组合下对读延迟的影响很微妙最好先在仿真中确认。还有一个细节在Simple Dual Port RAM下一个端口写、另一个端口读如果读地址和写地址指向同一个地址那么读出来的数据取决于写操作的相对时序。这种情况算“端口冲突”不属于前三种操作模式的定义范围。Xilinx在用户手册UG473里给出的行为是“不保证”建议在硬件设计上避免这种同时读写同一地址的场景或者通过外部握手逻辑保证读写操作不在同一时刻发生。3.3 握手和使能信号ENA、WEA、RSTA这些信号怎么控制BMG产生的原生接口信号不算多但每个信号的时序都要注意。先列一下最常见的几个CLKA端口A时钟。所有端口A的信号都在这个时钟的上升沿采样。ENA端口A使能。为高时端口A的读写操作正常进行为低时端口A上的读数据输出保持当前值不变写操作也被忽略。WEA端口A写使能。对于Native接口WEA的位宽可能是1位也可能是按字节拆分的多bit。位宽为1时高电平表示写位宽为多bit时每一位对应一个字节。ADDRA端口A地址。宽度根据Memory Depth自动生成。DINA端口A写数据。DOUTA端口A读数据。RSTA端口A输出寄存器复位信号。复位的是输出寄存器不是RAM存储阵列。ENA这个信号经常被忽略。很多人都以为只要CLKA正常RAM就能正常读写但实际上如果ENA没有拉高读写操作不会执行。如果你忘记连ENA把它悬空了Vivado在综合时可能会给你接个高电平也可能保持低电平具体情况看IP配置里有没有勾选“Enable”。更稳妥的做法是在例化时显式拉高ENA除非你确实需要门控时钟或门控操作。RSTA也有个常见误解。前面强调过RSTA复位的是输出寄存器不会清空RAM里的数据。如果你的需求是“上电后RAM内容全为0”那必须用初始化文件填0而不是靠复位。这个坑我见过好几个人踩仿真器里RAM内容显示为X他们以为是复位没接好实际上复位根本管不到存储单元的初始化。4. 用COE文件把ROM“填满”初始化你的存储内容4.1 COE文件到底长什么样BMG支持用COE文件定义RAM/ROM的初始值。COE文件本质上是一个文本文件后缀是.coe语法很简单。举个例子一个16深、8位宽的ROM初始内容用十六进制表示长这样memory_initialization_radix16; memory_initialization_vector A5, 5A, 00, FF, 11, 22, 33, 44, 55, 66, 77, 88, 99, AA, BB, CC;第一行memory_initialization_radix指定了数值进制常见的有2、10、16。第二行memory_initialization_vector后面跟着一列数据每个数据之间用逗号分隔最后一行以分号结束。数据的个数必须等于你配置的Memory Depth如果数据个数少了剩下地址的内容会保持未初始化状态仿真为X数据个数多了多余的数据会被忽略。特别注意如果数据个数少于Memory Depth而且你在Other Options页勾选了Fill Remaining Memory Locations并填入0那么剩余地址会被初始化为0。如果没有勾选那剩余地址就是未定义状态。这也是我之前说建议始终勾选Fill Remaining的原因——它能在仿真和上板上消除很多随机不定态的干扰。4.2 用Matlab生成COE文件的实战写法通常ROM里的内容不是手写的而是由Matlab、Python或Vivado里的Tcl脚本自动生成。我经常用Matlab生成FIR滤波器系数表、正弦波查找表这类数据写起来就是一小段循环代码depth 1024; width 16; t linspace(0, 2*pi, depth); sine_wave round((sin(t) 1) * (2^(width-1) - 1)); sine_wave max(0, min(2^width - 1, sine_wave)); fid fopen(sine_lut.coe, w); fprintf(fid, memory_initialization_radix16;\n); fprintf(fid, memory_initialization_vector\n); for i 1:depth-1 fprintf(fid, %04X,\n, sine_wave(i)); end fprintf(fid, %04X;\n, sine_wave(depth)); fclose(fid);生成文件后在BMG配置界面的Other Options里选中Memory Initialization然后浏览到COE文件路径即可。记得点击Reload确保Vivado重新读取文件内容有时候你改了COE文件但没Reload综合出来的ROM还是旧内容这个问题我在工程里遇到过好几次。4.3 初始化不生效的常见原因如果你明明在配置界面里指定了COE文件仿真或上板时却发现ROM内容是空的大概率是下面这几个问题。第一种是综合时没有把COE文件的作用范围正确传递。对于一般工程COE文件路径写在IP配置里就够了Vivado会自动处理。但如果你的IP核是通过Tcl脚本生成的或者在多个工程之间复制IP核有时路径会失效。解决办法是在BMG配置界面里重新指定一次COE文件路径确保路径是绝对路径或相对工程根目录的正确路径。第二种是COE文件的格式和配置的位宽深度不匹配。比如IP配置是32位宽而COE里的数据是16位或8位数Vivado在加载时不会报错而是会按照十六进制逐位截断或扩展最终得到的ROM内容和你想的完全不一样。发生这种情况时建议先检查memory_initialization_radix是否和编写时的进制一致再检查每个数据对应的位宽是否正好等于你配置的Write Width或Read Width。第三种是使用ROM但同时勾选了输出寄存器和复位寄存器导致仿真时数据输出有额外延迟看起来像是“初始化没有生效”实际只是延迟问题。这种情况不是初始化失效而是时序对齐的问题把时序对齐后再看ROM内容你会发现数据是正确存在的。5. 实操记录一个双端口RAM从配置到上板跑通的完整流程5.1 从配置界面一步步走下来为了给你一个完整的参照我这里用一个实际例子走一遍流程。工程环境是Vivado 2019.2器件选xc7z010clg400-1需求是做一个1Kx32的Simple Dual Port RAMA口负责写入B口负责读取用于两个不同时钟域模块之间的数据缓存。第一步打开IP Catalog搜索Block Memory Generator双击进入配置界面。Component Name里填一个有意义的名字比如ram_1kx32_dp。注意Component Name不能以数字开头也不能包含中文。第二步Basic页里Interface Type选NativeMemory Type选Simple Dual Port RAMMemory Implementation选Block RAMECC选No ECC。第三步Port A Options页里写数据宽度填32写深度填1024。勾选Enable Write Enable这样WEA信号是1bit再勾选Enable Byte Write Enable这样WEA会变成4bit每一位控制1字节。Operating Mode选NO_CHANGE读写宽度保持一致都是32。Primitives Output Register先不勾选因为这里我需要尽可能快速的响应。第四步Port B Options页里读数据宽度填32读深度填1024。勾选Enable Output Register让读数据经过输出寄存器这样B口的读时序更干净。Operating Mode选READ_FIRST。因为A口是纯写、B口是纯读Port A的Operating Mode其实影响不大但为了避免歧义我还是把两边都设置成与读写角色相匹配的模式。第五步Other Options页里Memory Initialization留空因为这是RAM缓存不需要预置内容。勾选Fill Remaining Memory Locations填充值设为0。第六步点击OK然后在弹出的Generate Output Products选择Generate等待IP核生成完成。至于Instantiation Template在Sources窗口里找到ram_1kx32_dp.veo文件位于IP核目录下的Instantiation Template子目录里面就是示例例化代码。5.2 例化代码和接口连接中的注意事项生成的例化模板可以直接复制到你的顶层模块里。我用的是Verilog典型例化长这样ram_1kx32_dp u_ram_1kx32_dp ( .clka (wr_clk), // 输入时钟端口A .wea (ram_wea), // 输入4bit字节写使能 .addra (wr_addr), // 输入10bit写地址 .dina (wr_data), // 输入32bit写数据 .clkb (rd_clk), // 输入时钟端口B .addrb (rd_addr), // 输入10bit读地址 .doutb (rd_data) // 输出32bit读数据 );这里要特别注意几个细节。wea的位宽在勾选了Byte Write Enable后是4bit每一位对应dina的一个字节具体对应关系是wea[0]控制dina[7:0]wea[1]控制dina[15:8]以此类推。如果你习惯用整个32位写就把wea全部拉高也就是4hF。很多人在只写低字节的时候只拉高wea[0]结果发现dina[31:16]这些高位其实没写进去这不是IP核出错是字节使能的正常行为。addra和addrb的位宽是10bit因为1024个地址需要10根地址线。如果你在地址线上给了超出范围的宽总线最高位会被忽略不会报错。这点在做地址位宽裁剪时尤其注意。doutb在B口勾选了Enable Output Register的情况下从addrb给出地址到doutb稳定输出中间有1个时钟周期的延迟。如果后续逻辑的对齐逻辑是按同一个时钟周期来采数的那必须把读数据整体打一拍或者把写地址同步到读时钟域后再比较否则会差一个周期。另外接口上的时钟域问题必须强调。A口和B口的时钟可以不同但这两个时钟之间如果频率和相位没有特定约束就称为异步时钟异步时钟之间传输数据是有亚稳态风险的。BMG本身不会帮你做跨时钟域同步它只能保证BRAM存储单元在两个端口独立访问时不冲突但地址和数据在跨越时钟边界时必须由外部逻辑做好同步。最稳妥的做法是如果读写时钟频率有差异或相位不固定不要用普通BMG做缓存应该用异步FIFO。5.3 调试时怎么用ILA确认读写时序工程跑在板上后如果发现读出数据和预期不符合首选手段是用ILAIntegrated Logic Analyzer抓内部信号。在Vivado里在需要观察的信号上加上MARK_DEBUG属性综合后打开硬件管理器把信号加入波形窗口就能实时观察。以这个双端口RAM为例我会对wr_addr、wr_data、wr_wea、rd_addr、rd_data这五个信号加上MARK_DEBUG然后在System ILA核里设置触发条件。具体做法是在XDC或RTL里写(* mark_debug true *) wire [9:0] rd_addr_debug; (* mark_debug true *) wire [31:0] rd_data_debug; assign rd_addr_debug rd_addr; assign rd_data_debug rd_data;综合后布局布线生成比特流下载然后在Hardware Manager里找到ILA设置addrb等于某个测试地址再设置单次触发就能看到那一时刻rd_data是否等于之前写入的值。这里有一个排查经验如果抓到rd_data的值总比预期晚一个周期先回去看Port B的Enable Output Register有没有勾选。如果勾选了读数据延迟一拍是正常现象代码里把采样沿改到下一个周期即可。如果没勾选但数据还是晚了一拍那可能是ILA采样时钟和RAM时钟的相位关系问题这时要检查ILA核的采样时钟是否与rd_clk同步。6. 常见问题与排查技巧实录6.1 数据对不齐小心“延迟”这个隐形变量在调试BRAM时最高频的问题就是数据对不齐。写端明明写入了地址0x10的数据读端读出来却对不上。原因几乎全部指向“读延迟”和“地址比较寄存器延迟”。先看一个典型场景。假设你有一个Simple Dual Port RAM写端口在wr_clk域写地址wr_addr和写数据wr_data由上游逻辑在同一拍产生。读端口在rd_clk域读地址rd_addr由读控制逻辑产生。你在读控制逻辑里做了一个计数器每次rd_addr递增同时希望doutb在下一个周期就出现某块数据。由于读端口有输出寄存器doutb实际是在rd_addr变化后的第二个时钟沿才稳定。所以如果你在rd_addr变化后的第一个时钟沿就去采样doutb采到的还是旧地址的数据或不定态。解决思路有两条。一条是直接调整逻辑把所有下游采样逻辑的时序基准往后移一拍。另一条是给rd_addr的参考值也打两拍让比较基准和数据对齐。实际工程里我会在写RTL时先做一个表格把“地址发出到数据有效”需要的时钟周期数写清楚然后再开始写状态机不把延迟问题留到仿真以后。6.2 Byte Write使能的位宽和数据总线的对应关系Byte Write Enable是个很容易搞混的配置项。简单说它的位宽等于写数据位宽除以8。如果数据位宽是32bit那Byte Write Enable就是4bit如果是64bit就是8bit如果数据位宽不是8的整数倍比如你配了37bit那么Byte Write Enable位宽是5bit其中最后一bit控制的只是一个宽5bit的字节。使用时的核心问题是使能位与数据字节的对应是固定的从低位开始。比如32bit数据最低字节是dina[7:0]那对应的写使能是wea[0]。高字节依次对应。如果你的外部总线和这个高低字节序不一致就会发生“写进去的数据错位”的现象。尤其在连接一些外部芯片时如果对方的数据总线字节序定义不同需要在转换逻辑里先把字节swap掉再来控制BMG的写使能。6.3 Vivado版本和器件选型的影响最后简单提一下版本问题。BMG v8.4在不同Vivado版本里界面风格基本一致但在UltraScale器件上会多出一些特性比如可配置的UltraRAM支持。如果你在工程中同时用到了7系列和UltraScale建议每个器件单独生成一份IP核配置不要用一套IP核配置直接跨器件复用因为某些参数在两个系列上的底层实现方式不同跨器件复用轻则约束报错重则综合结果不符合预期。还有一个常见问题在Vivado工程中右击IP核选Upgrade IP后COE路径偶尔会失效。升级后一定要重新检查一遍Other Options页的初始化文件是否还在。如果IP核从v8.3升级到v8.4基本不影响用户侧的逻辑但如果升级后综合报出端口数量变化或者端口名称变化先查一下是不是升级时把某些配置项重置了。在实际工程中BRAM相关的很多问题都不是IP核本身的Bug而是配置和时序没对齐。BMG v8.4作为一个成熟IP核稳定性是没问题的你把上述这些配置选项、延迟关系、初始化机制理清楚后它就是你手里最可靠的一块存储积木。
返回列表