ARTICLE DETAIL

资讯详情

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

ZYNQ PS读写BRAM的5个关键配置细节与排查方法

ZYNQ PS读写BRAM的5个关键配置细节与排查方法 1. 先把PS读写BRAM这条路走通通路拆解与问题分类前阵子帮一个刚接触ZYNQ的朋友排查问题他在Vivado 2022.1里搭了个最简单的Block DesignPS通过AXI总线读写PL侧的BRAM逻辑上看起来毫无问题可到Vitis里一跑读回来的数据要么是0要么是上一次写入的旧值。折腾了一个晚上最后发现是缓存一致性在作祟——一个只改一行代码就能解决的问题却让他差点把整块板子翻了个底朝天。这个场景太典型了。ZYNQ的PS端读写BRAM几乎是所有做PL与PS数据交互的人都会遇到的第一道坎。BRAM不像DDR那样需要复杂的控制器和时序训练也不像AXI GPIO那样每次访问都要经过一堆协议封装它在Vivado里连线简单、延迟可控、占用资源小特别适合做PS与PL之间的轻量级共享内存。但也正因为看着简单很多人把IP一连、地址一配就以为完事了等踩到坑才发现自己对这些IP内部的行为完全没概念。这篇文章就基于Vivado 2022.1把我在实际项目里反复踩过、也帮别人排查过的5个最容易忽略的配置细节一次性说透。先交代一下基本路径后面对照问题就清楚得多。一个标准的PS读写BRAM设计在Block Design里通常长这样添加ZYNQ7 Processing System IP使能M_AXI_GP032位或M_AXI_GP1。添加AXI BRAM Controller IP。添加Block Memory GeneratorBMGIP配置成真双口RAM。用Run Connection Automation自动连线生成AXI Interconnect把PS的GP口连接到AXI BRAM Controller。在Address Editor里给AXI BRAM Controller分配基地址和范围。导出XSA在Vitis里用Xil_Out32/Xil_In32或者直接指针访问。数据通路是这样的PS的CPU发出读写请求经过M_AXI_GP0进入AXI Interconnect到达AXI BRAM Controller控制器把AXI事务翻译成BRAM接口的读写时序最终作用到BMG内部的存储阵列上。PL侧如果想跟PS共享这块BRAM通常把BMG的B端口引出来接到自己的逻辑上。从CPU发出一条Xil_Out32到数据真正落进BRAM中间隔了这么多层任何一层的配置不对最终表现都是数据不对。我把实际踩到的坑归成三类IP互联参数不匹配、地址映射与Cache问题、传输协议与字节使能问题。下面顺着这三类逐个把5个细节讲透。注意2022.1版本里Xilinx SDK已经彻底被Vitis取代导出硬件后是在Vitis里创建平台工程和应用工程。xparameters.h不在FPGA工程目录里而是在Vitis平台的BSP包中生成找不到宏定义时先检查有没有正确生成平台工程。2. 细节一AXI BRAM Controller与Block Memory Generator的参数必须严丝合缝2.1 数据宽度不一致的后果AXI BRAM Controller的配置页面里有一个Data Width选项它决定AXI总线的数据位宽。ZYNQ的M_AXI_GP0是32位口所以这里最常用的是32。Block Memory Generator的Port A数据宽度必须和它保持一致。很多人在BMG里图省事用了默认的32位没问题但如果哪天你把控制器配成64位、BMG还是32位Vivado在Validate Design时大概率会报端口宽度不匹配的错偶尔版本宽容连上了运行起来就是随机错数据。反过来也一样BMG配64位、控制器配32位控制器只会驱动BRAM的低32位数据线高32位永远悬空或者被其他逻辑占用写入的数据会以你完全想不到的方式残缺。我的建议是先决定PS用哪个GP口再定数据宽度。GP0/GP1是32位就用32位BRAM如果你用PL侧的AXI HP口反向访问DDR那是另一套逻辑不在本文讨论范围。32位宽度对绝大多数PS-BRAM共享内存场景完全够用没必要为了看起来高级去选64位。2.2 Number of BRAM Interfaces对地址空间的拆分AXI BRAM Controller还有一个容易忽略的选项Number of BRAM Interfaces默认是1。当你把它改成2或更多时控制器会把AXI地址空间按接口数均分每个BRAM接口映射到一片连续的地址区间。比如接口数为2就分上下两半接口0占用低半区接口1占用高半区。这里最容易犯的错是接口数设成2但BMG只实例化了一个单端口RAM或者虽用了双端口RAM但两个端口都被连到了同一个控制器接口上导致第二个接口没有实际存储阵列。表现出来就是前一半地址读写正常后一半地址读到全0或全部是前一半的内容。这在Address Editor里看的时候特别迷惑——地址明明是分配好了的怎么就是不对。严格来说接口数大于1时你需要有对应数量的存储端口。最常见的做法是一个BMG配成True Dual Port RAMPort A连控制器的BRAM_PORTAPort B连控制器的BRAM_PORTB。这样两个接口各占一半地址空间互不干扰。如果PL侧还想自己访问那就再加一个BMG或者干脆用简单的方案接口数保持1把BMG的Port B留给PL自定义逻辑这样PS从A口读写PL从B口读写双口天然隔离反而更清爽。2.3 BMG的Memory Type和Write Enable设置BMG配置页面里的Memory Type有三个选项Single Port RAM、Simple Dual Port RAM、True Dual Port RAM。跟AXI BRAM Controller搭配时我的经验是直接选True Dual Port RAM。即使你只用了Port A一个口也建议保持这个配置因为你不知道后面调试时会不会想从PL侧加一个观察口。True Dual Port RAM两个端口完全独立A口给PSB口给PL比Simple Dual Port灵活得多。更关键的是Write Enable选项。在BMG的Port A Options里有个下拉框让你选Write Enable还是Byte Write Enable。和AXI BRAM Controller配合使用时必须选Byte Write Enable。原因很简单AXI总线上有WSTRB字节使能信号PS可能只改一个字节。如果BMG用整字Write Enable那一个字节的写操作会被控制器展开成整字写其他三个字节的数据从哪来从控制器读回的旧数据里来。一旦读到的是缓存里的脏数据或者时序不对的数据相邻字节就被破坏了。2.4 验证方法这些配置问题Vivado的Validate Design能拦下一部分宽度不匹配、端口连接错误但拦不住接口数对应关系和字节使能这类逻辑上合法但行为错误的配置。我的验证习惯是在Block Design里把AXI BRAM Controller和BMG两个IP的配置页对照着截图逐项核对Data Width、Read Latency、Memory Type、Byte Write Enable然后再看连线。别人的工程出了问题我第一件事也是干这个。3. 细节二Address Editor里的Range与Base Address决定XPAR宏和访问范围3.1 XPAR宏到底是哪来的在Vitis里写Xil_Out32(XPAR_AXI_BRAM_CTRL_0_BASEADDR, data)这个宏定义在xparameters.h里。它由Vivado的Address Editor根据你分配的地基地址自动生成。如果你在Block Design里忘了给AXI BRAM Controller分配地址或者分配后没有重新Export HardwareVitis工程里的xparameters.h就找不到XPAR_AXI_BRAM_CTRL_0_BASEADDR这个定义编译直接报undeclared identifier。这个坑太常见了尤其是一些人喜欢在SDK/Vitis里硬编码0x40000000。硬编码在大多数情况下能用但一旦地址分配变了你的程序还在访问旧地址数据自然全乱。我强烈建议源码里永远用XPAR宏不要手写地址。3.2 Range与BRAM实际容量不匹配Address Editor里给AXI BRAM Controller分配的Range必须等于BRAM的实际容量。这个等于不是大概是严格等于。举个例子BMG配置成32KB那么Range就该是0x8000分配的地址区间就是0x40000000到0x40007FFF。很多人做完BMG之后不去看实际容量随手给控制器分配一个0x1000或0x100000于是问题来了Range小于实际容量你只能访问到BRAM的一部分超出Range的地址可能触发DECERR或者落到其他外设上。ZYNQ的地址译码在这个区间的行为不一定让你立刻看到报错但数据绝对是错的。Range大于实际容量多出来的地址区间在多数实现里会回卷映射到同一片BRAM。你访问0x40008000得到的其实是0x40000000的内容。这种能读能写但内容错位的现象比直接报错更让人抓狂。所以分配地址前先看BMG配置摘要里的Memory Size算好16进制再填进Address Editor的Range列。3.3 多接口时的Range计算如果AXI BRAM Controller设了多个BRAM接口Address Editor里的Range就不是单个BRAM容量了而是单个BRAM容量 × 接口数。比如两个接口、每个32KB总Range就是0x1000064KB接口0占据0x40000000-0x40007FFF接口1占据0x40008000-0x4000FFFF。这个关系在Address Editor里不会自动给你拆成两行而是都算在一个从设备的地址映射里。调试时如果用PL侧的B端口数据不对先算算当前访问的地址到底落在哪个接口的区间里别对着错误接口瞎查。3.4 地址冲突检查Address Editor里如果某个地址和已有的外设重叠行背景会变成红色或粉色。ZYNQ的地址空间中DDR通常从0x00100000开始PL外设一般自动分配到0x40000000区域。新手最容易犯的错是手动给BRAM填了一个和DDR重叠的地址Vivado虽然标红但很多人没注意直接生成比特流。结果在Vitis里一跑板子直接挂死或者DDR数据被破坏。经验每次添加或修改地址映射后养成习惯看一眼Address Editor有没有红色行。Vivado的自动分配Assign All通常很靠谱除非有特殊原因别手改。4. 细节三缓存一致性——BRAM数据不更新的头号元凶4.1 现象描述这是PS读写BRAM时最经典、也最让人崩溃的坑。现象通常是以下两种第一种PL侧逻辑持续往BRAM里写某个状态标志PS通过轮询方式等待这个标志变成1。但不管PL写多少遍PS读到的永远是0。偶尔PS重启或者优化等级改了又能读到了但数据是很久以前的。第二种PS往BRAM里写了一批数据然后通过另一个地址通知PL数据就绪。PL侧读到的却是全0或是上次写入的旧内容好像PS根本没写过。我见过有人因此去怀疑PL逻辑花几个小时翻RTL代码也有人怀疑是BRAM配置问题反复重建IP。其实问题根本不在PL也不在BRAM而在PS自己的CPU缓存。4.2 根因ZYNQ的A9缓存与不可一致的GP口ZYNQ的Cortex-A9带有L1和L2 Cache。M_AXI_GP0这条通路是非一致性的——CPU访问GP0外设时不会自动帮你在Cache和外部内存之间做一致性同步。换句话说Cache里的数据和BRAM里的数据在PS看来是两份完全独立的东西全靠你手动维护。那为什么有的人就没事因为这取决于整个地址空间的MMU映射。FSBL初始化阶段会建立页表决定哪些地址是Cacheable、哪些不是。不同版本的FSBL和BSP对0x40000000这个PL外设区域的默认映射并不一致有的映射成Device非缓存有的映射成Normal Write-Back缓存。你在某个版本上跑的没问题换个工程模板或者BSP升级后问题就冒出来了。还有一个更隐蔽的点Xil_In32和Xil_Out32的实现只是加了volatile的指针读写。volatile只保证编译器不优化掉这个访问它根本不绕过CPU缓存。所以别指望换这两个函数就能解决问题。4.3 方案A直接把BRAM区域标记为非缓存最干净利落的做法是在应用启动时调用Xil_SetTlbAttributes把BRAM所在地址区域标成Non-Cacheable#include xil_mmu.h #include xparameters.h #define BRAM_BASEADDR XPAR_AXI_BRAM_CTRL_0_BASEADDR #define BRAM_SIZE 0x8000 void bram_cache_setup(void) { /* 把这个1MB段section标为非缓存 */ Xil_SetTlbAttributes(BRAM_BASEADDR, NORM_NONCACHE); Xil_DCacheFlush(); /* 设置完顺手刷一次清掉可能残留的脏行 */ }注意Xil_SetTlbAttributes操作的最小粒度是1MB的section。你的BRAM如果基地址是0x40000000那么0x40000000到0x400FFFFF这整个1MB都会被标记为非缓存。对BRAM这种小容量共享区来说这片区域里就算还有其他外设通常影响也不大可以接受。设置之后所有对这段地址的读写都会直接穿透Cache到达AXI总线。这样PS写的就是BRAM实际收到的PS读的就是BRAM当前的真实内容。用这种方式PL和PS之间的共享内存交互就完全不需要考虑刷Cache的问题了确定性最好。4.4 方案B保留缓存手动刷写缓存行如果你不想动MMU设置或者BRAM区域刚好和其他必须缓存的外设挤在同一个1MB段里那就用Cache维护指令#include xil_cache.h #define BRAM_BASEADDR XPAR_AXI_BRAM_CTRL_0_BASEADDR /* PS写完数据后通知PL之前把这段从Cache刷出去 */ Xil_DCacheFlushRange((UINTPTR)BRAM_BASEADDR, len); /* PL写完数据后PS读取之前让这段Cache失效强制从BRAM重读 */ Xil_DCacheInvalidateRange((UINTPTR)BRAM_BASEADDR, len);流程上要记住一个口诀写后Flush读前Invalidate。写BRAM之后数据可能还赖在Cache里没落地要Flush出去读BRAM之前Cache里可能还是旧数据要把对应的缓存行作废让CPU下次真正去BRAM里拿。完整场景是这样的/* PS - PL写数据 */ memcpy((void *)BRAM_DATA_OFFSET, src, len); Xil_DCacheFlushRange((UINTPTR)BRAM_DATA_OFFSET, len); Xil_Out32(BRAM_FLAG_OFFSET, 1); /* 写就绪标志 */ Xil_DCacheFlushRange((UINTPTR)BRAM_FLAG_OFFSET, 4); /* PL - PS读数据 */ while (Xil_In32(BRAM_FLAG_OFFSET) 0); /* 轮询时也要注意缓存 */ Xil_DCacheInvalidateRange((UINTPTR)BRAM_DATA_OFFSET, len); memcpy(dst, (void *)BRAM_DATA_OFFSET, len);轮询标志这件事也要小心如果标志地址是Cacheable的你轮询的可能是Cache里的旧值。所以要么把标志所在的区域也设成非缓存要么轮询每次前做Invalidate。用方案B做共享内存细节很多前后顺序记错一个就白忙活。4.5 我的建议个人习惯BRAM这种小容量、高频率访问的共享区直接非缓存最省心。Cache刷写方案更适合大块数据搬运比如视频帧、大数组那时候用FlushRange/InvalidateRange的开销相对数据量来说可以接受。小数据交互还用Cache方案反而容易在标志位和缓冲区的缓存状态组合上翻车。辅助判断技巧如果PS读PL写的标志位在调试器里单步执行时能读到正确值正常运行就卡死十有八九是Cache问题。因为调试器的内存窗口走的是调试访问通道不经过CPU的Cache跟你程序里读到的完全是两回事。5. 细节四Read Latency与BRAM输出寄存器的匹配回读乱码的隐形开关5.1 Read Latency到底是什么BRAM的读操作从地址稳定输出到读数据在数据线上有效是需要时间的。在FPGA里这个时间以时钟周期数来衡量就是Read Latency。Block Memory Generator允许你调整这个参数范围通常是1到32。Latency 1表示不加输出寄存器地址打进去一拍后数据直接出来。这个模式延迟最低但BRAM输出路径的组合逻辑到后级的建立时间会比较紧张频率跑不高。Latency 1时BRAM内部会插入输出寄存器把数据打一拍或多拍再送出来时序更好但代价是读延迟增加。AXI BRAM Controller里也有一个Read Latency选项它的意思是控制器认为BRAM从收到地址到返回数据需要多少个时钟周期控制器按照这个预期去采样读数据。5.2 两边不一致会发生什么如果BMG配了Read Latency 3插了两级输出寄存器但AXI BRAM Controller的Read Latency还是默认的1控制器会在地址发出后的第1拍就去采样数据总线。这时候BRAM的数据根本还没出来控制器采到的是上一拍的旧数据或者不定态。最终表现就是回读的数据全乱或者第一次读对、第二次读错和Cache问题的表象非常像。反过来控制器Read Latency设得比BMG大比如BMG实际是1控制器配了3结果不会出错只是每次读操作多等两拍性能略降。所以这里的规则是控制器的Read Latency必须大于等于BMG的Read Latency。5.3 推荐的配置组合场景BMG Read LatencyAXI BRAM Ctrl Read Latency说明简单功能验证、时钟不高11默认配置延迟最小时钟较高或BRAM较大22最常用BMG插一级输出寄存器时序紧张需要更多打拍33必须两边同步改我在实际项目里凡是BRAM容量超过16KB或者时钟高于100MHz都会用Latency 2的组合。改的时候一定两个IP一起改我在Vivado 2022.1里就犯过只改BMG忘了改控制器的错结果浪费了两个小时查回读乱码。记住BMG负责我需要几拍控制器负责我等几拍两边必须对上。5.4 排查手法遇到回读数据不对先用排除法确认不是Cache问题比如用Xil_SetTlbAttributes标了非缓存之后依然不对然后去看两个IP的Read Latency配置。最直接的手段是挂ILA在AXI BRAM Controller的S_AXI接口上挂一个ILA核抓一组读事务看地址通道发出后读数据通道到底第几拍返回数据再对照控制器的Latency配置一眼就能看出是不是采样早了。经验修改Read Latency后必须重新生成比特流。有些状态机逻辑会在地址分配图上体现出额外的等待拍数但BRAM本身的行为只由Bitstream决定。只改了IP配置没重新综合布线等于没改。6. 细节五AXI4/AXI4-Lite协议模式与字节使能DMA联调时才暴露6.1 协议模式的本质差异AXI BRAM Controller的Protocol选项可以选AXI4或AXI4-Lite。两者的核心区别在于AXI4支持突发传输Burst一次交易可以连续传多个数据AXI4-Lite是精简版每次只能传一个数据不支持Burst。如果你只用CPU的Xil_Out32/Xil_In32逐字读写BRAMAXI4-Lite完全够用因为每次访问都是一次单拍交易。但这个配置会在你接DMA时突然爆炸。6.2 DMA联调时的典型翻车场景是这样的你用AXI DMA把传感器数据搬到BRAM里或者用DMA把BRAM里的数据搬到DDR。Block Design里AXI BRAM Controller保持默认或者被向导设成了AXI4-LiteCPU单拍读写测试一切正常你信心满满地启动DMA——然后发现DMA事务卡死、报DECERR错误或者数据到了但全是错位的。原因很简单DMA为了吞吐率必然发INCR突发。AXI4-Lite的从设备接收到突发请求时要么返回错误响应要么根本不支持导致总线挂起。CPU单拍测试压根不会触发这个问题所以排查起来特别迷惑很多人的第一反应是DMA配置错了实际上问题出在BRAM控制器这边。结论只要你的设计里有任何可能发起突发的MasterAXI DMA、更复杂的CPU memcpy路径AXI BRAM Controller的Protocol就选AXI4。6.3 字节使能Byte Write Enable的隐蔽影响接上DMA之后数据传输就不仅仅是32位整字了。DMA可能按字节流搬运数据AXI总线上会出现非对齐传输和部分字节写。这时候就得回到配置细节一里强调的Byte Write Enable。AXI总线的写数据通道带有WSTRB信号表示每一路字节是否有效。AXI BRAM Controller会把WSTRB映射到BRAM的字节写使能引脚上。但如果BMG用的是普通Write Enable而不是Byte Write Enable相当于告诉BRAM这一整字都是有效数据——于是本该只写一个字节的操作把整个32位字都写进去了。相邻的、你不打算动的字节会被重新写入内容可能是零也可能是脏数据。表现就是数据的大部分字节是对的但每隔几个字节就夹杂着一个错的或者连续写入后回读数据里出现回声。正确的BMG配置是Port A Options里选Byte Write EnableByte Size保持默认的8。这样AXI侧的WSTRB才能正确传递到BRAM端口。6.4 配合DMA的完整注意事项如果你确定要上DMA除了协议选AXI4、BMG开Byte Write Enable之外还要注意Cache问题。DMA本身不走CPU的Cache它是直接通过AXI访问BRAM的所以DMA和PL之间的数据交互天然一致。麻烦出在CPU和DMA之间CPU写数据到BRAM再启动DMA读走CPU写的数据可能还在Cache里DMA读的是BRAM两边数据不一致。需要Xil_DCacheFlushRange先把数据刷出去。DMA写完BRAMCPU去读CPU的Cache里可能有旧值需要Xil_DCacheInvalidateRange让Cache失效。这和细节四的缓存维护思路完全一致只是参与方多了一个DMA更容易忘记。我见过有人CPU写完了DMA搬运的却是全零数据就是漏了Flush。6.5 协议模式的另一个连带选择AXI BRAM Controller还有一个伴随选项ECC类型。默认是None这个保持默认就好。开了ECC之后BRAM会额外占用资源而且写数据时要计算校验、读数据时要校验会有额外延迟对绝大多数PS共享内存场景没必要。等哪天你的设计里有单粒子翻转风险、或者做航空航天级产品时再回来研究它不迟。7. 配置速查表与一次完整的验证流程把前面5个细节汇总成一张速查表建议截图存下来下次搭工程直接对照配置项推荐值踩坑后果AXI BRAM Ctrl Data Width与BMG Port A一致通常32连接报错或随机乱码AXI BRAM Ctrl ProtocolAXI4DMA突发传输卡死或错数据AXI BRAM Ctrl Read Latency≥ BMG Read Latency回读乱码Number of BRAM Interfaces1除非确实需要多接口地址空间拆分错乱BMG Memory TypeTrue Dual Port RAM双接口无实际存储BMG Write EnableByte Write Enable相邻字节被覆盖BMG Read Latency与控制器一致常见2回读乱码Address Range容量单接口或容量×接口数地址回卷或DECERRCache策略非缓存 或 Flush/Invalidate读到旧数据、写不进去配置做完别急着上业务逻辑先用一段固定的验证流程把通路跑通。我自己的验证顺序是这样的第一步UART打印XPAR_AXI_BRAM_CTRL_0_BASEADDR和HIGHADDR确认宏的值和Address Editor里的分配一致。这一步能拦截一半的地址问题。第二步写读回测试。往BRAM里依次写0xDEADBEEF、0x12345678、0x00000001然后读回比对。这个简单测试能发现数据线短接、字节使能不对、Read Latency不匹配等大部分问题。第三步全地址遍历。把整片BRAM填满(i * 4 0x55AA5500)这样的递增模式再依次读回。这一步能发现地址线问题。如果某个地址读到的内容恰好是相邻地址的内容基本就是地址译码或Range设置的问题。第四步PL到PS方向测试。在PL里写一个简单的计数器持续往BRAM的某个地址递增PS这边轮询该地址看数据是否连续变化。如果读到的数据卡住或者跳变不连续优先怀疑Cache一致性直接标非缓存再看。第五步如果设计里有DMA最后单独测DMA通路先搬一小段数据比如32字节确认无误再放大数据量。DMA和CPU交互时每一步问自己数据现在在哪里Cache里、BRAM里还是FLIT的途中断了最后分享一个土办法在所有涉及到BRAM的调试初期我习惯把Xil_SetTlbAttributes(BRAM_BASEADDR, NORM_NONCACHE)无条件加上把Read Latency配成两边都是2。这两件事做完大概能排除掉七成以上PS读写BRAM不正常的怪异问题。等基础通路彻底跑通了再根据性能需求逐项优化也不迟——毕竟BRAM这种轻量级共享内存稳定可靠永远比那几百纳秒的延迟更重要。
返回列表