Pular para o conteúdo

PCI

PCI (Peripheral Component Interconnect) é um padrão de barramento para conectar hardware de expansão (controladores de disco, placas de rede, placas de vídeo, controladores host USB) à CPU e ao subsistema de memória de um computador. Toda função PCI expõe um bloco padronizado de 256 bytes de espaço de configuração identificando o dispositivo e seus recursos, permitindo que um sistema operacional enumere o hardware instalado sem conhecimento prévio do que está presente.

O PCI 1.0 foi introduzido em 1992 como um barramento paralelo compartilhado, originalmente operando a 33 MHz com um caminho de dados de 32 bits, oferecendo uma banda teórica de pico de 132 MB/s. Revisões posteriores estenderam isso para um caminho de 64 bits e um clock de 66 MHz, dobrando a largura e quase dobrando a frequência para um pico combinado de 528 MB/s, mantendo compatibilidade elétrica e de software com dispositivos anteriores de 32 bits e 33 MHz. Como todo dispositivo em um segmento PCI físico compartilha o mesmo conjunto de fios e se reveza para controlá-los sob um esquema de arbitragem, um único segmento suporta apenas um número limitado de cargas elétricas, comumente citado como dez, o que é uma das razões práticas pelas quais uma placa-mãe típica divide seus slots PCI e dispositivos onboard entre mais de um segmento, unidos por pontes.

PCI-X, voltado principalmente para servidores, manteve o mesmo modelo elétrico de barramento compartilhado enquanto elevava ainda mais as frequências de clock e reduzia os segmentos a menos dispositivos cada, trocando flexibilidade por banda. PCI Express, introduzido em 2003, substituiu o barramento compartilhado inteiramente por links seriais ponto a ponto comutados, chamados de lanes; um slot pode oferecer uma lane ou várias, cada uma full-duplex de forma independente, e o que costumava ser o segundo barramento de uma ponte agora é uma porta de um switch. Nada disso muda o modelo de configuração descrito neste artigo: uma função PCI Express ainda responde a um endereço de barramento/dispositivo/função e expõe o mesmo header padrão, e é por isso que software escrito para PCI convencional continua funcionando, em grande parte sem modificação, em hardware PCI Express.

O PCI convencional também definia duas tensões de sinalização, 5 volts e 3,3 volts, que não eram intercambiáveis; uma placa e um slot construídos para tensões diferentes podiam ser danificados se forçados a se conectar. O conector físico resolvia isso com entalhes de chaveamento posicionados de forma diferente dependendo da tensão para a qual a placa foi projetada (ou, para placas universais que suportam ambas, uma posição de entalhe que aceita qualquer uma), impedindo que uma placa incompatível fosse sequer inserida, em vez de depender do software ou do usuário para detectar a incompatibilidade.

O espaço de configuração não é acessível através de leituras e escritas de memória comuns no PCI convencional; alcançá-lo exige um mecanismo de acesso definido.

O mecanismo especificado desde a introdução do PCI usa duas portas de I/O de 32 bits, CONFIG_ADDRESS (0xCF8) e CONFIG_DATA (0xCFC). O software escreve um endereço empacotado na primeira porta, depois lê ou escreve quatro bytes através da segunda:

static inline uint32_t pci_read32(uint8_t bus, uint8_t dev, uint8_t fn, uint8_t offset) {
uint32_t address = (1u << 31)
| ((uint32_t)bus << 16)
| ((uint32_t)dev << 11)
| ((uint32_t)fn << 8)
| (offset & 0xFC);
outl(0xCF8, address);
return inl(0xCFC);
}

Uma leitura direcionada a um endereço sem nenhum dispositivo presente retorna todos os bits definidos (0xFFFFFFFF); uma escrita no mesmo endereço é silenciosamente descartada. A presença de um dispositivo é inferida a partir dessa convenção, e não de qualquer mecanismo explícito de consulta.

