Conectamos el gamepad de PS1 / PS2 a Raspberry pi

En el patio están las vacaciones de mayo. Y eso significa tiempo para comer kebabs y beber varias bebidas de barril y embotelladas. Y hago todo tipo de tonterías. La idea de este proyecto surgió antes del año nuevo. Pero durante mucho tiempo no pude darme cuenta. Después de comprar el primer gamepad (que tiré más tarde) y de intentar interrogarlo, me di cuenta de que no basta con leer los manuales, aunque hay suficiente información. Afortunadamente, para este 23 de febrero, recibí como regalo no espuma de afeitar empacada en calcetines, sino 600 rublos. con el deseo de no negarte nada en aliexpress. Donde se compró la copia china del analizador lógico Saleae Logic. Aquellos que estén interesados ​​en lo que surgió pueden hacer clic en el botón a continuación. Y aquellos que son demasiado flojos para leer pueden ver inmediatamente el resultado aquí .



Después de atornillar el analizador lógico a un diseño sin complicaciones, vi que se envió una escoria explícita al gamepad.

Un momento de teoría.

El hecho es que la consola se comunica con el gamepad a través de la interfaz SPI. Y hay modos regulados de operación de esta interfaz. En este caso, debe haber MODO 3, transmisión hacia adelante y el nivel lógico en la línea Habilitar chip o Seleccionar esclavo al acceder al gamepad "0". La frecuencia en la línea clk es de 250 kHz. Pero hice 100 kHz, y funciona bien. Todo, en principio, se describe claramente aquí . Y el sistema de comandos para el gamepad, y la transcripción de respuestas aquí . También en el portal Radiokot, hay una publicación, pero hay errores en los equipos. En pocas palabras, para interrogar a un gamepad estándar, debe darle un comando:

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

Donde 0x41 es el tipo de gamepad, y los últimos 2 bytes es el estado de los botones. Con analógico, todo es similar, solo necesita agregar 4 bytes cero más al paquete.

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

Aquí 0x73 esto significa que el gamepad es analógico, los últimos 4 bytes son el estado de los análogos.

Es importante que el gamepad funcione desde 3.3 V., lo que le permite alimentarse directamente desde Raspberry pi. En modo estándar (solo funcionan los botones), el consumo es de aproximadamente 2 mA, en modo analógico 12 mA. Y luego estos 10 mA agregan un LED en el controlador. Y en la línea Raspberry pi de 3.3 V, se puede plantar una carga de hasta 50 mA.

En realidad, para conectar un gamepad, necesitas 4 puertos GPIO y energía. Solo 6 cables.

Entonces aquí. Me di cuenta de que las bibliotecas para trabajar con GPIO, que bcm2835, ese cableadoPi, funcionan muy mal con SPI. En principio, no saben cómo transmitir paquetes con el bit hacia adelante menos significativo. En los muelles de uno de ellos, esto se describe honestamente, pero en algún lugar muy profundo. Y realmente no pueden cumplir con los regímenes.

Pero nada impide reproducir el paquete en sí y leer los datos del gamepad. Apenas dicho que hecho. Tome la biblioteca de cableado y escriba:

 #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); } } 

Todo es clásico, declaramos puertos GPIO, variables y una variedad de comandos. A continuación, se configuran los modos de funcionamiento del puerto y se restablecen las líneas clk, mosi y ce (habilitación de chip). Luego se forman 2 ciclos. En el primero, los bytes del comando se ordenan, y en el segundo, los bytes mismos son en forma de bits, para proporcionar una unidad lógica o cero a la línea mosi, dependiendo del valor del bit. Y al mismo tiempo leemos la respuesta. Cuando vi la atesorada respuesta:

 0xff 0x41 0x5a 0xff 0xff 

Casi salté al techo. Aquí hay un ejemplo de la salida del programa, en este caso, estaba jugando con el análogo izquierdo.

 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 

Bueno, entonces ya estaba listo para integrar este código en el emulador. En este caso, la pregunta con la elección del emulador no fue. Definitivamente pcsx_rearmed . Al igual que la vez anterior, fue necesario encontrar una función que se lanzó una de las primeras para inicializar la biblioteca en ella. PsxInit resultó ser una función de este tipo; se encuentra en el archivo r3000a.c, que emula el funcionamiento del procesador central. Agregué el archivo de encabezado de la biblioteca a este archivo y declaró los puertos GPIO. En la función psxInit, inicializo la biblioteca y establezco los niveles iniciales en los puertos GPIO. A continuación se muestran las primeras líneas de este archivo con los cambios.

 #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(); } 

Luego, tuve que descubrir cómo el emulador simula el funcionamiento de un gamepad. Hay un archivo de encabezado psemu_plugin_defs.h, se encuentra en el directorio de inclusión, aquí contiene la estructura en la que se almacenan las variables en las que el tipo de gamepad es estándar, analógico, variable de estado de botón, variables de estado analógico y variables de control de vibración.

 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; 

Entonces, la tarea principal es escribir los datos recibidos en estas variables. Este emulador tiene complementos. Incluyendo el complemento de gamepad, en el directorio de complementos hay un subdirectorio dfinput, en el que se encuentra el archivo pad.c deseado, en el que se lee el estado del gamepad, así como su configuración. Aquí es donde se transfirió el código del programa de prueba. Los puertos y el archivo de encabezado de la biblioteca también se declaran. También tiene variables que almacenan el código de los comandos enviados al gamepad. Estas variables se utilizan en las funciones de acceso al gamepad. A continuación se muestra este 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 interesados ​​en el primero, que tiene un valor de 42. Aquí, cuando se llama a esta variable, se ejecutará el código. Además, en el archivo original está vacío. Y en la función de enumeración del comando do_cmd, inserté el 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; } } 

Luego, cuando el comando actual está leyendo, el gamepad se sondea y los datos se escriben en variables, y luego el emulador los toma.

Eso es todo, solo queda compilar el proyecto. Para que el compilador recoja la biblioteca wirigPi, debe insertar un enlace en el Makefile del proyecto. Esto es suficiente para hacer desde el principio.

 # 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 

Y es importante. Para usar un gamepad analógico, el emulador debe especificar en la configuración que se usa.

No tengo muchos juegos para PS1, revisé en algún lugar alrededor de 7. No funcionó en Tomb Raider2 y Nuclear strike, pero esto es evidente porque estos juegos no saben qué es un gamepad analógico. Probablemente necesite seleccionar el estándar en la configuración e intentarlo.

PD: el emulador en sí es mejor para ensamblar con banderas de optimización. Está bien descrito aquí .

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


All Articles