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:

avrdude -p m328p -P usb -c usbasp -U flash:r:firmware.bin:r

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:

.data:00000a5e 66 23    and    r22, r22      ;Depending on the value of r22, does sbi or cbi.
.data:00000a60 11 f0    breq    .+4            ;  0x00000a66
.data:00000a62 5b 9a    sbi    0x0b, 3    ; 11  ;command/data line?
.data:00000a64 01 c0    rjmp    .+2            ;  0x00000a68
.data:00000a66 5b 98    cbi    0x0b, 3    ; 11
.data:00000a68 90 e0    ldi    r25, 0x00    ; 0
while:
.data:00000a6a 5a 98    cbi    0x0b, 2    ; 11
.data:00000a6c 87 ff    sbrs    r24, 7      ;Skip if Bit in Register is Set. Doesn't change the line? Maybe data line?
.data:00000a6e 02 c0    rjmp    .+4            ;  0x00000a74   ;jump and does not set bit
.data:00000a70 59 9a    sbi    0x0b, 1    ; 11  ;set bit. Maybe data line
.data:00000a72 01 c0    rjmp    .+2            ;  0x00000a76   ;
yes:
.data:00000a74 59 98    cbi    0x0b, 1    ; 11  ;clear bit depending on r24
.data:00000a76 5a 9a    sbi    0x0b, 2    ; 11  ;plossibly clock line
.data:00000a78 9f 5f    subi    r25, 0xFF    ; 255    ;subtract immediate r25=r25-0xFF (???)
.data:00000a7a 98 30    cpi    r25, 0x08    ; 8        ;compare with immediate
.data:00000a7c 11 f0    breq    .+4            ;  0x00000a82   ;escape
.data:00000a7e 88 0f    add    r24, r24                    
.data:00000a80 f4 cf    rjmp    .-24           ;  0x00000a6a   ;while?
.data:00000a82 08 95    ret
 
 
r22 is only passed with a value of 0x00 or 0x01, A great chance it is a cmd/data line
 
Example
.data:00000c14 8a ea    ldi    r24, 0xAA    ; 170
.data:00000c16 60 e0    ldi    r22, 0x00    ; 0
.data:00000c18 0e 94 2f 05 call    0xa5e    ;  0x00000a5e
 
I suspect r24 is the main value passed to the routine

Daí eu descobri que:

a0           PORTD3
clock        PORTD2
data         PORTD1

E de outras partes do disassembly:

reset        PORTD4 //only called at the start of LCD
cs           PORTD5

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:

/*
 *  Chinese clone T3/T4 with ST7565 display
 *  - thanks to tom666 @ EEVblog forum 
 */

#if 0
#define LCD_ST7565R_SPI
#define LCD_GRAPHIC                     /* monochrome graphic display */
#define LCD_PORT         PORTD          /* port data register */
#define LCD_DDR          DDRD           /* port data direction register */
#define LCD_RESET        PD4            /* port pin used for /RES */
#define LCD_A0           PD3            /* port pin used for A0 */
#define LCD_SCL          PD2            /* port pin used for SCL */
#define LCD_SI           PD1            /* port pin used for SI (LCD's data input) */
#define LCD_CS           PD5            /* port pin used for /CS1 (optional) */
#define LCD_DOTS_X       128            /* number of horizontal dots */
#define LCD_DOTS_Y       64             /* number of vertical dots */
#define LCD_START_Y      0              /* start line (0-63) */
#define LCD_CONTRAST     11             /* default contrast (0-63) */
#define FONT_8X8_V                      /* 8x8 font, vertically aligned */
#define SYMBOLS_24X24_V                 /* 24x24 symbols, vertically aligned */
#endif

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.

// the most basic function, set a single pixel
void setpixel(uint8_t *buff, uint8_t x, uint8_t y, uint8_t color) {
  if ((x >= LCDWIDTH) || (y >= LCDHEIGHT))
    return;

  // x is which column
  if (color) 
    buff[x+ (y/8)*128] |= _BV(7-(y%8));  
  else
    buff[x+ (y/8)*128] &= ~_BV(7-(y%8)); 
}

void drawbitmap(uint8_t *buff, uint8_t x, uint8_t y, 
		const uint8_t bitmap, uint8_t w, uint8_t h,
		uint8_t color) {
  for (uint8_t j=0; j<h; j++) {
    for (uint8_t i=0; i<w; i++ ) {
      if (pgm_read_byte(bitmap + i + (j/8)*w) & _BV(j%8)) {
	setpixel(buff, x+i, y+j, color);
      }
    }
  }
}

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:

void drawbitmap2(uint8_t *buff, uint8_t x, uint8_t y,
const uint8_t *bitmap, uint8_t w, uint8_t h,uint8_t color) {
  if((y%8)==0){
    for (uint8_t j=0; j<(h/8); j++) {
      for (uint8_t i=0; i<w; i++ ) {
        if(color){
          buff[(x+i)+((j+(y/8))*128)]|=pgm_read_byte(bitmap + i + (j)*w);
        }else{
          buff[(x+i)+((j+(y/8))*128)]&=~pgm_read_byte(bitmap + i + (j)*w);
        }
      }
    }
  }else{
    uint8_t shift=y%8;
    for (uint8_t j=0; j<(h/8); j++) {
      for (uint8_t i=0; i<w; i++ ) {
        if(color){
          buff[(x+i)+((j+(y/8))*128)]|=(pgm_read_byte(bitmap + i + (j)*w)<<shift);
          buff[(x+i)+((j+1+(y/8))*128)]|=(pgm_read_byte(bitmap + i + (j)*w)>>(8-shift));
        }else{
          buff[(x+i)+((j+(y/8))*128)]&=~(pgm_read_byte(bitmap + i + (j)*w)<<shift);
          buff[(x+i)+((j+1+(y/8))*128)]&=~(pgm_read_byte(bitmap + i + (j)*w)>>(8-shift));
        }
      }
    }
  }
}

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 ():

