Projekte.PCI-Tutorial-STIEntwurf (Struktur)


Hardwareentwurf eines einfacher PCI-Initiators

Übersicht

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

In diesem Kapitel wird der Entwurf eines möglichst einfachen PCI-Busmasters (in der PCI-Terminologie: Initiator) beschrieben. Seine Aufgabe ist es, selbstständig den PCI-Bus anzufordern und das durch die DIP-Schalter dargestellte Byte an den Daten-I/O-Port der parallelen Schnittstelle des Rechners zu übertragen, sobald sich die DIP-Schaltereinstellung auf dem PCI-Board geändert hat. Da I/O-Portzugriffe im Gegensatz zu I/O-Speicherzugriffen nicht immer auf voller 32 Bit PCI-Busbreite erfolgen (der Datenport der parallelen Schnittstelle ist nur 1 Byte groß), wird hier auch die Bytemaskierung kurz erklärt.

PCI-Commands und Byte Enables

PCI-Commands

Ein PCI-Busmaster muss außer der Zieladresse in der Adresspahse angeben, was für eine Transaktion er durchführen möchte. Bei Datentransfers ist die Richtungsangabe unerlässlich (Soll dem adressierten Target Daten geschickt oder vom adressierten Target Daten gelesen werden?) sowie die Angabe, auf welchen Adressraum sich die Zieladresse bezieht (Konfigurationsspeicher, I/O-Ports oder der "normale" Speicheradressraum?). Außerdem gibt es noch erweiterte Modi für den Speichertransfer und spezielle Buszyklen.
Der Busmaster gibt die Art des Transfers während der Adressphase auf den PCI-Busleitungen C/BE[3:0] gemäß folgender Tabelle an:

C/BE[3:0]PCI-Befehl
0000Interrupt Acknowledge
ein spezieller ix86-spezifischer Buszyklus, siehe Kapitel Der PCI Configuration Header
0001Special Cycle
erlaubt es, bestimmte Nachrichten (Messages) auf den Bus abzugeben.
0010I/O Read
Lesen von I/O-Ports
0011I/O Write
Schreiben auf I/O-Ports
0100reserviert
0101
0110Memory Read
Lesen im "normalen" Speicheradressraum
0111Memory Write
Schreiben im "normalen" Speicheradressraum
1000reserviert
1001
1010Configuration Read
Lesen im Konfigurationspeicher
1011Configuration Write
Schreiben in den Konfigurationsspeicher
1100Memory Read Multiple
ein spezieller Speicherlesebefehl für optimierten Speicherzugriff: Der Initiator möchte Daten aus dem Speicher im Umfang mehrerer Cache-Zeilen lesen. Wenn das Target (die Speichersteuerung) diesen Befehl unterstützt, wird es damit angewiesen, Speicherzeilen schon im voraus aus dem Speicher zu holen (prefetch) und für den Transfer bereitzuhalten.
1101Dual Address Cycle
In einem System mit 64 Bit Adressen benötigt ein 32 Bit Initiator zwei Adressphasen auf dem 32 Bit PCI-Bus, um die 64 Bit Zieladresse zu übertragen. Im ersten Schritt werden die unteren 32 Bit der Adresse zusammen mit dem "Dual Address Cycle"-Kommando geschickt, in der zweiten Phase dann die oberen 32 Bit mit dem eigentlichen PCI-Befehl.
1110Memory Read Line
ein spezieller Lesebefehl für optimierten Speicherzugriff: Eine ganze Cache-Zeile soll aus dem Speicher gelesen werden.
1111Memory Wirte-and-Invalidate
ein spezieller Speicherbefehl für optimierten Speicherzugriff: Der PCI-Initiator möchte Daten vom Umfang einer ganzen Cache-Zeile in den Speicher schreiben. Nun kann es sein, dass gerade diese Zeile im Cache oder im Write-Back Cache der PCI-Bridge vorhanden und als dirty markiert ist, also noch in den Speicher zurückgeschrieben werden müsste. Da der PCI-Initiator aber sowieso die gesamte Zeile im Speicher mit seinen Daten überschreiben wird, kann das Rückschreiben aus dem Cache in den Speicher vor dem PCI-Transfer des Initiators eingespart werden. Es genügt, die betreffende Zeile im Cache als ungültig zu markieren.

