我们将游戏手柄从PS1 / PS2连接到Raspberry Pi

在院子里是五月的假期。 这就意味着有时间吃羊肉串,喝各种生水和瓶装饮料。 而且我胡说八道。 关于这个项目的想法是在新的一年之前出现的。 但是很长一段时间我都没有意识到。 购买了第一个游戏手柄(后来我扔掉了),并试图对其进行审问,我意识到,尽管有足够的信息,但仅仅阅读手册还是不够的。 幸运的是,今年2月23日,我收到了礼物,不是剃掉袜子里的泡沫,而是600卢布。 希望不要在速卖通上否认自己的任何事情。 购买了Saleae Logic逻辑分析仪的中文副本的位置。 感兴趣的人可以单击下面的按钮。 那些懒惰的人可以在这里立即看到结果。



将逻辑分析仪拧紧到一个简单的设计之后,我看到明显的炉渣被发送到了游戏手柄。

理论的时刻。

事实是控制台通过SPI接口与游戏手柄通信。 并且该接口有规范的操作模式。 在这种情况下,访问游戏手柄“ 0”时,应该有“模式3”,正向传输和“芯片使能”或“选择从机”线路上的逻辑电平。 clk线上的频率为250 kHz。 但是我做了100 kHz,它工作正常。 原则上,所有内容均在此处明确描述。 以及游戏手柄的命令系统,以及答案的笔录。 同样在Radiokot门户上,有出版物,但团队中有错误。 简而言之,要询问标准游戏手柄,您需要给它一个命令:

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

其中0x41是游戏手柄的类型,最后2个字节是按钮的状态。 对于模拟量,一切都相似,只需要向软件包中再添加4个零字节即可。

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

这里的0x73表示游戏手柄是模拟的,最后4个字节是模拟的状态。

重要的是,游戏手柄必须在3.3 V.的电压下工作,这样才能直接从Raspberry pi为其供电。 在标准模式下(仅按钮起作用),功耗约为2 mA,在模拟模式下为12 mA。 然后,这10 mA会在控制器上增加一个LED。 在3.3 V Raspberry pi线上,可以植入高达50 mA的负载。

实际上,要连接游戏手柄,您需要任何4个GPIO端口和电源。 仅6线。

所以在这里。 我意识到,用于GPIO的库,bcm2835,wireingPi与SPI的配合非常差。 原则上,他们不知道如何以最低有效位发送包。 在其中一个码头上,这是诚实的描述,但在某个地方却很深。 他们确实不能遵守政权。

但是没有什么可以阻止复制包本身以及从游戏手柄读取数据。 言归正传。 采取wireingPi库并编写:

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

一切都很经典,我们声明了GPIO端口,变量和命令数组。 接下来,配置端口操作模式,并重置clk,mosi和ce(芯片使能)线。 然后形成2个循环。 在第一个中,命令的字节进行排序,在第二个中,字节本身按位排序,以便根据位的值向mosi行提供逻辑单元或零。 同时,我们阅读了答案。 当我看到宝贵的答案时:

 0xff 0x41 0x5a 0xff 0xff 

我差点跳到天花板上。 这是程序输出的示例,在这种情况下,我正在使用左模拟。

 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 

好了,接下来就可以将此代码集成到仿真器中了。 在这种情况下,选择仿真器的问题就没有了。 绝对是pcsx_rearmed 。 就像上一次一样,有必要找到一个首先启动的函数,以便在其中初始化库。 事实证明PsxInit就是这样的功能;它位于r3000a.c文件中,该文件模拟中央处理器的操作。 我将库头文件添加到此文件,并声明了GPIO端口。 在psxInit函数中,我初始化库并在GPIO端口上设置初始级别。 以下是此文件中包含更改的第一行。

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

接下来,我必须弄清楚模拟器如何模拟游戏手柄的操作。 有一个头文件psemu_plugin_defs.h,位于include目录中,此处包含用于存储变量的结构,其中,手柄的类型为标准,模拟,按钮状态变量,模拟状态变量和振动控制变量。

 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; 

因此,主要任务是将接收到的数据写入这些变量。 该模拟器具有插件。 包括gamepad插件在内,plugins目录中有一个dfinput子目录,在该子目录中可以找到所需的pad.c文件,可以在其中读取游戏板的状态及其设置。 这是从测试程序中传输代码的地方。 端口和库头文件也被声明。 它还具有存储用于发送到游戏手柄的命令代码的变量。 这些变量用于访问游戏手柄的功能。 下面是这段代码:

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

我们对第一个值42感兴趣。在这里,当调用此变量时,将执行代码。 此外,在原始文件中它为空。 在do_cmd命令枚举函数中,我插入了主要代码:

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

然后,在读取当前命令时,将对游戏手柄进行轮询,并将数据写入变量,然后仿真器本身将其获取。

仅此而已,仅编译项目即可。 为了使编译器能够使用wirigPi库,您需要在项目的Makefile中插入指向它的链接。 从一开始就足以做到这一点。

 # 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 

这很重要。 为了使用模拟游戏手柄,仿真器必须在设置中指定使用它。

我没有很多PS1游戏,我检查了大约7个地方。它在《古墓丽影2》和《核打击》上不起作用,但这很明显,因为这些游戏不知道什么是模拟游戏手柄。 可能需要在设置中选择标准并尝试。

PS仿真器本身最好与优化标志组合在一起。 这里有很好的描述。

Source: https://habr.com/ru/post/zh-CN403753/


All Articles