半小时速通ZYNQ PL端发送数据到PS DDR

文章目录[x]
  1. 1:前言
  2. 2:明确数据交互方式
  3. 3:DataMover
  4. 3.1:命令接口与接口时序
  5. 3.2:状态接口与接口时序
  6. 3.3:DataMoverS2MM通道实操
  7. 3.4:DMA_FRM_GEN设计
  8. 3.5:写操作
  9. 3.6:PS访问DMA发送的数据
  10. 3.7:DataMover吞吐计算
  11. 4:总结
  12. 5:祝君好运,乐享其中。 Good Luck & Have Fun
  13. 6:参考文档

本文章来自网友朔望,我为代发

前言

ZYNQ开发过程中 PL与PS DDR数据交互属于老生常谈的问题了,国内销售开发板的通常是介绍一些开源的DMA实现,这种方式在使用VIVADO BLOCK DESIGN 时会比较麻烦,为此本文介绍XILINX提供的官方底层DMA AXI DataMover的S2MM通道使用流程。

明确数据交互方式

在正式逻辑设计开始前,需要嵌入式工程师与FPGA工程师共同确定数据的交互方式,具体根据数据路径和数据率量大小决定。

  1. 对于PS到PL侧的少量数据交互的情况,通常考虑直接外挂PL侧AXI FULL/LITE总线从机,PS通过AXI GP接口直接对特定地址区域进行读写。
  2. 对于PS到PL侧的大量数据通常考虑使用使用ZYNQ内部内建DMA将数据直接从DDR搬运到PL侧。

对于PS到PL的数据路径通常是由PS进行控制,对于嵌入式开发较为友好,整体开发难度不高。但由于ZYNQ使用的CORTEX-A9处理器是一颗多级流水的应用处理器,因此在响应延迟差于传统的嵌入式处理(CORTEX-M3,M4)。

在大多数PL访问PS侧DDR应用场景下,都是PL读写DDR中连续的内存区域,有高带宽的特性。由于这个特性PL到PS的数据交互通常都是使用PL侧DMA来进行,在这种应用场景下DMA通常由PL侧逻辑控制,不过在对响应要求不苛刻的情况下也可由PS控制DMA。

DataMover

本文主要目的是介绍XILINX提供的底层DMA IP:AXI DataMover,该DMA是XILINX提供的一系列复杂DMA IP的基础设施。

  1. AXI MCDMA底层是DataMover搭配额外控制逻辑。

MCDMA_FIGURE

  1. 同样的AXI VDMA也是在DataMover上实现的。

VDMA_FIGURE

AXI DataMover拥有两个数据通道,分别是:

  • MM2S 即内存映射数据转换为流式数据,实现AXI-FULL协议到AXI-STREAM协议的转换;
  • S2MM 即流式数据转换为内存映射数据,实现AXI-STREAM协议到AXI-FULL协议的转换。

DataMover_datapath

图1

本文主要介绍S2MM通道,从图1可以看出DataMover的S2MM通道是其内部的写通道。
写通道主要由命令、状态逻辑与写引擎构成,其中:

  • 命令逻辑是一个FIFO,也就是说里面可以实现一个DMA传输队列;
  • 状态逻辑是一个FIFO,里面放有上次DMA传输的状态报告;
  • 写引擎,写引擎负责执行命令、协议转换。

命令接口与接口时序

在字节传输模式(BTT MODE)下,DataMover的命令由如下图2所示
datamover_command_fig

图2

在一般应用场景下通常不使用xCACHE与xUSER,故该部分无效。

在不使用xCACHE和xUSER的情况下DataMover的指令长度为N+40位。根据手册描述,N是内存映射总线总线地址的宽度,无论如何都必须是8的倍数,若不为8的倍数时需要向上取整,手册上举例:当总线宽度配置位33位时需要向上取整拓展到40位,多出来的高位在IP内部会忽略。

命令字节描述:

  • TAG :命令标识,用于标记当前命令,便于后续出问题时在状态总线上排查问题。
  • SADDR:起始地址,指示数据从哪个具体的地址开始。
  • DRR:MM2S时需要,本文讲S2MM,该部分不做描述。
  • EOF:指示本次传输是否是最后一次传输。
  • DSA:字节对齐功能,S2MM应用不需要使用。
  • TYPE:通常为1,实现递增,不然DMA只会向起始地址写数据。
  • BTT:在字节传输模式下指示一次发送多少数据。最小为1,最大为8MiB。

datamover_cmd_if_fig

图3