Fazit: Für die meisten Anwendungen sind nur die PCI-Kommandos I/O-Read und I/O-Write bzw. Memory Read und Memory Write wichtig.

Byte Enables

Während im I/O-Speicher die Speicherregister von vornherein auf 32 Bit Zugriffe ausgelegt sind, kommt es bei den I/O-Ports oft darauf an, nur einzelne Bytes zu adressieren.

Beispiel: I/O-Ports der parallelen Schnittstelle im PC (Basisadresse z.B. 0x378)

Offset 0x00: Datenregister (8 Bit)
Offset 0x01: Statusregister (8 Bit)
Offset 0x02: Steuerregister (8 Bit)
=> Ein PCI-Zugriff auf voller Busbreite würde immer alle Register erfassen.
Für den Zugriff auf einzelne Bytes gibt es die PCI-Busleitungen C/BE[3:0], mit denen man Bytes innerhalb des des 32 Bit PCI-Datenworts maskieren kann (Enabled = 0). Man spricht auch von den vier Datenpfaden des PCI-Bus, die mit den Leitungen C/BE[3:0] ausgewählt werden.
Bei einer I/O-Transaktion hat die I/O-Startadresse, die in der Adressphase der Transaktion übermittelt wird, folgendes Format:
ADDR[31:2] gibt die Adresse des Doppelwortes im I/O-Portraum an.
ADDR[1:0] identifiziert das letzte signifikante Byte (das Startbyte) innerhalb des Doppelwortes, das der Initiator übertragn möchte.
Die Byte Enables, die in der Datenphase gesetzt werden, identifizieren das letzte signifikante Byte innerhalb des auf den Datenleitungen übetragenen Doppelwortes (das gleiche Startbyte, das ADDR[1:0] zuvor angegeben hat) sowie weitere zusätzliche Bytes innerhalb des adressierten Doppelworts. Es ist verboten und amacht auch keinen Sinn für den Initiator, Byte Enables zu setzen, die ein Byte mit geringerer Signifikanz als das durch ADDR[1:0] angegebene, freigeben. Tritt dieser verbotene Fall trotzdem auf, antwortet das Target mit einem Target Abort.

Beispiel: Erlaubte I/O-Portadressierungen

ADDR[31:0]C/BE[3:0]Ergebnis
0x000010001110 nur das Byte 0x1000
0x000095A20011 Bytes 0x95A2 und 0x95A3
0x000015100000 alle vier Bytes 0x1510 bis 0x1513
0x0000AE210001 Bytes 0xAE21 bis 0xAE23

Konfiguration des LogiCore PCI Interfaces

Der Initiatorteil des LogiCore PCI Interfaces braucht nicht explizit aktiviert werden. Daher behalten in der Konfigurationsdatei cfg.vhd alle Optionen ihre Standardeinstellungen bei. Insbesondere werden kein I/O-Speichebereich und auch keine I/O-Ports belegt.

Initiator-Signale des LogiCore PCI Interfaces

Die Signale werden nur so weit erklärt, wie es für Single Transfer Initiator Designs notwendig ist. Die exakten vollständigen Definitionen sind dem [Logic Core PCI Design Guide] zu entnehmen.

Ausgangssignale aus dem LogiCore PCI Interface

M_ADDR_N
(PCI → Userapp.)
M_ADDR_N (negiert!) zeigt an, dass sich der Initiatorautomat im Adresszustand befindet. Die Userapplikation muss in dieser Zeit eine gültige Adresse auf dem ADIO-Bus und einen gültigen PCI-Befehl auf dem M_CBE-Bus erzeugen.
M_DATA
(PCI → Userapp.)
Ist M_DATA gesetzt, befindet sich der Initiatorautomat im Datentransferzustand.
M_DATA_VLD
(PCI → Userapp.)
M_DATA_VLD hat zwei Bedeutungen, die von der Richtung des Datentransfers abhängen:
  • Bei einem Lesezyklus (PCI → Userapp.) sind die Daten auf dem ADIO-Bus gültig, wenn M_DATA_VLD gesetzt ist.
  • Innerhalb eines Schreibzyklus (Userapp. → PCI) signalisiert M_DATA_VLD, dass eine Datenphase auf dem PCI-Bus abgeschlossen wurde.
CSR(39 downto 0)
(PCI → Userapp.)
CSR ermögilcht einen Lesezugriff auf die auf die jeweils 16 Bits des Command- und Status-Registers im PCI Configuration Header. Die Bits 32 bis 39 sind Transaktionsstatussignale, die sich (bis auf Bit 39) unmittelbar aus den PCI-Bus-Steuerleitungen ergeben:
Bit 32: Data Transfer
Bit 33: Transaction End
Bit 34: Normal Termination
Bit 35: Target Termination
Bit 36: Disconnect Without Data
Bit 37: Disconnect With Data
Bit 38: Target Abort
Bit 39: Master Abort
Details siehe LogiCore PCI Design Guide und PCI Spezifikation.

Eingangssignale in das LogiCore PCI Interface

M_CBE(3 downto 0)
(Userapp. → PCI)
Dieser Eingang erwartet einen gültigen PCI-Befehl während der Adressphase des Transfers (M_ADDR_N = '0') und die passenden Bytemaskierungen während den Datenphasen.
M_WRDN
(Userapp. → PCI)
M_WRDN gibt die gewünschte Richtung des Transfers an (und darf nicht im Widerspruch zum PCI-Befehl stehen).
  • '1' : schreiben (Userapp. → PCI)
  • '0' : lesen (PCI → Userapp.)
Während eines Transfers sollte M_WRDN auf konstantem Wert bleiben.
M_READY
(Userapp. → PCI)
Mit M_READY signalisiert die Userapplikation, ob sie bereit ist, Daten zu transferieren. Wenn nicht, kann sie mithilfe dieses Signals Wartezyklen zwischen der Adressphase und der ersten Datenphase auf dem PCI-Bus einfügen. Einmal gesetzt, darf innerhalb der Datenphase eines Bursttransfers M_READY nicht wieder gelöscht werden.
REQUEST
(Userapp. → PCI)
REQUEST fordert das PCI Interface auf, den PCI-Bus anzufordern und einen Initiator-Transfer einzuleiten.
COMPLETE
(Userapp. → PCI)
Dieses Signal informiert den Initiatorautomaten, dass er die laufende Transaktion beenden soll. Ein einmal gesetztes COMPLETE-Signal muss bis zum Ende der Datenphase (M_DATA = '1') gesetzt bleiben.

Der Hardware-Entwurf des Initiators

Vorarbeit

Der Entwurf soll die DIP-Schalter, den User-Taster und eine der 7-Segment-Anzeigen ansteuern. Dazu müssen die Anschlüsse bzw. die sie repräsentierenden Signale von dem Top-Level-Modul pcim_top zur Komponente userapp, in der der eigentliche Beispielentwurf geschieht, "durchgereicht" werden. Dies erfordert Ergänzungen den Portbeschreibungen in den Dateien userapp.vhd und pcim_top.vhd.
    entity ... is
      port (


        [...]

        USER_BUTTON     : in std_logic;
        ONE_DIGIT       : out std_logic_vector(6 downto 0);
        DIP_SWITCHES    : in std_logic_vector(7 downto 0)
      );
    end ...;
Die passenden Pinbelegungen müssen in der User Constraint Datei xc2s200fg456_32_33.ucf vereinbart werden:
    NET "USER_BUTTON"       LOC = "B1";

    NET "ONE_DIGIT<0>"   LOC = "E10" ;  //DISPLAY.1A
    NET "ONE_DIGIT<1>"   LOC = "E9" ;   //DISPLAY.1B
    NET "ONE_DIGIT<2>"   LOC = "E8" ;   //DISPLAY.1C
    NET "ONE_DIGIT<3>"   LOC = "E6" ;   //DISPLAY.1D
    NET "ONE_DIGIT<4>"   LOC = "E7" ;   //DISPLAY.1E
    NET "ONE_DIGIT<5>"   LOC = "F11" ;  //DISPLAY.1F
    NET "ONE_DIGIT<6>"   LOC = "E11" ;  //DISPLAY.1G

    NET "DIP_SWITCHES<7>"   LOC = "D2" ;  //DIP7
    NET "DIP_SWITCHES<6>"   LOC = "C1" ;  //DIP6
    NET "DIP_SWITCHES<5>"   LOC = "F4" ;  //DIP5
    NET "DIP_SWITCHES<4>"   LOC = "G5" ;  //DIP4
    NET "DIP_SWITCHES<3>"   LOC = "F5" ;  //DIP3
    NET "DIP_SWITCHES<2>"   LOC = "E3" ;  //DIP2
    NET "DIP_SWITCHES<1>"   LOC = "F3" ;  //DIP1
    NET "DIP_SWITCHES<0>"   LOC = "E4" ;  //DIP0

    NET "DIP_SWITCHES<7>"   PULLUP;
    NET "DIP_SWITCHES<6>"   PULLUP;
    NET "DIP_SWITCHES<5>"   PULLUP;
    NET "DIP_SWITCHES<4>"   PULLUP;
    NET "DIP_SWITCHES<3>"   PULLUP;
    NET "DIP_SWITCHES<2>"   PULLUP;
    NET "DIP_SWITCHES<1>"   PULLUP;
    NET "DIP_SWITCHES<0>"   PULLUP;

Transfer-Parameter

Die Parameter der PCI-Transaktion werden, da sie in dieser Anwendung immer gleich sind, durch die Zuweisung von Konstanten eingestellt.
    signal dir               : std_logic;
    signal command, bytemask : std_logic_vector(3 downto 0);
    signal write_address     : std_logic_vector(31 downto 0);

    [...]

    -- Voreinstellungen
    write_address <= x"00000378";  -- Basisadresse parallele Schnittstelle (8 Bit I/O-Port)
    dir           <= '1';          -- Transferrichtung Schreiben
    command       <= "001" & dir;  -- I/O Transfer 
    bytemask      <= "1110";       -- 8 Bit-Zugriff: Nur das unterste Byte schreiben.
Die Basisadressen der parallelen Schnittstellen des PCs werden beim Systemstart angezeigt. Unter Linux können sie auch mit
    cat /proc/ioports
erfragt werden.
Da das Signal M_WRDN aus Optimierungsgründen nicht mit einem konstanten Wert belegt werden darf, wird das dir-Signal über ein Flipflop an M_WRDN zugewiesen.
    process (CLK, RST)
    begin
      if RST = '1' then
        M_WRDN <= not dir;
      elsif CLK'event and CLK = '1' then
        M_WRDN <= dir;
      end if;
    end process;

Der Steuerautomat


Abb.: Der Steuerautomat für den PCI-Transfer in der Userapplikation

Die Abbildung zeigt den Automaten, der die PCI-Transaktion im Zusammenspiel mit dem PCI Interface koordiniert. Obwohl in diesem Beispielentwurf nur der Schreibtransfer benutzt wird, ist der Fall Lesezugriff in diesem Diagramm mit eingezeichnet und auch im Quelltext mit allen zugehörigen Steuersignalen auskommentiert ausgeführt.
Ausgangspunkt ist der Ruhezustand IDLE_S. Wenn das Startsignal start gesetzt ist, geht der Automat in den REQ_S (Request: Die Transferanforderung an das PCI Interface wird hier erstmals gesetzt.) und im nächsten Takt gleich weiter in den Zustand READ_S bzw. WRITE_S in Abhängigkeit von der gewünschten Transferrichtung, die das Signal dir repräsentiert. In diesem Zustand wird der gesamte PCI-Transaktion ausgeführt, im wesentlichen gesteuert durch die Signale M_DATA und M_ADDR_N, die die Adress- und die Datenphase der Transaktion anzeigen. Am Ende der Datenphase (m_data_fell = '1') gibt es drei mögliche Folgezustände:

  • DONE_S: Die Transaktion wurde erfolgreich abgeschlossen.
  • RETRY_S: Das Target hat mit einem Disconnect Without Data geantwortet. Der Initiator soll es dann nochmals probieren. Auch wenn die PCI Spezifikation hier keine Grenze setzt, ist es sinnvoll, einen Retry-Zähler einzubauen, um die Transferversuche bei anhaltendem Misserfolg den Transferversuch abzubrechen. (Die wiederholte Antwort "Disconnect Without Data" kann z.B. durch einen Fehler in der Targetlogik hervorgerufen werden.)
  • DEAD_S: Bei einem schweren Fehler, d.h. Target Abort oder Master Abort (Fehler im eigenen PCI Interface), tritt der Automat in den Fehlerzustand DEAD_S ein. In diesem Entwurfsbeispiel führt nur ein Gesamtreset oder ein Druck des User-Buttons wieder zurück in den IDLE_S-Zustand.
Bemerkung 1: Die Zustände READ_S und WRITE_S sind symmetrisch. Man kann sie auch zusammenfassen zu einem Transferzustand XFER_S o.ä. und zwischen LEsen und Schreiben direkt mit dem Signal dir unterscheiden.

Bemerkung 2: Der Automat ist nicht minimal. Die Zustände REQ_S, DONE_S und RETRY_S können im Prinzip herausoptimiert werden. Bei komplexeren Anwendungen jedoch braucht man sie meist für Transfervorbereitung und -abschluss. Darum sind sie auch hier vorhanden.

Der VHDL-Code in vhdl/userapp.vhd setzt diesen Automaten wie beschrieben um und wird daher hier nicht nochmals angegeben.

Register für die DIP-Schalter und Erzeugung des Startsignals start

Die Anwendung soll die DIP-Schalter kontinuierlich überwachen und bei Veränderung eine neue Schreibtransaktion einleiten.
    signal write_reg : std_logic_vector(7 downto 0);
    signal reg_oe    : std_logic;
    signal start     : std_logic;

    [...]

    process (CLK, RST)
    begin
      if RST = '1' then
        write_reg <= DIP_SWITCHES;
        start <= '0';
      elsif CLK'event and CLK = '1' then
        if write_reg /= DIP_SWITCHES and state = IDLE_S then
          write_reg <= DIP_SWITCHES;
          start <= '1';
        else
          start <= '0';
        end if;
      end if;
    end process;    
Die Ausgabe von write_reg auf den ADIO-Bus und damit an das PCI Interface erfolgt, wenn sich der Steuerautomat im WRITE_S-Zustand befindet und das PCI Interface in der Datenphase des Transfers.
  reg_oe   <= '1' when state = WRITE_S and M_DATA = '1' else '0';

  -- Daten auf den ADIO-Bus bei Schreibtransfers
  ADIO    <=  x"000000" & write_reg when reg_oe = '1' else
              (others => 'Z');

Erkennung der fallenden Flanke von M_DATA

    signal m_data_delay, m_data_fell : std_logic;

    [...]

    process (RST, CLK)
    begin
      if RST = '1' then
        m_data_delay <= '0';
      elsif CLK'event and CLK = '1' then
         m_data_delay <= M_DATA;
      end if;
    end process;

    m_data_fell <= m_data_delay and not M_DATA;

"Rückgabewert" der beendeten Transaktion

