Praktische Arbeit mit FPGAs im Redd-Set. Mastering von DMA für den Avalon-ST-Bus und Umschalten zwischen Avalon-MM-Bussen

Wir arbeiten weiter an der Schaffung realer Geräte, die auf dem Redd FPGA-Komplex basieren. Für ein anderes All-Hardware-Projekt benötige ich einen einfachen Logikanalysator, also werden wir uns in diese Richtung bewegen. Glück - und zum USB-Bus-Analysator (aber das ist noch auf lange Sicht). Das Herz eines jeden Analysators ist RAM und eine Einheit, die zuerst Daten hochlädt und dann abruft. Heute werden wir es entwerfen.

Dazu meistern wir den DMA-Block. Im Allgemeinen ist DMA mein Lieblingsthema. Ich habe sogar einen großartigen Artikel über DMA auf einigen ARM-Controllern geschrieben . Aus diesem Artikel geht hervor, dass DMA Taktzyklen vom Bus nimmt. Im aktuellen Artikel werden wir untersuchen, wie es mit dem FPGA-basierten Prozessorsystem läuft.




Hardware-Erstellung


Wir beginnen mit der Erstellung der Hardware. Um zu verstehen, inwieweit der DMA-Block mit Taktzyklen in Konflikt steht, müssen wir bei hoher Last auf dem Avalon-MM-Bus (Avalon Memory-Mapped) genaue Messungen durchführen. Wir haben bereits herausgefunden, dass die JTAG-zu-Avalon-MM-Brücke von Altera keine hohen Buslasten liefern kann. Daher müssen wir heute dem System einen Prozessorkern hinzufügen, damit es mit hoher Geschwindigkeit auf den Bus zugreift. Wie dies gemacht wird, wurde hier beschrieben . Deaktivieren Sie aus Gründen der Optimalität beide Caches für den Prozessorkern, erstellen Sie jedoch wie hier einen stark verbundenen Bus .



Fügen Sie 8 Kilobyte Programmspeicher und Datenspeicher hinzu. Denken Sie daran, dass der Speicher ein Dual-Port sein muss und eine Adresse in einem bestimmten Bereich haben muss (um zu verhindern, dass er springt, sperren Sie ihn, wir haben die Gründe dafür hier besprochen ).



Wir haben das Projekt bereits tausend Mal erstellt, so gibt es nichts besonders interessant in dem Entstehungsprozess selbst (wenn überhaupt, alle Schritte für die Erstellung es beschrieben hier ).

Die Basis ist fertig. Jetzt brauchen wir eine Datenquelle, die wir speichern werden. Das Ideale ist ein ständig tickender Timer. Wenn der DMA-Block während einer Maßnahme die Daten nicht verarbeiten konnte, sehen wir dies sofort am fehlenden Wert. Nun, das heißt, wenn es im Speicher Werte 1234 und 1236 gibt, bedeutet dies, dass der DMA-Block auf der Uhr, als der Zeitgeber 1235 ausgab, keine Daten übertrug. Erstellen Sie eine DateiTimer_ST.sv mit einem so einfachen Zähler:

module Timer_ST (
  input              clk,
  input              reset,
	
  input  logic       source_ready,
  output logic       source_valid,
  output logic[31:0] source_data
	
);
    logic [31:0] counter;
    always @ (posedge clk, posedge reset)
    if (reset == 1)
    begin
        counter <= 0;
    end else
    begin
        counter <= counter + 1;
    end

    assign source_valid = 1;
    assign source_data [31:24] = counter [7:0];
    assign source_data [23:16] = counter [15:8];
    assign source_data [15:8] = counter [23:16];
    assign source_data [7:0] = counter [31:24];

endmodule

Dieser Zähler ist wie ein Pionier: Er ist immer bereit (am Ausgang ist source_valid immer eins) und zählt immer (mit Ausnahme der Momente des Rücksetzzustands). Warum das Modul genau diese Signale hat - haben wir in diesem Artikel besprochen .

