MyToyOS: Como PCI funciona?!

O padrão usual de leitura e escrita em portas de I/O é através das instruções IN e OUT do processador. Essas instruções usam um “espaço de endereçamento” diferente do usado para acessar memória, onde os endereços podem ter, no máximo 16 bits de tamanho… Além disso, as instruções IN e OUT são lerdas! Por esses dois motivos os fabricantes de PCs meio que deixaram o padrão ISA (Industry Standard Architecture) de lado e começaram a lançar padrões mais avançados, como o EISA, o VESA Local Bus, o AGP e, o mais recente, PCI.

Nos PCs atuais o padrão EISA continua existindo. Ele é um upgrade do barramento ISA, com suporte para vários processadores compartilhando o mesmo barramento e tamanho de 32 bits… Já o padrão PCI trata-se de um isolamento entre diversos barramentos e o barramento EISA. É por isso que dizemos que existe uma “ponte” EISA-PCI (EISA/PCI Bridge).

O padrão PCI suporta até 256 barramentos diferentes. Cada barramento é dividido em até 32 “dispositivos” (devices) e cada dispositivo em até 8 “funções” (functions) e cada função suporta 64 registradores de 32 bits.

O espaço de endereçamento de configuração de dispositivos

O mais importante é saber que esses barramentos/dispositivos/funções/registradores não são acessados no espaço de endereçamento de I/O normal, mas são mapeados diretamente na memória. No entanto, o endereço base de um dispositivo deve ser obtido através das portas de I/O 0xCF8 (chamada de CONFIG_ADDRESS) e 0xCFC (chamada de CONFIG_DATA) da seguinte maneira:

Escrevemos o valor de 32 bits abaixo na porta CONFIG_ADDRESS:

0b10000000_bbbbbbbb_ddddd_fff_rrrrrr_00

Onde os bits ‘b’ especificam o barramento. Os bits ‘d’, o dispositivo no barramento. Os bits ‘f’, uma das funções do dispositivo e os bits ‘r’, um dos registradores da função. Note que os registradores são alinhados por DWORD (veja na figura abaixo) e eles devem ser lidos ou escritos uma DWORD por vez… Assim, os bits ‘r’ compõem o offset no configuration space, mas por causa da restrição do alinhamento apenas os 6 bits superiores são informados e os 2 bits inferiores têm que ser zerados.

Na literatura você pode encontrar a especificação do barramento/dispositivo/função assim, por exemplo: B0:D1:F4, para barramento 0 (zero), dispositivo 1 e função 4. É comum encontrar a especificação apenas do dispositivo e da função, já que o barramento do PC costuma ser um só. Por exemplo, o SATA Host Controller, no chipset ICH9 da Intel, está localizado em D31:F2 e D31:F5. Outro exemplo é relativo ao USB ECHI Host Controller, do mesmo chipset, localizado em D29:F7 e D26:F7.

Uma vez escrito em CONFIG_ADDRESS a requisição da configuração do dispositivo, podemos ler o registrador de dados de configuração via porta CONFIG_DATA. O valor lido deve ser de 32 bits também… Leituras e escritas de bytes ou words nessas portas resultam em nada (ou seja, os dados lidos/escritos virão ou irão para o barramento EISA, não para o EISA-PCI bridge). Deve-se sempre ler/escrever dwords.

No espaço de configuração de um disposito a seguinte estrutura é padronizada:

450px-pci-config-space-svg

A partir do offset 0x40 os registradores são dependentes do dispositivo…

A forma de interpretar esses campos de endereços base (Base Address Registers) dependem também do dispositivo. Os campos DeviceID, VendorID, Header Type, Subsystem ID e Subsystem Vendor ID, além de Class Code e Revision ID podem ser usados para determinar quel é exatamente o dispositivo com qual estamos lidando.

