Forum: FPGA, VHDL & Co. Zynq: Messdaten via DMA an Ethernet senden


von Julian B. (Firma: Hochschule Reutlingen) (julian-bauer)


Angehängte Dateien:

Lesenswert?

Hallo Zusammen,

zuerst einmal erkläre ich euch mein Grundkonzept, was ich erreichen 
will.
Anschließend beschreibe ich den aktuellen Stand und zuletzt gehe ich auf 
meine noch zu lösenden Probleme ein.

FPGA ist ein ZedBoard. Ethernet PHY ist direkt drauf, muss also nicht 
viel beachtet werden was die Ansteuerung angeht.
Xilix Vivado und dessen SDK sind die folgenden Entwicklungsumgebungen.

Ich möchte gerne Messdaten, welche als stream über ein AXIS Streaming 
Interface empfange zunächst einmal einlesen, um sie dann memory mapped 
in der SDK benutzen zu können.
Daraufhin sollen die Messdaten über Ethernet mit 1GigE an einen Desktop 
Rechner gesendet werden. Dabei sollen zumindest 30MB/s erreicht werden, 
besser deutlich mehr. Die Messdaten sollen kontinuierlich gesamplet 
werden und nicht einmal alle X Intervalle gespeichert werden.

In der angehängten Grafik ist mein aktuelles Block Design zu sehen. Die 
"Messdaten" werden aktuell vom "AXI_Streaming_Generator" erzeugt. Es 
handelt sich um einen Counter, der hoch zählt und dabei in seiner 
Geschwindigkeit einstellbar ist.
Der Daten-Stream geht zum DMA und wird memory mapped. Von dort aus 
werden die Daten wieder raus an "AXI_jb_data_to_LED" geschrieben, wo sie 
auf die LEDs umgesetzt werden und als Lauflicht zusätzlich angezeigt 
werden.

Zu meinem Programmcode:

-Zuerst initialisiere ich den StreamingGenerator
-Initialisiere den DMA, den private-Timer und den TripleTimerCounter 
(TTC0)
-Initialisiere danach das Interrupt System und enable alle Interrupts

Ethernetfunktionalität stelle ich mittels lwIP-TCP/IP-Stack her.

Programmablauf ist nun folgender:
Zu Beginn wird ein Sendepuffer angelegt, bestehend aus einem Feld aus 
Charactern mit Zahlen zwischen 0 bis 9. Das sind die Daten welche per 
Ethernet gesendet werden.
Über den TTC0 Timer kann das Zeitintervall eingestellt werden, in 
welchem ein Paket an den TCP-Stack übergeben wird. Beim Überlauf wird 
TtcTimerFlag incrementiert.
In Hauptprogramm wird zyklisch gesendet und die Funktion transfer_data() 
aufgerufen. Jedes Mal wird die Queue des TCP Stacks gefüllt. Überträgt 
der Transportlayer Daten, wird die Callback-Fnkt aufgerufen und die 
Queue wieder gefüllt. Bei jedem Mal, wo ein Datenpaket eingefüllt wird, 
wird TtcTimerFlag wieder decrementiert.

Bei jedem Überlauf von TTC0 wird ebenfalls geprüft, ob der DMA-Transfer 
bereits abgeschlossen ist (TxDone und RxDone - Flags) und ggf. ein neuer 
Transfer eingeleitet. Dabei werden die Daten aus dem StreamingGenerator 
wieder weiter an die LED gesendet.
1
while(TxPerfDone==0){
2
  if (TcpFastTmrFlag) {
3
    tcp_fasttmr();
4
    TcpFastTmrFlag = 0;
5
  }
6
  if (TcpSlowTmrFlag) {
7
    tcp_slowtmr();
8
    TcpSlowTmrFlag = 0;
9
  }
10
  xemacif_input(netif);    
11
  transfer_data();      
12
}