Jetzt erstellen wir unsere eigene Komponente (wie dies gemacht wird, wird hier beschrieben ). Die Automatisierung hat fälschlicherweise den Avalon_MM-Bus für uns ausgewählt. Ersetzen Sie es durch avalon_streaming_source und ordnen Sie die Signale wie folgt zu :



Großartig. Fügen Sie unsere Komponente zum System hinzu. Jetzt suchen wir nach dem DMA-Block ... Und wir finden nicht einen, sondern drei. Alle von ihnen sind im Dokument Embedded Peripheral IP User Guide von Altera beschrieben (wie immer gebe ich Namen, aber keine Links, da sich Links immer ändern).



Welches verwenden? Ich kann Nostalgie nicht widerstehen. Bereits 2012 habe ich ein System entwickelt, das auf dem PCIe-Bus basiert. Alle Handbücher von Altera enthielten ein Beispiel, das auf dem ersten dieser Blöcke basierte. Aber er mit der PCIe-Komponente gab eine Geschwindigkeit von nicht mehr als 4 Megabyte pro Sekunde. Damals spuckte ich und schrieb meinen DMA-Block. Jetzt erinnere ich mich nicht mehr an seine Geschwindigkeit, aber er hat die Daten von SATA-Laufwerken an die Grenzen der Fähigkeiten von Laufwerken und SSDs dieser Zeit gebracht. Das heißt, ich habe einen Zahn an diesem Block geschärft. Aber ich werde nicht in einen Vergleich der drei Blöcke schlüpfen. Tatsache ist, dass wir heute mit einer Quelle arbeiten müssen, die auf Avalon-ST (Avalon Streaming Interface) basiert, und nur der DMA- Block Modular Scatter-Gather unterstützt solche Quellen . Hier setzen wir es auf das Diagramm.

Wählen Sie in den Blockeinstellungen den ModusStreaming in den Speicher zugeordnet . Plus - Ich möchte Daten vom Start bis zum Befüllen des SDRAMs übertragen, daher habe ich die maximale Datenübertragungseinheit von 1 Kilobyte auf 4 Megabyte ersetzt. Es stimmt, ich wurde gewarnt, dass der FMax-Parameter am Ende nicht so heiß sein wird (selbst wenn Sie den maximalen Block durch 2 Kilobyte ersetzen). Aber für heute ist FMax akzeptabel (104 MHz), und dann werden wir es herausfinden. Ich habe die restlichen Parameter unverändert gelassen. Sie können den Übertragungsmodus auch auf Nur Vollwortzugriff einstellen. Dadurch wird FMax auf 109 MHz erhöht. Aber wir werden heute nicht um Leistung kämpfen.



Damit. Die Quelle ist, DMA ist. Empfänger ... SDRAM? In zukünftigen Kampfbedingungen ja. Aber heute brauchen wir eine Erinnerung mit bekannten Eigenschaften. Leider muss SDRAM regelmäßig Befehle senden, die mehrere Taktzyklen dauern, und dieser Speicher kann durch Regeneration belegt werden. Daher verwenden wir stattdessen den integrierten FPGA-Speicher. Alles funktioniert für sie in einem Schritt, ohne unvorhersehbare Verzögerungen.

Da der SDRAM-Controller Single-Port ist, kann der eingebaute Speicher auch ausschließlich im Single-Port-Modus verwendet werden. Es ist wichtig. Tatsache ist, dass wir mit dem DMA-Blockmaster in den Speicher schreiben möchten, andererseits aber mit dem Prozessorkern oder dem Altera JTAG-zu-Avalon-MM-Block aus diesem Speicher lesen möchten. Die Hand streckt die Hand aus und verbindet die Schreib- und Leseblöcke mit zwei verschiedenen Ports ... Aber das können Sie nicht! Vielmehr ist es durch die Bedingungen des Problems verboten. Denn heute ist es möglich, aber morgen werden wir den Speicher durch einen ausschließlich Single-Port-Speicher ersetzen. Im Allgemeinen erhalten wir einen solchen Block aus drei Komponenten (Timer, DMA und Speicher):