ATENÇÃO: Embora dê para ler o DeviceID e o VendorID dos dispositivos atachados no barramento PCI no modo real, e até mesmo alguns outros campos, a maioria deles não é confiável nesse modo. Ao que parece, é necessário que o processador esteja configurado no modo protegido para obter os endereços base, por exemplo. O programinha abaixo, escrito para o MS-DOS, usando o Borland C++ 3.1 parece demonstrar esse fato:

#include <stdio.h>

#define CFGREG(bus,dev,func,reg) \
  ((1UL << 31) | \
   (((unsigned long)(bus) & 0xff) << 16) | \
   (((unsigned long)(dev) & 0x1f) << 11) | \
   (((unsigned long)(func) & 7) << 8) | \
   ((reg) & 0xfc))

extern unsigned long inpd(unsigned short);
extern void outpd(unsigned short, unsigned long);

void main(void)
{
  int i;

  for (i = 0; i < 256; i += 4)
  {
    // Neste teste quero os registradores do
    // bus 0, device 0 e function 0.
    outpd(0xcf8, CFGREG(0,0,0,i));
    printf("%08lx <- %02x\n", inpd(0xcfc), i);
  }
}

Onde as funções inpd() e outpd() são definidas, num código em assembly, ussando a sintaxe do MASM/TASM:

.386

; Ao invés de usar os atalhos '.model' e '.code',
; o MASM/TASM precisa do modo tradicional para definir 
; segmentos de 16 bits quando a diretiva .386 é usada. 
; O alinhamento por parágrafo é preciosismo meu...

_TEXT segment para public 'CODE' use16
      assume cs:_TEXT

; Estrutura da pilha para inpd().
inpdstk struc
oldbp   dw ?
retaddr dw ?
inport  dw ?
inpdstk ends

      public _inpd
      align 4
_inpd proc near
  push bp
  mov  bp,sp
  mov  dx,[bp].inport
  in   eax,dx
  mov  edx,eax  ; long retornado em DX:AX, não em EAX!
  shr  edx,16
  pop  bp
  ret
_inpd endp

; Estrutura da pilha para outpd().
outpdstk struc
oldbp   dw ?
retaddr dw ?
outport dw ?
data    dd ?
outpdstk ends

       public _outpd
       align 4
_outpd proc near
   push bp
   mov  bp,sp
   mov  eax,[bp].data
   mov  dx,[bp].outport
   out  dx,eax
   pop  bp
   ret
_outpd endp

_TEXT ends

end

Compilando, linkando e executando o teste, temos:

C:\WORK> bcc -3 -c -otest.obj test.c
C:\WORK> tasm -m2 inout.asm,inout.obj
C:\WORK> tlink \bcc\lib\c0s.obj+test.obj+inout.obj,test.exe,nul,\bcc\lib\cs.lib
C:\WORK> test
12378086 <- 00
00000000 <- 04
06000002 <- 08

...
00000000 <- f8
00000000 <- fc

Neste caso o Vendor ID 0x8086 indica “Intel Corporation” e o Device ID 0x1237 é a controladora “PCI 440LX”, emulada pelo VirtualBox (eis uma listagem dos Vendor IDs e Device IDs: aqui). Repare também que todos os outros campos, exceto Class CodeRevision ID, estão zerados!! Isso até poderia ser esperado para o D0:F0, mas experimente obter as configurações de outro dispositivo e verá algo parecido… Os dois primeiros campos estão corretos, mas o resto estará meio maluco. Só posso supor que isso ocorra no modo real. Não tive paciência, ainda, para testar isso no modo protegido…

Uma vez obtido o endereço base de I/O nos registradores de configuração do dispositivo, escrever e ler dados de I/O num dispositivo PCI qualquer fica tão fácil quando usar uma simples instrução MOV, ou usar um ponteiro em C. É claro que essa região de memória é protegida pelo kernel para que os processos do userspace não possam bagunçar com os dispositivos…

Se quiser dar uma olhada na especificação 3.0 do PCI Local Bus, baixe o pdf aqui.

Anúncios