void write_buffer(uint8_t *buffer) {
  uint8_t c, p;
  for(p = 0; p < 8; p++) {
      st7565_command(CMD_SET_PAGE | p);
      st7565_command(CMD_SET_COLUMN_LOWER | 0);
      st7565_command(CMD_SET_COLUMN_UPPER | 0);
      st7565_command(CMD_RMW);

      for(c = 0; c < 128; c++) {
        st7565_data(buffer[(128*p)+c]);
      }
   }
}

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

void write_part(uint8_t *buffer,uint8_t x, uint8_t y, uint8_t w,uint8_t h) {
  uint8_t c, p;
  for(p = 0; p < 8; p++) {
    if((p*8)>=y && (p*8)<(y+h)){
      st7565_command(CMD_SET_PAGE | p);

      st7565_command(CMD_SET_COLUMN_LOWER | (y&0x0f));
      st7565_command(CMD_SET_COLUMN_UPPER | ((y>>4)&0x0f));
      st7565_command(CMD_RMW);

      for(c = x; c < x+w; c++) {
        st7565_data(buffer[(128*p)+c]);
      }
    }
  }
}

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:

points[]={38,31,28,25,23,22,20,19,18,17,16,15,14,14,13,12,12,11,11,10,10,10,9,9,9,8,8,8,8,8,8};

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:

uint8_t drawbitmap2(uint8_t *buff, uint8_t x, uint8_t y,const uint8_t *bitmap, uint8_t w, uint8_t h,uint8_t color) {
  uint8_t new_byte,status=0,prev_byte;
  if((y%8)==0){
    for (uint8_t j=0; j<(h/8); j++) {
      if(y>LCDHEIGHT)break;
      for (uint8_t i=0; i<w; i++ ) {
        if(x+i>=LCDWIDTH)break;
        if(color){
          new_byte=pgm_read_byte(bitmap + i + (j)*w);
          prev_byte=buff[(x+i)+((j+(y/8))*128)];
          status=status|(new_byte&prev_byte);
          buff[(x+i)+((j+(y/8))*128)]=prev_byte|new_byte;
        }else{
          buff[(x+i)+((j+(y/8))*128)]&=~pgm_read_byte(bitmap + i + (j)*w);
        }
      }
    }
  }else{
    uint8_t shift=y%8;
    for (uint8_t j=0; j<(h/8); j++) {
      if(y>LCDHEIGHT)break;
      for (uint8_t i=0; i<w; i++ ) {
        if(x+i>=LCDWIDTH)break;
        if(color){
          new_byte=(pgm_read_byte(bitmap + i + (j)*w)<<shift);
          prev_byte=buff[(x+i)+((j+(y/8))*128)];
          status=status|(new_byte&prev_byte);
          buff[(x+i)+((j+(y/8))*128)]=prev_byte|new_byte;
          //
          if(y>(LCDHEIGHT-8))continue;

          new_byte=(pgm_read_byte(bitmap + i + (j)*w)>>(8-shift));
          prev_byte=buff[(x+i)+((j+1+(y/8))*128)];
          status=status|(new_byte&prev_byte);
          buff[(x+i)+((j+1+(y/8))*128)]=prev_byte|new_byte;
        }else{
          buff[(x+i)+((j+(y/8))*128)]&=~(pgm_read_byte(bitmap + i + (j)*w)<<shift);
          if(y>(LCDHEIGHT-8))continue;
          buff[(x+i)+((j+1+(y/8))*128)]&=~(pgm_read_byte(bitmap + i + (j)*w)>>(8-shift));
        }
      }
    }
  }
  return status;
}

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

draw_dino(Rex,1);
...
bump=draw_cactus(cactus,1);
...
if(bump){
  //game over
}
  • 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.

//at startup:
score=0;
highscore=get_score();//from eeprom

//when game over:
if(score>highscore)update_score(score);//to 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

Recentemente graduado Engenheiro Eletricista. Gosto de dedicar meu tempo a aprender sobre computadores, eletrônica, programação e engenharia reversa. Documento meus projetos no Dragão quando possível.

6 thoughts to “Rodando o jogo do T-Rex em um testador de componentes”

  1. Legal, Robson!!

    Mas não teria como disponibilizar o firmware extraído do testador pra gente tentar fazer ele funcionar num outro arduino? Seria um projeto bem interessante!

    1. Olá David.
      O firmware extraido está no repositório, em formato binário, claro.
      O projeto do testador é baseado em um projeto de código aberto de Markus Frejek, o código está disponível online.

      Robson

      1. Valeu, Robson! Consegui localizá-lo aqui!
        Acho que devem ser feitas algumas adaptações, pois ele pressupõe um hardware básico pré-existente. Mas acredito que nada do outro mundo hehehehe! Eu inclusive já tinha testado aqui capacímetros e ohmimetros básicos, e a ideia (pelo que pude perceber numa olhada rápida pela lógica do firmware) não foge muito do que já imaginava.

        pra falar a verdade, meu interesse maior era descobrir como o firmware reconhecia os diferentes tipos de transistores, e como ele conseguia identificar os CMOS. estou quase descobrindo, hehehehehe

        1. Parabéns David! Eu não olhei o código direito ainda, mas acredito que deve ser bastante modular.
          Quem sabe você não desenvolve um shield para conectar ao Arduino?
          De qualquer forma, boa sorte com o hack!

          Robson

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *