Aprendendo PCI-e: driver e DMA
(blog.davidv.dev)- Saindo da fase de fazer
peek/pokediretamente em um endereço BAR0 hardcoded, o sistema passa a localizar a memória BAR pelo subsistema PCI do Linux, e o driver de kernel inicializa o dispositivo - O driver começa com a tabela de IDs de
struct pci_drivere a funçãoprobe, mapeia a BAR0 para um endereço virtual do kernel e prepara o acesso a partir do espaço de usuário - Por meio do dispositivo de caractere
/dev/gpu-io, ele conectaread(2)ewrite(2), e usacontainer_ofpara recuperar o estado do driver a partir das operações de arquivo - A cópia em unidades de DWORD levava cerca de 800ms para transferir 1.2MiB, mas ao trocar por uma chamada de DMA baseada em registradores MMIO isso caiu para cerca de 300µs
- A espera pela conclusão do DMA é tratada com interrupções MSI-X e wait queue, e no fim ele funciona como uma GPU falsa que exibe o conteúdo do framebuffer no console do QEMU
Encontrando e mapeando a BAR0 no driver de kernel
- A implementação anterior lia e escrevia diretamente, em unidades de 32 bits, no endereço BAR0
0xfe000000copiado dolspci - Para evitar hardcode do endereço, ele obtém do subsistema PCI do Linux as informações de mapeamento de memória do dispositivo
struct pci_driverprecisa de dois campos principais- uma tabela de pares device/vendor ID compatíveis
- uma função
probechamada quando o ID corresponde
- O dispositivo de exemplo corresponde a
PCI_DEVICE(0x1234, 0x1337) - O estado do driver
GpuStateguardastruct pci_dev *pdeveu8 __iomem * hwmempara a memória BAR - A função
probeprepara o dispositivo na seguinte ordem- ativa o acesso à memória do dispositivo com
pci_enable_device_mem(pdev) - obtém o bitfield dos BARs de memória disponíveis com
pci_select_bars(pdev, IORESOURCE_MEM) - solicita a posse do espaço de endereços BAR com
pci_request_region(pdev, bars, "gpu-pci") - obtém o endereço inicial e o tamanho da BAR0 com
pci_resource_start(pdev, 0)epci_resource_len(pdev, 0) - mapeia o endereço físico para um endereço virtual do kernel com
ioremap(mmio_start, mmio_len)
- ativa o acesso à memória do dispositivo com
- Ao chamar
pci_register_driveremmodule_init, o log de boot mostrammio starts at 0xfe000000e o endereço virtual do kernel
Expondo como dispositivo de caractere no espaço de usuário
- Depois de mapear o espaço de endereços BAR0 no driver de kernel, ele cria um dispositivo de caractere para que programas no espaço de usuário interajam com o dispositivo PCIe via
read(2)ewrite(2) - Este driver precisa apenas de três operações de arquivo:
open,readewrite GpuStaterecebe umstruct cdev cdev, esetup_chardevfaz o seguinte- aloca um número de dispositivo com
alloc_chrdev_region - registra o dispositivo de caractere com
cdev_initecdev_add - cria
/dev/gpu-iocomdevice_create
- aloca um número de dispositivo com
- O script de init recebe
/busybox mdev -spara popular o pseudossistema de arquivos/dev/ - Depois disso,
/dev/gpu-ioaparece como dispositivo de caractere e, no exemplo, usa major241e minor0
Encontrando o estado do driver nas operações de arquivo com container_of
- Na implementação de
write,private_datadestruct file*precisa ser preenchido poropen, masopennão recebe um argumento separadoprivate_dataouuser_data struct inodecontém um ponteirostruct cdev *i_cdevpara o dispositivo de caractere- Como
GpuStateembutestruct cdev, é possível recuperar o ponteiro deGpuStatecomcontainer_of(inode->i_cdev, struct GpuState, cdev) gpu_openarmazena oGpuStateobtido emfile->private_data- Depois disso,
gpu_readegpu_writeusam oGpuStateextraído defile->private_data - As versões iniciais de
read/writetratavam um DWORD por vezgpu_readlê comioread32(gpu->hwmem + *offset)e copia para o buffer do usuário comcopy_to_usergpu_writecopia 4 bytes do buffer do usuário e incrementa o offset em 4
- Funciona para transferências pequenas, mas é lento em transferências grandes porque a CPU precisa continuar processando um pacote por vez
- Uma transferência de 1.2MiB, equivalente a 640×480 em 32bpp, levava cerca de
800ms
Criando chamadas de DMA com registradores MMIO
- Em vez de a CPU repetir cópias em unidades de DWORD, ele usa DMA para que o próprio dispositivo copie os dados
- A solicitação de trabalho é enviada por memory-mapped IO
- alguns endereços de memória funcionam como registradores cujos valores são os argumentos da chamada DMA
- outros endereços funcionam como comandos que significam executar a chamada
- A interface DMA exige que a CPU informe ao dispositivo os seguintes valores
- endereço source e tamanho dos dados a copiar
- endereço destination
- direção dos dados: para a main memory ou a partir da main memory
- um sinal de que está tudo pronto para iniciar a cópia
- O dispositivo precisa avisar a CPU quando a transferência terminar
- Os registradores de exemplo são definidos assim
REG_DMA_DIRREG_DMA_ADDR_SRCREG_DMA_ADDR_DSTREG_DMA_LEN
CMD_DMA_STARTé usado como um endereço de comando para separar o preenchimento dos registradores do início real do DMA- No driver de kernel,
execute_dmausaiowrite32para gravar direção, source, destination e tamanho, e por fim escreve1emCMD_DMA_START
Processamento de DMA no lado do dispositivo QEMU
- A
gpu_writeMMIO do adaptador QEMU substitui a implementação anterior e passa a tratar registradores e comandos DMA - Escritas na área de registradores armazenam os valores em
gpu->registers[reg] - Quando
REG_DMA_STARTchega na área de comandos, ele verifica a direção do DMA - Na direção
DIR_HOST_TO_GPU, ele chamapci_dma_read- o endereço host vem de
REG_DMA_ADDR_SRC - o endereço device vem de
gpu->framebuffer + REG_DMA_ADDR_DST - o tamanho vem de
REG_DMA_LEN
- o endereço host vem de
- As outras direções de DMA são tratadas no código de exemplo como
Unimplemented DMA direction gpu_fb_writeno driver de kernel entrega os dados do usuário ao DMA com o seguinte fluxo- aloca um buffer de kernel com
kmalloc(count, GFP_KERNEL) - copia os dados do usuário para o buffer de kernel com
copy_from_user - gera um endereço DMA com
dma_map_single(&gpu->pdev->dev, kbuf, count, DMA_TO_DEVICE) - chama
execute_dma(gpu, DIR_HOST_TO_GPU, dma_addr, *offset, count) - libera o buffer com
kfree(kbuf)
- aloca um buffer de kernel com
- Essa abordagem ficou rápida o suficiente para medir cerca de 300µs no sistema de exemplo
Sinalizando conclusão do DMA com interrupções MSI-X
- Como a execução do DMA é assíncrona, fica mais conveniente fazer
writebloquear até sua conclusão - A placa PCI-e pode sinalizar a CPU com Message Signalled Interrupts
- Diferentemente das interrupções clássicas, que usam uma conexão elétrica dedicada, MSI envia a interrupção como um pacote de mensagem normal no barramento
- Para configurar MSI-X, o dispositivo QEMU possui duas áreas
- a tabela MSI-X, que armazena a configuração de cada interrupção
- a PBA, um bitmap de interrupções pendentes
- As constantes de exemplo são as seguintes
IRQ_COUNTvale1IRQ_DMA_DONE_NRvale0MSIX_ADDR_BASEvale0x1000PBA_ADDR_BASEvale0x3000
- Em
pci_gpu_realize, o QEMU chamamsix_initemsix_vector_usepara inicializar o MSI-X - Em
lspci -vv, o MSI-X aparece ativado, com a vector table em BAR0 offset00001000e a PBA em BAR0 offset00003000 - Depois de
pci_dma_read, ele chamamsix_notify(&gpu->pdev, IRQ_DMA_DONE_NR)para disparar a interrupção
Handler de IRQ no kernel e bus mastering
- O driver de kernel aloca vetores MSI-X/MSI com
pci_alloc_irq_vectorse obtém o número da IRQ compci_irq_vector - Ele registra o handler
GPU-Dma0comrequest_threaded_irq - Após o boot,
/proc/interruptsmostra, no exemplo, a IRQ24comoPCI-MSIX-0000:00:02.0eGPU-Dma0 - No começo isso não funciona porque a placa não tem permissão para enviar mensagens à CPU de forma independente
- A capacidade que permite ao dispositivo manipular diretamente a memória do sistema sem intervenção da CPU é o bus mastering
- Ao chamar
pci_set_master(pdev)emgpu_probe, o dispositivo recebe permissão de bus master - Depois disso, ao chamar
writeduas vezes, o log do kernel mostraIRQ 24 receivedduas vezes
Implementando write realmente bloqueante com wait queue
- Com a notificação baseada em interrupção pronta, é possível transformar
writeem uma chamada bloqueante com a wait queue do Linux - O estado global usa
wait_queue_head_t wqevolatile int irq_fired = 0 - O handler de IRQ faz o seguinte
- define
irq_fired = 1como estado de conclusão - chama
wake_up_interruptible(&wq)para acordar a thread em espera - retorna
IRQ_HANDLED
- define
setup_msiadicionainit_waitqueue_head(&wq)- Depois de executar o DMA,
gpu_fb_writeespera pela interrupção comwait_event_interruptible(wq, irq_fired != 0) - Se a espera for interrompida, ele retorna
-ERESTARTSYS
Exibindo o framebuffer no console do QEMU
- Como agora existe um framebuffer que recebe
write(2)do espaço de usuário e o envia ao dispositivo PCI-e via DMA, ele pode ser ligado à saída do console do QEMU para se comportar como uma GPU funcional GpuStatedo QEMU recebeQemuConsole* con- Em
pci_gpu_realize, ele cria o console comgraphic_console_inite obtém a display surface comqemu_console_surface - Um padrão de teste inicial preenche os dados da surface no intervalo 640×480 para exibição
vga_update_displaycopia o conteúdo degpu->framebufferpara a display surface do QEMUdpy_gfx_update(gpu->con, 0, 0, 640, 480)atualiza a área 640×480- Depois disso, ao gravar um padrão no dispositivo subjacente, a exibição muda
- O código-fonte está no repositório no Github
1 comentários
Comentários do Hacker News
Comprei uma Tang Mega 138k [0] para começar, mas há pouca documentação, então está levando tempo
Se alguém tiver recomendações de placas FPGA baratas com PCI-e hard IP, seria ótimo saber
[0]: https://wiki.sipeed.com/hardware/en/tang/tang-mega-138k/mega...
Spartan 6 https://www.blackmagicdesign.com/products/decklink/techspecs...
Artix https://www.blackmagicdesign.com/products/decklink/techspecs...
Artix https://www.blackmagicdesign.com/products/decklink/techspecs...
Artix https://www.blackmagicdesign.com/products/decklink/techspecs...
Porém, a única interface externa de alta velocidade é uma USB 3.1 Gen 1
https://shop.lambdaconcept.com/home/50-screamer-pcie-squirre...
O Litefury é um kit FPGA Xilinx no formato “NVMe SSD” (2280 Key M), usa um Xilinx XC7A100T e custa 102 euros
Ele tem apenas algumas entradas/saídas LVDS externas de alta velocidade
https://rhsresearch.com/collections/rhs-public/products/lite...
O Vivado não é uma ferramenta “excelente” pelos padrões de engenheiros de software profissionais, mas, em desenvolvimento e implementação com FPGA, certamente está entre as melhores da indústria
O caminho de desenvolvimento de dispositivos PCIe da Xilinx também é bem amadurecido
Nunca mexi diretamente com drivers de dispositivo no Linux, mas alguns anos atrás trabalhei em vários drivers PCIe em outro sistema operacional, e os conceitos parecem muito familiares
Seria bom ver mais conteúdo desse tipo
Ele inclui apenas código suficiente para mostrar o essencial e vai construindo tudo passo a passo
Nunca tive vontade de criar um novo dispositivo PCI na vida, mas agora fiquei com um pouco de vontade; talvez esse seja o teste decisivo de uma boa escrita técnica
Eu queria montar um ambiente de desenvolvimento e playtest para um projeto, mas nem sabia quais termos pesquisar, e era exatamente disso que eu precisava
Os outros dois artigos também foram bons, com muito conteúdo prático e detalhes pequenos, mas úteis, como como usar código de driver de boot services após a saída, bus mastering e MSI-X