Projekte.PCI-Tutorial-MasterEntwurf (Struktur)


Ein PCI-Initiator mit Burstunterstützung

Übersicht

Die für dieses Projekt modifizerten Dateien des LogiCore PCI Interface und die darüber hinaus benötigten Dateien sind: [benign.vhd] [ramb4_s32.vhd] [xc2s200fg456_32_33.ucf] [buffer_1k_32b.vhd] [pcim_top.vhd] [cfg.vhd] [displayrom.vhd] [userapp.vhd] [target_control.vhd]

Dieses Projekt implementiert einen vollständigen PCI-Initiator (Master) mit Burstunterstützung für DMA (Direct Memory Access). Das Gerät verfügt über einen 1 K x 32 Bit großen Puffer, in den es die über den PCI-Bus angeforderten Daten abspeichern oder den Pufferinhalt über den PCI-Bus an ein anderes PCI-Gerät senden kann. Nach Abschluss eines Transfers wird ein Interrupt ausgelöst, um den Teiber zu informieren.
Die notwendigen DMA-Parameter jedes Transfers (Anzahl der Datenwörter, Quell- bzw. Zieladresse), der Gerätestatus und der Befehl zum Start des Transfers werden über ein einfaches Single Transfer Target Interface gesetzt bzw. ausgelesen. Dafür gibt es insgesamt vier 32 Bit Register:
  • Quell- / Zieladresse (read/write), Offset 0x00
    Bit 31 - Bit 2Bit 1 - Bit 0
    pci_addr00
    Dieses Register enthält zu Beginn eines Transfers die Anfangsadresse des Speicherbereichs (eines entfernten PCI-Targets), in den Daten geschrieben oder aus dem Daten gelesen werden sollen. Das PCI-Target kann ein anderes PCI-Gerät, z.B. die Grafikkarte, aber auch der Hauptspeicher sein. Während des Transfers wird die Adresse hochgezählt und kann auch abgefragt werden.
    Die unteren zwei Bits werden automatisch auf 0 gesetzt, da PCI-Transfers im Speicheradressraum im Gegensatz zum I/O-Adressraum immer 32 Bit ausgerichtet auf voller PCI-Bus-Breite erfolgen.
     
  • Transferzähler (read/write), Offset 0x04
    Bit 31 - Bit 11Bit 10 - Bit 0
    0000 0000 0000 0000 0000 0xfer_count
    Der zweite DMA-Parameter, die Anzahl der Datenwörter, wird in diesem Register eingestellt. Gezählt wird nicht in Bytes, sondern in 32 Bit Wörtern. Wegen des vorhandenen 1 K x 32 Bit großen Puffers sind Werte zwischen 1 bis 1024 sinnvoll. Nach Beendigung eines erfolgreich abgeschlossenen Transfers muss dieses Register 0 sein.
     
  • Befehlsregister (write only), Offset 0x08
    Bit 31 - Bit 1Bit 0
    nicht benutztdir
    start
    Durch einen Schreibzugriff auf das Befehls-"Register" wird der Transfer gestartet und zugleich die Transferrichtung festgelegt:
    • Bit 0 = 0 : PCI-Lesetransfer, d.h. Daten werden von einem entfernten PCI-Target gelesen und in den internen Puffer geschrieben.
    • Bit 0 = 1 : PCI-Schreibtransfer, d.h. Daten werden aus dem internen Puffer gelesen und in den Speicherbereich eines entfernten PCI-Targets geschrieben.

    Statusregister (read only), Offset 0x08
    Bit 31 - Bit 1Bit 0
    nicht benutztidle_state
    idle_state = 1 : Die Transfersteuerung befindet sich im Ruhezustand und damit bereit für einen Transfer.
     
  • Interrupt-Flag (read only), Offset 0x0C
    Bit 31 - Bit 1Bit 0
    nicht benutztintr_n_flag
    intr_n_flag = 0 (negierte Logik) : Ein Transfer wurde abgeschlossen und ein Interrupt ausgelöst.
    Das Interrupt-Flag ist zwingend notwendig, da es beim PCI-Bus Shared Interrupts gibt. Wird der Interrupthandler des Treibers für ein PCI-Gerät aufgerufen, muss dieser ersteinmal bei "seinem" Gerät anfragen, ob es überhaupt für die Interruptauslösung verantwortlich ist.
    Durch das Lesen des Interrupt-Flags wird es automatisch wieder zurück auf 1 gesetzt.
Der Entwurf des PCI-Masters erfolgt in der Komponente userapp (Datei userapp.vhd). Diese bindet weitere untergeordnete VHDL-Module ein. Der VHDL-Code ist sehr ausführlich kommentiert, weshalb er hier nicht nochmals im Detail erläutert wird.

