☰
ZYNQ PS端AXI读写BRAM的5个关键配置与调试技巧
2026/10/3 0:47:27 网站建设 项目流程

很多刚开始接触ZYNQ的朋友,第一次做PS与PL数据交互,通常都会从一个最简单的实验开始:让ARM核往BRAM里写一串数据,再读出来看看对不对。这个实验看着简单,实际做起来坑并不少。我带项目这几年,至少帮人排查过十几次这类问题,最后发现大多数情况都不是逻辑写错了,而是Vivado里几个不起眼的配置没搞对,或者软件访问方式踩了缓存和字节使能的坑。

这篇文章会聚焦ZYNQ PS端通过AXI总线读写BRAM的完整链路,把最容易被人忽略的5个配置细节完整拆开,讲清楚每个细节背后的原理、出错时的典型现象,以及调试心得。我用的工具版本是Vivado 2022.1,文中提到的界面、参数和流程都基于这个版本,个别设置在其他版本里也大同小异。适合正在学ZYNQ的开发者、做嵌入式软硬件协同设计的工程师,以及被PL-PS数据交互搞得头大的朋友参考。

1. 先理清PS读写BRAM的完整链路,再谈配置

1.1 你面对的是一条什么样的数据通路

在ZYNQ里,PS端要读写PL侧的BRAM,硬件上走的是一条AXI总线通路。最常见的设计是:PS的M_AXI_GP0(通用主接口)或M_AXI_HP(高性能接口)发出读写请求,经过AXI Interconnect,到达AXI BRAM Controller,然后再把我们习惯说的“BRAM”实例化出来。

这条链路里,AXI BRAM Controller本身是一个IP核,它干的事情是把AXI协议的读写事务翻译成BRAM端口时序。BRAM那一侧暴露出来的通常是标准的RAM接口,有地址、写数据、读数据、写使能、字节使能这些信号,可以直接连接到Block Memory Generator生成的RAM,也可以接到你自己手写的RTL逻辑。

很多人以为在Block Design里添加一个AXI BRAM Controller,然后把ZYNQ的AXI接口连上去就行了,实际上这一步做完只算搭好了“骨架”。控制器两侧的数据位宽是否匹配、地址分配是否合理、复位时钟是否接对、字节使能是否生效,都会直接决定你后面读写到的数据是否正确。我在实际项目里看到过不少案例,综合实现都过了,上板下载也正常,但PS端读回来的数据就是不对,问题大多出在这一层。

1.2 高频翻车点一览

结合我遇到过的项目情况,PS端读写BRAM翻车的高频点基本可以归为这五类:AXI与BRAM数据位宽不匹配、地址映射与基地址使用不当、缓存一致性与编译优化问题、字节使能与BRAM写模式理解偏差、时钟复位与AXI接口使能遗漏。这五个问题如果单独看都不复杂,但它们埋在不同配置界面的角落里,平时不做软硬件协同的人很难一下全注意到。

我把这5个配置细节在下表里先给个总览,后面逐项展开。

配置细节出错时的典型表现影响程度
BRAM数据位宽与AXI接口宽度不匹配地址错位、读回数据跳到相邻地址高
地址映射与基地址使用不当访问到别的外设、读回全F高
缓存一致性处理不到位PL侧读不到最新写入内容、数据有时对有时错高
字节使能与写模式设置偏差低字节写对了、高字节还是旧值中
时钟复位与AXI接口使能遗漏总线握手超时、数据全零高

2. 细节一:BRAM数据位宽与AXI接口宽度不匹配

2.1 这个配置藏在哪里

在Vivado 2022.1中,双击Block Design里的AXI BRAM Controller,可以看到“Data Width”这个参数。这个Data Width指的是AXI侧的数据宽度,默认值为32bit。真正容易让人忽略的是它下面还有一个BRAM接口数据宽度的概念,在IP配置界面里体现为“Number of BRAM interfaces”和对应接口的“Memory Size”等参数。实际控制BRAM侧位宽的,是你在生成BRAM那一侧使用的Block Memory Generator的“Read Width”和“Write Width”。