从图3可以看出命令接口时序是一种简化的AXIS接口,去掉了tkeep与tlast信号。当主从握手信号就绪时总线上的命令送入命令FIFO,下面代码是对该接口的一种简单实现。

//send command to command interface
always_ff@(posedge clk) begin
    if(srstn == 1'b0) begin
        m_axis_s2mm_cmd_tvalid <= 1'b0;
        m_axis_s2mm_cmd_tdata <= 72'd0;
    end
    else if(trans_start_pos) begin
        m_axis_s2mm_cmd_tvalid <= 1'b1;
        //enable increase,enable eof,disable DSA and DRR,no TAG
        m_axis_s2mm_cmd_tdata <= {9'd0,DEST_ADDR,2'b01,6'd0,1'b1,trans_byte};
    end
    else if(m_axis_s2mm_cmd_tready) begin
        m_axis_s2mm_cmd_tvalid <= 1'b0;
        m_axis_s2mm_cmd_tdata <= 72'd0;
    end
end

状态接口与接口时序

状态接口用于指示上次DMA传输事务的结果,可供外部用户逻辑对DMA状态将进行监控。

datamover_sts_fig

图4

状态字节描述:

  • OKAY:拉高指示一次成功的DMA传输,拉低表示失败。
  • SLVERR:从机错误,拉高从机内部错误,拉低表示没有错误。
  • DECERR:解码错误,通常是地址错误,拉低表示正常,拉高表示错误。
  • INTERR:内部错误,要求DMA发0个数据,tlast信号到来的时间不对等均会引发内部错误,拉低表示正常,拉高表示错误。
  • TAG:对应命令中的TAG。

datamover_sts_if_fig

图5

图5是状态接口的时序,同样也是AXIS总线接口,除了上图中的信号,DataMover还额外提供s2mm_err/mm2s_err信号,当DMA出现错误时对应信号就会拉高。

需要额外注意的是当DataMover发生错误后,只能通过对DataMover进行复位才能进行后续数据传输操作。

DataMoverS2MM通道实操

理论部分讲完了下面开始DataMover的实操,本章节以一个简单的dma单向吞吐测试工程为例进行实操讲解。

顶层互联概述

design_fig

图6

图6是整个设计的顶层互联,其中:

  • VIO负责设置DMA_FRM_GEN模块的初值和提供DMA传输信号。
  • DMA_FRM_GEN负责生成AXIS数据流和DataMover命令流送入DataMover对应接口。
  • design1_wrapper是block design的对外封装文件。

    block design 概述

bd_fig

图7

图7是图6中wrapper的内部,其为一个简单的DataMoverS2MM IP测试设计。其数据路径为:

  • DMA_FRM_GEN模块生成的数据通过axis总线向外输出。
  • DMA_FRM_GEN的AXIS接口接到图7的s_axis_fifo端口随后由位宽转换模块将DMA_FRM_GEN模块的32位AXIS总线转换为64位。
  • 位宽拓展后的数据送入到FIFO模块缓存。
  • 当DataMover收到命令后将 FIFO数据通过PS的AXI HP接口送入到PS侧DDR中。

zynq_inter_fig

图8

通常PL DMA都接在PS的AXI HP端口,为啥不用AXI GP端口呢?要问AXI GP端口支持不支持访问PS DDR控制器,这当然是支持的,但两个接口以下差别让高速数据传输任务更加青睐AXI HP接口:

  • AXI HP接口数据位宽可选为32/64bit,而AXI GP最大只有32bit,也就是说在相同工作时钟下,AXI HP的带宽最大可以是AXI GP接口的一倍。
  • 为了提高吞吐每个AXI HP接口内部均有带有1KB的读写FIFO,而AXI GP没有带FIFO。在高速数据流到来时且DDR控制器被其它设备使用时,AXI GP接口由于没有缓存能力,会出现阻塞,AXI HP带有FIFO可以先缓存,等待DDR空闲后将数据写进去。

ip_cfg_fig

图9

DataMoverIP设置。

  • 由于只测试s2mm故只选择enable s2mm;
  • channel type选择full,以支持更大的猝发传输次数;
  • memory map data width 与 stream data width 均选择64位,便于测试dma最大吞吐;
  • maximum burst size选择256,以提高总线效率;
  • width of btt field根据需求选择,本次选择16即可;
  • advanced页面设置不需要修改,保持默认。

DMA_FRM_GEN设计

本次设计是为了快速测试DataMoverS2MM吞吐,故DMA_FRM_GEN模块在收到VIO发出的起始信号后向DMA发送指定数量的数据即可,下面是该模块的代码。

module dma_frm_gen
#(
    parameter TRANS_BEATS_NUM = 2048,
    parameter TDATA_WIDTH = 32,
    parameter DEST_ADDR = 32'h1FFF_0000
)
(
    input       logic           clk,
    input       logic           srstn,

    input       logic   [7:0]   init_val,
    input       logic           trans_start,

    output      logic [TDATA_WIDTH-1:0]m_axis_fifo_tdata,
    output      logic [(TDATA_WIDTH/8)-1:0]m_axis_fifo_tkeep,
    output      logic m_axis_fifo_tlast,
    input       logic m_axis_fifo_tready,
    output      logic m_axis_fifo_tvalid,

    output      logic [71:0]m_axis_s2mm_cmd_tdata,
    input       logic m_axis_s2mm_cmd_tready,
    output      logic m_axis_s2mm_cmd_tvalid
    );

