quarta-feira, 5 de janeiro de 2011

Implementação da tela VGA em assembly

Aproveitando o post anterior do THOR27 sobre a implementação de cores em sistemas operacionais, vou postar um pouco sobre como o computador transforma os seus comandos em cores visíveis na tela do computador. A implementação será feita em linguagem assembly do processador MIPS, um dos modelos de processador mais usados para ensinar arquiteturas de processadores.

Introdução


Se você não sabe o que é assembly, nada que uma boa pesquisa no Google não resolva, mas acho que vale a pena perder alguns segundos com a definição. Os computadores são baseados num dos princípios mais simples que existem em elétrica ou eletrônica. Se temos um fio que conduz eletricidade, existem duas possibilidades para ele em qualquer tempo: ou passa corrente ou não passa corrente. Se passa corrente, representamos como 1; se não passa, representamos como 0. Somente com essas simples definições é possível construir computadores como os que temos hoje, mas falar sobre isso ia tomar muito tempo, então deixo como pesquisa para quem quiser se inteirar.

O coração de todo computador é o processador, que se baseia nos mesmos princípios: ou passa corrente (0) ou não passa (1). Claro que a maior parte dos programadores não fica plugando cabos e conectores para descobrir as funções que podem ser executadas pelo processador, então todo fabricante publica uma especificação de seu código assembly, que basicamente diz todas as funções que podem ser executadas pelo processador.

Há uma grande discussão na indústria se o conjunto de instruções deve ser reduzido (RISC) para facilitar o aprendizado ou deve ser mais completo (CISC) para fornecer mais possibilidades de implementação ao programador. De modo geral podemos dizer que os computadores maiores utilizam CISC (processadores Intel e AMD) e os menores utilizam RISC (ARM e MIPS). Como o objetivo é mostrar uma possibilidade de implementação que seja fácil de entender, o processador que vamos utilizar no exemplo é o MIPS.

A principal vantagem do MIPS em relação aos seus competidores é que o conjunto mínimo de instruções contém apenas oito instruções, ou seja, o processador só executa oito diferentes comandos. E muito fácil então a qualquer programador conhecer os comandos que ele executa, ainda que seja difícil implementar algumas funcionalidades com tão poucos comandos. Passa a ser importante então uma outra característica de todo programador: a criatividade.

Voltando ao objetivo do post, o que queremos é possibilitar a escrita de alguma coisa na tela de um computador. Via de regra toda tela de computador é representada como uma sequência de pixels, ou o menor ponto que a resolução é capaz de representar. O computador desenha a tela quebrando a imagem em pontos no mesmo princípio que discutimos anteriormente: tem cor (1) ou não tem cor (0). Para facilitar o exemplo vamos trabalhar com a resolução de uma tela VGA que tem 640x480, ou seja, 640 pontos na vertical e 480 na horizontal. Então, sempre que quisermos desenhar o caracter, precisamos passar sua posição na horizontal, na vertical e indicar se o ponto contém luz (1) ou não contém (0).

Você deve estar se imaginando como é possível desenhar, então já vou entregar o maior segredo: nesse link está uma tabela que diz qual o código de cada um dos caracteres na famosa tabela ascii e como ele é representado. Guarde com cuidado, pois foi muito difícil encontrar essa informação. Podemos ver que cada caracter ocupa um espaço de 8x8 pixels, ou seja, 64 bits. São necessários então 64 bits para representar um caracter da tabela ascii numa tela VGA.

Código Assembly

Agora que já sabemos do que estamos tratando podemos passar para o código propriamente dito. O objetivo então é escrever um programa que tenha a capacidade de, dado um determinado caracter, escrevê-lo na tela na posição (x,y). Traduzindo para a linguagem assembly, vamos receber em um registrador e em outros dois a posição que ele deve ser escrito.

O primeiro passo é carregar na memória o mapa de caracteres VGA fornecido acima como dado para o programa.


 .data
char: .word 0x00000041 # Aqui vai o caracter ascii a ser impresso. 41 é A.
min: .word 0x00000021 # Abaixo disso, nenhum caracter será impresso (tudo 0)
max: .word 0x0000007F # Acima disso, a tela fica preta (todos os caracteres 1)
# Caracteres para a tabela ascii
ini: .word 0x00000021 # O primeiro caracter da tabela ascii
incr: .word 0x00000001 # Vou utilizar essa word para incremendar o contador
# Não consegui descobrir outra forma que não seja declarar os dados aqui
excla1: .word 0x08080808
excla2: .word 0x00000800

(...)

til2: .word 0x4C000000
pr1: .word 0x11111111 # Tudo 1
br1: .word 0x00000000 # Tudo 0


Já temos a primeira parte do programa, que é a declaração dos dados e variáveis. Agora passamos ao texto ou ao programa propriamente dito:

print_char:
lw $a0, char # A word representando o ascii que vai ser impresso vai em $a0
# Aqui começa a descrição dos caracteres que serão impressos. Uma verdadeira tabela de símbolos
lw $t1, max # Armazenamos a maior word representável no VGA
slt $t0, $a0, $t1 # Se a word for menor do que o maior valor representável, $t0 vai pra 1
beq $t0, $zero, pr # Se $t0 estiver em 0, significa que a word é maior que o máximo. Imprimos tudo preto.
lw $t1, min # Agora $t1 recebe o mínimo
slt $t0, $t1, $a0 # Agora se $a0 for menor que o mínimo, imprimimos tudo branco
beq $t0, $zero, br
# Agora fazemos a substituição dos caracteres
lw $t0, ini # Código ascii

(...)

# lw $t0, 0x0000007D # Código ascii
add $t0, $t0, $t1 # Próximo da fila
beq $a0, $t0, chav2 # Branch para o label de impressão do caracter
# lw $t0, 0x0000007E # Código ascii
add $t0, $t0, $t1 # Próximo da fila
beq $a0, $t0, til # Branch para o label de impressão do caracter
beq $a0, $t0, excla # Branch para o label de impressão do caracter
lw $t1, incr # Armazeno o valor a ser incrementado no registrador $t1
# lw $t0, 0x00000022 # Código ascii
add $t0, $t0, $t1 # Próximo da fila
beq $a0, $t0, aspas # Branch para o label de impressão do caracter

A lógica da chamada é simples e certamente pode ser melhorada (contribuições são bem vindas). O registrador $a0 possui o caracter que vai ser impresso na tela. O registrador $t1 é incrementado e vai verificando se o valor que possui é o que deve ser impresso. Quando encontra o valor que deve ser impresso, vai para a posição da impressão do caracter, representada abaixo:

excla: # Agora é dividir os bits: os primeiros 32 em $a1 e o restantes em $a2
lw $a1, excla1
lw $a2, excla2
j print # Depois vamos para a impressão
aspas:
lw $a1, aspas1
lw $a2, aspas2
j print # Depois vamos para a impressão

(...)

til:
lw $a1, til1
lw $a2, til2
j print # Depois vamos para a impressão
pr:
lw $a1, pr1 # Tudo 1
lw $a2, pr1 # Tudo 1
j print
br:
lw $a1, br1 # Tudo 0
lw $a2, br1 # Tudo 0
j print

Precisamos agora escrever na tela propriamente dita. A função abaixo representa a impressão:

# Se chegamos até aqui, hora de imprimir de acordo com a tabela
print:
la $t0, 0x00001001 # A impressão do caracter começa a partir do endereço 4097. Ponho em $t0.
sw $a1, 0($t0) # $a1 contém os primeiros 32 bits dos caracteres
sw $a2, 4($t0) # $a2 contém os próximos 32 dos caracteres
# sw $a1, 0($ra) # $a1 contém os primeiros 32 bits dos caracteres
# sw $a2, 4($ra) # $a2 contém os próximos 32 dos caracteres
jr $ra # Retorna para quem chamou

Parabéns! Você acaba de imprimir o seu primeiro caracter numa tela VGA.

Como pode ser facilmente percebido, o código não é nenhum pouco eficiente, e precisa ser integrado a outros programas em MIPS para ter alguma utilidade prática. Afinal, o caracter a ser impresso está hard coded na letra A. O exemplo serve mais para ajudar quem quiser começar a mexer com o assunto, porque quando estava estudando isso foi muito difícil achar exemplos para aprender.

Se tiver alguma sugestão, fique à vontade para apresentá-la. E não se esqueça de postar se conseguir tornar o código mais limpo e eficiente.

Obs.: Você pode baixar o código completo aqui.

2 comentários:

  1. Ae Eduardo! Parabéns pelo Post! Muito legal!

    E pra testar esse código? #comofaz?

    Valeu :D

    ResponderExcluir
  2. Fala Thommy,

    Existem várias formas de testar, mas para verificar ele escrevendo na tela mesmo você vai precisar escrever na memória de alguma placa de vídeo.

    Para compilar tem o MARS (http://courses.missouristate.edu/KenVollmar/MARS/), um aplicativo feito em Java que simula um processador MIPS. Você pode verificar o que está escrito na memória de saída e ver se representa o código hexadecimal que deveria.

    Tem também o SPIM (http://pages.cs.wisc.edu/~larus/spim.html) que possui até um compilador de linha de comando Linux para o assembly do MIPS.

    Para ver o caracter mesmo, só com um circuito lógico programável ou algum dispositivo que contenha um chip MIPS e uma tela VGA acoplada. Dentre os circuitos lógicos programáveis, os mais populares são as placas da Altera, mas o preço é realmente salgado. O simulador Quartus II (http://www.altera.com/products/software/quartus-ii/subscription-edition/qts-se-index.html) é bem útil e pode ser ajuda suficiente para começar, pois ele permite ler a memória também.

    Enfim, trabalhar com assembly sempre tem o problema de que nunca poderemos ver facilmente o dispositivo funcionando, pois precisaríamos programar diretamente o processador. Mas alguns simuladores podem ajudar.

    Abraços

    ResponderExcluir