Per Ethernet Daten zu senden funktioniert auch wunderbar. Das FPGA 
verbindet sich als Client mit dem PC. Ich erhalte eine sich 
wiederholende Zahlenfolge von 0 bis 9 und kann angeben, wieviel Megabyte 
oder Gigabyte an Daten ich übertragen will, bis meine Applikation 
beendet wird. Die Übertragungsgeschwindigkeit liegt bei bis zu 800MBit/s 
und ist mehr als ausreichend. Jedoch sind die aktuell gesendeten Daten 
relativ unspektakulär. Ich will gerne beide Elemente miteinander 
verbinden:

ich möchte kein vorgefertigtes Feld an Daten über Ethernet senden, 
sondern die per DMA emfpangenen Daten, welche vom StreamingGenerator 
erzeugt wurden. Dabei müssen die Daten jedoch so schnell 
zwischengespeichert werden, dass möglichst wenig Bandbreite verloren 
geht.

Wie ich hier weiter mache bin ich mir noch nicht im klaren. Ich habe 
schon an die Implementierung eines Rindpuffers oder ähnliches gedacht. 
BEvor ich viel Zeit investiere würde ich gerne jedoch gerne von euch 
Erfahren, was die wohl eleganteste Lösung ist und ob das aktuelle Design 
dazu überhaupt zu gebrauchen ist?

Im Anhang ist das gezippte Projekt dabei. In der SDK sind unter anderem 
die .c Files. Alles hier anzugeben würde den Rahmen sprengen. Das 
Wesentliche ist in main.c und txperf.c zu finden.

Vielen Dank,
Grüße Julian

: Bearbeitet durch User
von Klakx (Gast)


Lesenswert?

um nicht so sehr in die Details zu gehen.

Wäre es möglich, dass du deinen Stream in ein FIFO schickst und dieses 
MemoryMapped (mit Hilfe DMA) ausließt?

von Julian B. (Firma: Hochschule Reutlingen) (julian-bauer)


Lesenswert?

danke für deine Antwort.

daran habe ich auch schon gedacht. Das würde jedoch erst einmal das 
Problem lösen wie die Daten in den DMA gelangen. Momentan ist ein 
"idealer Stream" davor, der in seiner Geschwindigkeit voreingestellt 
werden kann (wie groß die Frame_Size ist bis der Stream quitiert wird)
Ein FiFo würde meines Erachtens nach jedoch nicht das Problem lösen, 
dass ich pro Zeitintervall des TTC0 Timers, bei dessen Ablauf ich die 
TCP-queue wieder fülle, auch genug Daten mit dem DMA aus dem FiFo 
gezogen habe.
Wir sprechen hier von einer Speichergröße der Queue von 64 Kilobyte 
(65536).
Die komplette Queue muss zu beginn einmal gefüllt werden. Danach dann 
zyklisch, in Abhängigkeit von der Frequenz des TTC0 wieder entsprechend 
aufgefüllt. In der Praxis werden so ungefähr zwischen zwei und drei 
Kilobyte dazwischen versendet. Sich darauf zu verlassen wäre jedoch 
nicht ideal.

Daher auch meine Vorstellung, die Daten nach dem Auslesen manuell in 
eine neue Queue zu laden, dabei das Laden vom TTC0 Timer abzukoppeln in 
einen neuen Timer TTC1 der eine höhere Frequenz hat und so z.B. 4 mal so 
oft Überläuft und damit jedesmal ein neues Element ausliest, 
vorausgesetzt, die Queue hat weniger als 64 Elemente.
Dann könnte nämlich im Extremfall die komplette Queue aus dem DMA an die 
Queue des TCP Stacks überreicht werden.

Der Aufwand ist jedoch sehr groß, da ich noch nie eine Queue 
implementiert habe und ich ncht einschätzen kann, ob es zielführend ist 
oder mir die Bandbreite total einbrechen wird.


Vielleicht hat jemand bereits ein ähnliches Projekt selbst gemacht oder 
irgendwo nachgelesen? Ich denke das wird wohl ein generelles Problem 
sein, wie Daten so schnell versendet werden können, unabhängig von 
meiner konkreten Aufgabenstellung?