这里最容易出的状况是:AXI侧是32bit,但BRAM侧被你或者自动生成的IP配置成了64bit。AXI BRAM Controller为了适配两侧宽度差异,会把一次AXI事务拆成两次BRAM访问,或者反过来把两次AXI事务合并成一次BRAM访问。逻辑上控制器会做处理,但地址的对应关系不再是简单的“AXI地址等于BRAM地址”。很多人拿PS端地址去对PL端仿真波形里的BRAM地址,发现对不上,然后就慌了。

2.2 为什么会造成地址错位

我举个实际例子。AXI侧32bit,BRAM侧64bit,PS端往地址0x0写一个32位数据0x11223344。AXI BRAM Controller收到写事务后,BRAM侧一次写操作要覆盖8字节空间,于是它会把这个32bit数据放到BRAM的低32位,同时字节使能信号只拉低4个字节。PL侧如果直接在地址0x0读BRAM的64位数据,读到的是0x????????11223344,高32位是原来残留的内容,看起来就像是“数据漂移”了。反过来,如果PL侧往BRAM地址0x0写了一个64位数据,PS端从AXI地址0x0读32位数据,可能只能拿到低32位,高32位要再从地址0x4去读。

这种宽度不匹配本身不是错误,控制器也确实能正确处理AXI窄位传输,但问题是很多人并没有意识到两侧宽度不一致,所以在调试时用错误的地址去比对数据,白白浪费很多时间。如果你在PL侧用ILA抓BRAM的地址和数据,想验证PS写进去的内容,就更要搞清楚这个换算关系。

2.3 实际操作建议

我的建议是:除非PL侧确实需要64bit或更宽的存储位宽,否则尽量把AXI侧和BRAM侧都保持为32bit。这样地址一一对应,最简单,也最容易排查。如果你确实需要64bit宽BRAM,那就别在PS端用32bit访问去拼地址了,直接把AXI数据宽度也改成64bit,让整个链路的数据宽度保持一致,避免窄位传输带来的各种隐性问题。

还有一个建议:在Block Design里生成AXI BRAM Controller时,它会自动创建一个Block Memory Generator,你双击后去查看“Read Width”和“Write Width”,确认它们和AXI数据宽度一致。这个习惯我一直保持到现在,因为某些版本里自动生成的BRAM宽度不会跟你AXI侧设置完全同步,这是踩过一次坑之后长记性的。

3. 细节二:地址映射与基地址使用不当

3.1 Address Editor里的地址不是随便分的

在Block Design里完成连接后,Vivado下方有一个Address Editor标签页,这里会给所有挂在AXI总线上的从设备分配地址空间。AXI BRAM Controller默认可能被分配到0x40000000这样的位置,对应ZYNQ M_AXI_GP0的地址空间。这个地址会直接影响后续SDK或Vitis里生成的xparameters.h头文件里的宏定义。

我见过很多人做这一步时不看Address Editor,觉得默认分配就行。但在比较复杂的设计里,总线上可能挂了好几个外设,比如AXI BRAM Controller、AXI UART、AXI GPIO、VDMA,甚至多个BRAM控制器。Vivado的自动分配逻辑能保证地址不冲突,但可读性很差,容易导致你分不清哪个地址对应哪个IP。更关键的是,有些操作会把地址范围改掉,而你SDK工程里还留着旧地址的宏,编译出来的程序访问的自然就是错误位置。

另一个容易被忽略的点是地址范围大小。AXI BRAM Controller有一个“Memory Size”或“Depth”参数,决定了它能映射多大空间。比如你设了32KB,但代码里往基地址加0x12000这种偏移去写数据,实际上已经超出BRAM的物理深度,访问会落回或者其他总线区域。而AXI接口对这种访问不会报错,它会直接返回一个总线错误或者读回垃圾数据,表现出来就是你写进去的数据“消失”了。

3.2 基地址到底该怎么取

