Já falei sobre PCI aqui neste artigo. Embora existam alguns pequeno errinhos de interpretação de minha parte nele, o artigo é, essencialmente, correto. Aqui eu vou corrigir esses errinhos e mostrar como usar o configuration space direito. Outra coisa que falarei é sobre as modificações do padrão PCI Express.
Em primeiro lugar, não é que PCI oferece acesso de I/O mapeado em memória apenas. Ele oferece isso preferencialmente. A especificação nos diz que o espaço de endereçamento de I/O (portas) são usados, tipicamente, para dispositivos legados ou, como está dito lá, “espalhados” (onde, apenas o endereço base é fornecido)… O header no configuration space nos dá, pelo menos, dois BARs (Base Address Registers), que são os registradores com os endereços base do dispositivo, codificados.
A outra coisa, que pode não ter ficado perfeitamente clara no artigo anterior, é que existem 3 “espaços” de endereçamento: Memória, I/O (portas) e Configuração… A ideia do PCI não é apenas fornecer um barramento de alta velocidade entre o processador e o dispositivo, mas também uma maneira padronizada de “perguntar” aos dispositivos como podemos acessá-los. Para isso este espaço adicional (o de Configuração) existe. No padrão PCI ele é acessado pelas portas 0xCF8 e 0xCFC, escrevendo e lendo DWORDs sempre… A tentativa de escrever ou ler outra coisa que não DWORDs não acessará o configuration space.
No PCI, o configuration space fornece blocos de 256 bytes (64 DWORDs) contendo significados fixos:
Essa estrutura aplica-se ao barramento PCI local e apenas se estamos lidando com um dispositivo que não seja uma PCI-to-PCI bridge. Mesmo assim, os 24 bytes iniciais aplicam-se a todos os dispositivos…
No offset 0 temos o VendorID e o DeviceID, onde VendorID é usado como condição para a existência do dispositivo. Se você obtiver um VendorID igual a -1 (ou 0xffff), não tem um dispositivo pendurado no barramento, de acordo com o configuration space. O DeviceID, claro, identifica o dispositivo, assim como o VendorID identifica o fabricante (0x8086, por exemplo, é “Intel”).
As rotinas abaixo, escritas para MS-DOS, usando o DJGPP, demonstra a varredura de todos os dispositivos conectados ao barramento PCI:
/* pci.c */ #include <pc.h> #include <stdint.h> #include "pci.h" #ifndef __DJGPP__ #error Isso só compila no DJGPP! #endif /* Lê um DWORD do configuration space. */ __attribute__((regparm(3))) uint32_t pciGetConfigLong(uint8_t bus, uint8_t device, uint8_t function, uint8_t offset) { uint32_t data; outportl(0xCF8, PCI_CONFIG_ADDR(bus, device, function, offset)); return inportl(0xCFC); } /* Retorna VendorID e DeviceID (se deviceIDptr != NULL). */ __attribute__((regparm(3))) uint16_t pciGetConfigVendorID(uint8_t bus, uint8_t device, uint8_t function, uint16_t *deviceIDptr) { uint32_t data; data = pciGetConfigLong(bus, device, function, 0); if (deviceIDptr) *deviceIDptr = data >> 16; return data & 0xffffU; } /* Pega o Base Address Register 0 */ __attribute__((regparm(3))) uint32_t pciGetConfigBAR0(uint8_t bus, uint8_t device, uint8_t function, enum bartype_e *type) { uint32_t data; data = pciGetConfigLong(bus, device, function, 0x10); /* Repare que o bit 0 do BAR nos indica o espaço de endereçamento usado! */ if ((*type = (enum bartype_e)(data & 1)) == BARTYPE_MEM) data &= 0xfffffff0U; // memory addr. // 16 bytes aligned. else data &= 0xfffffffcU; // I/O addr. // 4 bytes aligned. return data; } /* Pega a classe do dispositivo. */ __attribute__((regparm(3))) uint8_t pciGetConfigClass(uint8_t bus, uint8_t device, uint8_t function) { return pciGetConfigLong(bus, device, function, 8) >> 24; }
O arquivo pci.h define, entre outras coisas, o macro PCI_MASTERBUS_ADDR:
/* pci.h */ #ifndef __PCI_INCLUDED__ #define __PCI_INCLUDED__ #include <stdint.h> /* Número máximo de devices num barramento PCI */ #define PCI_MAXDEVS 64 /* Número máximo de funções num device. */ #define PCI_MAXFUNCS 8 /* Monta o endereço do bus/device/func/reg para o configuration space */ #define PCI_CONFIG_ADDR(bus,dev,func,offset) \ ( ( 1U << 31 ) | \ ( ((uint32_t)(bus) & 0xff) << 16 ) | \ ( ((uint32_t)(dev) & 0x1f) << 11 ) | \ ( ((uint32_t)(func) & 0x07) << 8 ) | \ ( (uint32_t)(offset) & 0xfc) ) __attribute__((regparm(3))) uint32_t pciGetConfigLong(uint8_t, uint8_t, uint8_t, uint8_t); /* Retorna VendorID e DeviceID (se deviceIDptr != NULL). */ __attribute__((regparm(3))) uint16_t pciGetConfigVendorID(uint8_t, uint8_t, uint8_t, uint16_t *); enum bartype_e { BARTYPE_MEM, BARTYPE_IO }; __attribute__((regparm(3))) uint32_t pciGetConfigBAR0(uint8_t, uint8_t, uint8_t, enum bartype_e *); __attribute__((regparm(3))) uint8_t pciGetConfigClass(uint8_t, uint8_t, uint8_t); #endif
A varredura dos devices no barramento 0 pode ser feita assim:
/* scan.c */ #include <stdio.h> #include <stdint.h> #include "pci.h" /* Essas são as classes padrão da especificação PCI 3 */ const char *classes[] = { "Unknown [old]", "Mass Storage", "Network", "Display", "Multimedia", "Memory", "PCI Bridge", "Simple Communication", "Base System Peripheral", "Input Device", "Docking Station", "Processor", "Serial Bus", "Wireless", "Inteligent I/O", "Satellite Communications", "Encrypt/Decrypt", "DSP", [255]="Unknown" }; void main(void) { uint16_t vendorid, deviceid; uint8_t dev, func, class; const char *p; static const char *reserved = "Reserved"; static const char *types[] = { "Memory", "I/O", "<none>" }; enum bartype_e type; uint32_t addr; fputs("Dispositivos encontrados (Bus #0):\n", stdout); /* Varre todos os dispositivos. */ for (dev = 0; dev < PCI_MAXDEVS; dev++) { /* E varre todas as funções de um dispositivo. */ for (func = 0; func < PCI_MAXFUNCS; func++) { /* Lê VendorID para determinar se o dispositivo existe. */ vendorid = pciGetConfigVendorID(0, dev, func, &deviceid); /* Lê a classe do dispositivo para determinar o que ele é. */ class = pciGetConfigClass(0, dev, func); if (!(p = classes[class])) p = reserved; /* Pega o near pointer (endereço físico) do BAR0 */ addr = pciGetConfigBAR0(0, dev, func, &type); /* Se VendorID for diferente de 0xffff, temos um dispositivo! */ if (vendorid != 0xffff) printf("Device #0x%02hhx:0x%02hhx -> VendorID: %#hx, DeviceID: %#hx,\n" " Class: %#hhx (%s)\n" " BAR #0: %#x (%s)\n", dev, func, vendorid, deviceid, class, p, addr, addr ? types[type] : types[2]); } } }
O resultado, no MS-DOS, rodando uma VM no VirtualBox, é este:
Estamos no caminho certo… Note que nem todos os dispositivos fornecem um endereço de acesso (pelo menos, não bo BAR0), mas o Display e Multimedia fornecem endereços base de I/O mapeados em memória e o dispositivo da classe Base System Peripheral fornece um endereço no espaço de I/O (porta).
Para determinar o que são, realmente, os dispositivos, existem outros campos no espaço de configuração que devem ser levados em conta… Além da classe temos a subclasse, que nos diz, por exemplo, se o dispositivo 2:0 é compatível com VGA ou não, ou se o dispositivo 5:0 é uma placa de áudio ou outra coisa “multimídia” qualquer…
O campo header type nos diz, também, a estrutura dos registros que o seguem. O bit 7, por exemplo, indica se estamos lidando com um dispositivo com múltiplas funções…
Se você está lidando com um dispositivo (que não seja uma ponte), o DWORD final, no offset 0x3C da estrutura do configuration space, lhe dará informações sobre interrupções.
Neste ponto, sabendo o endereço base, e algumas características do dispositivo, fica a cargo do próprio dispositivo qualquer tipo de protocolo para seu uso. Por exemplo, mesmo que você possa obter o BAR da sua placa nVidia, sem saber como lidar com ela não vai te ajudar muito… O que quero dizer é que, do ponto de vista do software a especificação PCI só lhe deixará chegar até o ponto onde você obterá configurações do dispositivo.
Aviso sobre o configuration space
Apenas os campos do offset 0 até o offset 15 (os primeiros 4 DWORDs) são comuns a todos os dispositivos PCI. Os demais campos podem ser diferentes. Os dispositivos do PIIX3, por exemplo, costumam deixar disponíveis o BAR em outro lugar que não no offset 16…
Você pode esperar padronização em dispositivos como PCI-to-ISA bridge e PCI-to-PCI bridge, mas todo o resto depende do que obtiver o VendorID, Class, Subclass e Header Type, bem como outros campos…
Configuration space e PCI Express
A especificação PCI Express estende um bocado o configuration space e não exige que o espaço de endereços de I/O (portas 0xCF8 e 0xCFC) sejam usados para acessá-lo. No PCIe, o configuration space é mapeado na memória e, cada função de um device tem um espaço de 1 página (4 KiB) e o acesso é feito de acordo com o endereço base do configuration space adicionado a sequência de bits que monta o barramento, device, function, deslocados 12 bits para a esquerda:
Os 12 bits inferiores são o registrador no configuration space. Resta entender como obter o ponteiro para a região da memória usada como configuration space e isso é feito via ACPI (que está fora do escopo deste artigo).
Essencialmente, PCIe é a mesma coisa que PCI, mas com um método de obtenção das configurações diferente… Felizmente a especificação PCIe nos diz que o método antigo (portas 0xCF8 e 0xCFC) têm que continuar valendo…
Você precisa fazer login para comentar.