von Martin S. (strubi)


Lesenswert?

Moin,

ich habe mir für solche Sachen die DMA-Engine des Blackfin 
reimplementiert. Guck mal ins TRM des ADSP-BF537 o.ä., insbesondere zum 
Autobuffer/Descriptoren-Thema. Was Ethernet-Uebertragung angeht, steht 
da auch einiges in der Section zum "EMAC".

Der Gag ist, dass du eine verkettete Liste von Deskriptoren 
typischerweise in einem L1-SRAM ablegst, auf die die DMA-Engine 
zugreift. Damit kannst du prima Buffer queues implementieren, die 
nahtlos und verlustfrei arbeiten (und ohne Umkopieren). Wenn die 
Peripherie auf einem andern Clock nudelt, brauchst du entsprechend 
DualClock-fähige FIFOs, die gibt's normalerweise als klickisupi-IP-Core.
Wenn du die EMAC-Engine auch implementierst, kannst du einen grossen 
zusammenhängenden Datenblob im Speicher entsprechend ohne Overhead und 
Umkopieren in UDP-Pakete zersäbeln und auf den TriMAC raushauen.
Allerdings hast du da u.U. mit lwip keinen Spass und implementierst 
zumindest die UDP-Seite plus ARP usw. besser selber.

Gruss,

- Strubi

von Julian B. (Firma: Hochschule Reutlingen) (julian-bauer)


Lesenswert?

Hallo Strubi,

danke für deine ausführliche Beschreibung. Im Beispeil mit einem scatter 
gather DMA habe ich folgende Code-Zeilen mal rauskopiert. Diese 
beschreiben eine solche verkettete Liste wie du sie beschrieben hast. 
Ist dir das Beispiel vielleicht zufällig bekannt?

Ich überlege, ob ich das Beispiel mit dem SG-DMA mir noch ein wenig 
anschauen soll und dann dies in mein Design integrieren soll und ob das 
zielführend ist.
So tief bin ich leider nicht in der C-Programmierung drin, dass ich das 
hier alles so geschwind mache :D

Im Zusammenhang mit EMAC und ARP habe ich leider nichts mehr verstanden.
Einen eigenen EMAC möchte ich im BlockDesign nicht implementieren. Das 
ZedBoard hat den PHY direkt implementiert und ich kann aus der SDK 
heraus darauf zugreifen.

Aber die Überlegung mit den verketteten Listen klingt doch erstmal ganz 
gut und wird mich erstmal beschäftigen! selbst wenn die Bandbreite erst 
einmal nicht maximal ausgelastet wird ist es mir egal. ein erstes 
Ergebnis wäre schön, an dem dann weiter gebastelt werden kann.

Übrigens verwende ich TCP, nicht UDP :)

