Kampis Elektroecke

AXI-Stream FIFO

Sollen im FPGA z. B. Audio-, Video- oder Netzwerkinterfaces genutzt werden, so wird in den meisten Fällen auf ein AXI-Stream Interface zurückgegriffen um den jeweiligen IP-Core für das Interface mit dem Prozessor oder der programmierbaren Logik zu verbinden. Um eine ununterbrochene Datenversorgung für das AXI-Stream Interface zu gewährleisten, wird in der Regel auf einen AXI-Stream FIFO oder einen AXI-DMA zurückgegriffen. Beide IP-Cores werden einmalig konfiguriert und übertragen dann die Daten entweder direkt aus dem Hauptspeicher über das AXI-Stream Interface (DMA) oder aus einem lokalen Speicher (FIFO). 

In diesem Beispiel möchte ich zeigen, wie ein AXI-Stream FIFO genutzt werden kann um Daten vom Processing System über das AXI-Stream Interface zu übertragen. Der FIFO stellt in diesem Beispiel sowohl den Master (über den Sendekanal), als auch den Slave (über den Empfangskanal) dar. Der FIFO kann auf zwei verschiedene Arten betrieben werden:

Verfahren Beschreibung
Cut-Through Der FIFO startet die Übertragung über das AXI-Stream Interface sofort, sobald er genügend Daten aus dem aktuellen Paket über das AXI-Interface erhalten hat. Es wird auf den kompletten Transfer eines Paketes gewartet.In diesem Modus muss sichergestellt werden, dass immer genügend Daten für das AXI-Stream Interface zur Verfügung stehen.Während des Empfangens werden die Daten automatisch in den internen Speicher geschrieben. Das RLR-Register beinhaltet dann die aktuelle Größe des Paketes.
Store-and-Forward In diesem Modus startet der FIFO eine Übertragung, wenn

  • Sämtliche Daten eines Paketes eingetroffen sind
  • Wenn die Länge in das TLR-Register geschrieben wurde

Während des Empfangens werden Daten automatisch in den internen Speicher geschrieben. Sobald das Ende eines Paketes erkannt wurde, wird das RLR-Register aktualisiert und ggf. ein Interrupt ausgelöst.

In diesem Beispiel soll der Store-and-Forward Modus genutzt werden. Das dazu gehörige Design sieht folgenermaßen aus:

Wobei der FIFO wie folgt konfiguriert wird:


Hinweis:

Der Transmit Control Kanal wird für bestimmte VLAN Features bei den AXI Ethernet IP-Cores genutzt und kann daher in diesem Beispiel ignoriert werden.


Die AXI-Stream Ports können zudem noch zusätzliche Einstellungen erweitert werden:

Name Beschreibung
TID Aktiviert ein 1 bis 8 Bytes großes ID-Feld für die Ports. Kann genutzt werden um mehrere Datenströme zu identifizieren.
TDEST Aktiviert ein 1 bis 4 Bytes großes Adressfeld, das genutzt werden kann um Routinginformationen zu speichern. Auf diese Weise kann ein einzelner FIFO mehrere Busteilnehmer mit Daten versorgen.
TUSER Aktiviert ein 4 bis 16 Bytes großes Datenfeld, das für beliebige Daten genutzt werden kann.
TSTRB Aktiviert ein zusätzlich Byte das angibt ob das dazu gehörige Datenbyte aus TDATA als ein Daten- oder ein Positionsbyte behandelt werden soll.
TKEEP Aktiviert ein zusätzliches Byte mit dem Daten, die nicht zum Stream gehören, als ungültig maskiert werden können.

Für die Software wird ein neues Projekt angelegt und der entsprechende Header eingefügt um den FIFO nutzen zu können:

#include "xllfifo.h"

Dann kann der FIFO auch schon initialisiert werden:

FIFO_ConfigPtr = XLlFfio_LookupConfig(XPAR_FIFO_DEVICE_ID);
if(FIFO_ConfigPtr == NULL)
{
	xil_printf("[ERROR] Invalid FIFO configuration!\r\n");
	return XST_FAILURE;
}

if(XLlFifo_CfgInitialize(&Fifo, FIFO_ConfigPtr, FIFO_ConfigPtr->BaseAddress) != XST_SUCCESS)
{
	xil_printf("[ERROR] FIFO Initialization failed!\n\r");
	return XST_FAILURE;
}

Wenn die Interrupts genutzt werden sollen, müssen zusätzlich noch der GIC initialisiert und die Interrupts konfiguriert werden:

GIC_ConfigPtr = XScuGic_LookupConfig(XPAR_PS7_SCUGIC_0_DEVICE_ID);
if(GIC_ConfigPtr == NULL)
{
	xil_printf("[ERROR] Invalid GIC configuration!\n\r");
	return XST_FAILURE;
}
XScuGic_CfgInitialize(&GIC, GIC_ConfigPtr, GIC_ConfigPtr->CpuBaseAddress);

XScuGic_SetPriorityTriggerType(&GIC, XPAR_FABRIC_LLFIFO_0_VEC_ID, 0xA0, 0x03);
if(XScuGic_Connect(&GIC, XPAR_FABRIC_LLFIFO_0_VEC_ID, (Xil_ExceptionHandler)FifoHandler, &Fifo) != XST_SUCCESS)
{
	xil_printf("[ERROR] Can not connect GpioHandler!\n\r");
	return XST_FAILURE;
}
XScuGic_Enable(&GIC, XPAR_FABRIC_LLFIFO_0_VEC_ID);

Xil_ExceptionInit();
Xil_ExceptionRegisterHandler(XIL_EXCEPTION_ID_INT, (Xil_ExceptionHandler)XScuGic_InterruptHandler, &GIC);
Xil_ExceptionEnable();

XLlFifo_IntClear(&Fifo, 0xFFFFFFFF);
XLlFifo_IntEnable(&Fifo, XLLF_INT_ALL_MASK);

Als nächstes werden der Empfangs- und der Sendekanal des FIFO zurückgesetzt, damit sie sich in einem definierten Zustand befinden:

XLlFifo_TxReset(&Fifo);
XLlFifo_RxReset(&Fifo);

Jetzt müssen noch Daten für den Transfer erzeugt werden. Sämtliche gesendeten und empfangenen Daten werden in einem entsprechenden Buffer gespeichert. Bei Bedarf können die Daten dann in den FIFO geschrieben werden, sodass der FIFO die Daten dann an einen angeschlossenen AXI-Stream Slave sendet oder Daten, die von einem angeschlossenen AXI-Stream Master empfangen wurden können bei Bedarf aus dem FIFO ausgelesen werden.

u32 SourceBuffer[64];
u32 DestinationBuffer[64]

xil_printf("[INFO] Generate data!\n\r");
for(u32 i = 0x00; i < 64; i++)
{
	SourceBuffer[i] = i * 2;
}

Hinweis:

Die Übertragung der Daten zum FIFO geschieht immer wortweise, wobei die Länge eines Wortes der Datenbreite des AXI-Interfaces entspricht. Bei einem AXI-Lite Interface sind es immer 32 Bit und bei einem AXI-Interface kann die Busbreite auf bis zu 512 Bit umgestellt werden.


Sobald eine Übertragung gestartet werden soll, müssen die einzelnen Datenwörter über die Funktion XLlFifo_TxPutWord in den FIFO geschrieben werden.

for(u32 i = 0x00; i < 64; i++)
{
	if(XLlFifo_iTxVacancy(&Fifo))
	{
		XLlFifo_TxPutWord(&Fifo, SourceBuffer[i]);
	}
}

Über das TDFV-Register (Transmit Data FIFO Vacancy) kann die Software prüfen wie viele Daten noch in den FIFO geschrieben werden können. Der Wert kann über die Funktion XLlFifo_iTxVacancy ausgelesen werden und ist 0, wenn der FIFO voll ist. Wenn alle Daten in den FIFO kopiert wurden, kann die Übertragung gestartet werden, indem die Anzahl der zu übertragenden Bytes mit der Funktion XLlFifo_iTxSetLen in das TLF-Register geschrieben wird.

