Nós conectamos o gamepad do PS1 / PS2 ao Raspberry pi

No quintal são os feriados de maio. E isso significa tempo para comer kebabs e beber vários drinks e bebidas engarrafadas. E eu faço todo tipo de besteira. O pensamento sobre esse projeto surgiu antes do ano novo. Mas durante muito tempo não pude perceber. Tendo comprado o primeiro gamepad (que joguei fora mais tarde) e tentando interrogá-lo, percebi que não basta ler apenas os manuais, embora haja informações suficientes. Felizmente, em 23 de fevereiro, recebi de presente não espuma de barbear embalada em meias, mas 600 rublos. com o desejo de não se negar nada no aliexpress. Onde foi adquirida a cópia em chinês do analisador lógico Saleae Logic. Quem estiver interessado no que aconteceu, pode clicar no botão abaixo. E aqueles que estão com preguiça de ler podem ver imediatamente o resultado aqui .



Tendo parafusado o analisador lógico em um design descomplicado, vi que uma escória explícita foi enviada ao gamepad.

Um momento de teoria.

O fato é que o console se comunica com o gamepad através da interface SPI. E existem modos regulamentados de operação dessa interface. Nesse caso, deve haver o MODO 3, a transmissão direta e o nível lógico na linha Chip habilitar ou Selecionar escravo ao acessar o gamepad "0". A frequência na linha clk é de 250 kHz. Mas eu fiz 100 kHz, e funciona bem. Tudo, em princípio, está claramente descrito aqui . E o sistema de comandos para o gamepad, e a transcrição das respostas aqui . Também no portal Radiokot, há uma publicação, mas há erros nas equipes. Simplificando, para interrogar um gamepad padrão, você precisa dar um comando:

0x1 0x42 0x0 0x0 0x0   , -  0xff 0x41 0x5a 0xff 0xff 

Onde 0x41 é o tipo de gamepad e os últimos 2 bytes são o estado dos botões. Com o analógico, tudo é semelhante, apenas você precisa adicionar mais 4 bytes nulos ao pacote.

 0x1, 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0   , -  0xff 0x73 0x5a 0xff 0xff 0x80 0x80 0x80 0x80 

Aqui, 0x73 significa que o gamepad é analógico, os últimos 4 bytes são o status dos análogos.

É importante que o gamepad funcione a partir de 3,3 V., o que permite que ele seja alimentado diretamente do Raspberry pi. No modo padrão (apenas os botões funcionam), o consumo é de cerca de 2 mA, no modo analógico de 12 mA. E esses 10 mA adicionam um LED ao controlador. E na linha Raspberry pi de 3,3 V, pode ser plantada uma carga de até 50 mA.

Na verdade, para conectar um gamepad, você precisa de 4 portas e energia GPIO. Apenas 6 fios.

Então aqui. Percebi que as bibliotecas para trabalhar com o GPIO, que o bcm2835, que o wirePi funciona muito mal com o SPI. Em princípio, eles não sabem como transmitir pacotes com o bit menos significativo adiante. Nas docas de um deles, isso é descrito honestamente, mas em algum lugar muito profundo. E eles realmente não podem respeitar os regimes.

Mas nada impede a reprodução do próprio pacote e a leitura dos dados do gamepad. Mal disse o que fez. Pegue a biblioteca de wirePi e escreva:

 #include <stdio.h> #include <unistd.h> #include <wiringPi.h> #define mosi 12 #define miso 13 #define clk 14 #define ce 5 unsigned char i, b; unsigned char cmd[9] = {0x1, 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}; int main() { wiringPiSetup () ; //  pinMode (mosi, OUTPUT); pinMode (clk, OUTPUT); pinMode (ce, OUTPUT); pinMode (miso, INPUT); digitalWrite (clk, HIGH); digitalWrite (mosi, LOW); digitalWrite (ce, HIGH); while(1) { unsigned char data[9] = {0}; digitalWrite (ce, LOW); delayMicroseconds(20); for (i = 0; i < 9; i++) { for (b = 0; b < 8; b++) { if (((cmd[i] >> b)&1) == 1) { digitalWrite (mosi, HIGH); digitalWrite (clk, LOW); delayMicroseconds(5); digitalWrite (clk, HIGH); data[i] ^= digitalRead(miso) << b; delayMicroseconds(5); digitalWrite (mosi, LOW); } else { digitalWrite (clk, LOW); delayMicroseconds(5); digitalWrite (clk, HIGH); data[i] ^= digitalRead(miso) << b; delayMicroseconds(5); } } delayMicroseconds(40); } digitalWrite (ce, HIGH); printf("%x %x %x %x %x %x\n", data[3], data[4], data[5], data[6], data[7], data[8]); delayMicroseconds(100); } } 

Tudo é clássico, declaramos portas GPIO, variáveis ​​e uma matriz de comandos. Em seguida, os modos operacionais da porta são configurados e as linhas clk, mosi e ce (chip enable) são redefinidas. Então 2 ciclos são formados. No primeiro, os bytes do comando são classificados e, no segundo, os bytes em si são bit a bit, para fornecer uma unidade lógica ou zero à linha mosi, dependendo do valor do bit. E, ao mesmo tempo, lemos a resposta. Quando vi a resposta preciosa:

 0xff 0x41 0x5a 0xff 0xff 

Eu quase pulei no teto. Aqui está um exemplo da saída do programa, neste caso, eu estava jogando com o analógico esquerdo.

 ff ff 80 80 ff 80 ff ff 80 80 ff 80 ff ff 80 80 ff 80 ff ff 80 80 ff 80 ff ff 80 80 ff 80 ff ff 80 80 ff 80 ff ff 80 80 ff 40 ff ff 80 80 ff 4b ff ff 80 80 ff 4b ff ff 80 80 ff 4b ff ff 80 80 ff 4b ff ff 80 80 ff 4b ff ff 80 80 ff 1a ff ff 80 80 ff 1a ff ff 80 80 ff 1a ff ff 80 80 ff 1a ff ff 80 80 ff 1a ff ff 80 80 ff 0 

Bem, então já estava pronto para integrar esse código no emulador. Nesse caso, a questão com a escolha do emulador não era. Definitivamente pcsx_rearmed . Assim como na época anterior, foi necessário encontrar uma função que foi lançada como uma das primeiras para inicializar a biblioteca. O PsxInit acabou sendo uma função; está localizado no arquivo r3000a.c, que emula a operação do processador central. Adicionei o arquivo de cabeçalho da biblioteca a este arquivo e declarei as portas GPIO. Na função psxInit, inicializo a biblioteca e defino os níveis iniciais nas portas GPIO. Abaixo estão as primeiras linhas deste arquivo com as alterações.

 #include "r3000a.h" #include "cdrom.h" #include "mdec.h" #include "gte.h" #include <wiringPi.h> #define mosi 12 #define miso 13 #define clk 14 #define ce 5 R3000Acpu *psxCpu = NULL; psxRegisters psxRegs; int psxInit() { SysPrintf(_("Running PCSX Version %s (%s).\n"), PACKAGE_VERSION, __DATE__); wiringPiSetup () ; //  pinMode (mosi, OUTPUT); pinMode (clk, OUTPUT); pinMode (ce, OUTPUT); pinMode (miso, INPUT); digitalWrite (clk, HIGH); digitalWrite (mosi, LOW); digitalWrite (ce, HIGH); #ifdef PSXREC if (Config.Cpu == CPU_INTERPRETER) { psxCpu = &psxInt; } else psxCpu = &psxRec; #else psxCpu = &psxInt; #endif Log = 0; if (psxMemInit() == -1) return -1; return psxCpu->Init(); } 

