MyToyOS: PCI e PCI Express

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:

Configuration space

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)  & 0x3f) << 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:

PhysAddr=BaseAddr+((bus\,shl\,20)\,or\,(dev\,shl\,15)\,or\,(func\,shl\,12))

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…

Anúncios