Comunicação com Processador Gráfico

Sobre

O projeto consistiu em desenvolver um módulo de Kernel Linux e uma biblioteca em linguagem C com o intuito de criar uma ponte de comunicação entre o processador gráfico que se encontra na FPGA contida na placa DE1-SoC e um programa a nível de usuário.

A plataforma DE1-SoC combina um processador ARM (HPS) com um processador gráfico na FPGA. Essa integração permite que o HPS controle e interaja com o processador gráfico, possibilitando manipulação visual em um monitor VGA de 640×480 pixels.

A abordagem do projeto incluiu a implementação de funcionalidades para desenhar dois tipos de polígonos quadrados ou triângulos, renderização de sprites na tela VGA, além de desenhar background color e background block. Os módulos de Kernel Linux foram desenvolvidos para realizar a comunicação entre o HPS e o Processador Gráfico.

O objetivo principal foi estabelecer uma comunicação eficiente entre o processador gráfico na FPGA da plataforma DE1-SoC e o HPS disponível na mesma plataforma.

Sumário

Equipe

  • Jeferson Almeida da Silva Ribeiro
  • Letícia Teixeira Ribeiro dos Santos
  • Vitória Tanan dos Santos

Softwares e Periféricos Utilizados

Kit de desenvolvimento DE1-SoC

A placa de desenvolvimento DE1-SoC é uma plataforma baseada no chip Altera Cyclone V SoC, que integra um processador ARM Cortex-A9 dual-core com uma FPGA da família Cyclone V. A DE1-SoC possui uma ampla variedade de periféricos e interfaces, incluindo:

  • Interfaces de entrada/saída;
  • Conexões de áudio de vídeo;
  • Interface USB;
  • Conexões Ethernet;
  • LEDS, chaves e botões, entre outros.

Placa DE1-SoC
Figura 1. Placa de desenvolvimento DE1-SoC.

Linguagem C

A linguagem C é uma linguagem de programação de alto nível que foi criada nos anos 70 com o propósito inicial de desenvolver sistemas operacionais. Nos dias atuais, ela permanece bastante popular, sendo amplamente utilizada em sistemas embarcados, no Kernel do Linux, aleḿ de também ter servido de influência para criação de outras linguagens como C#, C++ e Java. No projeto, essa linguagem foi utilizada para desenvolver o código fonte em conjunto com o compilador GCC para execução do programa.

Editor de Texto VSCode

O Visual Studio Code é um editor de texto bastante popular que tem suporte para Windows, Linux e MacOS. Neste projeto, o VSCode foi utilizado para fins de edição do código em linguagem C, usufruindo do realce de sintaxe que o programa propociona.

Compilador GCC

GCC é sigla para GNU Compiler Collection, trata-se de um conjunto de compiladores para linguagem de programação C, C++, Objective-C, Fortran, Ada, Go, entre outras. Lançado em 1987, é o compilador padrão na maior parte das distribuições Linux além de estar disponível para muitos sistemas embarcados, incluindo chips baseados em ARM e Power ISA. No projeto, foi utilizado para compilar o código fonte escrito em linguagem C.

Monitor Tubo CRT

O monitor utilizado é um modelo de tubo CRT (Cathode Ray Tube) da DELL, com uma tela de 17 polegadas e uma resolução máxima de 1280x1024 pixels. Ele possui uma interface VGA para conectar-se a placa de desenvolvimento DE1-SoC e exibir imagens.


Figura 2. Monitor Tubo CRT

Padrão VGA

O padrão gráfico utilizado foi o VGA com resolução de 640x480 pixels. A placa DE1-SoC possui um conector D-SUB de 15 pinos para saída VGA, com sinais de sicronização gerados pelo FPGA Cyclone V Soc. Um DAC de vídeo triplo ADV7123 converte sinais digitais para analógicos, representando as cores vermelho, verde e azul, suportando até a resolução SXGA (1280x1024) a 100 MHz.

A sicronização VGA envolve pulsos de sicronização horizontal (hsync) e vertical (vsync), com períodos específicos denominados back porch, front porch e intervalo de exibição para controlar os dados RGB. Após o pulso hsync, os sinais RGB são desligados (back porch), seguidos pelo intervalo de exibição onde os dados RGB ativam cada pixel, e depois desligados novamente (front porch) antes do próximo pulso hsync.

VGA
Figura 3. Conexões entre o FPGA e o VGA

Arquitetura e Instruções do Processador Gráfico

O Processador Gráfico é responsável pela renderização e execução de um conjunto de instruções que permitem mover e controlar sprites, modificar a configuração do background da tela e renderizar polígonos, como quadrados e triângulos. As saídas do Processador Gráfico incluem os sinais de sicronização horizontal (h_sync) e vertical (v_sync) do monitor VGA, além dos bits de cores RGB (Red, Green, Blue). A Figura 4 ilustra a arquitetura completa do processador gráfico, conforme detalhado no TCC.

VGA
Figura 4. Estrutura Interna do Processador Gráfico. (Fonte: TCC de [Gabriel B. Alves])

Unidade de Controle

A Unidade de Controle do processador gráfico da placa DE1-SoC desempenha um papel fundamental na gestão das operações internas do processador, operando como uma Máquina de Estados que coordena o fluxo de instruções:

  • Leitura de Instruções: Responsável por ler as instruções armazenadas na memória, que incluem comandos para modificar o background, movimentar sprites e renderizar polígonos.
  • Decodificação de Instruções: Interpreta os bits das instruções para determinar as ações específicas a serem realizadas pelo processador gráfico.
  • Execução de Instruções: Realiza as operações indicadas pelas instruções, incluindo a alteração do background, renderização de polígonos e movimento de sprites.

Banco de Registradores

O Banco de Registradores é composto por 32 registradores. O primeiro é reservado para a cor de fundo da tela, enquanto os 31 restantes são dedicados ao armazenamento das informações dos sprites. Essa organização permite que o processador gráfico temporariamente armazene informações essenciais de cada sprite, como coordenadas, offset de memória de um bit de ativação, possibilitando o gerenciamento de até 31 sprites simultaneamente em um único frame de tela.

Módulo de Desenho

O Módulo de Desenho gerencia todo o processo de renderização dos pixels no monitor VGA. Ele converte e envia os dados de cor RGB para cada pixel, garantindo a precisão da imagem exibida no monitor. A utilização de uma arquitetura de Pipeline permite ao módulo processar múltiplas instruções ao mesmo tempo, aumentando a eficiência do processamento e previnindo distorções na saída do monitor VGA.

Controlador VGA

O Controlador VGA é responsável por gerar os sinais de sicronização vertical (vsync) e horizontal (hsync), essenciais para a exibição correta dos frames no monitor. Estes sinais são fundamentais para coordenar o processo de varredura do monitor, que ocorre da esquerda para a direita e de cima para baixo. O controlador também fornece as coordenadas X e Y para o processo de varredura, assefurando que cada linha do frame seja renderizada corretamente. Considerando os tempos de sincronização vertical e horizontal, cada tela é atualizada a cada 16,768 ms, resultando em uma taxa de aproximadamente 60 frames por segundo. O módulo coordena ainda a geração dos sinais de sicronização para evitar distorções e garantir que a exibição esteja dentro dos padrões de resolução e taxa de atualização estabelecidos.

Memória de Sprites

A memória de sprites é responsável por armazenar os bitmapes de cada sprite. Ela possui uma capacidade de 12.800 palavras de 9 bits, sendo 3 bits destinados para cada componente de cor RBG. Cada sprite tem um tamanho fixo de 20x20 pixels, ocupando 400 posições de memória. Isso permite que até 32 sprites distintos sejam armazenados simultaneamente para uso. Esta estrutura é essencial para a correta exibição e manipulação dos sprites na tela.

Memória de Background

A memória de background é usada para modificar pequenas partes do fundo da tela. Ela consiste em 4.800 palavras de 9 bits cada, com 3 bits destinados a cada componente de cor RGB. Esta configuração permite ajustar e atualizar dinamicamente seções específicas do background, garantindo flexibilidade e precisão na exibição gráfica.

Co-Processador

O Co-Processador é responsável por gerenciar a construção de polígonos convexos, como quadrados e triângulos. Estes polígonos são renderizados na tela do monitor VGA, trabalhando em conjunto com os sprites e o background. A arquitetura do Co-Processador permite a execução de cálculos necessários para determinar a posição e as características desses polígonos. Ao fazer isso, ele assegura que os polígonos sejam integrados corretamente com outros elementos gráficos, proporcionando uma renderização precisa e sincronizada. Isso é essencial para a exibição coerente e harmoniosa de todos os componentes visuais na tela.

Instruções do Processador Gráfico

Escrita no Banco de Registradores (WBR): Essa instrução armazena informações sobre a cor base do background e dos sprites. Para que o processador gráfico execute essa instrução, o opcode é configurado como 0000. Dos 32 registradores dispobíveis, o primeiro é utilizado para armazenar as informações do background, enquanto os outros 31 registradores guardam informações dos sprites. A estrutura para configurar os campos da cor base do background está representada na Figura 5, onde os campos R, G e B configuram a cor base. A configuração dos sprites está na Figura 6, onde o sprite é definido pelo offset, que indica o endereço de memória, os campos X e Y definem as coordenadas de localização dos sprites, e o campo sp serve para habilitar ou desabilitar um sprite.

Instrução WBR
Figura 5. Instruções WBR para alteração da cor base do background. (Fonte: TCC de [Gabriel B. Alves])

Instrução WBR
Figura 6. Instruções WBR para configurar um sprite. (Fonte: TCC de [Gabriel B. Alves])

Escrita na Memória de Sprites (WSM): Essa instrução armazena ou altera o conteúdo presente na memória de sprites. Para que o processador gráfico execute essa instrução, o opcode é configurado como 0001. A instrução é representada na Figura 7. O campo endereço de memória especifica a localização do sprite na memória a ser editado, enquanto os campos R, G e B definem as novas cores para o local desejado.

Instrução WSM
Figura 7. Instruções WSM para editar o conteúdo na memória de sprites. (Fonte: TCC de [Gabriel B. Alves])

Escrita na Memória de Background (WBM): Essa instrução armazena ou altera o conteúdo na Memória de Background. Para que o processador gráfico execute essa instrução, o opcode é configurado como 0010. O campo endereço de memória corresponde a um bloco 8x8 pixels. Com uma resolução de 640x480 pixels, a tela é dividida em 80x60 blocos, conforme representado na Figura 8.

Instrução WBM
Figura 8. Divisão da área do Background. (Fonte: TCC de [Gabriel B. Alves])

Definição de um Polígono (DP): Essa instrução é utilizada para renderizar polígonos, conforme mostrado na Figura 9. Para que o processador gráfico execute essa instrução, o opcode é configurado como 0011. O campo endereço indica a posição de memória onde a instrução será armazenada. Os campos ref_point X e ref_point Y definem as coordenadas para a renderização do polígono. O campo tamanho especifica as dimensões do polígono (base e altura), conforme indicado na Figura 10. As componentes RGB determinam a cor do polígono, e o campo forma define se o polígono será um quadrado (0) ou triângulo (1).

Instrução DP
Figura 9. Instruções DP para definição de um polígono. (Fonte: TCC de [Gabriel B. Alves])

Dimensões dos polígonos
Figura 10. Dimensões dos Polígonos. (Fonte: TCC de [Gabriel B. Alves])

Detalhamento da Lógica de Comunicação

Mapeamento de Memória

Para que haja comunicação entre hardware e software no ambiente Linux, é necessário aplicar a técnica de mapeamento de memória. Devido ao Linux utilizar um sistema de memória virtual, os endereços físicos do hardware não ficam disponíveis para acesso direto em programas em execução. O Kernel disponibiliza a função mmap que pode ser usada em conjunto com o arquivo /dev/mem e assim mapear os endereços físicos para endereços virtuais, permitindo o acesso ao hardware.

No contexto do projeto, essa técnica foi usada para ter acesso aos barramentos Data A e Data B do Processador Gráfico, que se encontra na FPGA. O processador ARM (HPS) possui as pontes de acesso HPS-to-FPGA e LightWeight-to-FPGA, que são mapeadas para regiões no espaço de memória do HPS, ao utilizar uma delas é possível acessar os barramentos através da soma da ponte + offset, que representa o endereço base.

// LW_virtual => endereço da LightWeight-to-FPGA
// DATA_A_BASE => endereço base do barramento data A, representando o offset
data_a_ptr = (int*)(LW_virtual + DATA_A_BASE);

Driver de Caractere

Para que um usuário tenha acesso a dispositivos de hardware, é necessário interagir com arquivos especiais de dispositivo, que estão agrupados no diretório /dev. As chamadas de sistema como open, read, write, close, lseek, mmap, entre outras, são usadas para interagir com esses dispositivos. Quando tais chamadas são realizadas, o sistema operacional as redireciona para o driver do dispositivo associado ao dispositivo físico.

O driver de dispositivo é um componente do kernel que interage diretamente com o hardware. No caso dos drivers de dispositivos de caractere, eles gerenciam uma pequena quantidade de dados e o acesso a esses dados não requer operações frequentes de busca. Neste caso, as chamadas de sistema são encaminhadas diretamente para os drivers de dispositivo de caractere, que manipulam a comunicação com o hardware de maneira eficiente.

No projeto, foi implementado as funções open, read, write e release. As funções que formam as expressões a serem passadas para os barramentos Data A e Data B também foram incluídas no driver. O fluxo de escrita nos barramentos utilizando a linguagem C acontece da seguinte forma:

  1. Programa de usuário chama uma função da biblioteca passando parâmetros, como por exemplo set_background_color(1, 2, 3)
  2. A função equivalente na biblioteca abre o arquivo especial no diretório /dev através da função open()
  3. É utilizada a função sprintf() para formar uma string contendo todos os valores que vão ser enviados pro driver e passa pra variável buffer. E a write() pra escrever o que está no buffer direto no driver através do fd (file descriptor). Ao executar essa ação, a função write do driver é chamada.
    sprintf(buffer, "%d %d %d %d %d", WSM, R, G, B, endereco_memoria);  
    int bytesWritten = write(fd, buffer, strlen(buffer)); 
  1. Quando a função write do driver é chamada, há uma lógica condicional que verifica qual instrução foi solicitada, e a partir daí chama a função respectiva para formar a expressão a ser passada para os barramentos e enviar o pulso de clock para efetivar a escrita. A identificação das instruções se dão através de constantes, por exemplo, a WBR é identificada pelo número 1.

Fluxograma da Solução Geral do Projeto

Fluxograma
Figura 11. Fluxograma da Solução Geral do Projeto.

Imagem Final

Imagem Final do Projeto
Figura 12. Imagem Final do Projeto.

Execução do Projeto

Para que todo o fluxo funcione corretamente, alguns comandos precisam ser executados. A partir da pasta raiz do projeto, executar:

make
make lib

Os comandos irão inserir o módulo no kernel e criar o arquivo especial na pasta /dev. Também irá compilar a biblioteca e o arquivo principal (main). Para mostrar a imagem no monitor, executar:

sudo ./main

Cenário de Testes

Os cenários de testes foram desenvolvidos para verificar as funções do projeto e se as mesmas estavam se comportando conforme o esperado. Abaixo está cada cenário de teste realizado:

Exibição da cor do background da tela:

Para configurar a cor de fundo da tela, foi utilizada a função set_background_color(int R, int G, int B). Essa função escreve no registrado necessário para definir a cor de fundo. No projeto, a cor escolhida foi azul, representando o céu diurno. A figura abaixo exemplifica como ficaria a cor do background no monitor

Função Background Color
Figura 13. Exemplo da cor de Background no monitor

Desenho de background blocks:

O desenho de blocos no background foi realizado utilizando a função set_background_block(int endereco_memoria, int R, int G, int B). Essa função permite desenhar elementos como a grama e a tartaruga (exceto suas patas) na memória de background. Para facilitar o processo, um laço for foi utilizado para definir as áreas na memória onde esses blocos deveriam ser desenhados. A Figura 14 mostra um exemplo de como deveria ficar o desenho usando os background blocks.

Função Background Block
Figura 14. Exemplo do desenho utilizando apenas os blocos

Desenho de polígonos:

Para renderizar e definir polígonos na tela, foi utilizada a função define_poligon(int forma, int R, int G, int B, int tamanho, int x, int y, int endereco). Polígonos foram fundamentais para desenhar elementos como o sol, as patas da tartaruga e a estrutura da casa no projeto. Abaixo, na Figura 15 está presente um exemplo dos polígonos utilizados no desenho

Função de Polígonos
Figura 15. Polígonos utilizados no desenho

Configuração de sprites:

A função set_sprite(int reg, int x, int y, int offset, int activation_bit) foi empregada para configurar e exibir sprites na tela. A Figura 16 demonstra um exemplo final do desenho com sprites.

Função de Sprites
Figura 16. Exemplo desenho final com os sprites

Conclusão

O objetivo deste projeto foi estabelecer uma comunicação eficiente entre o HPS e o Processador Gráfico. Os resultados alcançados foram satisfatórios, especialmente na geração de uma imagem estática. No entanto, para avançar em direção à geração de imagens dinâmicas, é necessário realizar estudos adicionais e desenvolver projetos futuros.

Dentro do escopo proposto, foi possível efetuar a comunicação entre o software e o hardware utilizando barramentos. Isso demonstra a viabilidade e a funcionalidade da interface de comunicação.

Em conclusão, o projeto mostrou que a comunicação entre o HPS e o Processador Gráfico é viável e eficaz para a geração de imagens estáticas. O próximo passo será aprofundar os estudos e os desenvolvimentos necessários para alcançar a geração de imagens dinâmicas.

Referências

Character device drivers — The Linux Kernel documentation. Disponível em: https://linux-kernel-labs.github.io/refs/heads/master/labs/device_drivers.html.

Memory mapping — The Linux Kernel documentation. Disponível em: https://linux-kernel-labs.github.io/refs/heads/master/labs/memory_mapping.html.

Technologies, Terasic. DE1-SoC User Manual. Disponível em: http://www.ee.ic.ac.uk/pcheung/teaching/ee2_digital/de1-soc_user_manual.pdf.

SÁ BARRETO, Gabriel. Desenvolvimento de uma Arquitetura Baseada em Sprites para criação de Jogos 2D em Ambientes Reconfiguráveis utilizando dispositivos FPGA. s.d. 14 f. Trabalho de Conclusão de Curso (Graduação em Engenharia de Computação) Universidade Estadual de Feira de Santana, Bahia.

Lab 1: Acessando dispositivos de Hardware da FPGA. Bahia: Universidade Estadual de Feira de Santana, 2024