In der Adressphase werden die Signale fatal und retry zurückgesetzt und während der Datenphase durch Auswertung des PCI Interface Status Busses ggf. gesetzt.
    process (RST, CLK)
    begin
      if RST = '1' then
        fatal <= '0';
        retry <= '0';
      elsif CLK'event and CLK = '1' then
        if M_ADDR_N = '0' then
          fatal <= '0';
          retry <= '0';
        elsif M_DATA = '1' then
          fatal <= CSR(39) or CSR(38);  -- Master / Target Abort
          retry <= CSR(36);             -- Target Disconnect Without Data
        end if;
      end if;
    end process;

Transferanforderung und -beendigung

Im Zustand REQ_S wird der PCI-Bus angefordert und eine Transaktion eingeleitet. Das Signal COMPLETE teilt dem PCI Interface mit, dass die aktuelle Transaktion beendet werden soll. Im Falle von Single Transfers darf es nicht später als einen Takt nach REQUEST gesetzt und erst wieder gelöscht werden, wenn der Transfer abgeschlossen ist.
  REQUEST <= '1' when state = REQ_S else '0';

  process (CLK, RST)
  begin
    if RST = '1' then
      COMPLETE <= '0';
    elsif CLK'event and CLK = '1' then
      case state is
        when REQ_S | READ_S | WRITE_S  => COMPLETE <= '1';
        when others                    => COMPLETE <= '0';
      end case;
    end if;
  end process;

Zieladresse, PCI Command und Byte Enables

In der Adressphase wird die Zieladresse write_address auf den ADIO-Bus geschrieben und das PCI-Kommando an M_CBE übergeben. In der Datenphase (und sonst auch) wird M_CBE mit den Byte Enables bedient.
  ADIO  <= write_address when M_ADDR_N = '0' and dir = '1' else (others => 'Z');
  M_CBE <= command when M_ADDR_N = '0' else bytemask;

Beschaltung der verbleibenden Eingangssignale des LogiCore PCI Interfaces

  -- Master: always ready
  process (RST, CLK)
  begin
    if RST = '1' then
      M_READY <= '0';
    elsif CLK'event and CLK = '1' then
      M_READY <= '1';
    end if;
  end process;
        

  -- Card Bus CIS Pointer Daten / Subsystem ID Daten
  SUB_DATA    <= x"00000000" ;

  -- ADIO-Bus immer mit LogiCore PCI Interface verbunden
  KEEPOUT     <= '0'; -- ADIO-Bus immer enabled

  -- Steuersignale für Konfigurationstransaktionen
  C_READY     <= '1';  
  C_TERM      <= '1';

  -- Initiator Steuersignale
  REQUESTHOLD <= '0';
  CFG_SELF    <= '0';

  static_config: process (CLK, RST)
  begin
    if RST = '1' then 

      -- hier die invertierten Default-Werte, Kommentare siehe unten
      S_ABORT <= '1';
      S_READY <= '0';
      S_TERM  <= '0';
      INTR_N <= '0';

    elsif (CLK'event and CLK='1') then 

      -- Target-Eingangssignale
      S_ABORT <= '0';
      S_READY <= '1';
      S_TERM  <= '1';

      -- keine Interrupt-Funktion
      INTR_N  <= '1';

    end if;
  end process;

Zusatzlogik zum Debuggen

Zum Debuggen wird die Zustandsnummer des aktuellen Zustands des Stuerautomaten auf der 7-Segment-Anzeige ausgegeben.
    component displayrom 
      port (
        ADDR : in std_logic_vector(3 downto 0);
        DATA : out std_logic_vector(6 downto 0)
      );
    end component;
  
    signal start_nr : std_logic_vector(3 downto 0);

    [...]

    with state select
      state_nr <= "0000" when IDLE_S,
                  "0001" when REQ_S,
                  "0010" when READ_S,
                  "0011" when WRITE_S,
                  "0100" when DEAD_S,
                  "0101" when RETRY_S,
                  "0110" when DONE_S,
                  "1111" when others;

    displayrom_inst : displayrom
      port map (
        ADDR => state_nr,
        DATA => ONE_DIGIT
      );


Autor: gkemnitz, Letzte Änderung: 14.04.2011 15:09:59


 TU Clausthal 2020  Impressum