Zuletzt noch der angesprochene Code-Auszug
1
/*
2
*
3
* This function sets up RX channel of the DMA engine to be ready for packet
4
* reception
5
*
6
* @param  AxiDmaInstPtr is the pointer to the instance of the DMA engine.
7
*
8
* @return  - XST_SUCCESS if the setup is successful.
9
*    - XST_FAILURE if fails.
10
*
11
* @note    None.
12
*
13
******************************************************************************/
14
static int RxSetup(XAxiDma * AxiDmaInstPtr)
15
{
16
  XAxiDma_BdRing *RxRingPtr;
17
  int Status;
18
  XAxiDma_Bd BdTemplate;
19
  XAxiDma_Bd *BdPtr;
20
  XAxiDma_Bd *BdCurPtr;
21
  int BdCount;
22
  int FreeBdCount;
23
  u32 RxBufferPtr;
24
  int Index;
25
26
  RxRingPtr = XAxiDma_GetRxRing(&AxiDma);
27
28
  /* Disable all RX interrupts before RxBD space setup */
29
  XAxiDma_BdRingIntDisable(RxRingPtr, XAXIDMA_IRQ_ALL_MASK);
30
31
  /* Setup Rx BD space */
32
  BdCount = XAxiDma_BdRingCntCalc(XAXIDMA_BD_MINIMUM_ALIGNMENT,
33
        RX_BD_SPACE_HIGH - RX_BD_SPACE_BASE + 1);
34
35
  Status = XAxiDma_BdRingCreate(RxRingPtr, RX_BD_SPACE_BASE,
36
          RX_BD_SPACE_BASE,
37
          XAXIDMA_BD_MINIMUM_ALIGNMENT, BdCount);
38
  if (Status != XST_SUCCESS) {
39
    xil_printf("Rx bd create failed with %d\r\n", Status);
40
    return XST_FAILURE;
41
  }
42
43
  /*
44
   * Setup a BD template for the Rx channel. Then copy it to every RX BD.
45
   */
46
  XAxiDma_BdClear(&BdTemplate);
47
  Status = XAxiDma_BdRingClone(RxRingPtr, &BdTemplate);
48
  if (Status != XST_SUCCESS) {
49
    xil_printf("Rx bd clone failed with %d\r\n", Status);
50
    return XST_FAILURE;
51
  }
52
53
  /* Attach buffers to RxBD ring so we are ready to receive packets */
54
  FreeBdCount = XAxiDma_BdRingGetFreeCnt(RxRingPtr);
55
56
  Status = XAxiDma_BdRingAlloc(RxRingPtr, FreeBdCount, &BdPtr);
57
  if (Status != XST_SUCCESS) {
58
    xil_printf("Rx bd alloc failed with %d\r\n", Status);
59
    return XST_FAILURE;
60
  }
61
62
  BdCurPtr = BdPtr;
63
  RxBufferPtr = RX_BUFFER_BASE;
64
65
  for (Index = 0; Index < FreeBdCount; Index++) {
66
67
    Status = XAxiDma_BdSetBufAddr(BdCurPtr, RxBufferPtr);
68
    if (Status != XST_SUCCESS) {
69
      xil_printf("Rx set buffer addr %x on BD %x failed %d\r\n",
70
      (unsigned int)RxBufferPtr,
71
      (unsigned int)BdCurPtr, Status);
72
73
      return XST_FAILURE;
74
    }
75
76
    Status = XAxiDma_BdSetLength(BdCurPtr, MAX_PKT_LEN,
77
          RxRingPtr->MaxTransferLen);
78
    if (Status != XST_SUCCESS) {
79
      xil_printf("Rx set length %d on BD %x failed %d\r\n",
80
          MAX_PKT_LEN, (unsigned int)BdCurPtr, Status);
81
82
      return XST_FAILURE;
83
    }
84
85
    /* Receive BDs do not need to set anything for the control
86
     * The hardware will set the SOF/EOF bits per stream status
87
     */
88
    XAxiDma_BdSetCtrl(BdCurPtr, 0);
89
90
    XAxiDma_BdSetId(BdCurPtr, RxBufferPtr);
91
92
    RxBufferPtr += MAX_PKT_LEN;
93
    BdCurPtr = XAxiDma_BdRingNext(RxRingPtr, BdCurPtr);
94
  }
95
96
  /*
97
   * Set the coalescing threshold, so only one receive interrupt
98
   * occurs for this example
99
   *
100
   * If you would like to have multiple interrupts to happen, change
101
   * the COALESCING_COUNT to be a smaller value
102
   */
103
  Status = XAxiDma_BdRingSetCoalesce(RxRingPtr, COALESCING_COUNT,
104
      DELAY_TIMER_COUNT);
105
  if (Status != XST_SUCCESS) {
106
    xil_printf("Rx set coalesce failed with %d\r\n", Status);
107
    return XST_FAILURE;
108
  }
109
110
  Status = XAxiDma_BdRingToHw(RxRingPtr, FreeBdCount, BdPtr);
111
  if (Status != XST_SUCCESS) {
112
    xil_printf("Rx ToHw failed with %d\r\n", Status);
113
    return XST_FAILURE;
114
  }
115
116
  /* Enable all RX interrupts */
117
  XAxiDma_BdRingIntEnable(RxRingPtr, XAXIDMA_IRQ_ALL_MASK);
118
119
  /* Start RX DMA channel */
120
  Status = XAxiDma_BdRingStart(RxRingPtr);
121
  if (Status != XST_SUCCESS) {
122
    xil_printf("Rx start BD ring failed with %d\r\n", Status);
123
    return XST_FAILURE;
124
  }
125
126
  return XST_SUCCESS;
127
}
1
static void RxCallBack(XAxiDma_BdRing * RxRingPtr)
2
{
3
  xil_printf("----startet RxCallBack----\r\n");
4
  int BdCount;
5
  XAxiDma_Bd *BdPtr;
6
  XAxiDma_Bd *BdCurPtr;
7
  u32 BdSts;
8
  int Index;
9
10
  /* Get finished BDs from hardware */
11
  BdCount = XAxiDma_BdRingFromHw(RxRingPtr, XAXIDMA_ALL_BDS, &BdPtr);
12
13
  BdCurPtr = BdPtr;
14
  for (Index = 0; Index < BdCount; Index++) {
15
16
    /*
17
     * Check the flags set by the hardware for status
18
     * If error happens, processing stops, because the DMA engine
19
     * is halted after this BD.
20
     */
21
    BdSts = XAxiDma_BdGetSts(BdCurPtr);
22
    if ((BdSts & XAXIDMA_BD_STS_ALL_ERR_MASK) ||
23
        (!(BdSts & XAXIDMA_BD_STS_COMPLETE_MASK))) {
24
      Error = 1;
25
      break;
26
    }
27
28
    /* Find the next processed BD */
29
    BdCurPtr = XAxiDma_BdRingNext(RxRingPtr, BdCurPtr);
30
    RxDone += 1;
31
  }
32
33
}