Anpassung des LogiCore PCI Interfaces

Das LogiCore PCI Interface wird in der Datei cfg.vhd mit symbolischen Konstanten wie schon beschrieben konfiguriert.

Für das PCI-Target wird ein 4 x 32 Bit = 16 Byte umfassender Adressraum benötigt. Wegen der "Mindestens 4 KByte"-Empfehlung wird ein 4 KByte großer Block im Basisadressregister angefordert.

Die PCI-Initiator-Funktionialität ist immer vorhanden und bedarf keiner weiteren Eintragungen im Konfigurationsmodul. Sofern es das PCI-BIOS unterstützt, kann zur Leistungsoptimierung mit dem Latency Timer Mechanismus, der über die Felder MAX_LAT und MIN_GNT im PCI Configuration Header eingestellt wird, experimentiert werden. Eine Beschreibung der Wirkungsweise befindet sich aud Seite Der PCI Configuration Header im Abschnitt "Das Max_Lat-Register- ...".

    [...]

    --------------------------------------------------------------
    -- Configure Base Address Registers
    --------------------------------------------------------------

      -- BAR0
      cfg_int(0)              <= ENABLE ;
      cfg_int(32 downto 1)    <= SIZE4K ;
      cfg_int(33)             <= NOFETCH ;
      cfg_int(35 downto 34)   <= TYPE00 ;
      cfg_int(36)             <= MEMORY ;

      -- BAR1
      cfg_int(37)             <= DISABLE ;
      cfg_int(69 downto 38)   <= SIZE2G ;
      cfg_int(70)             <= NOFETCH ;
      cfg_int(72 downto 71)   <= TYPE00 ;
      cfg_int(73)             <= MEMORY ;

      -- BAR2
      cfg_int(74)             <= DISABLE ;
      cfg_int(106 downto 75)  <= SIZE2G ;
      cfg_int(107)            <= NOFETCH ;
      cfg_int(109 downto 108) <= TYPE00 ;
      cfg_int(110)            <= MEMORY ;

    --------------------------------------------------------------
    -- Configure MAX_LAT MIN_GNT
    --------------------------------------------------------------

      -- MAX_LAT and MIN_GNT
      cfg_int(231 downto 224) <= X"00" ; 
      cfg_int(223 downto 216) <= X"00" ; 

    [...]

Verwendete untergeordnete Module

Modul displayrom

Das schon häufig verwendete Modul displayrom (Datei displayrom.vhd) enthält den "Zeichensatz" für die 7-Segment-Anzeigen.

Modul benign

Das Modul benign (Datei benign) dient als Sammelbecken für nicht relevante Eingangssignale des LogiCore PCI Interfaces und sorgt für deren korrekte Beschaltung.

Modul target_control

    entity target_control is
      port ( 
        -- Signale des PCI-Interfaces 
        RST                  : in std_logic;
        CLK                  : in std_logic;
        BASE_HIT             : in std_logic_vector(7 downto 0);
        S_DATA               : in std_logic;
        S_WRDN               : in std_logic;
        ADDR                 : in std_logic_vector(31 downto 0);
        S_DATA_VLD           : in std_logic;
        S_READY              : out std_logic;
        S_TERM               : out std_logic;
        S_ABORT              : out std_logic;

        -- im Modul gebildete Ausgangssignale
        OE_PCI_START_REG     : out std_logic;
        OE_XFER_COUNT_REG    : out std_logic;
        OE_STATUS_REG        : out std_logic;
        OE_INTR_FLAG         : out std_logic;
        LOAD_PCI_START_REG   : out std_logic;
        LOAD_XFER_COUNT_REG  : out std_logic;
        LOAD_COMMAND_REG     : out std_logic
      );
    end target_control;
Das Modul target_control (Datei target_control.vhd) enthält die schon bekannte Steuerlogik eines Single Transfer PCI Targets, jedoch nicht die oben definierten Target-Register, die sich in der Komponente userapp befinden. Es werden nur die Steuersignale für die Register gebildet:
  • OE_xyz_REG = 1 (output enable) : Wegen eines laufenden Target-Lesezugriffs muss der Inhalt des Registers xyz auf den ADIO-Bus durchgeschaltet werden.
  • LOAD_xyz_REG = 1 : Wegen eines laufenden Target-Schreibzugriffs sind die Daten auf dem ADIO-Bus in das Register xyz zu übernehmen.

Modul buffer_1k_32b

    entity buffer_1k_32b is
      port ( 
        RST      : in std_logic;
        CLK      : in std_logic;
 
        DATA_IN  : in std_logic_vector(31 downto 0);
        PUSH     : in std_logic;

        DATA_OUT : out std_logic_vector(31 downto 0);
        POP      : in std_logic;
        ACK      : in std_logic;
        BACK_UP  : in std_logic
      );
    end buffer_1k_32b;
Das Modul buffer_1k_32b (Datei buffer_1k_32b.vhd) implementiert den 1 K x 32 Bit Datenpuffer (keine FIFO!). Dazu verwendet es acht "parallelgeschaltete" Block RAMs des FPGAs mit jeweils 1 K x 4 Bit Kapazität, die zunächst in der Komponente ramb4_s32 (Datei ramb4_s32.vhd) entsprechend der Notation der Block RAMs der Xilinx Bibliothek zusammen gefasst werden.
Von außen kann auf die Daten im Puffer nicht wahlfrei zugegriffen werden. Stattdessen wird vor jedem Transfer zunächst über den RST-Eingang der interne Adresszähler zurü:ckgesetzt. Dann können Daten mit PUSH hineingeschrieben und mit POP ausgelesen werden, wobei sich der Adresszähler immer um 1 erhöht.
 
CLK Takteingang (PCI-Takt)
RST Zurücksetzen der internen Adresszähler auf den Pufferanfang (Adresse 0). Danach ist der Puffer bereit, Daten zu speichern oder seinen Inhalt auszugeben.
DATA_IN Daten-Eingangsport
DATA_OUT Daten-Ausgangsport
PUSH Durch Setzen von PUSH werden mit der nächsten steigenden Tanktflanke die Daten von DATA_IN in den aktuellen Pufferplatz übernommen und der interne Adresszähler addr_req um 1 erhöht.
POP Durch Setzen von POP wird ein neues Datenwort aus dem Puffer angefordert. Es steht mit der nächsten steigenden Taktflanke (die auf die Tankflanke folgt, in der POP='1' erkannt wurde), auf DATA_OUT bereit. Außerdem wird der interne Adresszähler ack_req inkrementiert.
ACK Der Puffer enthält einen zusätzlichen zweiten Adresszähler addr_ack, der mit ACK hochgezählt werden kann.
BACK_UP Durch Setzen des BACK_UP-Eingangs übernimmt der Adresszähler ack_req den Inhalt des zweiten Adresszählers addr_ack.
 
Bemerkungen:
  1. Die Timingvorgaben des PCI-Interfaces fordern, dass das neue Datenwort schon im auf die Anforderung (M_SRC_EN-Signal des Interfaces => POP-Signal des Puffers) folgenden Takt bereitsteht.
    Die Block RAMs verfügen über ein Ausgangslatch, das sich mit dem EN-Eingang der Block RAM Module steuern lässt und mit dessen Hilfe diese Bedingung eingehalten werden kann: Während am RAM schon eine neue Adresse anliegt und intern das entsprechende Datenwort bereitgestellt wird (vor dem Latch), hält der Ausgang des Block RAMs (hinter dem Latch) noch das Datenwort der vorherigen Adresse.
    Mit POP wird also ein neues (das intern schon bereitstehende) Datenwort in das Ausgangslatch übernommen und zugleich die Adresse des nachfolgenden Datenworts berechnet und dieses einen Takt später intern vom RAM zur Übernahme in das Ausgangslatch bereitgestellt. Damit kann schon in dem auf ein gestztes POP folgenden Takt ein neues Datenwort geliefert werden.
    Ohne diesen Latch-Mechanismus (EN-Eingang der Block RAM Module immer auf 1) bräuchte man zwei Takte für ein neues Datenwort: Nachdem POP gesetzt wäre, würde im nachfolgenden Takt erst die neue Adresse, d.h. die Adresse des benötigeten Datenworts berechnet. Noch einen Takt später stünde dann das Datenwort bereit.
     
  2. Der Puffer braucht den zweiten Adresszähler addr_ack, weil bei einem PCI-Schreibzugriff (=Puffer-Lesezugriff) das PCI-Interface dem tatsächlichen Busgeschehen um bis zu zwei Takte voraus sein kann. Oder anders gesagt: Nicht alle Daten, die aus dem Puffer gelesen werden (Adresszähler addr_req), werden auch erfolgreich über den PCI-Bus übertragen. Deshalb zählt der Adresszähler addr_ack mit dem ACK-Signal die erfolgreich gesendeten Datenwörter mit. Kommt es zu einer Transferunterbrechung durch das Target oder die Busarbitrierung, kann mit dem BACK_UP-Signal der Adresszähler addr_req korrigiert und dann an der richtigen Stelle mit dem Transfer fortgefahren werden.
    Unten wird noch eine ressourcensparendere Variante für diese Adressenkorrektur beschrieben.

