Existe mais entre ISA e PCI do que supõe nossa vã filosofia…

Se você acompanha este blog deve ter percebido que editei o artigo anterior umas quinze vezes, acertando pequenos erros de interpretação quanto ao funcionamento fundamental da arquitetura de barramentos PCI. De fato, estou aprendendo o bicho e compartilhando minhas conclusões com você. Coisa que costumo fazer por aqui, como deve ter reparado…

No começo da era dos PCs, especialmente na arquitetura PC-AT, o padrão ISA imperava… A sigla nos diz que é um “padrão de indústria”, onde o “barramento” é o próprio do processador, junto com uma série de controladores padrão para tratamento de interrupções, DMA, teclado e outras coisinhas. Só que esse padrão foi ficando velho e lento. Rapidamente surgiram outros: EISA, MicroChannel, VESA Local Bus, …

O objetivo, é claro, é oferecer um barramento rápido o suficiente para suportar os “novos” dispositivos: HDs maiores com taxas maiores de transferência, memórias mais rápidas (com zero wait-states), placas de vídeo com aceleração e baixa latência… Coisas que, no velho ISA, ficava difícil de fazer e exigia alguns malabarismos de hardware.

O padrão PCI oferece um barramento isolado do processador de 32 bits… Tanto os dados quanto os endereços compartilham todo o espectro de endereçamento: 32 bits de dados e 32 bits de endereços. Isso quer dizer que o barramento é multiplexado na totalidade e, é claro, com a maior demanda por memória, existe uma extensão para 64 bits… Com o isolamento, PCI funciona na base de troca de mensagens: Um comando é enviado para a controladora para realizar uma transação… Mas isso é transparente para o processador, uma vez que os dispositivos ligados nesse barramento estejam configurados. Isso implica que temos, do ponto de vista do ISA-to-PCI host temos, agora, 3 “espaços de endereçamento”.

Num processador temos dois espaços de endereçamento diferentes: O primeiro é o espaço de acesso à memória e o outro, aos dispositivos de I/O. No caso do PCI, temos também o espaço de endereçamento de configuração… O motivo é simples: Os endereços de configuração devem ser fixos e facilmente conhecidos, enquanto os endereços de acesso à memoria (I/O mapeada em memória) podem variar de dispositivo para dispositivo. Por exemplo, num dos PCs que uso como teste para meus códigos o espaço de endereçamento de memória para acesso aos registradores da “placa de vídeo” (vídeo on-chip da Intel) está compreendido entre 0xf7800000 e 0xf7bfffff, e um bloco adicional entre 0xe0000000 até 0xefffffff. Isso não significa que o espaço de I/O esteja vazio… A porta 0xf000 também é mapeada para este dispositivo.

Para obter esses endereços é preciso acessar o espaço de configuração usando o esquema de endereçamento especificado no artigo anterior… No caso desse dispositivo de vídeo, o device é 0 (zero) e a função é 2. Assim, ao consultar o bloco de 256 bytes do espaço de configuração no endereço 0x80000200, obtemos a estrutura que descreve as características do dispositivo. A estrutura, como mostrei anteriormente, é esta (para o padrão PCI Local Bus 3.0):

#include <stdint.h>

union pcicfg_addr_t {
  uint32_t value;
  struct {
    uint32_t reg:8;
    uint32_t function:3;
    uint32_t device:5;
    uint32_t bus:8;
    uint32_t :7;          // reservados.
    uint32_t enabled:1;
  };
};

A regra para listar todos os dispositivos conectados ao barramento local (sempre o barramento 0) é varrer todo o espaço de endereçamento de configuração e verificar os primeiros bits do registrador 0 (Vendor ID). Se ele for 0xffff, então o dispositivo/função não existe, caso contrário, as outras informações do dispositivo devem ser válidas:

int i, j;
uint32_t data;

for (i = 0; i < 32; i++)
  for (j = 0; j < 7; j++)
  {
    union pcicfg_addr_t addr = { .enabled = 1 };

    addr.function = j;
    addr.device = i;
    outpd(0xcf8, addr.value);
    if (((data = inpd(0xcfc)) & 0xffff) != 0xffff)
      register_device(data);
  }

Aqui register_device() obterá as características do dispositivo e adicionará numa lista.

O padrão PCI permite que existam vários barramentos isolados no mesmo computador. O barramento “local” é sempre o zero, mas podem existir pontes entre o barramento local e outros barramentos. Para isso usa-se uma PCI-to-PCI bridge. Por isso o campo bus na estrutura do endereço no espaço de configuração.

Resta-nos apenas entender como obter essas características… Isso é complicado. Existem protocolos, padrões, para lidarmos com diversos tipos diferentes de dispositivos. É bom lembrar que PCI é uma especificação de barramento e, para tal, é preciso obter ou ajustar configurações tanto dos espaços de endereçamento de memória e I/O, mas também o comportamento de sinais como DMA, Interrupções, timings, etc. Uma visão superficial de como isso pode ser feito pode ser visto no código fonte do kernel do Linux, no diretório arch/x86/pci/. Observe as rotinas em early.c e verá algo similar com o código mostrado no artigo anterior (e no listado acima), mas repare que existem códigos específicos para processadores AMD e Intel, bem como para dispositivos como ACPI, chipsets de rede da BroadCom e barramentos NUMA (Non Uniform Memory Access). Ou seja, a obtenção das configurações não é assim tão simples porque cada dispositivo tem lá sua maneira de fazer as coisas, mesmo que o método seja unificado.

No final das contas, a maioria das portas de I/O de dispositivos são mapeadas no espaço de endereçamento de memória, mas alguns continuam no espaço de I/O, só que, diferente do padrão ISA, essas portas não são fixas. Elas dependem da configuração default do dispositivo ou, em alguns casos explícitos, da configuração que o seu sistema faz no dispositivo (as portas de I/O 0xCF8 e 0xCFA, usadas para acessar o espaço de configuração, podem ser escritas também!).

Anúncios