von Martin S. (strubi)


Lesenswert?

Hi Julian,

genau, Scatter-Gather (SG DMA) war das Zauberwort. Das würde ich schon 
mal zumindest in Software probieren: wenn ein Slice per DMA raus ist, 
kannst du im Interrupt nen neuen DMA-Transfer per Hand (also CPU) 
ankicken.
Ich bin mir aber nicht sicher, ob das Xilinx-Refdesign SGDMA im lwip 
unterstützt, da musste man früher gehörig an den Interna rumbohren. Da 
hast du dann zwei Baustellen, so oder so musst du dich dann auf 
Protokollseite (logischer Layer über TCP) um das Handshaking und 
allfälliges Spülen des Ringbuffers beim Ueberlauf kümmern. Ich würde mal 
mit einem Software-FIFO (Buffer Queue) auf höchstem Layer anfangen, ohne 
Umkopieren geht vermutlich auch beim neuesten lwip nix.
ARP ist address resolution protocol, macht der lwip schon alles. Sorry 
für die Verwirrung.

von Julian B. (Firma: Hochschule Reutlingen) (julian-bauer)


Lesenswert?

Hi nochmal,

zuerst einmal: reference design benutze ich nicht mehr, ich habe mein 
eigenes Blockdesign erstellt und in das boardsupportpackage (BSP) 
lwip141(v1.0) eingebunden un konfiguriert, um eine möglichst hohe 
Bandbreite zu realisieren. Ich kann also das aktuelle Bd ändern und den 
Bitstream an die SDK senden.

"so oder so musst du dich dann auf Protokollseite (logischer Layer über 
TCP) um das Handshaking und allfälliges Spülen des Ringbuffers beim 
Ueberlauf kümmern. Ich würde mal mit einem Software-FIFO (Buffer Queue) 
auf höchstem Layer anfangen, ohne Umkopieren geht vermutlich auch beim 
neuesten lwip nix."

