Подключаем геймпад от PS1-PS2 к Raspberry pi

в 13:03, , рубрики: diy или сделай сам, gpio, Raspberry Pi, старое железо

На дворе майские праздники. А значит время есть шашлыки и употреблять различные разливные и бутилированные напитки. А я занимаюсь всякой ерундой. Мысль о данном проекте пришла, ещё до нового года. Но я долго не мог его реализовать. Прикупив первый геймпад (который я выкинул в последствии), и попробовав опросить его, я понял, что тут уже не обойтись простым чтением мануалов, хотя инфы достаточно. Благо, на это 23 февраля я получил в подарок не пену для бритья, упакованную в носки, а 600 р. с пожеланием не отказывать себе ни в чём на aliexpress. Где и была куплена китайская копия логического анализатора Saleae Logic. Те, кому интересно, что из этого получилось, могут ткнуть мышкой по кнопке ниже. А те, кому лень читать могут сразу посмотреть результат вот тут

Подключаем геймпад от PS1-PS2 к Raspberry pi - 1


Прикрутив логический анализатор к незамысловатой конструкции, я увидел, что геймпаду отправляется откровенный шлак.

Минутка теории.
Дело в том, что консоль с геймпадом общаются по интерфейсу SPI. И есть регламентированные режимы работы этого интерфейса. В данном случае должен быть MODE 3, передача младшим битом вперёд, и логический уровень на линии Chip enable или Select slave при обращении к геймпаду «0». Частота на линии clk 250 kHz. Но я сделал 100 kHz, и нормально работает. Всё в принципе наглядно описано вот здесь. А система команд геймпаду, и расшифровка ответов вот здесь. Так же на портале «Радиокот», есть публикация, но там есть ошибки в командах. Проще говоря, чтобы опросить стандартный геймпад, нужно дать ему команду:

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 В., что позволяет его питать напрямую от Raspberry pi. В стандартном режиме (работают только кнопки), потребление порядка 2 мА, в аналоговом режиме 12 мА. И то эти 10 мА добавляет светодиод на геймпаде. А на линию 3.3 В Raspberry pi можно сажать нагрузку до 50 мА.
Собственно, для подключения геймпада, понадобится любые 4 порта GPIO и питание. Всего 6 проводов.

Так вот. Я понял, что библиотеки для работы с GPIO, что bcm2835, что wiringPi, очень плохо работают с SPI. Они в принципе не умеют передавать посылки младшим битом вперёд. В доках к одной из них это честно описано, но где-то очень глубоко. Да и режимы они толком соблюдать не умеют.
Но ничего не мешает воспроизвести посылки самому, и читать данные от геймпада. Сказано, сделано. Берём библиотеку wiringPi и пишем:

#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 %xn", data[3], data[4], data[5], data[6], data[7], data[8]);
		delayMicroseconds(100);
	}
}

Всё классически, объявляем порты GPIO, переменные и массив команды. Далее настраиваются режимы работы портов, и приводятся в исходное состояния линии clk, mosi и ce (chip enable). Потом формируются 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;

Так вот основная задача записать полученные данные в эти переменные. У этого эмулятора имеются плагины. В том числе и плагин геймпадов, в каталоге 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. Не заработало на Tomb Raider2 и Nuclear strike, но это видимо потому, что эти игры не знают, что такое аналоговый геймпад. Наверно надо выбрать в настройках стандартный и попробовать.

P.S. Сам эмулятор лучше собирать с флагами оптимизации. Это хорошо описано вот здесь

Автор: lisovsky1

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js