在裸机开发里,Xilinx的BSP会自动生成很多以XPAR_开头的宏。比如XPAR_AXI_BRAM_CTRL_0_BASEADDR,这就是AXI BRAM Controller的基地址。很多人在代码里直接写死一个地址,比如0x40000000,这是非常危险的做法。因为只要你改了Block Design里的地址分配,导出硬件后,SDK或Vitis里的宏会自动更新,而你手写的硬编码地址是不会变的,两边一旦不一致,整个程序的行为就变得莫名其妙。

我自己的习惯是永远从宏取基地址,最多配合一个相对偏移宏来做寄存器或者内存块寻址。比如这样写:

#define BRAM_BASEADDR XPAR_AXI_BRAM_CTRL_0_BASEADDR #define BRAM_ADDR(offset) (BRAM_BASEADDR + (offset)) u32 val = Xil_In32(BRAM_ADDR(0x100));

这样不管Vivado里地址怎么重新分配,我的代码都能跟着走,不用每次导完硬件都回代码里改一遍。

3.3 怎么检查地址是否冲突

在Vivado 2022.1里,Validate Design会帮你检查地址冲突,如果有两个IP分配了相同地址,会报错。但这个检查只会拦“完全重叠”的情况,不会拦“部分重叠”或者“访问越界”。所以我建议你在导出硬件之前,手动打开Address Editor看一眼,确认AXI BRAM Controller的起始地址和结束地址是否和你预期一致。

有一个实用的小技巧:在调试阶段,我一般先把BRAM控制器映射到GP0地址空间靠前的位置,然后在Vitis的Memory窗口里输入基地址,看能不能直接读出内容。如果Memory窗口显示全F或者访问失败,基本可以断定地址映射或者总线连接有问题,这时候再去查Address Editor,比对着代码瞎猜快得多。

4. 细节三:缓存一致性与编译优化问题

4.1 裸机下的缓存问题没那么简单

说到PS端读写BRAM,很多人会忽略一个问题:ZYNQ的CPU在访问外设地址时,可能经过L1/L2 Cache,也可能不经过。具体走不走Cache,取决于页表里的内存属性配置,这个配置在SDK/Vitis的BSP里默认已经帮你做好了,通常会把PL外设地址映射成“Device”或者“Strongly-Ordered”,也就是说默认情况下不会缓存。

但实际开发中,有不少人会在代码里把BRAM基地址强制转换成一个普通的unsigned int指针,然后频繁访问。这种操作本身没问题,问题出在两处:第一,如果编译器做了优化,连续多次对同一个地址赋值,可能把中间结果优化掉;第二,如果你在推理寄存器地址时误把BRAM地址当成了普通DDR内存,开启了Cache属性,写操作就只会进Cache,不会立即落到BRAM里,PL侧逻辑去读BRAM时自然读不到最新数据。

4.2 正确做法和排错思路

我的建议是:在裸机环境里访问BRAM这类PL外设地址,不要用普通指针直接操作,至少也要加上volatile关键字,或者直接用Xilinx提供的Xil_In32和Xil_Out32。这两个函数内部会保证访问不被编译器优化,并且在底层指令层面会走正确的内存外设属性。

如果你确实在代码里用了内存拷贝的方式往BRAM写大块数据,比如从DDR里memcpy到BRAM地址,那么写完以后、在通知PL侧逻辑启动处理之前,建议加一次数据同步屏障。在ARM里可以使用dsb指令,在Xilinx裸机BSP里可以用Xil_DCacheFlushRange这一类接口。虽然GP口访问不一定经过Cache,但这个操作可以确保写缓冲区的数据都真正落到了目标地址,不会因为写缓冲的延迟导致PL侧抢先读到了旧值。

如果你用的是Linux系统来访问BRAM,那就更要注意了。在设备树里把BRAM地址范围映射出去的时候,如果默认用了Cacheable属性,用户空间通过mmap访问到的数据可能一直在Cache里打转。正确的做法是把这个地址范围标记为non-cacheable,或者用memremap这类接口指定不缓存属性。这个坑在Linux环境下比裸机更常见,因为内核的默认行为有时会让你措手不及。

5. 细节四:字节使能与BRAM写模式设置偏差

5.1 什么是字节使能,为什么它会坑你