Der Entwurf des PCI-Masters

Die Verhaltensbeschreibung des PCI-Masters in der Koponente userapp (Datei userapp.vhd) ist im Quellcode in mehrere Teile untergliedert:

1. Target-Steuerung und Target-Register

In diesem Abschnitt werden zunächst die Targetschnittstelle (Modul target_control) instanziiert und die Register pci_addr (Quell- bzw. Zieladresse) und xfer_count (Transfer-Zähler) beschrieben mit einer Möglichkeit zum Inkrementieren (inc_pci_addr) bzw. Dekrementieren (dec_xfer_count).

Das Interrupt-Flag intr_n_flag wird durch das Signal req_interrupt nach Abschluss eines Transfers auf 0 gesetzt und nach einem Auslesen über die Target-Schnittstelle (fallende Flanke von oe_intr_n) wieder in den Ruhezustand auf 1 gesetzt.

Das Befehlsregister besteht aus dem Signal dir, in dem die Richtung des angeforderten Transfers gespeichert ist. Ein Schreibzugriff auf das Befehlsregister startet zugleich den Transfer (Signal start).

Bei Lesezugriffen auf die Register über die Target-Schnittstelle müssen schließlich noch die Registerinhalte auf den ADIO-Bus gelegt werden.

2. Der Transfer-Steuerautomat


Abb.: Transfer-Steuerautomat

Die Abbildung zeigt den Graphen des Automaten, der den Bursttransfer abwickelt.
 

IDLE_S ist der Ruhezustand. Mit dem Signal start beginnt ein Bursttransfer.
REQ_S Im Zustand REQ_S (Request) wird das REQUEST-Signal des PCI-Interfaces für diesen einen Takt gesetzt und damit ein Transferzyklus eingeleitet. Im Falle eines PCI-Schreibtransfers (=Puffer lesen) wird außerdem POP gesetzt, damit der Puffer das erste Datenwort an seinem Ausgang bereitstellt.
XFER_S Der gesamte Transfer im engeren Sinn spielt sich im Transferzustand XFER_S ab und kann in drei Phasen unterteilt werden, die anhand der entsprechende Ausgangssignale des LogiCore PCI Interfaces unterschieden werden:
  • Warten:
    Mit dem REQUEST-Signal wurde ein Transferzyklus eingeleitet. Nun muss abgewartet werden, bis der PCI-Bus zugeteilt wurde und das PCI-Interface in die nächste Phase eintritt.
  • Adressphase:
    Das Signal M_ADDR_N (negiert) zeigt den Eintriff in die Adressphase an. Solange M_ADDR_N = 0 ist, wird die Zieladresse (pci_addr) auf den ADIO-Bus gelegt.
  • Datenphase:
    Der Eintritt in die Datenphase signalisiert das Signal M_DATA. Abhängig davon, ob ein PCI-Schreibtransfer oder -Lesetransfer stattfindet (was jederzeit durch das Signal dir festgestellt werden kann), spielen zwei Signale eine entscheidene Rolle:
    • PCI-Lesetransfer (dir = 0): Das Signal M_DATA_VLD zeigt an, dass die Daten auf dem ADIO-Bus gültig und in den Puffer zu übernehmen sind.
    • PCI-Schreibtransfer (dir = 1): Solange M_DATA = 1 ist, ist der Datenausgang des Puffers mit dem ADIO-Bus verbunden. Das erste zu übertragene Datenwort steht schon bereit. Beginnt der Bursttransfer, setzt das PCI-Interface das Signal M_SRC_EN, mit dem es für den folgenden Takt das nächste Datenwort anfordert. Ein erfolgreich gesendetes Datenwort wird mit M_DATA_VLD angezeigt.
Mit der fallenden Flanke von M_DATA endet die Transferphase.
DEAD_S Wenn die Datenphase im Zustand XFER_S aufgrund eines schweren Fehlers (Target Abort oder Master Abort) verlassen wurde, geht der Steuerautomat in den Fehlerzustand DEAD_S.
OOPS_S Der Zustand XFER_S wurde verlassen, weil der Transfer abgeschlossen wurde (done = 1) oder weil er unterbrochen wurde durch die Busarbitrierung oder wegen eines Target Disconnects (done = 0). In diesem Fall wird in den Zustand REQ_S gesprungen und ein neuer Transferzyklus gestartet, um den Gesamttransfer fortzusetzen.
Handelt es sich um einen PCI-Schreibtransfer (=Puffer lesen), wird der Oops-Zustand genutzt, um den Adresszähler im Puffer um die Anzahl der aus dem Puffer gelesenen, aber nicht wirklich übertragenen Daten zu korrigieren (Signal BACK_UP des Puffers). Bei einem PCI-Lesetransfer oder nach Abschluss des Gesamttransfers ist dieser Zustand überflüssig, wird aber zur Vereinfachung trotzdem durchlaufen.

