Backup e gravação de Memory Card PS1 (Arduino & Python)

Dont speak portuguese? English version here

Intro

Gostaria de começar o post falando das muitas horas de diversão na minha infância proporcionadas pelo meu Playstation, mas não, nunca tive um. Se não me engano, o único jogo do PS1 que joguei foi Digimon Rumble Arena. Apesar disso, pelo meu interesse por video games e hardware, sei uma coisa ou outra sobre ele.

Nintendo e Sony se juntaram pra criar um  aparelho que permitisse ao SNES o uso de CDs. A Nintendo cancelou(Má escolha) o projeto com a Sony, que decidiu criar seu próprio console. A Nintendo depois de um tempo se juntaria(E cancelaria também) à Philips com o mesmo propósito. O que resultou no CD-i, e em pérolas como essa.

Voltando ao Playstation. Encontrei no eBay esse aparelho chamado PocketStation, que foi lançado no Japão apenas. O PocketStation pode ser usado como memory card, mas também como um minúsculo console portátil, rodando extras que vinham com Jogos pro PS1. Final Fantasy XIII por exemplo, vinha com Chocobo World pro Pocket Station. O Pocket Station permitia também enviar ou trocar saves com amigos através de uma porta infravermelho.

pcket

Pesquisando um pouco, descobri que o PocketStation possui um processador AMR 7TDMI, o mesmo que o GBA. A resolução da tela é 32×32 pixels, 128KB de memória de programa, 2KB de RAM, especificações poderosas pra algo de 16 anos atrás. Acabei por comprar uma unidade, mesmo sem saber sobre o protocolo de comunicação ou como “injetar” código no dispositivo.

Primeiro, tinha que descobrir como acessar a memória de programa. Os programas que o PocketStation roda ficam gravados como saves. Sendo normalmente gravados pelo console que reconhece o PocketStation como se fosse um memory card comum. Pesquisei então pela comunicação console-memory card.

Leitura dos Saves

Protocolo e Sinais

Martin Korth, além de programar o emulador no$psx(no cash psx) fez um excelente trabalho de documentação sobre o PlayStation. Em sua página é explicado tudo sobre o hardware do console e os sinais e protocolos usados entre o video game e controles, memory cards e até controles pra jogos de pesca(eita).

Nesse link, é possível encontrar as especificações do protocolo usado pelo console pra acessar controles e memory cards.  Em resumo, o protocolo é similar ao SPI. A linha de clock fica ociosa em estado alto(CPOL=1), a entrada é colhida na borda de subida e a saída alterada na borda de descida(CPHA=1), enquanto os dados são transferidos a 250KHz, começando pelo bit menos significativo(LSB first). Além disso, também há um pino ACK que responde com um pulso logo após um byte ser transmitido.

Além disso, pesquisei por uma implementação pronta pra gravar os saves. Encontrei o blog de ShendoXT, mas não consegui utilizar seu script python. Meu código Arduino é levemente baseado em sua implementação.

Conexão do memory card/pocketstation ao Arduino

O código Arduino usa a porta SPI, Esses pinos não podem ser configurados e dependem da placa. Na maioria dos Arduíno isso era padrão, mas com o surgimento de novas placas os pinos da SPI variam bastante. Eu usei um Arduino mini que comprei pelo eBay por menos de dois dólares.

img_0288

Abaixo uma descrição dos pinos do memory card e sua conexão ao Arduino (UNO,mini,Nano,NG):

  _____________
 /             \       Pin connections:
|               |         1 - Data        -> Connect to pin 12 on Arduino (MISO)
|               |         2 - Command     -> Pin 11 of Arduino (MOSI)
|  MEMORY CARD  |         3 - 7.6V        -> 5V of Arduino
|               |         4 - GND         -> Gnd of Arduino
|               |         5 - 3.6V        -> 3.3 of Arduino
|_______________|         6 - Atention    -> Pin 10 of Arduino (CS)
|1 2|3 4 5|6 7 8|         7 - Clock       -> Pin 13 of Arduino (SCL) 
                          8 - Acknowledge -> Pin 2 of Arduino (INT0)

Mas se eu nao tenho um slot sucata de memory card, o que fazer?

Eu tenho usado esses headers em todos tipos de gambiarras, haha. Aqui usei uma barra dupla, a conexão é estável o suficiente. Sempre funciona ;)

img_20160918_111618760Conector memory card ps1 improvisado.

Okay, depois de conectar tudo. Hora do código.

Configuração da porta SPI no Arduino

A partir das especificações na página do no$psx, a SPI foi configurada da seguinte forma:

  //activate pull up
  pinMode(2, INPUT);
  digitalWrite(2, HIGH);
  pinMode(12, INPUT);//For Arduino Uno,Mini...
  digitalWrite(12, HIGH);
  
  SPI.setClockDivider(SPI_CLOCK_DIV64);//16MHz/64 =250KHz
  SPI.setBitOrder(LSBFIRST);
  SPI.setDataMode(SPI_MODE3);//CPHA=1 CPOL=1
  SPI.begin();

As primeiras instruções são pra ativar os resistores pull up nos pinos de entrada, pois o memory card pode apenas baixar o nível da linha. Novos modelos de Arduino possuem a SPI em pinos diferentes, sendo necessário ajustar o código a isso.

Transmissão de bytes

byte transfer(byte data) {
  byte rx;
  response = HIGH;
  rx = SPI.transfer(data);
  int timeout = 2000;
  while (response == HIGH) {
    delayMicroseconds(1);
    timeout--;
    if (timeout == 0) return 0; 
  }
  return rx;
}

