阅读学习杰哥过去留下的 limfx 文档及相关资料后,我发现完整工程并不是一次性搭建完成的,而是由多个小实验逐步验证、组合而成。因此,本阶段我也计划先从几个关键实验入手,在实践中逐步理解 PS 端 DMA 搬运、DDR 缓存以及后续留盘功能的实现方式。
PL 端产生测试数据,通过 AXI DMA 搬运到 PS 端 DDR 中,再由 Vitis 裸机程序读取 DDR 数据,并通过 UART 串口打印出来,以验证 PL 到 PS DDR 的数据通路是否正确。
总体流程如下所示:
测试数据产生模块(testdata_gen.v) // 模拟产生 5 张子卡、共 160 通道的同步数据
↓
数据格式转换模块(ad_to_dma.v) // 将 5 路 512 bit 数据整理为 AXI-Stream 数据,并通过FIFO完成缓存与跨时钟域,将数据送入 DMA
↓
AXI DMA // 通过 S2MM 通道将数据写入 PS DDR
↓
Vitis 裸机程序读取 DDR // 从指定 DDR 地址读取 DMA 写入的数据
↓
UART 串口打印验证 // 打印数据内容,判断搬运结果是否正确
testdata_gen.v 用于在 PL 端产生测试数据,模拟真实 AD 采集数据。当前设计中模拟 5 张子卡,每张子卡以2M/s的采样率输出 512 bit 数据,对应 32 个 16 bit 通道。该模块的输入输出为:
input wire clk, //系统时钟
input wire rst_n, //复位信号
input wire enable, //模块使能信号
output reg [511:0] testdata0_o, //子卡0测试数据
output reg [511:0] testdata1_o, //子卡1测试数据
output reg [511:0] testdata2_o, //子卡2测试数据
output reg [511:0] testdata3_o, //子卡3测试数据
output reg [511:0] testdata4_o, //子卡4测试数据
output reg data_valid //数据有效信号
测试数据格式为:
testdata0_o: sample_cnt + 通道号
testdata1_o: sample_cnt + 通道号 + 0x1000
testdata2_o: sample_cnt + 通道号 + 0x2000
...
ad_to_dma.v 用于接收 5 张子卡的数据,并将其拆分、整理为 128 bit 宽度的 AXI-Stream 数据流,供后续 AXI DMA 接收。
由于每个采样点的数据大小为512*5 = 2560 bit,也就是需要拆分成 20 个 128 bit 的数据,考虑到为便于后续数据解析,每个采样点的边界应当清晰,所以在每个采样点前加一帧作为帧头,因此当前帧格式为:
1 个帧头 + 20 拍数据
数据转换模块的输入输出为:
input wire ad_clk, //PL侧时钟信号
input wire ad_rst_n, //PL侧复位信号
input wire dma_clk, //PS侧时钟信号
input wire dma_rst_n, //PS侧复位信号
input wire enable_i, //模块使能信号
input wire [511:0] ad_data0_i, //采集数据及有效信号输入
input wire [511:0] ad_data1_i,
input wire [511:0] ad_data2_i,
input wire [511:0] ad_data3_i,
input wire [511:0] ad_data4_i,
input wire data_valid_i,
output wire [127:0] axis_tdata_o, //AXI-Stream 数据总线,输出给 AXI DMA S_AXIS_S2MM
output wire [15:0] axis_tkeep_o, //字节有效标志,每 1 bit 对应 tdata 中 1 byte,16'hFFFF 表示 128 bit 全部有效
output wire axis_tvalid_o, //数据有效信号,表示当前 tdata/tkeep/tlast 有效
input wire axis_tready_i, //下游就绪信号,由 AXI DMA/FIFO 返回,表示可以接收当前数据
output wire axis_tlast_o //帧结束信号,拉高表示当前拍是一帧数据的最后一拍
Block Design 中需要加入 Zynq PS、AXI DMA 以及自定义 RTL 模块。 加入后首先配置Zynq UItraScale+ MPSoC:

配置好Zynq UItraScale+ MPSoC后需要进行DMA配置。DMA配置为:
SG:关闭
Control/Status Stream:关闭
MM2S:关闭
S2MM:开启
S2MM Stream Width:128 bit
S2MM Memory Map Width:128 bit
然后进行Block Design连线。Block Design设计如下所示:

首先将之前Vivado中的设计导出为.xsa文件。然后在vitis中:
main.c部分代码如下:
//关键参数
#define DMA_BASEADDR XPAR_XAXIDMA_0_BASEADDR //AXI DMA 控制寄存器基地址
#define GPIO_BASEADDR XPAR_XGPIO_0_BASEADDR //AXI GPIO 控制寄存器基地址
#define GPIO_CHANNEL 1U
#define GPIO_ENABLE_MASK 0x1U
#define RX_BUFFER_BASE 0x10100000U //DMA 写入 DDR 的目标地址
#define FRAME_BEATS 21U
#define BEAT_BYTES 16U
#define RX_BYTES (FRAME_BEATS * BEAT_BYTES) //一次 DMA 接收字节数
//main函数
int main(void)
{
int status;
xil_printf("\r\n");
xil_printf("========================================\r\n");
xil_printf("DDAQ simple PL -> DMA -> DDR test\r\n");
xil_printf("DMA baseaddr : 0x%08x\r\n", DMA_BASEADDR);
xil_printf("GPIO baseaddr : 0x%08x\r\n", GPIO_BASEADDR);
xil_printf("DDR buffer : 0x%08x\r\n", RX_BUFFER_BASE);
xil_printf("RX bytes : %d\r\n", RX_BYTES);
xil_printf("========================================\r\n");
status = init_gpio();
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
status = init_dma();
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
status = init_dma_interrupt();
if (status != XST_SUCCESS) {
return XST_FAILURE;
}
status = run_dma_once_by_interrupt();
if (status != XST_SUCCESS) {
XGpio_DiscreteWrite(&Gpio, GPIO_CHANNEL, 0x0U);
xil_printf("DMA test failed\r\n");
print_rx_buffer();
return XST_FAILURE;
}
print_rx_buffer();
xil_printf("\r\nTest finished\r\n");
while (1) {
}
return XST_SUCCESS;
}
通过顶层模块将测试数据产生模块和数据格式转换模块实例化,外部接入使能信号,拉高后模块进行工作。然后搭建仿真平台,通过仿真波形观察数据及其时序是否正确。当前仿真结果显示,每帧输出稳定为 21 拍,帧头固定为55AA55AA55AA55AA55AA55AA55AA55AA,符合当前帧格式设计。

运行Vitis裸机程序后,串口打印出来的数据如下所示,可以看出 PL 数据可经 DMA 正确写入 DDR,传输长度、数据顺序和中断完成均正常。

本文章使用limfx的vscode插件快速发布