Um método anterior, mantido apenas até ser descontinuado pelo PCI 2.0, funcionava de forma diferente: uma porta de 8 bits em 0xCF8 tanto habilitava o mecanismo quanto selecionava uma função, uma segunda porta em 0xCFA selecionava um número de barramento, e o espaço de configuração era então alcançado através de uma faixa de 4.096 portas de I/O (0xC000–0xCFFF) codificando diretamente um número de dispositivo e um offset de registrador (uma faixa de 12 bits que só conseguia endereçar dezesseis dispositivos), e que sobrevive apenas em hardware da era 80486 e Pentium inicial.

Determinar qual mecanismo uma determinada máquina suporta exige consultar o firmware diretamente. O firmware BIOS reporta isso através de uma chamada de interrupção de software INT 0x1A; o firmware UEFI não oferece chamada equivalente, e o suporte costuma ser inferido através de uma consulta de protocolo seguida de uma busca em tabelas ACPI. Nenhum dos dois métodos tem sucesso garantido, e software que recorre a sondagem direta precisa levar em conta chipsets legados que fazem alias de endereços de portas de I/O altas para endereços baixos: sondar a porta 0xCF8 em um sistema sem barramento PCI algum arrisca endereçar um dispositivo ISA não relacionado localizado na porta 0xF8.

O PCI Express define uma alternativa mapeada em memória, localizada através da tabela ACPI MCFG, onde o espaço de configuração de cada função (agora 4 KB) é diretamente endereçável em base + (bus << 20) + (device << 15) + (function << 12) + offset. As portas legadas continuam funcionando para os primeiros 256 bytes mesmo em um sistema desse tipo, razão pela qual a maioria dos kernels as implementa primeiro. Uma máquina com mais de uma ponte host lista mais de um endereço-base na MCFG, um por segmento PCI; o endereçamento então precisa de um número de segmento além de barramento, dispositivo e função, com valor padrão zero em qualquer lugar onde uma única ponte host seja suficiente.

No barramento físico subjacente ao PCI convencional, selecionar um dispositivo-alvo durante um ciclo de configuração envolve um sinal separado da codificação de endereço descrita acima: o pino IDSEL (Initialization Device Select) de cada dispositivo, conectado a uma linha de endereço específica durante a fase de endereço do ciclo, desempenha o papel de uma linha de chip-select e determina qual dispositivo físico no barramento responde a um determinado número de dispositivo. Esse detalhe é invisível para o software rodando sobre qualquer um dos dois mecanismos de acesso (o número de dispositivo em CONFIG_ADDRESS é traduzido para a ativação correta do IDSEL inteiramente pela fiação da placa-mãe), mas explica por que números de dispositivo em um barramento PCI físico não são construções arbitrárias de software; eles correspondem a uma decisão fixa de fiação de hardware tomada quando a placa foi projetada.

Localizar o hardware instalado é um problema de busca sobre até 65.536 combinações possíveis de barramento/dispositivo/função, quase todas correspondendo a nenhum dispositivo.

Uma busca que espelha a topologia real da máquina começa no barramento 0 e só visita um barramento adicional quando uma ponte ali o reporta, através do campo Secondary Bus Number da ponte:

void scan_bus(uint8_t bus) {
for (uint8_t dev = 0; dev < 32; dev++) {
if (pci_read16(bus, dev, 0, 0x00) == 0xFFFF) continue;
handle_function(bus, dev, 0);
if (pci_read8(bus, dev, 0, 0x0B) == 0x06 && pci_read8(bus, dev, 0, 0x0A) == 0x04) {
scan_bus(pci_read8(bus, dev, 0, 0x19)); // secondary bus
}
// tratamento de multi-função coberto mais adiante
}
}

Isso depende desses números de barramento já estarem corretos, o que por sua vez depende de quem configurou a hierarquia (quase sempre o firmware, antes de qualquer kernel rodar) tê-los atribuído em profundidade primeiro: dando ao barramento secundário de uma ponte o próximo número livre, numerando recursivamente tudo atrás dele, e só então fechando seu campo subordinado com o maior número alcançado. Essa atribuição é essencialmente sempre confiável em máquinas com firmware BIOS ou UEFI padrão, razão pela qual um kernel raramente precisa refazê-la.

Uma busca menos seletiva, em vez disso, testa diretamente cada uma das 65.536 combinações e verifica 0xFFFF. Isso examina endereços que a topologia real jamais poderia popular, mas é mais simples de implementar corretamente, e é usada por um número substancial de sistemas operacionais por esse motivo.

Um dispositivo localizado dessa forma muitas vezes precisa ser correlacionado com um objeto correspondente no namespace ACPI antes que gerenciamento de energia, hot-plug ou certos métodos de roteamento de interrupção possam ser usados com ele. A ACPI representa cada dispositivo ou ponte PCI encontrado ao percorrer seu namespace como um objeto de dispositivo carregando um método _ADR, que retorna o número de dispositivo e função desse objeto compactados em um único valor; comparar isso com o endereço de barramento/dispositivo/função encontrado durante a enumeração é o que permite que métodos fornecidos pelo firmware, como _PRT, sejam associados ao dispositivo físico correto em vez de aplicados genericamente.

Os bytes em um endereço ocupado seguem um layout fixo para todo dispositivo, começando com o mesmo header de sessenta e quatro bytes:

OffsetTamanhoCampoDescrição
0x0016 bitsVendor ID
0x0216 bitsDevice ID
0x0416 bitsCommandDesabilitado no reset; ver os registradores Command e Status
0x0616 bitsStatusO bit 4 indica que uma lista de capabilities está presente
0x088 bitsRevision ID
0x09–0x0B24 bitsClass codeInterface de programação, subclasse, classe
0x0C–0x0D(n/d)Cache Line Size / Latency TimerParâmetros legados de arbitragem de barramento
0x0E8 bitsHeader typeSeleciona o layout após o byte 0x0E; o bit 7 sinaliza múltiplas funções
0x0F8 bitsBISTAutoteste embutido

O class code em 0x0B e a subclasse em 0x0A identificam que tipo de dispositivo é esse antes de verificar qualquer coisa específica do fabricante:

ClasseCategoriaSubclasses / interfaces de programação selecionadas
0x01Controlador de armazenamento em massa0x01 IDE, 0x06 SATA (Prog IF 0x01 = AHCI), 0x08 NVM (Prog IF 0x02 = NVMe)
0x02Controlador de rede0x00 Ethernet
0x03Controlador de vídeo0x00 compatível com VGA
0x04Controlador multimídia0x00 vídeo, 0x01 áudio
0x05Controlador de memória0x00 RAM, 0x01 flash
0x06Dispositivo de ponte0x00 host bridge, 0x04 ponte PCI-para-PCI, 0x07 ponte CardBus
0x08Periférico básico de sistema0x00 PIC, 0x01 controlador DMA, 0x02 timer, 0x06 IOMMU
0x0CControlador de barramento serial0x03 USB (Prog IF distingue UHCI/OHCI/EHCI/xHCI)
0x0DControlador sem fio0x11 Bluetooth, 0x20/0x21 802.11

A host bridge encontrada logo no início de qualquer varredura (barramento 0, dispositivo 0, função 0) é ela mesma classe 0x06, subclasse 0x00. Vendor IDs, ao contrário dos class codes, são atribuídos centralmente e se tornam reconhecíveis rapidamente: 0x8086 é Intel, 0x1022 é AMD, 0x10DE é NVIDIA, 0x1AF4 é a família virtio que o QEMU e a maioria dos hypervisors usam para dispositivos paravirtualizados.

O que vem depois do byte 0x0E depende do header type nos bits 0–6 do offset 0x0E. O tipo 0x00, um dispositivo comum, define até seis Base Address Registers, um par de Subsystem Vendor/Device ID e um Capabilities Pointer. O tipo 0x01, uma ponte PCI-para-PCI, define em vez disso os números de barramento e as faixas de endereço atrás dela; o Secondary Bus Number é o campo do qual a busca acima depende. O tipo 0x02, uma ponte CardBus, é hoje em grande parte uma curiosidade histórica em hardware fabricado depois de meados dos anos 2000, definindo duas janelas de memória e duas de I/O em vez de uma de cada, como uma ponte comum.

Independentemente do header type, uma transação de configuração carrega uma distinção de tipo própria. Uma ponte que recebe uma transação cujo número de barramento corresponde ao seu próprio barramento secundário a converte em uma transação de tipo 0 endereçada diretamente abaixo dela; uma transação cujo número de barramento está mais distante é encaminhada sem alteração como uma transação de tipo 1, para ser tratada da mesma forma por uma ponte mais profunda. Esse mecanismo permite que o endereçamento de configuração funcione corretamente através de uma hierarquia de pontes arbitrariamente profunda, sem que nenhuma ponte individual precise conhecer a topologia completa.

Um header do tipo 0x01 define não apenas os números de barramento atrás de uma ponte, mas também as faixas de endereço de memória e I/O que a ponte tem permissão de encaminhar para lá, cada uma expressa como uma base e um limite, em vez de uma base e um tamanho. A janela de I/O é armazenada com granularidade de 4 KB: apenas os 12 bits superiores de um endereço de 16 bits são mantidos no registrador, com os bits inferiores considerados todos zero para a base e todos um para o limite, enquanto a janela de memória usa granularidade de 1 MB da mesma forma. Uma transação só atravessa a ponte se seu endereço estiver dentro da faixa resultante; qualquer dispositivo configurado atrás da ponte precisa, portanto, receber um endereço dentro dessas janelas; uma base maior que o limite correspondente sinaliza uma faixa vazia, sem nada sendo encaminhado.

O campo Revision ID no offset 0x08, embora seja apenas um byte, é frequentemente consultado por drivers de produção para contornar defeitos de hardware específicos de um lote de fabricação inicial de um chip, uma errata conhecida afetando apenas revisões de silício abaixo de um determinado valor, por exemplo, fazendo com que um driver aplique um atraso extra ou evite uma sequência específica de registradores em revisões mais antigas, usando o caminho direto em revisões mais novas. Isso torna o campo consideravelmente mais relevante na prática do que seu tamanho de um único byte sugeriria, e é uma das razões mais comuns pelas quais código de driver de produção ramifica com base em um valor lido do espaço de configuração, em vez de tratar um modelo de dispositivo como eletricamente uniforme em todas as unidades já produzidas.

Um dispositivo não armazena onde seus registradores vivem: ele reporta quanto espaço precisa através de até seis Base Address Registers, e algo mais atribui a localização real. O bit 0 de um BAR distingue espaço de memória (0) de espaço de I/O (1); para memória, os bits 2:1 indicam 32 ou 64 bits (um BAR de 64 bits usa o próximo slot de BAR para sua metade superior de endereço), e o bit 3 marca a região como prefetchable.

Extrair um endereço já atribuído significa mascarar esses bits baixos, e a máscara depende do tipo do BAR: bar & 0xFFFFFFF0 para um BAR de memória de 32 bits, a mesma máscara na metade baixa combinada com os 32 bits completos do próximo BAR para um de 64 bits, e bar & 0xFFFFFFFC para espaço de I/O, que reserva apenas seus dois bits inferiores. Como um BAR é naturalmente alinhado ao seu próprio tamanho, apenas os bits acima desse tamanho são graváveis: um dispositivo que precisa de 16 MB lê de volta 0xFF000000 antes de decodificar, com apenas seus 8 bits superiores de fato ajustáveis.

Determinar quanto espaço um BAR requer, ao contrário de ler um endereço já atribuído, exige sondá-lo diretamente: a decodificação de I/O e memória do dispositivo é desabilitada primeiro (alguns hardwares tratam a sondagem como um acesso não intencional caso contrário), todos os bits são definidos como 1 no registrador, o resultado é lido de volta, e o valor original é restaurado.

uint32_t original = pci_read32(bus, dev, fn, bar_offset);
pci_write32(bus, dev, fn, bar_offset, 0xFFFFFFFF);
uint32_t probed = pci_read32(bus, dev, fn, bar_offset);
pci_write32(bus, dev, fn, bar_offset, original);
uint32_t size = (~(probed & ~0xFu)) + 1;

Um BAR que reporta 0xFFFF0000 após o mascaramento, por exemplo, resulta em 0x00010000 (64 KB) depois de invertido e incrementado, o que é tanto o tamanho da região quanto o alinhamento que seu endereço-base eventual precisa respeitar. O que quer que atribua esse endereço, firmware, ou ocasionalmente o próprio kernel, o escreve de volta no BAR uma vez escolhido.

Mapear o resultado importa tanto quanto encontrá-lo: registradores MMIO comuns devem ser mapeados como não cacheáveis, já que uma leitura cacheada desatualizada anularia silenciosamente o propósito de ler o estado atual do hardware, mas memória de framebuffer é uma exceção comum, mapeada como write-combining para que a CPU possa agrupar escritas de pixel em transações maiores no barramento.

Uma sétima janela, com propósito diferente, fica no offset 0x30: o endereço-base da expansion ROM, contendo imagens de firmware como o código de boot PXE de uma placa de rede, em vez de registradores de tempo de execução, controlada por seu próprio bit de habilitação separado que vem desabilitado por padrão.

Enquanto o PCI convencional trata BARs de espaço de memória e de espaço de I/O como duas opções igualmente válidas, o hardware PCI Express tem se afastado cada vez mais do espaço de I/O por completo: uma parcela crescente de dispositivos modernos, particularmente aqueles sem requisito de compatibilidade legada a preservar, expõe apenas registradores mapeados em memória e deixa todo BAR de espaço de I/O não implementado. Isso reflete as origens do espaço de I/O como um conceito específico do x86, sem equivalente na maioria das outras arquiteturas, enquanto registradores mapeados em memória não exigem suporte de instrução específico de arquitetura e se comportam de forma idêntica seja acessados por um driver escrito para x86, ARM ou qualquer outra plataforma para a qual o PCI Express tenha sido adaptado.

Nenhuma das regiões de memória ou I/O de um dispositivo responde até que o software as habilite. O bit 1 do registrador Command (Memory Space Enable) e o bit 0 (I/O Space Enable) controlam se os BARs do dispositivo decodificam acessos de barramento; um BAR totalmente configurado pode permanecer completamente sem resposta se o bit correspondente nunca foi definido, e o bit 2 (Bus Master Enable) controla separadamente se o dispositivo pode iniciar uma transferência DMA, de forma silenciosa, sem qualquer erro caso um driver esqueça dele.

O restante do registrador Command cobre comportamentos usados com menos frequência:

BitSignificado
3Special Cycle Enable
4Memory Write and Invalidate Enable
5VGA Palette Snoop Enable
6Parity Error Response
8SERR# Enable
9Fast Back-to-Back Enable
10Interrupt Disable (suprime o sinal legado INTx# descrito abaixo)

O registrador Status pareado, majoritariamente somente-leitura à parte de alguns bits de erro do tipo write-one-to-clear, reporta condições em nível de transação em vez de qualquer coisa específica do dispositivo: entre elas o bit 4 (capabilities presentes), os bits 11–14 (várias condições de abort e erro), e o bit 15 (um erro de paridade detectado).

O comportamento write-one-to-clear nos bits de erro do registrador Status existe especificamente para evitar uma condição de corrida inerente a um esquema mais simples de leitura-modificação-escrita: se limpar um bit de erro exigisse ler o registrador, mascarar o bit em software e escrever o resultado de volta, um segundo erro ocorrendo entre a leitura e a escrita seria silenciosamente perdido quando a escrita o sobrescrevesse com um valor desatualizado já limpo. Escrever um 1 para confirmar um bit específico, em contraste, limpa apenas aquele bit independentemente do que mais tenha mudado no registrador nesse meio-tempo, deixando qualquer erro que tenha chegado durante a própria confirmação ainda definido e visível na próxima leitura.

Um dispositivo usando o esquema de sinalização original ativa uma de quatro linhas físicas, INTA# a INTD#, registrada em seu campo Interrupt Pin no offset 0x3D. Essas linhas são compartilhadas entre dispositivos, então um driver em uma linha compartilhada precisa verificar seus próprios bits de status a cada interrupção para confirmar que o evento era realmente seu. Descobrir a qual entrada de CPU a linha de um determinado slot realmente chega é, do mesmo modo, uma questão de firmware, não do PCI em si: sistemas anteriores à ACPI expõem isso através de uma tabela $PIR encontrada varrendo a memória baixa, sistemas ACPI através de um método _PRT por ponte.

Message Signaled Interrupts eliminam completamente a necessidade desse roteamento: um dispositivo levanta uma interrupção com uma escrita de memória comum de um valor fixo em um endereço fixo, entregue pelo chipset como uma interrupção com seu próprio vetor, independente de qualquer linha compartilhada ou tabela de roteamento. Localizar essa capability, e outras semelhantes, exige percorrer uma lista encadeada dentro do espaço de configuração, presente apenas se o bit 4 do registrador Status estiver definido. A lista começa no offset dado pelo Capabilities Pointer (0x34); cada entrada contém um Capability ID e um Next Pointer para a entrada seguinte, terminando com um next pointer igual a zero:

uint8_t offset = pci_read8(bus, dev, fn, 0x34) & 0xFC;
while (offset != 0) {
uint8_t id = pci_read8(bus, dev, fn, offset);
uint8_t next = pci_read8(bus, dev, fn, offset + 1);
// id: 0x01 power management, 0x05 MSI, 0x10 PCI Express, 0x11 MSI-X
offset = next;
}

O MSI (capability ID 0x05) define um registrador Message Control (um bit de habilitação, contagens de vetores solicitados e concedidos, uma flag de endereçamento de 64 bits) junto com os valores de endereço e dado que o sistema operacional programa. Seu sucessor, o MSI-X (ID 0x11), realoca a tabela de vetores para um dos próprios BARs do dispositivo, suportando até 2048 entradas mascaráveis independentemente, em vez do punhado disponível diretamente dentro da estrutura do próprio MSI.

Além de remover a necessidade de compartilhamento de linha, a entrega por sinalização de mensagem também resolve um problema para o qual as linhas INTx legadas não têm resposta alguma: direcionar uma interrupção específica para uma CPU específica em um sistema multiprocessado. Como uma linha de interrupção legada é um fio físico roteado por fim até uma única entrada no controlador de interrupções, redirecioná-la para um processador diferente exige reprogramar o próprio roteamento. A CPU-alvo de um vetor MSI ou MSI-X, em contraste, é codificada diretamente na porção de endereço da escrita de memória que o dispositivo realiza, que o sistema operacional controla ao programar a capability, tornando simples para um driver ou escalonador direcionar as interrupções de um dispositivo para o processador que estiver atualmente lidando com a carga de trabalho desse dispositivo, uma técnica comumente chamada de afinidade de interrupção.

Funções PCI Express carregam uma segunda lista de capabilities, distinta da descrita acima, ocupando o espaço de configuração além do byte 256 que só o acesso mapeado em memória consegue alcançar. Capabilities estendidas usam uma estrutura de lista encadeada semelhante (cada entrada fornece um capability ID de 16 bits e um offset de 12 bits para a próxima entrada), mas descrevem recursos sem equivalente no PCI convencional. Advanced Error Reporting (capability estendida ID 0x0001) é um exemplo representativo: em vez dos únicos bits Detected Parity Error e Signaled System Error disponíveis no registrador Status padrão, ela expõe registradores separados e mascaráveis individualmente para erros corrigíveis (eventos recuperáveis em nível de link que não precisam de intervenção do software), erros não corrigíveis, e uma divisão adicional entre casos fatais e não fatais, oferecendo diagnósticos consideravelmente mais granulares do que o relatório de erros do PCI convencional permite.

Outras capabilities estendidas tratam de questões que só surgem quando muitos dispositivos independentes compartilham um fabric comutado em vez de um único barramento compartilhado. Access Control Services governa se o tráfego entre duas funções conectadas ao mesmo switch tem permissão de passar diretamente entre elas ou precisa ser redirecionado primeiro até o root complex, relevante para isolar máquinas virtuais umas das outras quando dispositivos são atribuídos diretamente a convidados. Resizable BAR permite que uma função anuncie vários tamanhos diferentes com os quais é capaz de operar para um determinado BAR, em vez do tamanho único e fixo que a sondagem comum de BAR reporta, permitindo que o software solicite uma alocação menor que o máximo do dispositivo, caso o tamanho completo não seja necessário.

Além do header comum de sessenta e quatro bytes, quase tudo é opcional, descoberto através da mesma lista de capabilities usada acima, em vez de offsets fixos. Power management (capability ID 0x01) é um exemplo comum, definindo um registrador de controle/status que move uma função entre D0 (totalmente ligado) e D3 (efetivamente desligado, com contexto perdido a menos que a variante de menor consumo D3hot seja suportada).

Entre esses dois extremos, a especificação também define dois estados intermediários, D1 e D2, embora a implementação de qualquer um deles seja opcional e deixada a critério de cada dispositivo; uma função só é obrigada a implementar D0 e D3, e muitas não implementam nada entre eles. Onde D1 e D2 são suportados, eles geralmente trocam uma economia de energia menor por uma transição mais rápida e menos disruptiva de volta a D0 do que D3 oferece, dando ao dispositivo mais granularidade entre “totalmente ativo” e “totalmente desligado” do que apenas os dois estados obrigatórios fornecem.

Uma única placa física também não está limitada a uma função: o bit 7 do byte Header Type da função 0 sinaliza isso, e se definido, um enumerador precisa sondar as funções 1 a 7 em vez de tratar um resultado 0xFFFF ali como prova de que nada mais existe. Uma função diferente de zero pode carregar um Device ID, class code ou até header type inteiramente diferente da função 0.

Várias capabilities são definidas apenas para PCI Express e estendem, em vez de substituir, os mecanismos descritos acima. Suporte a hot-plug permite que os sinais de presença e falha de energia de um slot notifiquem o software sobre mudanças de hardware ocorridas após o boot. SR-IOV permite que uma função física exponha múltiplas funções virtuais leves, cada uma individualmente atribuível a uma máquina virtual separada. ARI relaxa o limite de oito funções por dispositivo para dispositivos, mais comumente endpoints SR-IOV, que precisam de mais de oito funções.

Os passos acima podem ser ilustrados rastreando como um kernel localizaria e prepararia um controlador SATA típico.

Um enumerador varrendo o barramento 0 encontra uma função reportando class code 0x01, subclasse 0x06 (um controlador de armazenamento em massa, subclasse SATA) com Prog IF 0x01, identificando-o especificamente como um controlador compatível com AHCI, em vez de uma interface mais antiga no estilo IDE. Seu Vendor ID lê 0x8086, identificando a Intel como fabricante; o Device ID que o acompanha é específico do chipset exato e só é consultado em uma tabela mantida pelo fabricante se o comportamento do driver precisar variar entre revisões de chipset, o que um driver AHCI escrito para a interface de registradores padrão geralmente não precisa fazer.

Ler o Header Type retorna 0x00, um dispositivo comum, então o BAR5 (por convenção AHCI, o ABAR, ou AHCI Base Address Register) é sondado: escrito com todos os bits em 1, lido de volta, e o resultado invertido e incrementado para determinar que o controlador requer 4 KB de espaço de registradores mapeado em memória. O firmware normalmente já atribuiu um endereço real aqui, que o kernel lê diretamente em vez de reatribuir, e mapeia como não cacheável em suas tabelas de página, já que os registradores da AHCI precisam refletir o estado ativo do hardware a cada acesso.

Antes que esse mapeamento seja útil, o kernel define os bits 1 (Memory Space Enable) e 2 (Bus Master Enable) do registrador Command: a AHCI depende de DMA para toda transferência de dados, então omitir o bit 2 deixaria todo comando subsequente silenciosamente ineficaz. A entrega de interrupção é então estabelecida percorrendo a lista de capabilities em busca de uma entrada MSI ou MSI-X; encontrar uma permite que o driver registre um vetor dedicado, em vez de compartilhar uma das quatro linhas INTx legadas com o que quer que mais esteja atrás da mesma ponte.

Se o enumerador mais tarde encontrar um segundo controlador SATA em outro lugar da árvore (uma placa de expansão discreta atrás de uma ponte PCI-para-PCI, digamos, além do controlador onboard do próprio chipset), nada no processo acima muda para ele: é descoberto no número de barramento que a varredura de pontes alcançar, identificado pelo mesmo class code e Prog IF independentemente do barramento físico em que esteja, e configurado através da mesma sequência de sondagem de BAR e registrador Command, independentemente do primeiro. Essa independência é precisamente o que a estrutura de class code e header torna possível: um driver vinculado à categoria “controladores de armazenamento em massa compatíveis com AHCI”, em vez de a um endereço específico, se conecta igualmente bem a qualquer uma das instâncias sem precisar saber de antemão quantas existem.

Omitir o Bus Master Enable antes de programar um dispositivo capaz de DMA é uma fonte frequente de um dispositivo que parece sem resposta sem qualquer erro: a transferência simplesmente nunca acontece. Sondar o tamanho de um BAR sem antes desabilitar a decodificação pode disparar uma falha de barramento em alguns chipsets, já que o valor de todos os bits em 1 faz o dispositivo reivindicar brevemente uma faixa de endereço grande e provavelmente conflitante. O BAR0 não tem garantia de conter a janela principal de registradores de um dispositivo, então um driver deve identificar o BAR correto por suas propriedades reportadas, em vez de presumir sua posição.

Escritas em registradores mapeados em memória também são postadas: uma instrução de store se completa assim que a escrita deixa a CPU, sem garantia de que já tenha chegado ao dispositivo. Seguir uma escrita crítica com uma leitura do mesmo dispositivo (que não pode se completar até que a escrita anterior a ela tenha se completado) a esvazia antes que o software dependa do resultado, como antes de armar uma interrupção que o dispositivo possa levantar quase imediatamente.

Uma classe adicional de erro vem de hardware PCI Express hot-pluggable sendo fisicamente removido enquanto um driver ainda o mantém mapeado e acredita configurado: qualquer acesso subsequente aos BARs agora vazios do dispositivo retorna o mesmo padrão de todos os bits em 1 usado para sinalizar um dispositivo ausente durante a enumeração, o que um driver que só verificou esse padrão durante a descoberta inicial pode interpretar erroneamente como um valor de registrador legítimo, em vez de como evidência de que o dispositivo se foi. Kernels que pretendem suportar remoção de hardware em tempo de execução geralmente precisam de um caminho explícito de remoção-surpresa, orientado pela própria sinalização de detecção de presença da capability de hot-plug, em vez de perceber leituras de registrador malformadas depois do fato, já que, no momento em que as leituras começam a retornar todos os bits em 1, qualquer DMA em andamento que o dispositivo estivesse realizando já falhou silenciosamente também.

  1. ^ PCI-SIG, PCI Local Bus Specification, revisão 3.0 (define o layout do espaço de configuração, a distinção entre transações tipo 0/tipo 1 e a semântica de command/status).
  2. ^ PCI-SIG, PCI-to-PCI Bridge Architecture Specification, revisão 1.2 (define o encaminhamento de transações de configuração por pontes e a atribuição de números de barramento).
  3. ^ OSDev Wiki, “PCI” (uma referência hobbyist com detalhes adicionais de implementação em nível de registrador).
  4. ^ PCI-SIG, MSI-X ECN to the PCI Local Bus Specification (define a estrutura da capability MSI-X e o formato da tabela de vetores).
  • The IDT: como um vetor de interrupção é despachado assim que chega.
  • PIC & APIC: linhas INTx legadas são roteadas através de um destes antes de chegar à CPU.
  • Port I/O versus Memory-Mapped I/O: os dois mecanismos dos quais as portas de configuração legadas e a alternativa MCFG deste artigo são exemplos concretos.
  • VirtIO: a família de dispositivos paravirtualizados identificada pelo vendor ID acima.
  • Rede: uma NIC descoberta da mesma forma que qualquer outro dispositivo PCI, antes de seus anéis TX/RX poderem ser configurados.
  • NVMe: um dispositivo descoberto e mapeado por BAR da mesma forma, usando um modelo de comando baseado em fila inteiramente diferente uma vez mapeado.
  • Áudio (AC97/HDA): outra família de dispositivo descoberta e mapeada através deste mesmo mecanismo.
  • Boot via Rede (PXE): o código de boot que a expansion ROM acima contém para uma placa de rede.