Nun, und nur für Pro-Forma werde ich UT und Sysid zum JTAG-System hinzufügen (obwohl das zweite nicht geholfen hat, musste ich trotzdem mit einem JTAG-Adapter zaubern). Was es ist und wie ihre Zugabe kleine Probleme löst, haben wir bereits untersucht. Ich werde die Reifen nicht tönen, mit ihnen ist alles klar. Zeigen Sie einfach, wie alles in meinem Projekt aussieht:



Das war's. Das System ist bereit. Wir weisen Adressen zu, weisen Prozessorvektoren zu, generieren ein System (vergessen Sie nicht, dass Sie unter demselben Namen wie das Projekt selbst speichern müssen, dann wird es auf die oberste Ebene der Hierarchie verschoben) und fügen es dem Projekt hinzu. Machen Sie das Reset- Bein virtuell, verbinden Sie clk mit Pin_25- Bein. Wir bauen das Projekt zusammen und schütten es in die Ausrüstung ... Wie ist es, armes Ding, im leeren Büro wegen der total abgelegenen Lage? ... Es ist einsam und beängstigend für sie, wahrscheinlich allein ... Aber ich war abgelenkt.

Erstellen eines Software-Teils


Ausbildung


Im BSP-Editor schalte ich mit der üblichen Handbewegung die C ++ - Unterstützung ein. Ich habe so oft einen Screenshot dieses Falls eingefügt, dass ich damit aufhöre. Aber ein anderer Screenshot, obwohl er bereits gesehen wurde, ist immer noch so verbreitet. Besprechen wir es also noch einmal. Wir erinnern uns, dass das System versucht, Daten in den größten Speicherbereich zu stellen. Und so ist Buffer . Deshalb zwingen wir alles zu Daten :



Programmexperiment


Wir machen einen Code, der einfach den Speicher mit dem Inhalt der Quelle füllt (in der Rolle des Zählers).

Code anzeigen
#include "sys/alt_stdio.h"
#include <altera_msgdma.h>
#include <altera_msgdma_descriptor_regs.h>
#include <system.h>
#include <string.h>

int main()
{ 
  alt_putstr("Hello from Nios II!\n");

  memset (BUFFER_BASE,0,BUFFER_SIZE_VALUE);

  //  ,   
  IOWR_ALTERA_MSGDMA_CSR_CONTROL(MSGDMA_0_CSR_BASE,
      ALTERA_MSGDMA_CSR_STOP_DESCRIPTORS_MASK);

  //   ,      ,
  //    ,   .  .

  //    FIFO
  IOWR_ALTERA_MSGDMA_DESCRIPTOR_READ_ADDRESS(MSGDMA_0_DESCRIPTOR_SLAVE_BASE,
      (alt_u32)0);
  IOWR_ALTERA_MSGDMA_DESCRIPTOR_WRITE_ADDRESS(MSGDMA_0_DESCRIPTOR_SLAVE_BASE,
      (alt_u32)BUFFER_BASE);
  IOWR_ALTERA_MSGDMA_DESCRIPTOR_LENGTH(MSGDMA_0_DESCRIPTOR_SLAVE_BASE,
      BUFFER_SIZE_VALUE);
  IOWR_ALTERA_MSGDMA_DESCRIPTOR_CONTROL_STANDARD(MSGDMA_0_DESCRIPTOR_SLAVE_BASE,
      ALTERA_MSGDMA_DESCRIPTOR_CONTROL_GO_MASK);

   //  ,    
   IOWR_ALTERA_MSGDMA_CSR_CONTROL(MSGDMA_0_CSR_BASE,
       ALTERA_MSGDMA_CSR_STOP_ON_ERROR_MASK
       & (~ALTERA_MSGDMA_CSR_STOP_DESCRIPTORS_MASK)
       &(~ALTERA_MSGDMA_CSR_GLOBAL_INTERRUPT_MASK)) ;


   //   
   static const alt_u32 errMask = ALTERA_MSGDMA_CSR_STOPPED_ON_ERROR_MASK |
           ALTERA_MSGDMA_CSR_STOPPED_ON_EARLY_TERMINATION_MASK |
           ALTERA_MSGDMA_CSR_STOP_STATE_MASK |
           ALTERA_MSGDMA_CSR_RESET_STATE_MASK;

  volatile alt_u32 status;
  do
  {
     status = IORD_ALTERA_MSGDMA_CSR_STATUS(MSGDMA_0_CSR_BASE);
  } while (!(status & errMask) &&(status & ALTERA_MSGDMA_CSR_BUSY_MASK));     

  alt_putstr("You can play with memory!\n");

  /* Event loop never exits. */
  while (1);

  return 0;
}


Wir fangen an und warten auf die Nachricht "Du kannst mit dem Gedächtnis spielen!" , setzen Sie das Programm auf Pause und schauen Sie sich den Speicher ab Adresse 0 an. Zuerst hatte ich große Angst:



Ab Adresse 0x80 ändert der Zähler seinen Wert stark. Darüber hinaus eine sehr große Menge. Aber es stellte sich heraus, dass alles in Ordnung ist. Bei uns hält der Zähler nie an und ist immer bereit, und DMA hat eine eigene Vorauslesewarteschlange. Ich möchte Sie an die Einstellungen des DMA-Blocks erinnern:



0x80 Bytes sind 0x20 32-Bit-Wörter. Nur 32 Dezimalstellen. Es passt alles zusammen. Unter Debugging-Bedingungen ist dies nicht beängstigend. Unter Kampfbedingungen funktioniert die Quelle korrekter (ihre Bereitschaft wird zurückgesetzt). Daher ignorieren wir diesen Abschnitt einfach. In anderen Bereichen zählt das Messgerät nacheinander. Ich werde nur das Dump-Fragment in der Breite zeigen. Nehmen Sie ein Wort, dass ich es in seiner Gesamtheit untersucht habe.



Ich traute meinen Augen nicht und schrieb einen Code, der die Daten automatisch überprüft:

  volatile alt_u32* pData = (alt_u32*)BUFFER_BASE;
  volatile alt_u32 cur = pData[0x10];
  int nLine = 0;
  for (volatile int i=0x11;i<BUFFER_SIZE_VALUE/4;i++)
  {
	  if (pData[i]!=cur+1)
	  {
		  alt_printf("Problem at 0x%x\n",i*4);
		  if (nLine++ > 10)
		  {
			  break;
		  }
	  }
	  cur = pData[i];
  }

Er offenbart auch keine Probleme.

Ich versuche zumindest einige Probleme zu finden


In der Tat ist das Fehlen von Problemen nicht immer gut. Als Teil des Artikels musste ich die Probleme finden und dann zeigen, wie sie behoben werden. Immerhin liegen die Probleme auf der Hand. Ein ausgelasteter Bus kann keine Daten ohne Verzögerungen weitergeben! Es sollte Verzögerungen geben! Aber lassen Sie uns überprüfen, warum alles so schön passiert. Zunächst kann sich herausstellen, dass sich das Ganze im FIFO des DMA-Blocks befindet. Reduzieren Sie ihre Größe auf ein Minimum:



Alles funktioniert weiter! Gut. Stellen Sie sicher, dass die Anzahl der Zugriffe auf den Bus größer ist als die FIFO-Dimension. Fügen Sie einen Trefferzähler hinzu:



Gleicher Text:
  volatile alt_u32 status;
  volatile int n = 0;
  do
  {
	  status = IORD_ALTERA_MSGDMA_CSR_STATUS(MSGDMA_0_CSR_BASE);
	  n += 1;
  } while (!(status & errMask) &&(status & ALTERA_MSGDMA_CSR_BUSY_MASK));  


Am Ende der Arbeit ist es 29. Dies ist mehr als 16. Das heißt, das FIFO sollte überlaufen. Für alle Fälle fügen wir weitere Statusregisterwerte hinzu. Hilft nicht.

Mit Trauer habe ich mich vom entfernten Redd-Komplex getrennt und das Projekt auf mein vorhandenes Steckbrett übertragen, mit dem ich mich jetzt mit einem Oszilloskop verbinden kann (im Büro ist niemand in der Ferne, ich kann das Oszilloskop nicht erreichen). Dem Timer wurden zwei Ports hinzugefügt:

   output    clk_copy,
   output    ready_copy

Und ernannte sie:

    assign clk_copy = clk;
    assign ready_copy = source_ready;

Infolgedessen sah das Modul folgendermaßen aus:

module Timer_ST (
   input           clk,
   input           reset,
	
   input logic     source_ready,
   output logic    source_valid,
   output logic[31:0] source_data,

   output    clk_copy,
   output    ready_copy
	
);
    logic [31:0] counter;
    always @ (posedge clk, posedge reset)
    if (reset == 1)
    begin
        counter <= 0;
    end else
    begin
        counter <= counter + 1;
    end

    assign source_valid = 1;
    assign source_data [31:24] = counter [7:0];
    assign source_data [23:16] = counter [15:8];
    assign source_data [15:8] = counter [23:16];
    assign source_data [7:0] = counter [31:24];

    assign clk_copy = clk;
    assign ready_copy = source_ready;

endmodule

Zuhause habe ich ein kleineres Modell mit einem Kristall, also musste ich den Appetit auf Erinnerung reduzieren. Und es stellte sich heraus, dass mein primitives Programm nicht in einen 4-Kilobyte-Abschnitt passen würde. Das im letzten Artikel angesprochene Thema ist also, oh, wie relevant. Speicher im System - kaum genug!

Wenn das Programm startet, erhalten wir eine fertig Welle von entweder 16 oder 17 Maßnahmen. Dies ist mit dem FIFO des DMA-Blocks gefüllt. Der gleiche Effekt, der mich am Anfang erschreckt hat. Es sind diese Daten, die die sehr falsche Pufferfüllung bilden.



Als nächstes haben wir ein schönes Bild bei 40960 Nanosekunden, dh 2048 Zyklen (mit einem Heimkristall musste der Puffer auf 8 Kilobyte reduziert werden, dh 2048 32-Bit-Wörter). Hier ist der Anfang:



Hier ist das Ende:



Nun, und überall - kein einziger Fehler. Nein, es war klar, dass dies passieren würde, aber es gab einige Hoffnung ...

Vielleicht sollten wir versuchen, im Bus zu schreiben und nicht nur daraus zu lesen? Ich habe dem System einen GPIO-Block hinzugefügt:



Einen Eintrag hinzugefügt, während auf die Bereitschaft gewartet wurde:



Gleicher Text
  volatile alt_u32 status;
  volatile int n = 0;
  do
  {
	status = IORD_ALTERA_MSGDMA_CSR_STATUS(MSGDMA_0_CSR_BASE);
       IOWR_ALTERA_AVALON_PIO_DATA (PIO_0_BASE,0x01);
       IOWR_ALTERA_AVALON_PIO_DATA (PIO_0_BASE,0x00);

       n += 1;
  } while (!(status & errMask) &&(status & ALTERA_MSGDMA_CSR_BUSY_MASK));  


Es gibt keine Probleme und das wars! Wer ist schuld?

Es gibt keine Wunder, aber es gibt unerforschte Dinge


Wer ist schuld? Mit Trauer begann ich, alle Menüs des Platform Designer- Tools zu studieren, und fand anscheinend einen Hinweis. Wie sieht ein Reifen normalerweise aus? Der Kabelsatz, an den Clients angeschlossen sind. Damit? Es scheint so. Wir sehen dies aus den Zahlen im Herausgeber. Das zweite Ziel des Artikels war es zu zeigen, wie der Bus in zwei unabhängige Segmente unterteilt werden kann, von denen jedes funktioniert, ohne das andere zu stören.

Schauen wir uns jedoch die Meldungen an, die beim Generieren des Systems angezeigt werden. Markieren Sie Stichwörter:



Und es gibt viele ähnliche Nachrichten: Es wird hinzugefügt, es wird hinzugefügt. Es stellt sich heraus, dass nach dem Bearbeiten mit Stiften dem System viele zusätzliche Dinge hinzugefügt werden. Wie würden Sie ein Schema betrachten, in dem all dies bereits verfügbar ist? Ich schwimme immer noch darin, aber wir können die wahrscheinlichste Antwort im Rahmen des Artikels erhalten, indem wir diesen Menüpunkt wählen: Das



geöffnete Bild ist an sich beeindruckend, aber ich werde es nicht geben. Und sofort



werde ich diese Registerkarte auswählen: Und dort sehen wir Folgendes: Ich werde



das Wichtigste größer anzeigen:



Reifen werden nicht kombiniert! Sie sind segmentiert! Ich kann es nicht rechtfertigen (vielleicht werden mich Experten in den Kommentaren korrigieren), aber es scheint, dass das System die Schalter für uns eingesetzt hat! Es sind diese Schalter, die die isolierten Bussegmente bilden, und das Hauptsystem kann parallel zur DMA-Einheit arbeiten, die zu diesem Zeitpunkt ohne Konflikte auf den Speicher zugreifen kann!

Wir provozieren echte Probleme


Nachdem wir all dieses Wissen erhalten haben, kommen wir zu dem Schluss, dass wir sehr gut Probleme provozieren können. Dies ist erforderlich, um sicherzustellen, dass das Testsystem sie erstellen kann, was bedeutet, dass die Entwicklungsumgebung sie wirklich unabhängig auflöst. Wir werden uns nicht auf abstrakte Geräte auf dem Bus beziehen, sondern auf denselben Pufferspeicher, so dass der cmd_mux_005- Block den Bus zwischen dem Prozessorkern und dem DMA-Block verteilt. Wir schreiben die Funktion des langmütigen Wartens wie folgt um:



Gleicher Text
  volatile alt_u32 status;
  volatile int n = 0;
  volatile alt_u32* pBuf = (alt_u32*)BUFFER_BASE;
  volatile alt_u32 sum = 0;
  do
  {
	  status = IORD_ALTERA_MSGDMA_CSR_STATUS(MSGDMA_0_CSR_BASE);
	  sum += pBuf[n];

	  n += 1;
  } while (!(status & errMask) &&(status & ALTERA_MSGDMA_CSR_BUSY_MASK));


Und schließlich erschienen Einbrüche auf der Wellenform!



Die Speicherprüfungsfunktion hat auch viele Auslassungen festgestellt:



Ja, und wir sehen sehr gut, dass die Daten von Zeile zu Zeile verschoben werden:



Und hier ist ein Beispiel für einen bestimmten fehlerhaften Punkt (6CCE488F fehlt):



Jetzt sehen wir, dass das Experiment korrekt durchgeführt wurde, nur die Entwicklungsumgebung wurde ausgeführt Optimierung für uns. Dies ist der Fall, wenn ich den Satz „Alles schadet allen Stahl“ nicht mit Spott, sondern mit Dankbarkeit ausspreche. Vielen Dank an die Quartus-Entwickler für diese Angelegenheit!

Fazit


Wir haben gelernt, wie man einen DMA-Block in das System einfügt, um Streaming-Daten in den Speicher zu übertragen. Wir haben auch sichergestellt, dass der Download anderer Geräte auf dem Bus den Download-Prozess nicht beeinträchtigt. Die Entwicklungsumgebung erstellt automatisch ein isoliertes Segment, das parallel zu anderen Abschnitten des Busses ausgeführt wird. Wenn sich jemand demselben Segment zuwendet, sind Konflikte und Zeitaufwand für deren Lösung natürlich unvermeidlich, aber der Programmierer kann solche Dinge durchaus vorhersehen.

Im nächsten Artikel werden wir RAM durch einen SDRAM-Controller und den Timer durch einen echten "Kopf" ersetzen und den ersten logischen Analysator herstellen. Wird es funktionieren? Ich weiß es noch nicht. Ich hoffe die Probleme treten nicht auf.

All Articles