PCI-Scan
-
Na gut. Letzter Versuch. Mit dem von XanClic geposteten Code kann man gezielt ein Byte aus dem "Config-Space" auslesen:
case 1: *value = inb(0xCFC + (reg & 3)); break;
Aber dazu muß u.U. die Portadresse geändert werden. Das ist nur typisch für den Zugriff auf (Hardware) Register.
P.S.: Im Internet werden Falschmeldungen zu Tatsachen einzig und alleine durch Verbreitung.
-
Bei mir sieht das nun praktisch wie folgt aus:
#include "pci.h" static uint32_t pci_config_read(uint32_t bus, uint32_t device, uint32_t func, uint32_t reg) { outportl(PCI_CONFIGURATION_ADDRESS, 0x80000000 | (bus << 16) | (device << 11) | (func << 8) | (reg & 0xFC )); // Real hardware ignores bits 0 and 1, and reads only bits 7-2. // The bit mask FCh = 11111100b ensures register numbers aligned with 4. return inportl(PCI_CONFIGURATION_DATA); } uint32_t pci_config_read_32 (uint32_t bus, uint32_t device, uint32_t func, uint32_t reg) { return pci_config_read(bus, device, func, reg); } uint16_t pci_config_read_16 (uint32_t bus, uint32_t device, uint32_t func, uint32_t reg) { return pci_config_read(bus, device, func, reg) & 0xFFFF; } uint8_t pci_config_read_8 (uint32_t bus, uint32_t device, uint32_t func, uint32_t reg) { return pci_config_read(bus, device, func, reg) & 0xFF; } void pci_config_write(uint32_t bus, uint32_t device, uint32_t func, uint32_t reg, uint32_t val) { outportl(PCI_CONFIGURATION_ADDRESS, 0x80000000 | (bus << 16) | (device << 11) | (func << 8) | (reg & 0xFC )); outportl(PCI_CONFIGURATION_DATA, val); }
Die write-Funktion benötigen für die BARs (siehe oben).
Bei tyndur ist der Schutz übrigens mit ... &=~11b und ... &11111100b m.E. übertrieben:
static dword pci_config_read(..., dword reg) { ... reg &= ~0x3; outl(... | ((reg & 0xFC))); ... }
Ich würde vorschlagen, eine von beiden Schutzmasken zu entfernen. Dann läuft der PCI Scan deutlich schneller.
-
Deine pci_config_read_16 und pci_config_read_8 können übrigens keine Werte, die nicht 4 Byte aligned sind, lesen. Nicht dass du dich z.B. irgendwann wunderst, dass du damit nicht die Device ID nicht auslesen kannst.
-
Ja, das ist richtig und mir zum Glück auch klar. Das ist noch nicht optimal. Die DeviceID habe ich mir z.B. so berechnen müssen: (pciDevVend&0xFFFF0000)>>16
Die Funktionen sind wirklich noch nicht bahnbrechend. Da könnte man z.B. noch einen Start-Parameter pos (von Byte 0 aus gerechnet) einführen mit 0,1,2,3. Würde das Sinn machen? Vielleicht sogar nur eine Funktion mit pci_config_read_value(bus,dev,func,reg,width,pos) und uint32_t als return type.
IRQ (Version 110 in Arbeit) klappt auch sehr gut (Vergleich PCI Scan mit BIOS-PCI-Auflistung per Foto), lediglich das IDE-Interface mit IRQ 14/15 wurde vom PrettyOS PCI Scan mit IRQ 0 angezeigt. Die beiden USB-Devices haben beide IRQ 9, so dass man das IRQ sharing hiermit erkennen kann.
Nun kommt das Thema BARs.
-
Zum Thema maskieren mit 0xFC eine Antwort bei osdev.org:
http://forum.osdev.org/viewtopic.php?f=1&t=21098The rule is, you can only read doublewords from configuration space. If you don't I have enough pieces of real hardware that make your code fail.
It is your choice whether you need the 0xfc or not.
-
Erhard Henkes schrieb:
Hier ist auch eine interessante Darstellung der letzten zwei Bits als "Type":
http://www.martin-schreiber.net/data/pages/progref/bussysteme/pci/configaddress.gif00b = Konfigurationszyklus
01b = Einheit ist hinter dieser Bridge. Diese kopiert den Inhalt von CONFIG_ADDRESS unverändert auf den Adress-/Datenbus
10b = ?
11b = ?Da ist was durcheinandergeworfen worden. Das Bild zeigt, was auf dem Bus übertragen wird, nicht das CONFIG_ADDRESS-Register.
Und zum 0xfc, lies dir einfach mal selbst Abschnitt 3.2.2.3.2. in der Spezifikation durch. Auf CONFIG_ADDRESS darfst du nur dwords schreiben und Bits 0 und 1 sind read-only und auf 0 hartverdrahtet. Ich würde es trotzdem drinlassen, weil auch Hardware buggy sein kann. Um genau zu sein, liest meine Funktion ja sogar immer von der abgerundeten Adresse und schiebt das Ergebnis hinterher so zurecht, dass ich genau das Register bekomme, das ich angefordert habe.
-
+gjm+ schrieb:
Aber dazu muß u.U. die Portadresse geändert werden. Das ist nur typisch für den Zugriff auf (Hardware) Register.
Der I/O-Port hat doch nix mit dem Konfigurationsadressraum zu tun. Ich schreibe die Adresse im Konfigurationsraum nach 0xCF8. Dann steht ab 0xCFC automatisch der Wert des adressierten DWords. Das heißt nicht, dass diese DWord in 0xCFC steht und ich an 0xCFD was völlig anderes auslese. In 0xCFC steht das niederwertigste Byte dieses DWords. In 0xCFD das zweite, in 0xCFE das dritte und in 0xCFF das höchstwertige Byte (Little Endian). Wenn ich also ein Word ab 0x02 auslesen möchte, fordert diese Linuxfunktion das DWord ab 0x00 an, ab 0xCFC steht dann dessen Wert. Die Linuxfunktion liest dann also ein Word ab 0xCFE ein, das heißt, das niederwertige Byte aus 0xCFE und das höherwertige Byte aus 0xCFF (geschieht automatisch mit inw).
Ich hoffe, ich habe deinen Einwand verstanden und hoffe auch, dass ich verständlich antworten konnte.
-
Sehr gut beschrieben.
XanClic schrieb:
Ich schreibe die Adresse im Konfigurationsraum nach 0xCF8.
Dann steht ab 0xCFC automatisch der Wert des adressierten DWords.Für die Bildung der "Adresse" stehen aber nur Bit 7-2 zur Verfügung. (Bit 1-0 sind reserviert). Somit geht der Bereich von "Adresse" nur von 0x00-0x3F. Die Darstellung des "Config-Space" mit einem "Offset" links oder rechts der Tabelle ist deshalb falsch. Hoffentlich war das verständlich. Didaktisch begabt bin ich leider nicht. Aber ich arbeite daran.
-
Die offizielle Spezifikation als falsch zu bezeichnen ist zumindest mutig.
-
+gjm+ schrieb:
Für die Bildung der "Adresse" stehen aber nur Bit 7-2 zur Verfügung. (Bit 1-0 sind reserviert).
Es gibt jede Menge Beispiele, in denen man z. B. in ein 32-Bit-Feld eine Adresse eintragen muss, wo aber die unteren 4 Bits oder so reserviert sind (siehe z. B. die OHCI-Spezifikation, Abschnitt 4.2.1 "Endpoint Descriptor Formats" und dort das Feld "TD Queue Tail Pointer", hier sind die unteren Bits nicht reserviert, sondern stehen für den Programmierer frei zur Verfügung). Im erwähnten Beispiel ist in den Bits 4 bis 31 eine Adresse einzutragen. Man trägt dort aber nicht die Bits 0 bis 27 der Adresse ein, sondern die Bits 4 bis 31. Dass die unteren 4 Bits von der Hardware ignoriert werden, heißt hier nur, dass die Adresse an 16 Bytes ausgerichtet sein muss.
Und genau so wird es bei PCI auch sein, dass die unteren 2 Bites ignoriert werden, heißt nur, dass die Adresse an 4 Bytes ausgerichtet sein muss.
-
Ich denke, inzwischen ist zumindest die Sachlage jedem klar. Daher gehe ich darauf nicht mehr ein. Der Grund, warum man in der Tat reg & 0xFC bzw. alternativ reg & ~0x03 verwenden sollte, ist hauptsächlich die mögliche Inkompatibilität mit ausgefallener Hardware bzw. mit Simulationsprogrammen wie qemu.
Nun bleibt die Frage, wie man zu der gewünschten Information gelangt. Hierbei muss noch folgendes passieren:
a) Der Offset zu Bit 0 muss angegeben werden. Hier gibt es zwei Varianten:
a1) Man akzeptiert "falsche" Register-Nummern und erhält den Offset durch reg % 4 (Methode bei tyndur)
a2) Man rundet "falsche" Register-Nummern ab und ignoriert den "Rest". Der Offset muss dann als separater Parameter (z.B. pos: 0,1,2,3) übergeben werden (diese Vorgehensweise gefällt mir didaktisch besser)b) Die Breite (byte, word, dword) muss angegeben werden (tyndur erledigt dies durch drei verschiedene Funktionen)
Ich neige inzwischen zur Kombination von a2) und b) im Rahmen einer einzigen Funktion mit den Parametern bus, dev, func, reg, pos, width, d.h. in der inneren lesenden Funktion nur "saubere" Register zulassen (z.B. bei Abweichung eine Warnung ausgeben, damit Bugs auffallen) und nur in der umhüllenden Funktion schieben und schneiden.
Das ist sicher Geschmacksache, ob man die tyndur-Variante (innere Funktion: "non-aligned" lesen und schieben; drei äußere Funktionen zum Schneiden) verwendet oder den von mir vorgeschlagenen weg einschlägt.
Man könnte alle drei Aufgaben (lesen, schieben, schneiden) sogar in einer einzigen Funktion verdichten. Das würde allerdings für Außenstehende nicht leicht verständlich.
Die tyndur-Methode (a1) hat allerdings einen praktischen Vorteil: Man kann das nicht direkt lesbare Register als Konstante durchreichen (tyndur - pcihw.h):
#define PCI_VENDOR_ID 0x00 #define PCI_DEVICE_ID 0x02 #define PCI_COMMAND 0x04 #define PCI_STATUS 0x06 #define PCI_REVISION 0x08 #define PCI_CLASS 0x0B #define PCI_SUBCLASS 0x0A #define PCI_INTERFACE 0x09 #define PCI_HEADERTYPE 0x0E #define PCI_BAR0 0x10 #define PCI_INTERRUPT 0x3C
Register und Offset sind darin bereits enthalten. Es fehlt aber noch die Breite!
Ideal wäre eine Funktion read(bus,dev,func,PCI_BLABLA), die alles erledigt.
Dazu benötigt man aber noch die Breite, vielleicht:#define PCI_VENDOR_ID 0x0002 #define PCI_DEVICE_ID 0x0202 ... #define PCI_INTERRUPT 0x3C01 ...
also Register/Offset vorne und Breite (length) in Bytes hinten. Dann könnte man mit read(bus,dev,func,PCI_BLABLA) alles in einem Schritt erledigen (auch ideal für eine Schleife, um eine Struktur zu füllen). Wäre das elegant oder eher schrecklich?
Bin etwas hin und her gerissen zwischen Didaktik (mehrere klare Schritte, dafür kompliziert) und einer optimalen praktischen Lösung (nur eine Funktion für alles, Konstante bringt alle Infos mit!).
Also was ist der schönste Weg?
EDIT: Im IRC hat mich XanClic bestätigt den Weg mit nur einer Funktion und Konstanten, die alles mitbringen, zu gehen.
-
Das ist jetzt dabei heraus gekommen und funktioniert bisher gut. Ist das Innenleben der Funktion pci_config_read(...) ausreichend verständlich?
#ifndef PCI_H #define PCI_H #include "os.h" #define PCI_CONFIGURATION_ADDRESS 0xCF8 // Address I/O Port #define PCI_CONFIGURATION_DATA 0xCFC // Data I/O Port // upper byte: length in bytes // lower byte: register number plus offset #define PCI_VENDOR_ID 0x0200 // length: 0x02 reg: 0x00 offset: 0x00 #define PCI_DEVICE_ID 0x0202 // length: 0x02 reg: 0x00 offset: 0x02 #define PCI_COMMAND 0x0204 #define PCI_STATUS 0x0206 #define PCI_REVISION 0x0108 #define PCI_CLASS 0x010B #define PCI_SUBCLASS 0x010A #define PCI_INTERFACE 0x0109 #define PCI_HEADERTYPE 0x010E #define PCI_BAR0 0x0410 #define PCI_BAR1 0x0414 #define PCI_BAR2 0x0418 #define PCI_BAR3 0x041C #define PCI_BAR4 0x0420 #define PCI_BAR5 0x0424 #define PCI_IRQLINE 0x013C uint32_t pci_config_read( uint8_t bus, uint8_t device, uint8_t func, uint16_t content ); void pci_config_write_dword( uint8_t bus, uint8_t device, uint8_t func, uint8_t reg, uint32_t val ); #endif
#include "pci.h" uint32_t pci_config_read( uint8_t bus, uint8_t device, uint8_t func, uint16_t content ) { // example: PCI_VENDOR_ID 0x0200 ==> length: 0x02 reg: 0x00 offset: 0x00 uint8_t length = content >> 8; uint8_t reg_off = content & 0x00FF; uint8_t reg = reg_off & 0xFC; // bit mask: 11111100b uint8_t offset = reg_off % 0x04; // remainder of modulo operation provides offset outportl(PCI_CONFIGURATION_ADDRESS, 0x80000000 | (bus << 16) | (device << 11) | (func << 8) | (reg )); // use offset to find searched content uint32_t readVal = inportl(PCI_CONFIGURATION_DATA) >> (8 * offset); switch(length) { case 1: readVal &= 0x000000FF; break; case 2: readVal &= 0x0000FFFF; break; case 4: readVal &= 0xFFFFFFFF; break; } return readVal; } void pci_config_write_dword( uint8_t bus, uint8_t device, uint8_t func, uint8_t reg, uint32_t val ) { outportl(PCI_CONFIGURATION_ADDRESS, 0x80000000 | (bus << 16) | (device << 11) | (func << 8) | (reg & 0xFC )); outportl(PCI_CONFIGURATION_DATA, val); }
Code in main(): siehe unten
-
fricky sprach im IRC das Thema PCI-Aktivierung an:
A PCI device ignores all IO and memory access from the PCI bus until it has been activated, steht z.b. dort: http://ecos.sourceware.org/docs-latest/ref/ecos-pci-library.html
weiterhin:
<fricky> erhard: zum testen: scanne den PCI-bus nach einem EHCI controller
<*ehenkes> muss erstmal einer da sein
<*ehenkes> if( pciProgrInterface==0x20 ) { printformat("EHCI"); }
<fricky> an offset 2 der memory-mapped i/o-register befindet sich das register HCIVERSION
<*ehenkes> du bekommst von dem: bus:device.function usw.
<fricky> wenn du dort einen 16-bit lesezugriff machst, sollte der wert 0x100 zu lesen sein
-
Ich habe nun auch die Bestimmung der BARs / Memory Size versuchsweise implementiert.
/// TEST PCI-SCAN BEGIN /// Erweiterte Version siehe unten ... /// TEST PCI-SCAN END
Typisches Ergebnis bezüglich USB-Devices:
0:3.0 IRQ 5 USB OHCI BAR0: CFFFD000h size: 4096 0:3.1 IRQ 11 USB OHCI BAR0: CFFFE000h size: 4096 0:3.2 IRQ 5 USB EHCI BAR0: CFFFF000h size: 4096
Sieht als Einstieg nicht schlecht aus. Klappt aber nur, weil die letzten Bits 00b sind. Die letzten 4 Bits müssen daher noch gesondert ausgewertet werden: (siehe Beitrag weiter oben)
Ein Speicherbereich hat eine Mindestgröße von 16 Byte und ist immer ein Vielfaches von 2. Dadurch stehen die 4 unteren Bits für zusätzliche Codierungen zur Verfügung. Bit 0 gibt Auskunft darüber, ob es sich um eine IO Adresse (der Zugriff muß über Port-Befehle erfolgen) oder eine Memory Adresse handelt. Handelt es sich um eine Memory Adresse, geben Bit 1 und 2 den Adresstyp an. 00 signalisiert, dass es sich um eine gewöhnliche 32 bit Adresse handelt. Die Kombination 01 bedeutet, dass die Adresse unterhalb der aus alten Zeiten bekannten 1 Megabyte Grenze liegt und die Kombination 10 wird für eine 64 bit Adresse verwednet. Bei einer 64 bit Adresse werden zwei „Base Address“ Einträge zur Adressbestimmung gebraucht. Bit 3 schließlich gibt Auskunft darüber, ob auf diesen Adressbereich im Prefetch-Mode zugegriffen werden darf oder nicht. Ein gesetztes Bit bedeutet dabei, dass nicht im Prefetch-Mode zugegriffen werden darf. Im Prefetch-Mode speichert die PCI-Bridge (d.h. der Baustein, der den PCI-Bus an den Processorbus ankoppelt) Daten zwischen, bevor die eigentliche Übertragung zur CPU (lesen oder schreiben) gestartet wird.
Sobald das stabil steht, schiebe ich es als Test-Version 110 hoch.
-
In einem Fall ergab die Auswertung eines PCI-Gerätes den IRQ 255. Macht das Sinn? Wohl nicht.
bus:dev.func: 1:0.1 device: 4170h vendor: 1002h IRQ: 255
-
//... struct pciBasicAddressRegister { uint32_t baseAddress; size_t memorySize; uint8_t memoryType; }; //...
/// TEST PCI-SCAN BEGIN uint16_t pciDevice = 0; // device uint16_t pciVendor = 0; // vendor uint8_t pciClass = 0; // device class uint8_t pciSubclass = 0; // device subclass uint8_t pciProgrInterface = 0; // program interface uint8_t pciIRQLine = 0; // IRQ line (interrupt line) uint8_t bus = 0; // max. 256 uint8_t device = 0; // max. 32 uint8_t func = 0; // max. 8 struct pciBasicAddressRegister pciBAR[6]; // base address, size, type of BAR0 - BAR5 uint32_t pciBar = 0; for(bus=0;bus<8;++bus) { for(device=0;device<32;++device) { for(func=0;func<8;++func) { pciVendor = pci_config_read( bus, device, func, PCI_VENDOR_ID ); pciDevice = pci_config_read( bus, device, func, PCI_DEVICE_ID ); pciClass = pci_config_read( bus, device, func, PCI_CLASS ); pciSubclass = pci_config_read( bus, device, func, PCI_SUBCLASS ); pciProgrInterface = pci_config_read( bus, device, func, PCI_INTERFACE ); pciIRQLine = pci_config_read( bus, device, func, PCI_IRQLINE ); pciBAR[0].baseAddress = pci_config_read( bus, device, func, PCI_BAR0 ); pciBAR[1].baseAddress = pci_config_read( bus, device, func, PCI_BAR1 ); pciBAR[2].baseAddress = pci_config_read( bus, device, func, PCI_BAR2 ); pciBAR[3].baseAddress = pci_config_read( bus, device, func, PCI_BAR3 ); pciBAR[4].baseAddress = pci_config_read( bus, device, func, PCI_BAR4 ); pciBAR[5].baseAddress = pci_config_read( bus, device, func, PCI_BAR5 ); if( pciVendor != 0xFFFF ) { if(pciIRQLine!=255) { printformat("%d:%d.%d\t dev:%x vend:%x IRQ:%d ", bus, device, func, pciDevice, pciVendor, pciIRQLine); } else // "255 is defined as meaning "unknown" or "no connection" to the interrupt controller" { printformat("%d:%d.%d\t dev:%x vend:%x IRQ:-- ", bus, device, func, pciDevice, pciVendor); } if( (pciClass==0x0C) && (pciSubclass==0x03) ) // USB { printformat(" USB "); if( pciProgrInterface==0x00 ) { printformat("UHCI "); } if( pciProgrInterface==0x10 ) { printformat("OHCI "); } if( pciProgrInterface==0x20 ) { printformat("EHCI "); } if( pciProgrInterface==0x80 ) { printformat("no HCI "); } if( pciProgrInterface==0xFE ) { printformat("any "); } for(i=0;i<6;++i) // check USB BARs { pciBAR[i].memoryType = pciBAR[i].baseAddress & 0x01; if(pciBAR[i].baseAddress) { if(pciBAR[i].memoryType == 0) { printformat("%d:%X MEM ", i, pciBAR[i].baseAddress & 0xFFFFFFF0 ); } if(pciBAR[i].memoryType == 1) { printformat("%d:%X I/O ", i, pciBAR[i].baseAddress & 0xFFFFFFFC ); } /// TEST Memory Size Begin cli(); pci_config_write_dword ( bus, device, func, PCI_BAR0 + 4*i, 0xFFFFFFFF ); pciBar = pci_config_read( bus, device, func, PCI_BAR0 + 4*i ); pci_config_write_dword ( bus, device, func, PCI_BAR0 + 4*i, pciBAR[i].baseAddress ); sti(); pciBAR[i].memorySize = (~pciBar | 0x0F) + 1; printformat("sz:%d ", pciBAR[i].memorySize ); /// TEST Memory Size End } } } printformat("\n"); } // Bit 7 in header type (Bit 23-16) --> multifunctional if( !(pci_config_read(bus, device, 0, PCI_HEADERTYPE) & 0x80) ) { break; // --> not multifunctional } } } } printformat("\n"); /// TEST PCI-SCAN END
Damit man mehr ausdrucken kann in einer Monitorzeile habe ich in printformat(...) %X auf acht Zeichen und %x auf vier Zeichen Hexadezimal-Ausgabe gesetzt. %x macht z.B. Sinn bei VendorID und DeviceID.
Bei USB gegeb ich nun aus:
USB UHCI 4:0000D400h I/O sz: 32
USB UHCI 4:0000D000h I/O sz: 32von rechts nach links: Subclass Program-Interface BAR-Nr. Memory-Address
Memory-Type(MEM oder I/O) Memory-SizeHier zwei konkrete Beispiele:
http://www.henkessoft.de/OS_Dev/Bilder/USB_UHCI_IO_32.JPG (UHCI IO 32 Byte)
http://www.henkessoft.de/OS_Dev/Bilder/USB_OHCI_EHCI_MEM_4096.JPG (OHCI u. EHCI MEM 4096 Byte)
-
Erhard Henkes schrieb:
In einem Fall ergab die Auswertung eines PCI-Gerätes den IRQ 255. Macht das Sinn?
Das würde "Sinn" machen bei PCI-Geräten, die keinen Interrupt brauchen (i.e. 0xFF -> nicht vorhanden, nicht gebraucht). Hast du mal auf dem Testrechner im BIOS alle Optionen de/aktiviert, die in etwa heißen "plug and play os" oder "pci-autoconfig" o.ä? Mein BIOS kennt solche Optionen nicht, da der Rechner nicht dafür ausgelegt ist, nach dem Kauf noch großartig aufgerüstet zu werden. Aber der PCI-Scan sollte dann völlig neue Ergebnisse bringen.
-
Ja, 255 bedeutet nicht da, hab ich im Code ergänzt, mache da jetzt "IRQ:--"
Genaue Daten sind: 1:0.1 dev:4170h vend:1002h IRQ:255 (jetzt: IRQ:--)
-
Meine teuer bezahlten "RAM-Controller" und "PCI-PCI-Bridges" haben/nutzen auch keinen Interrupt. Hast du den PCI-Scan mal mit geänderten BIOS-Einstellungen gemacht? Das Ergebnis interessiert mich.
Hier noch ein Bit Twiddle Hack:
uint32_t pci_config_read( uint8_t bus, uint8_t device, uint8_t func, uint16_t content ) { (...) uint8_t reg = reg_off & 0xFC; // bit mask: 11111100b // uint8_t offset = reg_off % 0x04; // remainder of modulo operation provides offset uint8_t offset = reg_off & 0x03; // x & 0x03 == x % 0x04 // aber das wird der Kompiler eh so machen :) (...) }
-
Bit Twiddle Hack
Danke! Interessanter Link.