Signale für das PCI-Interface und die Target-Register

Auf den Automaten folgt ein Abschnitt im Sourcecode, in dem verschiedene Steuersignale für das PCI-Interface erzeugt werden.
In der Adressphase des Transfers wird die Zieladresse pci_addr auf den Bus durchgeschaltet und auf M_CBE der richtige PCI-Befehl (Memory Read / Memory Write) eingestellt.

Um dem Initiator-Zustandsautomaten des PCI-Interfaces zu signalisieren, dass der laufende Transfer abgeschlossen werden soll, weil das Ende des Gesamttransfers erreicht wird, wird das COMPLETE-Signal gesetzt. Dabei müssen die letzten drei Datenwörter eines Transfers bzw. Transfers, die von vornherein oder nach Unterbrechung nur 1, 2 oder 3 Datenwörter umfassen, besonders betrachtet werden.

Der Transferzähler xfer_count und der Zieladresszähler pci_addr wird mit jedem gelesenen oder mit jedem erfolgreich versendeten Datenwort, also durch M_DATA_VLD, dekrementiert bzw. inkrementiert. Nach einem erfolgreich abgeschlossenen Gesamtransfer (xfer_count = 0 bzw. done = 1) wird das Interrupt-Flag gesetzt und ein Interrupt ausgelöst.

Anbindung des Puffers

Die Anbindung des Puffers kann mit den Kommentaren im Sourcecode dem vorher Gesagten leicht nachvollzogen werden und braucht daher hier nicht nochmals erläutert werden.

Abschlussbemerkungen und Optimierungshinweise

  1. Das Design hat keine Logik zur Behandlung von unvorhergesehenen Fehlern. Beispielsweise gibt es keine Möglichkeit, den Zustand DEAD_S wieder zu verlassen. Weiter entwickeltere Entwürfe könnten detailliertere Statusinformationen bereitstellen, einen Reset-Befehl in der Target-Schnittstelle einbauen und einen Interrupt gerade auch im Fehlerfall auslösen.
    Gelangt dieses Design in den Zustand DEAD_S während eines Transfers, wartet der Gerätetreiber vergeblich auf den "erlösenden" Interrupt.
  2. Der Zähler addr_ack für die tatsächlich übertragenen Daten im Puffer und der Transferzähler xfer_count laufen völlig synchron zueinander. Sie können daher zusammengefasst werden.
  3. Bei PCI-Schreibtransfers kann gerade bei großen Puffern und Transferlängen die Verwendung zweier Zähler für die bereitgestellten und die tatsächlich übertragenen Daten sehr ressourcenaufwendig werden. Der LogiCore PCI Design Guide verwendet in seinen Referenzdesigns einen anderen Ansatz, den "Oops-Counter" (Seite 14-16f), der dort zwar als Automatenfunktion angegeben, aber nicht erklärt wird. Daher an dieser Stelle iene Erläuterung:
    Darauf aufbauend, dass die Differenz zwischen den bereitgestellten, aber (noch) nicht gesendeten Daten maximal zwei betragen kann (Das ist die Pipelinetiefe des PCI Interfaces.), verfolgt der Automat während der Transfer-Phase das Verhältnis zwischen M_SRC_EN (angeforderte Daten) und M_DATA_VLD (übertragene Daten). Dabei sind vier Fälle zu unterscheiden:
    • M_SRC_EN = 1 und M_DATA_VLD = 0
      Daten angefordert aber nichts übertragen => Differenz erhöht sich um 1.
    • M_SRC_EN = 0 und M_DATA_VLD = 1
      Daten übertragen und keine weiteren angefordert => Differenz erniedrigt sich um 1.
    • (M_SRC_EN = 0 und M_DATA_VLD = 0) oder (M_SRC_EN = 0 und M_DATA_VLD = 0)
      => Differenz bleibt konstant
    Am Ende des Transfers wird dann der Adresszähler des Puffer während des OOPS_S-Zustands des Steuerautomaten um die ermittelte Differenz (0, 1, oder 2) angepasst.


Autor: gkemnitz, Letzte Änderung: 14.04.2011 15:10:00


 TU Clausthal 2020  Impressum