参考资料:
Xilinx官方参考文档:PG021_axi_dma、UG585_zynq_7000_TRM等
AXI DMA开发 http://www.fpgadeveloper.com/2014/08/using-the-axi-dma-in-vivado.html
黑金教程 course_s2_ALINX ZYNQ开发平台SDK应用教程V2.02.pdf
米联客教程 S03_CH01_AXI_DMA_LOOP(视频) https://v.qq.com/x/page/e0349oqwa27.html
本文介绍在进行AXI_DMA_LOOP测试时遇到的问题和走的弯路,记录下。
前言
之前一直做FPGA开发,在网络方向的,主要是二层、三层交换方向,采用的硬件架构也是分离式的CPU+FPGA的架构。想想zynq也出了好久了,一直想研究的,无奈工作太忙加上自己太懒一直耽搁了,既然有时间,说搞就搞。想想之前项目的应用环境,FPGA与CPU间是通过以太网通信。ZYNQ里集成了ARM,于是便先搞清楚FPGA与ARM间的通信方式。
PS(ARM)与PL(FPGA逻辑)间采用比较多的通信方式就是通过AXI总线方式进行通信,具体AXI协议不做介绍,网上资料很多。
AXI4-Lite有轻量级,结构简单的特点,适合小批量数据、简单控制场合。不支持批量传输,读写时一次只能读写一个字(32bit),主要用于访问一些低速外设和外设的控制。
AXI4接口和AXI-Lite差不多,只是增加了一项功能就是批量传输,可以连续对一片地址进行一次性读写。也就是说具有数据读写的burst 功能。
AXI4-Stream是一种连续流接口,不需要地址线(类似FIFO),对于这类IP,ARM不能通过上面的内存映射方式控制(FIFO没有地址线),必须有一个转换装置,例如AXI-DMA模块来实现内存映射到流式接口的转换。AXI4-Stream本质都是针对数据流构建的数据通路。
AXI4-Lite和AXI4均采用内存映射控制方式,即ARM将用户自定义IP编入某一地址空间进行访问,读写时就像在读写自己的片内RAM,编程也方便,开发难度较低。代价就是资源占用过多,需要额外的读写地址线、读写数据线以及应答信号等。
AXI4-Stream是一种连续流接口,不需要地址线(类似FIFO),对于这类IP,ARM不能通过上面的内存映射方式控制(FIFO没有地址线),必须有一个转换装置,例如AXI-DMA模块来实现内存映射到流式接口的转换。AXI4-Stream本质都是针对数据流构建的数据通路。
因为以前做的二层、三层交换也是数据流驱动架构,并且原来做以太网的接口方式也是AXI4-Stream方式,对AXI4-Stream比较熟悉,因此先研究AXI4-Stream接口。
几个常用的AXI_stream接口的IP介绍:
做嵌入式驱动的经常会用到DMA,先从DMA入手吧。
简单介绍zynq的AXI_DMA模块,该模块用到了三种总线,AXI4_Lite用于对寄存器进行配置(这个地方之前没了解清楚,吃了亏,后面介绍弯路),AXI4 Memory Map用于与内存交互,在此模块又分立出了AXI4 Memory Map Read和AXI4 Memory Map Write两个接口,又分别叫做M_AXI_MM2S和M_AXI_S2MM。AXI4_Stream接口用于对用户逻辑进行通信,其中AXI4 Stream Master(MM2S)是PS to PL方向,AMI4 Stream Slave(S2MM)是PL to PS方向。
下面实例学习:开发环境 vivado 2017.4 SDK 2017.4
AXI_DMA_LOOP的硬件框图:
这里为了与以前的项目对应,FIFO的宽度定义为8bit(vivado 2017.4版本AXI DMA有个BUG后面介绍),采取中断触发方式。在vivado中建立系统block(方法网上自查)
导入SDK后发现(只有dmaps驱动示例,没有axidma的驱动示例):
错误案例一总结:AXI DMA的数据传输虽然是用AXI-HP的接口,但是AXI DMA的配置接口S_AXI_Lite用的是AXI-GP接口,所以AXI-GP接口不能擅自去掉。
AXI DMA定制Wrire Channl默认32位,但是在进行连接时可以自适应修改宽度
官方手册里说明
AXI4_Stream Data fifo定制fifo的位宽为8位
正常版本validate后位宽就该改为一样,但vivado2017.4不能自适应修改并报警告:
而正常版本validate后:
定制zynq时PL时钟选择两个,一个低速时钟给DMA和FIFO使用,另一个高速时钟作为debug的采样时钟,用来分析axi4_stream接口时序。
系统硬件图,在AXI4_stream相关地方加了debug信号:
生成bit文件导入SDK,并建立hello world实例代码:
驱动部分编写fd_dma_intr.c用来处理中断和dma的初始化,中断处理等,在helloworld.c中编写测试部分程序,测试方式是通过arm发送16组数据通过DMA发送给PL部分,在从PL部分读回这16组数据,在串口上打印进行比对,确认PS与PL通过DMA数据交互正常。源码如下(大量参考官方驱动axidma,非常有用):
先了解系统地址分配(其中DDR为 0x00100000—0x3fffffff)
helloworld.c
/*
* helloworld.c: simple test application
*/
#include
#include "platform.h"
#include "xil_printf.h"
#include "fd_dma_intr.h"
//相关宏定义在官方驱动里有说明
#define DMA_DEV_ID XPAR_AXIDMA_0_DEVICE_ID
#define INTC_DEVICE_ID XPAR_SCUGIC_SINGLE_DEVICE_ID
#define RX_INTR_ID XPAR_FABRIC_AXIDMA_0_S2MM_INTROUT_VEC_ID
#define TX_INTR_ID XPAR_FABRIC_AXIDMA_0_MM2S_INTROUT_VEC_ID
//要在系统DDR地址范围内
#define MEM_BASE_ADDR 0x01000000
//TX:PS TO DMA TO PL
//RX:PL TO DMA TO PS
//定义收发缓存地址
#define TX_BUFFER_BASE (MEM_BASE_ADDR + 0x00100000)
#define RX_BUFFER_BASE (MEM_BASE_ADDR + 0x00300000)
#define RX_BUFFER_HIGH (MEM_BASE_ADDR + 0x004FFFFF)
//每组数据长度为16个字节
#define MAX_PKT_LEN 0x10
/* Device instance definitions */
static XAxiDma AxiDma; /* Instance of the XAxiDma */
static XScuGic Intc; /* Instance of the Interrupt Controller */
/* Flags interrupt handlers use to notify the application context the events. */
int axi_dam_test();
int main()
{
int Status;
init_platform();
//DMA 初始化
Status = DMA_Init(&AxiDma, DMA_DEV_ID);
if(Status != XST_SUCCESS){
print("DMA Initial Failed!\n\r");
}
//配置系统中断
Status = SetupIntrSystem(&Intc, &AxiDma, TX_INTR_ID, RX_INTR_ID);
if(Status != XST_SUCCESS){
print("Interrupt Config Failed!\n\r");
}
//使能DMA中断
DMA_Intr_Enable(&AxiDma);
//测试函数
axi_dam_test();
//关闭DMA中断
DMA_Intr_Disable(&AxiDma);
cleanup_platform();
return 0;
}
int axi_dam_test()
{
u8 *TxBufferPtr;
u8 *RxBufferPtr;
u8 Value;
int i;
int Status;
u8 index;
int delay;
TxBufferPtr = (u8 *)TX_BUFFER_BASE; //PS TO DMA TO PL
RxBufferPtr = (u8 *)RX_BUFFER_BASE; //PL TO DMA TO PS
/* Initialize flags before start transfer test */
TxDone = 0;
RxDone = 0;
Error = 0;
printf("Start DMA test!\n\n\r");
//测试开始前内存相关地方清零
for(i=0; i
fd_dma_intr.c(按照官方驱动一步步来即可)
/***************************** Include Files *********************************/
#include "fd_dma_intr.h"
volatile int TxDone;
volatile int RxDone;
volatile int Error;
/*****************************************************************************/
/*
*
* This is the DMA TX Interrupt handler function.
*
* It gets the interrupt status from the hardware, acknowledges it, and if any
* error happens, it resets the hardware. Otherwise, if a completion interrupt
* is present, then sets the TxDone.flag
*
* @param Callback is a pointer to TX channel of the DMA engine.
*
* @return None.
*
* @note None.
*
******************************************************************************/
static void TxIntrHandler(void *Callback)
{
u32 IrqStatus;
int TimeOut;
XAxiDma *AxiDmaInst = (XAxiDma *)Callback;
/* Read pending interrupts */
IrqStatus = XAxiDma_IntrGetIrq(AxiDmaInst, XAXIDMA_DMA_TO_DEVICE);
/* Acknowledge pending interrupts */
XAxiDma_IntrAckIrq(AxiDmaInst, IrqStatus, XAXIDMA_DMA_TO_DEVICE);
/*
* If no interrupt is asserted, we do not do anything
*/
if (!(IrqStatus & XAXIDMA_IRQ_ALL_MASK)) {
return;
}
/*
* If error interrupt is asserted, raise error flag, reset the
* hardware to recover from the error, and return with no further
* processing.
*/
if ((IrqStatus & XAXIDMA_IRQ_ERROR_MASK)) {
Error = 1;
/*
* Reset should never fail for transmit channel
*/
XAxiDma_Reset(AxiDmaInst);
TimeOut = RESET_TIMEOUT_COUNTER;
while (TimeOut) {
if (XAxiDma_ResetIsDone(AxiDmaInst)) {
break;
}
TimeOut -= 1;
}
return;
}
/*
* If Completion interrupt is asserted, then set the TxDone flag
*/
if ((IrqStatus & XAXIDMA_IRQ_IOC_MASK)) {
TxDone = 1;
}
}
/*****************************************************************************/
/*
*
* This is the DMA RX interrupt handler function
*
* It gets the interrupt status from the hardware, acknowledges it, and if any
* error happens, it resets the hardware. Otherwise, if a completion interrupt
* is present, then it sets the RxDone flag.
*
* @param Callback is a pointer to RX channel of the DMA engine.
*
* @return None.
*
* @note None.
*
******************************************************************************/
static void RxIntrHandler(void *Callback)
{
u32 IrqStatus;
int TimeOut;
XAxiDma *AxiDmaInst = (XAxiDma *)Callback;
/* Read pending interrupts */
IrqStatus = XAxiDma_IntrGetIrq(AxiDmaInst, XAXIDMA_DEVICE_TO_DMA);
/* Acknowledge pending interrupts */
XAxiDma_IntrAckIrq(AxiDmaInst, IrqStatus, XAXIDMA_DEVICE_TO_DMA);
/*
* If no interrupt is asserted, we do not do anything
*/
if (!(IrqStatus & XAXIDMA_IRQ_ALL_MASK)) {
return;
}
/*
* If error interrupt is asserted, raise error flag, reset the
* hardware to recover from the error, and return with no further
* processing.
*/
if ((IrqStatus & XAXIDMA_IRQ_ERROR_MASK)) {
Error = 1;
/* Reset could fail and hang
* NEED a way to handle this or do not call it??
*/
XAxiDma_Reset(AxiDmaInst);
TimeOut = RESET_TIMEOUT_COUNTER;
while (TimeOut) {
if(XAxiDma_ResetIsDone(AxiDmaInst)) {
break;
}
TimeOut -= 1;
}
return;
}
/*
* If completion interrupt is asserted, then set RxDone flag
*/
if ((IrqStatus & XAXIDMA_IRQ_IOC_MASK)) {
RxDone = 1;
}
}
//系统中断配置
int SetupIntrSystem(XScuGic * IntcPtr, XAxiDma * AxiDmaPtr, u16 TxIntrId, u16 RxIntrId)
{
int Status;
XScuGic_Config *IntcConfig; /* The configuration parameters of the controller */
//Initialize the interrupt controller driver so that it is ready to use.
IntcConfig = XScuGic_LookupConfig(INTC_DEVICE_ID);
if (NULL == IntcConfig) {
return XST_FAILURE;
}
Status = XScuGic_CfgInitialize(IntcPtr, IntcConfig,
IntcConfig->CpuBaseAddress);
if (Status != XST_SUCCESS) {
return XST_FAILURE;
}
XScuGic_SetPriorityTriggerType(IntcPtr, TxIntrId, 0xA0, 0x3);
XScuGic_SetPriorityTriggerType(IntcPtr, RxIntrId, 0xA0, 0x3);
/*
* Connect the device driver handler that will be called when an
* interrupt for the device occurs, the handler defined above performs
* the specific interrupt processing for the device.
*/
Status = XScuGic_Connect(IntcPtr, TxIntrId,
(Xil_InterruptHandler)TxIntrHandler,
AxiDmaPtr);
if (Status != XST_SUCCESS) {
return Status;
}
Status = XScuGic_Connect(IntcPtr, RxIntrId,
(Xil_InterruptHandler)RxIntrHandler,
AxiDmaPtr);
if (Status != XST_SUCCESS) {
return Status;
}
XScuGic_Enable(IntcPtr, TxIntrId);
XScuGic_Enable(IntcPtr, RxIntrId);
/* Enable interrupts from the hardware */
Xil_ExceptionInit();
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT,
(Xil_ExceptionHandler)XScuGic_InterruptHandler,
(void *)IntcPtr);
Xil_ExceptionEnable();
return XST_SUCCESS;
}
int DMA_Intr_Enable(XAxiDma * AxiDmaPtr)
{
/* Disable all interrupts before setup */
XAxiDma_IntrDisable(AxiDmaPtr, XAXIDMA_IRQ_ALL_MASK,
XAXIDMA_DMA_TO_DEVICE);
XAxiDma_IntrDisable(AxiDmaPtr, XAXIDMA_IRQ_ALL_MASK,
XAXIDMA_DEVICE_TO_DMA);
/* Enable all interrupts */
XAxiDma_IntrEnable(AxiDmaPtr, XAXIDMA_IRQ_ALL_MASK,
XAXIDMA_DMA_TO_DEVICE);
XAxiDma_IntrEnable(AxiDmaPtr, XAXIDMA_IRQ_ALL_MASK,
XAXIDMA_DEVICE_TO_DMA);
}
int DMA_Intr_Disable(XAxiDma * AxiDmaPtr)
{
/* Disable all interrupts before setup */
XAxiDma_IntrDisable(AxiDmaPtr, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DMA_TO_DEVICE);
XAxiDma_IntrDisable(AxiDmaPtr, XAXIDMA_IRQ_ALL_MASK, XAXIDMA_DEVICE_TO_DMA);
}
//DMA 设备初始化
int DMA_Init(XAxiDma *DMAPtr,u32 DeviceId)
{
int Status;
XAxiDma_Config *Config;
Config = XAxiDma_LookupConfig(DeviceId);
if (!Config) {
xil_printf("No config found for %d\r\n", DeviceId);
return XST_FAILURE;
}
/* Initialize DMA engine */
Status = XAxiDma_CfgInitialize(DMAPtr, Config);
if (Status != XST_SUCCESS) {
xil_printf("Initialization failed %d\r\n", Status);
return XST_FAILURE;
}
if(XAxiDma_HasSg(DMAPtr)){
xil_printf("Device configured as SG mode \r\n");
return XST_FAILURE;
}
return XST_SUCCESS;
}
运行结果如下:
通过打印比对发现收发数据一致,PS与PL间通过DMA交互数据正常。
此外可以单步DEBUG调试,实时查看DDR内数据情况
另外我们看下AXI4_stream的时序,根据官方手册自行研究
此外如果测试程序没完成就关闭的话在测试的时候会出现如下结果,这是由于虽然测试程序重启了,但是PL的AXI_DMA和FIFO的状态还处于上次测试没完成的状态,导致后面的测试出现于预期结果不匹配的问题,此时需要重新下载bit文件。在以后的实际应用中也要注意ARM的复位与PL复位同步问题。
总结:
文章主要介绍了PS与PL通过AXI DMA通信的相关基本原理和测试过程中遇到的一些问题,供以后回顾。
后面会继续介绍zynq系统与用户逻辑结合的问题,有问题大家可以一起交流。