AXI协议里每个写事务都带一组WSTRB信号,用来指示8位字节通道里哪些字节是有效的,这就是字节使能。AXI BRAM Controller在默认配置下,会把AXI的WSTRB信号映射到BRAM侧独立的字节使能信号上。这意味着,如果你用32位指针往地址0x0写,但只写了一个8位数据(实际会走一个字节的写事务),那BRAM只会更新对应的那个字节,其余3个字节保持原值。

很多人在这里容易懵:我明明往地址0x0写了一个值,读回来整个32位怎么不完全是这个值?对,因为其他字节还是旧数据。如果你之前在这个地址里留下过其他内容,那读回来的就是一个“混合体”。在实际项目里,这个问题最典型的体现就是:PS端往BRAM里写了一个结构体,但结构体里有8位、16位、32位混合的成员,写完后PL侧读到的数据结构里有些字段不对。实际上就是字节使能捣的鬼。

5.2 什么时候应该关掉字节使能

AXI BRAM Controller的配置界面里有一个“Byte Write Enable”选项。如果勾选,控制器会把WSTRB传到BRAM;如果取消勾选,控制器会忽略WSTRB,每次都产生完整的字节使能,也就是一次写入会把整个数据字的所有字节都覆盖。

如果你PL侧的BRAM是纯数据缓存用途,而且每次写入都希望覆盖整字,那关闭字节使能会让调试变得省心不少。但如果你要靠BRAM做FIFO或者寄存器阵列,并且存在只更新一个字节的场景,那还是保留字节使能比较合理。我的经验是:在早期调试阶段,先把这个选项关掉,确保读写整字数据是稳定正确的,之后再按需打开。这样可以减少一个变量,把问题范围缩小。

5.3 BRAM写模式对读写并发的影响

Block Memory Generator里有一个“Operating Mode”参数,可选“Write First”、“Read First”和“No Change”。默认情况是“No Change”,意思是写操作不会改变读数据线上的内容,读端口只有在读地址有效时才输出数据。这个模式在PS写PL读的场景下一般没有大问题。

但在某些设计里,PS端和PL端会同时访问BRAM的同一个地址,尤其是同一个端口的读和写并发,这时就要关注写模式了。比如你在PS端往一个地址写数据,紧接着马上读同一个地址,如果是“Read First”模式,读出来的可能是旧值,因为读操作优先于写操作。这种时序细节在仿真里容易忽略,上板后表现出来就是“刚写进去就读不对”。如果你要确保读写同一个地址时拿到的是最新数据,推荐把BRAM配置成“Write First”,或者尽量避免在同一端口对同一地址做背靠背的读写。

6. 细节五:时钟复位与AXI接口使能遗漏

6.1 ZYNQ的AXI接口不是默认全部打开的

在ZYNQ PS端的配置界面里,有多个AXI主接口和从接口,比如M_AXI_GP0、M_AXI_GP1、S_AXI_HP0、S_AXI_HP1等。很多人新建Block Design后,用Run Block Automation添加ZYNQ,然后只勾选了其中一个GP接口,其他接口默认是关闭的。如果你后面想加第二个BRAM控制器,却发现PS端没有多余的AXI主接口可用,或者在连接时找不到你预期的那个口,就得回到ZYNQ配置里手动打开。

这个操作本身不难,难的是打开接口之后还要注意对应的时钟。ZYNQ PS端的AXI接口和互联时钟来自FCLK_CLK0、FCLK_CLK1这些输出。如果你打开了一个新接口但没有使能它的时钟,Block Design里可能会自动补一个外部时钟或者显示时钟未连接,综合实现也许能过,但上板后AXI访问就是起不来。

6.2 复位信号的坑

AXI BRAM Controller的复位输入通常是低有效,即S_AXI_ARESETN。ZYNQ PS端输出的外设复位peripheral_aresetn也是低有效,两者可以直接相连。问题常常出在手动连线的时候:有些人习惯性地把一个高有效的复位信号接到这个端口上,或者接到高有效复位端(如果有的话),结果就是控制器一直被复位住,AXI事务永远无法完成。