localparam IDLE = 0;
localparam EXE  = 1;
localparam DONE = 2;
localparam BYTE_NUM = (TDATA_WIDTH/8);

logic [15:0] trans_cnt;
logic [1:0] trans_state;

logic [1:0] trans_start_ff;
logic trans_start_pos;
logic [22:0] trans_byte;

logic [7:0] trans_data;

always_ff@(posedge clk) begin
    trans_byte <= TRANS_BEATS_NUM*BYTE_NUM;
end

// generate start posedge signal
always_ff@(posedge clk) begin
    if(srstn == 1'b0) begin
        trans_start_ff <= 2'b00;
    end
    else begin
        trans_start_ff <= {trans_start_ff[0],trans_start};
    end
end

always_ff@(posedge clk) begin
    if(srstn == 1'b0) begin
        trans_start_pos <= 1'b0;
    end
    else begin
        trans_start_pos <= (~trans_start_ff[1]) && trans_start_ff[0];
    end
end

always_ff@(posedge clk) begin
    if(srstn == 1'b0) begin
        trans_cnt <= 16'b0;
        trans_state <= IDLE;
        trans_cnt <= 16'd0;
        m_axis_fifo_tdata <= 32'd0;
    end
    else begin
        case (trans_state)
        IDLE: begin
            if(trans_start_pos) begin
                m_axis_fifo_tdata <= {4{init_val}};
                trans_state <= EXE;
                trans_cnt <= 16'd0;
            end
            else begin
                trans_state <= IDLE;
                trans_cnt <= 16'd0;
            end
        end

        EXE: begin //when slv ready 
            if(trans_cnt < TRANS_BEATS_NUM) begin
                if(m_axis_fifo_tready) begin
                    if(trans_cnt == 0) begin
                        m_axis_fifo_tdata <= {BYTE_NUM{trans_data}};
                    end
                    else begin
                        m_axis_fifo_tdata <= {BYTE_NUM{trans_data}};
                    end
                    trans_cnt <= trans_cnt + 1'b1;
                end
            end
            else begin
                trans_state <= DONE;
            end

        end
        DONE: begin
            trans_state <= IDLE;
            trans_cnt <= 16'd0;
        end
            default: trans_state <= IDLE;
        endcase
    end
end

always_ff@(posedge clk) begin
    if(srstn == 1'b0) begin
        trans_data <= 8'd0;
    end
    else if(trans_start_pos) begin
        trans_data <= init_val;
    end
    else if(trans_state == EXE && m_axis_fifo_tready) begin
        trans_data <= trans_data + 1'b1;
    end
end

// generate tvalid signal for axis bus
always_ff @(posedge clk ) begin
    if(srstn == 1'b0) begin
        m_axis_fifo_tvalid <= 1'b0;
    end
    else begin 
        if(trans_state == EXE) begin
            if(trans_cnt < TRANS_BEATS_NUM) begin
                m_axis_fifo_tvalid <= 1'b1;
            end
            else begin
                m_axis_fifo_tvalid <= 1'b0;
            end
        end
        else begin
            m_axis_fifo_tvalid <= 1'b0;
        end
    end
end

//generate tlast signal for last axis transaction event
always_ff @( posedge clk ) begin
    if(srstn == 1'b0) begin
        m_axis_fifo_tlast <= 1'b0;
    end
    else begin 
        if(trans_state == EXE) begin
            if(trans_cnt == TRANS_BEATS_NUM-1) begin
                m_axis_fifo_tlast <= 1'b1;
            end
            else begin
                m_axis_fifo_tlast <= 1'b0;
            end
        end
        else begin
            m_axis_fifo_tlast <= 1'b0;
        end
    end
end

always_ff @( posedge clk ) begin
    if(srstn == 1'b0) begin
        m_axis_fifo_tkeep <= 4'b0000;
    end
    else begin 
        m_axis_fifo_tkeep <= 4'b1111;
    end
end

//send command to command interface
always_ff@(posedge clk) begin
    if(srstn == 1'b0) begin
        m_axis_s2mm_cmd_tvalid <= 1'b0;
        m_axis_s2mm_cmd_tdata <= 72'd0;
    end
    else if(trans_start_pos) begin
        m_axis_s2mm_cmd_tvalid <= 1'b1;
        //enable increase,enable eof,disable DSA and DRR,no TAG
        m_axis_s2mm_cmd_tdata <= {9'd0,DEST_ADDR,2'b01,6'd0,1'b1,trans_byte};
    end
    else if(m_axis_s2mm_cmd_tready) begin
        m_axis_s2mm_cmd_tvalid <= 1'b0;
        m_axis_s2mm_cmd_tdata <= 72'd0;
    end
end

endmodule

写操作

在block design中将相关AXI总线信号添加到ILA即可观察总线行为。
测试中设置DMA传输8KiB数据,则dma_frm_gen需要输出2048拍数据。
axis_fig

图10

图10为dma_frm_gen模块对外输出的情况,由于逻辑资源有限只能抓取一部分接口波形。

ila_axi_full_fig

图11

图11展示了DataMover向AXI HP接口写数据的情况,由于前面将总线从32bit拓宽到64bit,此处总线带宽翻倍,相应的传输相同大小的数据所需时间为32bit总线的一半,故ILA可以抓全信号。

我们可以看到DataMover将8KiB数据拆分为4次burst进行传输,根据之前DataMoverIP设置,单次burst为256拍数据,数据位宽为64位,则一次burst可以传输256x64/8 = 2048字节数据=2KiB,2048x4=8KiB,与ila抓到的信号情况一致。

dma_err_free_fig

图12

图12展示了状态总线报告的DMA传输状态,可以看到传输正常,错误信号没有拉高。

PS访问DMA发送的数据

相较于逻辑端费力的操作,PS侧访问DDR数据较为容易,使用for循环加库函数即可访问,但需要注意:

  • 缓存一致性问题;

对于缓存一致性问题,请使用Xil_DCacheInvalidate();函数更新cache。
下面上PS裸机代码。

#include <stdio.h>
#include "platform.h"
#include "xil_printf.h"
#include "xil_io.h"
#include "xil_cache.h"

volatile unsigned int rd;
volatile unsigned int rd_cnt;

int main()
{
    init_platform();
    while(1) {
        Xil_DCacheInvalidate();
        for (rd_cnt = 0; rd_cnt < 2048;rd_cnt = rd_cnt + 1) {
            rd = Xil_In32(0x1FFF0000 + rd_cnt*4);
            xil_printf("Address offset 0x%08x data in hex is 0x%08x \n\r",rd_cnt*4,rd);
        }
    }
    cleanup_platform();
    return 0;
}

0x00_uart_fig

图13 使用PS输出DMA搬运的数据,VIO设置初始值为0x00

0x01_uart_fig

图14 使用PS输出DMA搬运的数据,VIO设置初始值为0x01

DataMover吞吐计算

在DataMover到PS DDR之间传输8KiB数据花费了1026个时钟周期,而理论最少时钟周期为1024。由于整个系统运行在125MHz,一个时钟周期为8ns,则DataMover理论数据率为8KiB/(1024x8)=1000MiB/S,实际速率为8KiB/(1026x8)=998MiB/S,效率为99.8%。当然以上估计存在数据量过少,测试环境过于理想的问题。实际DMA使用过程中并不会有这么理想的流式数据以及大容量fifo作为dma缓存。

总结

本文简单介绍了使用AXI DataMover作为DMA将PL侧数据送到PS DDR侧的方法,相较于xilinx提供的配置繁杂的AXI DMA,AXI CDMA等DMA,DataMover使用简单且快速,适合简单流式数据传输如(高速ADC采样数据,SDR上下行数据流)。对于需要DMA完成复杂地址映射的场景, DataMover使用起来就较为别扭,此时推荐使用其它DMA。

此外AXI DataMover适合逻辑侧直接控制,对于PS部分来讲该部分是完全透明的,只需要定时访问DDR特定区域即可,能够降低嵌入式的工作量,不过这种方式对于逻辑端来讲就不太友好了。

最后在ZYNQ应用过程中应该在设计起始阶段就应该约定好PS与PL数据交互方式并以文档方式记录在案,这样设计更加清晰,更重要是可以减少无所谓的时间浪费。

祝君好运,乐享其中。 Good Luck & Have Fun

参考文档

点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据