XLlFifo_iTxSetLen(&Fifo, 64 * sizeof(u32));

Wenn keine Interrupts genutzt werden, muss die Funktion XLlFifo_IsTxDone verwendet werden, um auf das Ende der Übertragung zu warten.

while(!(XLlFifo_IsTxDone(&Fifo)));

Anschließend kann die Funktion XLlFifo_iRxOccupancy genutzt werden, um das RDFO-Register (Receive Data FIFO Occupancy) auszulesen. Wenn der Wert größer als 0 ist, sind neue Pakete empfangen worden.

while(XLlFifo_iRxOccupancy(&Fifo))
{
}

Falls neue Pakete empfangen worden sind, muss die Anzahl der empfangenen Datenbytes aus dem RLF-Register (Receive Length FIFO) ausgelesen werden um die Anzahl der empfangenen Datenwörter zu bestimmen. Mit diesem Wert können die einzelnen Datenwörter dann aus dem FIFO ausgelesen werden.

u32 ReceiveLength = (XLlFifo_iRxGetLen(InstancePtr)) / sizeof(u32);
for(u32 i = 0x00; i < ReceiveLength; i++)
{
	u32 RxWord = XLlFifo_RxGetWord(&Fifo);
	DestinationBuffer[i] = RxWord;
	xil_printf("		Data %lu: %lu\n\r", i, RxWord);
}

Alternativ können auch der Callbacks des Interrupts verwendet werden um auf das Ende einer Übertragung zu warten, bzw. auf ein empfangenes Paket zu reagieren.

static void FifoHandler(void* CallbackRef)
{
	XLlFifo* InstancePtr = (XLlFifo*)CallbackRef;

	u32 Pending = XLlFifo_IntPending(InstancePtr);
	while(Pending)
	{
		if(Pending & XLLF_INT_RC_MASK)
		{
			xil_printf("	Receive complete!\n\r");
			while(XLlFifo_iRxOccupancy(InstancePtr))
			{
				u32 ReceiveLength = (XLlFifo_iRxGetLen(InstancePtr)) / sizeof(u32);
				for(int i = 0; i < ReceiveLength; i++)
				{
					u32 RxWord = XLlFifo_RxGetWord(InstancePtr);
					DestinationBuffer[i] = RxWord;
					xil_printf("		Data %lu: %lu\n\r", i, RxWord);
				}
			}
			XLlFifo_IntClear(InstancePtr, XLLF_INT_RC_MASK);
		}

		else if(Pending & XLLF_INT_ERROR_MASK)
		{
			xil_printf("	FIFO Error...\n\r");
			XLlFifo_IntClear(InstancePtr, XLLF_INT_ERROR_MASK);
		}
                else
                {
                        XLlFifo_IntClear(InstancePtr, Pending);
                }
		Pending = XLlFifo_IntPending(InstancePtr);
	}
}

In dem Callback wird zuerst über die Funktion XLlFifo_IntPending das IS-Register (Interrupt Status) ausgelesen. Durch einen Vergleich mit den Bitmasken XLLF_INT_RC_MASK und XLLF_INT_ERROR_MASK wird ermittelt ob ein neues Paket empfangen wurde oder ob ein Fehler vorliegt.

Falls ein neues Paket empfangen wurde, wird die Anzahl der empfangenen Datenbytes aus dem RLF-Register ausgelesen werden um die Anzahl der empfangenen Datenwörter zu bestimmen. Mit diesem Wert können die einzelnen Datenwörter dann aus dem FIFO ausgelesen werden.

Dann wird das jeweilige Interruptbit über die Funktion XLlFifo_IntClear gelöscht, ein neuer Wert aus dem IS-Register ausgelesen und die ganze Abfrage wird erneut durchgeführt. Dieser Vorgang wiederholt sich so lange, bis alle Interrupts behandelt wurden und kein Interruptbit mehr gesetzt ist.

Das komplette Projekt steht in meinem GitHub-Repository zum Download bereit.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert