Rodando o jogo do T-Rex em um testador de componentes

Intro

Faz algum tempo que comprei um desses testadores de transistor no eBay por cerca de 10$. É bastante útil, especialmente porque meu multímetro não mede capacitância ou indutância. Eu fiz um pequeno post sobre ele aqui. A primeira coisa que notei foi o popular Atmega 328p na parte de trás da placa.

Por algum motivo desconhecido, eu tentei calibrar a placa (não havia necessidade) e acabei bagunçando os valores de calibração. O que quer que fosse errado, estava dentro do chip, especificamente dentro da eeprom do chip. Depois, eu encontrei uma maneira de ler e programar o atmega, e tentei apagar a eeprom usando o avrdude, mas não consegui obter as medidas corretas novamente. Isso me fez pensar que o testador já vem calibrado do vendedor. De qualquer forma, já que era tão fácil de programar o chip, eu tinha decidido que se eu não pudesse conseguir fazê-lo voltar a funcionar, eu ia só usá-lo para qualquer outra coisa. Acontece que eu estava usando um capacitor pequeno para calibrá-lo, depois de usar um capacitor de maior valor do que o recomendado a calibração funcionou muito bem.

De qualquer forma, o atmega328p utilizado na placa é o mesmo presente em muitos duinos por aí (UNO, Nano, Mini, mil clones .. etc), então isso me fez pensar “Por que algo tão hackeável não está sendo modificado?”. Digo, ele tem um display LCD, uma espécie de botão, conexão de bateria e uma porta SPI disponível.

Nesse post, vou explicar como eu consegui fazer o testador de componentes executar um jogo semelhante ao jogo T-Rex do Chrome. Não é prefeitamente fiel, mas eu aprendi alguns truques ao longo do caminho pra torná-lo cada vez melhor. Como esta é minha primeira tentativa de programar algo jogo/gráfico, muito do que eu fiz aqui foi feito sem conhecimento prévio. Sugestões são bem-vindas!

Primeiro, um pouco de engenharia reversa

Eu comecei a trabalhar neste projeto em julho passado. Mas só no fim do ano eu consegui tempo livre da faculdade (férias) e tive tempo para terminá-lo e documentá-lo. Algumas informações aqui podem ser apenas um pouco imprecisas, mas não se preocupe, eu tenho memória meio que boa :)

Eu sabia que o testador era baseado em um projeto de código aberto de Markus Frejek. Mas como havia tantas versões que eu não acreditava que o repo original iria conter arquivos para o meu testador, então eu comecei a procurar online para descobrir a referência do LCD e o pinout.

Pesquisando online eu li que um LCD que corresponde  (em tamanho e aparência) ao LCD na placa é baseado no controlador ST7565, de qualquer forma, eu tinha que saber quais pinos do atmega estavam conectados ao LCD. Simplesmente usando um multímetro e o teste de continuidade, foi fácil descobrir que o LCD estava conectado a PORTD do microcontrolador. Mas como eu  saberia quais pinos do microcontrolador estavam conectados a cada pino do LCD (IO, RESET, CLK e assim por diante), se eu não conseguia encontrar a pinagem do LCD?

A melhor maneira, eu pensei, seria ter um olhar o firmware, isso se ele estivesse disponível. A flash não estava protegida e eu poderia ler sem problema. Eu usei ODA para fazer o disassembly. Há uma porta ISCP “invertida” na placa, então eu não tive que soldar fios a fim de ler o chip. Veja na figura abaixo como eu conectei meu USBASP ao Atmega.

Antes de qualquer coisa, eu fiz backup do firmware original:

E simplesmente usei o ODA para fazer o disassembly.

Mas agora que eu tinha o firmware antigo, o que eu estava procurando aqui? Muito fácil, procurando por escritas no endereço de PORTD, eventualmente eu iria encontrar os pinos utilizados como SPI. A hardware SPI do mega328p  está em PORTB, então os GPIOs em PORTD estavam enviando os dados para o LCD como uma software SPI.

Vamos dar uma olhada nesta tabela que peguei do Datasheet do mega328p:

Da tabela, o endereço de PORTD é 0x0B quando não estiver usando as instruções LD ou ST. Procurando este endereço no disassembly, encontrei o seguinte código:

Daí eu descobri que:

E de outras partes do disassembly:

Massa, e fiquei até feliz por descobrir tudo sozinho, mas alguns dias depois eu encontrei o seguinte pedaço de código no arquivo config.h do repositório de código aberto:

Beleza, todo o trabalho anterior para nada. Como meu amigo Márcio costumava dizer: “Muito trabalho é perdido”.

Fazendo o LCD funcionar

Ok, eu tinha o pinout do LCD, e sua referência. Eu procurei por uma biblioteca para controlá-lo e encontrei o repositório da biblioteca da Adafruit aqui.

Eu não consegui fazer funcionar no início, mas depois de variar o contraste, pude ver alguma imagem no LCD, mas esta estava revertida.

A biblioteca do Adafruit foi escrita para um módulo invertido (veja a figura abaixo). Para usá-la corretamente com o testador de transistor eu teria que modificá-la.

 

Adafruit ST7565 LCD. Disponível here.

No testador de componentes, a cabo do LCD está em cima.

Eu fiz as modificações necessárias para usar a biblioteca com o T3 transistor testador, tanto para C e Arduino. Eu coloquei tudo em um repositorio no githubAgora que eu tinha uma biblioteca de vídeo, eu poderia começar a trabalhar no jogo. Começar a trabalhar no jogo em C. Arduino é muito lento e inviável aqui, cada digitalWrite() é traduzido em um monte de instruções pra traduzir um número em um endereço de PORT e um bit nesse endereço. Mais sobre performance um pouco mais abaixo.

Porque o T-Rex?

Primeiro de tudo, é um jogo quase monocromático (no entanto, as nuvens são de um cinza mais claro), por isso ficaria ok no LCD. Em segundo lugar, eu diria que é um jogo um pouco simples. Isto é, eu não sei nada sobre programação de jogos e consegui fazer funcionar. Além disso, é um  joguinho popular popular e facilmente reconhecível. Finalmente, dinossauros são muito daora.

Eu encontrei um  repostorio no github com o jogo do t-rex extraído do Chromium. A partir dos arquivos de lá eu poderia obter os sprites e olhar o código fonte do jogo, para ver como reproduzi-lo em C. Muito obrigado a Wayou por compartilhar esse repositório!

Gráficos

Eu peguei a seguinte imagem do repositório mencionado acima. Então, eu redimensionei-a para que o olho do rex tivesse um pixel de tamanho.

Então eu tive a tarefa irada de cortar todos os sprites do dino, solo e cactos. Eu usei o Gimp para cortar e convertê-los para bitmap.

Em seguida, usei a ferramenta recomendada no repositório daa Adafruit, bmp2glc para converter cada um dos bitmaps em arquivos .h com matrizes de bytes. zzz

Game Logic

Eu acredito que o jogo pode ser resumido em um loop que:

  • Calcula a nova posição dos sprites
  • Checa colisões entre o cactos e o rex
  • Desenha os sprites no LCD
  • Atualiza o score

Essas etapas são ok de se trabalhar. Mas depois de escrever alguns sprites para no LCD, notei que havia alguns problemas sérios com a performance. Eu nem poderia testar o jogo a essa velocidade. Então é melhor dar uma olhada nisso primeiro.

Problemas de performance

Eu tive dois problemas que eu não consegui corrigir:

Primeiro, o Atmega estava funcionando em 8MHz – Este é 40% da frequência de clock máxima que o mega328p pode ter. Eu ainda queria que ele funcionasse como um componente tester, então eu não iria mudar o cristal.

Em segundo lugar, a tela é conectada a uma soft SPI  – Isso significa que em vez de transferir um byte em apenas um ciclo de clock, o micro tem que mudar alguns dados, checar um bit, atualizar os GPIO de dado e clock … tudo isso oito vezes por byte.

Todos os outros problemas estavam relacionados à biblioteca do LCD ou ao próprio jogo. Sem a biblioteca do Adafruit eu nem trabalharia neste projeto, é muito útil e fácil de usar, mas só precisava de alguns ajustes para o melhor desempenho.

Analisando os códigos, Eu vi algum código que poderia ser reformulado:

A biblioteca usa um buffer, primeiro escrevemos sprites para o buffer e depois escrevemos o buffer para o LCD.

Então, para escrever um bitmap para o buffer, ele é feito bit a bit chamando setpixel () a cada pixel. Eu modifiquei isto para escrever logo um byte:

Se um byte do bitmap estiver alinhado a uma página no LCD, basta copiá-lo, se não, dividi-lo em dois bytes e gravá-los no buffer. Eu fui esperto aqui e alinhei os sprites no jogo, assim que o único sprite que necessita o ‘else’ acima é o dino, mas somente quando está a saltar.

Outro problema foi que para atualizar a tela, era necessário gravar todo o buffer para o LCD. No começo eu pensei em escrever apenas as páginas modificadas, mas então eu percebi que eu poderia criar uma nova função para escrever apenas alguma parte do buffer, passando as coordenadas para a função.

A antiga função write_buffer ():

Uma função nova, pra escrever apenas parte do buffer no LCD:

Assim, eu poderia controlar as atualizações do LCD escrevendo apenas dentro das coordenadas dos sprites.

Outra coisa, o ST7565 não tem um comando para limpar a tela. Mesmo assim,  a biblioteca tem uma função para isso, porem esta escreve 1024 zeros na tela. Eu vi em um dos códigos de exemplo que é melhor apenas escrever o negativo dos sprites no mesmo lugar do que limpar toda a tela.

Se o jogo fosse fiel, as posições do rex deveriam ser calculadas em tempo real. Mas calcular raízes quadradas, potencias e outras coisas em um micro controlador de 8 bits seria um saco e ia demorar muito tempo. Assim, a trajetória do salto é fixa e sempre a mesma. Em vez de calcular a parábola, o código apenas acessa uma tabela com os valores y para o dinossauro.

Game Logic (Cont.)

Ok, eu disse antes que o jogo seria resumido em:

  • Calcular as novas posições dos sprites

Como seria muito lento para calcular a trajetória do dino ao saltar, usando parâmetros de velocidade e aceleração. Optei por usar apenas uma tabela com valores fixos de y. Ao saltar a posição do rex é obtida a partir do seguinte vetor:

Os cactos e o solo, no entanto, estão apenas tendo seus valores de x diminuídos cada frame.

  • Verificar colisão entre os cactos e rex

Aqui, eu acho que o usual é usar hitboxes. Mas nesse caso, eu percebi que ao escrever um bitmap para o buffer, eu poderia verificar se havia pelo menos um pixel, um único pixel, onde os bitmaps colidiam. Ele funciona assim, antes de eu escrever um byte para o buffer, Eu faço um and (&) do byte anterior com o novo byte. Somente quando um bit é definido na mesma posição em ambos os bytes, temos uma colisão.

A função drawbitmap () que vimos antes se torna:

E no código do jogo eu tenho algo como:

  • Desenhar os sprites no LCD

Eu apenas uso as funções de biblioteca para escrever os bitmaps para o LCD. No entanto, por causa do desempenho e um problema de ghosting, os bitmaps são apenas escritos para o LCD quando mover a posição do objeto (como os cactos) ou alterar um bitmap (como quando o dino muda a perna tocando o chão).

  • Atualizar o score

Eu uso a EEPROM para manter o hiscore quando a placa é desligada. Eu procurei uma área não utilizada do firmware antigo, então eu poderia alternar entre testador de componentes e minigame sem se preocupar com a EEPROM.

  • Mais

Eu usei um pino ADC desconectado como uma fonte de aleatoriedade par srand (), isso significa que eu tenho que esperar por uma conversão ADC. Eu também acho que há alguma matemática em srand (e eu também uso% para limitar o número retornado) que retarda as coisas apenas um pouco, mas eu estou apenas adivinhando, já que não vi o código. Tentei apenas obter um número do timer em modo CTC. Mas é horrível. Preciso estudar isso um pouco mais.

Há um ghosting característico no LCD. Não há nenhuma maneira de corrigir isso, mas ok, até mesmo o Game Boy tinha um pouco disso :)

O LCD leva algum tempo para mudar o estado de um pixel, por isso entre os frames há algum ghosting visível. Ele funciona melhor em fps baixo.

Sobre entrada, o botão presente na placa é usado para iniciar o microcontrolador. Sempre que é pressionado, a luz de fundo do LCD apaga-se. Por isso, usei outro botão conectado aos terminais 1 e 3 do socket ZIF para permitir cliques sem desligar a luz de fundo.

Botão conectado entre 1 e 3 no socket ZIF

Video

Você pode ver um vídeo do jogo abaixo.

Os cactos parecem melhores vistos em pessoa. Eu acho que por causa da persistência do efeito de visão. O testador de componentes seria melhor para jogos mais lentos. Eu acho que arkanoid ou space invaders seria muito bacana, mas a luz de fundo teria que ser desligada, a fim de usar os dois botões.

Conclusions

Eu passei mais tempo neste projeto do que eu queria, por isso ainda não está totalmente acabado. Está faltando os pterodactyls, por exemplo, e a maneira que os cactos repetem no jogo original. Provavelmente vou trabalhar nele no futuro com outro microcontrolador ou LCD. O código está disponível em um repositório github aqui.

Se for tentar usar o meu código, favor o faça por sua conta e risco. Não é garantido que o seu testador de componentes seja exatamente igual ao meu. Aconselho fazer um backup da flash e eeprom antes de substituir o firmware.

Esta foi principalmente uma forma de treinar programação em C, e estou bastante satisfeito com os resultados. Eu consegui aprender a manipular bitmaps, tenho usado ponteiros com frequência, structs e algumas outras pequenas coisas. Melhorar a biblioteca foi um desafio, mas muito divertido.

Gostaria de agradecer a Rafael por seu interesse no projeto, discutimos algumas das idéias que eu implementei aqui.
Acho que paramos por aqui.


Obrigado pela leitura. Até um próximo post o /

 

Robson Couto

Estudante de engenharia elétrica. As vezes parece gostar mais dos consoles antigos que dos jogos. Tem interesse em dominar bits, bytes e afins.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

*