在上一篇文章中简要介绍了AXI DMA的特性,本文将就这一重要的功能模块做详细的介绍,主要分为以下几个部分:
一、关于DMA的介绍
二、利用一个SG DMA的环通测试实验进一步了解DMA的应用
三、通过上板验证BD链表的创建
四、关于中断的一些内容补充
上一篇文章的链接如下:
基于 AN108 模块的ADC 采集以太网传输_Laid-back guy的博客-CSDN博客
通过学习ZYNQ开发可以得知PL 和 PS 的高效交互是 ZYNQ开发的重中之重,通常情况下我们需要将PL 端的大量数据实时送到 PS 端处理,或者将 PS 端处理结果实时送到 PL 端处理。在PS和PL之间的主要连接是通过一组9个AXI接口,每个接口有多个通道组成,其接口及相关描述如下:
其中AXI GP(General Purpose AXI)接口为通用接口,适用于PL和PS之间的中低速通信。通常情况下, ARM 可以利用M_AXI_GP 主机接口主动访问 PL 逻辑,ARM通过将PL映射到某个地址的方式来读写PL寄存器。
HP 口是 High-Performance Ports 的缩写,四个高性能 AXI 接口,带有FIFO缓冲来提供“批量”读写操作,并支持PL和PS中的存储器单元的高速率通信。
这两个端口在ZYNQ中的设置方式如下:
然而,通常情况下我们不会直连具有AXI4接口的IP核与AXI_HP接口来实现PL与PS端之间的数据交互,示例如下(图片摘自领航者ZYNQ之嵌入式SDK开发指南——第十七章):
显然,外设数据读入内存或者将内存传送到外设,一般都要通过 CPU 控制完成,如采用查询或中断方式。然而,对于高速、批量传输数据的情景下,虽然中断的方式可以提高CPU的利用率,但是PL端反复的发起中断再经CPU处理显然会十分影响CPU的效率与数据传输速度。此时,采用DMA的方式可以解决上述难点。
CPU 只需要提供地址和长度给 DMA,DMA 即可接管总线,访问内存,等 DMA 完成工作后,告知 CPU,交出总线控制权。即DMA的工作主要由下面三点构成
AXI DMA IP在AXI4内存映射与AXI4-Stream接口之间提供了高宽带直接存储访问。核心的功能如下图所示:
AXI DMA用到了三种总线:
总之,AXI_MM2S和AXI_S2MM是存储器端映射的AXI4总线,提供对存储器(DDR3)的访问。AXIS_MM2S 和 AXIS_S2MM 是 AXI4-streaming 总线,可以发送和接收连续的数据流,无需地址。
AXI DMA 提供 3 种模式,分别是 Direct Register 模式、Scatter/Gather 模式和 Cyclic DMA 模式。在上一节实验中采用了Scatter/Gather 模式,因此本文将着重介绍Scatter/Gather 模式。
Scatter/Gather DMA:相较于Direct Register 模式,允许将单个 DMA 事务中将数据传输到多个存储区域或从多个存储区域传输数据。SG DMA处理一个数据包可以将数据包分解为一个或者多个事务。举例说明:一个以太网的数据包由14字节报头以及1或多字节的有效负载构成。采用SG DMA模式的情况下,PS端的Application Project可以将BD(Buffer Descriptor,用于描述事务的对象)指向报头,将另一个 BD 指向有效负载,然后将二者作为单个不同的消息传输并将报头与数据保存在不同的内存区域。
当配置AXI DMA IP核,使能Enable Scatter Gather Engine时,会出现 M_AXI_SG 接口,用于读写链表
链表(BD Chain)以Buffer Descriptor(BD)为基本单元,每个Descriptor有13个寄存器,地址需要以 64 字节对齐,即0x00,0x40,0x80;
每个链表中所包含的信息如下表所示:
NXTDESC表示下一个BD的地址;BUFFER_ADDRESS为数据缓存的地址;Control 储存Frame的开始、结束、每个 Pachage的长度信息(以字节为单位);Status 存储错误、结束等信息, APP0~APP4 为用户使用空间(仅在打开 Status Control Stream 时有效,否则 DMA 不会抓取此信息)。
在 SG DMA 启动后,DMA 会通过 M_AXI_SG 抓取第一个 Buffer Descriptor,读取 BufferAddr,Control 等信息,等传输完 Control 设置的长度后,开始抓取下一个Descriptor信息。依次类推,直到最后一个 Descriptor。
需要注意的是,MM2S_CONTRL的TXSOF(Frame开始)和TXEOF(Frame 结束)需要软件设置。而S2MM_CONTRL 的 RXSOF 和 RXEOF 不需要软件设置,在接收数据后由 DMA 控制,这一点将在PS端的代码所体现。
SG DMA的使用流程
MM2S 端:
S2MM端:
在上一节的实验当中,我们仅利用了AXI DMA IP核的S2MM端口,本节的实验情景为上位机通过udp将数据发送至板卡,ARM端通过控制DMA实现与FPGA端的数据交互。其结构图如下图所示:
AXI互联模块(AXI Interconnect) 是一种用于连接和管理AXI总线设备的IP核。在FPGA和SoC设计中,AXI Interconnect常用于构建多个AXI总线设备之间的连接,实现数据的传输和交换。FPGA端硬件结构可以分为以下几个部分:
ZYNQ处理器通过AXI_GP接口连接到DMA与FPGA编、译码器的AXI_LITE接口,读写DMA和FPGA编、译码器内部寄存器。
DMA通过M_AXI接口(MM2S与S2MM)连接到AXI互联模块的S00_AXI接口,再通过互联模块的M00_AXI接口连接到ZYNQ的AXI_HP接口,从而实现DMA对DDR中数据的读写
DMA通过MM2S与S2MM端口与FPGA编译码器直连从而实现对其的数据读写
使能Enable Scatter Gather Engine时,使能Enable Write Channel
本次实验PS端的代码是在上一实验的基础上修改而来,本文着重分析DMA的部分并在代码的基础上分析PS驱动DMA的逻辑。
DMA初始化函数
XAxiDma_Initial(DMA_DEV_ID, S2MM_INTR_ID, &AxiDma, &XScuGicInstance) ;
该函数与上一实验中分析的并无二致,需要注意的是在本次环通实验中同样仅使能S2MM中断,关闭使能MM2S中断,即当PS端通过DMA的MM2S端口将数据发送至PL端的模块,经一定处理后,再通过S2MM回传至板卡,当数据回传结束后发出中断通知CPU一次数据环通完成。
DMA中断处理函数
void Dma_Interrupt_Handler(void *CallBackRef)
{
XAxiDma *XAxiDmaPtr ;
XAxiDmaPtr = (XAxiDma *) CallBackRef ;
int s2mm_sr = 0;
s2mm_sr = XAxiDma_IntrGetIrq(XAxiDmaPtr, XAXIDMA_DEVICE_TO_DMA) ;
//xil_printf("Interrupt Value is 0x%x\r\n", s2mm_sr) ;
if (s2mm_sr & XAXIDMA_IRQ_IOC_MASK)
{
/* Clear interrupt */
XAxiDma_IntrAckIrq(XAxiDmaPtr, XAXIDMA_IRQ_IOC_MASK,
XAXIDMA_DEVICE_TO_DMA) ;
s2mm_flag = 1;
}
}
因为上一实验中对DMA中断处理的理解模棱两可,因此本节展开分析DMA的中断处理函数。
该部分代码首先读取待处理中断并赋值给s2mm_sr,其中参数XAXIDMA_DEVICE_TO_DMA定义在xaxidma_hw.h文件当中,其值为0x01意为DMA的传输方向为s2mm
通过XAxiDma_IntrGetIrq()函数获取DMA中断并进行判决,判决条件为:
s2mm_sr & XAXIDMA_IRQ_IOC_MASK
其中XAXIDMA_IRQ_IOC_MASK是xaxidma_hw.h中的宏定义,其值为0x00001000,用于配置DMA中断的触发方式
IOC代表"Interrupt on Completion",即该宏指定了DMA传输完成时(成功将指定长度的数据从指定的地址由外设传输到内存,或反之)触发中断,发出一个中断信号通知CPU中断完成。
具体的 ,当获取的DMA此刻的中断值s2mm_sr&XAXIDMA_IRQ_IOC_MASK为1时,条件满足,令中断信号s2mm_flag置一,来通知CPU中断完成进行下一步的工作(本实验中为CPU将内存接收的数据通过以太网发送至上位机)
在main()函数当中,会通过以下函数建立DMA的S2MM中断S2MM_INTR_ID与其相应的中断处理函数Dma_Interrupt_Handler之间的关联
InterruptConnect(&XScuGicInstance,S2MM_INTR_ID,Dma_Interrupt_Handler, &AxiDma,0,3);
具体的:DMA的S2MM通道接收到数据并满足触发条件时,会触发S2MM中断,并跳转到指定的中断处理函数进行处理,处理过程如上述Dma_Interrupt_Handler所示
while(1){
if(dataTransfer_flag > 0){
memcpy(DmaTxBuffer, LLRs_Buffer, LLRs_NUM) ;
Xil_DCacheFlushRange((UINTPTR)DmaTxBuffer, LLRs_NUM);
/* Create BD and Start*/
CreateBdChain(BdChainBuffer, BD_COUNT, LLRs_NUM, (unsigned char*)DmaTxBuffer, TXPATH);//Create BD chain
XAxiDma_TX(BdChainBuffer, BD_COUNT, &AxiDma);//DMA_MM2S
/* Wait for times */
usleep(50);
/* Create BD and Start*/
Xil_DCacheInvalidateRange((UINTPTR)DmaRxBuffer, LLRs_NUM);
CreateBdChain(BdChainBuffer, BD_COUNT, LLRs_NUM, (unsigned char*)DmaRxBuffer, RXPATH);//Create BD chain
XAxiDma_RX(BdChainBuffer, BD_COUNT, &AxiDma);//DMA_MM2S /* Start ADC channel 0 */
usleep(50);
if (FrameLengthCurr > 0)
{
/* Check if DMA completed */
if (s2mm_flag >= 0)
{
int udp_len;
Xil_DCacheInvalidateRange((u32) DmaRxBuffer, FrameLengthCurr );
for(int i = 0 ; i < LLRs_NUM ; i++)
{
DmaBufferTmp_1[i] = (DmaRxBuffer_1[i]) ;
}
fg = 0;
/* Separate data into package */
for (int i = 0; i < FrameLengthCurr_1; i += 1024)
{
fg = fg +1 ;
char buf[2] = {0};
buf[0] = (fg >> 8 & 0xFF);
buf[1] = fg & 0xFF;
memcpy((unsigned char*)(TargetHeader + 5),&buf,2);
if ((i + 1024) > FrameLengthCurr_1)
udp_len = FrameLengthCurr - i;
else
udp_len = 1024;
send_adc_data((const char *) DmaBufferTmp + i, udp_len);
}
/* Clear DMA flag and frame length */
s2mm_flag = -1;
FrameLengthCurr = 0;
}
}
dataTransfer_flag = 0;
}
}
在start_udp(8080);函数接收到上位机发来的数据时,标志位dataTransfer_flag>0,满足条件的情况下启动DMA传输
首先DMA需将数据从内存传输至PL端通过以下函数创建BD链表
CreateBdChain(BdChainBuffer, BD_COUNT, LLRs_NUM, (unsigned char*)DmaTxBuffer, TXPATH);
具体函数可以参考ALINX的ZYNQ开发平台VITIS应用教程,该函数内部通过XAxiDma_BdWrite()函数生成了BdCount个数的BD链表,链表的结构如下:
XAxiDma_BdWrite(BdPtrCurr, XAXIDMA_BD_NDESC_OFFSET, (u32)BdPtrNext & XAXIDMA_DESC_LSB_MASK) ;
XAxiDma_BdWrite(BdPtrCurr, XAXIDMA_BD_BUFA_OFFSET, (u32)RxBufCurr) ;
首先按照上文所述BD链表的结构,通过写函数将下一描述符指针地址写入寄存器对应的偏移地址内,其中XAXIDMA_DESC_LSB_MASK为下一描述符指针地址的低32位地址掩码。通过写函数将数据缓存的地址写入寄存器相应的偏移地址内。
此后需注意方向为TXPATH或RXPATH,即MM2S或S2MM(上文已提及二者的Control 配置方式不同)
if (Direction == TXPATH)
{
if (i == 0)
XAxiDma_BdWrite(BdPtrCurr, XAXIDMA_BD_CTRL_LEN_OFFSET, (u32)(TotalByteLen/BdCount) | XAXIDMA_BD_CTRL_TXSOF_MASK) ;
else if (i == BdCount - 1)
XAxiDma_BdWrite(BdPtrCurr, XAXIDMA_BD_CTRL_LEN_OFFSET, (u32)(TotalByteLen/BdCount) | XAXIDMA_BD_CTRL_TXEOF_MASK) ;
else
XAxiDma_BdWrite(BdPtrCurr, XAXIDMA_BD_CTRL_LEN_OFFSET, (u32)(TotalByteLen/BdCount) ) ;
}
else
XAxiDma_BdWrite(BdPtrCurr, XAXIDMA_BD_CTRL_LEN_OFFSET, (u32)(TotalByteLen/BdCount) ) ;
当方向为TXPATH时,需通过写函数配置Frame的开始、结束及每个BD传输的Pachage长度
当方向为RXPATH时,仅需配置其每个BD传输的Pachage长度
创建BD链表后,通过传输函数将数据从内存传输至PL端
void XAxiDma_TX(u32* BdChainBuffer, u16 BdCount, XAxiDma* AxiDma)//DMA_MM2S
{
/* Clear BD Status */
Bd_StatusClr(BdChainBuffer, BdCount);
/* start DMA translation from DDR3 to FIFO channel */
Bd_Start(BdChainBuffer, BdCount, AxiDma, TXPATH);
}
之后,DMA需将数据从PL端传输至内存,其创建BD链表及传输数据函数除方向外并无差别,因此不再重复叙述。
为更进一步理解BD链表的创建,通过 Debug 查看 memory 信息
首先Run Debug进入Debug界面,并在第一次创建BD链表并启动XAxiDma_TX()函数后设置断点
点击Resume按钮,运行至断点处
此时程序会运行至while(1)循环内并一直判决 dataTransfer_flag > 0,通过上位机发送数据至板卡使dataTransfer_flag > 0此时程序运行至断点处
将鼠标移至BdChainBuffer_1处可显示其地址。
在 Memory 窗口点击添加按钮,填入链表地址,可查看BD内容如下:
可以看到第一个 Descriptor 的 NEXDESC 指向下一个 Descriptor 地址也就是 0x10304000,第 一个 BUFFER_ADDRESS 为 0x08302840,即DmaTxBuffer_1的地址,CONTROL 为0x8000100,0x0000100即每个BD传输的package大小,在本实验中即LLR_NUM/BD_COUNT,在本实验中设置缓存空间是连续的,0x8302840+0x00000100=0x8302940,也就是下一个的 BUFFER_ADDRESS。最后一个 Descriptor 的 NEXDESC 指向第一个 Descriptor 的地址
在上文中分析了PS端如何处理DMA的中断信号,其中中断信号的掩码通常用于屏蔽或启动特定的中断信号,位掩码(bit mask),其中每个位对应一个特定的中断信号。通过设置或清除位掩码中的相应位,可以控制哪些中断信号被屏蔽(禁用)或启用(允许)。
在编程中,使用掩码的常见操作是使用位逻辑运算符(如按位与、按位或)来设置或清除掩码中的位。以下是一些常见的操作示例:
1.设置中断信号的掩码: mask |= (1 << interrupt_number);
通过按位或运算符(|)将一个特定中断信号的位设置为1,表示启用该中断信号。
2.清除中断信号的掩码: mask &= ~(1 << interrupt_number);
通过按位与运算符(&)将一个特定中断信号的位设置为0,表示屏蔽(禁用)该中断信号。
3.检查中断信号是否启用: if (mask & (1 << interrupt_number)) { // 中断信号已启用 }
使用按位与运算符(&)检查中断信号的位是否为1,判断中断信号是否启用。