在Vivado 2022.1里,如果你用Connection Automation自动连接,时钟和复位网络一般都会被正确处理。但如果你的设计里手动添加了多个BRAM控制器,或者手动做了一个复位树,就需要仔细检查每个控制器的复位信号是否都接到了同一个低有效复位网络上。

我遇到过一个比较隐蔽的情况:设计里用了Processor System Reset IP,它输出的peripheral_aresetn默认没问题,但后来为了满足外部接口时序要求,我额外加了一个外部复位芯片,复位极性配置反了,导致整个PL侧逻辑在外部复位拉高时被重置。这种问题排查起来特别费劲,因为它不是每次都复位失败,而是受外部信号时机影响,表现成偶发性总线超时。

6.3 时钟频率的一致性

AXI BRAM Controller的S_AXI_ACLK需要和ZYNQ AXI接口输出的时钟频率保持一致,毕竟它们处在同一个总线事务里。大多数情况下这个时钟就是FCLK_CLK0,默认100MHz或者50MHz,Block Design里也会自动连接。但如果你改了FCLK_CLK0的频率,一定要确认AXI Interconnect和AXI BRAM Controller是否跟着更新了约束。Vivado一般会自动传播时钟信息,但偶尔在自定义约束文件里手动设了周期,就会和BD里的实际频率不一致,导致时序收敛异常或者上板偶发读写错误。这类问题在静态时序分析里会暴露出来,但很多人不习惯看时序报告,所以一直没发现。

7. 实操:Vivado 2022.1下的最小工程搭建与读写验证

7.1 从新建工程到生成硬件导出的关键步骤

工具版本为Vivado 2022.1。新建工程,选择ZYNQ-7000系列芯片,比如xc7z020clg400-1。创建一个Block Design,添加ZYNQ7 Processing System IP,运行Run Block Automation,勾选M_AXI_GP0,保留默认的FCLK_CLK0设置。

接着从IP Catalog里添加AXI BRAM Controller。双击AXI BRAM Controller,把Data Width设为32bit,Memory Size按需设置,我这里设成8KB,BRAM接口类型选Single Port BRAM。确认Byte Write Enable暂时关闭,等基础读写验证完再打开。关闭后,控制器会生成一个Block Memory Generator实例,双击它查看Read Width和Write Width,确认都是32位。

在Diagram窗口里,先用Connection Automation把ZYNQ的M_AXI_GP0连到AXI BRAM Controller的S_AXI端口。这个自动化过程会把AXI Interconnect、时钟和复位网络都补上。分别确认连线后,打开Address Editor,找到AXI BRAM Controller的地址范围,记录下基地址。

然后点击Validate Design,确认没有错误。创建HDL wrapper,让Vivado自动管理顶层。生成比特流,这里要注意,如果你的设计不需要PL侧的硬件逻辑,只是想让PS能访问BRAM,那比特流其实可以很轻量。生成完比特流后,选择File -> Export Hardware,勾选Include bitstream,导出到最后一步,再Launch Vitis 2022.1。

7.2 在Vitis里做一次完整的读写验证

在Vitis里新建一个Application Project,平台选择导出的硬件平台。模板可以选Hello World,然后我们把main函数替换成更直接的读写验证代码。

#include "xparameters.h" #include "xil_io.h" #include "xil_cache.h" #include "stdio.h" #define BRAM_BASEADDR XPAR_AXI_BRAM_CTRL_0_BASEADDR int main() { volatile u32 *bram = (volatile u32 *)BRAM_BASEADDR; u32 read_back; int i; xil_printf("BRAM base address: 0x%08x\r\n", BRAM_BASEADDR); // 写入递增序列 for (i = 0; i < 128; i++) { bram[i] = 0xA0000000 + i; } // 同步写缓冲 Xil_DCacheFlushRange((UINTPTR)BRAM_BASEADDR, 128 * 4); // 回读验证 for (i = 0; i < 128; i++) { read_back = Xil_In32(BRAM_BASEADDR + i * 4); if (read_back != (0xA0000000 + i)) { xil_printf("Mismatch at offset 0x%x: expect 0x%08x, got 0x%08x\r\n", i * 4, 0xA0000000 + i, read_back); return -1; } } xil_printf("BRAM write/read test passed!\r\n"); return 0; }