kannst du das bei Gelegenheit bitte noch ein wenig näher formulieren?
Wieso um Handshaking kümmern und was ist Spülen?
Und von wo nach wo wird kopiert? aus der Software-Queue in den TCP 
Buffer?

Danke nochmal!

von Martin S. (strubi)


Lesenswert?

Moin,

in Kürze zu Handshaking: Wenn du einen Buffer rausschickst, aber ein ACK 
verloren geht oder der Empänger einfach grade nicht will, läuft deine 
Queue voll. Wenn du dann nicht mehr puffern kannst, brauchst du ein 
"Flush"-Szenario (Spülen).
Kopieren: Ja, Software-FIFO nach TCP-Buffer. Das kannst du immerhin mit 
Memory to memory SGDMA machen. Aber ob das Konzept so korrekt ist, kann 
ich dir nicht sagen, da ich wie gesagt lwip komplett umgehe für die 
GigE-Anwendungen.

von Julian B. (Firma: Hochschule Reutlingen) (julian-bauer)


Lesenswert?

Martin S. schrieb:
> in Kürze zu Handshaking: Wenn du einen Buffer rausschickst, aber ein ACK
> verloren geht oder der Empänger einfach grade nicht will, läuft deine
> Queue voll. Wenn du dann nicht mehr puffern kannst, brauchst du ein
> "Flush"-Szenario (Spülen).

Da würde ich jetzt einfach mal sagen, dass die ältesten Werte einfach 
verloren gehen, wenn der Buffer voll läuft. Eine andere Möglichkeit 
bleibt ja nicht, wenn ich schon am Limit was das Senden angeht bin. 
oder?

> Kopieren: Ja, Software-FIFO nach TCP-Buffer. Das kannst du immerhin mit
> Memory to memory SGDMA machen. Aber ob das Konzept so korrekt ist, kann
> ich dir nicht sagen, da ich wie gesagt lwip komplett umgehe für die
> GigE-Anwendungen.

was verwendest du für GigE wenn nicht lwip?

danke dir!

von Martin S. (strubi)


Lesenswert?

Julian B. schrieb:

> Da würde ich jetzt einfach mal sagen, dass die ältesten Werte einfach
> verloren gehen, wenn der Buffer voll läuft. Eine andere Möglichkeit
> bleibt ja nicht, wenn ich schon am Limit was das Senden angeht bin.
> oder?
>

lwip wirft nix weg, das musst du dann selber machen,d.h. deinen 'tail' 
der Queue den ältesten Buffer überspringen lassen.

>> Kopieren: Ja, Software-FIFO nach TCP-Buffer. Das kannst du immerhin mit
>> Memory to memory SGDMA machen. Aber ob das Konzept so korrekt ist, kann
>> ich dir nicht sagen, da ich wie gesagt lwip komplett umgehe für die
>> GigE-Anwendungen.
>
> was verwendest du für GigE wenn nicht lwip?
>

Rohes UDP.

von Julian B. (Firma: Hochschule Reutlingen) (julian-bauer)


Lesenswert?

Hallo,

ich wollte mich nun noch einmal zurück melden - es funktioniert! :)

die Eckdaten:
Messwerteerfassung eines 32-bit Messwerts mit bis zu 100 MHz => 400 MB/s 
Daten. Messfrequenz wird auf 25 MHz geteilt um 100 MB/s zu erhalten.

Zwischenspeichern der Messwerte in einem hardware-FIFO.

Einlesen von Messdaten über den DMA in den DDR als Burst von 32 kB 
Blöcken mit bis zu 330 MB/s. Der DMA wird im Simple Mode betrieben (kein 
SG_DMA!) Es wird ein eigener Ringpuffer im Adressbereich des RAM 
implementiert.

Übergeben der ältestens Messwerte in 32 kB Blöcken an den TCP Stack. 
Damit erreiche ich nun auch 100 MB/s über Ethernet und erreiche somit 
80% Auslastung ohne bislang ein Tuning des Stacks vorzunehmen.

Danke für die Hilfe!

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.