Hardwareentwurf eines einfacher PCI-Initiators
Die für dieses Projekt modifizerten Dateien des LogiCore PCI Interface und die darüber hinaus benötigten Dateien sind:
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
C/BE[3:0] | PCI-Befehl |
0000 | Interrupt Acknowledge ein spezieller ix86-spezifischer Buszyklus, siehe Kapitel Der PCI Configuration Header |
0001 | Special Cycle erlaubt es, bestimmte Nachrichten (Messages) auf den Bus abzugeben. |
0010 | I/O Read Lesen von I/O-Ports |
0011 | I/O Write Schreiben auf I/O-Ports |
0100 | reserviert |
0101 | |
0110 | Memory Read Lesen im "normalen" Speicheradressraum |
0111 | Memory Write Schreiben im "normalen" Speicheradressraum |
1000 | reserviert |
1001 | |
1010 | Configuration Read Lesen im Konfigurationspeicher |
1011 | Configuration Write Schreiben in den Konfigurationsspeicher |
1100 | Memory 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. |
1101 | Dual 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. |
1110 | Memory Read Line ein spezieller Lesebefehl für optimierten Speicherzugriff: Eine ganze Cache-Zeile soll aus dem Speicher gelesen werden. |
1111 | Memory 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 |
0x00001000 | 1110 | nur das Byte 0x1000 |
0x000095A2 | 0011 | Bytes 0x95A2 und 0x95A3 |
0x00001510 | 0000 | alle vier Bytes 0x1510 bis 0x1513 |
0x0000AE21 | 0001 | 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:
|
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).
|
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
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 );