这段代码有两个细节值得说明:第一,写数据用的volatile指针,读数据用的Xil_In32,两者都绕开了编译优化问题;第二,写完以后加了一次Xil_DCacheFlushRange,虽然BRAM地址通常不是Cacheable,但加上这个同步操作能确保写缓冲区被清空,避免PL侧在同一时刻读到不一致数据。实测下来,这个工程在ZYNQ-7020板卡上能稳定通过。

7.3 如何用PL侧进一步验证

如果想要更彻底地确认数据真的是进了BRAM而不是只被PS端看到了,可以在Block Design里把AXI BRAM Controller生成的BRAM端口额外引出来,接一个简单的ILA,或者直接接一组GPIO点亮LED来观察。更简单的方式是:在PL侧例化一个计数器,每隔一段时间去读BRAM里某个固定地址,然后把读出的值显示到LED上。只要PS端往那个地址里写值,LED状态跟着变,就说明整条链路完全打通了。

我在实际调试时比较常用ILA抓BRAM的写数据、写地址和写使能信号。先把PS端程序跑起来,让它往BRAM写一组固定的数,然后用ILA抓一段波形,看BRAM接口上的写地址是否按预期递增,写数据是否符合预期。这一步能快速判断问题出在AXI到BRAM控制器的翻译阶段,还是出在BRAM本身的使用方式上。

8. 常见问题与排查技巧实录

调试BRAM读写问题,最怕的就是漫无目的地改代码。下面这张表是我根据过去项目里遇到的典型情况整理的速查表,排查时可以按图索骥。

现象可能原因排查方向
读BRAM全返回0xFFFFFFFF地址越界、访问了未分配空间、总线没接对检查Address Editor的地址范围、xparameters.h里的基地址、AXI接口是否使能
写进去0x11223344,读出来是0x00003344字节使能未区分、可能只写入了部分字节检查是否用了8位/16位写操作、确认Byte Write Enable配置
PL侧读不到PS写的数据,一直旧值Cache一致性问题、写缓冲未同步使用Xil_DCacheFlushRange、确认BRAM地址无Cache属性
第一次读写正常,运行一段时间后数据错乱地址冲突、复位毛刺、总线时钟频率变化检查多个AXI Master的仲裁、复位网络、FCLK频率
Vitis里用了一个地址,程序却访问到另一个外设硬编码地址没有跟随Vivado地址分配更新替换为XPAR宏,重新导出硬件,不要手写固定地址
AXI事务偶发超时,总线卡住复位未正确释放、时钟没有稳定、AXI接口被外部信号干扰查看复位时序、确认peripheral_aresetn连接、检查时钟约束

在实际排查过程中,我有一个屡试不爽的原则:先把复杂功能全部去掉,只保留最简单的单次32位写读。比如写固定值0xA5A5A5A5,读回来比对。这一步通过了,再逐步增加偏移、增加数据量、增加多字节访问。很多人上手就写一大段业务逻辑,出了问题数据满天飞,很难定位。

另外送你一个排查小技巧:如果你的工程里同时有多个AXI外设,可以在Address Editor里查看每个外设的地址范围,然后在Vitis里用Xil_In32去读那些外设的基地址,看哪个寄存器地址能读到符合预期的值。这样能快速判断是不是访问错了外设。这个办法在处理VDMA、AXI UART和BRAM混在一起的设计时特别有用。

踩过几次坑之后我的体会是,ZYNQ PS端读写BRAM这件事,真正常出问题的不是BRAM本身,而是你对AXI链路各个配置项的认知是否准确。位宽匹配、地址映射、缓存处理、字节使能、时钟复位,这五点只要有一个地方理解偏差,表现出来就是各种奇怪的数据问题。建议大家在做软硬件协同设计时,先把这条链路的原理吃透,再动手搭工程,遇到问题也更有底气。后面如果你们在调试中碰到其他有意思的坑,欢迎留言交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询