Em seguida, tive que descobrir como o emulador simula a operação de um gamepad. Há um arquivo de cabeçalho psemu_plugin_defs.h, localizado no diretório include, aqui contém a estrutura na qual as variáveis ​​são armazenadas, nas quais o tipo de gamepad é padrão, analógico, variável de estado de botão, variáveis ​​de estado analógico e variáveis ​​de controle de vibração.

 typedef struct { // controler type - fill it withe predefined values above unsigned char controllerType; // status of buttons - every controller fills this field unsigned short buttonStatus; // for analog pad fill those next 4 bytes // values are analog in range 0-255 where 127 is center position unsigned char rightJoyX, rightJoyY, leftJoyX, leftJoyY; // for mouse fill those next 2 bytes // values are in range -128 - 127 unsigned char moveX, moveY; unsigned char Vib[2]; unsigned char VibF[2]; unsigned char reserved[87]; } PadDataS; 

Portanto, a principal tarefa é gravar os dados recebidos nessas variáveis. Este emulador possui plugins. Incluindo o plug-in gamepad, no diretório plugins, existe um subdiretório dfinput, no qual o arquivo pad.c desejado está localizado, no qual o status do gamepad é lido e suas configurações. É aqui que o código do programa de teste foi transferido. Portas e o arquivo de cabeçalho da biblioteca também são declarados. Também possui variáveis ​​que armazenam o código para os comandos enviados ao gamepad. Essas variáveis ​​são usadas nas funções de acesso ao gamepad. Abaixo está este pedaço de código:

 #include <stdint.h> #include "../include/psemu_plugin_defs.h" #include "main.h" #include <wiringPi.h> #define mosi 12 #define miso 13 #define clk 14 #define ce 5 unsigned char a, b; unsigned char cmd[9] = {0x1, 0x42, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}; enum { ANALOG_LEFT = 0, ANALOG_RIGHT, ANALOG_TOTAL }; enum { CMD_READ_DATA_AND_VIBRATE = 0x42, CMD_CONFIG_MODE = 0x43, CMD_SET_MODE_AND_LOCK = 0x44, CMD_QUERY_MODEL_AND_MODE = 0x45, CMD_QUERY_ACT = 0x46, // ?? CMD_QUERY_COMB = 0x47, // ?? CMD_QUERY_MODE = 0x4C, // QUERY_MODE ?? CMD_VIBRATION_TOGGLE = 0x4D, }; 

Estamos interessados ​​no primeiro, que tem um valor de 42. Aqui, quando essa variável é chamada, o código será executado. Além disso, no arquivo original, ele está vazio. E na função de enumeração do comando do_cmd, inseri o código principal:

 static uint8_t do_cmd(void) { PadDataS *pad = &padstate[CurPad].pad; int pad_num = CurPad; unsigned char data[9] = {0}; CmdLen = 8; switch (CurCmd) { case CMD_SET_MODE_AND_LOCK: buf = stdmode[pad_num]; return 0xF3; case CMD_QUERY_MODEL_AND_MODE: buf = stdmodel[pad_num]; buf[4] = padstate[pad_num].PadMode; return 0xF3; case CMD_QUERY_ACT: buf = unk46[pad_num]; return 0xF3; case CMD_QUERY_COMB: buf = unk47[pad_num]; return 0xF3; case CMD_QUERY_MODE: buf = unk4c[pad_num]; return 0xF3; case CMD_VIBRATION_TOGGLE: buf = unk4d[pad_num]; return 0xF3; case CMD_CONFIG_MODE: if (padstate[pad_num].ConfigMode) { buf = stdcfg[pad_num]; return 0xF3; } // else FALLTHROUGH case CMD_READ_DATA_AND_VIBRATE: digitalWrite (ce, LOW); delayMicroseconds(20); for (a = 0; a < 9; a++) { for (b = 0; b < 8; b++) { if (((cmd[a] >> b)&1) == 1) { digitalWrite (mosi, HIGH); digitalWrite (clk, LOW); delayMicroseconds(5); digitalWrite (clk, HIGH); data[a] ^= digitalRead(miso) << b; delayMicroseconds(5); digitalWrite (mosi, LOW); } else { digitalWrite (clk, LOW); delayMicroseconds(5); digitalWrite (clk, HIGH); data[a] ^= digitalRead(miso) << b; delayMicroseconds(5); } } delayMicroseconds(40); } digitalWrite (ce, HIGH); pad->buttonStatus = data[4]; pad->buttonStatus = pad->buttonStatus << 8; pad->buttonStatus |= data[3]; pad->rightJoyX = data[5]; pad->rightJoyY = data[6]; pad->leftJoyX = data[7]; pad->leftJoyY = data[8]; default: buf = stdpar[pad_num]; buf[2] = pad->buttonStatus; buf[3] = pad->buttonStatus >> 8; if (padstate[pad_num].PadMode == 1) { buf[4] = pad->rightJoyX; buf[5] = pad->rightJoyY; buf[6] = pad->leftJoyX; buf[7] = pad->leftJoyY; } else { CmdLen = 4; } return padstate[pad_num].PadID; } } 

Então, quando o comando atual está sendo lido, o gamepad é pesquisado e os dados são gravados em variáveis, e então o próprio emulador as leva.

Isso é tudo, resta apenas compilar o projeto. Para que o compilador selecione a biblioteca wirigPi, é necessário inserir um link no Makefile do projeto. Isso é suficiente para fazer desde o início.

 # Makefile for PCSX ReARMed # default stuff goes here, so that config can override TARGET ?= pcsx CFLAGS += -Wall -ggdb -Ifrontend -ffast-math -I/usr/include -I/usr/include/SDL LDLIBS += -lpthread -lSDL -lpng -lwiringPi ifndef DEBUG CFLAGS += -DNDEBUG -g endif CXXFLAGS += $(CFLAGS) #DRC_DBG = 1 #PCNT = 1 

E isso é importante. Para usar um gamepad analógico, o emulador deve especificar nas configurações que ele é usado.

Não tenho muitos jogos para PS1, verifiquei em algum lugar cerca de 7. Não funcionou no Tomb Raider2 e no Nuclear strike, mas isso é evidente porque esses jogos não sabem o que é um gamepad analógico. Provavelmente você precisa selecionar o padrão nas configurações e tentar.

PS O próprio emulador é melhor montar com sinalizadores de otimização. Está bem descrito aqui .

Source: https://habr.com/ru/post/pt403753/


All Articles