A transmissão é simples, já que a porta SPI toma conta. No entanto, repare no delay depois da transmissão. É esperado um ACK pra confiramr o recebimento do byte. Quando o ACK é recebido, o programa parte pra transmissão do próximo byte.

Ao mesmo tempo que a porta SPI transmite um byte, outro byte é recebido, esse byte é retornado pela função tranfer(). Isso é útil, pois o memory card retorna valores específicos em determinadas situações, por exemplo, quando o ultimo byte de um pacote é transmitido, um 0x47 (Ascii ‘G’ for good) é retornado.

Leitura dos saves no Arduino

Dadas as informações sobre os comandos de leitura e escrita na página do no$psx, o seguinte código é usado para fazer a leitura de um setor da memória. Há ao todo 1024 setores de 128 bytes, 16 desses setores formam um bloco e um save usa no mínimo um bloco. O primeiro bloco é usado como diretório e contem informação de quais blocos estão ocupados e com quais saves.

void readSector(unsigned int Adr) {
  digitalWrite(CS, LOW);
  transfer(0x81);
  transfer(0x52);
  transfer(0x00);
  transfer(0x00);
  transfer(Adr >> 8);
  transfer(Adr & 0xFF);
  transfer(0x00);
  transfer(0x00);
  transfer(0x00);
  transfer(0x00);
  for (int i = 0; i < 128; i++) {
    Serial.write(transfer(0x00));
  }
  transfer(0x00);
  SPI.transfer(0x00);
  digitalWrite(CS, HIGH);
  delayMicroseconds(500);
}

Essa função é usada 1024 vezes para realizar a leitura de todos os setores. Simples assim.

Script de Leitura em Python

O scrip Python apenas recebe os bytes da porta serial e os salva em um arquivo.

romsize=128*1024 #128K
name=input("What the name of the file?")
ser.write(bytes("R","ASCII"))
numBytes=0
f = open(name, 'ab')#open for appending
while (numBytes<romsize):
    while ser.inWaiting()==0:
        print("waiting...\n",numBytes)#shows the number of bytes read
        time.sleep(0.1)
    data = ser.read(1)
    f.write(data)#put a byte to the file
    numBytes=numBytes+1

Gravação de Saves

Pra gravar os saves basta usar o mesmo script Python e selecionar a opção write. A gravação é um processo mais delicado que a leitura e cada vez que um setor é enviado pro Arduino, os bytes são verificados antes de se gravar na memória, caso o checksum esteja diferente do esperado, o Arduino requisita a retransmissão do setor.

Abaixo a função writeSector() no Arduino:

void writeSector(unsigned char MSB, unsigned char LSB){
  byte dataBuffer[128];
  byte response;
  for(int i=0;i<128;i++){
        while(Serial.available()==0);
        dataBuffer[i]=Serial.read();
  }
  byte CHK = MSB;
  CHK^=LSB;
  digitalWrite(CS, LOW);
  transfer(0x81);
  transfer(0x57);
  transfer(0x00);
  transfer(0x00);
  transfer(MSB);
  transfer(LSB);
  for (int i = 0; i < 128; i++)
  {
    transfer(dataBuffer[i]);
    CHK ^= dataBuffer[i];
  }
  transfer(CHK);
  transfer(0x00);
  transfer(0x00);
  response = SPI.transfer(0x00);//At least for the Pocket Station, the last byte never gives a ACK, so the tranfer method is not used here to agilise.
  digitalWrite(CS, HIGH);
  delayMicroseconds(500);
  if(response!='G')CHK=~CHK;//problem at writing sector, forces the PC to resend the sector by chaging the CHK
  Serial.write(CHK);
}

No código Python:

f = open(name, 'rb')
for i in range(1024):
    ser.write(bytes("W","ASCII" ))
    time.sleep(0.001)
    ser.write(struct.pack(">B",i>>8))
    CHK=i>>8
    time.sleep(0.001)
    ser.write(struct.pack(">B",i&0xFF))
    CHK^=i&0xFF
    time.sleep(0.001)
    data=f.read(128);
    print(data)
    print("CHK:", CHK)
    for i in range(len(data)):
         CHK =CHK^data[i]
    time.sleep(0.001)
    print("CHK:", CHK)
    response=~CHK
    while response!=CHK:
        ser.write(data)
        while ser.inWaiting()==0:
            time.sleep(0.0001)
        response=ord(ser.read(1))
        print("rsp", response)    
f.close()

O código completo se encontra no meu github.

Abaixo encontra-se uma rom de Megaman2 pra pocketstation que foi gravado com o Arduino. Sucesso!

img_20160918_111346913

Alternativa

Ficar conectando um monte de cabos toda vez que for conectar o memory card/pocketstation não é legal.  Por isso soldei a placa abaixo, usando um dos atmega8 reserva que tenho aqui. Dessa forma eu só conecto o dispositivo a um conversor ftdi e posso trocar os saves facilmente.

img_20160918_111153367

Sempre que o pocketstation está sendo gravado, ele mostra essa mensagem e acende o led no topo.

img_20160918_111208485Um Atmega8 foi usado. Nenhuma modificação no código foi necessária.

Então é isso. Agora você já deve saber como transferir seus saves pro computador ou gravar saves da internet no seu memory card. :)

Até um próximo post 0/

 WTFPL

O conteúdo nesse post está sobre a licença pública Do What The Fuck You Want To.

 

 

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.

5 thoughts to “Backup e gravação de Memory Card PS1 (Arduino & Python)”

Deixe um comentário

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